Merge branch 'refs-sidebar' into dev
This commit is contained in:
commit
aba91f719c
@ -26,7 +26,7 @@
|
||||
"less": "^4.1.3",
|
||||
"marked": "^13.0.2",
|
||||
"marked-highlight": "^2.1.4",
|
||||
"md-editor-v3": "^4.16.7",
|
||||
"md-editor-v3": "^4.21.3",
|
||||
"pinia": "^2.0.32",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.1.6"
|
||||
|
||||
@ -81,13 +81,15 @@
|
||||
</div>
|
||||
<div class="chat-box" :class="{ 'wide-screen': meta.wideScreen, 'font-smaller': meta.fontSize === 'smaller', 'font-larger': meta.fontSize === 'larger' }">
|
||||
<MessageComponent
|
||||
v-for="message in conv.messages"
|
||||
v-for="(message, index) in conv.messages"
|
||||
:message="message"
|
||||
:key="message.id"
|
||||
:is-processing="isStreaming"
|
||||
:show-refs="true"
|
||||
:show-refs="['copy', 'regenerate', 'subGraph', 'webSearch', 'knowledgeBase']"
|
||||
:is-latest-message="isLatestMessage(index)"
|
||||
@retry="retryMessage(message.id)"
|
||||
@retryStoppedMessage="retryStoppedMessage(message.id)"
|
||||
@openRefs="handleOpenRefs"
|
||||
>
|
||||
</MessageComponent>
|
||||
</div>
|
||||
@ -142,6 +144,13 @@
|
||||
<p class="note">请注意辨别内容的可靠性 By {{ configStore.config?.model_provider }}: {{ configStore.config?.model_name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 添加全局Refs侧边栏 -->
|
||||
<RefsSidebar
|
||||
ref="refsSidebarRef"
|
||||
:visible="refsSidebarVisible"
|
||||
:latestRefs="currentRefs"
|
||||
@update:visible="refsSidebarVisible = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -178,6 +187,7 @@ import { useConfigStore } from '@/stores/config'
|
||||
import { message } from 'ant-design-vue'
|
||||
import MessageInputComponent from '@/components/MessageInputComponent.vue'
|
||||
import MessageComponent from '@/components/MessageComponent.vue'
|
||||
import RefsSidebar from '@/components/RefsSidebar.vue'
|
||||
|
||||
const props = defineProps({
|
||||
conv: Object,
|
||||
@ -222,6 +232,41 @@ const meta = reactive(JSON.parse(localStorage.getItem('meta')) || {
|
||||
wideScreen: false,
|
||||
})
|
||||
|
||||
// 添加全局refs状态
|
||||
const refsSidebarVisible = ref(false)
|
||||
const currentRefs = ref({})
|
||||
|
||||
// 处理打开refs侧边栏
|
||||
const handleOpenRefs = ({ type, refs }) => {
|
||||
console.log('ChatComponent handleOpenRefs called with type:', type);
|
||||
console.log('Refs data structure:', JSON.stringify(refs));
|
||||
|
||||
// 先更新引用数据,确保数据在设置标签页之前已更新
|
||||
currentRefs.value = Object.assign({}, refs);
|
||||
|
||||
// 强制在下一个tick更新,确保数据已经被正确应用
|
||||
nextTick(() => {
|
||||
// 显示抽屉
|
||||
refsSidebarVisible.value = true;
|
||||
|
||||
// 再次检查引用是否正确
|
||||
console.log('Updated refs data:', JSON.stringify(currentRefs.value));
|
||||
|
||||
// 根据type自动选择标签页
|
||||
if (refsSidebarRef.value) {
|
||||
console.log('Setting active tab to:', type);
|
||||
// 延迟50毫秒设置标签页,确保抽屉已打开
|
||||
setTimeout(() => {
|
||||
refsSidebarRef.value.setActiveTab(type);
|
||||
}, 50);
|
||||
} else {
|
||||
console.error('refsSidebarRef is not available');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加对RefsSidebar的ref
|
||||
const refsSidebarRef = ref(null)
|
||||
|
||||
const consoleMsg = (msg) => console.log(msg)
|
||||
onClickOutside(panel, () => setTimeout(() => opts.showPanel = false, 30))
|
||||
@ -393,6 +438,11 @@ const updateMessage = (info) => {
|
||||
propertiesToUpdate.forEach(prop => {
|
||||
if (info[prop] != null && (typeof info[prop] !== 'string' || info[prop] !== '')) {
|
||||
msg[prop] = info[prop];
|
||||
|
||||
// 如果更新了refs,同时更新全局refs
|
||||
if (prop === 'refs' && info.refs) {
|
||||
currentRefs.value = info.refs;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -481,6 +531,12 @@ const fetchChatResponse = (user_input, cur_res_id) => {
|
||||
console.log(msg)
|
||||
groupRefs(cur_res_id);
|
||||
updateMessage({showThinking: "no", id: cur_res_id});
|
||||
// 更新全局refs为最新消息的refs
|
||||
if (msg && msg.refs) {
|
||||
// 深拷贝refs以确保不会出现引用问题
|
||||
currentRefs.value = JSON.parse(JSON.stringify(msg.refs));
|
||||
console.log('Updated currentRefs on response completion:', currentRefs.value);
|
||||
}
|
||||
isStreaming.value = false;
|
||||
if (conv.value.messages.length === 2) { renameTitle(); }
|
||||
return;
|
||||
@ -606,6 +662,11 @@ onMounted(() => {
|
||||
const parsedMeta = JSON.parse(storedMeta);
|
||||
Object.assign(meta, parsedMeta);
|
||||
}
|
||||
|
||||
// 检查refsSidebarRef是否正确挂载
|
||||
nextTick(() => {
|
||||
console.log('Is refsSidebarRef mounted?', !!refsSidebarRef.value);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -677,6 +738,26 @@ const selectModel = (provider, name) => {
|
||||
configStore.setConfigValue('model_name', name)
|
||||
// message.success(`已切换到模型: ${name} | ${provider}`)
|
||||
}
|
||||
|
||||
// 判断是否是最新的助手消息
|
||||
const isLatestMessage = (index) => {
|
||||
// 找到最后一条助手消息的索引
|
||||
const lastAssistantMsgIndex = findLastIndex(conv.value.messages,
|
||||
msg => (msg.role === 'received' || msg.role === 'assistant') && msg.status === 'finished');
|
||||
|
||||
// 如果当前索引等于最后一条助手消息的索引,则为最新消息
|
||||
return index === lastAssistantMsgIndex;
|
||||
}
|
||||
|
||||
// 辅助函数:从后向前查找满足条件的元素索引
|
||||
const findLastIndex = (array, predicate) => {
|
||||
for (let i = array.length - 1; i >= 0; i--) {
|
||||
if (predicate(array[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
<script setup>
|
||||
import { Graph } from "@antv/g6";
|
||||
import { onMounted, watch, ref } from 'vue';
|
||||
import { onMounted, watch, ref, onUnmounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
graphData: {
|
||||
@ -16,21 +16,49 @@ const props = defineProps({
|
||||
|
||||
const container = ref(null);
|
||||
let graphInstance = null;
|
||||
let resizeObserver = null;
|
||||
|
||||
const initGraph = () => {
|
||||
// 确保容器大小已经稳定
|
||||
const width = container.value.offsetWidth;
|
||||
const height = container.value.offsetHeight;
|
||||
|
||||
if (width < 100) {
|
||||
// 如果容器宽度太小,可能是抽屉还在展开中,稍后重试
|
||||
setTimeout(initGraph, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保容器干净,没有残留元素
|
||||
if (container.value) {
|
||||
container.value.innerHTML = '';
|
||||
}
|
||||
|
||||
// 确保没有已存在的实例
|
||||
if (graphInstance) {
|
||||
graphInstance.destroy();
|
||||
graphInstance = null;
|
||||
}
|
||||
|
||||
graphInstance = new Graph({
|
||||
container: container.value,
|
||||
width: container.value.offsetWidth,
|
||||
height: container.value.offsetHeight,
|
||||
width: width,
|
||||
height: height,
|
||||
autoFit: true,
|
||||
autoResize: true,
|
||||
layout: {
|
||||
type: 'd3-force',
|
||||
preventOverlap: true,
|
||||
kr: 20,
|
||||
kr: 30,
|
||||
linkDistance: 200,
|
||||
nodeStrength: -100,
|
||||
collide: {
|
||||
strength: 1.0,
|
||||
strength: 1.5,
|
||||
radius: 60,
|
||||
},
|
||||
alpha: 0.8,
|
||||
alphaDecay: 0.028,
|
||||
center: [width / 2, height / 2],
|
||||
},
|
||||
node: {
|
||||
type: 'circle',
|
||||
@ -56,10 +84,51 @@ const initGraph = () => {
|
||||
};
|
||||
|
||||
const renderGraph = () => {
|
||||
// 如果已有实例且容器大小发生明显变化,则销毁重建
|
||||
if (graphInstance && container.value) {
|
||||
const currentWidth = container.value.offsetWidth;
|
||||
const currentHeight = container.value.offsetHeight;
|
||||
let graphWidth = 0;
|
||||
let graphHeight = 0;
|
||||
|
||||
// 安全地获取图表宽高
|
||||
try {
|
||||
if (typeof graphInstance.getWidth === 'function') {
|
||||
graphWidth = graphInstance.getWidth();
|
||||
} else if (graphInstance.cfg && graphInstance.cfg.width) {
|
||||
graphWidth = graphInstance.cfg.width;
|
||||
}
|
||||
|
||||
if (typeof graphInstance.getHeight === 'function') {
|
||||
graphHeight = graphInstance.getHeight();
|
||||
} else if (graphInstance.cfg && graphInstance.cfg.height) {
|
||||
graphHeight = graphInstance.cfg.height;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error getting graph dimensions:', e);
|
||||
}
|
||||
|
||||
// 如果宽度变化超过50px,重新初始化图表
|
||||
if (Math.abs(currentWidth - graphWidth) > 50 || Math.abs(currentHeight - graphHeight) > 50) {
|
||||
graphInstance.destroy();
|
||||
graphInstance = null;
|
||||
|
||||
// 清理容器内容
|
||||
if (container.value) {
|
||||
container.value.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!graphInstance) {
|
||||
initGraph();
|
||||
}
|
||||
|
||||
if (!graphInstance) {
|
||||
// 初始化可能推迟执行,此时直接返回
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedData = {
|
||||
nodes: props.graphData.nodes.map(node => ({
|
||||
id: node.id,
|
||||
@ -74,14 +143,86 @@ const renderGraph = () => {
|
||||
|
||||
graphInstance.setData(formattedData);
|
||||
graphInstance.render();
|
||||
|
||||
// 确保图表正确居中和适应视图
|
||||
setTimeout(() => {
|
||||
if (graphInstance) {
|
||||
graphInstance.fitCenter();
|
||||
graphInstance.fitView();
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// 添加供父组件调用的刷新方法
|
||||
const refreshGraph = () => {
|
||||
// 销毁现有图表实例
|
||||
if (graphInstance) {
|
||||
graphInstance.destroy();
|
||||
graphInstance = null;
|
||||
}
|
||||
|
||||
// 清理容器内容
|
||||
if (container.value) {
|
||||
container.value.innerHTML = '';
|
||||
}
|
||||
|
||||
// 延迟重新渲染,确保容器大小已经稳定
|
||||
setTimeout(() => {
|
||||
renderGraph();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 向父组件暴露方法
|
||||
defineExpose({
|
||||
refreshGraph
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 使用ResizeObserver监听容器大小变化
|
||||
if (window.ResizeObserver) {
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === container.value) {
|
||||
renderGraph();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (container.value) {
|
||||
resizeObserver.observe(container.value);
|
||||
}
|
||||
}
|
||||
|
||||
renderGraph();
|
||||
window.addEventListener('resize', renderGraph);
|
||||
});
|
||||
|
||||
watch(() => props.graphData, renderGraph, { deep: true });
|
||||
// 添加组件卸载时的清理
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', renderGraph);
|
||||
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
resizeObserver = null;
|
||||
}
|
||||
|
||||
if (graphInstance) {
|
||||
graphInstance.destroy();
|
||||
graphInstance = null;
|
||||
}
|
||||
|
||||
// 确保清理容器内容
|
||||
if (container.value) {
|
||||
container.value.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.graphData, (newData, oldData) => {
|
||||
// 强制重新渲染图表
|
||||
if (newData !== oldData) {
|
||||
refreshGraph();
|
||||
}
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
|
||||
|
||||
<div v-if="(message.role=='received' || message.role=='assistant') && message.status=='finished' && showRefs">
|
||||
<RefsComponent :message="message" :show-refs="showRefs" @retry="emit('retry')" />
|
||||
<RefsComponent :message="message" :show-refs="showRefs" :is-latest-message="isLatestMessage" @retry="emit('retry')" @openRefs="emit('openRefs', $event)" />
|
||||
</div>
|
||||
<!-- 错误消息 -->
|
||||
</div>
|
||||
@ -111,6 +111,11 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否为最新消息
|
||||
isLatestMessage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const editorRef = ref()
|
||||
@ -122,7 +127,7 @@ const statusDefination = {
|
||||
error: '错误'
|
||||
}
|
||||
|
||||
const emit = defineEmits(['retry', 'retryStoppedMessage']);
|
||||
const emit = defineEmits(['retry', 'retryStoppedMessage', 'openRefs']);
|
||||
|
||||
// 推理面板展开状态
|
||||
const reasoningActiveKey = ref(['show']);
|
||||
|
||||
@ -11,101 +11,32 @@
|
||||
v-if="showKey('regenerate')"
|
||||
class="item btn" @click="regenerateMessage()" title="重新生成"><ReloadOutlined /></span>
|
||||
<span
|
||||
v-if="showKey('subGraph') && hasSubGraphData(msg)"
|
||||
v-if="isLatestMessage && showKey('subGraph') && hasSubGraphData(msg)"
|
||||
class="item btn"
|
||||
@click="openSubGraph(msg)"
|
||||
@click="openGlobalRefs('graph')"
|
||||
>
|
||||
<DeploymentUnitOutlined /> 关系图
|
||||
</span>
|
||||
<span
|
||||
class="item btn"
|
||||
@click="showWebResult(msg)"
|
||||
v-if="showKey('webSearch') && msg.refs?.web_search.results.length > 0"
|
||||
v-if="isLatestMessage && showKey('webSearch') && msg.refs?.web_search.results.length > 0"
|
||||
@click="openGlobalRefs('webSearch')"
|
||||
>
|
||||
<GlobalOutlined /> 网页搜索 {{ msg.refs.web_search?.results.length }}
|
||||
</span>
|
||||
<span class="filetag item btn"
|
||||
v-for="(results, filename) in msg.groupedResults"
|
||||
:key="filename"
|
||||
@click="toggleDrawer(filename)"
|
||||
<span
|
||||
class="item btn"
|
||||
v-if="isLatestMessage && showKey('knowledgeBase') && hasKnowledgeBaseData(msg)"
|
||||
@click="openGlobalRefs('knowledgeBase')"
|
||||
>
|
||||
<FileTextOutlined /> {{ filename }}
|
||||
<a-drawer
|
||||
v-model:open="openDetail[filename]"
|
||||
:title="filename"
|
||||
width="700"
|
||||
:contentWrapperStyle="{ maxWidth: '100%'}"
|
||||
placement="right"
|
||||
class="retrieval-detail"
|
||||
rootClassName="root"
|
||||
>
|
||||
<div class="fileinfo">
|
||||
<p><FileOutlined /> {{ results[0].file.type }}</p>
|
||||
<p><ClockCircleOutlined /> {{ formatDate(results[0].file.created_at) }}</p>
|
||||
</div>
|
||||
<div class="results-list">
|
||||
<div v-for="res in results" :key="res.id" class="result-item">
|
||||
<div class="result-meta">
|
||||
<div class="score-info">
|
||||
<span>
|
||||
<strong>相似度:</strong>
|
||||
<a-progress :percent="getPercent(res.distance)"/>
|
||||
</span>
|
||||
<span v-if="res.rerank_score">
|
||||
<strong>重排序:</strong>
|
||||
<a-progress :percent="getPercent(res.rerank_score)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="result-id">ID: #{{ res.id }}</div>
|
||||
</div>
|
||||
<div class="result-text">{{ res.entity.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-drawer>
|
||||
<FileTextOutlined /> 知识库
|
||||
</span>
|
||||
</div>
|
||||
<a-modal
|
||||
v-model:open="subGraphVisible"
|
||||
title="相关实体与关系"
|
||||
:width="800"
|
||||
:footer="null"
|
||||
>
|
||||
<GraphContainer :graphData="subGraphData" />
|
||||
</a-modal>
|
||||
<a-drawer
|
||||
v-model:open="webResultVisible"
|
||||
title="网页搜索结果"
|
||||
width="700"
|
||||
:contentWrapperStyle="{ maxWidth: '100%'}"
|
||||
placement="right"
|
||||
class="web-result-detail"
|
||||
rootClassName="root"
|
||||
>
|
||||
<div class="results-list">
|
||||
<div v-for="result in webResults" :key="result.url" class="result-item">
|
||||
<div class="result-meta">
|
||||
<div class="score-info">
|
||||
<span>
|
||||
<strong>相关度:</strong>
|
||||
<a-progress :percent="getPercent(result.score)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="result-url">
|
||||
<a :href="result.url" target="_blank">{{ result.url }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-content">
|
||||
<h3 class="result-title">{{ result.title }}</h3>
|
||||
<div class="result-text">{{ result.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
@ -114,19 +45,19 @@ import {
|
||||
CopyOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
BulbOutlined,
|
||||
FileOutlined,
|
||||
ClockCircleOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import GraphContainer from './GraphContainer.vue' // 导入 GraphContainer 组件
|
||||
|
||||
|
||||
const emit = defineEmits(['retry']);
|
||||
const emit = defineEmits(['retry', 'openRefs']);
|
||||
const props = defineProps({
|
||||
message: Object,
|
||||
showRefs: {
|
||||
type: [Array, Boolean],
|
||||
default: () => false
|
||||
},
|
||||
isLatestMessage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@ -158,58 +89,14 @@ const copyText = async (text) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 其他方法
|
||||
const likeThisResponse = (msg) => {
|
||||
console.log('Like this response:', msg)
|
||||
}
|
||||
|
||||
const dislikeThisResponse = (msg) => {
|
||||
console.log('Dislike this response:', msg)
|
||||
}
|
||||
|
||||
// 使用 reactive 创建一个响应式对象来存储每个文件的抽屉状态
|
||||
const openDetail = reactive({})
|
||||
|
||||
// 初始化 openDetail 对象
|
||||
for (const filename in msg.value.groupedResults) {
|
||||
openDetail[filename] = false
|
||||
}
|
||||
|
||||
const toggleDrawer = (filename) => {
|
||||
openDetail[filename] = !openDetail[filename]
|
||||
}
|
||||
|
||||
const showRefs = computed(() => (msg.value.role=='received' || msg.value.role=='assistant') && msg.value.status=='finished')
|
||||
|
||||
const subGraphVisible = ref(false)
|
||||
const subGraphData = ref(null)
|
||||
|
||||
|
||||
const openSubGraph = (msg) => {
|
||||
if (hasSubGraphData(msg)) {
|
||||
subGraphData.value = msg.refs.graph_base.results
|
||||
subGraphVisible.value = true
|
||||
} else {
|
||||
console.error('无法获取子图数据')
|
||||
}
|
||||
}
|
||||
|
||||
const closeSubGraph = () => {
|
||||
subGraphVisible.value = false
|
||||
}
|
||||
|
||||
// 添加网页搜索结果相关变量
|
||||
const webResultVisible = ref(false)
|
||||
const webResults = ref(null)
|
||||
|
||||
// 添加显示网页搜索结果的方法
|
||||
const showWebResult = (msg) => {
|
||||
if (msg.refs?.web_search) {
|
||||
webResults.value = msg.refs?.web_search.results
|
||||
webResultVisible.value = true
|
||||
} else {
|
||||
console.error('无法获取网页搜索结果')
|
||||
}
|
||||
// 打开全局refs抽屉
|
||||
const openGlobalRefs = (type) => {
|
||||
emit('openRefs', {
|
||||
type,
|
||||
refs: msg.value.refs
|
||||
})
|
||||
}
|
||||
|
||||
const hasSubGraphData = (msg) => {
|
||||
@ -218,14 +105,10 @@ const hasSubGraphData = (msg) => {
|
||||
msg.refs.graph_base.results.nodes.length > 0;
|
||||
}
|
||||
|
||||
// 添加日期格式化函数
|
||||
const formatDate = (timestamp) => {
|
||||
return new Date(timestamp * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
// 添加百分比计算函数
|
||||
const getPercent = (value) => {
|
||||
return parseFloat((value * 100).toFixed(2))
|
||||
const hasKnowledgeBaseData = (msg) => {
|
||||
return msg.refs &&
|
||||
msg.refs.knowledge_base &&
|
||||
msg.refs.knowledge_base.results.length > 0;
|
||||
}
|
||||
|
||||
// 添加重新生成方法
|
||||
@ -265,160 +148,6 @@ const regenerateMessage = () => {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.filetag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.retrieval-detail {
|
||||
.fileinfo {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.score-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
margin-bottom: 8px;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
strong {
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ant-progress {
|
||||
width: 170px;
|
||||
margin-bottom: 0;
|
||||
margin-inline: 10px;
|
||||
|
||||
.ant-progress-bg {
|
||||
background-color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-id {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background-color: #f9f9f9;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
.results-list {
|
||||
.result-item {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 16px 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.web-result-detail {
|
||||
.results-list {
|
||||
.result-item {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 16px 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.score-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
margin-bottom: 8px;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
strong {
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ant-progress {
|
||||
width: 170px;
|
||||
margin-bottom: 0;
|
||||
margin-inline: 10px;
|
||||
|
||||
.ant-progress-bg {
|
||||
background-color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-url {
|
||||
font-size: 12px;
|
||||
color: #1677FF;
|
||||
margin-bottom: 8px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.result-content {
|
||||
.result-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background-color: #f9f9f9;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
426
web/src/components/RefsSidebar.vue
Normal file
426
web/src/components/RefsSidebar.vue
Normal file
@ -0,0 +1,426 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-drawer
|
||||
:open="visible"
|
||||
title="参考信息"
|
||||
:width="700"
|
||||
:contentWrapperStyle="{ maxWidth: '100%'}"
|
||||
placement="right"
|
||||
class="refs-sidebar"
|
||||
rootClassName="root"
|
||||
@close="$emit('update:visible', false)"
|
||||
@afterOpenChange="handleAfterVisibleChange"
|
||||
>
|
||||
<a-tabs v-model:activeKey="activeTab">
|
||||
<!-- 关系图 -->
|
||||
<a-tab-pane key="graph" tab="关系图" :disabled="!hasGraphData">
|
||||
<div v-if="hasGraphData" class="graph-container">
|
||||
<GraphContainer :graphData="latestRefs.graph_base.results" ref="graphContainerRef" />
|
||||
</div>
|
||||
<div v-else class="empty-data">
|
||||
<p>当前对话没有关系图数据</p>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 网页搜索 -->
|
||||
<a-tab-pane key="webSearch" tab="网页搜索" :disabled="!hasWebSearchData">
|
||||
<div v-if="hasWebSearchData" class="results-list">
|
||||
<div v-for="result in latestRefs.web_search.results" :key="result.url" class="result-item">
|
||||
<div class="result-meta">
|
||||
<div class="score-info">
|
||||
<span>
|
||||
<strong>相关度:</strong>
|
||||
<a-progress :percent="getPercent(result.score)"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-content">
|
||||
<h3 class="result-title">{{ result.title }}</h3>
|
||||
<div class="result-url">
|
||||
<a :href="result.url" target="_blank">{{ result.url }}</a>
|
||||
</div>
|
||||
<div class="result-text">{{ result.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-data">
|
||||
<p>当前对话没有网页搜索数据</p>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 知识库 -->
|
||||
<a-tab-pane key="knowledgeBase" tab="知识库" :disabled="!hasKnowledgeBaseData">
|
||||
<div v-if="hasKnowledgeBaseData">
|
||||
<div class="file-list">
|
||||
<a-collapse v-model:activeKey="activeFiles">
|
||||
<a-collapse-panel
|
||||
v-for="(results, filename) in groupedKnowledgeResults"
|
||||
:key="filename"
|
||||
:header="filename"
|
||||
>
|
||||
<div class="fileinfo" v-if="results.length > 0">
|
||||
<p><FileOutlined /> {{ results[0].file.type }}</p>
|
||||
<p><ClockCircleOutlined /> {{ formatDate(results[0].file.created_at) }}</p>
|
||||
</div>
|
||||
<div class="results-list">
|
||||
<div v-for="res in results" :key="res.id" class="result-item">
|
||||
<div class="result-meta">
|
||||
<div class="score-info">
|
||||
<span>
|
||||
<strong>相似度:</strong>
|
||||
<a-progress :percent="getPercent(res.distance)"/>
|
||||
</span>
|
||||
<span v-if="res.rerank_score">
|
||||
<strong>重排序:</strong>
|
||||
<a-progress :percent="getPercent(res.rerank_score)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="result-id">ID: #{{ res.id }}</div>
|
||||
</div>
|
||||
<div class="result-text">{{ res.entity.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-data">
|
||||
<p>当前对话没有知识库查询数据</p>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, reactive, watch, nextTick } from 'vue'
|
||||
import {
|
||||
FileOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import GraphContainer from './GraphContainer.vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
latestRefs: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
// 标签页相关
|
||||
const activeTab = ref('graph')
|
||||
const activeFiles = ref([])
|
||||
|
||||
// 计算属性:是否有各类数据
|
||||
const hasGraphData = computed(() => {
|
||||
try {
|
||||
return !!(props.latestRefs &&
|
||||
props.latestRefs.graph_base &&
|
||||
props.latestRefs.graph_base.results &&
|
||||
props.latestRefs.graph_base.results.nodes &&
|
||||
Array.isArray(props.latestRefs.graph_base.results.nodes) &&
|
||||
props.latestRefs.graph_base.results.nodes.length > 0);
|
||||
} catch (e) {
|
||||
console.error('Error checking graph data:', e);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
const hasWebSearchData = computed(() => {
|
||||
try {
|
||||
// 更加容忍数据结构的不完整
|
||||
return !!(props.latestRefs &&
|
||||
props.latestRefs.web_search &&
|
||||
Array.isArray(props.latestRefs.web_search.results) &&
|
||||
props.latestRefs.web_search.results.length > 0);
|
||||
} catch (e) {
|
||||
console.error('Error checking web search data:', e);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
const hasKnowledgeBaseData = computed(() => {
|
||||
try {
|
||||
return !!(props.latestRefs &&
|
||||
props.latestRefs.knowledge_base &&
|
||||
props.latestRefs.knowledge_base.results &&
|
||||
Array.isArray(props.latestRefs.knowledge_base.results) &&
|
||||
props.latestRefs.knowledge_base.results.length > 0);
|
||||
} catch (e) {
|
||||
console.error('Error checking knowledge base data:', e);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
// 知识库结果按文件分组
|
||||
const groupedKnowledgeResults = computed(() => {
|
||||
if (!hasKnowledgeBaseData.value) return {}
|
||||
|
||||
return props.latestRefs.knowledge_base.results
|
||||
.filter(result => result.file && result.file.filename)
|
||||
.reduce((acc, result) => {
|
||||
const { filename } = result.file
|
||||
if (!acc[filename]) {
|
||||
acc[filename] = []
|
||||
}
|
||||
acc[filename].push(result)
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
|
||||
// 自动选择有数据的第一个标签页
|
||||
watch(() => props.visible, (newValue) => {
|
||||
console.log('RefsSidebar visible changed to', newValue, 'activeTab is', activeTab.value);
|
||||
|
||||
if (newValue) {
|
||||
console.log('Checking which tabs are available');
|
||||
// 只有在activeTab无效的情况下才自动选择标签页
|
||||
const currentTabValid =
|
||||
(activeTab.value === 'graph' && hasGraphData.value) ||
|
||||
(activeTab.value === 'webSearch' && hasWebSearchData.value) ||
|
||||
(activeTab.value === 'knowledgeBase' && hasKnowledgeBaseData.value);
|
||||
|
||||
if (!currentTabValid) {
|
||||
console.log('Current tab is invalid, finding first available tab');
|
||||
// 当前标签无效,需要寻找一个有效的标签
|
||||
if (hasGraphData.value) {
|
||||
console.log('Selected graph tab');
|
||||
activeTab.value = 'graph';
|
||||
} else if (hasWebSearchData.value) {
|
||||
console.log('Selected webSearch tab');
|
||||
activeTab.value = 'webSearch';
|
||||
} else if (hasKnowledgeBaseData.value) {
|
||||
console.log('Selected knowledgeBase tab');
|
||||
activeTab.value = 'knowledgeBase';
|
||||
// 打开第一个文件
|
||||
if (Object.keys(groupedKnowledgeResults.value).length > 0) {
|
||||
activeFiles.value = [Object.keys(groupedKnowledgeResults.value)[0]];
|
||||
}
|
||||
} else {
|
||||
console.log('No valid tabs available');
|
||||
}
|
||||
} else {
|
||||
console.log('Current tab is valid, keeping it:', activeTab.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 百分比计算函数
|
||||
const getPercent = (value) => {
|
||||
return parseFloat((value * 100).toFixed(2))
|
||||
}
|
||||
|
||||
// 日期格式化函数
|
||||
const formatDate = (timestamp) => {
|
||||
return new Date(timestamp * 1000).toLocaleString()
|
||||
}
|
||||
|
||||
// 手动设置活动标签页
|
||||
const setActiveTab = (tab) => {
|
||||
console.log('RefsSidebar setActiveTab called with tab:', tab);
|
||||
console.log('Current tabs available:', {
|
||||
graph: hasGraphData.value,
|
||||
webSearch: hasWebSearchData.value,
|
||||
knowledgeBase: hasKnowledgeBaseData.value
|
||||
});
|
||||
console.log('Full latestRefs structure:', JSON.stringify(props.latestRefs));
|
||||
|
||||
// 如果要设置的标签是有效的,直接设置
|
||||
if ((tab === 'graph' && hasGraphData.value) ||
|
||||
(tab === 'webSearch' && hasWebSearchData.value) ||
|
||||
(tab === 'knowledgeBase' && hasKnowledgeBaseData.value)) {
|
||||
console.log('Setting activeTab to:', tab);
|
||||
activeTab.value = tab;
|
||||
} else {
|
||||
console.warn(`Cannot set tab to ${tab}, tab is disabled or invalid`);
|
||||
|
||||
// 如果特定标签数据不可用,但数据存在,尝试强制启用相应标签
|
||||
if (tab === 'webSearch' && props.latestRefs.web_search) {
|
||||
console.log('Forcing webSearch tab even though hasWebSearchData is false');
|
||||
activeTab.value = 'webSearch';
|
||||
} else if (tab === 'knowledgeBase' && props.latestRefs.knowledge_base) {
|
||||
console.log('Forcing knowledgeBase tab even though hasKnowledgeBaseData is false');
|
||||
activeTab.value = 'knowledgeBase';
|
||||
} else if (tab === 'graph' && props.latestRefs.graph_base) {
|
||||
console.log('Forcing graph tab even though hasGraphData is false');
|
||||
activeTab.value = 'graph';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图表ref
|
||||
const graphContainerRef = ref(null);
|
||||
|
||||
// 处理抽屉完全打开后的事件
|
||||
const handleAfterVisibleChange = (visible) => {
|
||||
console.log('RefsSidebar afterOpenChange fired, visible:', visible, 'activeTab:', activeTab.value);
|
||||
|
||||
if (!visible) return;
|
||||
|
||||
// 根据当前活动标签页执行相应的刷新操作
|
||||
nextTick(() => {
|
||||
if (activeTab.value === 'graph' && hasGraphData.value) {
|
||||
console.log('Refreshing graph after drawer opened');
|
||||
// 触发图表容器的重新布局
|
||||
if (graphContainerRef.value && graphContainerRef.value.refreshGraph) {
|
||||
graphContainerRef.value.refreshGraph();
|
||||
}
|
||||
} else {
|
||||
console.log('Current active tab is', activeTab.value, 'no need to refresh graph');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 监听标签页变化
|
||||
watch(activeTab, (newTab) => {
|
||||
if (newTab === 'graph' && hasGraphData.value && props.visible) {
|
||||
// 切换到图表标签时,需要延迟一下重新渲染图表
|
||||
nextTick(() => {
|
||||
if (graphContainerRef.value && graphContainerRef.value.refreshGraph) {
|
||||
graphContainerRef.value.refreshGraph();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 监视latestRefs的变化,便于调试
|
||||
watch(() => props.latestRefs, (newRefs) => {
|
||||
console.log('RefsSidebar latestRefs changed:', {
|
||||
hasGraph: hasGraphData.value,
|
||||
hasWeb: hasWebSearchData.value,
|
||||
hasKB: hasKnowledgeBaseData.value,
|
||||
graph: newRefs.graph_base?.results?.nodes?.length,
|
||||
web: newRefs.web_search?.results?.length,
|
||||
kb: newRefs.knowledge_base?.results?.length
|
||||
});
|
||||
}, { deep: true });
|
||||
|
||||
// 向父组件暴露方法
|
||||
defineExpose({
|
||||
setActiveTab
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.refs-sidebar {
|
||||
.empty-data {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
min-height: 500px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fileinfo {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.results-list {
|
||||
.result-item {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 16px 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.score-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
margin-bottom: 8px;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
strong {
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ant-progress {
|
||||
width: 170px;
|
||||
margin-bottom: 0;
|
||||
margin-inline: 10px;
|
||||
|
||||
.ant-progress-bg {
|
||||
background-color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-id {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-content {
|
||||
.result-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
.result-url {
|
||||
font-size: 12px;
|
||||
color: #0563e7;
|
||||
margin-bottom: 8px;
|
||||
a {
|
||||
color: #0563e7;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.result-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background-color: #f9f9f9;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user