diff --git a/ai_knowledge_base_v2/apps/admin-web/src/components/ChatDetailDrawer.vue b/ai_knowledge_base_v2/apps/admin-web/src/components/ChatDetailDrawer.vue index ab7cad5..8a1ce8f 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/components/ChatDetailDrawer.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/components/ChatDetailDrawer.vue @@ -17,9 +17,15 @@ const emit = defineEmits<{ "update:modelValue": [value: boolean] }>(); const detail = ref(null); const loading = ref(false); const messagesLoading = ref(false); +const aiLogsLoading = ref(false); +const aiLogDetailsLoading = ref([]); +const activeAiLogIds = ref([]); +const messageKeyword = ref(""); +const appliedMessageKeyword = ref(""); const exporting = ref(false); const bodyRef = ref(null); const pager = reactive({ page: 1, pageSize: 20, total: 0 }); +const aiPager = reactive({ page: 1, pageSize: 20, total: 0 }); watch( () => [props.modelValue, props.sessionId, props.focusMessageId] as const, @@ -27,6 +33,10 @@ watch( if (!open || !sessionId) return; detail.value = null; Object.assign(pager, { page: 1, total: 0 }); + Object.assign(aiPager, { page: 1, total: 0 }); + messageKeyword.value = ""; + appliedMessageKeyword.value = ""; + activeAiLogIds.value = []; await loadDetail(sessionId, 1, pager.pageSize); }, ); @@ -45,6 +55,7 @@ async function loadDetail(sessionId: number, page: number, pageSize: number) { pageSize: messagePage?.pageSize ?? pageSize, total: messagePage?.total ?? detail.value.messages.length, }); + void loadAiLogs(sessionId, 1, aiPager.pageSize); await scrollToFocusedMessage(); } catch (error) { ElMessage.error( @@ -68,7 +79,11 @@ async function changeMessagePage(page: number, pageSize: number) { if (!props.sessionId || !detail.value) return; messagesLoading.value = true; try { - const result = await api.chatMessages(props.sessionId, { page, pageSize }); + const result = await api.chatMessages(props.sessionId, { + keyword: appliedMessageKeyword.value || undefined, + page, + pageSize, + }); detail.value = { ...detail.value, messages: result.items, @@ -80,7 +95,7 @@ async function changeMessagePage(page: number, pageSize: number) { total: result.total, }); await nextTick(); - bodyRef.value?.scrollTo({ top: 0, behavior: "smooth" }); + bodyRef.value?.querySelector(".conversation-title-row")?.scrollIntoView({ behavior: "smooth", block: "start" }); } catch (error) { ElMessage.error( error instanceof Error ? error.message : "聊天消息加载失败", @@ -90,6 +105,75 @@ async function changeMessagePage(page: number, pageSize: number) { } } +async function searchMessages() { + appliedMessageKeyword.value = messageKeyword.value.trim(); + await changeMessagePage(1, pager.pageSize); +} + +async function clearMessageSearch() { + messageKeyword.value = ""; + if (!appliedMessageKeyword.value) return; + appliedMessageKeyword.value = ""; + await changeMessagePage(1, pager.pageSize); +} + +function messageParts(content: string) { + const keyword = appliedMessageKeyword.value; + if (!keyword) return [{ text: content, matched: false }]; + const parts: Array<{ text: string; matched: boolean }> = []; + const lowerContent = content.toLocaleLowerCase(); + const lowerKeyword = keyword.toLocaleLowerCase(); + let cursor = 0; + let index = lowerContent.indexOf(lowerKeyword); + while (index >= 0) { + if (index > cursor) parts.push({ text: content.slice(cursor, index), matched: false }); + parts.push({ text: content.slice(index, index + keyword.length), matched: true }); + cursor = index + keyword.length; + index = lowerContent.indexOf(lowerKeyword, cursor); + } + if (cursor < content.length) parts.push({ text: content.slice(cursor), matched: false }); + return parts.length ? parts : [{ text: content, matched: false }]; +} + +async function loadAiLogs(sessionId: number, page: number, pageSize: number) { + aiLogsLoading.value = true; + try { + const result = await api.aiLogs({ sessionId, page, pageSize }); + if (!detail.value || props.sessionId !== sessionId) return; + detail.value.aiLogs = result.items; + Object.assign(aiPager, { + page: result.page, + pageSize: result.pageSize, + total: result.total, + }); + activeAiLogIds.value = []; + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : "关联 AI 请求加载失败"); + } finally { + aiLogsLoading.value = false; + } +} + +async function handleAiLogChange(activeNames: string | number | Array) { + const ids = (Array.isArray(activeNames) ? activeNames : [activeNames]) + .map(Number) + .filter(Number.isFinite); + for (const id of ids) { + const log = detail.value?.aiLogs.find((item) => item.id === id); + if (!log || Object.prototype.hasOwnProperty.call(log, "prompt") || aiLogDetailsLoading.value.includes(id)) continue; + aiLogDetailsLoading.value = [...aiLogDetailsLoading.value, id]; + try { + const fullLog = await api.aiLogDetail(id); + const index = detail.value?.aiLogs.findIndex((item) => item.id === id) ?? -1; + if (detail.value && index >= 0) detail.value.aiLogs[index] = fullLog; + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : "AI 请求详情加载失败"); + } finally { + aiLogDetailsLoading.value = aiLogDetailsLoading.value.filter((item) => item !== id); + } + } +} + async function exportConversation() { if (!props.sessionId) return; exporting.value = true; @@ -193,6 +277,11 @@ async function exportConversation() { + -

完整对话

+
+

完整对话

+ 找到 {{ pager.total }} 条匹配消息 +
+ 触发人工关注的消息{{ formatDateTime(message.createdAt) }} -
{{ message.content }}
+
状态:{{ message.messageStatus }}

关联 AI 请求

耗时:{{ log.costMs || "-" }}ms时间:{{ formatDateTime(log.createdAt) }}
+

正在加载请求详情…

-
{{
+            
{{
               log.prompt || "无 Prompt 记录"
             }}

@@ -325,6 +433,13 @@ async function exportConversation() {

+ span { + color: #7b8494; + font-size: 13px; +} + +.conversation-search { + margin: 0 0 12px; +} + +.conversation-search :deep(.el-input) { + max-width: 360px; +} + +.conversation-message mark { + padding: 1px 3px; + border-radius: 4px; + background: #ffe58f; + color: inherit; +} + +.ai-log-loading { + padding: 16px; + color: #7b8494; + text-align: center; +} + .conversation-message.focus-message { border: 2px solid #e6a23c; background: #fffaf0; box-shadow: 0 0 0 4px rgb(230 162 60 / 10%); + animation: focus-message-pulse 1.2s ease-out; +} + +@keyframes focus-message-pulse { + 0% { box-shadow: 0 0 0 0 rgb(230 162 60 / 38%); } + 65% { box-shadow: 0 0 0 12px rgb(230 162 60 / 8%); } + 100% { box-shadow: 0 0 0 4px rgb(230 162 60 / 10%); } } .conversation-message.focus-message .message-meta { diff --git a/ai_knowledge_base_v2/apps/admin-web/src/components/FeedbackManagementView.vue b/ai_knowledge_base_v2/apps/admin-web/src/components/FeedbackManagementView.vue index d785ec8..69c3fb5 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/components/FeedbackManagementView.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/components/FeedbackManagementView.vue @@ -1,6 +1,6 @@