Merge remote-tracking branch 'origin/feat/Plugin-front-end' into feat/back-to-front

This commit is contained in:
XiaoFeng
2025-12-25 16:18:05 +08:00
14 changed files with 3487 additions and 1229 deletions

View File

@ -9,5 +9,7 @@
"dist": true // set this to false to include "dist" folder in search results "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 // 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"
} }

View File

@ -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 页面,集成波形预览组件
**修改内容:**
- 导入波形预览组件的样式和脚本
-`<style>` 标签中插入 `getWaveformPreviewContent()`
-`<script>` 标签中插入 `getWaveformPreviewScript()`
- 添加 `vcdGenerated` 消息处理逻辑
- 添加 `vcdInfo` 消息处理逻辑
**消息处理:**
```javascript
case 'vcdGenerated':
// VCD 文件生成成功,显示带波形预览的消息
- 创建消息 div
- 添加成功消息文本
- 调用 addWaveformPreviewToMessage() 添加波形预览卡片
case 'vcdInfo':
// 接收到 VCD 文件信息,渲染波形预览
- 调用 renderWaveformInfo() 渲染波形
```
---
### 3. `src/utils/messageHandler.ts`
**功能:** 处理用户消息和 VCD 生成请求
**修改内容:**
- 导入 `path` 模块
- 修改 VCD 生成成功后的消息发送逻辑
**关键代码:**
```typescript
if (result.success) {
if (result.vcdFilePath) {
const fileName = path.basename(result.vcdFilePath);
panel.webview.postMessage({
command: "vcdGenerated", // 发送 vcdGenerated 消息
text: successMsg,
vcdFilePath: result.vcdFilePath,
fileName: fileName,
});
}
}
```
---
### 4. `src/panels/ICHelperPanel.ts`
**功能:** IC 助手面板,处理 WebView 消息
**修改内容:**
- 导入 `VCDViewerPanel`
- 添加 `openWaveformViewer` 命令处理
- 添加 `getVCDInfo` 命令处理
- 新增 `getVCDFileInfo()` 函数
- 新增 `parseVCDSignals()` 函数
**新增函数:**
#### `getVCDFileInfo(panel, vcdFilePath, containerId)`
**功能:** 获取 VCD 文件信息并解析信号数据
**处理流程:**
1. 检查文件是否存在
2. 获取文件大小
3. 读取 VCD 文件内容
4. 解析信号数量
5. 解析时间范围
6. 调用 `parseVCDSignals()` 解析前 3 个信号的数据
7. 发送 `vcdInfo` 消息回前端
**返回数据结构:**
```typescript
{
command: "vcdInfo",
containerId: string,
vcdInfo: {
signalCount: string,
timeRange: string,
fileSize: string,
signals: Array<{
name: string,
identifier: string,
width: number,
values: Array<{ time: number, value: string }>
}>
}
}
```
#### `parseVCDSignals(content, maxSignals)`
**功能:** 解析 VCD 文件中的信号数据
**处理流程:**
1. 使用正则表达式解析信号定义部分(`$var ... $end`
2. 提取信号名称、标识符、位宽
3. 找到数据变化部分(`$dumpvars` 之后)
4. 解析每个信号的值变化
- 单比特信号:格式 `0!``1!`
- 多比特信号:格式 `b1010 !`
5. 限制最多 50 个采样点(避免数据过多)
6. 返回信号数据数组
**VCD 文件格式示例:**
```vcd
$var wire 1 ! clk $end
$var wire 8 " data $end
$dumpvars
0!
b00000000 "
#10
1!
#20
0!
b00000001 "
```
**消息处理:**
```typescript
case "openWaveformViewer":
// 打开完整波形查看器
VCDViewerPanel.createOrShow(context.extensionUri, message.vcdFilePath);
case "getVCDInfo":
// 获取 VCD 文件信息
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
```
---
### 5. `src/panels/VCDViewerPanel.ts`
**功能:** 完整的 VCD 波形查看器面板(已存在)
**作用:** 当用户点击"展开查看"按钮时,打开此面板显示完整的波形
---
## 数据流
### 1. VCD 生成流程
```
用户输入 → messageHandler.handleUserMessage()
检测到 VCD 生成命令
messageHandler.handleVCDGeneration()
iverilogRunner.generateVCD()
生成 VCD 文件
发送 vcdGenerated 消息到前端
webviewContent 接收消息
创建波形预览卡片
```
### 2. 波形预览加载流程
```
createWaveformPreview() 创建卡片
loadMiniWaveform() 请求 VCD 信息
发送 getVCDInfo 消息到后端
ICHelperPanel 接收消息
getVCDFileInfo() 读取并解析 VCD 文件
parseVCDSignals() 解析信号数据
发送 vcdInfo 消息到前端
renderWaveformInfo() 渲染波形
drawRealWaveform() 绘制 SVG 波形图
```
### 3. 展开查看流程
```
用户点击"展开查看"按钮
openFullWaveform() 发送消息
发送 openWaveformViewer 消息到后端
ICHelperPanel 接收消息
VCDViewerPanel.createOrShow()
打开完整波形查看器窗口
```
---
## 样式说明
### CSS 类名
- `.waveform-preview` - 波形预览卡片容器
- `.waveform-preview-header` - 卡片头部
- `.waveform-preview-title` - 标题区域
- `.waveform-expand-btn` - 展开按钮
- `.waveform-preview-content` - 内容区域
- `.waveform-mini-viewer` - 波形显示容器
- `.waveform-loading` - 加载提示
### 波形绘制
- **单比特信号**:使用 SVG `<path>` 绘制数字波形
- 高电平y = 顶部
- 低电平y = 底部
- 垂直跳变表示信号变化
- **多比特信号**:使用 SVG `<path>` 绘制总线波形
- 梯形表示数据变化
- 中间线表示稳定状态
- **颜色方案**
- 第1个信号蓝色 (`var(--vscode-charts-blue)`)
- 第2个信号绿色 (`var(--vscode-charts-green)`)
- 第3个信号橙色 (`var(--vscode-charts-orange)`)
---
## 配置参数
### 可调整参数
**在 `ICHelperPanel.ts` 中:**
```typescript
const signals = parseVCDSignals(content, 3); // 解析前3个信号
```
- 修改数字可以改变显示的信号数量
**在 `parseVCDSignals()` 中:**
```typescript
if (values.length >= 50) {
break; // 限制最多50个采样点
}
```
- 修改数字可以改变采样点数量
**在 `drawRealWaveform()` 中:**
```typescript
const signalHeight = 20; // 信号高度
const signalSpacing = 30; // 信号间距
const leftMargin = 80; // 左边距(信号名称区域)
const rightMargin = 20; // 右边距
```
- 修改这些参数可以调整波形显示样式
---
## 性能优化
1. **限制信号数量**:只解析前 3 个信号
2. **限制采样点**:每个信号最多 50 个采样点
3. **轻量级渲染**:使用 SVG 而不是 Canvas
4. **按需加载**:只在需要时读取和解析 VCD 文件
---
## 错误处理
### 文件不存在
```typescript
if (!fs.existsSync(vcdFilePath)) {
// 返回错误信息
vcdInfo: {
signalCount: 'N/A',
timeRange: 'N/A',
fileSize: 'N/A',
error: '文件不存在'
}
}
```
### 解析失败
```typescript
try {
// 解析逻辑
} catch (error) {
console.error('解析 VCD 信号数据失败:', error);
return []; // 返回空数组
}
```
### 无信号数据
```typescript
if (!signals || signals.length === 0) {
return `<svg>无波形数据</svg>`;
}
```
---
## 未来扩展
### 可能的改进方向
1. **交互功能**
- 鼠标悬停显示信号值
- 点击信号高亮显示
- 缩放和平移功能
2. **显示优化**
- 自动选择最有代表性的信号
- 智能采样(保留关键变化点)
- 支持更多信号类型(模拟信号等)
3. **性能优化**
- 使用 Web Worker 解析大文件
- 虚拟滚动显示大量信号
- 缓存解析结果
4. **功能增强**
- 支持信号搜索和过滤
- 导出波形图片
- 比较多个 VCD 文件
---
## 调试技巧
### 查看消息流
在浏览器开发者工具中查看 `vscode.postMessage()` 的调用:
```javascript
console.log('发送消息:', message);
vscode.postMessage(message);
```
### 查看解析结果
`parseVCDSignals()` 中添加日志:
```typescript
console.log('解析到的信号:', signals);
```
### 查看 SVG 输出
`drawRealWaveform()` 中添加日志:
```javascript
console.log('SVG 内容:', svgContent);
```
---
## 相关文件
- `src/utils/iverilogRunner.ts` - VCD 文件生成
- `src/panels/VCDViewerPanel.ts` - 完整波形查看器
- `media/vcdrom/` - VCDrom 波形查看库
---
## 版本历史
### v1.0 (当前版本)
- ✅ 创建独立的波形预览组件
- ✅ 解析 VCD 文件中的真实信号数据
- ✅ 绘制单比特和多比特信号波形
- ✅ 支持展开查看完整波形
- ✅ 轻量级预览,快速加载
---
## 总结
波形预览功能通过以下文件协同工作:
1. **`waveformPreviewContent.ts`** - 组件核心逻辑
2. **`webviewContent.ts`** - 集成到主页面
3. **`messageHandler.ts`** - 处理 VCD 生成
4. **`ICHelperPanel.ts`** - 解析 VCD 数据和消息处理
整个功能采用模块化设计,易于维护和扩展。

View File

@ -97,7 +97,7 @@
"properties": { "properties": {
"icCoder.backendUrl": { "icCoder.backendUrl": {
"type": "string", "type": "string",
"default": "http://localhost:2233", "default": "http://192.168.1.108:2233",
"description": "后端服务地址" "description": "后端服务地址"
}, },
"icCoder.timeout": { "icCoder.timeout": {

View File

@ -2,7 +2,7 @@
* 配置管理 * 配置管理
* 从 VSCode 设置读取配置项 * 从 VSCode 设置读取配置项
*/ */
import * as vscode from 'vscode'; import * as vscode from "vscode";
/** 配置项接口 */ /** 配置项接口 */
export interface IccoderConfig { export interface IccoderConfig {
@ -16,21 +16,21 @@ export interface IccoderConfig {
/** 默认配置 */ /** 默认配置 */
const DEFAULT_CONFIG: IccoderConfig = { const DEFAULT_CONFIG: IccoderConfig = {
backendUrl: 'http://localhost:8080', backendUrl: "http://localhost:8080",
timeout: 60000, timeout: 60000,
userId: 'default-user' userId: "default-user",
}; };
/** /**
* 获取配置项 * 获取配置项
*/ */
export function getConfig(): IccoderConfig { export function getConfig(): IccoderConfig {
const config = vscode.workspace.getConfiguration('icCoder'); const config = vscode.workspace.getConfiguration("icCoder");
return { return {
backendUrl: config.get<string>('backendUrl', DEFAULT_CONFIG.backendUrl), backendUrl: config.get<string>("backendUrl", DEFAULT_CONFIG.backendUrl),
timeout: config.get<number>('timeout', DEFAULT_CONFIG.timeout), timeout: config.get<number>("timeout", DEFAULT_CONFIG.timeout),
userId: config.get<string>('userId', DEFAULT_CONFIG.userId) userId: config.get<string>("userId", DEFAULT_CONFIG.userId),
}; };
} }
@ -40,7 +40,9 @@ export function getConfig(): IccoderConfig {
export function getApiUrl(path: string): string { export function getApiUrl(path: string): string {
const { backendUrl } = getConfig(); const { backendUrl } = getConfig();
// 确保 URL 格式正确 // 确保 URL 格式正确
const baseUrl = backendUrl.endsWith('/') ? backendUrl.slice(0, -1) : backendUrl; const baseUrl = backendUrl.endsWith("/")
const apiPath = path.startsWith('/') ? path : `/${path}`; ? backendUrl.slice(0, -1)
: backendUrl;
const apiPath = path.startsWith("/") ? path : `/${path}`;
return `${baseUrl}${apiPath}`; return `${baseUrl}${apiPath}`;
} }

View File

@ -0,0 +1,52 @@
/**
* 工具图标定义
* 包含各种工具的 SVG 图标
*/
/**
* 折叠图标 SVG用于可折叠的工具结果
*/
export const collapseIconSvg = `
<span class="tool-collapse-icon">
<svg class="icon-collapsed" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M355.05845325 160.07583932c-19.63862503 19.63862503-19.63862503 51.53175211 0 71.17037712L618.05891976 494.24668297c9.74075802 9.74075802 9.74075802 25.76587604 0 35.50663406L355.05845325 792.75378356c-19.63862503 19.63862503-19.63862503 51.53175211 0 71.17037712s51.53175211 19.63862503 71.17037716 0L706.98261396 583.17037714c39.27725009-39.27725009 39.27725009-102.90639522 0-142.18364526L426.22883041 160.07583932c-19.63862503-19.63862503-51.53175211-19.63862503-71.17037716 0z" fill="#8a8a8a"/>
</svg>
<svg class="icon-expanded" style="display:none;" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M899.70688 272.92672l-382.19776 373.53472-393.45664-384.512a43.52 43.52 0 0 0-60.52352 0 41.14944 41.14944 0 0 0 0 59.14624l423.72096 414.11584a43.35616 43.35616 0 0 0 60.56448 0l412.4672-403.11296a41.20064 41.20064 0 0 0 11.06432-40.41728 42.3424 42.3424 0 0 0-30.2848-29.58336 43.52 43.52 0 0 0-41.35424 10.84416z m0 0" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 文件写入完成图标 SVG
*/
export const fileWriteIconSvg = `
<span class="tool-file-write-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M866.304 852.096H161.728a31.36 31.36 0 0 1-30.528-30.592 31.36 31.36 0 0 1 30.528-30.528h704.64a31.36 31.36 0 0 1 30.528 30.528c0 16.32-12.224 30.592-30.592 30.592z m-65.152-134.4h-392.96a31.36 31.36 0 0 1-30.592-30.592 31.36 31.36 0 0 1 30.528-30.528h391.04a31.36 31.36 0 0 1 30.528 30.528c0 16.32-12.224 30.592-28.544 30.592z m-596.672-179.2l91.648 93.632-40.704 40.768-91.648-91.648 40.704-42.752zM552.704 188.16l91.648 93.696-42.752 40.704-91.648-91.648 42.752-42.752z" fill="#8a8a8a"/>
<path d="M176 733.952a72.96 72.96 0 0 1-50.88-22.4c-14.272-14.272-22.4-36.672-22.4-56.96l8.128-99.84 423.552-425.6a104.576 104.576 0 0 1 75.328-30.528c32.64 0 63.168 14.272 85.568 36.672 24.384 24.384 36.608 56.96 36.608 89.6 0 28.48-12.16 54.976-32.576 73.28l-419.456 427.648-99.84 8.128H176z m-4.096-152.704l-6.08 77.376c0 4.096 2.048 8.128 4.096 8.128h2.048l77.376-6.08 417.344-425.6c8.128-8.128 12.224-18.304 12.224-28.48 0-14.272-6.08-28.48-16.32-38.72-10.24-10.24-22.4-16.32-36.672-16.32-12.16 0-22.4 4.096-30.528 12.224L171.904 581.248z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 语法检查图标 SVG
*/
export const syntaxCheckIconSvg = `
<span class="tool-syntax-check-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M143.36 241.8688h638.976a33.28 33.28 0 0 0 0-66.56H143.36a33.28 33.28 0 0 0 0 66.56zM143.36 421.2736h423.5264a33.28 33.28 0 0 0 0-66.56H143.36a33.28 33.28 0 0 0 0 66.56zM419.0208 532.8384H143.36a33.28 33.28 0 0 0 0 66.56h275.6608a33.28 33.28 0 0 0 0-66.56zM365.5168 709.5296H129.0752a33.28 33.28 0 0 0 0 66.56h236.4416a33.28 33.28 0 1 0 0-66.56zM918.4256 791.8592l-82.5856-82.432a178.8928 178.8928 0 1 0-47.0528 47.0528l82.5856 82.4832a33.28 33.28 0 1 0 47.0528-47.104z m-342.3232-182.9376a112.128 112.128 0 1 1 112.128 112.0768 112.2816 112.2816 0 0 1-112.128-112.0768z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 已检索代码图标 SVG
*/
export const SearchCode = `
<span class="tool-search-code-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M916.33 859.76L678.51 621.94A318.92 318.92 0 0 0 768 400c0-176.73-143.27-320-320-320S128 223.27 128 400s143.27 320 320 320a318.48 318.48 0 0 0 167.88-47.55l243.88 243.88a40 40 0 1 0 56.57-56.57zM192 400c0-141.38 114.62-256 256-256s256 114.62 256 256-114.62 256-256 256-256-114.62-256-256z" fill="#8a8a8a"/>
</svg>
</span>
`;

View File

@ -8,18 +8,22 @@ import {
handleRenameFile, handleRenameFile,
handleReplaceInFile, handleReplaceInFile,
handleUserAnswer, handleUserAnswer,
abortCurrentDialog abortCurrentDialog,
} from "../utils/messageHandler"; } from "../utils/messageHandler";
import { VCDViewerPanel } from "./VCDViewerPanel";
/** /**
* 创建并显示 IC 助手面板 * 创建并显示 IC 助手面板
*/ */
export function showICHelperPanel(context: vscode.ExtensionContext) { export function showICHelperPanel(
context: vscode.ExtensionContext,
viewColumn?: vscode.ViewColumn
) {
// 创建WebView面板 // 创建WebView面板
const panel = vscode.window.createWebviewPanel( const panel = vscode.window.createWebviewPanel(
"icCoder", // 面板ID "icCoder", // 面板ID
"IC Coder", // 面板标题 "IC Coder", // 面板标题
vscode.ViewColumn.Beside, // 显示在旁边 viewColumn || vscode.ViewColumn.Beside, // 默认显示在旁边,但可以指定
{ {
enableScripts: true, enableScripts: true,
retainContextWhenHidden: true, retainContextWhenHidden: true,
@ -28,7 +32,11 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
); );
// 设置标签页图标 // 设置标签页图标
panel.iconPath = vscode.Uri.joinPath(context.extensionUri, "media", "图案(方底).png"); panel.iconPath = vscode.Uri.joinPath(
context.extensionUri,
"media",
"图案(方底).png"
);
// 获取页面内图标URI // 获取页面内图标URI
const iconUri = panel.webview.asWebviewUri( const iconUri = panel.webview.asWebviewUri(
@ -55,7 +63,12 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
handleRenameFile(panel, message.oldPath, message.newPath); handleRenameFile(panel, message.oldPath, message.newPath);
break; break;
case "replaceInFile": case "replaceInFile":
handleReplaceInFile(panel, message.filePath, message.searchText, message.replaceText); handleReplaceInFile(
panel,
message.filePath,
message.searchText,
message.replaceText
);
break; break;
case "insertCode": case "insertCode":
insertCodeToEditor(message.code); insertCodeToEditor(message.code);
@ -63,9 +76,42 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
case "showInfo": case "showInfo":
vscode.window.showInformationMessage(message.text); vscode.window.showInformationMessage(message.text);
break; break;
case "openWaveformViewer":
// 打开波形查看器
if (message.vcdFilePath) {
VCDViewerPanel.createOrShow(
context.extensionUri,
message.vcdFilePath
);
}
break;
case "getVCDInfo":
// 获取 VCD 文件信息
if (message.vcdFilePath && message.containerId) {
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
}
break;
case "createNewConversation":
// 创建新会话 - 在当前编辑器组中打开新标签页
showICHelperPanel(context, panel.viewColumn);
break;
case "loadConversationHistory":
// 加载会话历史(暂未实现)
panel.webview.postMessage({
command: "conversationHistory",
history: [],
});
break;
case "selectConversation":
// 选择会话(暂未实现)
break;
// 新增:处理用户回答 // 新增:处理用户回答
case "submitAnswer": case "submitAnswer":
handleUserAnswer(message.askId, message.selected, message.customInput); handleUserAnswer(
message.askId,
message.selected,
message.customInput
);
break; break;
// 新增:中止对话 // 新增:中止对话
case "abortDialog": case "abortDialog":
@ -77,3 +123,182 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
context.subscriptions context.subscriptions
); );
} }
/**
* 获取 VCD 文件信息
*/
async function getVCDFileInfo(
panel: vscode.WebviewPanel,
vcdFilePath: string,
containerId: string
) {
try {
const fs = require("fs");
const path = require("path");
// 检查文件是否存在
if (!fs.existsSync(vcdFilePath)) {
panel.webview.postMessage({
command: "vcdInfo",
containerId: containerId,
vcdInfo: {
signalCount: "N/A",
timeRange: "N/A",
fileSize: "N/A",
error: "文件不存在",
},
});
return;
}
// 获取文件大小
const stats = fs.statSync(vcdFilePath);
const fileSizeKB = stats.size / 1024;
const fileSize =
fileSizeKB < 1024
? `${fileSizeKB.toFixed(2)} KB`
: `${(fileSizeKB / 1024).toFixed(2)} MB`;
// 读取 VCD 文件内容
const content = fs.readFileSync(vcdFilePath, "utf-8");
// 解析信号数量
const varMatches = content.match(/\$var/g);
const signalCount = varMatches ? varMatches.length : 0;
// 解析时间范围
let timeRange = "N/A";
const timeMatch = content.match(/#(\d+)/g);
if (timeMatch && timeMatch.length > 0) {
const times = timeMatch.map((t: string) => parseInt(t.substring(1)));
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
timeRange = `${minTime} - ${maxTime}`;
}
// 解析前几个信号的真实数据
const signals = parseVCDSignals(content, 3); // 只解析前3个信号
// 发送信息回前端
panel.webview.postMessage({
command: "vcdInfo",
containerId: containerId,
vcdInfo: {
signalCount: signalCount.toString(),
timeRange: timeRange,
fileSize: fileSize,
signals: signals, // 添加真实信号数据
},
});
} catch (error) {
console.error("获取 VCD 文件信息失败:", error);
panel.webview.postMessage({
command: "vcdInfo",
containerId: containerId,
vcdInfo: {
signalCount: "N/A",
timeRange: "N/A",
fileSize: "N/A",
error: error instanceof Error ? error.message : "未知错误",
},
});
}
}
/**
* 解析 VCD 文件中的信号数据
*/
function parseVCDSignals(content: string, maxSignals: number = 3) {
const signals: Array<{
name: string;
identifier: string;
width: number;
values: Array<{ time: number; value: string }>;
}> = [];
try {
// 1. 解析信号定义部分
const varRegex = /\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+([^\$]+?)\s+\$end/g;
let match;
const signalDefs: Array<{
name: string;
identifier: string;
width: number;
}> = [];
while (
(match = varRegex.exec(content)) !== null &&
signalDefs.length < maxSignals
) {
const width = parseInt(match[2]);
const identifier = match[3];
const name = match[4].trim();
signalDefs.push({ name, identifier, width });
}
// 2. 找到数据变化部分的起始位置
const dumpvarsIndex = content.indexOf("$dumpvars");
if (dumpvarsIndex === -1) {
return signals;
}
const dataSection = content.substring(dumpvarsIndex);
// 3. 解析每个信号的值变化
for (const signalDef of signalDefs) {
const values: Array<{ time: number; value: string }> = [];
let currentTime = 0;
// 分行处理数据
const lines = dataSection.split("\n");
for (const line of lines) {
const trimmedLine = line.trim();
// 解析时间戳
if (trimmedLine.startsWith("#")) {
currentTime = parseInt(trimmedLine.substring(1));
continue;
}
// 解析信号值变化
// 格式1: 单比特信号 "0!" 或 "1!"
// 格式2: 多比特信号 "b1010 !"
if (signalDef.width === 1) {
// 单比特信号
const singleBitMatch = trimmedLine.match(
new RegExp(`^([01xz])${signalDef.identifier}$`)
);
if (singleBitMatch) {
values.push({ time: currentTime, value: singleBitMatch[1] });
}
} else {
// 多比特信号
const multiBitMatch = trimmedLine.match(
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`)
);
if (multiBitMatch) {
values.push({ time: currentTime, value: multiBitMatch[1] });
}
}
// 限制采样点数量,避免数据过多
if (values.length >= 50) {
break;
}
}
signals.push({
name: signalDef.name,
identifier: signalDef.identifier,
width: signalDef.width,
values: values,
});
}
} catch (error) {
console.error("解析 VCD 信号数据失败:", error);
}
return signals;
}

View File

@ -37,6 +37,8 @@ export interface DialogCallbacks {
onToolError?: (toolName: string, error: string) => void; onToolError?: (toolName: string, error: string) => void;
/** 显示问题ask_user */ /** 显示问题ask_user */
onQuestion?: (askId: string, question: string, options: string[]) => void; onQuestion?: (askId: string, question: string, options: string[]) => void;
/** 实时更新段落(流式过程中) */
onSegmentUpdate?: (segments: MessageSegment[]) => void;
/** 对话完成,返回所有段落 */ /** 对话完成,返回所有段落 */
onComplete?: (segments: MessageSegment[]) => void; onComplete?: (segments: MessageSegment[]) => void;
/** 错误 */ /** 错误 */
@ -155,6 +157,8 @@ export class DialogSession {
this.appendText(data.text); this.appendText(data.text);
console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length); console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length);
callbacks.onText?.(this.accumulatedText, true); callbacks.onText?.(this.accumulatedText, true);
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
}, },
onToolCall: async (data: ToolCallRequest) => { onToolCall: async (data: ToolCallRequest) => {
@ -166,16 +170,22 @@ export class DialogSession {
console.log('[DialogSession] onToolCall: 跳过重复的工具段落:', toolName); console.log('[DialogSession] onToolCall: 跳过重复的工具段落:', toolName);
} else { } else {
this.addToolSegment(toolName, 'running'); this.addToolSegment(toolName, 'running');
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
} }
// 注意:不在这里调用 callbacks.onToolStart避免与 onToolStart 事件重复 // 注意:不在这里调用 callbacks.onToolStart避免与 onToolStart 事件重复
try { try {
await executeToolCall(data, this.toolContext); await executeToolCall(data, this.toolContext);
this.updateToolSegment(toolName, 'success', '执行完成'); this.updateToolSegment(toolName, 'success', '执行完成');
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
// 也不调用 callbacks.onToolComplete避免重复 // 也不调用 callbacks.onToolComplete避免重复
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : '未知错误'; const errorMsg = error instanceof Error ? error.message : '未知错误';
this.updateToolSegment(toolName, 'error', errorMsg); this.updateToolSegment(toolName, 'error', errorMsg);
callbacks.onToolError?.(toolName, errorMsg); callbacks.onToolError?.(toolName, errorMsg);
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
} }
}, },
@ -187,6 +197,8 @@ export class DialogSession {
console.log('[DialogSession] 跳过重复的工具段落:', data.tool_name); console.log('[DialogSession] 跳过重复的工具段落:', data.tool_name);
} else { } else {
this.addToolSegment(data.tool_name, 'running'); this.addToolSegment(data.tool_name, 'running');
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
} }
console.log('[DialogSession] segments 数量:', this.segments.length); console.log('[DialogSession] segments 数量:', this.segments.length);
callbacks.onToolStart?.(data.tool_name); callbacks.onToolStart?.(data.tool_name);
@ -195,11 +207,15 @@ export class DialogSession {
onToolComplete: (data) => { onToolComplete: (data) => {
this.updateToolSegment(data.tool_name, 'success', data.result); this.updateToolSegment(data.tool_name, 'success', data.result);
callbacks.onToolComplete?.(data.tool_name, data.result); callbacks.onToolComplete?.(data.tool_name, data.result);
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
}, },
onToolError: (data) => { onToolError: (data) => {
this.updateToolSegment(data.tool_name, 'error', data.error); this.updateToolSegment(data.tool_name, 'error', data.error);
callbacks.onToolError?.(data.tool_name, data.error); callbacks.onToolError?.(data.tool_name, data.error);
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
}, },
onAskUser: async (data: AskUserEvent) => { onAskUser: async (data: AskUserEvent) => {
@ -210,6 +226,9 @@ export class DialogSession {
question: data.question, question: data.question,
options: data.options options: data.options
}); });
// 实时发送段落更新(包含问题)
callbacks.onSegmentUpdate?.(this.segments);
// 同时调用 onQuestion 用于更新状态栏等
callbacks.onQuestion?.(data.askId, data.question, data.options); callbacks.onQuestion?.(data.askId, data.question, data.options);
try { try {
await userInteractionManager.handleAskUser(data, this.taskId); await userInteractionManager.handleAskUser(data, this.taskId);

View File

@ -42,15 +42,8 @@ export class UserInteractionManager {
console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`); console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`);
// 通过 WebView 显示问题 // 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理
if (this.webviewPanel) { // 这里不再单独发送 showQuestion 命令,避免重复显示
this.webviewPanel.webview.postMessage({
command: 'showQuestion',
askId,
question,
options
});
}
// 创建 Promise 等待用户回答 // 创建 Promise 等待用户回答
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -1,4 +1,5 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import * as path from "path";
import { readFileContent } from "./readFiles"; import { readFileContent } from "./readFiles";
import { import {
createFile, createFile,
@ -115,23 +116,19 @@ async function handleUserMessageWithBackend(
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
currentSession!.sendMessage(text, { currentSession!.sendMessage(text, {
onText: (fullText, isStreaming) => { onText: (fullText, isStreaming) => {
if (isStreaming) { // 不再单独处理文本,统一通过 onSegmentUpdate 处理
// 流式更新消息 },
onSegmentUpdate: (segments) => {
// 实时发送段落更新,按后端返回顺序展示
panel.webview.postMessage({ panel.webview.postMessage({
command: "updateStreamingMessage", command: "updateSegments",
text: fullText, segments: segments,
}); });
}
// 注意:完成时通过 onComplete 发送分段消息
}, },
onToolStart: (toolName) => { onToolStart: (toolName) => {
// 实时显示工具状态 // 更新状态
panel.webview.postMessage({
command: "toolStart",
toolName,
});
// 同时更新状态栏
panel.webview.postMessage({ panel.webview.postMessage({
command: "updateStatus", command: "updateStatus",
text: `正在执行 ${toolName}...`, text: `正在执行 ${toolName}...`,
@ -140,25 +137,15 @@ async function handleUserMessageWithBackend(
}, },
onToolComplete: (toolName, result) => { onToolComplete: (toolName, result) => {
// 实时更新工具状态 // 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
panel.webview.postMessage({
command: "toolComplete",
toolName,
result,
});
}, },
onToolError: (toolName, error) => { onToolError: (toolName, error) => {
// 实时显示工具错误 // 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
panel.webview.postMessage({
command: "toolError",
toolName,
error,
});
}, },
onQuestion: (askId, question, options) => { onQuestion: (askId, question, options) => {
// 问题会在分段消息中显示,这里只更新状态栏 // 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
panel.webview.postMessage({ panel.webview.postMessage({
command: "updateStatus", command: "updateStatus",
text: "等待用户回答...", text: "等待用户回答...",
@ -172,13 +159,14 @@ async function handleUserMessageWithBackend(
command: "hideStatus", command: "hideStatus",
}); });
// 发送分段消息 // 最后一次发送完整的段落
console.log('[MessageHandler] 发送分段消息, 段落数:', segments.length); console.log('[MessageHandler] 对话完成, 段落数:', segments.length);
console.log('[MessageHandler] segments 内容:', JSON.stringify(segments)); console.log('[MessageHandler] segments 内容:', JSON.stringify(segments));
const result = await panel.webview.postMessage({ const result = await panel.webview.postMessage({
command: "receiveSegments", command: "updateSegments",
segments: segments, segments: segments,
isComplete: true,
}); });
console.log('[MessageHandler] postMessage 返回值:', result); console.log('[MessageHandler] postMessage 返回值:', result);
@ -777,17 +765,24 @@ async function handleVCDGeneration(
successMsg += `\n\n仿真输出:\n${result.stdout}`; successMsg += `\n\n仿真输出:\n${result.stdout}`;
} }
// 发送带波形预览的消息
if (result.vcdFilePath) {
const fileName = path.basename(result.vcdFilePath);
panel.webview.postMessage({
command: "vcdGenerated",
text: successMsg,
vcdFilePath: result.vcdFilePath,
fileName: fileName,
});
vscode.window.showInformationMessage(
`VCD 文件生成成功: ${fileName}`
);
} else {
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
text: successMsg, text: successMsg,
}); });
// 自动打开 VCD 波形查看器
if (result.vcdFilePath) {
vscode.commands.executeCommand("ic-coder.openVCDViewer", result.vcdFilePath);
vscode.window.showInformationMessage(
`VCD 文件生成成功,已自动打开波形查看器`
);
} }
} else { } else {
let errorMsg = `${result.message}`; let errorMsg = `${result.message}`;

View File

@ -0,0 +1,308 @@
/**
* 获取会话历史栏的 HTML 内容
*/
export function getConversationHistoryBarContent(): string {
return `
<div class="conversation-history-bar">
<div class="history-dropdown-container">
<button class="history-dropdown-button" onclick="toggleHistoryDropdown()">
<span class="dropdown-label">Past Conversations</span>
<svg class="dropdown-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z" fill="currentColor"/>
</svg>
</button>
<div class="history-dropdown-menu" id="historyDropdownMenu">
<div class="history-list" id="historyList">
<!-- 会话历史列表将在这里动态生成 -->
</div>
</div>
</div>
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
</svg>
</button>
</div>
`;
}
/**
* 获取会话历史栏的 CSS 样式
*/
export function getConversationHistoryBarStyles(): string {
return `
.conversation-history-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: var(--vscode-tab-activeBackground);
border-bottom: 1px solid var(--vscode-panel-border);
flex-shrink: 0;
min-height: 35px;
}
.history-dropdown-container {
position: relative;
flex: 1;
}
.history-dropdown-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: transparent;
color: var(--vscode-input-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.history-dropdown-button:hover {
opacity: 0.8;
}
.dropdown-label {
white-space: nowrap;
}
.dropdown-icon {
width: 12px;
height: 12px;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.history-dropdown-button.active .dropdown-icon {
transform: rotate(180deg);
}
.history-dropdown-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
min-width: 300px;
max-height: 400px;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-height: 400px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.history-dropdown-menu.active {
display: block;
}
.history-list {
padding: 4px 0;
}
.history-item {
padding: 10px 16px;
cursor: pointer;
transition: background 0.2s ease;
border-bottom: 1px solid var(--vscode-panel-border);
}
.history-item:last-child {
border-bottom: none;
}
.history-item:hover {
background: var(--vscode-list-hoverBackground);
}
.history-item-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-item-time {
font-size: 12px;
opacity: 0.7;
}
.history-empty {
padding: 20px;
text-align: center;
color: var(--vscode-descriptionForeground);
font-size: 14px;
}
.new-conversation-button {
width: 36px;
height: 36px;
padding: 0;
background: transparent;
color: var(--vscode-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
}
.new-conversation-button:hover {
opacity: 0.7;
}
.new-conversation-button:active {
opacity: 0.5;
}
.new-conversation-button svg {
width: 20px;
height: 20px;
}
/* 滚动条样式 */
.history-dropdown-menu::-webkit-scrollbar {
width: 8px;
}
.history-dropdown-menu::-webkit-scrollbar-track {
background: transparent;
}
.history-dropdown-menu::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.5);
border-radius: 4px;
}
.history-dropdown-menu::-webkit-scrollbar-thumb:hover {
background: rgba(128, 128, 128, 0.7);
}
`;
}
/**
* 获取会话历史栏的 JavaScript 脚本
*/
export function getConversationHistoryBarScript(): string {
return `
// 会话历史相关变量
let conversationHistory = [];
let currentConversationId = null;
// 切换历史记录下拉菜单
function toggleHistoryDropdown() {
const menu = document.getElementById('historyDropdownMenu');
const button = document.querySelector('.history-dropdown-button');
if (menu.classList.contains('active')) {
menu.classList.remove('active');
button.classList.remove('active');
} else {
menu.classList.add('active');
button.classList.add('active');
// 加载会话历史
loadConversationHistory();
}
}
// 加载会话历史
function loadConversationHistory() {
vscode.postMessage({ command: 'loadConversationHistory' });
}
// 渲染会话历史列表
function renderConversationHistory(history) {
conversationHistory = history;
const historyList = document.getElementById('historyList');
if (!history || history.length === 0) {
historyList.innerHTML = '<div class="history-empty">暂无会话历史</div>';
return;
}
historyList.innerHTML = history.map(item => \`
<div class="history-item"
onclick="selectConversation('\${item.id}')">
<div class="history-item-title">\${item.title || '未命名会话'}</div>
<div class="history-item-time">\${formatTime(item.timestamp)}</div>
</div>
\`).join('');
}
// 选择会话
function selectConversation(conversationId) {
currentConversationId = conversationId;
vscode.postMessage({
command: 'selectConversation',
conversationId: conversationId
});
// 关闭下拉菜单
const menu = document.getElementById('historyDropdownMenu');
const button = document.querySelector('.history-dropdown-button');
menu.classList.remove('active');
button.classList.remove('active');
}
// 创建新会话
function createNewConversation() {
vscode.postMessage({ command: 'createNewConversation' });
}
// 格式化时间
function formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
// 小于1分钟
if (diff < 60000) {
return '刚刚';
}
// 小于1小时
if (diff < 3600000) {
return Math.floor(diff / 60000) + '分钟前';
}
// 小于1天
if (diff < 86400000) {
return Math.floor(diff / 3600000) + '小时前';
}
// 小于7天
if (diff < 604800000) {
return Math.floor(diff / 86400000) + '天前';
}
// 超过7天显示具体日期
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
// 点击外部关闭下拉菜单
document.addEventListener('click', (event) => {
const container = document.querySelector('.history-dropdown-container');
const menu = document.getElementById('historyDropdownMenu');
const button = document.querySelector('.history-dropdown-button');
if (menu && menu.classList.contains('active')) {
if (!container.contains(event.target)) {
menu.classList.remove('active');
button.classList.remove('active');
}
}
});
`;
}

623
src/views/inputArea.ts Normal file
View File

@ -0,0 +1,623 @@
import { getWaveformPreviewContent } from "./waveformPreviewContent";
/**
* 获取输入区域的 HTML 内容
*/
export function getInputAreaContent(): string {
return `
<div class="input-area">
<div class="input-group">
<div class="input-wrapper">
<!-- Plan 开关 -->
<div class="plan-toggle-container">
<div class="tooltip">
<label class="plan-toggle">
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()">
<span class="plan-toggle-slider"></span>
<span class="plan-toggle-label">Plan</span>
</label>
<span class="tooltiptext" id="planTooltip">启用 Plan 模式</span>
</div>
</div>
<textarea
id="messageInput"
placeholder="输入您的问题..."
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
></textarea>
<div class="input-bottom-row">
<div class="mode-selector">
<div class="tooltip">
<select id="modeSelect">
<option value="agent" selected>Agent</option>
<option value="ask">Ask</option>
<option value="auto">Auto</option>
</select>
<span class="tooltiptext">切换模型</span>
</div>
</div>
<div class="input-actions">
<!-- 上下文显示 -->
<div class="context-display">
<div class="context-info" onclick="toggleContextPanel()">
<div class="database-icon">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" class="db-svg">
<!-- 数据库容器主体 - 底层灰色 -->
<path d="M870.4 57.6C780.8 19.2 652.8 0 512 0 371.2 0 243.2 19.2 153.6 57.6 51.2 102.4 0 153.6 0 211.2l0 595.2c0 57.6 51.2 115.2 153.6 153.6C243.2 1004.8 371.2 1024 512 1024c140.8 0 268.8-19.2 358.4-57.6 96-38.4 153.6-96 153.6-153.6L1024 211.2C1024 153.6 972.8 102.4 870.4 57.6L870.4 57.6zM812.8 320C729.6 352 614.4 364.8 512 364.8 403.2 364.8 294.4 352 211.2 320 115.2 294.4 70.4 256 70.4 211.2c0-38.4 51.2-76.8 140.8-108.8C294.4 76.8 403.2 64 512 64c102.4 0 217.6 19.2 300.8 44.8 89.6 32 140.8 70.4 140.8 108.8C953.6 256 908.8 294.4 812.8 320L812.8 320zM819.2 505.6C736 531.2 620.8 550.4 512 550.4c-108.8 0-217.6-19.2-307.2-44.8C115.2 473.6 64 435.2 64 396.8L64 326.4C128 352 172.8 384 243.2 396.8 326.4 416 416 428.8 512 428.8c96 0 185.6-12.8 268.8-32C851.2 384 896 352 960 326.4l0 76.8C960 435.2 908.8 473.6 819.2 505.6L819.2 505.6zM819.2 710.4c-83.2 25.6-198.4 44.8-307.2 44.8-108.8 0-217.6-19.2-307.2-44.8C115.2 684.8 64 646.4 64 601.6L64 505.6c64 32 108.8 57.6 179.2 76.8C326.4 601.6 416 614.4 512 614.4c96 0 185.6-12.8 268.8-32C851.2 563.2 896 537.6 960 505.6l0 96C960 646.4 908.8 684.8 819.2 710.4L819.2 710.4zM512 960c-108.8 0-217.6-19.2-307.2-44.8C115.2 889.6 64 851.2 64 812.8l0-96c64 32 108.8 57.6 179.2 76.8 76.8 19.2 172.8 32 262.4 32 96 0 185.6-12.8 268.8-32 76.8-19.2 121.6-44.8 185.6-76.8l0 96c0 38.4-51.2 76.8-140.8 108.8C736 947.2 614.4 960 512 960L512 960z" fill="#94a3b8" class="db-body"/>
<!-- 填充进度效果 - 从下往上填充蓝色 -->
<defs>
<mask id="fill-mask">
<rect x="0" y="0" width="1024" height="1024" id="fillRect" fill="white"/>
</mask>
</defs>
<g mask="url(#fill-mask)">
<path d="M870.4 57.6C780.8 19.2 652.8 0 512 0 371.2 0 243.2 19.2 153.6 57.6 51.2 102.4 0 153.6 0 211.2l0 595.2c0 57.6 51.2 115.2 153.6 153.6C243.2 1004.8 371.2 1024 512 1024c140.8 0 268.8-19.2 358.4-57.6 96-38.4 153.6-96 153.6-153.6L1024 211.2C1024 153.6 972.8 102.4 870.4 57.6L870.4 57.6zM812.8 320C729.6 352 614.4 364.8 512 364.8 403.2 364.8 294.4 352 211.2 320 115.2 294.4 70.4 256 70.4 211.2c0-38.4 51.2-76.8 140.8-108.8C294.4 76.8 403.2 64 512 64c102.4 0 217.6 19.2 300.8 44.8 89.6 32 140.8 70.4 140.8 108.8C953.6 256 908.8 294.4 812.8 320L812.8 320zM819.2 505.6C736 531.2 620.8 550.4 512 550.4c-108.8 0-217.6-19.2-307.2-44.8C115.2 473.6 64 435.2 64 396.8L64 326.4C128 352 172.8 384 243.2 396.8 326.4 416 416 428.8 512 428.8c96 0 185.6-12.8 268.8-32C851.2 384 896 352 960 326.4l0 76.8C960 435.2 908.8 473.6 819.2 505.6L819.2 505.6zM819.2 710.4c-83.2 25.6-198.4 44.8-307.2 44.8-108.8 0-217.6-19.2-307.2-44.8C115.2 684.8 64 646.4 64 601.6L64 505.6c64 32 108.8 57.6 179.2 76.8C326.4 601.6 416 614.4 512 614.4c96 0 185.6-12.8 268.8-32C851.2 563.2 896 537.6 960 505.6l0 96C960 646.4 908.8 684.8 819.2 710.4L819.2 710.4zM512 960c-108.8 0-217.6-19.2-307.2-44.8C115.2 889.6 64 851.2 64 812.8l0-96c64 32 108.8 57.6 179.2 76.8 76.8 19.2 172.8 32 262.4 32 96 0 185.6-12.8 268.8-32 76.8-19.2 121.6-44.8 185.6-76.8l0 96c0 38.4-51.2 76.8-140.8 108.8C736 947.2 614.4 960 512 960L512 960z" fill="#409eff" class="db-fill"/>
</g>
</svg>
</div>
<span class="context-percentage" id="contextPercentage">0%</span>
</div>
<!-- 上下文信息弹窗 -->
<div id="contextPanel" class="context-panel">
<div class="context-panel-content">
<div class="context-info-text" id="contextInfoText">
0k / 200k 已用上下文
</div>
<button class="compress-button" onclick="compressConversation()">
压缩会话
</button>
</div>
</div>
</div>
<!-- 一键优化按钮 -->
<div class="tooltip">
<button id="optimizeButton" class="optimize-button" onclick="handleOptimize()">
<svg t="1765867478136" id="optimizeIcon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2314"><path d="M490.048929 399.773864c7.042381-21.120144 36.85976-21.120144 43.902142 0l41.273372 123.957105A184.967743 184.967743 0 0 0 692.274156 640.713687l123.890111 41.273373c21.119144 7.042381 21.119144 36.85976 0 43.902141L692.207161 767.162574A184.967743 184.967743 0 0 0 575.224443 884.212286l-41.273372 123.890111A23.09997 23.09997 0 0 1 512 1024c-9.983123 0-18.838344-6.409437-21.951071-15.897603L448.775557 884.145292A184.946745 184.946745 0 0 0 331.792839 767.162574L207.836733 725.889201A23.09997 23.09997 0 0 1 191.93813 703.93813c0-9.983123 6.409437-18.838344 15.897603-21.95107l123.957106-41.273373A184.946745 184.946745 0 0 0 448.775557 523.730969zM242.840657 73.466543A13.888779 13.888779 0 0 1 256.022498 63.94338c5.987474 0 11.299007 3.839663 13.182841 9.523163l24.767824 74.360464a111.070238 111.070238 0 0 0 70.19983 70.20083l74.360464 24.767824A13.888779 13.888779 0 0 1 448.05662 255.977502c0 5.987474-3.839663 11.299007-9.523163 13.182841l-74.360464 24.767823a110.947249 110.947249 0 0 0-70.20083 70.199831l-24.767824 74.360464A13.888779 13.888779 0 0 1 256.022498 448.011624a13.888779 13.888779 0 0 1-13.182841-9.523163l-24.767823-74.360464a110.947249 110.947249 0 0 0-70.199831-70.20083l-74.360464-24.767824A13.888779 13.888779 0 0 1 63.988376 255.977502c0-5.987474 3.839663-11.299007 9.523163-13.182841l74.360464-24.767824a110.947249 110.947249 0 0 0 70.20083-70.19983zM695.213897 6.335443a9.283184 9.283184 0 0 1 17.538459 0L729.260905 55.86509a73.889506 73.889506 0 0 0 46.843883 46.843883l49.530646 16.509549a9.283184 9.283184 0 0 1 0 17.538458L776.106787 153.266529a73.9585 73.9585 0 0 0-46.843882 46.843883l-16.509549 49.530647a9.283184 9.283184 0 0 1-17.538459 0L678.705348 200.112412a73.9585 73.9585 0 0 0-46.843883-46.843883l-49.468652-16.509549a9.283184 9.283184 0 0 1 0-17.538458l49.535646-16.509549a73.897505 73.897505 0 0 0 46.842883-46.843883L695.213897 6.397438z m0 0" p-id="2315" fill="#409eff"></path></svg>
</button>
<span class="tooltiptext" id="optimizeTooltip">一键优化</span>
</div>
<button onclick="sendMessage()">发送</button>
</div>
</div>
</div>
</div>
</div>
`;
}
/**
* 获取输入区域的样式
*/
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%;
}
/* Plan 开关样式 */
.plan-toggle-container {
display: flex;
justify-content: flex-end;
margin-bottom: -4px;
}
.plan-toggle {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
.plan-toggle input[type="checkbox"] {
display: none;
}
.plan-toggle-slider {
position: relative;
width: 36px;
height: 20px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 10px;
transition: all 0.3s ease;
}
.plan-toggle-slider::before {
content: "";
position: absolute;
width: 14px;
height: 14px;
left: 2px;
top: 2px;
background: var(--vscode-foreground);
border-radius: 50%;
transition: all 0.3s ease;
}
.plan-toggle input[type="checkbox"]:checked + .plan-toggle-slider {
background: #409eff;
border-color: #409eff;
}
.plan-toggle input[type="checkbox"]:checked + .plan-toggle-slider::before {
transform: translateX(16px);
background: white;
}
.plan-toggle-label {
font-size: 13px;
font-weight: 500;
color: var(--vscode-foreground);
}
.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();
}
// Plan 开关处理函数
function handlePlanToggle() {
const planToggle = document.getElementById('planToggle');
const planTooltip = document.getElementById('planTooltip');
if (planToggle && planTooltip) {
if (planToggle.checked) {
// 开启 Plan 模式
planTooltip.textContent = '关闭 Plan 模式';
} else {
// 关闭 Plan 模式
planTooltip.textContent = '启用 Plan 模式';
}
}
}
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 = '<path d="M581.056 288.32H232.96l108.352-102.208c15.552-15.744 19.456-31.104 4.16-46.208-16.064-15.872-32.576-15.808-48.64 0l-145.92 144.768c-8.768 8.832-23.488 20.608-22.08 32.448l0.64 4.8-0.64 4.864c-1.344 11.776 6.4 18.24 14.848 26.816l147.648 145.216c16.064 15.808 38.08 20.992 54.144 5.12 15.296-15.104 3.84-38.208-11.328-53.504L233.152 353.6 581.056 352c126.464 0 250.944 111.488 250.944 236.16C832 712.832 707.52 832 581.056 832H246.4c-22.592 0-29.696 9.6-29.696 32.256s7.04 31.744 29.696 31.744H581.12C755.136 896 896 757.696 896 588.16c0-169.408-140.8-299.84-314.944-299.84z" fill="currentColor"/><path d="M323.392 192a32 32 0 1 1 0-64 32 32 0 0 1 0 64zM320.192 514.048a32 32 0 1 1 0-64 32 32 0 0 1 0 64zM237.824 896a32 32 0 1 1 0-64 32 32 0 0 1 0 64z" fill="currentColor"/>';
optimizeTooltip.textContent = '撤回';
}
}
function resetOptimizeButton() {
const optimizeIcon = document.getElementById('optimizeIcon');
const optimizeTooltip = document.getElementById('optimizeTooltip');
if (optimizeIcon && optimizeTooltip) {
// 切换回优化图标(星星图标)
optimizeIcon.innerHTML = '<path d="M490.048929 399.773864c7.042381-21.120144 36.85976-21.120144 43.902142 0l41.273372 123.957105A184.967743 184.967743 0 0 0 692.274156 640.713687l123.890111 41.273373c21.119144 7.042381 21.119144 36.85976 0 43.902141L692.207161 767.162574A184.967743 184.967743 0 0 0 575.224443 884.212286l-41.273372 123.890111A23.09997 23.09997 0 0 1 512 1024c-9.983123 0-18.838344-6.409437-21.951071-15.897603L448.775557 884.145292A184.946745 184.946745 0 0 0 331.792839 767.162574L207.836733 725.889201A23.09997 23.09997 0 0 1 191.93813 703.93813c0-9.983123 6.409437-18.838344 15.897603-21.95107l123.957106-41.273373A184.946745 184.946745 0 0 0 448.775557 523.730969zM242.840657 73.466543A13.888779 13.888779 0 0 1 256.022498 63.94338c5.987474 0 11.299007 3.839663 13.182841 9.523163l24.767824 74.360464a111.070238 111.070238 0 0 0 70.19983 70.20083l74.360464 24.767824A13.888779 13.888779 0 0 1 448.05662 255.977502c0 5.987474-3.839663 11.299007-9.523163 13.182841l-74.360464 24.767823a110.947249 110.947249 0 0 0-70.20083 70.199831l-24.767824 74.360464A13.888779 13.888779 0 0 1 256.022498 448.011624a13.888779 13.888779 0 0 1-13.182841-9.523163l-24.767823-74.360464a110.947249 110.947249 0 0 0-70.199831-70.20083l-74.360464-24.767824A13.888779 13.888779 0 0 1 63.988376 255.977502c0-5.987474 3.839663-11.299007 9.523163-13.182841l74.360464-24.767824a110.947249 110.947249 0 0 0 70.20083-70.19983zM695.213897 6.335443a9.283184 9.283184 0 0 1 17.538459 0L729.260905 55.86509a73.889506 73.889506 0 0 0 46.843883 46.843883l49.530646 16.509549a9.283184 9.283184 0 0 1 0 17.538458L776.106787 153.266529a73.9585 73.9585 0 0 0-46.843882 46.843883l-16.509549 49.530647a9.283184 9.283184 0 0 1-17.538459 0L678.705348 200.112412a73.9585 73.9585 0 0 0-46.843883-46.843883l-49.468652-16.509549a9.283184 9.283184 0 0 1 0-17.538458l49.535646-16.509549a73.897505 73.897505 0 0 0 46.842883-46.843883L695.213897 6.397438z m0 0" fill="#409eff"/>';
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');
}
}
});
`;
}

1266
src/views/messageArea.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,350 @@
/**
* 获取波形预览组件的样式内容(纯 CSS不包含 style 标签)
*/
export function getWaveformPreviewContent(): string {
return `
/* 波形预览组件样式 */
.waveform-preview {
margin-top: 12px;
border: 1px solid var(--vscode-panel-border);
border-radius: 8px;
overflow: hidden;
background: var(--vscode-editor-background);
}
.waveform-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: var(--vscode-input-background);
border-bottom: 1px solid var(--vscode-panel-border);
}
.waveform-preview-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 500;
color: var(--vscode-foreground);
}
.waveform-preview-title svg {
width: 16px;
height: 16px;
color: var(--vscode-button-background);
}
.waveform-expand-btn {
padding: 4px 12px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
transition: opacity 0.2s ease;
}
.waveform-expand-btn:hover {
opacity: 0.9;
}
.waveform-expand-btn svg {
width: 14px;
height: 14px;
}
.waveform-preview-content {
padding: 0;
min-height: 200px;
max-height: 300px;
overflow: hidden;
position: relative;
background: var(--vscode-editor-background);
}
.waveform-preview-canvas {
width: 100%;
height: 100%;
min-height: 200px;
}
.waveform-preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--vscode-descriptionForeground);
font-size: 13px;
text-align: center;
padding: 20px;
}
.waveform-preview-placeholder svg {
width: 48px;
height: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.waveform-info {
margin-top: 8px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
}
.waveform-mini-viewer {
width: 100%;
height: 200px;
background: var(--vscode-editor-background);
position: relative;
overflow: hidden;
}
.waveform-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--vscode-descriptionForeground);
font-size: 12px;
}
`;
}
/**
* 获取波形预览组件的 JavaScript 代码
*/
export function getWaveformPreviewScript(): string {
return `
/**
* 创建波形预览组件
*/
function createWaveformPreview(vcdFilePath, fileName) {
const previewDiv = document.createElement('div');
previewDiv.className = 'waveform-preview';
// 头部
const header = document.createElement('div');
header.className = 'waveform-preview-header';
const title = document.createElement('div');
title.className = 'waveform-preview-title';
title.innerHTML = \`
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M128 512h128l64-128 64 128 64-256 64 384 64-128h320"
stroke="currentColor"
stroke-width="64"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"/>
</svg>
<span>波形预览 - \${fileName}</span>
\`;
const expandBtn = document.createElement('button');
expandBtn.className = 'waveform-expand-btn';
expandBtn.innerHTML = \`
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M342 88.3h340c56.6 0 102.6 46 102.6 102.6v340c0 56.6-46 102.6-102.6 102.6H342c-56.6 0-102.6-46-102.6-102.6v-340c0-56.6 46-102.6 102.6-102.6z"
fill="none"
stroke="currentColor"
stroke-width="48"/>
<path d="M239.4 390.5v340c0 56.6 46 102.6 102.6 102.6h340"
fill="none"
stroke="currentColor"
stroke-width="48"
stroke-linecap="round"/>
</svg>
展开查看
\`;
expandBtn.onclick = () => openFullWaveform(vcdFilePath);
header.appendChild(title);
header.appendChild(expandBtn);
// 内容区域 - 创建一个唯一ID的容器用于显示波形
const content = document.createElement('div');
content.className = 'waveform-preview-content';
const miniViewerId = 'waveform-mini-' + Date.now();
const miniViewer = document.createElement('div');
miniViewer.id = miniViewerId;
miniViewer.className = 'waveform-mini-viewer';
// 添加加载提示
const loadingDiv = document.createElement('div');
loadingDiv.className = 'waveform-loading';
loadingDiv.textContent = '正在加载波形预览...';
miniViewer.appendChild(loadingDiv);
content.appendChild(miniViewer);
previewDiv.appendChild(header);
previewDiv.appendChild(content);
// 异步加载波形数据
loadMiniWaveform(miniViewerId, vcdFilePath, loadingDiv);
return previewDiv;
}
/**
* 加载迷你波形预览
*/
async function loadMiniWaveform(containerId, vcdFilePath, loadingDiv) {
try {
// 请求 VCD 文件信息
vscode.postMessage({
command: 'getVCDInfo',
vcdFilePath: vcdFilePath,
containerId: containerId
});
} catch (error) {
console.error('加载波形预览失败:', error);
loadingDiv.textContent = '波形预览加载失败';
loadingDiv.style.color = 'var(--vscode-errorForeground)';
}
}
/**
* 渲染波形预览信息
*/
function renderWaveformInfo(containerId, vcdInfo) {
const container = document.getElementById(containerId);
if (!container) return;
// 清空容器
container.innerHTML = '';
// 绘制真实波形
const waveformSvg = document.createElement('div');
waveformSvg.innerHTML = drawRealWaveform(vcdInfo.signals || []);
container.appendChild(waveformSvg);
}
/**
* 绘制真实波形
*/
function drawRealWaveform(signals) {
if (!signals || signals.length === 0) {
return \`
<svg width="100%" height="80" viewBox="0 0 800 80" style="background: var(--vscode-editor-background);">
<text x="400" y="40" fill="var(--vscode-descriptionForeground)" font-size="12" text-anchor="middle">
无波形数据
</text>
</svg>
\`;
}
const svgWidth = 800;
const svgHeight = Math.max(80, signals.length * 30 + 20);
const signalHeight = 20;
const signalSpacing = 30;
const leftMargin = 80;
const rightMargin = 20;
const waveformWidth = svgWidth - leftMargin - rightMargin;
const colors = ['var(--vscode-charts-blue)', 'var(--vscode-charts-green)', 'var(--vscode-charts-orange)'];
let svgContent = \`<svg width="100%" height="\${svgHeight}" viewBox="0 0 \${svgWidth} \${svgHeight}" style="background: var(--vscode-editor-background);">\`;
// 绘制每个信号
signals.forEach((signal, index) => {
const y = 10 + index * signalSpacing;
const color = colors[index % colors.length];
// 绘制信号名称
svgContent += \`<text x="5" y="\${y + signalHeight / 2 + 4}" fill="var(--vscode-foreground)" font-size="10" opacity="0.8">\${signal.name}</text>\`;
// 如果没有值变化数据,显示提示
if (!signal.values || signal.values.length === 0) {
svgContent += \`<text x="\${leftMargin + waveformWidth / 2}" y="\${y + signalHeight / 2 + 4}" fill="var(--vscode-descriptionForeground)" font-size="9" text-anchor="middle" opacity="0.5">无数据</text>\`;
return;
}
// 计算时间范围
const times = signal.values.map(v => v.time);
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const timeRange = maxTime - minTime || 1;
// 绘制波形
let pathData = '';
let lastX = leftMargin;
let lastValue = signal.values[0].value;
signal.values.forEach((point, i) => {
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
const value = point.value;
if (signal.width === 1) {
// 单比特信号 - 绘制数字波形
const yHigh = y;
const yLow = y + signalHeight;
const currentY = (value === '1') ? yHigh : yLow;
if (i === 0) {
pathData = \`M \${x} \${currentY}\`;
} else {
// 绘制垂直跳变
const prevY = (lastValue === '1') ? yHigh : yLow;
if (prevY !== currentY) {
pathData += \` L \${x} \${prevY} L \${x} \${currentY}\`;
} else {
pathData += \` L \${x} \${currentY}\`;
}
}
lastValue = value;
lastX = x;
} else {
// 多比特信号 - 绘制总线波形(梯形)
const yTop = y + 5;
const yBottom = y + signalHeight - 5;
const transitionWidth = 5;
if (i === 0) {
pathData = \`M \${x} \${yTop + (yBottom - yTop) / 2}\`;
} else {
// 绘制梯形过渡
pathData += \` L \${x - transitionWidth} \${yTop} L \${x} \${yTop + (yBottom - yTop) / 2}\`;
}
lastX = x;
}
});
// 延伸到右边界
if (signal.width === 1) {
const lastY = (lastValue === '1') ? y : (y + signalHeight);
pathData += \` L \${leftMargin + waveformWidth} \${lastY}\`;
} else {
const yMid = y + signalHeight / 2;
pathData += \` L \${leftMargin + waveformWidth} \${yMid}\`;
}
svgContent += \`<path d="\${pathData}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
});
// 绘制时间轴
const timeAxisY = svgHeight - 5;
svgContent += \`<line x1="\${leftMargin}" y1="\${timeAxisY}" x2="\${leftMargin + waveformWidth}" y2="\${timeAxisY}" stroke="var(--vscode-foreground)" stroke-width="1" opacity="0.2"/>\`;
svgContent += \`</svg>\`;
return svgContent;
}
/**
* 打开完整波形查看器
*/
function openFullWaveform(vcdFilePath) {
vscode.postMessage({
command: 'openWaveformViewer',
vcdFilePath: vcdFilePath
});
}
/**
* 在消息中添加波形预览
*/
function addWaveformPreviewToMessage(messageDiv, vcdFilePath, fileName) {
const preview = createWaveformPreview(vcdFilePath, fileName);
messageDiv.appendChild(preview);
}
`;
}

File diff suppressed because it is too large Load Diff