From f2382a8eed1ac4cb5026083cdede7ccf348d0d2d Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Tue, 16 Dec 2025 16:58:35 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=E5=AE=9E=E7=8E=B0=E6=B3=A2=E5=BD=A2?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/波形预览功能技术文档.md | 444 ++++++++++++++++++++++++++++ src/panels/ICHelperPanel.ts | 182 ++++++++++++ src/utils/messageHandler.ts | 24 +- src/views/waveformPreviewContent.ts | 350 ++++++++++++++++++++++ src/views/webviewContent.ts | 28 ++ 5 files changed, 1020 insertions(+), 8 deletions(-) create mode 100644 docs/波形预览功能技术文档.md create mode 100644 src/views/waveformPreviewContent.ts diff --git a/docs/波形预览功能技术文档.md b/docs/波形预览功能技术文档.md new file mode 100644 index 0000000..3102c62 --- /dev/null +++ b/docs/波形预览功能技术文档.md @@ -0,0 +1,444 @@ +# 波形预览功能技术文档 + +## 功能概述 + +在对话界面中显示 VCD 波形文件的预览卡片,用户可以查看前几个信号的真实波形,并通过"展开查看"按钮打开完整的波形查看器。 + +## 功能流程 + +``` +用户输入"生成VCD"命令 + ↓ +系统执行 iverilog 编译和仿真 + ↓ +生成 VCD 文件 + ↓ +在对话界面显示波形预览卡片 + ├─ 显示真实的波形图(前3个信号) + ├─ 显示信号名称和波形 + └─ "展开查看"按钮 + ↓ +点击"展开查看"按钮 + ↓ +打开完整的 VCDViewerPanel 波形查看器 +``` + +## 文件结构 + +### 1. `src/views/waveformPreviewContent.ts` +**功能:** 波形预览组件的独立模块 + +**导出函数:** +- `getWaveformPreviewContent()` - 返回波形预览组件的 CSS 样式 +- `getWaveformPreviewScript()` - 返回波形预览组件的 JavaScript 代码 + +**主要功能:** +- 创建波形预览卡片的 HTML 结构 +- 从 VCD 文件中提取真实信号数据 +- 使用 SVG 绘制波形图 + - 单比特信号:绘制数字波形(高/低电平) + - 多比特信号:绘制总线波形(梯形) +- 处理"展开查看"按钮点击事件 + +**关键函数:** +```javascript +createWaveformPreview(vcdFilePath, fileName) + - 创建波形预览卡片的 DOM 结构 + - 包含头部(标题 + 展开按钮)和内容区域 + +loadMiniWaveform(containerId, vcdFilePath, loadingDiv) + - 请求后端获取 VCD 文件信息 + +renderWaveformInfo(containerId, vcdInfo) + - 接收 VCD 信息并渲染波形 + +drawRealWaveform(signals) + - 根据真实信号数据绘制 SVG 波形图 + - 支持单比特和多比特信号 + - 使用不同颜色区分信号 + +openFullWaveform(vcdFilePath) + - 发送消息打开完整波形查看器 + +addWaveformPreviewToMessage(messageDiv, vcdFilePath, fileName) + - 将波形预览组件添加到消息中 +``` + +--- + +### 2. `src/views/webviewContent.ts` +**功能:** 主 WebView 页面,集成波形预览组件 + +**修改内容:** +- 导入波形预览组件的样式和脚本 +- 在 ` + ${getConversationHistoryBarContent()}
IC Coder @@ -553,35 +571,7 @@ export function getWebviewContent(iconUri?: string): string {
-
-

📁 文件读取

-
- - -
-
- 文件内容将在这里显示... -
- -
- -
-

✏️ 编辑文件:

- -
- - -
-
-
-
- 👋 你好!我是 IC Coder 助手,可以帮你生成代码、回答问题。 -
-
+
@@ -823,10 +813,20 @@ export function getWebviewContent(iconUri?: string): string { div.appendChild(actionsDiv); } else { div.textContent = text; + // 当添加用户消息时,隐藏 header + hideHeaderIfNeeded(); } messagesEl.appendChild(div); messagesEl.scrollTop = messagesEl.scrollHeight; + + // 添加消息后检查 header 显示状态 + checkHeaderVisibility(); + } + + // 检查是否需要隐藏 header + function hideHeaderIfNeeded() { + checkHeaderVisibility(); } function copyMessage(text, button) { @@ -995,6 +995,32 @@ export function getWebviewContent(iconUri?: string): string { renderWaveformInfo(message.containerId, message.vcdInfo); } break; + case 'conversationHistory': + // 接收到会话历史数据 + if (message.history) { + renderConversationHistory(message.history); + } + break; + case 'conversationLoaded': + // 会话加载成功,清空当前消息并显示历史消息 + messagesEl.innerHTML = ''; + if (message.messages && message.messages.length > 0) { + message.messages.forEach(msg => { + addMessage(msg.text, msg.sender); + }); + } + currentConversationId = message.conversationId; + break; + case 'newConversationCreated': + // 新会话创建成功,清空消息区域 + messagesEl.innerHTML = ''; + currentConversationId = message.conversationId; + // 显示 header + const header = document.querySelector('.header'); + if (header) { + header.classList.remove('hidden'); + } + break; } }); @@ -1017,9 +1043,24 @@ export function getWebviewContent(iconUri?: string): string { // 初始化时调整一次高度 autoResizeTextarea(); + // 初始化时检查是否需要显示 header + checkHeaderVisibility(); + messageInput.focus(); + // 检查 header 显示状态 + function checkHeaderVisibility() { + const allMessages = messagesEl.querySelectorAll('.message'); + const header = document.querySelector('.header'); + if (allMessages.length > 0 && header) { + header.classList.add('hidden'); + } else if (header) { + header.classList.remove('hidden'); + } + } + ${getWaveformPreviewScript()} + ${getConversationHistoryBarScript()} `; From b0e199589705b8a89a9732b155909dcf1b6c2d70 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Wed, 17 Dec 2025 10:03:43 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E5=B0=8F=E7=BB=86?= =?UTF-8?q?=E8=8A=82=E8=BE=93=E5=85=A5=E6=A1=86=E7=9A=84=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/webviewContent.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 271f4d0..3674dbe 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -195,6 +195,7 @@ export function getWebviewContent(iconUri?: string): string { align-items: center; justify-content: space-between; gap: 10px; + margin-bottom: -17px; } .mode-selector { display: flex; @@ -208,12 +209,13 @@ export function getWebviewContent(iconUri?: string): string { } .mode-selector select { padding: 2px 4px; - background: transparent; + background: var(--vscode-input-background); color: var(--vscode-foreground); border: none; cursor: pointer; font-size: 12px; outline: none; + border-radius: 4px; } .mode-selector select:hover { background: var(--vscode-list-hoverBackground); From 10f0877a5e32f81be27e2c4917635c8bb7ce3155 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Wed, 24 Dec 2025 10:01:53 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DAI=E8=AF=A2?= =?UTF-8?q?=E9=97=AE=E6=97=B6=E9=80=89=E9=A1=B9=E7=82=B9=E5=87=BB=E5=90=8E?= =?UTF-8?q?=E9=80=89=E4=B8=AD=E7=8A=B6=E6=80=81=E4=B8=A2=E5=A4=B1=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 answeredQuestions Map 存储已回答问题的状态 - 在重新渲染时恢复选中状态和 answered 类 - 已回答的问题自动隐藏输入框并禁用点击事件 - 确保用户选择在页面更新时保持显示 --- .vscode/settings.json | 4 +- package.json | 2 +- src/config/settings.ts | 20 +- src/services/dialogService.ts | 19 + src/services/userInteraction.ts | 11 +- src/utils/messageHandler.ts | 47 +- src/views/inputArea.ts | 547 +++++++++++++ src/views/messageArea.ts | 1068 +++++++++++++++++++++++++ src/views/webviewContent.ts | 1303 +++---------------------------- 9 files changed, 1763 insertions(+), 1258 deletions(-) create mode 100644 src/views/inputArea.ts create mode 100644 src/views/messageArea.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 16a5c02..20d666f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,7 @@ "dist": true // set this to false to include "dist" folder in search results }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" + "typescript.tsc.autoDetect": "off", + // IC Coder 后端服务地址 + "icCoder.backendUrl": "http://192.168.1.108:2233" } diff --git a/package.json b/package.json index aad18aa..5a8eb53 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "properties": { "icCoder.backendUrl": { "type": "string", - "default": "http://localhost:2233", + "default": "http://192.168.1.108:2233", "description": "后端服务地址" }, "icCoder.timeout": { diff --git a/src/config/settings.ts b/src/config/settings.ts index f5348f3..e6c041f 100644 --- a/src/config/settings.ts +++ b/src/config/settings.ts @@ -2,7 +2,7 @@ * 配置管理 * 从 VSCode 设置读取配置项 */ -import * as vscode from 'vscode'; +import * as vscode from "vscode"; /** 配置项接口 */ export interface IccoderConfig { @@ -16,21 +16,21 @@ export interface IccoderConfig { /** 默认配置 */ const DEFAULT_CONFIG: IccoderConfig = { - backendUrl: 'http://localhost:8080', + backendUrl: "http://localhost:8080", timeout: 60000, - userId: 'default-user' + userId: "default-user", }; /** * 获取配置项 */ export function getConfig(): IccoderConfig { - const config = vscode.workspace.getConfiguration('icCoder'); + const config = vscode.workspace.getConfiguration("icCoder"); return { - backendUrl: config.get('backendUrl', DEFAULT_CONFIG.backendUrl), - timeout: config.get('timeout', DEFAULT_CONFIG.timeout), - userId: config.get('userId', DEFAULT_CONFIG.userId) + backendUrl: config.get("backendUrl", DEFAULT_CONFIG.backendUrl), + timeout: config.get("timeout", DEFAULT_CONFIG.timeout), + userId: config.get("userId", DEFAULT_CONFIG.userId), }; } @@ -40,7 +40,9 @@ export function getConfig(): IccoderConfig { export function getApiUrl(path: string): string { const { backendUrl } = getConfig(); // 确保 URL 格式正确 - const baseUrl = backendUrl.endsWith('/') ? backendUrl.slice(0, -1) : backendUrl; - const apiPath = path.startsWith('/') ? path : `/${path}`; + const baseUrl = backendUrl.endsWith("/") + ? backendUrl.slice(0, -1) + : backendUrl; + const apiPath = path.startsWith("/") ? path : `/${path}`; return `${baseUrl}${apiPath}`; } diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index a106a01..4ef9bd8 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -37,6 +37,8 @@ export interface DialogCallbacks { onToolError?: (toolName: string, error: string) => void; /** 显示问题(ask_user) */ onQuestion?: (askId: string, question: string, options: string[]) => void; + /** 实时更新段落(流式过程中) */ + onSegmentUpdate?: (segments: MessageSegment[]) => void; /** 对话完成,返回所有段落 */ onComplete?: (segments: MessageSegment[]) => void; /** 错误 */ @@ -155,6 +157,8 @@ export class DialogSession { this.appendText(data.text); console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length); callbacks.onText?.(this.accumulatedText, true); + // 实时发送段落更新 + callbacks.onSegmentUpdate?.(this.segments); }, onToolCall: async (data: ToolCallRequest) => { @@ -166,16 +170,22 @@ export class DialogSession { console.log('[DialogSession] onToolCall: 跳过重复的工具段落:', toolName); } else { this.addToolSegment(toolName, 'running'); + // 实时发送段落更新 + callbacks.onSegmentUpdate?.(this.segments); } // 注意:不在这里调用 callbacks.onToolStart,避免与 onToolStart 事件重复 try { await executeToolCall(data, this.toolContext); this.updateToolSegment(toolName, 'success', '执行完成'); + // 实时发送段落更新 + callbacks.onSegmentUpdate?.(this.segments); // 也不调用 callbacks.onToolComplete,避免重复 } catch (error) { const errorMsg = error instanceof Error ? error.message : '未知错误'; this.updateToolSegment(toolName, 'error', errorMsg); callbacks.onToolError?.(toolName, errorMsg); + // 实时发送段落更新 + callbacks.onSegmentUpdate?.(this.segments); } }, @@ -187,6 +197,8 @@ export class DialogSession { console.log('[DialogSession] 跳过重复的工具段落:', data.tool_name); } else { this.addToolSegment(data.tool_name, 'running'); + // 实时发送段落更新 + callbacks.onSegmentUpdate?.(this.segments); } console.log('[DialogSession] segments 数量:', this.segments.length); callbacks.onToolStart?.(data.tool_name); @@ -195,11 +207,15 @@ export class DialogSession { onToolComplete: (data) => { this.updateToolSegment(data.tool_name, 'success', data.result); callbacks.onToolComplete?.(data.tool_name, data.result); + // 实时发送段落更新 + callbacks.onSegmentUpdate?.(this.segments); }, onToolError: (data) => { this.updateToolSegment(data.tool_name, 'error', data.error); callbacks.onToolError?.(data.tool_name, data.error); + // 实时发送段落更新 + callbacks.onSegmentUpdate?.(this.segments); }, onAskUser: async (data: AskUserEvent) => { @@ -210,6 +226,9 @@ export class DialogSession { question: data.question, options: data.options }); + // 实时发送段落更新(包含问题) + callbacks.onSegmentUpdate?.(this.segments); + // 同时调用 onQuestion 用于更新状态栏等 callbacks.onQuestion?.(data.askId, data.question, data.options); try { await userInteractionManager.handleAskUser(data, this.taskId); diff --git a/src/services/userInteraction.ts b/src/services/userInteraction.ts index f22455d..f5e9a21 100644 --- a/src/services/userInteraction.ts +++ b/src/services/userInteraction.ts @@ -42,15 +42,8 @@ export class UserInteractionManager { console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`); - // 通过 WebView 显示问题 - if (this.webviewPanel) { - this.webviewPanel.webview.postMessage({ - command: 'showQuestion', - askId, - question, - options - }); - } + // 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理 + // 这里不再单独发送 showQuestion 命令,避免重复显示 // 创建 Promise 等待用户回答 return new Promise((resolve, reject) => { diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 500ab75..b6cdb3f 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -116,23 +116,19 @@ async function handleUserMessageWithBackend( return new Promise((resolve, reject) => { currentSession!.sendMessage(text, { onText: (fullText, isStreaming) => { - if (isStreaming) { - // 流式更新消息 - panel.webview.postMessage({ - command: "updateStreamingMessage", - text: fullText, - }); - } - // 注意:完成时通过 onComplete 发送分段消息 + // 不再单独处理文本,统一通过 onSegmentUpdate 处理 + }, + + onSegmentUpdate: (segments) => { + // 实时发送段落更新,按后端返回顺序展示 + panel.webview.postMessage({ + command: "updateSegments", + segments: segments, + }); }, onToolStart: (toolName) => { - // 实时显示工具状态 - panel.webview.postMessage({ - command: "toolStart", - toolName, - }); - // 同时更新状态栏 + // 更新状态栏 panel.webview.postMessage({ command: "updateStatus", text: `正在执行 ${toolName}...`, @@ -141,25 +137,15 @@ async function handleUserMessageWithBackend( }, onToolComplete: (toolName, result) => { - // 实时更新工具状态 - panel.webview.postMessage({ - command: "toolComplete", - toolName, - result, - }); + // 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新 }, onToolError: (toolName, error) => { - // 实时显示工具错误 - panel.webview.postMessage({ - command: "toolError", - toolName, - error, - }); + // 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新 }, onQuestion: (askId, question, options) => { - // 问题会在分段消息中显示,这里只更新状态栏 + // 只更新状态栏,问题显示由 onSegmentUpdate 统一处理 panel.webview.postMessage({ command: "updateStatus", text: "等待用户回答...", @@ -173,13 +159,14 @@ async function handleUserMessageWithBackend( command: "hideStatus", }); - // 发送分段消息 - console.log('[MessageHandler] 发送分段消息, 段落数:', segments.length); + // 最后一次发送完整的段落 + console.log('[MessageHandler] 对话完成, 段落数:', segments.length); console.log('[MessageHandler] segments 内容:', JSON.stringify(segments)); const result = await panel.webview.postMessage({ - command: "receiveSegments", + command: "updateSegments", segments: segments, + isComplete: true, }); console.log('[MessageHandler] postMessage 返回值:', result); diff --git a/src/views/inputArea.ts b/src/views/inputArea.ts new file mode 100644 index 0000000..ad52f4c --- /dev/null +++ b/src/views/inputArea.ts @@ -0,0 +1,547 @@ +import { getWaveformPreviewContent } from "./waveformPreviewContent"; + +/** + * 获取输入区域的 HTML 内容 + */ +export function getInputAreaContent(): string { + return ` +
+
+
+ +
+
+
+ + 切换模型 +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + +
+ 0% +
+ + +
+
+
+ 0k / 200k 已用上下文 +
+ +
+
+
+ + +
+ + 一键优化 +
+ + +
+
+
+
+
+ `; +} + +/** + * 获取输入区域的样式 + */ +export function getInputAreaStyles(): string { + return ` + .input-area { + border-top: 1px solid var(--vscode-panel-border); + padding-top: 15px; + flex-shrink: 0; + } + .input-group { + display: flex; + flex-direction: column; + gap: 10px; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + border-radius: 8px; + padding: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 6px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + } + .input-group:hover { + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2), 0 3px 8px rgba(0, 0, 0, 0.15); + } + .input-group:focus-within { + border-color: var(--vscode-focusBorder); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25), 0 3px 10px rgba(0, 0, 0, 0.2); + } + .input-wrapper { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + } + .input-bottom-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: -17px; + } + .mode-selector { + display: flex; + align-items: center; + position: relative; + } + .input-actions { + display: flex; + align-items: center; + gap: 10px; + } + .mode-selector select { + padding: 2px 4px; + background: var(--vscode-input-background); + color: var(--vscode-foreground); + border: none; + cursor: pointer; + font-size: 12px; + outline: none; + border-radius: 4px; + } + .mode-selector select:hover { + background: var(--vscode-list-hoverBackground); + } + /* Tooltip 样式 */ + .tooltip { + position: relative; + display: inline-block; + } + .tooltip .tooltiptext { + visibility: hidden; + width: auto; + background: #1e1e1e; + color: #ffffff; + text-align: center; + border-radius: 6px; + padding: 6px 12px; + position: absolute; + z-index: 1000; + bottom: 150%; + left: 50%; + transform: translateX(-50%) translateY(10px); + opacity: 0; + transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); + font-size: 12px; + font-weight: 500; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6), 0 2px 4px rgba(0, 0, 0, 0.3); + white-space: nowrap; + letter-spacing: 0.3px; + } + .tooltip .tooltiptext::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -6px; + border-width: 6px; + border-style: solid; + border-color: #1e1e1e transparent transparent transparent; + } + .tooltip .tooltiptext::before { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -7px; + border-width: 7px; + border-style: solid; + border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent; + z-index: -1; + } + .tooltip:hover .tooltiptext { + visibility: visible; + opacity: 1; + transform: translateX(-50%) translateY(0); + } + textarea { + width: 100%; + padding: 10px; + background: transparent; + color: var(--vscode-input-foreground); + border: none; + border-radius: 4px; + font-family: inherit; + resize: none; + min-height: 40px; + max-height: 200px; + outline: none; + box-sizing: border-box; + overflow-y: auto; + line-height: 1.5; + } + /* 简洁的滚动条样式 */ + textarea::-webkit-scrollbar { + width: 8px; + } + textarea::-webkit-scrollbar-track { + background: transparent; + } + textarea::-webkit-scrollbar-thumb { + background: rgba(128, 128, 128, 0.5); + border-radius: 4px; + } + textarea::-webkit-scrollbar-button { + display: none; + } + button { + padding: 0 20px; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 4px; + cursor: pointer; + } + .optimize-button { + padding: 8px; + background: transparent; + color: var(--vscode-foreground); + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.2s ease; + width: 32px; + height: 32px; + } + .optimize-button:hover { + opacity: 0.7; + } + .optimize-button svg { + width: 16px; + height: 16px; + } + .optimize-button-wrapper { + display: flex; + align-items: flex-end; + } + /* 上下文显示样式 */ + .context-display { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + } + .context-info { + display: flex; + align-items: center; + gap: 6px; + height: 40px; + background: transparent; + border: none; + border-radius: 4px; + font-size: 14px; + font-weight: 500; + color: var(--vscode-foreground); + transition: opacity 0.3s ease; + box-shadow: none; + position: relative; + overflow: hidden; + cursor: pointer; + } + .context-info:hover { + opacity: 0.8; + } + .database-icon { + display: flex; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; + position: relative; + } + .db-svg { + width: 100%; + height: 100%; + } + .db-body { + fill: #ffffff; + } + .db-fill { + fill: #409eff; + transition: all 0.3s ease; + } + .context-percentage { + font-size: 14px; + font-weight: 500; + color: var(--vscode-foreground); + text-align: right; + } + /* 上下文信息弹窗样式 */ + .context-panel { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 8px; + z-index: 1000; + animation: fadeInUp 0.2s ease-out; + display: none; + } + .context-panel.active { + display: block; + } + .context-panel::after { + content: ""; + position: absolute; + bottom: -6px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 6px solid #ffffff; + } + .context-panel-content { + background: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 8px; + padding: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(10px); + min-width: 160px; + } + .context-info-text { + font-size: 12px; + color: #374151; + text-align: center; + margin-bottom: 8px; + white-space: nowrap; + } + .compress-button { + width: 100%; + background: linear-gradient(145deg, #3b82f6 0%, #1d4ed8 100%); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 6px; + color: white; + font-size: 12px; + font-weight: 500; + padding: 6px 12px; + cursor: pointer; + transition: all 0.2s ease; + } + .compress-button:hover { + background: linear-gradient(145deg, #2563eb 0%, #1e40af 100%); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); + } + .compress-button:active { + transform: translateY(0); + } + @keyframes fadeInUp { + from { + opacity: 0; + transform: translateX(-50%) translateY(10px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } + } + `; +} + +/** + * 获取输入区域的脚本 + */ +export function getInputAreaScript(): string { + return ` + // 自动调整 textarea 高度 + function autoResizeTextarea() { + if (messageInput) { + messageInput.style.height = 'auto'; + messageInput.style.height = messageInput.scrollHeight + 'px'; + } + } + + // 监听输入事件,自动调整高度 + if (messageInput) { + messageInput.addEventListener('input', autoResizeTextarea); + + // 初始化时调整一次高度 + autoResizeTextarea(); + + // 聚焦到输入框 + messageInput.focus(); + } + + function sendMessage() { + const text = messageInput.value.trim(); + if (!text) return; + + const modeSelect = document.getElementById('modeSelect'); + const mode = modeSelect ? modeSelect.value : 'agent'; + + addMessage(text, 'user'); + vscode.postMessage({ command: 'sendMessage', text: text, mode: mode }); + messageInput.value = ''; + autoResizeTextarea(); // 重置输入框高度 + messageInput.focus(); + + // 重置优化状态 + resetOptimizeButton(); + } + + let isOptimized = false; // 标记是否已优化 + let originalText = ''; // 保存原始文本用于撤回 + + function handleOptimize() { + if (isOptimized) { + // 撤回操作 + messageInput.value = originalText; + resetOptimizeButton(); + } else { + // 优化操作 + originalText = messageInput.value; // 保存原始文本 + + // 使用死数据替换输入框内容 + const optimizedTexts = [ + '请帮我优化这段代码,提高性能和可读性', + '请分析这个问题并给出最佳解决方案', + '请帮我重构这段代码,使其更加简洁高效', + '请检查代码中的潜在问题并提供改进建议' + ]; + const randomText = optimizedTexts[Math.floor(Math.random() * optimizedTexts.length)]; + messageInput.value = randomText; + + // 切换到撤回状态 + isOptimized = true; + updateOptimizeButton(); + } + + messageInput.focus(); + autoResizeTextarea(); + } + + function updateOptimizeButton() { + const optimizeIcon = document.getElementById('optimizeIcon'); + const optimizeTooltip = document.getElementById('optimizeTooltip'); + + if (optimizeIcon && optimizeTooltip) { + // 切换为撤回图标 + optimizeIcon.innerHTML = ''; + optimizeTooltip.textContent = '撤回'; + } + } + + function resetOptimizeButton() { + const optimizeIcon = document.getElementById('optimizeIcon'); + const optimizeTooltip = document.getElementById('optimizeTooltip'); + + if (optimizeIcon && optimizeTooltip) { + // 切换回优化图标(星星图标) + optimizeIcon.innerHTML = ''; + optimizeTooltip.textContent = '一键优化'; + } + + isOptimized = false; + originalText = ''; + } + + // 上下文面板相关函数 + function toggleContextPanel() { + const contextPanel = document.getElementById('contextPanel'); + if (contextPanel) { + if (contextPanel.classList.contains('active')) { + contextPanel.classList.remove('active'); + } else { + contextPanel.classList.add('active'); + } + } + } + + function compressConversation() { + // 发送压缩会话请求 + vscode.postMessage({ command: 'compressConversation' }); + addMessage('正在压缩会话...', 'bot'); + + // 关闭面板 + const contextPanel = document.getElementById('contextPanel'); + if (contextPanel) { + contextPanel.classList.remove('active'); + } + } + + function updateContextDisplay(currentTokens, maxTokens) { + const percentage = Math.min(Math.round((currentTokens / maxTokens) * 100), 100); + + // 更新百分比显示 + const contextPercentage = document.getElementById('contextPercentage'); + if (contextPercentage) { + contextPercentage.textContent = percentage + '%'; + } + + // 更新详细信息 + const contextInfoText = document.getElementById('contextInfoText'); + if (contextInfoText) { + const currentK = Math.round((currentTokens / 1000) * 10) / 10; + const maxK = Math.round(maxTokens / 1000); + contextInfoText.textContent = \`\${currentK}k / \${maxK}k 已用上下文\`; + } + + // 更新SVG填充效果(从下往上填充) + const fillRect = document.getElementById('fillRect'); + if (fillRect) { + const fillHeight = (1024 * percentage) / 100; + const fillY = 1024 - fillHeight; + fillRect.setAttribute('y', fillY.toString()); + fillRect.setAttribute('height', fillHeight.toString()); + } + } + + // 点击外部关闭上下文面板 + document.addEventListener('click', (event) => { + const contextDisplay = document.querySelector('.context-display'); + const contextPanel = document.getElementById('contextPanel'); + + if (contextPanel && contextPanel.classList.contains('active') && contextDisplay) { + if (!contextDisplay.contains(event.target)) { + contextPanel.classList.remove('active'); + } + } + }); + `; +} diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts new file mode 100644 index 0000000..a70cbcf --- /dev/null +++ b/src/views/messageArea.ts @@ -0,0 +1,1068 @@ +/** + * 消息区域模块 + * + * 功能说明: + * - 负责聊天消息的显示和渲染 + * - 支持用户消息和 AI 消息的不同样式 + * - 提供消息操作功能(复制、点赞、点踩) + * - 支持流式消息实时更新 + * - 支持分段消息渲染(文本、工具调用、用户问题) + * - 显示工具执行状态和加载指示器 + */ + +/** + * 获取消息区域的 HTML 内容 + */ +export function getMessageAreaContent(): string { + return `
`; +} + +/** + * 获取消息区域的样式 + */ +export function getMessageAreaStyles(): string { + return ` + .messages { + flex: 1; + overflow-y: auto; + margin-bottom: 15px; + min-height: 0; + } + .message { + margin-bottom: 12px; + } + .user-message { + padding: 10px 15px; + border-radius: 8px; + background: var(--vscode-button-secondaryBackground); + border: 1px solid var(--vscode-input-border); + margin-left: auto; + width: fit-content; + max-width: 80%; + } + .bot-message { + padding: 0; + text-align: left; + color: var(--vscode-foreground); + max-width: 100%; + position: relative; + } + .message-actions { + display: flex; + gap: 8px; + margin-top: 12px; + margin-left: 10px; + opacity: 0.85; + transition: opacity 0.2s ease; + } + .message-actions:hover { + opacity: 1; + } + .action-btn { + background: transparent; + border: none; + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + color: var(--vscode-foreground); + opacity: 0.9; + transition: opacity 0.2s ease; + position: relative; + } + .action-btn:hover { + opacity: 1; + } + .action-btn svg { + width: 14px; + height: 14px; + } + .action-btn.active { + color: var(--vscode-button-background); + opacity: 1; + } + .action-btn .action-tooltip { + visibility: hidden; + width: auto; + background: #1e1e1e; + color: #ffffff; + text-align: center; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 4px 8px; + position: absolute; + z-index: 1000; + bottom: 125%; + left: 50%; + transform: translateX(-50%) translateY(5px); + opacity: 0; + transition: all 0.2s ease; + font-size: 12px;white-space: nowrap;pointer-events: none; + } + .action-btn .action-tooltip::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #1e1e1e transparent transparent transparent; + } + .action-btn .action-tooltip::before { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -6px; + border-width: 6px; + border-style: solid; + border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent; + z-index: -1; + } + .action-btn:hover .action-tooltip { + visibility: visible; + opacity: 1; + transform: translateX(-50%) translateY(0); + } + + /* 流式消息样式 */ + .streaming .message-content { + border-right: 2px solid var(--vscode-focusBorder); + animation: blink 1s infinite; + } + @keyframes blink { + 0%, 50% { border-color: var(--vscode-focusBorder); } + 51%, 100% { border-color: transparent; } + } + + /* 加载指示器样式 */ + .loading-message { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + color: var(--vscode-descriptionForeground); + } + .loading-dots { + display: flex; + gap: 4px;} + .loading-dots span { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--vscode-focusBorder); + animation: loadingDot 1.4s infinite ease-in-out; + } + .loading-dots span:nth-child(1) { animation-delay: 0s; } + .loading-dots span:nth-child(2) { animation-delay: 0.2s; } + .loading-dots span:nth-child(3) { animation-delay: 0.4s; } + @keyframes loadingDot { + 0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; } + 40% { transform: scale(1); opacity: 1; } + } + .loading-text { + font-size: 13px; + } + + /* 工具状态样式 */ + .tool-status { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin: 4px 0; + font-size: 12px; + border-radius: 6px; + background: var(--vscode-textBlockQuote-background); + } + .tool-status.tool-start { + border-left: 3px solid var(--vscode-charts-blue); + } + .tool-status.tool-complete { + border-left: 3px solid var(--vscode-charts-green); + } + .tool-status.tool-error { + border-left: 3px solid var(--vscode-charts-red); + } + .tool-icon { + font-size: 14px; + } + .tool-name { + font-weight: 500; + color: var(--vscode-foreground); + } + .tool-status-text { + color: var(--vscode-descriptionForeground); + } + .tool-detail { + margin-top: 4px; + font-size: 11px; + color: var(--vscode-descriptionForeground); + white-space: pre-wrap; + max-height: 100px; + overflow-y: auto; + } + + /* 用户问题样式 */ + .question-message { + padding: 16px; + } + .question-text { + margin-bottom: 12px; + font-weight: 500; + } + .question-options { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + .question-option { + padding: 8px 16px; + background: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + border: 1px solid var(--vscode-button-border); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; + } + .question-option:hover { + background: var(--vscode-button-secondaryHoverBackground); + } + .question-option.selected { + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + } + .question-message.answered .question-option:not(.selected) { + opacity: 0.5; + pointer-events: none; + } + .custom-input-container { + display: flex; + gap: 8px; + width: 100%; + margin-top: 8px; + } + .custom-input { + flex: 1; + padding: 8px 12px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border); + border-radius: 6px; + font-size: 13px; + } + .custom-submit { + padding: 8px 16px; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 6px; + cursor: pointer; + } + .custom-submit:hover { + background: var(--vscode-button-hoverBackground); + } + .question-message.answered .custom-input-container { + display: none; + } + + /* 分段消息样式 */ + .segmented-message { + padding: 0; + } + .message-segment { + padding: 10px 22px; + } + .segment-text { + line-height: 1.6; + } + + /* Markdown 样式 */ + .segment-text h1, + .segment-text h2, + .segment-text h3 { + margin: 16px 0 8px 0; + font-weight: 600; + line-height: 1.3; + } + .segment-text h1 { + font-size: 1.5em; + border-bottom: 1px solid var(--vscode-panel-border); + padding-bottom: 8px; + } + .segment-text h2 { + font-size: 1.3em; + } + .segment-text h3 { + font-size: 1.1em; + } + .segment-text pre { + background: var(--vscode-textCodeBlock-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + padding: 12px; + overflow-x: auto; + margin: 12px 0; + } + .segment-text code { + font-family: 'Courier New', Consolas, monospace; + font-size: 0.9em; + } + .segment-text pre code { + background: transparent; + padding: 0; + border: none; + } + .segment-text code:not(pre code) { + background: var(--vscode-textCodeBlock-background); + padding: 2px 6px; + border-radius: 3px; + color: var(--vscode-textPreformat-foreground); + } + .segment-text ul, + .segment-text ol { + margin: 8px 0; + padding-left: 24px; + } + .segment-text li { + margin: 4px 0; + line-height: 1.6; + } + .segment-text strong { + font-weight: 600; + color: var(--vscode-foreground); + } + .segment-text em { + font-style: italic; + } + .segment-text a { + color: var(--vscode-textLink-foreground); + text-decoration: none; + } + .segment-text a:hover { + text-decoration: underline; + } + .segment-text p { + margin: 8px 0; + } + + .segment-tool { + background: var(--vscode-textBlockQuote-background); + border-radius: 6px; + margin: 8px 0; + padding: 10px 14px; + } + .tool-segment-header { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + } + .tool-segment-icon { + font-size: 14px; + } + .tool-segment-name { + font-weight: 500; + color: var(--vscode-foreground); + } + .tool-segment-result { + margin-top: 6px; + font-size: 12px; + color: var(--vscode-descriptionForeground); + padding-left: 22px; + } + .segment-tool.tool-success { + border-left: 3px solid var(--vscode-charts-green); + } + .segment-tool.tool-error { + border-left: 3px solid var(--vscode-charts-red); + } + .segment-tool.tool-running { + border-left: 3px solid var(--vscode-charts-blue); + } + .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;} + `; +} + +/** + * 获取消息区域的脚本 + */ +export function getMessageAreaScript(): string { + return ` + // 添加消息 + function addMessage(text, sender) { + const div = document.createElement('div'); + div.className = \`message \${sender}-message\`; + + if (sender === 'bot') { + // 创建消息内容 + const messageContent = document.createElement('div'); + messageContent.textContent = text; + div.appendChild(messageContent); + + // 创建操作按钮容器 + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'message-actions'; + + // 复制按钮 + const copyBtn = document.createElement('button'); + copyBtn.className = 'action-btn'; + copyBtn.innerHTML = \`复制\`; + copyBtn.onclick = () => copyMessage(text, copyBtn); + + // 点赞按钮 + const likeBtn = document.createElement('button'); + likeBtn.className = 'action-btn'; + likeBtn.innerHTML = \`点赞\`; + likeBtn.onclick = () => toggleLike(likeBtn); + + // 点踩按钮 + const dislikeBtn = document.createElement('button'); + dislikeBtn.className = 'action-btn'; + dislikeBtn.innerHTML = \`点踩\`; + dislikeBtn.onclick = () => toggleDislike(dislikeBtn); + + actionsDiv.appendChild(copyBtn); + actionsDiv.appendChild(likeBtn); + actionsDiv.appendChild(dislikeBtn); + + div.appendChild(actionsDiv); + } else { + div.textContent = text; + // 当添加用户消息时,隐藏 header + hideHeaderIfNeeded(); + } + + messagesEl.appendChild(div); + messagesEl.scrollTop = messagesEl.scrollHeight; + + // 添加消息后检查 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; + } + } + + // 滚动到底部 + messagesEl.scrollTop = messagesEl.scrollHeight; + } + + // 完成流式消息 + 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; + } + + messagesEl.scrollTop = messagesEl.scrollHeight; + } + + // 显示加载指示器 + function showLoadingIndicator(text) { + hideLoadingIndicator(); + + loadingIndicator = document.createElement('div'); + loadingIndicator.className = 'message bot-message loading-message'; + loadingIndicator.innerHTML = \` +
+ +
+ \${text} + \`; + messagesEl.appendChild(loadingIndicator); + messagesEl.scrollTop = messagesEl.scrollHeight; + } + + // 隐藏加载指示器 + 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') { + const statusIcon = segment.toolStatus === 'success' ? '✅' : + segment.toolStatus === 'error' ? '❌' : '🔧'; + const statusClass = 'tool-' + (segment.toolStatus || 'running'); + segmentDiv.className += ' ' + statusClass; + segmentDiv.innerHTML = \` +
+ \${statusIcon} + \${segment.toolName || '工具'} +
+ \${segment.toolResult ? \`
\${segment.toolResult}
\` : ''} + \`; + } else if (segment.type === 'question') { + segmentDiv.className += ' segment-question'; + + // 检查是否已回答 + const isAnswered = answeredQuestions.has(segment.askId); + const selectedAnswer = answeredQuestions.get(segment.askId); + + if (isAnswered) { + segmentDiv.classList.add('answered'); + } + + // 检查是否有选项 + const hasOptions = segment.options && segment.options.length > 0; + + const optionsHtml = hasOptions + ? (segment.options || []).map(opt => { + const isSelected = isAnswered && opt === selectedAnswer; + return \`\`; + }).join('') + : ''; + + segmentDiv.innerHTML = \` +
\${segment.question || ''}
+ \${hasOptions ? \`
\${optionsHtml}
\` : ''} +
+ + +
+ \`; + + // 只在未回答时添加事件监听 + if (!isAnswered) { + setTimeout(() => { + if (hasOptions) { + const optionButtons = segmentDiv.querySelectorAll('.question-option'); + optionButtons.forEach(btn => { + btn.addEventListener('click', function() { + const option = this.getAttribute('data-option'); + handleQuestionAnswerInSegment(segment.askId, option, segmentDiv); + }); + }); + } + + const submitBtn = segmentDiv.querySelector('.custom-submit'); + const customInput = segmentDiv.querySelector('.custom-input'); + if (submitBtn && customInput) { + submitBtn.addEventListener('click', function() { + const customValue = customInput.value.trim(); + if (customValue) { + handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv); + } + }); + + // 支持回车提交 + customInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + const customValue = customInput.value.trim(); + if (customValue) { + handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv); + } + } + }); + } + }, 0); + } + } + + currentSegmentedMessage.appendChild(segmentDiv); + }); + + // 如果对话完成,添加操作按钮 + if (isComplete) { + console.log('[WebView] 对话完成,添加操作按钮'); + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'message-actions'; + const copyBtn = document.createElement('button'); + copyBtn.className = 'action-btn'; + copyBtn.innerHTML = ''; + copyBtn.onclick = () => { + const textContent = segments + .filter(s => s.type === 'text' && s.content) + .map(s => s.content) + .join('\\n'); + copyMessage(textContent, copyBtn); + }; + actionsDiv.appendChild(copyBtn); + currentSegmentedMessage.appendChild(actionsDiv); + + // 重置当前分段消息容器 + currentSegmentedMessage = null; + } + + // 滚动到底部 + messagesEl.scrollTop = messagesEl.scrollHeight; + } + + // 渲染分段消息(兼容旧代码) + 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') { + const statusIcon = segment.toolStatus === 'success' ? '✅' : + segment.toolStatus === 'error' ? '❌' : '🔧'; + const statusClass = 'tool-' + (segment.toolStatus || 'running'); + segmentDiv.className += ' ' + statusClass; + segmentDiv.innerHTML = \` +
+ \${statusIcon} + \${segment.toolName || '工具'} +
+ \${segment.toolResult ? \`
\${segment.toolResult}
\` : ''} + \`; + } else if (segment.type === 'question') { + segmentDiv.innerHTML = \` +
+
\${segment.question || ''}
+
+ \${(segment.options || []).map(opt => \`\${opt}\`).join('')} +
+
+ \`; + } + + container.appendChild(segmentDiv); + }); + + // 添加操作按钮 + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'message-actions'; + const copyBtn = document.createElement('button'); + copyBtn.className = 'action-btn'; + copyBtn.innerHTML = ''; + copyBtn.onclick = () => { + const textContent = segments + .filter(s => s.type === 'text' && s.content) + .map(s => s.content) + .join('\\n'); + copyMessage(textContent, copyBtn); + }; + actionsDiv.appendChild(copyBtn); + container.appendChild(actionsDiv); + + messagesEl.appendChild(container); + messagesEl.scrollTop = messagesEl.scrollHeight; + } + + // 格式化文本(支持 Markdown) + function formatText(text) { + if (!text) return ''; + + // 先转义 HTML 特殊字符 + let html = text + .replace(/&/g, '&') + .replace(//g, '>'); + + // 处理代码块(三个反引号包裹的代码) + html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) { + const language = lang || 'plaintext'; + return '
' + code.trim() + '
'; + }); + + // 处理行内代码(单个反引号包裹) + html = html.replace(/\`([^\`]+)\`/g, '$1'); + + // 处理标题 ### Title + html = html.replace(/^### (.+)$/gm, '

$1

'); + html = html.replace(/^## (.+)$/gm, '

$1

'); + html = html.replace(/^# (.+)$/gm, '

$1

'); + + // 处理粗体 **text** + html = html.replace(/\\*\\*(.+?)\\*\\*/g, '$1'); + + // 处理斜体 *text* + html = html.replace(/\\*(.+?)\\*/g, '$1'); + + // 处理无序列表 - item 或 * item + html = html.replace(/^[\\-\\*] (.+)$/gm, '
  • $1
  • '); + html = html.replace(/(
  • .*<\\/li>\\n?)+/g, '
      $&
    '); + + // 处理有序列表 1. item + html = html.replace(/^\\d+\\. (.+)$/gm, '
  • $1
  • '); + + // 处理链接 [text](url) + html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '$1'); + + // 处理换行 + html = html.replace(/\\n/g, '
    '); + + return html; + } + + // 添加工具状态消息 + function addToolStatus(toolName, status, detail) { + const statusIcons = { + start: '🔧', + complete: '✅', + error: '❌' + }; + const statusTexts = { + start: '正在执行', + complete: '执行完成', + error: '执行失败' + }; + + const div = document.createElement('div'); + div.className = \`message tool-status tool-\${status}\`; + div.innerHTML = \` + \${statusIcons[status]} + \${toolName} + \${statusTexts[status]} + \${detail ? \`
    \${detail}
    \` : ''} + \`; + messagesEl.appendChild(div); + messagesEl.scrollTop = messagesEl.scrollHeight; + + // 添加消息后检查 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); + messagesEl.scrollTop = messagesEl.scrollHeight; + + // 添加消息后检查 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 + }); + } + `; +} diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 02cc8ea..8918208 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -7,7 +7,16 @@ import { getConversationHistoryBarStyles, getConversationHistoryBarScript, } from "./conversationHistoryBar"; - +import { + getInputAreaContent, + getInputAreaStyles, + getInputAreaScript, +} from "./inputArea"; +import { + getMessageAreaContent, + getMessageAreaStyles, + getMessageAreaScript, +} from "./messageArea"; /** * 获取 WebView 面板的 HTML 内容 */ @@ -55,354 +64,11 @@ export function getWebviewContent(iconUri?: string): string { overflow: hidden; padding: 0 20px 20px 20px; } - .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); - } - .input-area { - border-top: 1px solid var(--vscode-panel-border); - padding-top: 15px; - flex-shrink: 0; - } - .input-group { - display: flex; - flex-direction: column; - gap: 10px; - background: var(--vscode-input-background); - border: 1px solid var(--vscode-input-border); - border-radius: 8px; - padding: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 6px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; - } - .input-group:hover { - box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2), 0 3px 8px rgba(0, 0, 0, 0.15); - } - .input-group:focus-within { - border-color: var(--vscode-focusBorder); - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25), 0 3px 10px rgba(0, 0, 0, 0.2); - } - .input-wrapper { - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - } - .input-bottom-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - margin-bottom: -17px; - } - .mode-selector { - display: flex; - align-items: center; - position: relative; - } - .input-actions { - display: flex; - align-items: center; - gap: 10px; - } - .mode-selector select { - padding: 2px 4px; - background: var(--vscode-input-background); - color: var(--vscode-foreground); - border: none; - cursor: pointer; - font-size: 12px; - outline: none; - border-radius: 4px; - } - .mode-selector select:hover { - background: var(--vscode-list-hoverBackground); - } - /* Tooltip 样式 */ - .tooltip { - position: relative; - display: inline-block; - } - .tooltip .tooltiptext { - visibility: hidden; - width: auto; - background: #1e1e1e; - color: #ffffff; - text-align: center; - border-radius: 6px; - padding: 6px 12px; - position: absolute; - z-index: 1000; - bottom: 150%; - left: 50%; - transform: translateX(-50%) translateY(10px); - opacity: 0; - transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); - font-size: 12px; - font-weight: 500; - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6), 0 2px 4px rgba(0, 0, 0, 0.3); - white-space: nowrap; - letter-spacing: 0.3px; - } - .tooltip .tooltiptext::after { - content: ""; - position: absolute; - top: 100%; - left: 50%; - margin-left: -6px; - border-width: 6px; - border-style: solid; - border-color: #1e1e1e transparent transparent transparent; - } - .tooltip .tooltiptext::before { - content: ""; - position: absolute; - top: 100%; - left: 50%; - margin-left: -7px; - border-width: 7px; - border-style: solid; - border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent; - z-index: -1; - } - .tooltip:hover .tooltiptext { - visibility: visible; - opacity: 1; - transform: translateX(-50%) translateY(0); - } - textarea { - width: 100%; - padding: 10px; - background: transparent; - color: var(--vscode-input-foreground); - border: none; - border-radius: 4px; - font-family: inherit; - resize: none; - min-height: 40px; - max-height: 200px; - outline: none; - box-sizing: border-box; - overflow-y: auto; - line-height: 1.5; - } - /* 简洁的滚动条样式 */ - textarea::-webkit-scrollbar { - width: 8px; - } - textarea::-webkit-scrollbar-track { - background: transparent; - } - textarea::-webkit-scrollbar-thumb { - background: rgba(128, 128, 128, 0.5); - border-radius: 4px; - } - textarea::-webkit-scrollbar-button { - display: none; - } - button { - padding: 0 20px; - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); - border: none; - border-radius: 4px; - cursor: pointer; - } - .optimize-button { - padding: 8px; - background: transparent; - color: var(--vscode-foreground); - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: opacity 0.2s ease; - width: 32px; - height: 32px; - } - .optimize-button:hover { - opacity: 0.7; - } - .optimize-button svg { - width: 16px; - height: 16px; - } - .optimize-button-wrapper { - display: flex; - align-items: flex-end; - } - .quick-actions { - display: flex; - gap: 8px; - margin-top: 10px; - flex-wrap: wrap; - flex-shrink: 0; - } - .quick-btn { - padding: 6px 12px; - background: var(--vscode-button-secondaryBackground); - color: var(--vscode-button-secondaryForeground); - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 12px; - } - .file-reader-section { - margin-bottom: 15px; - padding: 15px; - background: var(--vscode-input-background); - border: 1px solid var(--vscode-input-border); - border-radius: 8px; - flex-shrink: 0; - } - .file-reader-section h3 { - margin: 0 0 10px 0; - color: var(--vscode-button-background); - } - .file-input-group { - display: flex; - gap: 10px; - margin-bottom: 10px; - } - .file-input-group input { - flex: 1; - padding: 8px; - background: var(--vscode-input-background); - color: var(--vscode-input-foreground); - border: 1px solid var(--vscode-input-border); - border-radius: 4px; - } - .file-content { - margin-top: 15px; - padding: 10px; - background: var(--vscode-editor-background); - border: 1px solid var(--vscode-panel-border); - border-radius: 4px; - max-height: 120px; - overflow-y: auto; - font-family: 'Courier New', monospace; - font-size: 12px; - white-space: pre-wrap; - word-break: break-all; - } - .file-content.empty { - color: var(--vscode-descriptionForeground); - font-style: italic; - } - .error-message { - color: var(--vscode-errorForeground); - padding: 8px; - background: var(--vscode-inputValidation-errorBackground); - border: 1px solid var(--vscode-inputValidation-errorBorder); - border-radius: 4px; - margin-top: 10px; - } + ${getMessageAreaStyles()} ${getWaveformPreviewContent()} ${getConversationHistoryBarStyles()} + ${getInputAreaStyles()} + .file-editor-section { margin-bottom: 15px; padding: 15px; @@ -436,130 +102,6 @@ export function getWebviewContent(iconUri?: string): string { gap: 10px; margin-top: 10px; } - /* 上下文显示样式 */ - .context-display { - display: flex; - flex-direction: column; - align-items: center; - position: relative; - } - .context-info { - display: flex; - align-items: center; - gap: 6px; - height: 40px; - background: transparent; - border: none; - border-radius: 4px; - font-size: 14px; - font-weight: 500; - color: var(--vscode-foreground); - transition: opacity 0.3s ease; - box-shadow: none; - position: relative; - overflow: hidden; - cursor: pointer; - } - .context-info:hover { - opacity: 0.8; - } - .database-icon { - display: flex; - align-items: center; - justify-content: center; - width: 12px; - height: 12px; - position: relative; - } - .db-svg { - width: 100%; - height: 100%; - } - .db-body { - fill: #ffffff; - } - .db-fill { - fill: #409eff; - transition: all 0.3s ease; - } - .context-percentage { - font-size: 14px; - font-weight: 500; - color: var(--vscode-foreground); - text-align: right; - } - /* 上下文信息弹窗样式 */ - .context-panel { - position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%); - margin-bottom: 8px; - z-index: 1000; - animation: fadeInUp 0.2s ease-out; - display: none; - } - .context-panel.active { - display: block; - } - .context-panel::after { - content: ""; - position: absolute; - bottom: -6px; - left: 50%; - transform: translateX(-50%); - width: 0; - height: 0; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-top: 6px solid #ffffff; - } - .context-panel-content { - background: #ffffff; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 8px; - padding: 12px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); - backdrop-filter: blur(10px); - min-width: 160px; - } - .context-info-text { - font-size: 12px; - color: #374151; - text-align: center; - margin-bottom: 8px; - white-space: nowrap; - } - .compress-button { - width: 100%; - background: linear-gradient(145deg, #3b82f6 0%, #1d4ed8 100%); - border: 1px solid rgba(59, 130, 246, 0.3); - border-radius: 6px; - color: white; - font-size: 12px; - font-weight: 500; - padding: 6px 12px; - cursor: pointer; - transition: all 0.2s ease; - } - .compress-button:hover { - background: linear-gradient(145deg, #2563eb 0%, #1e40af 100%); - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); - } - .compress-button:active { - transform: translateY(0); - } - @keyframes fadeInUp { - from { - opacity: 0; - transform: translateX(-50%) translateY(10px); - } - to { - opacity: 1; - transform: translateX(-50%) translateY(0); - } - } /* 流式消息样式 */ .streaming .message-content { @@ -708,7 +250,7 @@ export function getWebviewContent(iconUri?: string): string { padding: 0; } .message-segment { - padding: 10px 14px; + padding: 10px 22 px; } .segment-text { line-height: 1.6; @@ -818,7 +360,7 @@ export function getWebviewContent(iconUri?: string): string {
    -
    + ${getMessageAreaContent()} -
    -
    -
    - -
    -
    -
    - - 切换模型 -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - - - - - -
    - 0% -
    - - -
    -
    -
    - 0k / 200k 已用上下文 -
    - -
    -
    -
    - - -
    - - 一键优化 -
    - - -
    -
    -
    -
    -
    + ${getInputAreaContent()}
    - + ${getInputAreaScript()} + `; } From 0b4ec2ca6eb8b48517ef4af21bff7c4172675d2b Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Wed, 24 Dec 2025 10:25:12 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9=E4=BA=86=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E8=B0=83=E7=94=A8=E7=9A=84=E6=A0=B7=E5=BC=8F=20+=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=A4=AA=E9=95=BF=E5=8F=AF=E4=BB=A5=E6=8A=98=E5=8F=A0?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/messageArea.ts | 179 ++++++++++++++++++++++++++++++++------- 1 file changed, 148 insertions(+), 31 deletions(-) diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index a70cbcf..342afab 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -349,38 +349,57 @@ export function getMessageAreaStyles(): string { } .segment-tool { - background: var(--vscode-textBlockQuote-background); - border-radius: 6px; - margin: 8px 0; - padding: 10px 14px; + margin: 4px 0; + padding: 4px 0; } .tool-segment-header { display: flex; align-items: center; - gap: 8px; - font-size: 13px; - } - .tool-segment-icon { - font-size: 14px; - } - .tool-segment-name { - font-weight: 500; - color: var(--vscode-foreground); - } - .tool-segment-result { - margin-top: 6px; + gap: 6px; font-size: 12px; color: var(--vscode-descriptionForeground); - padding-left: 22px; + cursor: pointer; } - .segment-tool.tool-success { - border-left: 3px solid var(--vscode-charts-green); + .tool-segment-icon { + font-size: 12px; } - .segment-tool.tool-error { - border-left: 3px solid var(--vscode-charts-red); + .tool-segment-name { + font-weight: normal; } - .segment-tool.tool-running { - border-left: 3px solid var(--vscode-charts-blue); + .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-segment-content { + overflow: hidden; + transition: max-height 0.3s ease; + } + .tool-segment-content.collapsed { + max-height: 0; } .segment-question { background: var(--vscode-textBlockQuote-background); @@ -689,15 +708,64 @@ export function getMessageAreaScript(): string { } else if (segment.type === 'tool') { const statusIcon = segment.toolStatus === 'success' ? '✅' : segment.toolStatus === 'error' ? '❌' : '🔧'; - const statusClass = 'tool-' + (segment.toolStatus || 'running'); - segmentDiv.className += ' ' + statusClass; + const toolResult = segment.toolResult || ''; + + // 检查工具结果是否过长(超过一行显示不下) + const shouldCollapse = toolResult && toolResult.length > 60; + + // 折叠图标 SVG + const collapseIconSvg = \` + + + + + + + \`; + segmentDiv.innerHTML = \` -
    +
    + \${shouldCollapse ? collapseIconSvg : ''} \${statusIcon} \${segment.toolName || '工具'} + \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''}
    - \${segment.toolResult ? \`
    \${segment.toolResult}
    \` : ''} + \${shouldCollapse ? \`\` : ''} \`; + + // 添加折叠/展开事件监听 + 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'; @@ -831,15 +899,64 @@ export function getMessageAreaScript(): string { } else if (segment.type === 'tool') { const statusIcon = segment.toolStatus === 'success' ? '✅' : segment.toolStatus === 'error' ? '❌' : '🔧'; - const statusClass = 'tool-' + (segment.toolStatus || 'running'); - segmentDiv.className += ' ' + statusClass; + const toolResult = segment.toolResult || ''; + + // 检查工具结果是否过长(超过一行显示不下) + const shouldCollapse = toolResult && toolResult.length > 60; + + // 折叠图标 SVG + const collapseIconSvg = \` + + + + + + + \`; + segmentDiv.innerHTML = \` -
    +
    + \${shouldCollapse ? collapseIconSvg : ''} \${statusIcon} \${segment.toolName || '工具'} + \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''}
    - \${segment.toolResult ? \`
    \${segment.toolResult}
    \` : ''} + \${shouldCollapse ? \`\` : ''} \`; + + // 添加折叠/展开事件监听 + 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 = \`
    From fb1156d24fc2d0bc86b6feba407250200c3ca5fc Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Wed, 24 Dec 2025 10:43:55 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat:=E5=B0=86=E8=8B=B1=E6=96=87=E7=9A=84?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=90=8D=E7=A7=B0=E6=98=A0=E5=B0=84=E4=B8=BA?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/messageArea.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index 342afab..fc6fb46 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -491,6 +491,19 @@ export function getMessageAreaStyles(): string { */ export function getMessageAreaScript(): string { return ` + // 工具名称映射 + function getToolDisplayName(toolName) { + const toolNameMap = { + 'file_read': '已完成文件读取', + 'file_write': '已完成文件写入', + 'file_list': '已读取目录', + 'syntax_check': '语法检查', + 'simulation': '仿真', + 'waveform_summary': '波形分析' + }; + return toolNameMap[toolName] || toolName; + } + // 添加消息 function addMessage(text, sender) { const div = document.createElement('div'); @@ -729,7 +742,7 @@ export function getMessageAreaScript(): string {
    \${shouldCollapse ? collapseIconSvg : ''} \${statusIcon} - \${segment.toolName || '工具'} + \${getToolDisplayName(segment.toolName) || '工具'} \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''}
    \${shouldCollapse ? \`\` : ''} @@ -920,7 +933,7 @@ export function getMessageAreaScript(): string {
    \${shouldCollapse ? collapseIconSvg : ''} \${statusIcon} - \${segment.toolName || '工具'} + \${getToolDisplayName(segment.toolName) || '工具'} \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''}
    \${shouldCollapse ? \`\` : ''} @@ -1054,7 +1067,7 @@ export function getMessageAreaScript(): string { div.className = \`message tool-status tool-\${status}\`; div.innerHTML = \` \${statusIcons[status]} - \${toolName} + \${getToolDisplayName(toolName)} \${statusTexts[status]} \${detail ? \`
    \${detail}
    \` : ''} \`; From 463eedf1dd1b9427da753d36f0317706a9e779ee Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Wed, 24 Dec 2025 11:26:25 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat:=E5=AE=9E=E7=8E=B0=E4=BA=86=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E8=B0=83=E7=94=A8=E7=9A=84icon=E6=9B=BF=E6=8D=A2=20-?= =?UTF-8?q?=20=E8=BF=98=E5=B0=86icon=E6=8A=BD=E5=8F=96=E5=87=BA=E6=9D=A5?= =?UTF-8?q?=E6=88=90=E4=B8=BA=E7=8B=AC=E7=AB=8B=E7=9A=84=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E6=96=B9=E4=BE=BF=E7=BB=9F=E4=B8=80=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/toolIcons.ts | 52 +++++++++++++++++++++ src/views/messageArea.ts | 92 ++++++++++++++++++++++++-------------- 2 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 src/constants/toolIcons.ts diff --git a/src/constants/toolIcons.ts b/src/constants/toolIcons.ts new file mode 100644 index 0000000..ca881be --- /dev/null +++ b/src/constants/toolIcons.ts @@ -0,0 +1,52 @@ +/** + * 工具图标定义 + * 包含各种工具的 SVG 图标 + */ + +/** + * 折叠图标 SVG(用于可折叠的工具结果) + */ +export const collapseIconSvg = ` + + + + + + +`; + +/** + * 文件写入完成图标 SVG + */ +export const fileWriteIconSvg = ` + + + + + + +`; + +/** + * 语法检查图标 SVG + */ +export const syntaxCheckIconSvg = ` + + + + + +`; + +/** + * 已检索代码图标 SVG + */ +export const SearchCode = ` + + + + + +`; diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index fc6fb46..3c5f4f2 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -10,6 +10,13 @@ * - 显示工具执行状态和加载指示器 */ +import { + collapseIconSvg, + fileWriteIconSvg, + syntaxCheckIconSvg, + SearchCode, +} from "../constants/toolIcons"; + /** * 获取消息区域的 HTML 内容 */ @@ -394,6 +401,39 @@ export function getMessageAreaStyles(): string { .tool-segment-header:not(.collapsed) .tool-collapse-icon { transform: rotate(0deg); } + .tool-file-write-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-file-write-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-syntax-check-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-syntax-check-icon svg { + width: 100%; + height: 100%; + display: block; + } + .tool-search-code-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 6px; + } + .tool-search-code-icon svg { + width: 100%; + height: 100%; + display: block; + } .tool-segment-content { overflow: hidden; transition: max-height 0.3s ease; @@ -491,15 +531,21 @@ export function getMessageAreaStyles(): string { */ export function getMessageAreaScript(): string { return ` + // 工具图标定义 + const collapseIconSvg = \`${collapseIconSvg}\`; + const fileWriteIconSvg = \`${fileWriteIconSvg}\`; + const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`; + const searchCodeIconSvg = \`${SearchCode}\`; + // 工具名称映射 function getToolDisplayName(toolName) { const toolNameMap = { 'file_read': '已完成文件读取', 'file_write': '已完成文件写入', - 'file_list': '已读取目录', - 'syntax_check': '语法检查', - 'simulation': '仿真', - 'waveform_summary': '波形分析' + 'file_list': '已检索代码文件', + 'syntax_check': '已完成语法检查', + 'simulation': '已完成仿真', + 'waveform_summary': '已完成波形分析' }; return toolNameMap[toolName] || toolName; } @@ -719,29 +765,18 @@ export function getMessageAreaScript(): string { segmentDiv.className += ' segment-text'; segmentDiv.innerHTML = formatText(segment.content); } else if (segment.type === 'tool') { - const statusIcon = segment.toolStatus === 'success' ? '✅' : - segment.toolStatus === 'error' ? '❌' : '🔧'; + const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; const toolResult = segment.toolResult || ''; // 检查工具结果是否过长(超过一行显示不下) const shouldCollapse = toolResult && toolResult.length > 60; - // 折叠图标 SVG - const collapseIconSvg = \` - - - - - - - \`; - segmentDiv.innerHTML = \`
    \${shouldCollapse ? collapseIconSvg : ''} - \${statusIcon} + \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''} + \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''} + \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''} \${getToolDisplayName(segment.toolName) || '工具'} \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''}
    @@ -910,29 +945,18 @@ export function getMessageAreaScript(): string { segmentDiv.className += ' segment-text'; segmentDiv.innerHTML = formatText(segment.content); } else if (segment.type === 'tool') { - const statusIcon = segment.toolStatus === 'success' ? '✅' : - segment.toolStatus === 'error' ? '❌' : '🔧'; + const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; const toolResult = segment.toolResult || ''; // 检查工具结果是否过长(超过一行显示不下) const shouldCollapse = toolResult && toolResult.length > 60; - // 折叠图标 SVG - const collapseIconSvg = \` - - - - - - - \`; - segmentDiv.innerHTML = \`
    \${shouldCollapse ? collapseIconSvg : ''} - \${statusIcon} + \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''} + \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''} + \${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''} \${getToolDisplayName(segment.toolName) || '工具'} \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''}
    From 9c787627a97c0eeaae78d63ec99638d43d0700c7 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Wed, 24 Dec 2025 12:00:03 +0800 Subject: [PATCH 8/9] =?UTF-8?q?feat:=E5=AE=9E=E7=8E=B0=E5=B7=B2=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E4=BB=BF=E7=9C=9F=E4=B9=8B=E5=90=8E=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E6=B3=A2=E5=BD=A2=E9=A2=84=E8=A7=88=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=B1=95=E7=A4=BA=E6=B3=A2=E5=BD=A2=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/messageArea.ts | 44 +++++++++++++++++++++++++++++++++++++ src/views/webviewContent.ts | 23 ++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index 3c5f4f2..f88f87f 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -16,6 +16,10 @@ import { syntaxCheckIconSvg, SearchCode, } from "../constants/toolIcons"; +import { + getWaveformPreviewContent, + getWaveformPreviewScript, +} from "./waveformPreviewContent"; /** * 获取消息区域的 HTML 内容 @@ -523,6 +527,8 @@ export function getMessageAreaStyles(): string { color: var(--vscode-button-secondaryForeground); border-radius: 4px; font-size: 12px;} + + ${getWaveformPreviewContent()} `; } @@ -783,6 +789,24 @@ export function getMessageAreaScript(): string { \${shouldCollapse ? \`\` : ''} \`; + // 如果是仿真工具且成功完成,尝试添加波形预览 + if (segment.toolName === 'simulation' && segment.toolStatus === 'success') { + // 优先使用显式提供的路径,否则从结果文本中解析 + let vcdPath = segment.vcdFilePath; + if (!vcdPath && segment.toolResult) { + const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/); + if (match && match[1]) { + vcdPath = match[1].trim(); + } + } + + if (vcdPath) { + const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd'; + const waveformPreview = createWaveformPreview(vcdPath, fileName); + segmentDiv.appendChild(waveformPreview); + } + } + // 添加折叠/展开事件监听 if (shouldCollapse) { setTimeout(() => { @@ -963,6 +987,24 @@ export function getMessageAreaScript(): string { \${shouldCollapse ? \`\` : ''} \`; + // 如果是仿真工具且成功完成,尝试添加波形预览 + if (segment.toolName === 'simulation' && segment.toolStatus === 'success') { + // 优先使用显式提供的路径,否则从结果文本中解析 + let vcdPath = segment.vcdFilePath; + if (!vcdPath && segment.toolResult) { + const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/); + if (match && match[1]) { + vcdPath = match[1].trim(); + } + } + + if (vcdPath) { + const fileName = segment.fileName || vcdPath.split(/[\\\\\/]/).pop() || 'waveform.vcd'; + const waveformPreview = createWaveformPreview(vcdPath, fileName); + segmentDiv.appendChild(waveformPreview); + } + } + // 添加折叠/展开事件监听 if (shouldCollapse) { setTimeout(() => { @@ -1218,5 +1260,7 @@ export function getMessageAreaScript(): string { customInput: answer }); } + + ${getWaveformPreviewScript()} `; } diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index 8918208..c3e321a 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -484,9 +484,30 @@ export function getWebviewContent(iconUri?: string): string { hideLoadingIndicator(); break; + case 'vcdInfo': + // 渲染迷你波形预览信息 + try { + if (message.containerId && typeof renderWaveformInfo === 'function') { + renderWaveformInfo(message.containerId, message.vcdInfo || {}); + } + } catch (e) { + console.warn('[WebView] 渲染波形信息失败:', e); + } + break; + case 'vcdGenerated': - // VCD 文件生成成功 + // VCD 文件生成成功,添加消息并附带波形预览 addMessage(message.text, 'bot'); + try { + if (message.vcdFilePath) { + const lastMsg = messagesEl ? messagesEl.lastElementChild : null; + if (lastMsg && typeof addWaveformPreviewToMessage === 'function') { + addWaveformPreviewToMessage(lastMsg, message.vcdFilePath, message.fileName || 'waveform.vcd'); + } + } + } catch (e) { + console.warn('[WebView] 添加波形预览失败:', e); + } break; case 'fileContent': From b676846b2f4ab61e9914df08504d4263a7e4430f Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Thu, 25 Dec 2025 09:23:53 +0800 Subject: [PATCH 9/9] =?UTF-8?q?feat:=E5=AE=9E=E7=8E=B0plan=E7=9A=84?= =?UTF-8?q?=E5=BC=80=E5=85=B3=E7=9A=84=E5=89=8D=E7=AB=AF=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/inputArea.ts | 80 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/src/views/inputArea.ts b/src/views/inputArea.ts index ad52f4c..5bf2ee1 100644 --- a/src/views/inputArea.ts +++ b/src/views/inputArea.ts @@ -8,6 +8,17 @@ export function getInputAreaContent(): string {
    + +
    +
    + + 启用 Plan 模式 +
    +