Files
IC-Coder-Plugin/src/views/messageArea.ts
Roe-xin 5287d483d8 feat: 为所有工具添加图标显示
- 新增多个工具图标定义(文件读取、删除、仿真、波形分析等)
   - 添加 getToolIcon 函数统一管理工具图标映射
   - 优化工具显示逻辑,所有工具现在都显示对应图标
   - 新增 addStateTransition 工具的图标和名称映射
   - 添加所有新图标的 CSS 样式
2025-12-31 09:43:30 +08:00

1442 lines
51 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 消息区域模块
*
* 功能说明:
* - 负责聊天消息的显示和渲染
* - 支持用户消息和 AI 消息的不同样式
* - 提供消息操作功能(复制、点赞、点踩)
* - 支持流式消息实时更新
* - 支持分段消息渲染(文本、工具调用、用户问题)
* - 显示工具执行状态和加载指示器
*/
import {
collapseIconSvg,
fileWriteIconSvg,
fileReadIconSvg,
fileDeleteIconSvg,
syntaxCheckIconSvg,
SearchCode,
agentIconSvg,
saveKnowledgeIconSvg,
simulationIconSvg,
waveformIconSvg,
knowledgeLoadIconSvg,
stateTransitionIconSvg,
} from "../constants/toolIcons";
import {
getWaveformPreviewContent,
getWaveformPreviewScript,
} from "./waveformPreviewContent";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
import { getPlanCardStyles, getPlanCardScript } from "./planCard";
/**
* 获取消息区域的 HTML 内容
*/
export function getMessageAreaContent(): string {
return `<div id="messages" class="messages"></div>`;
}
/**
* 获取消息区域的样式
*/
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-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-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()}
${getPlanCardStyles()}
${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}\`;
${getAgentCardScript()}
${getPlanCardScript()}
// 获取工具图标
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
};
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': '代码探索'
};
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 = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
copyBtn.onclick = () => copyMessage(text, copyBtn);
// 点赞按钮
const likeBtn = document.createElement('button');
likeBtn.className = 'action-btn';
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1
16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
likeBtn.onclick = () => toggleLike(likeBtn);
// 点踩按钮
const dislikeBtn = document.createElement('button');
dislikeBtn.className = 'action-btn';
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
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 = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474c-6.1-7.7-15.3-12.2-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1 0.4-12.8-6.3-12.8z" fill="currentColor"/></svg>\`;
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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
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 = \`
<div class="loading-dots">
<span></span><span></span><span></span>
</div>
<span class="loading-text">\${text}</span>
\`;
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 = \`
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
\${shouldCollapse ? collapseIconSvg : getToolIcon(segment.toolName)}
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
</div>
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
\`;
// 如果是仿真工具且成功完成,尝试添加波形预览
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 \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
}).join('')
: '';
segmentDiv.innerHTML = \`
<div class="question-text">\${segment.question || ''}</div>
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
<button class="custom-submit">提交</button>
</div>
\`;
// 只在未回答时添加事件监听
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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
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 = \`
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
\${shouldCollapse ? collapseIconSvg : getToolIcon(segment.toolName)}
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
</div>
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
\`;
// 如果是仿真工具且成功完成,尝试添加波形预览
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 = \`
<div class="question-segment">
<div class="question-text">\${segment.question || ''}</div>
<div class="question-options">
\${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')}
</div>
</div>
\`;
} 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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 处理代码块(三个反引号包裹的代码)
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
const language = lang || 'plaintext';
return '<pre><code class="language-' + language + '">' + code.trim() + '</code></pre>';
});
// 处理行内代码(单个反引号包裹)
html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
// 处理标题 ### Title
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// 处理粗体 **text**
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
// 处理斜体 *text*
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
// 处理无序列表 - item 或 * item
html = html.replace(/^[\\-\\*] (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
// 处理有序列表 1. item
html = html.replace(/^\\d+\\. (.+)$/gm, '<li>$1</li>');
// 处理链接 [text](url)
html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
// 处理换行
html = html.replace(/\\n/g, '<br>');
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 = \`
<span class="tool-icon">\${statusIcons[status]}</span>
<span class="tool-name">\${getToolDisplayName(toolName)}</span>
<span class="tool-status-text">\${statusTexts[status]}</span>
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
\`;
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()}
`;
}