Files
IC-Coder-Plugin/src/views/webviewContent.ts
Roe-xin 3f0cc8ae29 feat: 添加工作区状态检查功能,优化用户体验
- 用户鼠标聚焦到输入框中就弹窗提示用户打开 优化用户体验
2025-12-30 16:02:36 +08:00

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>`;
}