refactor: 增强消息来源处理,合并相关组件并优化状态管理
This commit is contained in:
parent
9cbed254bb
commit
3ac6fee7b8
@ -84,11 +84,8 @@
|
||||
<RefsComponent
|
||||
v-if="shouldShowRefs(conv)"
|
||||
:message="getLastMessage(conv)"
|
||||
:show-refs="['model', 'copy']"
|
||||
:show-refs="['model', 'copy', 'sources']"
|
||||
:is-latest-message="false"
|
||||
/>
|
||||
<ConversationSourcesPanel
|
||||
v-if="shouldShowRefs(conv)"
|
||||
:sources="getConversationSources(conv)"
|
||||
/>
|
||||
</div>
|
||||
@ -219,7 +216,6 @@ import HumanApprovalModal from '@/components/HumanApprovalModal.vue'
|
||||
import { useApproval } from '@/composables/useApproval'
|
||||
import { useAgentStreamHandler } from '@/composables/useAgentStreamHandler'
|
||||
import AgentPanel from '@/components/AgentPanel.vue'
|
||||
import ConversationSourcesPanel from '@/components/ConversationSourcesPanel.vue'
|
||||
|
||||
// ==================== PROPS & EMITS ====================
|
||||
const props = defineProps({
|
||||
@ -237,7 +233,9 @@ const {
|
||||
defaultAgentId,
|
||||
selectedAgentConfigId,
|
||||
agentConfig,
|
||||
configurableItems
|
||||
configurableItems,
|
||||
availableKnowledgeBases,
|
||||
availableMcps
|
||||
} = storeToRefs(agentStore)
|
||||
|
||||
// ==================== LOCAL CHAT & UI STATE ====================
|
||||
@ -294,10 +292,6 @@ const localUIState = reactive({
|
||||
isInitialRender: true
|
||||
})
|
||||
|
||||
// Mention resources
|
||||
const availableKnowledgeBases = ref([])
|
||||
const availableMcps = ref([])
|
||||
|
||||
// Agent Panel State
|
||||
const isAgentPanelOpen = ref(false)
|
||||
const isResizing = ref(false)
|
||||
@ -1135,19 +1129,6 @@ const resumeActiveRunForThread = async (threadId) => {
|
||||
clearActiveRunSnapshot(threadId)
|
||||
}
|
||||
|
||||
const fetchMentionResources = async () => {
|
||||
try {
|
||||
const [dbsRes, mcpsRes] = await Promise.all([
|
||||
databaseApi.getAccessibleDatabases().catch(() => ({ databases: [] })),
|
||||
mcpApi.getMcpServers().catch(() => ({ data: [] }))
|
||||
])
|
||||
availableKnowledgeBases.value = dbsRes.databases || []
|
||||
availableMcps.value = mcpsRes.data || []
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch mention resources', e)
|
||||
}
|
||||
}
|
||||
|
||||
const ensureActiveThread = async (title = '新的对话') => {
|
||||
if (currentChatId.value) return currentChatId.value
|
||||
try {
|
||||
@ -1648,7 +1629,7 @@ const showMsgRefs = (msg) => {
|
||||
|
||||
// 只有真正完成的消息才显示 refs
|
||||
if (msg.isLast && msg.status === 'finished') {
|
||||
return ['copy']
|
||||
return ['copy', 'sources']
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -1693,7 +1674,6 @@ const initAll = async () => {
|
||||
if (!agentStore.isInitialized) {
|
||||
await agentStore.initialize()
|
||||
}
|
||||
await fetchMentionResources()
|
||||
} catch (error) {
|
||||
handleChatError(error, 'load')
|
||||
}
|
||||
|
||||
@ -91,6 +91,7 @@
|
||||
:message="message"
|
||||
:show-refs="showRefs"
|
||||
:is-latest-message="isLatestMessage"
|
||||
:sources="messageSources"
|
||||
@retry="emit('retry')"
|
||||
@openRefs="emit('openRefs', $event)"
|
||||
/>
|
||||
@ -115,6 +116,7 @@ import { useAgentStore } from '@/stores/agent'
|
||||
import { useInfoStore } from '@/stores/info'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { MessageProcessor } from '@/utils/messageProcessor'
|
||||
|
||||
import { MdPreview } from 'md-editor-v3'
|
||||
import 'md-editor-v3/lib/preview.css'
|
||||
@ -223,9 +225,18 @@ const getErrorMessage = computed(() => {
|
||||
|
||||
// 引入智能体 store
|
||||
const agentStore = useAgentStore()
|
||||
const { availableKnowledgeBases } = storeToRefs(agentStore)
|
||||
const infoStore = useInfoStore()
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
// 提取消息来源
|
||||
const messageSources = computed(() => {
|
||||
if (props.message.type === 'ai') {
|
||||
return MessageProcessor.extractSourcesFromMessage(props.message, availableKnowledgeBases.value)
|
||||
}
|
||||
return { knowledgeChunks: [], webSources: [] }
|
||||
})
|
||||
|
||||
// 主题设置 - 根据系统主题动态切换
|
||||
const theme = computed(() => (themeStore.isDark ? 'dark' : 'light'))
|
||||
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
<template>
|
||||
<div v-if="hasAny" class="conversation-sources-panel">
|
||||
<button class="panel-header" type="button" @click="toggleExpanded">
|
||||
<div class="header-left">
|
||||
<div class="title">来源</div>
|
||||
<div class="summary">
|
||||
<span v-if="knowledgeChunks.length > 0">知识库 {{ knowledgeChunks.length }}</span>
|
||||
<span v-if="webSources.length > 0">网络 {{ webSources.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown :size="14" class="expand-icon" :class="{ rotated: !expanded }" />
|
||||
</button>
|
||||
|
||||
<div v-if="expanded" class="panel-body">
|
||||
<KnowledgeSourceSection v-if="knowledgeChunks.length > 0" :chunks="knowledgeChunks" />
|
||||
<WebSearchSourceSection v-if="webSources.length > 0" :sources="webSources" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ChevronDown } from 'lucide-vue-next'
|
||||
import KnowledgeSourceSection from '@/components/KnowledgeSourceSection.vue'
|
||||
import WebSearchSourceSection from '@/components/WebSearchSourceSection.vue'
|
||||
|
||||
const props = defineProps({
|
||||
sources: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
knowledgeChunks: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
webSources: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const expanded = ref(false)
|
||||
const knowledgeChunks = computed(() =>
|
||||
Array.isArray(props.sources?.knowledgeChunks) ? props.sources.knowledgeChunks : props.knowledgeChunks
|
||||
)
|
||||
const webSources = computed(() =>
|
||||
Array.isArray(props.sources?.webSources) ? props.sources.webSources : props.webSources
|
||||
)
|
||||
|
||||
const hasAny = computed(
|
||||
() =>
|
||||
(Array.isArray(knowledgeChunks.value) && knowledgeChunks.value.length > 0) ||
|
||||
(Array.isArray(webSources.value) && webSources.value.length > 0)
|
||||
)
|
||||
|
||||
const toggleExpanded = () => {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.conversation-sources-panel {
|
||||
margin: 8px 0 14px 0;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: 8px;
|
||||
background: var(--gray-25);
|
||||
overflow: hidden;
|
||||
|
||||
.panel-header {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
|
||||
.title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
color: var(--gray-600);
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.rotated {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -41,6 +41,31 @@
|
||||
title="重新生成"
|
||||
><RotateCcw size="12" />
|
||||
</span>
|
||||
|
||||
<!-- 来源按钮 - 使用 flex-grow 占据剩余空间并右对齐 -->
|
||||
<div v-if="hasSources && showKey('sources')" class="sources-spacer"></div>
|
||||
<span
|
||||
v-if="hasSources && showKey('sources')"
|
||||
class="item btn sources-btn"
|
||||
:class="{ expanded: isSourcesExpanded }"
|
||||
@click="toggleSources"
|
||||
:title="isSourcesExpanded ? '收起详情' : '查看来源详情'"
|
||||
>
|
||||
<BookOpen size="12" />
|
||||
<span class="sources-label">
|
||||
来源
|
||||
<template v-if="sourceCount > 0">
|
||||
{{ sourceCount }}
|
||||
</template>
|
||||
</span>
|
||||
<ChevronDown :size="12" class="expand-icon" :class="{ rotated: isSourcesExpanded }" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 来源详情面板 -->
|
||||
<div v-if="isSourcesExpanded" class="sources-panel-body">
|
||||
<KnowledgeSourceSection v-if="knowledgeChunks.length > 0" :chunks="knowledgeChunks" />
|
||||
<WebSearchSourceSection v-if="webSources.length > 0" :sources="webSources" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -68,8 +93,10 @@
|
||||
import { ref, computed, reactive, watch } from 'vue'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { ThumbsUp, ThumbsDown, Bot, Copy, Check, RotateCcw } from 'lucide-vue-next'
|
||||
import { ThumbsUp, ThumbsDown, Bot, Copy, Check, RotateCcw, BookOpen, ChevronDown } from 'lucide-vue-next'
|
||||
import { agentApi } from '@/apis'
|
||||
import KnowledgeSourceSection from '@/components/KnowledgeSourceSection.vue'
|
||||
import WebSearchSourceSection from '@/components/WebSearchSourceSection.vue'
|
||||
|
||||
const emit = defineEmits(['retry', 'openRefs'])
|
||||
const props = defineProps({
|
||||
@ -81,11 +108,39 @@ const props = defineProps({
|
||||
isLatestMessage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
sources: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const msg = ref(props.message)
|
||||
|
||||
// Sources state
|
||||
const isSourcesExpanded = ref(false)
|
||||
|
||||
const knowledgeChunks = computed(() =>
|
||||
Array.isArray(props.sources?.knowledgeChunks) ? props.sources.knowledgeChunks : []
|
||||
)
|
||||
const webSources = computed(() =>
|
||||
Array.isArray(props.sources?.webSources) ? props.sources.webSources : []
|
||||
)
|
||||
|
||||
const hasSources = computed(
|
||||
() =>
|
||||
knowledgeChunks.value.length > 0 ||
|
||||
webSources.value.length > 0
|
||||
)
|
||||
|
||||
const sourceCount = computed(
|
||||
() => knowledgeChunks.value.length + webSources.value.length
|
||||
)
|
||||
|
||||
const toggleSources = () => {
|
||||
isSourcesExpanded.value = !isSourcesExpanded.value
|
||||
}
|
||||
|
||||
// Feedback state
|
||||
const feedbackState = reactive({
|
||||
hasSubmitted: false,
|
||||
@ -272,11 +327,12 @@ const cancelDislike = () => {
|
||||
<style lang="less" scoped>
|
||||
.refs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 10px;
|
||||
color: var(--gray-500);
|
||||
font-size: 13px;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
|
||||
.item {
|
||||
background: var(--gray-50);
|
||||
@ -313,7 +369,66 @@ const cancelDislike = () => {
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
|
||||
.sources-spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.sources-btn {
|
||||
margin-left: auto;
|
||||
background: var(--gray-50);
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 10px;
|
||||
|
||||
&:hover {
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
background: var(--main-50);
|
||||
color: var(--main-700);
|
||||
border-color: var(--main-100);
|
||||
}
|
||||
|
||||
.sources-label {
|
||||
font-weight: 500;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
margin-left: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sources-panel-body {
|
||||
background: var(--gray-25);
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { agentApi } from '@/apis/agent_api'
|
||||
import { agentApi, databaseApi, mcpApi } from '@/apis'
|
||||
import { handleChatError } from '@/utils/errorHandler'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
@ -14,6 +14,10 @@ export const useAgentStore = defineStore(
|
||||
const selectedAgentId = ref(null)
|
||||
const defaultAgentId = ref(null)
|
||||
|
||||
// 资源相关状态
|
||||
const availableKnowledgeBases = ref([])
|
||||
const availableMcps = ref([])
|
||||
|
||||
// 智能体配置相关状态
|
||||
const agentConfig = ref({})
|
||||
const originalAgentConfig = ref({})
|
||||
@ -91,6 +95,22 @@ export const useAgentStore = defineStore(
|
||||
})
|
||||
|
||||
// ==================== 方法 ====================
|
||||
/**
|
||||
* 获取可提及的资源(知识库、MCP等)
|
||||
*/
|
||||
async function fetchMentionResources() {
|
||||
try {
|
||||
const [dbsRes, mcpsRes] = await Promise.all([
|
||||
databaseApi.getAccessibleDatabases().catch(() => ({ databases: [] })),
|
||||
mcpApi.getMcpServers().catch(() => ({ data: [] }))
|
||||
])
|
||||
availableKnowledgeBases.value = dbsRes.databases || []
|
||||
availableMcps.value = mcpsRes.data || []
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch mention resources:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 store
|
||||
*/
|
||||
@ -104,6 +124,7 @@ export const useAgentStore = defineStore(
|
||||
try {
|
||||
await fetchAgents()
|
||||
await fetchDefaultAgent()
|
||||
await fetchMentionResources()
|
||||
|
||||
if (!selectedAgent.value) {
|
||||
if (defaultAgent.value) {
|
||||
@ -449,6 +470,8 @@ export const useAgentStore = defineStore(
|
||||
agents.value = []
|
||||
selectedAgentId.value = null
|
||||
defaultAgentId.value = null
|
||||
availableKnowledgeBases.value = []
|
||||
availableMcps.value = []
|
||||
agentConfig.value = {}
|
||||
originalAgentConfig.value = {}
|
||||
agentConfigs.value = {}
|
||||
@ -468,6 +491,8 @@ export const useAgentStore = defineStore(
|
||||
agents,
|
||||
selectedAgentId,
|
||||
defaultAgentId,
|
||||
availableKnowledgeBases,
|
||||
availableMcps,
|
||||
agentConfig,
|
||||
originalAgentConfig,
|
||||
agentDetails,
|
||||
@ -495,6 +520,7 @@ export const useAgentStore = defineStore(
|
||||
fetchAgents,
|
||||
fetchAgentDetail,
|
||||
fetchDefaultAgent,
|
||||
fetchMentionResources,
|
||||
setDefaultAgent,
|
||||
selectAgent,
|
||||
selectAgentConfig,
|
||||
|
||||
@ -249,6 +249,23 @@ export class MessageProcessor {
|
||||
return webSources
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取单个消息中的来源
|
||||
* @param {Object} message - 消息对象
|
||||
* @param {Array} databases - 知识库列表
|
||||
* @returns {{knowledgeChunks: Array, webSources: Array}}
|
||||
*/
|
||||
static extractSourcesFromMessage(message, databases = []) {
|
||||
if (!message || message.type !== 'ai') return { knowledgeChunks: [], webSources: [] }
|
||||
|
||||
// 复用提取逻辑,通过构建临时对话对象
|
||||
const mockConv = { messages: [message] }
|
||||
return {
|
||||
knowledgeChunks: MessageProcessor.extractKnowledgeChunksFromConversation(mockConv, databases),
|
||||
webSources: MessageProcessor.extractWebSourcesFromConversation(mockConv),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取一轮对话中的全部来源(知识库+网络搜索)
|
||||
* @param {Object} conv - 单轮对话
|
||||
|
||||
Loading…
Reference in New Issue
Block a user