Files
IC-Coder-Plugin/src/views/webviewContent.ts
2026-01-05 16:19:53 +08:00

731 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import {
getWaveformPreviewContent,
getWaveformPreviewScript,
} from "./waveformPreviewContent";
import {
getConversationHistoryBarContent,
getConversationHistoryBarStyles,
getConversationHistoryBarScript,
} from "./conversationHistoryBar";
import {
getInputAreaContent,
getInputAreaStyles,
getInputAreaScript,
} from "./inputArea";
import {
getMessageAreaContent,
getMessageAreaStyles,
getMessageAreaScript,
} from "./messageArea";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
import {
getProgressBarContent,
getProgressBarStyles,
getProgressBarScript,
} from "./progressBar";
import { getCurrentEnv } from "../config/settings";
/**
* 获取 WebView 面板的 HTML 内容
*/
export function getWebviewContent(
iconUri?: string,
autoIconUri?: string,
liteIconUri?: string,
syIconUri?: string,
maxIconUri?: string
): string {
// 获取当前环境,只在 dev 和 test 环境下显示快速操作按钮
const currentEnv = getCurrentEnv();
const showQuickActions = currentEnv === "dev" || currentEnv === "test";
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IC Coder</title>
<style>
body {
font-family: var(--vscode-font-family);
background: var(--vscode-editor-background);
color: var(--vscode-foreground);
margin: 0;
padding: 0;
height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.header {
text-align: center;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
}
.header.hidden {
display: none;
}
.header h1 {
color: var(--vscode-button-background);
margin: 0 0 8px 0;
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
padding: 0 20px 20px 20px;
}
${getMessageAreaStyles()}
${getAgentCardStyles()}
${getWaveformPreviewContent()}
${getConversationHistoryBarStyles()}
${getProgressBarStyles()}
${getInputAreaStyles()}
.file-editor-section {
margin-bottom: 15px;
padding: 15px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 8px;
display: none;
flex-shrink: 0;
}
.file-editor-section.active {
display: block;
}
.file-editor-section h3 {
margin: 0 0 10px 0;
color: var(--vscode-button-background);
}
.file-editor-textarea {
width: 100%;
min-height: 300px;
padding: 10px;
background: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
resize: vertical;
}
.editor-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
/* 流式消息样式 */
.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;
}
.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);
}
.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;
}
/* 状态栏样式 */
.status-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--vscode-textBlockQuote-background);
border-radius: 6px;
margin: 8px 0;
font-size: 13px;
color: var(--vscode-descriptionForeground);
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--vscode-charts-blue);
animation: statusPulse 1.5s ease-in-out infinite;
}
@keyframes statusPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); }
}
.status-bar.working .status-indicator {
background: var(--vscode-charts-orange);
}
.status-bar.success .status-indicator {
background: var(--vscode-charts-green);
animation: none;
}
.status-bar.error .status-indicator {
background: var(--vscode-charts-red);
animation: none;
}
/* 快捷操作按钮样式 */
.quick-actions {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.quick-btn {
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;
font-size: 13px;
transition: all 0.2s;
}
.quick-btn:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
</style>
</head>
<body>
${getConversationHistoryBarContent()}
${getProgressBarContent()}
<div class="header">
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
<h1 style="margin: 0;">IC Coder</h1>
</div>
<p>专注于真实FPGA研发的Verilog智能体编程平台</p>
</div>
<div class="chat-container">
${getMessageAreaContent()}
<!-- 状态栏 -->
<div id="statusBar" class="status-bar" style="display: none;">
<div class="status-indicator"></div>
<span id="statusText">思考中...</span>
</div>
${
showQuickActions
? `<div class="quick-actions">
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
<button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
</div>`
: ""
}
${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
</div>
<script>
console.log('[WebView] 脚本开始执行');
const vscode = acquireVsCodeApi();
console.log('[WebView] vscode API 已获取');
const messageInput = document.getElementById('messageInput');
const modeSelect = document.getElementById('modeSelect');
const messagesEl = document.getElementById('messages');
// 全局变量
let currentStreamingMessage = null;
let loadingIndicator = null;
let currentSegmentedMessage = null; // 当前分段消息容器
// ========== 模式选择器脚本(直接内联,避免模板字符串嵌套问题)==========
let currentMode = 'agent';
function toggleModeDropdown() {
const modeSelectEl = document.getElementById('modeSelect');
const modelSelectEl = document.getElementById('modelSelect');
if (modeSelectEl) {
modeSelectEl.classList.toggle('active');
if (modelSelectEl) {
modelSelectEl.classList.remove('active');
}
}
}
function selectMode(value, label) {
currentMode = value;
const modeValue = document.getElementById('modeValue');
const modeTooltip = document.getElementById('modeTooltip');
if (modeValue) {
modeValue.textContent = label;
}
if (modeTooltip) {
const tooltipMap = {
'plan': 'plan模式',
'ask': 'ask模式',
'agent': 'agent模式'
};
modeTooltip.textContent = tooltipMap[value] || '切换模式';
}
const options = document.querySelectorAll('.mode-option');
options.forEach(option => {
if (option.getAttribute('data-value') === value) {
option.classList.add('selected');
} else {
option.classList.remove('selected');
}
});
const modeSelectEl = document.getElementById('modeSelect');
if (modeSelectEl) {
modeSelectEl.classList.remove('active');
}
}
function getCurrentMode() {
return currentMode;
}
document.addEventListener('click', (event) => {
const modeSelectEl = document.getElementById('modeSelect');
if (modeSelectEl && !modeSelectEl.contains(event.target)) {
modeSelectEl.classList.remove('active');
}
});
// ========== 模式选择器脚本结束 ==========
function quickAction(type) {
const questions = {
counter: '生成一个4位同步计数器',
fsm: '生成一个状态机',
testbench: '生成测试平台',
explore: '请启动知识探索智能体,分析当前项目结构'
};
if (questions[type]) {
messageInput.value = questions[type];
sendMessage();
}
}
// 检查 header 显示状态
function checkHeaderVisibility() {
const header = document.querySelector('.header');
const messages = document.getElementById('messages');
if (header && messages) {
if (messages.children.length > 0) {
header.classList.add('hidden');
} else {
header.classList.remove('hidden');
}
}
}
// 监听来自插件的消息
window.addEventListener('message', event => {
const message = event.data;
console.log('[WebView] 收到消息:', message.command, message);
switch (message.command) {
case 'receiveMessage':
// 接收完整消息
addMessage(message.text, 'bot');
break;
case 'updateStreamingMessage':
// 更新流式消息
updateOrCreateStreamingMessage(message.text);
break;
case 'updateSegments':
// 实时更新分段消息(按后端返回顺序)
console.log('[WebView] 实时更新段落, segments:', message.segments);
updateSegmentsRealtime(message.segments, message.isComplete);
// 如果对话完成,恢复发送按钮状态
if (message.isComplete && typeof setSendButtonState === 'function') {
setSendButtonState(false);
}
break;
case 'receiveSegments':
// 接收分段消息(兼容旧代码)
console.log('[WebView] 调用 renderSegments, segments:', message.segments);
renderSegments(message.segments);
break;
case 'toolStart':
// 工具开始执行
addToolStatus(message.toolName, 'start');
break;
case 'toolComplete':
// 工具执行完成
addToolStatus(message.toolName, 'complete', message.result);
break;
case 'toolError':
// 工具执行错误
addToolStatus(message.toolName, 'error', message.error);
break;
case 'updateStatus':
// 更新状态栏
const statusBar = document.getElementById('statusBar');
const statusText = document.getElementById('statusText');
if (statusBar && statusText) {
statusBar.style.display = 'flex';
statusText.textContent = message.text;
statusBar.className = 'status-bar ' + (message.type || '');
}
break;
case 'hideStatus':
// 隐藏状态栏
const statusBarHide = document.getElementById('statusBar');
if (statusBarHide) {
statusBarHide.style.display = 'none';
}
break;
case 'resetSegmentedMessage':
// 重置分段消息容器(停止对话时调用)
console.log('[WebView] 重置分段消息容器');
currentSegmentedMessage = null;
break;
case 'contextUsage':
// 更新上下文使用量显示
if (typeof updateContextDisplay === 'function') {
updateContextDisplay(message.currentTokens, message.maxTokens);
}
break;
case 'hideLoading':
// 隐藏加载指示器
hideLoadingIndicator();
break;
case 'workspaceStatus':
// 更新工作区状态
if (typeof hasWorkspace !== 'undefined') {
hasWorkspace = message.hasWorkspace;
console.log('[WebView] 工作区状态:', hasWorkspace);
}
break;
case 'vcdInfo':
// 渲染迷你波形预览信息
try {
if (message.containerId && typeof renderWaveformInfo === 'function') {
renderWaveformInfo(message.containerId, message.vcdInfo || {});
}
} catch (e) {
console.warn('[WebView] 渲染波形信息失败:', e);
}
break;
case 'vcdGenerated':
// VCD 文件生成成功,添加消息并附带波形预览
addMessage(message.text, 'bot');
try {
if (message.vcdFilePath) {
const lastMsg = messagesEl ? messagesEl.lastElementChild : null;
if (lastMsg && typeof addWaveformPreviewToMessage === 'function') {
addWaveformPreviewToMessage(lastMsg, message.vcdFilePath, message.fileName || 'waveform.vcd');
}
}
} catch (e) {
console.warn('[WebView] 添加波形预览失败:', e);
}
break;
case 'fileContent':
// 文件内容
addMessage('文件内容:\\n' + message.content, 'bot');
break;
case 'fileError':
// 文件错误
addMessage('❌ ' + message.error, 'bot');
break;
case 'showQuestion':
// 显示用户问题
showQuestion(message.askId, message.question, message.options);
break;
case 'conversationHistory':
// 渲染会话历史列表(支持分页)
renderConversationHistory({
items: message.items || [],
total: message.total || 0,
hasMore: message.hasMore || false
});
break;
case 'clearChat':
// 清空聊天界面
const messagesContainer = document.getElementById('messages');
if (messagesContainer) {
messagesContainer.innerHTML = '';
}
// 重置输入框布局到居中
if (typeof window.resetInputAreaLayout === 'function') {
window.resetInputAreaLayout();
}
break;
case 'addUserMessage':
// 添加用户消息
if (message.text) {
addMessage(message.text, 'user');
}
// 检查并更新输入框布局
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
window.checkMessagesAndUpdateLayout();
}
break;
case 'addAiMessage':
// 添加AI消息
if (message.text) {
addMessage(message.text, 'bot');
}
// 检查并更新输入框布局
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
window.checkMessagesAndUpdateLayout();
}
break;
case 'switchMode':
// 切换运行模式Plan 确认后自动切换到 Agent
if (message.mode && typeof selectMode === 'function') {
const labelMap = {
'plan': 'Plan',
'ask': 'Ask',
'agent': 'Agent',
'auto': 'Auto'
};
selectMode(message.mode, labelMap[message.mode] || message.mode);
console.log('[WebView] 模式已切换到:', message.mode);
}
break;
case 'addMessage':
// 添加消息(通用)
if (message.text && message.sender) {
addMessage(message.text, message.sender);
}
break;
default:
console.log('[WebView] 未处理的消息类型:', message.command);
}
});
${getMessageAreaScript()}
${getAgentCardScript()}
${getWaveformPreviewScript()}
${getConversationHistoryBarScript()}
${getProgressBarScript()}
${getInputAreaScript()}
</script></body>
</html>`;
}