/** * 消息区域模块 * * 功能说明: * - 负责聊天消息的显示和渲染 * - 支持用户消息和 AI 消息的不同样式 * - 提供消息操作功能(复制、点赞、点踩) * - 支持流式消息实时更新 * - 支持分段消息渲染(文本、工具调用、用户问题) * - 显示工具执行状态和加载指示器 */ import { collapseIconSvg, fileWriteIconSvg, fileReadIconSvg, fileDeleteIconSvg, syntaxCheckIconSvg, SearchCode, agentIconSvg, saveKnowledgeIconSvg, simulationIconSvg, waveformIconSvg, knowledgeLoadIconSvg, stateTransitionIconSvg, userQuestionIconSvg, } from "../constants/toolIcons"; import { getWaveformPreviewContent, getWaveformPreviewScript, } from "./waveformPreviewContent"; import { getAgentCardStyles, getAgentCardScript } from "./agentCard"; import { getPlanCardStyles, getPlanCardScript } from "./planCard"; import { getCodeHighlightStyles, getCodeHighlightScript, } from "../components/codeHighlight"; /** * 获取消息区域的 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); } /* 流式消息样式 */ .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: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); border: 1px solid var(--vscode-button-border); border-radius: 6px; cursor: pointer; transition: all 0.2s; } .question-option:hover { background: var(--vscode-button-secondaryHoverBackground); } .question-option.selected { background: var(--vscode-button-background); color: var(--vscode-button-foreground); } .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: 2px 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; } .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; } /* 低调显示的工具调用样式 */ .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: 10px; } .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: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); border: 1px solid var(--vscode-button-border); border-radius: 6px; cursor: pointer; transition: all 0.2s; font-size: 13px; } .segment-question .question-option:hover { background: var(--vscode-button-secondaryHoverBackground); } .segment-question .question-option.selected { background: var(--vscode-button-background); color: var(--vscode-button-foreground); } .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}\`; ${getAgentCardScript()} ${getPlanCardScript()} // 解析多 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, }; 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': '用户提问', }; 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 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 { 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) { console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete); if (!segments || segments.length === 0) { console.log('[WebView] segments 为空,跳过渲染'); return; } // 如果没有当前分段消息容器,创建一个 if (!currentSegmentedMessage) { console.log('[WebView] 创建新的分段消息容器'); // 移除流式消息(如果有) 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(); }); currentSegmentedMessage = document.createElement('div'); currentSegmentedMessage.className = 'message bot-message segmented-message'; messagesEl.appendChild(currentSegmentedMessage); } // 保存当前所有工具的展开/折叠状态 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 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}
\` : ''} \`; // 如果是仿真工具且成功完成,尝试添加波形预览 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'; // 检查是否已回答 const isAnswered = answeredQuestions.has(segment.askId); const selectedAnswer = answeredQuestions.get(segment.askId); if (isAnswered) { segmentDiv.classList.add('answered'); } // 检查是否有选项 const hasOptions = segment.options && segment.options.length > 0; const optionsHtml = hasOptions ? (segment.options || []).map(opt => { const isSelected = isAnswered && opt === selectedAnswer; return \`\`; }).join('') : ''; segmentDiv.innerHTML = \`
\${formatText(segment.question || '')}
\${hasOptions ? \`
\${optionsHtml}
\` : ''}
\`; // 只在未回答时添加事件监听 if (!isAnswered) { setTimeout(() => { if (hasOptions) { const optionButtons = segmentDiv.querySelectorAll('.question-option'); optionButtons.forEach(btn => { btn.addEventListener('click', function() { const option = this.getAttribute('data-option'); handleQuestionAnswerInSegment(segment.askId, option, segmentDiv); }); }); } const submitBtn = segmentDiv.querySelector('.custom-submit'); const customInput = segmentDiv.querySelector('.custom-input'); if (submitBtn && customInput) { submitBtn.addEventListener('click', function() { const customValue = customInput.value.trim(); if (customValue) { handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv); } }); // 支持回车提交 customInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { const customValue = customInput.value.trim(); if (customValue) { handleQuestionAnswerInSegment(segment.askId, customValue, 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 shouldCollapse = toolResult && toolResult.length > 60; segmentDiv.innerHTML = \`
\${shouldCollapse ? \`\${collapseIconSvg}\` : getToolIcon(segment.toolName)} \${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix} \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''}
\${shouldCollapse ? \`\` : ''} \`; // 如果是仿真工具且成功完成,尝试添加波形预览 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 .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(); } // 显示用户问题 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 }); } ${getWaveformPreviewScript()} ${getCodeHighlightScript()} `; }