Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"mitt": "^3.0.1",
"nanoid": "^6.0.1",
"nprogress": "^0.2.0",
"pdfjs-dist": "^6.5.438",
"pinia": "^3.0.4",
"prettier": "3.8.4",
"recorder-core": "^1.3.26070800",
Expand Down
2 changes: 2 additions & 0 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@ Chat 协议及认证接口见 [chat/README.md](chat/README.md)。

历史消息和流式聚合复用 `@/api/types` 的 `ChatContent`,定义在 `types/chat-log.ts`;
`ChatLogRecord.question` 与 `messages` 使用该内容项类型,扩展字段保留为 `unknown`。
`paragraph_list` 为 `ChatParagraphSource[]`,后端仅在开启「显示知识来源」或调试、日志时返回;
其 `meta` 在历史快照中可能是 JSON 字符串,使用方需兼容字符串与对象。
Chat、Workspace 调试和 System 调试的记录分页及详情均声明 `ChatLogRecord` 返回类型,
消息列表依赖从 `ConversationApiAdapter` 选取对应接口约定。
`conversation-panel/core/stream.ts` 的 `ConversationStream<Chunk>` 通过回调推断分片类型,
Expand Down
31 changes: 30 additions & 1 deletion ui/src/api/types/chat-log.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { CHAT_LOG_SOURCE } from '@/api/enums'
import type { Dict } from './common'
import type { DocumentResourceType } from './document'
import type { KnowledgeHitTestAsset } from './knowledge'

export type ChatLogSource = (typeof CHAT_LOG_SOURCE)[keyof typeof CHAT_LOG_SOURCE]

Expand Down Expand Up @@ -56,10 +58,37 @@ export interface ChatLogRecord extends Partial<ChatFeedback> {
answer_tokens?: number
run_time?: number
improve_paragraph_id_list?: string[]
paragraph_list?: Dict<unknown>[]
paragraph_list?: ChatParagraphSource[]
execution_details?: Dict<unknown>[]
}

/** 回答引用的知识库分段,来自检索与重排节点,仅在开启「显示知识来源」时返回。 */
export interface ChatParagraphSource {
id: string
title?: string
/** 分段正文,Markdown */
content?: string
similarity?: number
is_active?: boolean
document_id?: string
document_name?: string
document_resource_type?: DocumentResourceType | null
document_image_id?: string | null
hit_asset?: KnowledgeHitTestAsset | null
knowledge_id?: string
knowledge_name?: string
knowledge_type?: string
/** 历史快照中可能为 JSON 字符串 */
meta?: ChatParagraphSourceMeta | string | null
}

/** 分段来源文档的元数据。 */
export interface ChatParagraphSourceMeta {
source_file_id?: string
source_url?: string
allow_download?: boolean
}

