From 10f0877a5e32f81be27e2c4917635c8bb7ce3155 Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Wed, 24 Dec 2025 10:01:53 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DAI=E8=AF=A2=E9=97=AE?= =?UTF-8?q?=E6=97=B6=E9=80=89=E9=A1=B9=E7=82=B9=E5=87=BB=E5=90=8E=E9=80=89?= =?UTF-8?q?=E4=B8=AD=E7=8A=B6=E6=80=81=E4=B8=A2=E5=A4=B1=E7=9A=84=E9=97=AE?= =?UTF-8?q?=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()} + `; }