From c138406217dc6db7edff51655fa69b0334b76067 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Thu, 12 Mar 2026 15:46:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=8C=BA=E5=9F=9F=E6=A8=A1=E5=9D=97=E5=8C=96=E6=9E=B6?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 messageArea.ts 拆分为多个独立模块 - 新增 messageRenderer.ts:消息渲染逻辑 - 新增 messageStyles.ts:样式定义 - 新增 questionHandler.ts:问题处理 - 新增 segmentRenderer.ts:分段渲染 - 新增 textFormatter.ts:文本格式化 - 新增 toolHelpers.ts:工具辅助函数 --- src/views/messageArea.ts | 1882 +++------------------------------- src/views/messageRenderer.ts | 215 ++++ src/views/messageStyles.ts | 622 +++++++++++ src/views/questionHandler.ts | 118 +++ src/views/segmentRenderer.ts | 274 +++++ src/views/textFormatter.ts | 56 + src/views/toolHelpers.ts | 106 ++ 7 files changed, 1536 insertions(+), 1737 deletions(-) create mode 100644 src/views/messageRenderer.ts create mode 100644 src/views/messageStyles.ts create mode 100644 src/views/questionHandler.ts create mode 100644 src/views/segmentRenderer.ts create mode 100644 src/views/textFormatter.ts create mode 100644 src/views/toolHelpers.ts diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index 0a6a196..49cf669 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -1,13 +1,8 @@ /** * 消息区域模块 - * - * 功能说明: - * - 负责聊天消息的显示和渲染 - * - 支持用户消息和 AI 消息的不同样式 - * - 提供消息操作功能(复制、点赞、点踩) - * - 支持流式消息实时更新 - * - 支持分段消息渲染(文本、工具调用、用户问题) - * - 显示工具执行状态和加载指示器 + * 功能:消息区域入口,整合样式和脚本 + * 依赖:messageStyles, toolHelpers, textFormatter, questionHandler, messageRenderer + * 使用场景:webview 内容生成 */ import { @@ -17,7 +12,6 @@ import { fileDeleteIconSvg, syntaxCheckIconSvg, SearchCode, - agentIconSvg, saveKnowledgeIconSvg, simulationIconSvg, waveformIconSvg, @@ -27,1751 +21,165 @@ import { updateStageIconSvg, successIconSvg, } from "../constants/toolIcons"; -import { - getWaveformPreviewContent, - getWaveformPreviewScript, -} from "./waveformPreviewContent"; -import { getAgentCardStyles, getAgentCardScript } from "./agentCard"; -import { getPlanCardStyles, getPlanCardScript } from "./planCard"; -import { - getCodeHighlightStyles, - getCodeHighlightScript, -} from "../components/codeHighlight"; +import { getMessageAreaStyles } from "./messageStyles"; +import { getQuestionHandlerScript } from "./questionHandler"; +import { getMessageRendererScript } from "./messageRenderer"; +import { getSegmentRendererScript } from "./segmentRenderer"; -/** - * 获取消息区域的 HTML 内容 - */ export function getMessageAreaContent(): string { return `
`; } -/** - * 获取消息区域的样式 - */ -export function getMessageAreaStyles(): string { - return ` - .messages { - flex: 1; - overflow-y: auto; - margin-bottom: 15px; - min-height: 0; - } - .message { - margin-bottom: 12px; - } - .user-message { - padding: 10px 15px; - border-radius: 8px; - background: var(--vscode-button-secondaryBackground); - border: 1px solid var(--vscode-input-border); - margin-left: auto; - width: fit-content; - max-width: 80%; - } - .bot-message { - padding: 0; - text-align: left; - color: var(--vscode-foreground); - max-width: 100%; - position: relative; - } - .message-actions { - display: flex; - gap: 8px; - margin-top: 12px; - margin-left: 10px; - opacity: 0.85; - transition: opacity 0.2s ease; - } - .message-actions:hover { - opacity: 1; - } - .action-btn { - background: transparent; - border: none; - cursor: pointer; - padding: 4px; - display: flex; - align-items: center; - justify-content: center; - color: var(--vscode-foreground); - opacity: 0.9; - transition: opacity 0.2s ease; - position: relative; - } - .action-btn:hover { - opacity: 1; - } - .action-btn svg { - width: 14px; - height: 14px; - } - .action-btn.active { - color: var(--vscode-button-background); - opacity: 1; - } - .action-btn .action-tooltip { - visibility: hidden; - width: auto; - background: #1e1e1e; - color: #ffffff; - text-align: center; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.2); - padding: 4px 8px; - position: absolute; - z-index: 1000; - bottom: 125%; - left: 50%; - transform: translateX(-50%) translateY(5px); - opacity: 0; - transition: all 0.2s ease; - font-size: 12px;white-space: nowrap;pointer-events: none; - } - .action-btn .action-tooltip::after { - content: ""; - position: absolute; - top: 100%; - left: 50%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: #1e1e1e transparent transparent transparent; - } - .action-btn .action-tooltip::before { - content: ""; - position: absolute; - top: 100%; - left: 50%; - margin-left: -6px; - border-width: 6px; - border-style: solid; - border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent; - z-index: -1; - } - .action-btn:hover .action-tooltip { - visibility: visible; - opacity: 1; - transform: translateX(-50%) translateY(0); - } +export { getMessageAreaStyles }; - /* 流式消息样式 */ - .streaming .message-content { - border-right: 2px solid var(--vscode-focusBorder); - animation: blink 1s infinite; - } - @keyframes blink { - 0%, 50% { border-color: var(--vscode-focusBorder); } - 51%, 100% { border-color: transparent; } - } - - /* 加载指示器样式 */ - .loading-message { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 16px; - color: var(--vscode-descriptionForeground); - } - .loading-dots { - display: flex; - gap: 4px;} - .loading-dots span { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--vscode-focusBorder); - animation: loadingDot 1.4s infinite ease-in-out; - } - .loading-dots span:nth-child(1) { animation-delay: 0s; } - .loading-dots span:nth-child(2) { animation-delay: 0.2s; } - .loading-dots span:nth-child(3) { animation-delay: 0.4s; } - @keyframes loadingDot { - 0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; } - 40% { transform: scale(1); opacity: 1; } - } - .loading-text { - font-size: 13px; - } - - /* 工具状态样式 */ - .tool-status { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - margin: 4px 0; - font-size: 12px; - border-radius: 6px; - background: var(--vscode-textBlockQuote-background); - } - .tool-status.tool-start { - border-left: 3px solid var(--vscode-charts-blue); - } - .tool-status.tool-complete { - border-left: 3px solid var(--vscode-charts-green); - } - .tool-status.tool-error { - border-left: 3px solid var(--vscode-charts-red); - } - .tool-icon { - font-size: 14px; - } - .tool-name { - font-weight: 500; - color: var(--vscode-foreground); - } - .tool-status-text { - color: var(--vscode-descriptionForeground); - } - .tool-detail { - margin-top: 4px; - font-size: 11px; - color: var(--vscode-descriptionForeground); - white-space: pre-wrap; - max-height: 100px; - overflow-y: auto; - } - - /* 用户问题样式 */ - .question-message { - padding: 16px; - } - .question-text { - margin-bottom: 12px; - font-weight: 500; - } - .question-options { - display: flex; - flex-wrap: wrap; - gap: 8px; - } - .question-option { - padding: 8px 16px; - background: #007ACC; - color: #ffffff; - border: 1px solid #007ACC; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s; - } - .question-option:hover { - background: #005a9e; - border-color: #005a9e; - } - .question-option.selected { - background: #007ACC; - color: #ffffff; - border-color: #007ACC; - } - .question-message.answered .question-option:not(.selected) { - opacity: 0.5; - pointer-events: none; - } - .custom-input-container { - display: flex; - gap: 8px; - width: 100%; - margin-top: 8px; - } - .custom-input { - flex: 1; - padding: 8px 12px; - background: var(--vscode-input-background); - color: var(--vscode-input-foreground); - border: 1px solid var(--vscode-input-border); - border-radius: 6px; - font-size: 13px; - } - .custom-submit { - padding: 8px 16px; - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); - border: none; - border-radius: 6px; - cursor: pointer; - } - .custom-submit:hover { - background: var(--vscode-button-hoverBackground); - } - .question-message.answered .custom-input-container { - display: none; - } - - /* 分段消息样式 */ - .segmented-message { - padding: 0; - } - .message-segment { - padding: 10px 0; - } - .segment-text { - line-height: 1.6; - } - - /* Markdown 样式 */ - .segment-text h1, - .segment-text h2, - .segment-text h3, - .question-text h1, - .question-text h2, - .question-text h3 { - margin: 0px 0 -10px 0; - font-weight: 600; - line-height: 1.3; - } - .segment-text h1, - .question-text h1 { - font-size: 1.5em; - border-bottom: 1px solid var(--vscode-panel-border); - padding-bottom: 8px; - } - .segment-text h2, - .question-text h2 { - font-size: 1.3em; - } - .segment-text h3, - .question-text h3 { - font-size: 1.1em; - } - .segment-text ul, - .segment-text ol, - .question-text ul, - .question-text ol { - margin: 8px 0; - padding-left: 24px; - } - .segment-text li, - .question-text li { - line-height: 1; - } - .segment-text strong, - .question-text strong { - font-weight: 600; - color: var(--vscode-foreground); - } - .segment-text em, - .question-text em { - font-style: italic; - } - .segment-text a, - .question-text a { - color: var(--vscode-textLink-foreground); - text-decoration: none; - } - .segment-text a:hover, - .question-text a:hover { - text-decoration: underline; - } - .segment-text p, - .question-text p { - margin: 8px 0; - } - .segment-text code, - .question-text code { - background: var(--vscode-textCodeBlock-background); - padding: 2px 6px; - border-radius: 3px; - font-family: var(--vscode-editor-font-family); - font-size: 0.9em; - } - - .segment-tool { - margin: 4px 0; - padding: 4px 0; - } - /* 低调显示的工具调用 - 移除边距和背景 */ - .segment-tool.low-profile { - margin: 25px 0px; - padding: 0; - background: none; - } - .tool-segment-header { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var(--vscode-descriptionForeground); - cursor: pointer; - } - .tool-segment-icon { - font-size: 12px; - } - .tool-segment-name { - font-weight: normal; - } - .tool-segment-result { - display: inline; - font-size: 12px; - color: var(--vscode-descriptionForeground); - margin-left: 6px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 500px; - } - .tool-collapse-icon { - width: 12px; - height: 12px; - flex-shrink: 0; - transition: transform 0.2s ease; - cursor: pointer; - } - .tool-collapse-icon svg { - width: 100%; - height: 100%; - display: block; - } - .icon-expanded svg path { - fill: #007ACC !important; - } - .tool-segment-header.collapsed .tool-collapse-icon { - transform: rotate(-90deg); - } - .tool-segment-header:not(.collapsed) .tool-collapse-icon { - transform: rotate(0deg); - } - .tool-file-write-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - margin-right: 6px; - } - .tool-file-write-icon svg { - width: 100%; - height: 100%; - display: block; - } - .tool-file-read-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - margin-right: 6px; - } - .tool-file-read-icon svg { - width: 100%; - height: 100%; - display: block; - } - .tool-file-delete-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - margin-right: 6px; - } - .tool-file-delete-icon svg { - width: 100%; - height: 100%; - display: block; - } - .tool-syntax-check-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - margin-right: 6px; - } - .tool-syntax-check-icon svg { - width: 100%; - height: 100%; - display: block; - } - .tool-search-code-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - margin-right: 6px; - } - .tool-search-code-icon svg { - width: 100%; - height: 100%; - display: block; - } - .tool-save-knowledge-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - margin-right: 6px; - } - .tool-save-knowledge-icon svg { - width: 100%; - height: 100%; - display: block; - } - .tool-simulation-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - margin-right: 6px; - } - .tool-simulation-icon svg { - width: 100%; - height: 100%; - display: block; - } - .tool-waveform-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - margin-right: 6px; - } - .tool-waveform-icon svg { - width: 100%; - height: 100%; - display: block; - } - .tool-knowledge-load-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - margin-right: 6px; - } - .tool-knowledge-load-icon svg { - width: 100%; - height: 100%; - display: block; - } - .tool-state-transition-icon { - width: 16px; - height: 16px; - flex-shrink: 0; - margin-right: 6px; - } - .tool-state-transition-icon svg { - width: 100%; - height: 100%; - display: block; - } - .tool-segment-content { - overflow: hidden; - transition: max-height 0.3s ease; - } - .tool-segment-content.collapsed { - max-height: 0; - } - .tool-segment-description { - margin: 25px 0 0 0px; - font-size: 0.9rem; - color: var(--vscode-descriptionForeground); - line-height: 1.4; - } - /* 低调显示的工具调用样式 */ - .segment-tool.low-profile .tool-segment-header { - opacity: 0.65; - font-size: 12px; - } - .segment-tool.low-profile .tool-segment-icon { - opacity: 0.55; - font-size: 11px; - } - .segment-tool.low-profile .tool-segment-name { - font-weight: 300; - opacity: 0.8; - } - .segment-tool.low-profile .tool-segment-result { - opacity: 0.7; - font-size: 12px; - } - .segment-question { - background: var(--vscode-textBlockQuote-background); - border-radius: 6px; - margin: 8px 0; - padding: 12px 35px; - border-left: 3px solid var(--vscode-charts-orange); - } - .segment-question .question-text { - margin-bottom: 12px; - font-weight: 500; - } - .segment-question .question-options { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 8px; - } - .segment-question .question-option { - padding: 8px 16px; - background: #3d3f41; - color: #ffffff; - border: 1px solid #474747; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s; - font-size: 13px; - } - .segment-question .question-option:hover { - background: #005a9e; - border-color: #005a9e; - } - .segment-question .question-option.selected { - background: #007ACC; - color: #ffffff; - border-color: #007ACC; - } - .segment-question.answered .question-option:not(.selected) { - opacity: 0.5; - pointer-events: none; - } - .segment-question .custom-input-container { - display: flex; - gap: 8px; - width: 100%; - margin-top: 8px; - } - .segment-question .custom-input { - flex: 1; - padding: 8px 12px; - background: var(--vscode-input-background); - color: var(--vscode-input-foreground); - border: 1px solid var(--vscode-input-border); - border-radius: 6px; - font-size: 13px; - margin-left: -20px; - } - .segment-question .custom-submit { - padding: 8px 16px; - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); - border: none; - border-radius: 6px; - cursor: pointer; - } - .segment-question .custom-submit:hover { - background: var(--vscode-button-hoverBackground); - } - .segment-question.answered .custom-input-container { - display: none; - } - .question-segment .question-text { - margin-bottom: 8px; - font-weight: 500; - } - .question-segment .question-options { - display: flex; - flex-wrap: wrap; - gap: 6px; - } - .question-opt { - padding: 4px 10px; - background: var(--vscode-button-secondaryBackground); - color: var(--vscode-button-secondaryForeground); - border-radius: 4px; - font-size: 12px;} - - ${getAgentCardStyles()} - - ${getPlanCardStyles()} - - ${getCodeHighlightStyles()} - - ${getWaveformPreviewContent()} - `; -} - -/** - * 获取消息区域的脚本 - */ export function getMessageAreaScript(): string { return ` - // 工具图标定义 - const collapseIconSvg = \`${collapseIconSvg}\`; - const fileWriteIconSvg = \`${fileWriteIconSvg}\`; - const fileReadIconSvg = \`${fileReadIconSvg}\`; - const fileDeleteIconSvg = \`${fileDeleteIconSvg}\`; - const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`; - const searchCodeIconSvg = \`${SearchCode}\`; - const saveKnowledgeIconSvg = \`${saveKnowledgeIconSvg}\`; - const simulationIconSvg = \`${simulationIconSvg}\`; - const waveformIconSvg = \`${waveformIconSvg}\`; - const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`; - const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`; - const userQuestionIconSvg = \`${userQuestionIconSvg}\`; - const updateStageIconSvg = \`${updateStageIconSvg}\`; - const successIconSvg = \`${successIconSvg}\`; + const collapseIconSvg = \`${collapseIconSvg}\`; + const fileWriteIconSvg = \`${fileWriteIconSvg}\`; + const fileReadIconSvg = \`${fileReadIconSvg}\`; + const fileDeleteIconSvg = \`${fileDeleteIconSvg}\`; + const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`; + const searchCodeIconSvg = \`${SearchCode}\`; + const saveKnowledgeIconSvg = \`${saveKnowledgeIconSvg}\`; + const simulationIconSvg = \`${simulationIconSvg}\`; + const waveformIconSvg = \`${waveformIconSvg}\`; + const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`; + const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`; + const userQuestionIconSvg = \`${userQuestionIconSvg}\`; + const updateStageIconSvg = \`${updateStageIconSvg}\`; + const successIconSvg = \`${successIconSvg}\`; - ${getAgentCardScript()} + function getToolIcon(toolName) { + const iconMap = { + 'file_read': fileReadIconSvg, + 'file_write': fileWriteIconSvg, + 'file_delete': fileDeleteIconSvg, + 'file_list': searchCodeIconSvg, + 'syntax_check': syntaxCheckIconSvg, + 'simulation': simulationIconSvg, + 'waveform_summary': waveformIconSvg, + 'knowledge_save': saveKnowledgeIconSvg, + 'knowledge_load': knowledgeLoadIconSvg, + 'queryKnowledgeSummary': knowledgeLoadIconSvg, + 'queryRules': knowledgeLoadIconSvg, + 'setModule': fileWriteIconSvg, + 'addSignal': fileWriteIconSvg, + 'addSignalExample': fileWriteIconSvg, + 'validateKnowledgeGraph': syntaxCheckIconSvg, + 'querySignals': searchCodeIconSvg, + 'addPlan': fileWriteIconSvg, + 'addEdge': fileWriteIconSvg, + 'showPlan': searchCodeIconSvg, + 'addRule': fileWriteIconSvg, + 'updateNode': fileWriteIconSvg, + 'addStateTransition': stateTransitionIconSvg, + 'askUser': userQuestionIconSvg, + 'updatePhase': updateStageIconSvg, + 'iverilog': successIconSvg, + }; + return iconMap[toolName] || ''; + } - ${getPlanCardScript()} + function getToolDisplayName(toolName) { + const toolNameMap = { + 'file_read': '已完成文件读取', + 'file_write': '已完成文件写入', + 'file_delete': '已完成文件删除', + 'file_list': '已检索代码文件', + 'syntax_check': '已完成语法检查', + 'simulation': '已完成仿真', + 'waveform_summary': '已完成波形分析', + 'knowledge_save': '已保存知识库', + 'knowledge_load': '已加载知识库', + 'queryKnowledgeSummary': '已查询知识摘要', + 'queryRules': '已查询规则', + 'setModule': '已设置模块', + 'addSignal': '信号分析完成', + 'addSignalExample': '信号示例处理完成', + 'validateKnowledgeGraph': '已验证知识图谱', + 'querySignals': '已查询信号', + 'addPlan': '已添加计划', + 'addEdge': '已添加边', + 'showPlan': '已显示计划', + 'addRule': '已添加规则', + 'updateNode': '已更新节点', + 'addStateTransition': '已添加状态转换', + 'spawnExplorer': '代码探索', + 'spawnDebugger': '波形调试', + 'askUser': '用户提问', + 'updatePhase': '已更新阶段', + 'iverilog': '已完成编译', + }; + return toolNameMap[toolName] || toolName; + } - // 解析多 VCD 文件路径 - function parseMultiVcdPaths(toolResult) { - if (!toolResult) return []; - const result = String(toolResult); - - // 匹配 "- moduleName: path" 格式 - const vcdListMatch = result.match(/VCD 文件列表:[\\s\\S]*?(?=\\n\\n|$)/); - if (!vcdListMatch) return []; - - const paths = []; - const lineRegex = /- (\\w+): ([^\\n]+)/g; - let match; - while ((match = lineRegex.exec(vcdListMatch[0])) !== null) { - const name = match[1]; - const pathOrError = match[2].trim(); - // 跳过失败的条目 - if (!pathOrError.startsWith('失败')) { - paths.push({ name: name + '.vcd', path: pathOrError }); - } - } - return paths; - } - - // 获取工具图标 - function getToolIcon(toolName) { - const iconMap = { - 'file_read': fileReadIconSvg, - 'file_write': fileWriteIconSvg, - 'file_delete': fileDeleteIconSvg, - 'file_list': searchCodeIconSvg, - 'syntax_check': syntaxCheckIconSvg, - 'simulation': simulationIconSvg, - 'waveform_summary': waveformIconSvg, - 'knowledge_save': saveKnowledgeIconSvg, - 'knowledge_load': knowledgeLoadIconSvg, - 'queryKnowledgeSummary': knowledgeLoadIconSvg, - 'queryRules': knowledgeLoadIconSvg, - 'setModule': fileWriteIconSvg, - 'addSignal': fileWriteIconSvg, - 'addSignalExample': fileWriteIconSvg, - 'validateKnowledgeGraph': syntaxCheckIconSvg, - 'querySignals': searchCodeIconSvg, - 'addPlan': fileWriteIconSvg, - 'addEdge': fileWriteIconSvg, - 'showPlan': searchCodeIconSvg, - 'addRule': fileWriteIconSvg, - 'updateNode': fileWriteIconSvg, - 'addStateTransition': stateTransitionIconSvg, - 'askUser': userQuestionIconSvg, - 'updatePhase': updateStageIconSvg, - 'iverilog': successIconSvg, - }; - return iconMap[toolName] || ''; - } - - // 工具名称映射 - function getToolDisplayName(toolName) { - const toolNameMap = { - 'file_read': '已完成文件读取', - 'file_write': '已完成文件写入', - 'file_delete': '已完成文件删除', - 'file_list': '已检索代码文件', - 'syntax_check': '已完成语法检查', - 'simulation': '已完成仿真', - 'waveform_summary': '已完成波形分析', - 'knowledge_save': '已保存知识库', - 'knowledge_load': '已加载知识库', - 'queryKnowledgeSummary': '已查询知识摘要', - 'queryRules': '已查询规则', - 'setModule': '已设置模块', - 'addSignal': '信号分析完成', - 'addSignalExample': '信号示例处理完成', - 'validateKnowledgeGraph': '已验证知识图谱', - 'querySignals': '已查询信号', - 'addPlan': '已添加计划', - 'addEdge': '已添加边', - 'showPlan': '已显示计划', - 'addRule': '已添加规则', - 'updateNode': '已更新节点', - 'addStateTransition': '已添加状态转换', - 'spawnExplorer': '代码探索', - 'spawnDebugger': '波形调试', - 'askUser': '用户提问', - 'updatePhase': '已更新阶段', - 'iverilog': '已完成编译', - }; - return toolNameMap[toolName] || toolName; - } - - // 自动滚动控制标志 - let shouldAutoScroll = true; - let lastScrollHeight = 0; - - // 检查用户是否在底部附近(允许50px的误差) - function isUserNearBottom() { - const threshold = 50; - return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold; - } - - // 监听用户滚动行为 - messagesEl.addEventListener('scroll', () => { - const isAtBottom = isUserNearBottom(); - - // 如果用户滚动到底部,恢复自动滚动 - if (isAtBottom) { - shouldAutoScroll = true; - } else { - // 只有当内容高度没有变化时,才认为是用户主动滚动 - // 如果内容高度变化了,说明是因为新内容导致的位置变化,不应该停止自动滚动 - if (messagesEl.scrollHeight === lastScrollHeight) { - shouldAutoScroll = false; - } - } - - lastScrollHeight = messagesEl.scrollHeight; - }); - - // 智能滚动:只有在允许自动滚动时才滚动到底部 - function smartScrollToBottom() { - if (shouldAutoScroll) { - messagesEl.scrollTop = messagesEl.scrollHeight; - lastScrollHeight = messagesEl.scrollHeight; + function parseMultiVcdPaths(toolResult) { + if (!toolResult) return []; + const result = String(toolResult); + const vcdListMatch = result.match(/VCD 文件列表:[\\s\\S]*?(?=\\n\\n|$)/); + if (!vcdListMatch) return []; + const paths = []; + const lineRegex = /- (\\w+): ([^\\n]+)/g; + let match; + while ((match = lineRegex.exec(vcdListMatch[0])) !== null) { + const name = match[1]; + const pathOrError = match[2].trim(); + if (!pathOrError.startsWith('失败')) { + paths.push({ name: name + '.vcd', path: pathOrError }); } } - - // 添加消息 - function addMessage(text, sender) { - const div = document.createElement('div'); - div.className = \`message \${sender}-message\`; - - if (sender === 'bot') { - // 创建消息内容 - const messageContent = document.createElement('div'); - messageContent.textContent = text; - div.appendChild(messageContent); - - // 创建操作按钮容器 - const actionsDiv = document.createElement('div'); - actionsDiv.className = 'message-actions'; - - // 复制按钮 - const copyBtn = document.createElement('button'); - copyBtn.className = 'action-btn'; - copyBtn.innerHTML = \`复制\`; - copyBtn.onclick = () => copyMessage(text, copyBtn); - - // 点赞按钮 - const likeBtn = document.createElement('button'); - likeBtn.className = 'action-btn'; - likeBtn.innerHTML = \`点赞\`; - likeBtn.onclick = () => toggleLike(likeBtn); - - // 点踩按钮 - const dislikeBtn = document.createElement('button'); - dislikeBtn.className = 'action-btn'; - dislikeBtn.innerHTML = \`点踩\`; - dislikeBtn.onclick = () => toggleDislike(dislikeBtn); - - actionsDiv.appendChild(copyBtn); - actionsDiv.appendChild(likeBtn); - actionsDiv.appendChild(dislikeBtn); - - div.appendChild(actionsDiv); - } else { - // 用户消息:解析文件路径并转换为标签 - const parts = text.split(' '); - const filePaths = []; - const textParts = []; - - parts.forEach(part => { - // 判断是否为文件路径或代码片段:包含路径分隔符、文件扩展名或代码片段格式(文件名:行号-行号) - if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part) || /:[0-9]+-[0-9]+$/.test(part)) { - filePaths.push(part); - } else { - textParts.push(part); - } - }); - - if (filePaths.length > 0) { - div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' '); - } else { - div.textContent = text; - } - - // 当添加用户消息时,隐藏 header - hideHeaderIfNeeded(); - } - - messagesEl.appendChild(div); - smartScrollToBottom(); - - // 添加消息后检查 header 显示状态 - checkHeaderVisibility(); - } - - // 检查是否需要隐藏 header - function hideHeaderIfNeeded() { - checkHeaderVisibility(); - } - - // 复制消息 - function copyMessage(text, button) { - navigator.clipboard.writeText(text).then(() => { - const originalHTML = button.innerHTML; - button.innerHTML = \`\`; - setTimeout(() => { - button.innerHTML = originalHTML; - }, 2000); - }); - } - - // 点赞 - function toggleLike(button) { - const isActive = button.classList.contains('active'); - // 移除所有同级按钮的 active 状态 - const parent = button.parentElement; - parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active')); - - if (!isActive) { - button.classList.add('active'); - } - } - - // 点踩 - function toggleDislike(button) { - const isActive = button.classList.contains('active'); - // 移除所有同级按钮的 active 状态 - const parent = button.parentElement; - parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active')); - - if (!isActive) { - button.classList.add('active'); - } - } - - // 更新或创建流式消息 - function updateOrCreateStreamingMessage(text) { - hideLoadingIndicator(); - - if (!currentStreamingMessage) { - // 创建新的流式消息元素 - const div = document.createElement('div'); - div.className = 'message bot-message streaming'; - - const messageContent = document.createElement('div'); - messageContent.className = 'message-content'; - messageContent.textContent = text; - div.appendChild(messageContent); - - messagesEl.appendChild(div); - currentStreamingMessage = div; - } else { - // 更新现有消息内容 - const messageContent = currentStreamingMessage.querySelector('.message-content'); - if (messageContent) { - messageContent.textContent = text; - } - } - - // 智能滚动到底部 - smartScrollToBottom(); - } - - // 完成流式消息 - function finalizeStreamingMessage(finalText) { - if (currentStreamingMessage) { - const messageContent = currentStreamingMessage.querySelector('.message-content'); - if (messageContent) { - messageContent.textContent = finalText; - } - currentStreamingMessage.classList.remove('streaming'); - - // 添加操作按钮 - const actionsDiv = document.createElement('div'); - actionsDiv.className = 'message-actions'; - - const copyBtn = document.createElement('button'); - copyBtn.className = 'action-btn'; - copyBtn.innerHTML = ''; - copyBtn.onclick = () => copyMessage(finalText, copyBtn); - actionsDiv.appendChild(copyBtn); - - currentStreamingMessage.appendChild(actionsDiv); - currentStreamingMessage = null; - } - - smartScrollToBottom(); - } - - // 显示加载指示器 - function showLoadingIndicator(text) { - hideLoadingIndicator(); - - loadingIndicator = document.createElement('div'); - loadingIndicator.className = 'message bot-message loading-message'; - loadingIndicator.innerHTML = \` -
- -
- \${text} - \`; - messagesEl.appendChild(loadingIndicator); - smartScrollToBottom(); - } - - // 隐藏加载指示器 - function hideLoadingIndicator() { - if (loadingIndicator) { - loadingIndicator.remove(); - loadingIndicator = null; - } - } - - // 存储已回答问题的状态 - const answeredQuestions = new Map(); // askId -> answer - - // 存储工具展开/折叠状态 - const toolCollapseStates = new Map(); // index -> isCollapsed - - // 实时更新分段消息(按后端返回顺序) - function updateSegmentsRealtime(segments, isComplete) { - // 如果对话完成且没有新段落,只重置容器 - if (isComplete && (!segments || segments.length === 0)) { - currentSegmentedMessage = null; - return; - } - - if (!segments || segments.length === 0) { - return; - } - - // 如果没有当前分段消息容器,创建一个 - if (!currentSegmentedMessage) { - // 移除流式消息(如果有) - if (currentStreamingMessage) { - currentStreamingMessage.remove(); - currentStreamingMessage = null; - } - - // 移除所有工具状态消息(因为会在分段中显示) - const toolStatuses = messagesEl.querySelectorAll('.tool-status'); - toolStatuses.forEach(el => { - el.remove(); - }); - - // 检查最后一个容器是否是未完成的对话(没有操作按钮) - const lastSegmented = messagesEl.querySelector('.segmented-message:last-child'); - if (lastSegmented && !lastSegmented.querySelector('.message-actions')) { - // 复用未完成的容器 - currentSegmentedMessage = lastSegmented; - } else { - // 创建新容器 - currentSegmentedMessage = document.createElement('div'); - currentSegmentedMessage.className = 'message bot-message segmented-message'; - messagesEl.appendChild(currentSegmentedMessage); - } - renderedSegmentCount = 0; - } - - // 保存当前所有工具的展开/折叠状态 - if (currentSegmentedMessage) { - const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]'); - toolHeaders.forEach((header, idx) => { - const isCollapsed = header.classList.contains('collapsed'); - toolCollapseStates.set(idx, isCollapsed); - }); - } - - // 清空容器并重新渲染所有段落 - currentSegmentedMessage.innerHTML = ''; - - // 合并连续相同的工具调用 - const mergedSegments = []; - let i = 0; - while (i < segments.length) { - const segment = segments[i]; - if (segment.type === 'tool') { - // 统计连续相同的工具调用 - let count = 1; - while (i + count < segments.length && - segments[i + count].type === 'tool' && - segments[i + count].toolName === segment.toolName) { - count++; - } - // 添加合并后的段落(带计数) - mergedSegments.push({ ...segment, toolCount: count }); - i += count; - } else { - mergedSegments.push(segment); - i++; - } - } - - let toolIndex = 0; // 用于跟踪工具段落的索引 - mergedSegments.forEach((segment, index) => { - const segmentDiv = document.createElement('div'); - segmentDiv.className = 'message-segment segment-' + segment.type; - - if (segment.type === 'text' && segment.content) { - segmentDiv.className += ' segment-text'; - segmentDiv.innerHTML = formatText(segment.content); - } else if (segment.type === 'tool') { - // 过滤掉不需要显示的工具 - if (segment.toolName === 'spawnExplorer') { - return; - } - - // 所有工具调用都使用低调样式 - segmentDiv.className += ' low-profile'; - - const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; - const toolResult = segment.toolResult || ''; - const toolCount = segment.toolCount || 1; - const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : ''; - const toolDescription = segment.toolDescription || ''; - - // 检查工具结果是否过长(超过一行显示不下) - const shouldCollapse = toolResult && toolResult.length > 60; - - // 恢复之前保存的展开/折叠状态 - const savedState = toolCollapseStates.get(toolIndex); - const isCollapsed = savedState !== undefined ? savedState : shouldCollapse; - const currentToolIndex = toolIndex; - toolIndex++; // 递增工具索引 - - segmentDiv.innerHTML = \` -
- \${shouldCollapse ? \`\${collapseIconSvg}\` : getToolIcon(segment.toolName)} - \${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix} - \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''} -
- \${shouldCollapse ? \`
\${toolResult}
\` : ''} - \${toolDescription ? \`

\${toolDescription}

\` : ''} - \`; - - // 如果是仿真工具且成功完成,尝试添加波形预览 - if (segment.toolName === 'simulation' && segment.toolStatus === 'success') { - // 尝试解析多个 VCD 文件(多 VCD 模式) - const vcdPaths = parseMultiVcdPaths(segment.toolResult); - - if (vcdPaths.length > 0) { - // 多 VCD 模式:为每个文件创建预览 - vcdPaths.forEach(vcdInfo => { - const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name); - segmentDiv.appendChild(waveformPreview); - }); - } else { - // 单 VCD 模式(兼容旧逻辑) - let vcdPath = segment.vcdFilePath; - if (!vcdPath && segment.toolResult) { - const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/); - if (match && match[1]) { - vcdPath = match[1].trim(); - } - } - - if (vcdPath) { - const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd'; - const waveformPreview = createWaveformPreview(vcdPath, fileName); - segmentDiv.appendChild(waveformPreview); - } - } - } - - // 添加折叠/展开事件监听 - if (shouldCollapse) { - setTimeout(() => { - const header = segmentDiv.querySelector('.tool-segment-header'); - const content = segmentDiv.querySelector('.tool-segment-content'); - - if (header && content) { - header.addEventListener('click', function() { - const isCollapsed = header.classList.contains('collapsed'); - const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0'); - - if (isCollapsed) { - // 展开 - header.classList.remove('collapsed'); - content.classList.remove('collapsed'); - content.style.maxHeight = content.scrollHeight + 'px'; - toolCollapseStates.set(toolIdx, false); - } else { - // 折叠 - header.classList.add('collapsed'); - content.classList.add('collapsed'); - content.style.maxHeight = '0'; - toolCollapseStates.set(toolIdx, true); - } - }); - } - }, 0); - } - } else if (segment.type === 'question') { - segmentDiv.className += ' segment-question'; - - // 兼容旧格式:如果有 segment.question,转换为 questions 数组 - const questions = segment.questions || (segment.question ? [{ - question: segment.question, - options: segment.options || [], - multiSelect: false - }] : []); - - // 检查是否已回答 - const isAnswered = answeredQuestions.has(segment.askId); - const savedAnswers = answeredQuestions.get(segment.askId) || {}; - - if (isAnswered) { - segmentDiv.classList.add('answered'); - } - - // 渲染多个问题 - const questionsHtml = questions.map((q, qIndex) => { - const inputType = q.multiSelect ? 'checkbox' : 'radio'; - const inputName = \`q\${qIndex}\`; - const selectedAnswers = savedAnswers[qIndex] || []; - - // 如果没有选项,显示文本输入框 - let optionsHtml; - if (!q.options || q.options.length === 0) { - const savedText = selectedAnswers[0] || ''; - optionsHtml = \`\`; - } else { - optionsHtml = q.options.map(opt => { - const isSelected = selectedAnswers.includes(opt); - return \`\`; - }).join(''); - } - - return \` -
-
\${formatText(q.question)}
-
\${optionsHtml}
-
- \`; - }).join(''); - - segmentDiv.innerHTML = \` - \${questionsHtml} - - \`; - - // 只在未回答时添加事件监听 - if (!isAnswered) { - setTimeout(() => { - const submitBtn = segmentDiv.querySelector('.custom-submit'); - if (submitBtn) { - submitBtn.addEventListener('click', function() { - const answers = {}; - questions.forEach((q, qIndex) => { - // 检查是否是文本输入框 - const textarea = segmentDiv.querySelector(\`textarea[name="q\${qIndex}"]\`); - if (textarea) { - const value = textarea.value.trim(); - answers[qIndex] = value ? [value] : []; - } else { - const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`); - answers[qIndex] = Array.from(inputs).map(input => input.value); - } - }); - handleMultiQuestionAnswer(segment.askId, answers, segmentDiv); - }); - } - }, 0); - } - } else if (segment.type === 'agent') { - // 智能体卡片渲染 - renderAgentCard(segment, segmentDiv); - } else if (segment.type === 'plan') { - // 计划卡片渲染(使用独立组件) - renderPlanCardInSegment(segment, segmentDiv, answeredQuestions); - } - - currentSegmentedMessage.appendChild(segmentDiv); - }); - - // 如果对话完成,添加操作按钮并重置容器 - if (isComplete) { - console.log('[WebView] 对话完成,添加操作按钮'); - const actionsDiv = document.createElement('div'); - actionsDiv.className = 'message-actions'; - - // 复制按钮 - const copyBtn = document.createElement('button'); - copyBtn.className = 'action-btn'; - copyBtn.innerHTML = \`复制\`; - copyBtn.onclick = () => { - const textContent = segments - .filter(s => s.type === 'text' && s.content) - .map(s => s.content) - .join('\\n'); - copyMessage(textContent, copyBtn); - }; - - // 点赞按钮 - const likeBtn = document.createElement('button'); - likeBtn.className = 'action-btn'; - likeBtn.innerHTML = \`点赞\`; - likeBtn.onclick = () => toggleLike(likeBtn); - - // 点踩按钮 - const dislikeBtn = document.createElement('button'); - dislikeBtn.className = 'action-btn'; - dislikeBtn.innerHTML = \`点踩\`; - dislikeBtn.onclick = () => toggleDislike(dislikeBtn); - - actionsDiv.appendChild(copyBtn); - actionsDiv.appendChild(likeBtn); - actionsDiv.appendChild(dislikeBtn); - currentSegmentedMessage.appendChild(actionsDiv); - - // 重置当前分段消息容器(继续对话时创建新容器) - currentSegmentedMessage = null; - } - - // 智能滚动到底部 - smartScrollToBottom(); - } - - // 渲染分段消息(兼容旧代码) - function renderSegments(segments) { - console.log('[WebView] renderSegments 被调用, segments:', segments); - if (!segments || segments.length === 0) { - console.log('[WebView] segments 为空,跳过渲染'); - return; - } - - // 移除流式消息(如果有) - if (currentStreamingMessage) { - console.log('[WebView] 移除流式消息'); - currentStreamingMessage.remove(); - currentStreamingMessage = null; - } - - // 移除所有工具状态消息(因为会在分段中显示) - const toolStatuses = messagesEl.querySelectorAll('.tool-status'); - console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length); - toolStatuses.forEach(el => { - console.log('[WebView] 移除工具状态消息:', el.className); - el.remove(); - }); - - // 创建消息容器 - const container = document.createElement('div'); - container.className = 'message bot-message segmented-message'; - - // 合并连续相同的工具调用 - const mergedSegments = []; - let i = 0; - while (i < segments.length) { - const segment = segments[i]; - if (segment.type === 'tool') { - // 统计连续相同的工具调用 - let count = 1; - while (i + count < segments.length && - segments[i + count].type === 'tool' && - segments[i + count].toolName === segment.toolName) { - count++; - } - // 添加合并后的段落(带计数) - mergedSegments.push({ ...segment, toolCount: count }); - i += count; - } else { - mergedSegments.push(segment); - i++; - } - } - - mergedSegments.forEach((segment, index) => { - const segmentDiv = document.createElement('div'); - segmentDiv.className = 'message-segment segment-' + segment.type; - - if (segment.type === 'text' && segment.content) { - segmentDiv.className += ' segment-text'; - segmentDiv.innerHTML = formatText(segment.content); - } else if (segment.type === 'tool') { - // 过滤掉不需要显示的工具 - if (segment.toolName === 'spawnExplorer') { - return; - } - - // 所有工具调用都使用低调样式 - segmentDiv.className += ' low-profile'; - - const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; - const toolResult = segment.toolResult || ''; - const toolCount = segment.toolCount || 1; - const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : ''; - const toolDescription = segment.toolDescription || ''; - - // 检查工具结果是否过长(超过一行显示不下) - const shouldCollapse = toolResult && toolResult.length > 60; - - segmentDiv.innerHTML = \` -
- \${shouldCollapse ? \`\${collapseIconSvg}\` : getToolIcon(segment.toolName)} - \${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix} - \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''} -
- \${shouldCollapse ? \`\` : ''} - \${toolDescription ? \`

\${toolDescription}

\` : ''} - \`; - - // 如果是仿真工具且成功完成,尝试添加波形预览 - if (segment.toolName === 'simulation' && segment.toolStatus === 'success') { - // 尝试解析多个 VCD 文件(多 VCD 模式) - const vcdPaths = parseMultiVcdPaths(segment.toolResult); - - if (vcdPaths.length > 0) { - // 多 VCD 模式:为每个文件创建预览 - vcdPaths.forEach(vcdInfo => { - const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name); - segmentDiv.appendChild(waveformPreview); - }); - } else { - // 单 VCD 模式(兼容旧逻辑) - let vcdPath = segment.vcdFilePath; - if (!vcdPath && segment.toolResult) { - const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/); - if (match && match[1]) { - vcdPath = match[1].trim(); - } - } - - if (vcdPath) { - const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd'; - const waveformPreview = createWaveformPreview(vcdPath, fileName); - segmentDiv.appendChild(waveformPreview); - } - } - } - - // 添加折叠/展开事件监听 - if (shouldCollapse) { - setTimeout(() => { - const header = segmentDiv.querySelector('.tool-segment-header'); - const content = segmentDiv.querySelector('.tool-segment-content'); - - if (header && content) { - header.addEventListener('click', function() { - const isCollapsed = header.classList.contains('collapsed'); - const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0'); - - if (isCollapsed) { - // 展开 - header.classList.remove('collapsed'); - content.classList.remove('collapsed'); - content.style.maxHeight = content.scrollHeight + 'px'; - toolCollapseStates.set(toolIdx, false); - } else { - // 折叠 - header.classList.add('collapsed'); - content.classList.add('collapsed'); - content.style.maxHeight = '0'; - toolCollapseStates.set(toolIdx, true); - } - }); - } - }, 0); - } - } else if (segment.type === 'question') { - segmentDiv.innerHTML = \` -
-
\${formatText(segment.question || '')}
-
- \${(segment.options || []).map(opt => \`\${opt}\`).join('')} -
-
- \`; - } else if (segment.type === 'agent') { - // 智能体卡片渲染 - renderAgentCard(segment, segmentDiv); - } else if (segment.type === 'plan') { - // 计划卡片渲染(使用独立组件) - renderPlanCardStatic(segment, segmentDiv); - } - - container.appendChild(segmentDiv); - }); - - // 添加操作按钮 - const actionsDiv = document.createElement('div'); - actionsDiv.className = 'message-actions'; - - // 复制按钮 - const copyBtn = document.createElement('button'); - copyBtn.className = 'action-btn'; - copyBtn.innerHTML = \`复制\`; - copyBtn.onclick = () => { - const textContent = segments - .filter(s => s.type === 'text' && s.content) - .map(s => s.content) - .join('\\n'); - copyMessage(textContent, copyBtn); - }; - - // 点赞按钮 - const likeBtn = document.createElement('button'); - likeBtn.className = 'action-btn'; - likeBtn.innerHTML = \`点赞\`; - likeBtn.onclick = () => toggleLike(likeBtn); - - // 点踩按钮 - const dislikeBtn = document.createElement('button'); - dislikeBtn.className = 'action-btn'; - dislikeBtn.innerHTML = \`点踩\`; - dislikeBtn.onclick = () => toggleDislike(dislikeBtn); - - actionsDiv.appendChild(copyBtn); - actionsDiv.appendChild(likeBtn); - actionsDiv.appendChild(dislikeBtn); - container.appendChild(actionsDiv); - - messagesEl.appendChild(container); - smartScrollToBottom(); - } - - // 格式化文本(支持 Markdown) - function formatText(text) { - if (!text) return ''; - - let html = text; - - // 先提取并处理代码块(避免被转义) - const codeBlocks = []; - html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) { - const language = lang || 'plaintext'; - // 转义代码内容 - const escapedCode = code.trim() - .replace(/&/g, '&') - .replace(//g, '>'); - // 不再手动高亮,让 highlight.js 处理 - const placeholder = \`___CODE_BLOCK_\${codeBlocks.length}___\`; - codeBlocks.push('
' + escapedCode + '
'); - return placeholder; - }); - - // 提取行内代码(避免被转义) - const inlineCodes = []; - html = html.replace(/\`([^\`]+)\`/g, function(match, code) { - const escapedCode = code - .replace(/&/g, '&') - .replace(//g, '>'); - const placeholder = \`___INLINE_CODE_\${inlineCodes.length}___\`; - inlineCodes.push('' + escapedCode + ''); - return placeholder; - }); - - // 转义其他 HTML 特殊字符 - html = html + return paths; + } + + function formatText(text) { + if (!text) return ''; + let html = text; + const codeBlocks = []; + html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) { + const language = lang || 'plaintext'; + const escapedCode = code.trim() .replace(/&/g, '&') .replace(//g, '>'); - - // 处理标题 ### Title - html = html.replace(/^### (.+)$/gm, '

$1

'); - html = html.replace(/^## (.+)$/gm, '

$1

'); - html = html.replace(/^# (.+)$/gm, '

$1

'); - - // 处理粗体 **text** - html = html.replace(/\\*\\*(.+?)\\*\\*/g, '$1'); - - // 处理斜体 *text* - html = html.replace(/\\*(.+?)\\*/g, '$1'); - - // 处理无序列表 - item 或 * item - html = html.replace(/^[\\-\\*] (.+)$/gm, '
  • $1
  • '); - html = html.replace(/(
  • .*<\\/li>\\n?)+/g, ''); - - // 处理有序列表 1. item - html = html.replace(/^\\d+\\. (.+)$/gm, '
  • $1
  • '); - - // 处理链接 [text](url) - html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '$1'); - - // 处理换行(在恢复代码块之前) - html = html.replace(/\\n/g, '
    '); - - // 恢复代码块(在最后恢复,避免被其他处理影响) - codeBlocks.forEach((block, index) => { - html = html.replace(\`___CODE_BLOCK_\${index}___\`, block); - }); - - // 恢复行内代码 - inlineCodes.forEach((code, index) => { - html = html.replace(\`___INLINE_CODE_\${index}___\`, code); - }); - - return html; - } - - // 添加工具状态消息 - function addToolStatus(toolName, status, detail) { - const statusIcons = { - start: '🔧', - complete: '✅', - error: '❌' - }; - const statusTexts = { - start: '正在执行', - complete: '执行完成', - error: '执行失败' - }; - - const div = document.createElement('div'); - div.className = \`message tool-status tool-\${status}\`; - div.innerHTML = \` - \${statusIcons[status]} - \${getToolDisplayName(toolName)} - \${statusTexts[status]} - \${detail ? \`
    \${detail}
    \` : ''} - \`; - messagesEl.appendChild(div); - smartScrollToBottom(); - - // 添加消息后检查 header 显示状态 - checkHeaderVisibility(); + const placeholder = \`___CODE_BLOCK_\${codeBlocks.length}___\`; + codeBlocks.push('
    ' + escapedCode + '
    '); + return placeholder; + }); + const inlineCodes = []; + html = html.replace(/\`([^\`]+)\`/g, function(match, code) { + const escapedCode = code + .replace(/&/g, '&') + .replace(//g, '>'); + const placeholder = \`___INLINE_CODE_\${inlineCodes.length}___\`; + inlineCodes.push('' + escapedCode + ''); + return placeholder; + }); + html = html + .replace(/&/g, '&') + .replace(//g, '>'); + html = html.replace(/^### (.+)$/gm, '

    $1

    '); + html = html.replace(/^## (.+)$/gm, '

    $1

    '); + html = html.replace(/^# (.+)$/gm, '

    $1

    '); + html = html.replace(/\\*\\*(.+?)\\*\\*/g, '$1'); + html = html.replace(/\\*(.+?)\\*/g, '$1'); + html = html.replace(/^[\\-\\*] (.+)$/gm, '
  • $1
  • '); + html = html.replace(/(
  • .*<\\/li>\\n?)+/g, ''); + html = html.replace(/^\\d+\\. (.+)$/gm, '
  • $1
  • '); + html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '$1'); + html = html.replace(/\\n/g, '
    '); + codeBlocks.forEach((block, index) => { + html = html.replace(\`___CODE_BLOCK_\${index}___\`, block); + }); + inlineCodes.forEach((code, index) => { + html = html.replace(\`___INLINE_CODE_\${index}___\`, code); + }); + return html; } - // 显示用户问题 - function showQuestion(askId, question, options) { - console.log('[WebView] showQuestion 被调用:', askId, question, options); - - // 创建问题消息容器 - const div = document.createElement('div'); - div.className = 'message bot-message question-message'; - div.setAttribute('data-ask-id', askId); - - // 问题文本 - const questionText = document.createElement('div'); - questionText.className = 'question-text'; - questionText.textContent = question; - div.appendChild(questionText); - - // 选项容器 - const optionsContainer = document.createElement('div'); - optionsContainer.className = 'question-options'; - - // 添加选项按钮 - options.forEach((option, index) => { - const optionBtn = document.createElement('button'); - optionBtn.className = 'question-option'; - optionBtn.textContent = option; - optionBtn.onclick = () => handleQuestionAnswer(askId, option, div); - optionsContainer.appendChild(optionBtn); - }); - - div.appendChild(optionsContainer); - - // 添加自定义输入("其他"选项) - const customContainer = document.createElement('div'); - customContainer.className = 'custom-input-container'; - - const customInput = document.createElement('input'); - customInput.type = 'text'; - customInput.className = 'custom-input'; - customInput.placeholder = '输入其他答案...'; - - const customSubmit = document.createElement('button'); - customSubmit.className = 'custom-submit'; - customSubmit.textContent = '提交'; - customSubmit.onclick = () => { - const customValue = customInput.value.trim(); - if (customValue) { - handleQuestionAnswer(askId, customValue, div); - } - }; - - customContainer.appendChild(customInput); - customContainer.appendChild(customSubmit); - div.appendChild(customContainer); - - messagesEl.appendChild(div); - smartScrollToBottom(); - - // 添加消息后检查 header 显示状态 - checkHeaderVisibility(); - } - - // 处理问题回答 - function handleQuestionAnswer(askId, answer, questionDiv) { - console.log('[WebView] 用户选择答案:', askId, answer); - - // 标记问题已回答 - questionDiv.classList.add('answered'); - - // 高亮选中的选项 - const options = questionDiv.querySelectorAll('.question-option'); - options.forEach(opt => { - if (opt.textContent === answer) { - opt.classList.add('selected'); - } - }); - - // 发送答案到后端 - vscode.postMessage({ - command: 'submitAnswer', - askId: askId, - selected: [answer], - customInput: answer - }); - } - - // 处理段落中的问题回答 - function handleQuestionAnswerInSegment(askId, answer, segmentDiv) { - console.log('[WebView] 段落中用户选择答案:', askId, answer); - - // 保存答案到 Map 中 - answeredQuestions.set(askId, answer); - - // 标记问题已回答 - segmentDiv.classList.add('answered'); - - // 高亮选中的选项 - const options = segmentDiv.querySelectorAll('.question-option'); - options.forEach(opt => { - if (opt.getAttribute('data-option') === answer) { - opt.classList.add('selected'); - } - }); - - // 隐藏自定义输入 - const customContainer = segmentDiv.querySelector('.custom-input-container'); - if (customContainer) { - customContainer.style.display = 'none'; - } - - // 发送答案到后端 - vscode.postMessage({ - command: 'submitAnswer', - askId: askId, - selected: [answer], - customInput: answer - }); - } - - // 处理多问题答案提交 - function handleMultiQuestionAnswer(askId, answers, segmentDiv) { - console.log('[WebView] 多问题答案提交:', askId, answers); - - // 保存答案到 Map 中 - answeredQuestions.set(askId, answers); - - // 标记问题已回答 - segmentDiv.classList.add('answered'); - - // 禁用所有输入并保持选中状态的高亮 - const inputs = segmentDiv.querySelectorAll('input'); - inputs.forEach(input => { - input.disabled = true; - // 确保选中的选项保持高亮 - if (input.checked) { - const label = input.closest('.question-option'); - if (label) { - label.classList.add('selected'); - } - } - }); - - // 隐藏提交按钮 - const submitBtn = segmentDiv.querySelector('.custom-submit'); - if (submitBtn) { - submitBtn.style.display = 'none'; - } - - // 发送答案到后端 - vscode.postMessage({ - command: 'submitAnswer', - askId: askId, - answers: answers - }); - } - - ${getWaveformPreviewScript()} - - ${getCodeHighlightScript()} + ${getQuestionHandlerScript()} + ${getMessageRendererScript()} + ${getSegmentRendererScript()} `; } diff --git a/src/views/messageRenderer.ts b/src/views/messageRenderer.ts new file mode 100644 index 0000000..277b707 --- /dev/null +++ b/src/views/messageRenderer.ts @@ -0,0 +1,215 @@ +/** + * 消息渲染脚本模块 + * 功能:消息渲染、滚动控制、工具状态显示 + * 依赖:toolHelpers, textFormatter, waveformPreviewContent, agentCard, planCard, codeHighlight + * 使用场景:webview 中的消息显示逻辑 + */ + +import { collapseIconSvg } from "../constants/toolIcons"; +import { getWaveformPreviewScript } from "./waveformPreviewContent"; +import { getAgentCardScript } from "./agentCard"; +import { getPlanCardScript } from "./planCard"; +import { getCodeHighlightScript } from "../components/codeHighlight"; + +export function getMessageRendererScript(): string { + return ` + ${getAgentCardScript()} + ${getPlanCardScript()} + + const toolCollapseStates = new Map(); + let shouldAutoScroll = true; + let lastScrollHeight = 0; + + function isUserNearBottom() { + const threshold = 50; + return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold; + } + + messagesEl.addEventListener('scroll', () => { + const isAtBottom = isUserNearBottom(); + if (isAtBottom) { + shouldAutoScroll = true; + } else { + if (messagesEl.scrollHeight === lastScrollHeight) { + shouldAutoScroll = false; + } + } + lastScrollHeight = messagesEl.scrollHeight; + }); + + function smartScrollToBottom() { + if (shouldAutoScroll) { + messagesEl.scrollTop = messagesEl.scrollHeight; + lastScrollHeight = messagesEl.scrollHeight; + } + } + + function addMessage(text, sender) { + const div = document.createElement('div'); + div.className = \`message \${sender}-message\`; + if (sender === 'bot') { + const messageContent = document.createElement('div'); + messageContent.textContent = text; + div.appendChild(messageContent); + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'message-actions'; + const copyBtn = document.createElement('button'); + copyBtn.className = 'action-btn'; + copyBtn.innerHTML = \`复制\`; + copyBtn.onclick = () => copyMessage(text, copyBtn); + const likeBtn = document.createElement('button'); + likeBtn.className = 'action-btn'; + likeBtn.innerHTML = \`点赞\`; + likeBtn.onclick = () => toggleLike(likeBtn); + const dislikeBtn = document.createElement('button'); + dislikeBtn.className = 'action-btn'; + dislikeBtn.innerHTML = \`点踩\`; + dislikeBtn.onclick = () => toggleDislike(dislikeBtn); + actionsDiv.appendChild(copyBtn); + actionsDiv.appendChild(likeBtn); + actionsDiv.appendChild(dislikeBtn); + div.appendChild(actionsDiv); + } else { + const parts = text.split(' '); + const filePaths = []; + const textParts = []; + parts.forEach(part => { + if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part) || /:[0-9]+-[0-9]+$/.test(part)) { + filePaths.push(part); + } else { + textParts.push(part); + } + }); + if (filePaths.length > 0) { + div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' '); + } else { + div.textContent = text; + } + hideHeaderIfNeeded(); + } + messagesEl.appendChild(div); + smartScrollToBottom(); + checkHeaderVisibility(); + } + + function hideHeaderIfNeeded() { + checkHeaderVisibility(); + } + + function copyMessage(text, button) { + navigator.clipboard.writeText(text).then(() => { + const originalHTML = button.innerHTML; + button.innerHTML = \`\`; + setTimeout(() => { + button.innerHTML = originalHTML; + }, 2000); + }); + } + + function toggleLike(button) { + const isActive = button.classList.contains('active'); + const parent = button.parentElement; + parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active')); + if (!isActive) { + button.classList.add('active'); + } + } + + function toggleDislike(button) { + const isActive = button.classList.contains('active'); + const parent = button.parentElement; + parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active')); + if (!isActive) { + button.classList.add('active'); + } + } + + function updateOrCreateStreamingMessage(text) { + hideLoadingIndicator(); + if (!currentStreamingMessage) { + const div = document.createElement('div'); + div.className = 'message bot-message streaming'; + const messageContent = document.createElement('div'); + messageContent.className = 'message-content'; + messageContent.textContent = text; + div.appendChild(messageContent); + messagesEl.appendChild(div); + currentStreamingMessage = div; + } else { + const messageContent = currentStreamingMessage.querySelector('.message-content'); + if (messageContent) { + messageContent.textContent = text; + } + } + smartScrollToBottom(); + } + + function finalizeStreamingMessage(finalText) { + if (currentStreamingMessage) { + const messageContent = currentStreamingMessage.querySelector('.message-content'); + if (messageContent) { + messageContent.textContent = finalText; + } + currentStreamingMessage.classList.remove('streaming'); + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'message-actions'; + const copyBtn = document.createElement('button'); + copyBtn.className = 'action-btn'; + copyBtn.innerHTML = ''; + copyBtn.onclick = () => copyMessage(finalText, copyBtn); + actionsDiv.appendChild(copyBtn); + currentStreamingMessage.appendChild(actionsDiv); + currentStreamingMessage = null; + } + smartScrollToBottom(); + } + + function showLoadingIndicator(text) { + hideLoadingIndicator(); + loadingIndicator = document.createElement('div'); + loadingIndicator.className = 'message bot-message loading-message'; + loadingIndicator.innerHTML = \` +
    + +
    + \${text} + \`; + messagesEl.appendChild(loadingIndicator); + smartScrollToBottom(); + } + + function hideLoadingIndicator() { + if (loadingIndicator) { + loadingIndicator.remove(); + loadingIndicator = null; + } + } + + function addToolStatus(toolName, status, detail) { + const statusIcons = { + start: '🔧', + complete: '✅', + error: '❌' + }; + const statusTexts = { + start: '正在执行', + complete: '执行完成', + error: '执行失败' + }; + const div = document.createElement('div'); + div.className = \`message tool-status tool-\${status}\`; + div.innerHTML = \` + \${statusIcons[status]} + \${getToolDisplayName(toolName)} + \${statusTexts[status]} + \${detail ? \`
    \${detail}
    \` : ''} + \`; + messagesEl.appendChild(div); + smartScrollToBottom(); + checkHeaderVisibility(); + } + + ${getWaveformPreviewScript()} + ${getCodeHighlightScript()} + `; +} diff --git a/src/views/messageStyles.ts b/src/views/messageStyles.ts new file mode 100644 index 0000000..2e75afa --- /dev/null +++ b/src/views/messageStyles.ts @@ -0,0 +1,622 @@ +/** + * 消息样式模块 + * 功能:提供消息区域的所有 CSS 样式 + * 依赖:agentCard, planCard, codeHighlight, waveformPreviewContent + * 使用场景:webview 样式注入 + */ + +import { getAgentCardStyles } from "./agentCard"; +import { getPlanCardStyles } from "./planCard"; +import { getCodeHighlightStyles } from "../components/codeHighlight"; +import { getWaveformPreviewContent } from "./waveformPreviewContent"; + +export function getMessageAreaStyles(): string { + return ` + .messages { + flex: 1; + overflow-y: auto; + margin-bottom: 15px; + min-height: 0; + } + .message { + margin-bottom: 12px; + } + .user-message { + padding: 10px 15px; + border-radius: 8px; + background: var(--vscode-button-secondaryBackground); + border: 1px solid var(--vscode-input-border); + margin-left: auto; + width: fit-content; + max-width: 80%; + } + .bot-message { + padding: 0; + text-align: left; + color: var(--vscode-foreground); + max-width: 100%; + position: relative; + } + .message-actions { + display: flex; + gap: 8px; + margin-top: 12px; + margin-left: 10px; + opacity: 0.85; + transition: opacity 0.2s ease; + } + .message-actions:hover { + opacity: 1; + } + .action-btn { + background: transparent; + border: none; + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + color: var(--vscode-foreground); + opacity: 0.9; + transition: opacity 0.2s ease; + position: relative; + } + .action-btn:hover { + opacity: 1; + } + .action-btn svg { + width: 14px; + height: 14px; + } + .action-btn.active { + color: var(--vscode-button-background); + opacity: 1; + } + .action-btn .action-tooltip { + visibility: hidden; + width: auto; + background: #1e1e1e; + color: #ffffff; + text-align: center; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 4px 8px; + position: absolute; + z-index: 1000; + bottom: 125%; + left: 50%; + transform: translateX(-50%) translateY(5px); + opacity: 0; + transition: all 0.2s ease; + font-size: 12px;white-space: nowrap;pointer-events: none; + } + .action-btn .action-tooltip::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #1e1e1e transparent transparent transparent; + } + .action-btn .action-tooltip::before { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -6px; + border-width: 6px; + border-style: solid; + border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent; + z-index: -1; + } + .action-btn:hover .action-tooltip { + visibility: visible; + opacity: 1; + transform: translateX(-50%) translateY(0); + } + + .streaming .message-content { + border-right: 2px solid var(--vscode-focusBorder); + animation: blink 1s infinite; + } + @keyframes blink { + 0%, 50% { border-color: var(--vscode-focusBorder); } + 51%, 100% { border-color: transparent; } + } + + .loading-message { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + color: var(--vscode-descriptionForeground); + } + .loading-dots { + display: flex; + gap: 4px;} + .loading-dots span { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--vscode-focusBorder); + animation: loadingDot 1.4s infinite ease-in-out; + } + .loading-dots span:nth-child(1) { animation-delay: 0s; } + .loading-dots span:nth-child(2) { animation-delay: 0.2s; } + .loading-dots span:nth-child(3) { animation-delay: 0.4s; } + @keyframes loadingDot { + 0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; } + 40% { transform: scale(1); opacity: 1; } + } + .loading-text { + font-size: 13px; + } + + .tool-status { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin: 4px 0; + font-size: 12px; + border-radius: 6px; + background: var(--vscode-textBlockQuote-background); + } + .tool-status.tool-start { + border-left: 3px solid var(--vscode-charts-blue); + } + .tool-status.tool-complete { + border-left: 3px solid var(--vscode-charts-green); + } + .tool-status.tool-error { + border-left: 3px solid var(--vscode-charts-red); + } + .tool-icon { + font-size: 14px; + } + .tool-name { + font-weight: 500; + color: var(--vscode-foreground); + } + .tool-status-text { + color: var(--vscode-descriptionForeground); + } + .tool-detail { + margin-top: 4px; + font-size: 11px; + color: var(--vscode-descriptionForeground); + white-space: pre-wrap; + max-height: 100px; + overflow-y: auto; + } + + .question-message { + padding: 16px; + } + .question-text { + margin-bottom: 12px; + font-weight: 500; + } + .question-options { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + .question-option { + padding: 8px 16px; + background: #007ACC; + color: #ffffff; + border: 1px solid #007ACC; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + } + .question-option:hover { + background: #005a9e; + border-color: #005a9e; + } + .question-option.selected { + background: #007ACC; + color: #ffffff; + border-color: #007ACC; + } + .question-message.answered .question-option:not(.selected) { + opacity: 0.5; + pointer-events: none; + } + .custom-input-container { + display: flex; + gap: 8px; + width: 100%; + margin-top: 8px; + } + .custom-input { + flex: 1; + padding: 8px 12px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border); + border-radius: 6px; + font-size: 13px; + } + .custom-submit { + padding: 8px 16px; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 6px; + cursor: pointer; + } + .custom-submit:hover { + background: var(--vscode-button-hoverBackground); + } + .question-message.answered .custom-input-container { + display: none; + } + + .segmented-message { + padding: 0; + } + .message-segment { + padding: 10px 0; + } + .segment-text { + line-height: 1.6; + } + + .segment-text h1, + .segment-text h2, + .segment-text h3, + .question-text h1, + .question-text h2, + .question-text h3 { + margin: 0px 0 -10px 0; + font-weight: 600; + line-height: 1.3; + } + .segment-text h1, + .question-text h1 { + font-size: 1.5em; + border-bottom: 1px solid var(--vscode-panel-border); + padding-bottom: 8px; + } + .segment-text h2, + .question-text h2 { + font-size: 1.3em; + } + .segment-text h3, + .question-text h3 { + font-size: 1.1em; + } + .segment-text ul, + .segment-text ol, + .question-text ul, + .question-text ol { + margin: 8px 0; + padding-left: 24px; + } + .segment-text li, + .question-text li { + line-height: 1; + } + .segment-text strong, + .question-text strong { + font-weight: 600; + color: var(--vscode-foreground); + } + .segment-text em, + .question-text em { + font-style: italic; + } + .segment-text a, + .question-text a { + color: var(--vscode-textLink-foreground); + text-decoration: none; + } + .segment-text a:hover, + .question-text a:hover { + text-decoration: underline; + } + .segment-text p, + .question-text p { + margin: 8px 0; + } + .segment-text code, + .question-text code { + background: var(--vscode-textCodeBlock-background); + padding: 2px 6px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family); + font-size: 0.9em; + } + + .segment-tool { + margin: 4px 0; + padding: 4px 0; + } + .segment-tool.low-profile { + margin: 25px 0px; + padding: 0; + background: none; + } + .tool-segment-header { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--vscode-descriptionForeground); + cursor: pointer; + } + .tool-segment-icon { + font-size: 12px; + } + .tool-segment-name { + font-weight: normal; + } + .tool-segment-result { + display: inline; + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-left: 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 500px; + } + .tool-collapse-icon { + width: 12px; + height: 12px; + flex-shrink: 0; + transition: transform 0.2s ease; + cursor: pointer; + } + .tool-collapse-icon svg { + width: 100%; + height: 100%; + display: block; + } + .icon-expanded svg path { + fill: #007ACC !important; + } + .tool-segment-header.collapsed .tool-collapse-icon { + transform: rotate(-90deg); + } + .tool-segment-header:not(.collapsed) .tool-collapse-icon { + transform: rotate(0deg); + } + .tool-file-write-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-file-write-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-file-read-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-file-read-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-file-delete-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-file-delete-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-syntax-check-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-syntax-check-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-search-code-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-search-code-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-save-knowledge-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-save-knowledge-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-simulation-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-simulation-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-waveform-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-waveform-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-knowledge-load-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-knowledge-load-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-state-transition-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-state-transition-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-segment-content { + overflow: hidden; + transition: max-height 0.3s ease; + } + .tool-segment-content.collapsed { + max-height: 0; + } + .tool-segment-description { + margin: 25px 0 0 0px; + font-size: 0.9rem; + color: var(--vscode-descriptionForeground); + line-height: 1.4; + } + .segment-tool.low-profile .tool-segment-header { + opacity: 0.65; + font-size: 12px; + } + .segment-tool.low-profile .tool-segment-icon { + opacity: 0.55; + font-size: 11px; + } + .segment-tool.low-profile .tool-segment-name { + font-weight: 300; + opacity: 0.8; + } + .segment-tool.low-profile .tool-segment-result { + opacity: 0.7; + font-size: 12px; + } + .segment-question { + background: var(--vscode-textBlockQuote-background); + border-radius: 6px; + margin: 8px 0; + padding: 12px 35px; + border-left: 3px solid var(--vscode-charts-orange); + } + .segment-question .question-text { + margin-bottom: 12px; + font-weight: 500; + } + .segment-question .question-options { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 8px; + } + .segment-question .question-option { + padding: 8px 16px; + background: #3d3f41; + color: #ffffff; + border: 1px solid #474747; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + font-size: 13px; + } + .segment-question .question-option:hover { + background: #005a9e; + border-color: #005a9e; + } + .segment-question .question-option.selected { + background: #007ACC; + color: #ffffff; + border-color: #007ACC; + } + .segment-question.answered .question-option:not(.selected) { + opacity: 0.5; + pointer-events: none; + } + .segment-question .custom-input-container { + display: flex; + gap: 8px; + width: 100%; + margin-top: 8px; + } + .segment-question .custom-input { + flex: 1; + padding: 8px 12px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border); + border-radius: 6px; + font-size: 13px; + margin-left: -20px; + } + .segment-question .custom-submit { + padding: 8px 16px; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 6px; + cursor: pointer; + } + .segment-question .custom-submit:hover { + background: var(--vscode-button-hoverBackground); + } + .segment-question.answered .custom-input-container { + display: none; + } + .question-segment .question-text { + margin-bottom: 8px; + font-weight: 500; + } + .question-segment .question-options { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .question-opt { + padding: 4px 10px; + background: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + border-radius: 4px; + font-size: 12px;} + + ${getAgentCardStyles()} + + ${getPlanCardStyles()} + + ${getCodeHighlightStyles()} + + ${getWaveformPreviewContent()} + `; +} diff --git a/src/views/questionHandler.ts b/src/views/questionHandler.ts new file mode 100644 index 0000000..7945904 --- /dev/null +++ b/src/views/questionHandler.ts @@ -0,0 +1,118 @@ +/** + * 问题处理脚本模块 + * 功能:用户问题交互逻辑 + * 依赖:textFormatter + * 使用场景:webview 中的问题回答处理 + */ + +export function getQuestionHandlerScript(): string { + return ` + const answeredQuestions = new Map(); + + function handleQuestionAnswer(askId, answer, questionDiv) { + console.log('[WebView] 用户选择答案:', askId, answer); + questionDiv.classList.add('answered'); + const options = questionDiv.querySelectorAll('.question-option'); + options.forEach(opt => { + if (opt.textContent === answer) { + opt.classList.add('selected'); + } + }); + vscode.postMessage({ + command: 'submitAnswer', + askId: askId, + selected: [answer], + customInput: answer + }); + } + + function handleQuestionAnswerInSegment(askId, answer, segmentDiv) { + console.log('[WebView] 段落中用户选择答案:', askId, answer); + answeredQuestions.set(askId, answer); + segmentDiv.classList.add('answered'); + const options = segmentDiv.querySelectorAll('.question-option'); + options.forEach(opt => { + if (opt.getAttribute('data-option') === answer) { + opt.classList.add('selected'); + } + }); + const customContainer = segmentDiv.querySelector('.custom-input-container'); + if (customContainer) { + customContainer.style.display = 'none'; + } + vscode.postMessage({ + command: 'submitAnswer', + askId: askId, + selected: [answer], + customInput: answer + }); + } + + function handleMultiQuestionAnswer(askId, answers, segmentDiv) { + console.log('[WebView] 多问题答案提交:', askId, answers); + answeredQuestions.set(askId, answers); + segmentDiv.classList.add('answered'); + const inputs = segmentDiv.querySelectorAll('input'); + inputs.forEach(input => { + input.disabled = true; + if (input.checked) { + const label = input.closest('.question-option'); + if (label) { + label.classList.add('selected'); + } + } + }); + const submitBtn = segmentDiv.querySelector('.custom-submit'); + if (submitBtn) { + submitBtn.style.display = 'none'; + } + vscode.postMessage({ + command: 'submitAnswer', + askId: askId, + answers: answers + }); + } + + function showQuestion(askId, question, options) { + console.log('[WebView] showQuestion 被调用:', askId, question, options); + const div = document.createElement('div'); + div.className = 'message bot-message question-message'; + div.setAttribute('data-ask-id', askId); + const questionText = document.createElement('div'); + questionText.className = 'question-text'; + questionText.textContent = question; + div.appendChild(questionText); + const optionsContainer = document.createElement('div'); + optionsContainer.className = 'question-options'; + options.forEach((option, index) => { + const optionBtn = document.createElement('button'); + optionBtn.className = 'question-option'; + optionBtn.textContent = option; + optionBtn.onclick = () => handleQuestionAnswer(askId, option, div); + optionsContainer.appendChild(optionBtn); + }); + div.appendChild(optionsContainer); + const customContainer = document.createElement('div'); + customContainer.className = 'custom-input-container'; + const customInput = document.createElement('input'); + customInput.type = 'text'; + customInput.className = 'custom-input'; + customInput.placeholder = '输入其他答案...'; + const customSubmit = document.createElement('button'); + customSubmit.className = 'custom-submit'; + customSubmit.textContent = '提交'; + customSubmit.onclick = () => { + const customValue = customInput.value.trim(); + if (customValue) { + handleQuestionAnswer(askId, customValue, div); + } + }; + customContainer.appendChild(customInput); + customContainer.appendChild(customSubmit); + div.appendChild(customContainer); + messagesEl.appendChild(div); + smartScrollToBottom(); + checkHeaderVisibility(); + } + `; +} diff --git a/src/views/segmentRenderer.ts b/src/views/segmentRenderer.ts new file mode 100644 index 0000000..7a49689 --- /dev/null +++ b/src/views/segmentRenderer.ts @@ -0,0 +1,274 @@ +/** + * 分段消息渲染脚本模块 + * 功能:实时更新分段消息、工具调用展示 + * 依赖:toolHelpers, textFormatter, waveformPreviewContent + * 使用场景:webview 中的分段消息渲染 + */ + +export function getSegmentRendererScript(): string { + return ` + function updateSegmentsRealtime(segments, isComplete) { + if (isComplete && (!segments || segments.length === 0)) { + currentSegmentedMessage = null; + return; + } + if (!segments || segments.length === 0) return; + + if (!currentSegmentedMessage) { + if (currentStreamingMessage) { + currentStreamingMessage.remove(); + currentStreamingMessage = null; + } + const toolStatuses = messagesEl.querySelectorAll('.tool-status'); + toolStatuses.forEach(el => el.remove()); + const lastSegmented = messagesEl.querySelector('.segmented-message:last-child'); + if (lastSegmented && !lastSegmented.querySelector('.message-actions')) { + currentSegmentedMessage = lastSegmented; + } else { + currentSegmentedMessage = document.createElement('div'); + currentSegmentedMessage.className = 'message bot-message segmented-message'; + messagesEl.appendChild(currentSegmentedMessage); + } + renderedSegmentCount = 0; + } + + if (currentSegmentedMessage) { + const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]'); + toolHeaders.forEach((header, idx) => { + const isCollapsed = header.classList.contains('collapsed'); + toolCollapseStates.set(idx, isCollapsed); + }); + } + + currentSegmentedMessage.innerHTML = ''; + + const mergedSegments = []; + let i = 0; + while (i < segments.length) { + const segment = segments[i]; + if (segment.type === 'tool') { + let count = 1; + while (i + count < segments.length && + segments[i + count].type === 'tool' && + segments[i + count].toolName === segment.toolName) { + count++; + } + mergedSegments.push({ ...segment, toolCount: count }); + i += count; + } else { + mergedSegments.push(segment); + i++; + } + } + + let toolIndex = 0; + mergedSegments.forEach((segment, index) => { + const segmentDiv = document.createElement('div'); + segmentDiv.className = 'message-segment segment-' + segment.type; + + if (segment.type === 'text' && segment.content) { + segmentDiv.className += ' segment-text'; + segmentDiv.innerHTML = formatText(segment.content); + } else if (segment.type === 'tool') { + if (segment.toolName === 'spawnExplorer') return; + segmentDiv.className += ' low-profile'; + const toolResult = segment.toolResult || ''; + const toolCount = segment.toolCount || 1; + const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : ''; + const toolDescription = segment.toolDescription || ''; + const shouldCollapse = toolResult && toolResult.length > 60; + const savedState = toolCollapseStates.get(toolIndex); + const isCollapsed = savedState !== undefined ? savedState : shouldCollapse; + const currentToolIndex = toolIndex; + toolIndex++; + + segmentDiv.innerHTML = \` +
    + \${shouldCollapse ? \`\${collapseIconSvg}\` : getToolIcon(segment.toolName)} + \${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix} + \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''} +
    + \${shouldCollapse ? \`
    \${toolResult}
    \` : ''} + \${toolDescription ? \`

    \${toolDescription}

    \` : ''} + \`; + + if (segment.toolName === 'simulation' && segment.toolStatus === 'success') { + if (typeof createWaveformPreview === 'function') { + const vcdPaths = parseMultiVcdPaths(segment.toolResult); + if (vcdPaths.length > 0) { + vcdPaths.forEach(vcdInfo => { + const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name); + segmentDiv.appendChild(waveformPreview); + }); + } else { + let vcdPath = segment.vcdFilePath; + if (!vcdPath && segment.toolResult) { + const match = String(segment.toolResult).match(/(?:路径\\s*[::]\\s*|已生成[::]\\s*)(.+\\.vcd)/); + if (match && match[1]) { + vcdPath = match[1].trim(); + } + } + if (vcdPath) { + const fileName = segment.fileName || vcdPath.split(/[\\\\\\/]/).pop() || 'waveform.vcd'; + const waveformPreview = createWaveformPreview(vcdPath, fileName); + segmentDiv.appendChild(waveformPreview); + } + } + } else { + console.warn('[VCD Preview] createWaveformPreview function not found'); + } + } + + if (shouldCollapse) { + setTimeout(() => { + const header = segmentDiv.querySelector('.tool-segment-header'); + const content = segmentDiv.querySelector('.tool-segment-content'); + if (header && content) { + header.addEventListener('click', function() { + const isCollapsed = header.classList.contains('collapsed'); + const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0'); + if (isCollapsed) { + header.classList.remove('collapsed'); + content.classList.remove('collapsed'); + content.style.maxHeight = content.scrollHeight + 'px'; + toolCollapseStates.set(toolIdx, false); + } else { + header.classList.add('collapsed'); + content.classList.add('collapsed'); + content.style.maxHeight = '0'; + toolCollapseStates.set(toolIdx, true); + } + }); + } + }, 0); + } + } else if (segment.type === 'question') { + segmentDiv.className += ' segment-question'; + const questions = segment.questions || (segment.question ? [{ + question: segment.question, + options: segment.options || [], + multiSelect: false + }] : []); + const isAnswered = answeredQuestions.has(segment.askId); + const savedAnswers = answeredQuestions.get(segment.askId) || {}; + if (isAnswered) { + segmentDiv.classList.add('answered'); + } + + const questionsHtml = questions.map((q, qIndex) => { + const inputType = q.multiSelect ? 'checkbox' : 'radio'; + const inputName = \`q\${qIndex}\`; + const selectedAnswers = savedAnswers[qIndex] || []; + let optionsHtml; + if (!q.options || q.options.length === 0) { + const savedText = selectedAnswers[0] || ''; + optionsHtml = \`\`; + } else { + optionsHtml = q.options.map(opt => { + const isSelected = selectedAnswers.includes(opt); + return \`\`; + }).join(''); + } + return \` +
    +
    \${formatText(q.question)}
    +
    \${optionsHtml}
    +
    + \`; + }).join(''); + + segmentDiv.innerHTML = \` + \${questionsHtml} + + \`; + + if (!isAnswered) { + setTimeout(() => { + const submitBtn = segmentDiv.querySelector('.custom-submit'); + if (submitBtn) { + submitBtn.addEventListener('click', function() { + const answers = {}; + questions.forEach((q, qIndex) => { + const textarea = segmentDiv.querySelector(\`textarea[name="q\${qIndex}"]\`); + if (textarea) { + const value = textarea.value.trim(); + answers[qIndex] = value ? [value] : []; + } else { + const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`); + answers[qIndex] = Array.from(inputs).map(input => input.value); + } + }); + handleMultiQuestionAnswer(segment.askId, answers, segmentDiv); + }); + } + }, 0); + } + } else if (segment.type === 'agent') { + renderAgentCard(segment, segmentDiv); + } else if (segment.type === 'plan') { + renderPlanCardInSegment(segment, segmentDiv, answeredQuestions); + } + + currentSegmentedMessage.appendChild(segmentDiv); + }); + + if (isComplete) { + console.log('[WebView] 对话完成,添加操作按钮'); + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'message-actions'; + const copyBtn = document.createElement('button'); + copyBtn.className = 'action-btn'; + copyBtn.innerHTML = \`复制\`; + copyBtn.onclick = () => { + const textContent = segments + .filter(s => s.type === 'text' && s.content) + .map(s => s.content) + .join('\\n'); + copyMessage(textContent, copyBtn); + }; + const likeBtn = document.createElement('button'); + likeBtn.className = 'action-btn'; + likeBtn.innerHTML = \`点赞\`; + likeBtn.onclick = () => toggleLike(likeBtn); + const dislikeBtn = document.createElement('button'); + dislikeBtn.className = 'action-btn'; + dislikeBtn.innerHTML = \`点踩\`; + dislikeBtn.onclick = () => toggleDislike(dislikeBtn); + actionsDiv.appendChild(copyBtn); + actionsDiv.appendChild(likeBtn); + actionsDiv.appendChild(dislikeBtn); + currentSegmentedMessage.appendChild(actionsDiv); + currentSegmentedMessage = null; + } + + smartScrollToBottom(); + } + + function renderSegments(segments) { + console.log('[WebView] renderSegments 被调用, segments:', segments); + if (!segments || segments.length === 0) { + console.log('[WebView] segments 为空,跳过渲染'); + return; + } + if (currentStreamingMessage) { + console.log('[WebView] 移除流式消息'); + currentStreamingMessage.remove(); + currentStreamingMessage = null; + } + const toolStatuses = messagesEl.querySelectorAll('.tool-status'); + console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length); + toolStatuses.forEach(el => { + console.log('[WebView] 移除工具状态消息:', el.className); + el.remove(); + }); + const container = document.createElement('div'); + container.className = 'message bot-message segmented-message'; + updateSegmentsRealtime(segments, true); + messagesEl.appendChild(container); + smartScrollToBottom(); + } + `; +} diff --git a/src/views/textFormatter.ts b/src/views/textFormatter.ts new file mode 100644 index 0000000..9c0daee --- /dev/null +++ b/src/views/textFormatter.ts @@ -0,0 +1,56 @@ +/** + * 文本格式化模块 + * 功能:Markdown 文本转 HTML + * 依赖:无 + * 使用场景:消息内容格式化显示 + */ + +export function formatText(text: string): string { + if (!text) return ""; + + let html = text; + + const codeBlocks: string[] = []; + html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { + const language = lang || "plaintext"; + const escapedCode = code + .trim() + .replace(/&/g, "&") + .replace(//g, ">"); + const placeholder = `___CODE_BLOCK_${codeBlocks.length}___`; + codeBlocks.push(`
    ${escapedCode}
    `); + return placeholder; + }); + + const inlineCodes: string[] = []; + html = html.replace(/`([^`]+)`/g, (match, code) => { + const escapedCode = code.replace(/&/g, "&").replace(//g, ">"); + const placeholder = `___INLINE_CODE_${inlineCodes.length}___`; + inlineCodes.push(`${escapedCode}`); + return placeholder; + }); + + html = html.replace(/&/g, "&").replace(//g, ">"); + + html = html.replace(/^### (.+)$/gm, "

    $1

    "); + html = html.replace(/^## (.+)$/gm, "

    $1

    "); + html = html.replace(/^# (.+)$/gm, "

    $1

    "); + html = html.replace(/\*\*(.+?)\*\*/g, "$1"); + html = html.replace(/\*(.+?)\*/g, "$1"); + html = html.replace(/^[\-\*] (.+)$/gm, "
  • $1
  • "); + html = html.replace(/(
  • .*<\/li>\n?)+/g, ""); + html = html.replace(/^\d+\. (.+)$/gm, "
  • $1
  • "); + html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1'); + html = html.replace(/\n/g, "
    "); + + codeBlocks.forEach((block, index) => { + html = html.replace(`___CODE_BLOCK_${index}___`, block); + }); + + inlineCodes.forEach((code, index) => { + html = html.replace(`___INLINE_CODE_${index}___`, code); + }); + + return html; +} diff --git a/src/views/toolHelpers.ts b/src/views/toolHelpers.ts new file mode 100644 index 0000000..bc0de77 --- /dev/null +++ b/src/views/toolHelpers.ts @@ -0,0 +1,106 @@ +/** + * 工具辅助函数模块 + * 功能:工具图标、名称映射、VCD 路径解析 + * 依赖:toolIcons + * 使用场景:工具调用显示 + */ + +import { + fileWriteIconSvg, + fileReadIconSvg, + fileDeleteIconSvg, + syntaxCheckIconSvg, + SearchCode, + saveKnowledgeIconSvg, + simulationIconSvg, + waveformIconSvg, + knowledgeLoadIconSvg, + stateTransitionIconSvg, + userQuestionIconSvg, + updateStageIconSvg, + successIconSvg, +} from "../constants/toolIcons"; + +export function getToolIcon(toolName: string): string { + const iconMap: Record = { + file_read: fileReadIconSvg, + file_write: fileWriteIconSvg, + file_delete: fileDeleteIconSvg, + file_list: SearchCode, + syntax_check: syntaxCheckIconSvg, + simulation: simulationIconSvg, + waveform_summary: waveformIconSvg, + knowledge_save: saveKnowledgeIconSvg, + knowledge_load: knowledgeLoadIconSvg, + queryKnowledgeSummary: knowledgeLoadIconSvg, + queryRules: knowledgeLoadIconSvg, + setModule: fileWriteIconSvg, + addSignal: fileWriteIconSvg, + addSignalExample: fileWriteIconSvg, + validateKnowledgeGraph: syntaxCheckIconSvg, + querySignals: SearchCode, + addPlan: fileWriteIconSvg, + addEdge: fileWriteIconSvg, + showPlan: SearchCode, + addRule: fileWriteIconSvg, + updateNode: fileWriteIconSvg, + addStateTransition: stateTransitionIconSvg, + askUser: userQuestionIconSvg, + updatePhase: updateStageIconSvg, + iverilog: successIconSvg, + }; + return iconMap[toolName] || ""; +} + +export function getToolDisplayName(toolName: string): string { + const toolNameMap: Record = { + file_read: "已完成文件读取", + file_write: "已完成文件写入", + file_delete: "已完成文件删除", + file_list: "已检索代码文件", + syntax_check: "已完成语法检查", + simulation: "已完成仿真", + waveform_summary: "已完成波形分析", + knowledge_save: "已保存知识库", + knowledge_load: "已加载知识库", + queryKnowledgeSummary: "已查询知识摘要", + queryRules: "已查询规则", + setModule: "已设置模块", + addSignal: "信号分析完成", + addSignalExample: "信号示例处理完成", + validateKnowledgeGraph: "已验证知识图谱", + querySignals: "已查询信号", + addPlan: "已添加计划", + addEdge: "已添加边", + showPlan: "已显示计划", + addRule: "已添加规则", + updateNode: "已更新节点", + addStateTransition: "已添加状态转换", + spawnExplorer: "代码探索", + spawnDebugger: "波形调试", + askUser: "用户提问", + updatePhase: "已更新阶段", + iverilog: "已完成编译", + }; + return toolNameMap[toolName] || toolName; +} + +export function parseMultiVcdPaths(toolResult: string): Array<{ name: string; path: string }> { + if (!toolResult) return []; + const result = String(toolResult); + + const vcdListMatch = result.match(/VCD 文件列表:[\s\S]*?(?=\n\n|$)/); + if (!vcdListMatch) return []; + + const paths: Array<{ name: string; path: string }> = []; + const lineRegex = /- (\w+): ([^\n]+)/g; + let match; + while ((match = lineRegex.exec(vcdListMatch[0])) !== null) { + const name = match[1]; + const pathOrError = match[2].trim(); + if (!pathOrError.startsWith("失败")) { + paths.push({ name: name + ".vcd", path: pathOrError }); + } + } + return paths; +}