/** * 消息区域模块 * * 功能说明: * - 负责聊天消息的显示和渲染 * - 支持用户消息和 AI 消息的不同样式 * - 提供消息操作功能(复制、点赞、点踩) * - 支持流式消息实时更新 * - 支持分段消息渲染(文本、工具调用、用户问题) * - 显示工具执行状态和加载指示器 */ import { collapseIconSvg, fileWriteIconSvg, syntaxCheckIconSvg, SearchCode, agentIconSvg, } from "../constants/toolIcons"; import { getWaveformPreviewContent, getWaveformPreviewScript, } from "./waveformPreviewContent"; import { getAgentCardStyles, getAgentCardScript } from "./agentCard"; /** * 获取消息区域的 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 22px; } .segment-text { line-height: 1.6; } /* Markdown 样式 */ .segment-text h1, .segment-text h2, .segment-text h3 { margin: 16px 0 8px 0; font-weight: 600; line-height: 1.3; } .segment-text h1 { font-size: 1.5em; border-bottom: 1px solid var(--vscode-panel-border); padding-bottom: 8px; } .segment-text h2 { font-size: 1.3em; } .segment-text h3 { font-size: 1.1em; } .segment-text pre { background: var(--vscode-textCodeBlock-background); border: 1px solid var(--vscode-panel-border); border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; } .segment-text code { font-family: 'Courier New', Consolas, monospace; font-size: 0.9em; } .segment-text pre code { background: transparent; padding: 0; border: none; } .segment-text code:not(pre code) { background: var(--vscode-textCodeBlock-background); padding: 2px 6px; border-radius: 3px; color: var(--vscode-textPreformat-foreground); } .segment-text ul, .segment-text ol { margin: 8px 0; padding-left: 24px; } .segment-text li { margin: 4px 0; line-height: 1.6; } .segment-text strong { font-weight: 600; color: var(--vscode-foreground); } .segment-text em { font-style: italic; } .segment-text a { color: var(--vscode-textLink-foreground); text-decoration: none; } .segment-text a:hover { text-decoration: underline; } .segment-text p { margin: 8px 0; } .segment-tool { margin: 4px 0; padding: 4px 0; } .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(0deg); } .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-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-segment-content { overflow: hidden; transition: max-height 0.3s ease; } .tool-segment-content.collapsed { max-height: 0; } .segment-question { background: var(--vscode-textBlockQuote-background); border-radius: 6px; margin: 8px 0; padding: 12px 14px; 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; } .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()} /* 计划卡片样式 */ .segment-plan { margin: 8px 0; } .plan-card { border: 1px solid var(--vscode-input-border); border-radius: 8px; overflow: hidden; background: var(--vscode-editor-background); } .plan-header { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--vscode-sideBar-background); border-bottom: 1px solid var(--vscode-input-border); } .plan-icon { font-size: 18px; } .plan-title { font-weight: 600; font-size: 14px; } .plan-body { padding: 12px; } .plan-summary { color: var(--vscode-descriptionForeground); margin-bottom: 10px; font-size: 13px; } .plan-steps { font-size: 13px; } .plan-step { padding: 6px 8px; margin-bottom: 4px; background: var(--vscode-list-hoverBackground); border-radius: 4px; } .plan-step:last-child { margin-bottom: 0; } .step-num { color: var(--vscode-textLink-foreground); font-weight: 500; margin-right: 4px; } .plan-actions { display: flex; gap: 8px; padding: 12px; border-top: 1px solid var(--vscode-input-border); background: var(--vscode-sideBar-background); } .plan-btn { padding: 6px 16px; border-radius: 4px; border: none; cursor: pointer; font-size: 12px; font-weight: 500; } .plan-btn-confirm { background: var(--vscode-button-background); color: var(--vscode-button-foreground); } .plan-btn-confirm:hover { background: var(--vscode-button-hoverBackground); } .plan-btn-modify { background: var(--vscode-input-background); color: var(--vscode-foreground); border: 1px solid var(--vscode-input-border); } .plan-btn-cancel { background: transparent; color: var(--vscode-descriptionForeground); } ${getWaveformPreviewContent()} `; } /** * 获取消息区域的脚本 */ export function getMessageAreaScript(): string { return ` // 工具图标定义 const collapseIconSvg = \`${collapseIconSvg}\`; const fileWriteIconSvg = \`${fileWriteIconSvg}\`; const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`; const searchCodeIconSvg = \`${SearchCode}\`; ${getAgentCardScript()} // 工具名称映射 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': '已显示计划', 'spawnExplorer': '代码探索' }; return toolNameMap[toolName] || toolName; } // 检查用户是否在底部附近(允许50px的误差) function isUserNearBottom() { const threshold = 50; return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold; } // 智能滚动:只有用户在底部附近时才自动滚动 function smartScrollToBottom() { if (isUserNearBottom()) { messagesEl.scrollTop = 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 // 实时更新分段消息(按后端返回顺序) 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); } // 清空容器并重新渲染所有段落 currentSegmentedMessage.innerHTML = ''; segments.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; } const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; const toolResult = segment.toolResult || ''; // 检查工具结果是否过长(超过一行显示不下) const shouldCollapse = toolResult && toolResult.length > 60; segmentDiv.innerHTML = \`
\${shouldCollapse ? collapseIconSvg : ''} \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''} \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''} \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''} \${getToolDisplayName(segment.toolName) || '工具'} \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''}
\${shouldCollapse ? \`\` : ''} \`; // 如果是仿真工具且成功完成,尝试添加波形预览 if (segment.toolName === 'simulation' && segment.toolStatus === 'success') { // 优先使用显式提供的路径,否则从结果文本中解析 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'); const iconCollapsed = segmentDiv.querySelector('.icon-collapsed'); const iconExpanded = segmentDiv.querySelector('.icon-expanded'); if (header && content) { header.addEventListener('click', function() { const isCollapsed = header.classList.contains('collapsed'); if (isCollapsed) { // 展开 header.classList.remove('collapsed'); content.classList.remove('collapsed'); content.style.maxHeight = content.scrollHeight + 'px'; if (iconCollapsed) iconCollapsed.style.display = 'none'; if (iconExpanded) iconExpanded.style.display = 'block'; } else { // 折叠 header.classList.add('collapsed'); content.classList.add('collapsed'); content.style.maxHeight = '0'; if (iconCollapsed) iconCollapsed.style.display = 'block'; if (iconExpanded) iconExpanded.style.display = 'none'; } }); } }, 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 = \`
\${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') { // 计划卡片渲染(类似 askUser) segmentDiv.className += ' segment-plan'; // 检查是否已回答 const isAnswered = answeredQuestions.has(segment.askId); const selectedAnswer = answeredQuestions.get(segment.askId); if (isAnswered) { segmentDiv.classList.add('answered'); } const stepsHtml = (segment.planSteps || []).map((step, i) => \`
\${i + 1}. \${step}
\` ).join(''); // 选项按钮 const options = ['确认执行', '修改计划', '取消']; const optionsHtml = options.map(opt => { const isSelected = isAnswered && opt === selectedAnswer; return \`\`; }).join(''); segmentDiv.innerHTML = \`
📋 \${segment.planTitle || '执行计划'}
\${segment.planSummary || ''}
\${stepsHtml}
\${optionsHtml}
\`; // 只在未回答时添加事件监听 if (!isAnswered) { setTimeout(() => { const optionButtons = segmentDiv.querySelectorAll('.question-option'); optionButtons.forEach(btn => { btn.addEventListener('click', function() { const option = this.getAttribute('data-option'); // 发送答案到后端 handleQuestionAnswerInSegment(segment.askId, option, segmentDiv); // 同时发送 planAction 用于模式切换 const actionMap = { '确认执行': 'confirm', '修改计划': 'modify', '取消': 'cancel' }; vscode.postMessage({ command: 'planAction', action: actionMap[option] || option, planTitle: segment.planTitle }); }); }); 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); } } 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); }; actionsDiv.appendChild(copyBtn); 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'; segments.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; } const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; const toolResult = segment.toolResult || ''; // 检查工具结果是否过长(超过一行显示不下) const shouldCollapse = toolResult && toolResult.length > 60; segmentDiv.innerHTML = \`
\${shouldCollapse ? collapseIconSvg : ''} \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''} \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''} \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''} \${getToolDisplayName(segment.toolName) || '工具'} \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''}
\${shouldCollapse ? \`\` : ''} \`; // 如果是仿真工具且成功完成,尝试添加波形预览 if (segment.toolName === 'simulation' && segment.toolStatus === 'success') { // 优先使用显式提供的路径,否则从结果文本中解析 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'); const iconCollapsed = segmentDiv.querySelector('.icon-collapsed'); const iconExpanded = segmentDiv.querySelector('.icon-expanded'); if (header && content) { header.addEventListener('click', function() { const isCollapsed = header.classList.contains('collapsed'); if (isCollapsed) { // 展开 header.classList.remove('collapsed'); content.classList.remove('collapsed'); content.style.maxHeight = content.scrollHeight + 'px'; if (iconCollapsed) iconCollapsed.style.display = 'none'; if (iconExpanded) iconExpanded.style.display = 'block'; } else { // 折叠 header.classList.add('collapsed'); content.classList.add('collapsed'); content.style.maxHeight = '0'; if (iconCollapsed) iconCollapsed.style.display = 'block'; if (iconExpanded) iconExpanded.style.display = 'none'; } }); } }, 0); } } else if (segment.type === 'question') { segmentDiv.innerHTML = \`
\${segment.question || ''}
\${(segment.options || []).map(opt => \`\${opt}\`).join('')}
\`; } else if (segment.type === 'agent') { // 智能体卡片渲染 renderAgentCard(segment, segmentDiv); } else if (segment.type === 'plan') { // 计划卡片渲染 segmentDiv.className += ' segment-plan'; const stepsHtml = (segment.planSteps || []).map((step, i) => \`
\${i + 1}. \${step}
\` ).join(''); segmentDiv.innerHTML = \`
📋 \${segment.planTitle || '执行计划'}
\${segment.planSummary || ''}
\${stepsHtml}
\`; // 绑定按钮事件 setTimeout(() => { const planCard = segmentDiv.querySelector('.plan-card'); if (planCard) { planCard.querySelectorAll('.plan-btn').forEach(btn => { btn.addEventListener('click', (e) => { const action = e.currentTarget?.dataset?.action; vscode.postMessage({ command: 'planAction', action: action, planTitle: segment.planTitle }); }); }); } }, 0); } 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); }; actionsDiv.appendChild(copyBtn); container.appendChild(actionsDiv); messagesEl.appendChild(container); smartScrollToBottom(); } // 格式化文本(支持 Markdown) function formatText(text) { if (!text) return ''; // 先转义 HTML 特殊字符 let html = text .replace(/&/g, '&') .replace(//g, '>'); // 处理代码块(三个反引号包裹的代码) html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) { const language = lang || 'plaintext'; return '
' + code.trim() + '
'; }); // 处理行内代码(单个反引号包裹) html = html.replace(/\`([^\`]+)\`/g, '$1'); // 处理标题 ### 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, '
    '); 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()} `; }