From 2264a86851df6fbbfcdb7702992e47ee6763216a Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Tue, 7 Jul 2026 16:45:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=9E=81=E7=AE=80=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E6=91=98=E8=A6=81=E8=A1=8C=E6=94=B9=E4=B8=BA=E5=8D=95?= =?UTF-8?q?=E8=A1=8C=E6=B8=90=E5=8F=98+=E5=B7=A5=E5=85=B7=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=80=BB=E7=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/src/components/chat/MinimalBlocks.vue | 171 ++++++++++++++----- 1 file changed, 128 insertions(+), 43 deletions(-) diff --git a/static/src/components/chat/MinimalBlocks.vue b/static/src/components/chat/MinimalBlocks.vue index e02f922..e249589 100644 --- a/static/src/components/chat/MinimalBlocks.vue +++ b/static/src/components/chat/MinimalBlocks.vue @@ -34,7 +34,9 @@ @@ -208,7 +210,6 @@ const personalizationStore = usePersonalizationStore(); const expandedGroups = ref(new Set()); const thinkingRefs = new Map(); const summaryLoaders = new Map(); -const SUMMARY_PREVIEW_MAX_CHARS = 25; const TOOL_REEL_ITEM_HEIGHT = 26; const TOOL_REEL_INTERVAL_MS = 1450; const TOOL_REEL_ROLL_MS = 520; @@ -230,14 +231,6 @@ const toolReelStates = reactive>({}); const toolReelIntervals = new Map(); const toolReelTimeouts = new Map(); -const truncateSummaryPreview = (raw: string) => { - const text = typeof raw === 'string' ? raw : ''; - if (text.length > SUMMARY_PREVIEW_MAX_CHARS) { - return `${text.slice(0, SUMMARY_PREVIEW_MAX_CHARS)}...`; - } - return text; -}; - const getAnimatedSummaryChars = (text: string) => Array.from(text || ''); const getFirstLine = (raw: string) => { @@ -254,7 +247,7 @@ const getToolSummaryText = (action: Action) => { const intentText = tool.intent_rendered || tool.intent_full || ''; if (intentEnabled && intentText) { - return truncateSummaryPreview(getFirstLine(intentText)); + return getFirstLine(intentText); } if (tool.status === 'preparing') { @@ -426,10 +419,13 @@ const finishToolReel = (groupId: string, actions: Action[]) => { }, TOOL_REEL_ROLL_MS); pushToolReelTimeout(groupId, settleTimeout); - const cleanupTimeout = window.setTimeout(() => { - clearToolReelTimers(groupId); - delete toolReelStates[groupId]; - }, TOOL_REEL_ROLL_MS + TOOL_REEL_SETTLE_MS + 260); + const cleanupTimeout = window.setTimeout( + () => { + clearToolReelTimers(groupId); + delete toolReelStates[groupId]; + }, + TOOL_REEL_ROLL_MS + TOOL_REEL_SETTLE_MS + 260 + ); pushToolReelTimeout(groupId, cleanupTimeout); }); }; @@ -438,7 +434,11 @@ const syncToolReels = () => { const activeGroups = new Set(); blockGroups.value.forEach((group) => { - if (group.type !== 'summary' || !group.actions || !shouldShowToolReel(group.actions, group.id)) { + if ( + group.type !== 'summary' || + !group.actions || + !shouldShowToolReel(group.actions, group.id) + ) { return; } @@ -584,22 +584,64 @@ const blockGroups = computed(() => { return groups; }); -// 获取摘要组的预览文本(始终显示最新的思考/工具) -const getSummaryPreview = (actions: Action[]) => { - // 优先返回正在 streaming 的最后一个 +type ToolCategory = 'read' | 'command' | 'edit' | 'search' | 'webpage' | 'other'; + +const TOOL_CATEGORY_MAP: Record = { + // 读取文件 + read_file: 'read', + read_skill: 'read', + + // 运行指令 + run_command: 'command', + terminal_session: 'command', + terminal_input: 'command', + terminal_snapshot: 'command', + + // 编辑文件 + create_file: 'edit', + write_file: 'edit', + edit_file: 'edit', + delete_file: 'edit', + rename_file: 'edit', + create_folder: 'edit', + save_webpage: 'edit', + + // 搜索 + web_search: 'search', + + // 查看网页 + extract_webpage: 'webpage' +}; + +const CATEGORY_LABELS: Record string> = { + read: (n) => `读取了 ${n} 个文件`, + command: (n) => `运行了 ${n} 个指令`, + edit: (n) => `编辑了 ${n} 次文件`, + search: (n) => `搜索了 ${n} 次`, + webpage: (n) => `查看了 ${n} 次网页`, + other: (n) => `执行了 ${n} 次其他操作` +}; + +const CATEGORY_ORDER: ToolCategory[] = ['read', 'command', 'edit', 'search', 'webpage', 'other']; + +const getToolCategory = (action: Action): ToolCategory => { + const name = action.tool?.name; + if (typeof name !== 'string') return 'other'; + return TOOL_CATEGORY_MAP[name] || 'other'; +}; + +// 运行中:显示当前最新步骤的意图或状态(单行,由 CSS 截断) +const getRunningSummaryText = (actions: Action[]): string => { const streamingActions = actions.filter((a) => a.streaming); let currentStep; if (streamingActions.length > 0) { - // 如果有 streaming 的,返回最后一个 streaming 的 currentStep = streamingActions[streamingActions.length - 1]; } else { - // 如果没有 streaming 的(全部完成了),优先返回最后一个工具 const toolActions = actions.filter((a) => a.type === 'tool'); if (toolActions.length > 0) { currentStep = toolActions[toolActions.length - 1]; } else { - // 如果没有工具,返回最后一个思考 currentStep = actions[actions.length - 1]; } } @@ -607,13 +649,11 @@ const getSummaryPreview = (actions: Action[]) => { if (!currentStep) return ''; if (currentStep.type === 'thinking') { - // 思考内容:最多显示到第一个换行,且不超过25字符(不含...) const content = currentStep.content || ''; - const firstLineEnd = content.indexOf('\n'); - const textToShow = firstLineEnd > 0 ? content.substring(0, firstLineEnd) : content; - return truncateSummaryPreview(textToShow); - } else if (currentStep.type === 'tool') { - // 工具:优先显示 intent_rendered 或 intent_full + return getFirstLine(content); + } + + if (currentStep.type === 'tool') { const tool = currentStep.tool; if (!tool) return ''; @@ -621,13 +661,9 @@ const getSummaryPreview = (actions: Action[]) => { const intentText = tool.intent_rendered || tool.intent_full || ''; if (intentEnabled && intentText) { - // 开启intent模式且有intent时,只显示intent - const firstLineEnd = intentText.indexOf('\n'); - const textToShow = firstLineEnd > 0 ? intentText.substring(0, firstLineEnd) : intentText; - return truncateSummaryPreview(textToShow); + return getFirstLine(intentText); } - // 没有intent或未开启intent模式时显示状态 if (tool.status === 'preparing') { return `准备调用 ${tool.name || '工具'}...`; } @@ -635,15 +671,64 @@ const getSummaryPreview = (actions: Action[]) => { return `正在调用 ${tool.name || '工具'}...`; } if (tool.status === 'completed') { - return truncateSummaryPreview(tool.display_name || tool.name || '工具执行完成'); + return tool.display_name || tool.name || '工具执行完成'; } - return currentStep.streaming - ? '正在执行工具...' - : truncateSummaryPreview(tool.display_name || tool.name || '工具执行完成'); + return currentStep.streaming ? '正在执行工具...' : tool.display_name || tool.name || '执行工具'; } + return ''; }; +// 完成后:汇总本组所有工具的执行次数 +const getCompletedSummaryText = (actions: Action[]): string => { + const toolActions = actions.filter((a) => a.type === 'tool'); + + if (toolActions.length === 0) { + // 没有工具时回退到最后一条思考的单行内容 + const lastThinking = actions.filter((a) => a.type === 'thinking').pop(); + if (lastThinking) { + return getFirstLine(lastThinking.content || ''); + } + return ''; + } + + // 只有一个工具时,直接显示该工具的 intent/名称 + if (toolActions.length === 1) { + return getToolSummaryText(toolActions[0]); + } + + const counts: Record = { + read: 0, + command: 0, + edit: 0, + search: 0, + webpage: 0, + other: 0 + }; + + toolActions.forEach((action) => { + counts[getToolCategory(action)]++; + }); + + const parts: string[] = []; + CATEGORY_ORDER.forEach((category) => { + const count = counts[category]; + if (count > 0) { + parts.push(CATEGORY_LABELS[category](count)); + } + }); + + return parts.join(','); +}; + +// 摘要行统一入口:运行中显示实时进度,完成后显示统计总结 +const getSummaryLineText = (actions: Action[], groupId: string): string => { + if (isSummaryRunning(actions, groupId)) { + return getRunningSummaryText(actions); + } + return getCompletedSummaryText(actions); +}; + // 判断摘要组是否正在运行(显示加载动画还是对勾) const isSummaryRunning = (actions: Action[], groupId: string) => { if (!props.conversationRunning || !props.isLatestMessage) { @@ -890,10 +975,11 @@ onBeforeUnmount(() => { .summary-preview { display: block; - white-space: normal; - overflow-wrap: anywhere; - word-break: break-word; + white-space: nowrap; + overflow: hidden; color: inherit; + mask-image: linear-gradient(to right, black 85%, transparent 100%); + -webkit-mask-image: linear-gradient(to right, black 85%, transparent 100%); } .summary-tool-reel-window { @@ -1090,8 +1176,7 @@ onBeforeUnmount(() => { margin-top: calc((1em - 1.7em) / 2); /* 滚动条样式(参考 git 状态侧边栏文件列表,滚动槽透明贴合灰色底色) */ scrollbar-width: thin; /* Firefox */ - scrollbar-color: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent) - transparent; + scrollbar-color: color-mix(in srgb, var(--claude-text-secondary) 55%, transparent) transparent; } .thinking-content::-webkit-scrollbar {