/** * 消息区域模块 * * 功能说明: * - 负责聊天消息的显示和渲染 * - 支持用户消息和 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 = \`' + code.trim() + '';
});
// 处理行内代码(单个反引号包裹)
html = html.replace(/\`([^\`]+)\`/g, '$1');
// 处理标题 ### Title
html = html.replace(/^### (.+)$/gm, '