diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts
index 0a6a196..49cf669 100644
--- a/src/views/messageArea.ts
+++ b/src/views/messageArea.ts
@@ -1,13 +1,8 @@
/**
* 消息区域模块
- *
- * 功能说明:
- * - 负责聊天消息的显示和渲染
- * - 支持用户消息和 AI 消息的不同样式
- * - 提供消息操作功能(复制、点赞、点踩)
- * - 支持流式消息实时更新
- * - 支持分段消息渲染(文本、工具调用、用户问题)
- * - 显示工具执行状态和加载指示器
+ * 功能:消息区域入口,整合样式和脚本
+ * 依赖:messageStyles, toolHelpers, textFormatter, questionHandler, messageRenderer
+ * 使用场景:webview 内容生成
*/
import {
@@ -17,7 +12,6 @@ import {
fileDeleteIconSvg,
syntaxCheckIconSvg,
SearchCode,
- agentIconSvg,
saveKnowledgeIconSvg,
simulationIconSvg,
waveformIconSvg,
@@ -27,1751 +21,165 @@ import {
updateStageIconSvg,
successIconSvg,
} from "../constants/toolIcons";
-import {
- getWaveformPreviewContent,
- getWaveformPreviewScript,
-} from "./waveformPreviewContent";
-import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
-import { getPlanCardStyles, getPlanCardScript } from "./planCard";
-import {
- getCodeHighlightStyles,
- getCodeHighlightScript,
-} from "../components/codeHighlight";
+import { getMessageAreaStyles } from "./messageStyles";
+import { getQuestionHandlerScript } from "./questionHandler";
+import { getMessageRendererScript } from "./messageRenderer";
+import { getSegmentRendererScript } from "./segmentRenderer";
-/**
- * 获取消息区域的 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);
- }
+export { getMessageAreaStyles };
- /* 流式消息样式 */
- .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: #007ACC;
- color: #ffffff;
- border: 1px solid #007ACC;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.2s;
- }
- .question-option:hover {
- background: #005a9e;
- border-color: #005a9e;
- }
- .question-option.selected {
- background: #007ACC;
- color: #ffffff;
- border-color: #007ACC;
- }
- .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: 25px 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;
- }
- .icon-expanded svg path {
- fill: #007ACC !important;
- }
- .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;
- }
- .tool-segment-description {
- margin: 25px 0 0 0px;
- font-size: 0.9rem;
- color: var(--vscode-descriptionForeground);
- line-height: 1.4;
- }
- /* 低调显示的工具调用样式 */
- .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: 12px;
- }
- .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: #3d3f41;
- color: #ffffff;
- border: 1px solid #474747;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.2s;
- font-size: 13px;
- }
- .segment-question .question-option:hover {
- background: #005a9e;
- border-color: #005a9e;
- }
- .segment-question .question-option.selected {
- background: #007ACC;
- color: #ffffff;
- border-color: #007ACC;
- }
- .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}\`;
- const updateStageIconSvg = \`${updateStageIconSvg}\`;
- const successIconSvg = \`${successIconSvg}\`;
+ 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}\`;
+ const updateStageIconSvg = \`${updateStageIconSvg}\`;
+ const successIconSvg = \`${successIconSvg}\`;
- ${getAgentCardScript()}
+ 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,
+ 'updatePhase': updateStageIconSvg,
+ 'iverilog': successIconSvg,
+ };
+ return iconMap[toolName] || '';
+ }
- ${getPlanCardScript()}
+ 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': '用户提问',
+ 'updatePhase': '已更新阶段',
+ 'iverilog': '已完成编译',
+ };
+ return toolNameMap[toolName] || toolName;
+ }
- // 解析多 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,
- 'updatePhase': updateStageIconSvg,
- 'iverilog': successIconSvg,
- };
- 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': '用户提问',
- 'updatePhase': '已更新阶段',
- 'iverilog': '已完成编译',
- };
- 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 parseMultiVcdPaths(toolResult) {
+ if (!toolResult) return [];
+ const result = String(toolResult);
+ 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 });
}
}
-
- // 添加消息
- 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 {
- // 用户消息:解析文件路径并转换为标签
- const parts = text.split(' ');
- const filePaths = [];
- const textParts = [];
-
- parts.forEach(part => {
- // 判断是否为文件路径或代码片段:包含路径分隔符、文件扩展名或代码片段格式(文件名:行号-行号)
- if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part) || /:[0-9]+-[0-9]+$/.test(part)) {
- filePaths.push(part);
- } else {
- textParts.push(part);
- }
- });
-
- if (filePaths.length > 0) {
- div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' ');
- } else {
- div.textContent = text;
- }
-
- // 当添加用户消息时,隐藏 header
- hideHeaderIfNeeded();
- }
-
- messagesEl.appendChild(div);
- smartScrollToBottom();
-
- // 添加消息后检查 header 显示状态
- checkHeaderVisibility();
- }
-
- // 检查是否需要隐藏 header
- function hideHeaderIfNeeded() {
- checkHeaderVisibility();
- }
-
- // 复制消息
- function copyMessage(text, button) {
- navigator.clipboard.writeText(text).then(() => {
- const originalHTML = button.innerHTML;
- button.innerHTML = \`\`;
- setTimeout(() => {
- button.innerHTML = originalHTML;
- }, 2000);
- });
- }
-
- // 点赞
- function toggleLike(button) {
- const isActive = button.classList.contains('active');
- // 移除所有同级按钮的 active 状态
- const parent = button.parentElement;
- parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
-
- if (!isActive) {
- button.classList.add('active');
- }
- }
-
- // 点踩
- function toggleDislike(button) {
- const isActive = button.classList.contains('active');
- // 移除所有同级按钮的 active 状态
- const parent = button.parentElement;
- parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
-
- if (!isActive) {
- button.classList.add('active');
- }
- }
-
- // 更新或创建流式消息
- function updateOrCreateStreamingMessage(text) {
- hideLoadingIndicator();
-
- if (!currentStreamingMessage) {
- // 创建新的流式消息元素
- const div = document.createElement('div');
- div.className = 'message bot-message streaming';
-
- const messageContent = document.createElement('div');
- messageContent.className = 'message-content';
- messageContent.textContent = text;
- div.appendChild(messageContent);
-
- messagesEl.appendChild(div);
- currentStreamingMessage = div;
- } else {
- // 更新现有消息内容
- const messageContent = currentStreamingMessage.querySelector('.message-content');
- if (messageContent) {
- messageContent.textContent = text;
- }
- }
-
- // 智能滚动到底部
- smartScrollToBottom();
- }
-
- // 完成流式消息
- function finalizeStreamingMessage(finalText) {
- if (currentStreamingMessage) {
- const messageContent = currentStreamingMessage.querySelector('.message-content');
- if (messageContent) {
- messageContent.textContent = finalText;
- }
- currentStreamingMessage.classList.remove('streaming');
-
- // 添加操作按钮
- const actionsDiv = document.createElement('div');
- actionsDiv.className = 'message-actions';
-
- const copyBtn = document.createElement('button');
- copyBtn.className = 'action-btn';
- copyBtn.innerHTML = '';
- copyBtn.onclick = () => copyMessage(finalText, copyBtn);
- actionsDiv.appendChild(copyBtn);
-
- currentStreamingMessage.appendChild(actionsDiv);
- currentStreamingMessage = null;
- }
-
- smartScrollToBottom();
- }
-
- // 显示加载指示器
- function showLoadingIndicator(text) {
- hideLoadingIndicator();
-
- loadingIndicator = document.createElement('div');
- loadingIndicator.className = 'message bot-message loading-message';
- loadingIndicator.innerHTML = \`
-
-
-
- \${text}
- \`;
- messagesEl.appendChild(loadingIndicator);
- smartScrollToBottom();
- }
-
- // 隐藏加载指示器
- function hideLoadingIndicator() {
- if (loadingIndicator) {
- loadingIndicator.remove();
- loadingIndicator = null;
- }
- }
-
- // 存储已回答问题的状态
- const answeredQuestions = new Map(); // askId -> answer
-
- // 存储工具展开/折叠状态
- const toolCollapseStates = new Map(); // index -> isCollapsed
-
- // 实时更新分段消息(按后端返回顺序)
- function updateSegmentsRealtime(segments, isComplete) {
- // 如果对话完成且没有新段落,只重置容器
- if (isComplete && (!segments || segments.length === 0)) {
- currentSegmentedMessage = null;
- return;
- }
-
- if (!segments || segments.length === 0) {
- return;
- }
-
- // 如果没有当前分段消息容器,创建一个
- if (!currentSegmentedMessage) {
- // 移除流式消息(如果有)
- if (currentStreamingMessage) {
- currentStreamingMessage.remove();
- currentStreamingMessage = null;
- }
-
- // 移除所有工具状态消息(因为会在分段中显示)
- const toolStatuses = messagesEl.querySelectorAll('.tool-status');
- toolStatuses.forEach(el => {
- el.remove();
- });
-
- // 检查最后一个容器是否是未完成的对话(没有操作按钮)
- const lastSegmented = messagesEl.querySelector('.segmented-message:last-child');
- if (lastSegmented && !lastSegmented.querySelector('.message-actions')) {
- // 复用未完成的容器
- currentSegmentedMessage = lastSegmented;
- } else {
- // 创建新容器
- currentSegmentedMessage = document.createElement('div');
- currentSegmentedMessage.className = 'message bot-message segmented-message';
- messagesEl.appendChild(currentSegmentedMessage);
- }
- renderedSegmentCount = 0;
- }
-
- // 保存当前所有工具的展开/折叠状态
- if (currentSegmentedMessage) {
- const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
- toolHeaders.forEach((header, idx) => {
- const isCollapsed = header.classList.contains('collapsed');
- toolCollapseStates.set(idx, isCollapsed);
- });
- }
-
- // 清空容器并重新渲染所有段落
- currentSegmentedMessage.innerHTML = '';
-
- // 合并连续相同的工具调用
- const mergedSegments = [];
- let i = 0;
- while (i < segments.length) {
- const segment = segments[i];
- if (segment.type === 'tool') {
- // 统计连续相同的工具调用
- let count = 1;
- while (i + count < segments.length &&
- segments[i + count].type === 'tool' &&
- segments[i + count].toolName === segment.toolName) {
- count++;
- }
- // 添加合并后的段落(带计数)
- mergedSegments.push({ ...segment, toolCount: count });
- i += count;
- } else {
- mergedSegments.push(segment);
- i++;
- }
- }
-
- let toolIndex = 0; // 用于跟踪工具段落的索引
- mergedSegments.forEach((segment, index) => {
- const segmentDiv = document.createElement('div');
- segmentDiv.className = 'message-segment segment-' + segment.type;
-
- if (segment.type === 'text' && segment.content) {
- segmentDiv.className += ' segment-text';
- segmentDiv.innerHTML = formatText(segment.content);
- } else if (segment.type === 'tool') {
- // 过滤掉不需要显示的工具
- if (segment.toolName === 'spawnExplorer') {
- return;
- }
-
- // 所有工具调用都使用低调样式
- segmentDiv.className += ' low-profile';
-
- const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
- const toolResult = segment.toolResult || '';
- const toolCount = segment.toolCount || 1;
- const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
- const toolDescription = segment.toolDescription || '';
-
- // 检查工具结果是否过长(超过一行显示不下)
- const shouldCollapse = toolResult && toolResult.length > 60;
-
- // 恢复之前保存的展开/折叠状态
- const savedState = toolCollapseStates.get(toolIndex);
- const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
- const currentToolIndex = toolIndex;
- toolIndex++; // 递增工具索引
-
- segmentDiv.innerHTML = \`
-
- \${shouldCollapse ? \`\${toolResult}
\` : ''}
- \${toolDescription ? \`\${toolDescription}
\` : ''}
- \`;
-
- // 如果是仿真工具且成功完成,尝试添加波形预览
- if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
- // 尝试解析多个 VCD 文件(多 VCD 模式)
- const vcdPaths = parseMultiVcdPaths(segment.toolResult);
-
- if (vcdPaths.length > 0) {
- // 多 VCD 模式:为每个文件创建预览
- vcdPaths.forEach(vcdInfo => {
- const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
- segmentDiv.appendChild(waveformPreview);
- });
- } else {
- // 单 VCD 模式(兼容旧逻辑)
- let vcdPath = segment.vcdFilePath;
- if (!vcdPath && segment.toolResult) {
- const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
- if (match && match[1]) {
- vcdPath = match[1].trim();
- }
- }
-
- if (vcdPath) {
- const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
- const waveformPreview = createWaveformPreview(vcdPath, fileName);
- segmentDiv.appendChild(waveformPreview);
- }
- }
- }
-
- // 添加折叠/展开事件监听
- if (shouldCollapse) {
- setTimeout(() => {
- const header = segmentDiv.querySelector('.tool-segment-header');
- const content = segmentDiv.querySelector('.tool-segment-content');
-
- if (header && content) {
- header.addEventListener('click', function() {
- const isCollapsed = header.classList.contains('collapsed');
- const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
-
- if (isCollapsed) {
- // 展开
- header.classList.remove('collapsed');
- content.classList.remove('collapsed');
- content.style.maxHeight = content.scrollHeight + 'px';
- toolCollapseStates.set(toolIdx, false);
- } else {
- // 折叠
- header.classList.add('collapsed');
- content.classList.add('collapsed');
- content.style.maxHeight = '0';
- toolCollapseStates.set(toolIdx, true);
- }
- });
- }
- }, 0);
- }
- } else if (segment.type === 'question') {
- segmentDiv.className += ' segment-question';
-
- // 兼容旧格式:如果有 segment.question,转换为 questions 数组
- const questions = segment.questions || (segment.question ? [{
- question: segment.question,
- options: segment.options || [],
- multiSelect: false
- }] : []);
-
- // 检查是否已回答
- const isAnswered = answeredQuestions.has(segment.askId);
- const savedAnswers = answeredQuestions.get(segment.askId) || {};
-
- if (isAnswered) {
- segmentDiv.classList.add('answered');
- }
-
- // 渲染多个问题
- const questionsHtml = questions.map((q, qIndex) => {
- const inputType = q.multiSelect ? 'checkbox' : 'radio';
- const inputName = \`q\${qIndex}\`;
- const selectedAnswers = savedAnswers[qIndex] || [];
-
- // 如果没有选项,显示文本输入框
- let optionsHtml;
- if (!q.options || q.options.length === 0) {
- const savedText = selectedAnswers[0] || '';
- optionsHtml = \`\`;
- } else {
- optionsHtml = q.options.map(opt => {
- const isSelected = selectedAnswers.includes(opt);
- return \`\`;
- }).join('');
- }
-
- return \`
-
-
\${formatText(q.question)}
-
\${optionsHtml}
-
- \`;
- }).join('');
-
- segmentDiv.innerHTML = \`
- \${questionsHtml}
-
- \`;
-
- // 只在未回答时添加事件监听
- if (!isAnswered) {
- setTimeout(() => {
- const submitBtn = segmentDiv.querySelector('.custom-submit');
- if (submitBtn) {
- submitBtn.addEventListener('click', function() {
- const answers = {};
- questions.forEach((q, qIndex) => {
- // 检查是否是文本输入框
- const textarea = segmentDiv.querySelector(\`textarea[name="q\${qIndex}"]\`);
- if (textarea) {
- const value = textarea.value.trim();
- answers[qIndex] = value ? [value] : [];
- } else {
- const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
- answers[qIndex] = Array.from(inputs).map(input => input.value);
- }
- });
- handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
- });
- }
- }, 0);
- }
- } else if (segment.type === 'agent') {
- // 智能体卡片渲染
- renderAgentCard(segment, segmentDiv);
- } else if (segment.type === 'plan') {
- // 计划卡片渲染(使用独立组件)
- renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
- }
-
- currentSegmentedMessage.appendChild(segmentDiv);
- });
-
- // 如果对话完成,添加操作按钮并重置容器
- if (isComplete) {
- console.log('[WebView] 对话完成,添加操作按钮');
- const actionsDiv = document.createElement('div');
- actionsDiv.className = 'message-actions';
-
- // 复制按钮
- const copyBtn = document.createElement('button');
- copyBtn.className = 'action-btn';
- copyBtn.innerHTML = \`复制\`;
- copyBtn.onclick = () => {
- const textContent = segments
- .filter(s => s.type === 'text' && s.content)
- .map(s => s.content)
- .join('\\n');
- copyMessage(textContent, copyBtn);
- };
-
- // 点赞按钮
- const likeBtn = document.createElement('button');
- likeBtn.className = 'action-btn';
- likeBtn.innerHTML = \`点赞\`;
- likeBtn.onclick = () => toggleLike(likeBtn);
-
- // 点踩按钮
- const dislikeBtn = document.createElement('button');
- dislikeBtn.className = 'action-btn';
- dislikeBtn.innerHTML = \`点踩\`;
- dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
-
- actionsDiv.appendChild(copyBtn);
- actionsDiv.appendChild(likeBtn);
- actionsDiv.appendChild(dislikeBtn);
- currentSegmentedMessage.appendChild(actionsDiv);
-
- // 重置当前分段消息容器(继续对话时创建新容器)
- currentSegmentedMessage = null;
- }
-
- // 智能滚动到底部
- smartScrollToBottom();
- }
-
- // 渲染分段消息(兼容旧代码)
- function renderSegments(segments) {
- console.log('[WebView] renderSegments 被调用, segments:', segments);
- if (!segments || segments.length === 0) {
- console.log('[WebView] segments 为空,跳过渲染');
- return;
- }
-
- // 移除流式消息(如果有)
- if (currentStreamingMessage) {
- console.log('[WebView] 移除流式消息');
- currentStreamingMessage.remove();
- currentStreamingMessage = null;
- }
-
- // 移除所有工具状态消息(因为会在分段中显示)
- const toolStatuses = messagesEl.querySelectorAll('.tool-status');
- console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length);
- toolStatuses.forEach(el => {
- console.log('[WebView] 移除工具状态消息:', el.className);
- el.remove();
- });
-
- // 创建消息容器
- const container = document.createElement('div');
- container.className = 'message bot-message segmented-message';
-
- // 合并连续相同的工具调用
- const mergedSegments = [];
- let i = 0;
- while (i < segments.length) {
- const segment = segments[i];
- if (segment.type === 'tool') {
- // 统计连续相同的工具调用
- let count = 1;
- while (i + count < segments.length &&
- segments[i + count].type === 'tool' &&
- segments[i + count].toolName === segment.toolName) {
- count++;
- }
- // 添加合并后的段落(带计数)
- mergedSegments.push({ ...segment, toolCount: count });
- i += count;
- } else {
- mergedSegments.push(segment);
- i++;
- }
- }
-
- mergedSegments.forEach((segment, index) => {
- const segmentDiv = document.createElement('div');
- segmentDiv.className = 'message-segment segment-' + segment.type;
-
- if (segment.type === 'text' && segment.content) {
- segmentDiv.className += ' segment-text';
- segmentDiv.innerHTML = formatText(segment.content);
- } else if (segment.type === 'tool') {
- // 过滤掉不需要显示的工具
- if (segment.toolName === 'spawnExplorer') {
- return;
- }
-
- // 所有工具调用都使用低调样式
- segmentDiv.className += ' low-profile';
-
- const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
- const toolResult = segment.toolResult || '';
- const toolCount = segment.toolCount || 1;
- const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
- const toolDescription = segment.toolDescription || '';
-
- // 检查工具结果是否过长(超过一行显示不下)
- const shouldCollapse = toolResult && toolResult.length > 60;
-
- segmentDiv.innerHTML = \`
-
- \${shouldCollapse ? \`\${toolResult}
\` : ''}
- \${toolDescription ? \`\${toolDescription}
\` : ''}
- \`;
-
- // 如果是仿真工具且成功完成,尝试添加波形预览
- if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
- // 尝试解析多个 VCD 文件(多 VCD 模式)
- const vcdPaths = parseMultiVcdPaths(segment.toolResult);
-
- if (vcdPaths.length > 0) {
- // 多 VCD 模式:为每个文件创建预览
- vcdPaths.forEach(vcdInfo => {
- const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
- segmentDiv.appendChild(waveformPreview);
- });
- } else {
- // 单 VCD 模式(兼容旧逻辑)
- let vcdPath = segment.vcdFilePath;
- if (!vcdPath && segment.toolResult) {
- const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
- if (match && match[1]) {
- vcdPath = match[1].trim();
- }
- }
-
- if (vcdPath) {
- const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd';
- const waveformPreview = createWaveformPreview(vcdPath, fileName);
- segmentDiv.appendChild(waveformPreview);
- }
- }
- }
-
- // 添加折叠/展开事件监听
- if (shouldCollapse) {
- setTimeout(() => {
- const header = segmentDiv.querySelector('.tool-segment-header');
- const content = segmentDiv.querySelector('.tool-segment-content');
-
- if (header && content) {
- header.addEventListener('click', function() {
- const isCollapsed = header.classList.contains('collapsed');
- const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
-
- if (isCollapsed) {
- // 展开
- header.classList.remove('collapsed');
- content.classList.remove('collapsed');
- content.style.maxHeight = content.scrollHeight + 'px';
- toolCollapseStates.set(toolIdx, false);
- } else {
- // 折叠
- header.classList.add('collapsed');
- content.classList.add('collapsed');
- content.style.maxHeight = '0';
- toolCollapseStates.set(toolIdx, true);
- }
- });
- }
- }, 0);
- }
- } else if (segment.type === 'question') {
- segmentDiv.innerHTML = \`
-
-
\${formatText(segment.question || '')}
-
- \${(segment.options || []).map(opt => \`\${opt}\`).join('')}
-
-
- \`;
- } else if (segment.type === 'agent') {
- // 智能体卡片渲染
- renderAgentCard(segment, segmentDiv);
- } else if (segment.type === 'plan') {
- // 计划卡片渲染(使用独立组件)
- renderPlanCardStatic(segment, segmentDiv);
- }
-
- container.appendChild(segmentDiv);
- });
-
- // 添加操作按钮
- const actionsDiv = document.createElement('div');
- actionsDiv.className = 'message-actions';
-
- // 复制按钮
- const copyBtn = document.createElement('button');
- copyBtn.className = 'action-btn';
- copyBtn.innerHTML = \`复制\`;
- copyBtn.onclick = () => {
- const textContent = segments
- .filter(s => s.type === 'text' && s.content)
- .map(s => s.content)
- .join('\\n');
- copyMessage(textContent, copyBtn);
- };
-
- // 点赞按钮
- const likeBtn = document.createElement('button');
- likeBtn.className = 'action-btn';
- likeBtn.innerHTML = \`点赞\`;
- likeBtn.onclick = () => toggleLike(likeBtn);
-
- // 点踩按钮
- const dislikeBtn = document.createElement('button');
- dislikeBtn.className = 'action-btn';
- dislikeBtn.innerHTML = \`点踩\`;
- dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
-
- actionsDiv.appendChild(copyBtn);
- actionsDiv.appendChild(likeBtn);
- actionsDiv.appendChild(dislikeBtn);
- container.appendChild(actionsDiv);
-
- messagesEl.appendChild(container);
- smartScrollToBottom();
- }
-
- // 格式化文本(支持 Markdown)
- function formatText(text) {
- if (!text) return '';
-
- let html = text;
-
- // 先提取并处理代码块(避免被转义)
- const codeBlocks = [];
- html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
- const language = lang || 'plaintext';
- // 转义代码内容
- const escapedCode = code.trim()
- .replace(/&/g, '&')
- .replace(//g, '>');
- // 不再手动高亮,让 highlight.js 处理
- const placeholder = \`___CODE_BLOCK_\${codeBlocks.length}___\`;
- codeBlocks.push('' + escapedCode + '
');
- return placeholder;
- });
-
- // 提取行内代码(避免被转义)
- const inlineCodes = [];
- html = html.replace(/\`([^\`]+)\`/g, function(match, code) {
- const escapedCode = code
- .replace(/&/g, '&')
- .replace(//g, '>');
- const placeholder = \`___INLINE_CODE_\${inlineCodes.length}___\`;
- inlineCodes.push('' + escapedCode + '');
- return placeholder;
- });
-
- // 转义其他 HTML 特殊字符
- html = html
+ return paths;
+ }
+
+ function formatText(text) {
+ if (!text) return '';
+ let html = text;
+ const codeBlocks = [];
+ html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
+ const language = lang || 'plaintext';
+ const escapedCode = code.trim()
.replace(/&/g, '&')
.replace(//g, '>');
-
- // 处理标题 ### Title
- html = html.replace(/^### (.+)$/gm, '$1
');
- html = html.replace(/^## (.+)$/gm, '$1
');
- html = html.replace(/^# (.+)$/gm, '$1
');
-
- // 处理粗体 **text**
- html = html.replace(/\\*\\*(.+?)\\*\\*/g, '$1');
-
- // 处理斜体 *text*
- html = html.replace(/\\*(.+?)\\*/g, '$1');
-
- // 处理无序列表 - item 或 * item
- html = html.replace(/^[\\-\\*] (.+)$/gm, '$1');
- html = html.replace(/(.*<\\/li>\\n?)+/g, '');
-
- // 处理有序列表 1. item
- html = html.replace(/^\\d+\\. (.+)$/gm, '$1');
-
- // 处理链接 [text](url)
- html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '$1');
-
- // 处理换行(在恢复代码块之前)
- html = html.replace(/\\n/g, '
');
-
- // 恢复代码块(在最后恢复,避免被其他处理影响)
- codeBlocks.forEach((block, index) => {
- html = html.replace(\`___CODE_BLOCK_\${index}___\`, block);
- });
-
- // 恢复行内代码
- inlineCodes.forEach((code, index) => {
- html = html.replace(\`___INLINE_CODE_\${index}___\`, code);
- });
-
- return html;
- }
-
- // 添加工具状态消息
- function addToolStatus(toolName, status, detail) {
- const statusIcons = {
- start: '🔧',
- complete: '✅',
- error: '❌'
- };
- const statusTexts = {
- start: '正在执行',
- complete: '执行完成',
- error: '执行失败'
- };
-
- const div = document.createElement('div');
- div.className = \`message tool-status tool-\${status}\`;
- div.innerHTML = \`
- \${statusIcons[status]}
- \${getToolDisplayName(toolName)}
- \${statusTexts[status]}
- \${detail ? \`\${detail}
\` : ''}
- \`;
- messagesEl.appendChild(div);
- smartScrollToBottom();
-
- // 添加消息后检查 header 显示状态
- checkHeaderVisibility();
+ const placeholder = \`___CODE_BLOCK_\${codeBlocks.length}___\`;
+ codeBlocks.push('' + escapedCode + '
');
+ return placeholder;
+ });
+ const inlineCodes = [];
+ html = html.replace(/\`([^\`]+)\`/g, function(match, code) {
+ const escapedCode = code
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+ const placeholder = \`___INLINE_CODE_\${inlineCodes.length}___\`;
+ inlineCodes.push('' + escapedCode + '');
+ return placeholder;
+ });
+ html = html
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+ html = html.replace(/^### (.+)$/gm, '$1
');
+ html = html.replace(/^## (.+)$/gm, '$1
');
+ html = html.replace(/^# (.+)$/gm, '$1
');
+ html = html.replace(/\\*\\*(.+?)\\*\\*/g, '$1');
+ html = html.replace(/\\*(.+?)\\*/g, '$1');
+ html = html.replace(/^[\\-\\*] (.+)$/gm, '$1');
+ html = html.replace(/(.*<\\/li>\\n?)+/g, '');
+ html = html.replace(/^\\d+\\. (.+)$/gm, '$1');
+ html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '$1');
+ html = html.replace(/\\n/g, '
');
+ codeBlocks.forEach((block, index) => {
+ html = html.replace(\`___CODE_BLOCK_\${index}___\`, block);
+ });
+ inlineCodes.forEach((code, index) => {
+ html = html.replace(\`___INLINE_CODE_\${index}___\`, code);
+ });
+ return html;
}
- // 显示用户问题
- function 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
- });
- }
-
- // 处理多问题答案提交
- function handleMultiQuestionAnswer(askId, answers, segmentDiv) {
- console.log('[WebView] 多问题答案提交:', askId, answers);
-
- // 保存答案到 Map 中
- answeredQuestions.set(askId, answers);
-
- // 标记问题已回答
- segmentDiv.classList.add('answered');
-
- // 禁用所有输入并保持选中状态的高亮
- const inputs = segmentDiv.querySelectorAll('input');
- inputs.forEach(input => {
- input.disabled = true;
- // 确保选中的选项保持高亮
- if (input.checked) {
- const label = input.closest('.question-option');
- if (label) {
- label.classList.add('selected');
- }
- }
- });
-
- // 隐藏提交按钮
- const submitBtn = segmentDiv.querySelector('.custom-submit');
- if (submitBtn) {
- submitBtn.style.display = 'none';
- }
-
- // 发送答案到后端
- vscode.postMessage({
- command: 'submitAnswer',
- askId: askId,
- answers: answers
- });
- }
-
- ${getWaveformPreviewScript()}
-
- ${getCodeHighlightScript()}
+ ${getQuestionHandlerScript()}
+ ${getMessageRendererScript()}
+ ${getSegmentRendererScript()}
`;
}
diff --git a/src/views/messageRenderer.ts b/src/views/messageRenderer.ts
new file mode 100644
index 0000000..277b707
--- /dev/null
+++ b/src/views/messageRenderer.ts
@@ -0,0 +1,215 @@
+/**
+ * 消息渲染脚本模块
+ * 功能:消息渲染、滚动控制、工具状态显示
+ * 依赖:toolHelpers, textFormatter, waveformPreviewContent, agentCard, planCard, codeHighlight
+ * 使用场景:webview 中的消息显示逻辑
+ */
+
+import { collapseIconSvg } from "../constants/toolIcons";
+import { getWaveformPreviewScript } from "./waveformPreviewContent";
+import { getAgentCardScript } from "./agentCard";
+import { getPlanCardScript } from "./planCard";
+import { getCodeHighlightScript } from "../components/codeHighlight";
+
+export function getMessageRendererScript(): string {
+ return `
+ ${getAgentCardScript()}
+ ${getPlanCardScript()}
+
+ const toolCollapseStates = new Map();
+ let shouldAutoScroll = true;
+ let lastScrollHeight = 0;
+
+ 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 {
+ const parts = text.split(' ');
+ const filePaths = [];
+ const textParts = [];
+ parts.forEach(part => {
+ if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part) || /:[0-9]+-[0-9]+$/.test(part)) {
+ filePaths.push(part);
+ } else {
+ textParts.push(part);
+ }
+ });
+ if (filePaths.length > 0) {
+ div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' ');
+ } else {
+ div.textContent = text;
+ }
+ hideHeaderIfNeeded();
+ }
+ messagesEl.appendChild(div);
+ smartScrollToBottom();
+ checkHeaderVisibility();
+ }
+
+ 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');
+ 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');
+ 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;
+ }
+ }
+
+ 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();
+ checkHeaderVisibility();
+ }
+
+ ${getWaveformPreviewScript()}
+ ${getCodeHighlightScript()}
+ `;
+}
diff --git a/src/views/messageStyles.ts b/src/views/messageStyles.ts
new file mode 100644
index 0000000..2e75afa
--- /dev/null
+++ b/src/views/messageStyles.ts
@@ -0,0 +1,622 @@
+/**
+ * 消息样式模块
+ * 功能:提供消息区域的所有 CSS 样式
+ * 依赖:agentCard, planCard, codeHighlight, waveformPreviewContent
+ * 使用场景:webview 样式注入
+ */
+
+import { getAgentCardStyles } from "./agentCard";
+import { getPlanCardStyles } from "./planCard";
+import { getCodeHighlightStyles } from "../components/codeHighlight";
+import { getWaveformPreviewContent } from "./waveformPreviewContent";
+
+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: #007ACC;
+ color: #ffffff;
+ border: 1px solid #007ACC;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+ .question-option:hover {
+ background: #005a9e;
+ border-color: #005a9e;
+ }
+ .question-option.selected {
+ background: #007ACC;
+ color: #ffffff;
+ border-color: #007ACC;
+ }
+ .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;
+ }
+
+ .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: 25px 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;
+ }
+ .icon-expanded svg path {
+ fill: #007ACC !important;
+ }
+ .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;
+ }
+ .tool-segment-description {
+ margin: 25px 0 0 0px;
+ font-size: 0.9rem;
+ color: var(--vscode-descriptionForeground);
+ line-height: 1.4;
+ }
+ .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: 12px;
+ }
+ .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: #3d3f41;
+ color: #ffffff;
+ border: 1px solid #474747;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s;
+ font-size: 13px;
+ }
+ .segment-question .question-option:hover {
+ background: #005a9e;
+ border-color: #005a9e;
+ }
+ .segment-question .question-option.selected {
+ background: #007ACC;
+ color: #ffffff;
+ border-color: #007ACC;
+ }
+ .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()}
+ `;
+}
diff --git a/src/views/questionHandler.ts b/src/views/questionHandler.ts
new file mode 100644
index 0000000..7945904
--- /dev/null
+++ b/src/views/questionHandler.ts
@@ -0,0 +1,118 @@
+/**
+ * 问题处理脚本模块
+ * 功能:用户问题交互逻辑
+ * 依赖:textFormatter
+ * 使用场景:webview 中的问题回答处理
+ */
+
+export function getQuestionHandlerScript(): string {
+ return `
+ const answeredQuestions = new Map();
+
+ 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);
+ 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
+ });
+ }
+
+ function handleMultiQuestionAnswer(askId, answers, segmentDiv) {
+ console.log('[WebView] 多问题答案提交:', askId, answers);
+ answeredQuestions.set(askId, answers);
+ segmentDiv.classList.add('answered');
+ const inputs = segmentDiv.querySelectorAll('input');
+ inputs.forEach(input => {
+ input.disabled = true;
+ if (input.checked) {
+ const label = input.closest('.question-option');
+ if (label) {
+ label.classList.add('selected');
+ }
+ }
+ });
+ const submitBtn = segmentDiv.querySelector('.custom-submit');
+ if (submitBtn) {
+ submitBtn.style.display = 'none';
+ }
+ vscode.postMessage({
+ command: 'submitAnswer',
+ askId: askId,
+ answers: answers
+ });
+ }
+
+ 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();
+ checkHeaderVisibility();
+ }
+ `;
+}
diff --git a/src/views/segmentRenderer.ts b/src/views/segmentRenderer.ts
new file mode 100644
index 0000000..7a49689
--- /dev/null
+++ b/src/views/segmentRenderer.ts
@@ -0,0 +1,274 @@
+/**
+ * 分段消息渲染脚本模块
+ * 功能:实时更新分段消息、工具调用展示
+ * 依赖:toolHelpers, textFormatter, waveformPreviewContent
+ * 使用场景:webview 中的分段消息渲染
+ */
+
+export function getSegmentRendererScript(): string {
+ return `
+ function updateSegmentsRealtime(segments, isComplete) {
+ if (isComplete && (!segments || segments.length === 0)) {
+ currentSegmentedMessage = null;
+ return;
+ }
+ if (!segments || segments.length === 0) return;
+
+ if (!currentSegmentedMessage) {
+ if (currentStreamingMessage) {
+ currentStreamingMessage.remove();
+ currentStreamingMessage = null;
+ }
+ const toolStatuses = messagesEl.querySelectorAll('.tool-status');
+ toolStatuses.forEach(el => el.remove());
+ const lastSegmented = messagesEl.querySelector('.segmented-message:last-child');
+ if (lastSegmented && !lastSegmented.querySelector('.message-actions')) {
+ currentSegmentedMessage = lastSegmented;
+ } else {
+ currentSegmentedMessage = document.createElement('div');
+ currentSegmentedMessage.className = 'message bot-message segmented-message';
+ messagesEl.appendChild(currentSegmentedMessage);
+ }
+ renderedSegmentCount = 0;
+ }
+
+ if (currentSegmentedMessage) {
+ const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
+ toolHeaders.forEach((header, idx) => {
+ const isCollapsed = header.classList.contains('collapsed');
+ toolCollapseStates.set(idx, isCollapsed);
+ });
+ }
+
+ currentSegmentedMessage.innerHTML = '';
+
+ const mergedSegments = [];
+ let i = 0;
+ while (i < segments.length) {
+ const segment = segments[i];
+ if (segment.type === 'tool') {
+ let count = 1;
+ while (i + count < segments.length &&
+ segments[i + count].type === 'tool' &&
+ segments[i + count].toolName === segment.toolName) {
+ count++;
+ }
+ mergedSegments.push({ ...segment, toolCount: count });
+ i += count;
+ } else {
+ mergedSegments.push(segment);
+ i++;
+ }
+ }
+
+ let toolIndex = 0;
+ mergedSegments.forEach((segment, index) => {
+ const segmentDiv = document.createElement('div');
+ segmentDiv.className = 'message-segment segment-' + segment.type;
+
+ if (segment.type === 'text' && segment.content) {
+ segmentDiv.className += ' segment-text';
+ segmentDiv.innerHTML = formatText(segment.content);
+ } else if (segment.type === 'tool') {
+ if (segment.toolName === 'spawnExplorer') return;
+ segmentDiv.className += ' low-profile';
+ const toolResult = segment.toolResult || '';
+ const toolCount = segment.toolCount || 1;
+ const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
+ const toolDescription = segment.toolDescription || '';
+ const shouldCollapse = toolResult && toolResult.length > 60;
+ const savedState = toolCollapseStates.get(toolIndex);
+ const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
+ const currentToolIndex = toolIndex;
+ toolIndex++;
+
+ segmentDiv.innerHTML = \`
+
+ \${shouldCollapse ? \`\${toolResult}
\` : ''}
+ \${toolDescription ? \`\${toolDescription}
\` : ''}
+ \`;
+
+ if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
+ if (typeof createWaveformPreview === 'function') {
+ const vcdPaths = parseMultiVcdPaths(segment.toolResult);
+ if (vcdPaths.length > 0) {
+ vcdPaths.forEach(vcdInfo => {
+ const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
+ segmentDiv.appendChild(waveformPreview);
+ });
+ } else {
+ let vcdPath = segment.vcdFilePath;
+ if (!vcdPath && segment.toolResult) {
+ const match = String(segment.toolResult).match(/(?:路径\\s*[::]\\s*|已生成[::]\\s*)(.+\\.vcd)/);
+ 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);
+ }
+ }
+ } else {
+ console.warn('[VCD Preview] createWaveformPreview function not found');
+ }
+ }
+
+ if (shouldCollapse) {
+ setTimeout(() => {
+ const header = segmentDiv.querySelector('.tool-segment-header');
+ const content = segmentDiv.querySelector('.tool-segment-content');
+ if (header && content) {
+ header.addEventListener('click', function() {
+ const isCollapsed = header.classList.contains('collapsed');
+ const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
+ if (isCollapsed) {
+ header.classList.remove('collapsed');
+ content.classList.remove('collapsed');
+ content.style.maxHeight = content.scrollHeight + 'px';
+ toolCollapseStates.set(toolIdx, false);
+ } else {
+ header.classList.add('collapsed');
+ content.classList.add('collapsed');
+ content.style.maxHeight = '0';
+ toolCollapseStates.set(toolIdx, true);
+ }
+ });
+ }
+ }, 0);
+ }
+ } else if (segment.type === 'question') {
+ segmentDiv.className += ' segment-question';
+ const questions = segment.questions || (segment.question ? [{
+ question: segment.question,
+ options: segment.options || [],
+ multiSelect: false
+ }] : []);
+ const isAnswered = answeredQuestions.has(segment.askId);
+ const savedAnswers = answeredQuestions.get(segment.askId) || {};
+ if (isAnswered) {
+ segmentDiv.classList.add('answered');
+ }
+
+ const questionsHtml = questions.map((q, qIndex) => {
+ const inputType = q.multiSelect ? 'checkbox' : 'radio';
+ const inputName = \`q\${qIndex}\`;
+ const selectedAnswers = savedAnswers[qIndex] || [];
+ let optionsHtml;
+ if (!q.options || q.options.length === 0) {
+ const savedText = selectedAnswers[0] || '';
+ optionsHtml = \`\`;
+ } else {
+ optionsHtml = q.options.map(opt => {
+ const isSelected = selectedAnswers.includes(opt);
+ return \`\`;
+ }).join('');
+ }
+ return \`
+
+
\${formatText(q.question)}
+
\${optionsHtml}
+
+ \`;
+ }).join('');
+
+ segmentDiv.innerHTML = \`
+ \${questionsHtml}
+
+ \`;
+
+ if (!isAnswered) {
+ setTimeout(() => {
+ const submitBtn = segmentDiv.querySelector('.custom-submit');
+ if (submitBtn) {
+ submitBtn.addEventListener('click', function() {
+ const answers = {};
+ questions.forEach((q, qIndex) => {
+ const textarea = segmentDiv.querySelector(\`textarea[name="q\${qIndex}"]\`);
+ if (textarea) {
+ const value = textarea.value.trim();
+ answers[qIndex] = value ? [value] : [];
+ } else {
+ const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
+ answers[qIndex] = Array.from(inputs).map(input => input.value);
+ }
+ });
+ handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
+ });
+ }
+ }, 0);
+ }
+ } else if (segment.type === 'agent') {
+ renderAgentCard(segment, segmentDiv);
+ } else if (segment.type === 'plan') {
+ renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
+ }
+
+ currentSegmentedMessage.appendChild(segmentDiv);
+ });
+
+ if (isComplete) {
+ console.log('[WebView] 对话完成,添加操作按钮');
+ const actionsDiv = document.createElement('div');
+ actionsDiv.className = 'message-actions';
+ const copyBtn = document.createElement('button');
+ copyBtn.className = 'action-btn';
+ copyBtn.innerHTML = \`复制\`;
+ copyBtn.onclick = () => {
+ const textContent = segments
+ .filter(s => s.type === 'text' && s.content)
+ .map(s => s.content)
+ .join('\\n');
+ copyMessage(textContent, copyBtn);
+ };
+ const likeBtn = document.createElement('button');
+ likeBtn.className = 'action-btn';
+ likeBtn.innerHTML = \`点赞\`;
+ likeBtn.onclick = () => toggleLike(likeBtn);
+ const dislikeBtn = document.createElement('button');
+ dislikeBtn.className = 'action-btn';
+ dislikeBtn.innerHTML = \`点踩\`;
+ dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
+ actionsDiv.appendChild(copyBtn);
+ actionsDiv.appendChild(likeBtn);
+ actionsDiv.appendChild(dislikeBtn);
+ currentSegmentedMessage.appendChild(actionsDiv);
+ currentSegmentedMessage = null;
+ }
+
+ smartScrollToBottom();
+ }
+
+ function renderSegments(segments) {
+ console.log('[WebView] renderSegments 被调用, segments:', segments);
+ if (!segments || segments.length === 0) {
+ console.log('[WebView] segments 为空,跳过渲染');
+ return;
+ }
+ if (currentStreamingMessage) {
+ console.log('[WebView] 移除流式消息');
+ currentStreamingMessage.remove();
+ currentStreamingMessage = null;
+ }
+ const toolStatuses = messagesEl.querySelectorAll('.tool-status');
+ console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length);
+ toolStatuses.forEach(el => {
+ console.log('[WebView] 移除工具状态消息:', el.className);
+ el.remove();
+ });
+ const container = document.createElement('div');
+ container.className = 'message bot-message segmented-message';
+ updateSegmentsRealtime(segments, true);
+ messagesEl.appendChild(container);
+ smartScrollToBottom();
+ }
+ `;
+}
diff --git a/src/views/textFormatter.ts b/src/views/textFormatter.ts
new file mode 100644
index 0000000..9c0daee
--- /dev/null
+++ b/src/views/textFormatter.ts
@@ -0,0 +1,56 @@
+/**
+ * 文本格式化模块
+ * 功能:Markdown 文本转 HTML
+ * 依赖:无
+ * 使用场景:消息内容格式化显示
+ */
+
+export function formatText(text: string): string {
+ if (!text) return "";
+
+ let html = text;
+
+ const codeBlocks: string[] = [];
+ html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
+ const language = lang || "plaintext";
+ const escapedCode = code
+ .trim()
+ .replace(/&/g, "&")
+ .replace(//g, ">");
+ const placeholder = `___CODE_BLOCK_${codeBlocks.length}___`;
+ codeBlocks.push(`${escapedCode}
`);
+ return placeholder;
+ });
+
+ const inlineCodes: string[] = [];
+ html = html.replace(/`([^`]+)`/g, (match, code) => {
+ const escapedCode = code.replace(/&/g, "&").replace(//g, ">");
+ const placeholder = `___INLINE_CODE_${inlineCodes.length}___`;
+ inlineCodes.push(`${escapedCode}`);
+ return placeholder;
+ });
+
+ html = html.replace(/&/g, "&").replace(//g, ">");
+
+ html = html.replace(/^### (.+)$/gm, "$1
");
+ html = html.replace(/^## (.+)$/gm, "$1
");
+ html = html.replace(/^# (.+)$/gm, "$1
");
+ html = html.replace(/\*\*(.+?)\*\*/g, "$1");
+ html = html.replace(/\*(.+?)\*/g, "$1");
+ html = html.replace(/^[\-\*] (.+)$/gm, "$1");
+ html = html.replace(/(.*<\/li>\n?)+/g, "");
+ html = html.replace(/^\d+\. (.+)$/gm, "$1");
+ html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1');
+ html = html.replace(/\n/g, "
");
+
+ codeBlocks.forEach((block, index) => {
+ html = html.replace(`___CODE_BLOCK_${index}___`, block);
+ });
+
+ inlineCodes.forEach((code, index) => {
+ html = html.replace(`___INLINE_CODE_${index}___`, code);
+ });
+
+ return html;
+}
diff --git a/src/views/toolHelpers.ts b/src/views/toolHelpers.ts
new file mode 100644
index 0000000..bc0de77
--- /dev/null
+++ b/src/views/toolHelpers.ts
@@ -0,0 +1,106 @@
+/**
+ * 工具辅助函数模块
+ * 功能:工具图标、名称映射、VCD 路径解析
+ * 依赖:toolIcons
+ * 使用场景:工具调用显示
+ */
+
+import {
+ fileWriteIconSvg,
+ fileReadIconSvg,
+ fileDeleteIconSvg,
+ syntaxCheckIconSvg,
+ SearchCode,
+ saveKnowledgeIconSvg,
+ simulationIconSvg,
+ waveformIconSvg,
+ knowledgeLoadIconSvg,
+ stateTransitionIconSvg,
+ userQuestionIconSvg,
+ updateStageIconSvg,
+ successIconSvg,
+} from "../constants/toolIcons";
+
+export function getToolIcon(toolName: string): string {
+ const iconMap: Record = {
+ file_read: fileReadIconSvg,
+ file_write: fileWriteIconSvg,
+ file_delete: fileDeleteIconSvg,
+ file_list: SearchCode,
+ 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: SearchCode,
+ addPlan: fileWriteIconSvg,
+ addEdge: fileWriteIconSvg,
+ showPlan: SearchCode,
+ addRule: fileWriteIconSvg,
+ updateNode: fileWriteIconSvg,
+ addStateTransition: stateTransitionIconSvg,
+ askUser: userQuestionIconSvg,
+ updatePhase: updateStageIconSvg,
+ iverilog: successIconSvg,
+ };
+ return iconMap[toolName] || "";
+}
+
+export function getToolDisplayName(toolName: string): string {
+ const toolNameMap: Record = {
+ 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: "用户提问",
+ updatePhase: "已更新阶段",
+ iverilog: "已完成编译",
+ };
+ return toolNameMap[toolName] || toolName;
+}
+
+export function parseMultiVcdPaths(toolResult: string): Array<{ name: string; path: string }> {
+ if (!toolResult) return [];
+ const result = String(toolResult);
+
+ const vcdListMatch = result.match(/VCD 文件列表:[\s\S]*?(?=\n\n|$)/);
+ if (!vcdListMatch) return [];
+
+ const paths: Array<{ name: string; path: string }> = [];
+ 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;
+}