/** 对话反馈:-1 未评价,0 满意,1 不满意。 */
export interface ChatFeedback {
vote_status: '-1' | '0' | '1'
Expand Down
18 changes: 18 additions & 0 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,24 @@ Chat、Portal 和 Display 面板直接放置消息列表,并以当前会话 ID
同目录 `QuestionEditor.vue` 接收 `content`、`disabled`,维护本次编辑草稿,通过 `cancel` 和
`send(question)` 通知入口组件;进入时挂载并聚焦,退出时卸载。入口组件负责发送能力与附件传递。

### RightSidebar(对话模块)

`conversation-panel/right-sidebar/index.vue` 放在视图布局的 `#right` 插槽,按当前面板展示执行详情或知识来源,
两者共用右侧位置,后打开的替换当前面板;挤压与抽屉由 `ConversationLayout` 按视图模式处理,面板自身不使用抽屉。
每类面板各自有 `index.ts` 与 `index.vue`:`index.ts` 维护本面板数据,`index.vue` 只负责展示。
`right-sidebar/index.ts` 的 `createRightSidebarStore()` 维护当前面板与右侧开合,并组合
`execution-detail` 与 `knowledge-source` 的面板 store;由各视图创建并 provide `RIGHT_SIDEBAR_KEY`,
组件通过 `useRightSidebarStore()` 获取,面板经其 `executionDetail`、`knowledgeSource` 读取数据。消息底部的执行详情入口调用 `showExecutionDetail`,
知识来源入口在记录含分段(`ChatRecordMeta.paragraphList`)时显示并调用 `showKnowledgeSource`。
`knowledge-source/` 面板与执行详情结构一致:标题栏显示分段数量与关闭,内容区展示分段卡片;
其 `index.ts` 的 `createKnowledgeSourceStore()` 保存分段及正在预览的文档,按相似度从高到低整理分段,兼容字符串 `meta`,
提供召回图片与来源文档的打开方式:知识库文件中的 PDF 在知识来源内预览,其他文件以下载方式获取,
网页来源仅接受 http(s) 地址并在新窗口打开,无可用地址时按下载权限提示;来源文件不交给浏览器原生打开或预览。
文档预览是知识来源的下一级,位于 `knowledge-source/document-preview/`,覆盖在分段列表之上显示,
列表仅隐藏不卸载以便返回时保留滚动位置,打开新的知识来源时回到列表。预览以 pdf.js 把 PDF 逐页绘制为 canvas,不创建文本层与注释层,关闭 XFA,
避免原生预览执行文件内脚本;pdf.js 在首次预览时按需加载,支持返回知识来源、全屏及按宽度重绘;
预览区预留滚动条位置,重绘在后台完成后整体替换并保持滚动比例,不清空已显示页面。

### 分享选择(对话模块)

正式对话的 `message-list` 根据会话 Store 的 `isShared` 展示分享选择态,同轮提问与回答同步选择,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { ChatContent } from '@/api/types'
import { copyText } from '@/utils/clipboard'
import { datetimeFormat } from '@/utils/time'
import type { ChatMessage, ChatRecordMeta } from '../../core/types'
import { useExecutionDetailStore } from '../../right-sidebar/execution-detail/index'
import { useRightSidebarStore } from '../../right-sidebar/index'

const props = defineProps<{
api: Pick<typeof ChatLogApi, 'putChatRecordLog' | 'getMarkChatRecord' | 'deleteMarkChatRecord'>
Expand All @@ -17,13 +17,13 @@ const props = defineProps<{
message: ChatMessage
}>()

const detail = useExecutionDetailStore()
const rightSidebar = useRightSidebarStore()

const EMPTY: ChatRecordMeta = {
messageTokens: 0,
answerTokens: 0,
runTime: 0,
knowledgeCount: 0,
paragraphList: [],
executionDetails: [],
}
const meta = computed<ChatRecordMeta>(() => props.message.record || EMPTY)
Expand All @@ -43,7 +43,7 @@ function collectAnswerText(contents: ChatContent[]): string {
const answerText = computed(() => collectAnswerText(props.message.content))

const openExecutionDetail = () => {
detail.showExecutionDetail(meta.value.executionDetails || [])
rightSidebar.showExecutionDetail(meta.value.executionDetails || [])
}
</script>

Expand All @@ -53,9 +53,10 @@ const openExecutionDetail = () => {
<div class="mk-conversation-message-footer space-y-2">
<!-- 助手消息底部信息栏:知识来源 / 执行详情 / Tokens · 耗时 -->
<div class="flex-align-center gap-1">
<el-button v-if="meta.knowledgeCount" text class="-ml-1">
<!-- 知识来源:在右侧展示回答引用的分段 -->
<el-button v-if="meta.paragraphList.length" text class="-ml-1" @click="rightSidebar.showKnowledgeSource(meta.paragraphList)">
<MkIcon name="icon_reference_outlined" />
<span>知识来源 ({{ meta.knowledgeCount }})</span>
<span>知识来源 ({{ meta.paragraphList.length }})</span>
</el-button>

<!-- 执行详情:对话结束时已拉取并缓存,点击直接在右侧展示 -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const buildRecordMeta = (raw: ChatLogRecord): ChatRecordMeta => ({
messageTokens: raw.message_tokens || 0,
answerTokens: raw.answer_tokens || 0,
runTime: raw.run_time || 0,
knowledgeCount: (raw.paragraph_list || []).length,
paragraphList: raw.paragraph_list || [],
executionDetails: raw.execution_details || [],
feedback: { vote_status: raw.vote_status ?? '-1', vote_reason: raw.vote_reason, vote_other_content: raw.vote_other_content },
})
Expand Down
13 changes: 7 additions & 6 deletions ui/src/conversation-panel/components/message-footer/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { ChatContent, ChatFeedback } from '@/api/types'
import { copyText } from '@/utils/clipboard'
import { datetimeFormat } from '@/utils/time'
import type { ChatMessage, ChatRecordMeta } from '../../core/types'
import { useExecutionDetailStore } from '../../right-sidebar/execution-detail/index'
import { useRightSidebarStore } from '../../right-sidebar/index'

import { MESSAGE_LIST_KEY } from '../message-list/index'
import { useShareLinkStoreOptional } from '../message-list/share/index'
Expand All @@ -32,13 +32,13 @@ const shareLink = useShareLinkStoreOptional()
const messageList = inject(MESSAGE_LIST_KEY, null)
const canShare = computed(() => Boolean(messageList?.messages.value.length && !messageList.streamLoading.value))
const show_exec=computed(()=>messageList?.appInfo.value?.show_exec)
const detail = useExecutionDetailStore()
const rightSidebar = useRightSidebarStore()

const EMPTY: ChatRecordMeta = {
messageTokens: 0,
answerTokens: 0,
runTime: 0,
knowledgeCount: 0,
paragraphList: [],
executionDetails: [],
}
const meta = computed<ChatRecordMeta>(() => props.message.record || EMPTY)
Expand Down Expand Up @@ -93,7 +93,7 @@ function toggleReading() {
onBeforeUnmount(stopReading)

const openExecutionDetail = () => {
detail.showExecutionDetail(meta.value.executionDetails || [])
rightSidebar.showExecutionDetail(meta.value.executionDetails || [])
}
// 消息反馈:Footer 协调按钮、草稿面板与提交状态。
const canFeedback = computed(() => Boolean(messageList?.feedback && props.message.recordId && props.message.record))
Expand Down Expand Up @@ -137,9 +137,10 @@ function openFeedback(status: '0' | '1') {
<div ref="footerRef" class="mk-conversation-message-footer space-y-2">
<!-- 助手消息底部信息栏:知识来源 / 执行详情 / Tokens · 耗时 -->
<div class="flex-align-center gap-1">
<el-button v-if="meta.knowledgeCount" text class="-ml-1">
<!-- 知识来源:在右侧展示回答引用的分段 -->
<el-button v-if="meta.paragraphList.length" text class="-ml-1" @click="rightSidebar.showKnowledgeSource(meta.paragraphList)">
<MkIcon name="icon_reference_outlined" />
<span>知识来源 ({{ meta.knowledgeCount }})</span>
<span>知识来源 ({{ meta.paragraphList.length }})</span>
</el-button>

<!-- 执行详情:对话结束时已拉取并缓存,点击直接在右侧展示 -->
Expand Down
2 changes: 1 addition & 1 deletion ui/src/conversation-panel/components/message-list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const buildRecordMeta = (raw?: ChatLogRecord | null): ChatRecordMeta => ({
messageTokens: raw?.message_tokens || 0,
answerTokens: raw?.answer_tokens || 0,
runTime: raw?.run_time || 0,
knowledgeCount: (raw?.paragraph_list || []).length,
paragraphList: raw?.paragraph_list || [],
executionDetails: raw?.execution_details || [],
})

Expand Down
8 changes: 4 additions & 4 deletions ui/src/conversation-panel/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ChatFeedback, ChatApplicationProfile, ChatContent, ChatLogRecord, ChatUserProfile, Dict } from '@/api/types'
import type { ChatFeedback, ChatApplicationProfile, ChatContent, ChatLogRecord, ChatParagraphSource, ChatUserProfile, Dict } from '@/api/types'

/**
* 当前对话的智能体信息,由 view 加载并注入各组件 store,chat 与 debug 结构一致。
Expand All @@ -21,14 +21,14 @@ export interface Conversation {
update_time?: string
}

// 助手消息底部展示数据(对话结束后 / 历史加载时填充):知识来源数、tokens、耗时、执行详情
// 助手消息底部展示数据(对话结束后 / 历史加载时填充):知识来源分段、tokens、耗时、执行详情
export interface ChatRecordMeta {
feedback?: ChatFeedback
createTime?: string
messageTokens: number
answerTokens: number
runTime: number
knowledgeCount: number
paragraphList: ChatParagraphSource[]
executionDetails: NonNullable<ChatLogRecord['execution_details']>
}

Expand Down Expand Up @@ -82,4 +82,4 @@ export interface ConversationApiAdapter {
}

// 说明:各组件 store 的类型不在此集中声明,而是各组件 index.ts 用 ReturnType 自导出
// (ConversationListStore / MessageListStore / MessageInputStore / ExecutionDetailStore)。
// (ConversationListStore / MessageListStore / MessageInputStore / RightSidebarStore)。
32 changes: 8 additions & 24 deletions ui/src/conversation-panel/right-sidebar/execution-detail/index.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,18 @@
import { ref, inject, type InjectionKey } from 'vue'
import { ref } from 'vue'
import type { ChatRecordMeta } from '../../core/types'

/**
* execution-detail 组件 store:本组件独有——执行详情数据 + 右侧开合。
* 消息内容项点击「执行详情」时,注入本 store 调 showExecutionDetail 打开右侧。
* 执行详情面板 store:当前展示的执行详情数据。
* 由右侧栏 store 创建并组合,右侧开合与面板切换由右侧栏 store 维护。
*/
export function createExecutionDetailStore() {
const detail = ref<any[] | null>(null)
const rightSideOpen = ref(false)
const executionDetails = ref<ChatRecordMeta['executionDetails']>([])

const showExecutionDetail = (d: any[]) => {
detail.value = d
rightSideOpen.value = true
const setExecutionDetails = (details: ChatRecordMeta['executionDetails']) => {
executionDetails.value = details
}

return {
detail,
rightSideOpen,
showExecutionDetail,
toggleRightSide: () => (rightSideOpen.value = !rightSideOpen.value),
openRightSide: () => (rightSideOpen.value = true),
closeRightSide: () => (rightSideOpen.value = false),
}
return { executionDetails, setExecutionDetails }
}

export type ExecutionDetailStore = ReturnType<typeof createExecutionDetailStore>

export const EXECUTION_DETAIL_KEY: InjectionKey<ExecutionDetailStore> = Symbol('execution-detail')

export function useExecutionDetailStore(): ExecutionDetailStore {
const store = inject(EXECUTION_DETAIL_KEY)
if (!store) throw new Error('useExecutionDetailStore 必须在 ConversationView 内使用')
return store
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<script setup lang="ts">
import ExecutionDetailContent from '@/workflow-canvas/execution-details/index.vue'
import { WorkflowMode } from '@/workflow-canvas/types'
import { useExecutionDetailStore } from './index'
import { useRightSidebarStore } from '../index'

withDefaults(defineProps<{ workflowMode?: WorkflowMode }>(), {
workflowMode: WorkflowMode.Application,
})

const { detail, closeRightSide } = useExecutionDetailStore()
const { executionDetail, closeRightSide } = useRightSidebarStore()
const { executionDetails: detail } = executionDetail
</script>

<template>
Expand Down
61 changes: 61 additions & 0 deletions ui/src/conversation-panel/right-sidebar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { inject, ref, type InjectionKey } from 'vue'
import type { ChatParagraphSource } from '@/api/types'
import type { ChatRecordMeta } from '../core/types'
import { createExecutionDetailStore } from './execution-detail/index'
import { createKnowledgeSourceStore } from './knowledge-source/index'

/** 右侧栏面板:执行详情与知识来源共用同一位置,后打开的替换当前面板。 */
export const RIGHT_SIDEBAR_PANEL = {
EXECUTION_DETAIL: 'EXECUTION_DETAIL',
KNOWLEDGE_SOURCE: 'KNOWLEDGE_SOURCE',
} as const

export type RightSidebarPanel = (typeof RIGHT_SIDEBAR_PANEL)[keyof typeof RIGHT_SIDEBAR_PANEL]

/**
* 右侧栏 store:当前面板与右侧开合,组合各面板自己的 store。
* 由 view 创建并 provide;消息底部点击「执行详情」「知识来源」时写入对应面板数据并打开右侧。
*/
export function createRightSidebarStore() {
const panel = ref<RightSidebarPanel>(RIGHT_SIDEBAR_PANEL.EXECUTION_DETAIL)
const rightSideOpen = ref(false)
const executionDetail = createExecutionDetailStore()
const knowledgeSource = createKnowledgeSourceStore()

const openPanel = (target: RightSidebarPanel) => {
panel.value = target
rightSideOpen.value = true
}

// 展示执行详情
const showExecutionDetail = (details: ChatRecordMeta['executionDetails']) => {
executionDetail.setExecutionDetails(details)
openPanel(RIGHT_SIDEBAR_PANEL.EXECUTION_DETAIL)
}

// 展示回答引用的知识来源分段
const showKnowledgeSource = (paragraphs: ChatParagraphSource[]) => {
knowledgeSource.setParagraphs(paragraphs)
openPanel(RIGHT_SIDEBAR_PANEL.KNOWLEDGE_SOURCE)
}

return {
panel,
rightSideOpen,
executionDetail,
knowledgeSource,
showExecutionDetail,
showKnowledgeSource,
closeRightSide: () => (rightSideOpen.value = false),
}
}

export type RightSidebarStore = ReturnType<typeof createRightSidebarStore>

export const RIGHT_SIDEBAR_KEY: InjectionKey<RightSidebarStore> = Symbol('right-sidebar')

export function useRightSidebarStore(): RightSidebarStore {
const store = inject(RIGHT_SIDEBAR_KEY)
if (!store) throw new Error('useRightSidebarStore 必须在 ConversationView 内使用')
return store
}
15 changes: 15 additions & 0 deletions ui/src/conversation-panel/right-sidebar/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import ExecutionDetail from './execution-detail/index.vue'
import KnowledgeSource from './knowledge-source/index.vue'
import { RIGHT_SIDEBAR_PANEL, useRightSidebarStore } from './index'

defineOptions({ name: 'RightSidebar' })

const { panel } = useRightSidebarStore()
</script>

<template>
<!-- 右侧栏:按当前面板展示执行详情或知识来源 -->
<KnowledgeSource v-if="panel === RIGHT_SIDEBAR_PANEL.KNOWLEDGE_SOURCE" />
<ExecutionDetail v-else />
</template>
Loading
Loading