/** * 消息区域模块 * * 功能说明: * - 负责聊天消息的显示和渲染 * - 支持用户消息和 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 = \`' + 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, '