610 lines
17 KiB
TypeScript
610 lines
17 KiB
TypeScript
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";
|
|
/**
|
|
* 获取 WebView 面板的 HTML 内容
|
|
*/
|
|
export function getWebviewContent(iconUri?: string): string {
|
|
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()}
|
|
${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 22 px;
|
|
}
|
|
.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()}
|
|
<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>
|
|
|
|
<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()}
|
|
</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; // 当前分段消息容器
|
|
|
|
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 '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 = '';
|
|
}
|
|
break;
|
|
|
|
case 'addUserMessage':
|
|
// 添加用户消息
|
|
if (message.text) {
|
|
addMessage(message.text, 'user');
|
|
}
|
|
break;
|
|
|
|
case 'addAiMessage':
|
|
// 添加AI消息
|
|
if (message.text) {
|
|
addMessage(message.text, 'bot');
|
|
}
|
|
break;
|
|
|
|
default:
|
|
console.log('[WebView] 未处理的消息类型:', message.command);
|
|
}
|
|
});
|
|
|
|
${getMessageAreaScript()}
|
|
${getAgentCardScript()}
|
|
${getWaveformPreviewScript()}
|
|
${getConversationHistoryBarScript()}
|
|
${getInputAreaScript()}
|
|
</script></body>
|
|
</html>`;
|
|
}
|