Merge branch 'feat/codeToChat' into feat/personalRules
This commit is contained in:
@ -4,7 +4,14 @@
|
||||
export function getExampleShowcaseContent(): string {
|
||||
return `
|
||||
<div class="example-showcase" id="exampleShowcase">
|
||||
<div class="showcase-title">示例</div>
|
||||
<div class="showcase-header">
|
||||
<div class="showcase-title">示例</div>
|
||||
<button class="refresh-button" onclick="refreshExamples()" title="换一批">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5 2V8M21.5 8H15.5M21.5 8L18 4.5C16.7429 3.24286 15.1767 2.35596 13.4606 1.93597C11.7446 1.51598 9.94736 1.57986 8.26381 2.12059C6.58027 2.66131 5.07831 3.65985 3.91872 4.99987C2.75913 6.33989 1.98648 7.96902 1.68 9.71M2.5 22V16M2.5 16H8.5M2.5 16L6 19.5C7.25714 20.7571 8.82331 21.644 10.5394 22.064C12.2554 22.484 14.0526 22.4201 15.7362 21.8794C17.4197 21.3387 18.9217 20.3401 20.0813 19.0001C21.2409 17.6601 22.0135 16.031 22.32 14.29" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="example-cards">
|
||||
<div class="example-card" onclick="sendExample(0)">
|
||||
<div class="example-icon">
|
||||
@ -62,12 +69,44 @@ export function getExampleShowcaseStyles(): string {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.showcase-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.showcase-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
margin-bottom: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.refresh-button:hover {
|
||||
opacity: 1;
|
||||
background: var(--vscode-input-background);
|
||||
}
|
||||
|
||||
.refresh-button svg {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-button:active svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.example-cards {
|
||||
@ -220,15 +259,74 @@ export function getExampleShowcaseStyles(): string {
|
||||
*/
|
||||
export function getExampleShowcaseScript(): string {
|
||||
return `
|
||||
// 示例文本数组
|
||||
const exampleTexts = [
|
||||
'生成一个SPI控制器',
|
||||
'生成一个GMII接口的以太网UDP通信模块'
|
||||
// 所有可用的示例
|
||||
const allExamples = [
|
||||
'设计一个算术逻辑单元,完成常见运算',
|
||||
'实现一个优先编码器,多个输入同时有效时,只输出优先级最高的那个编号',
|
||||
'实现一个译码器,把二进制编号转换成 one-hot 输出',
|
||||
'实现一个移位寄存器,完成串行/并行数据移位与装载',
|
||||
'实现一个按键消抖模块,解决机械按键抖动问题',
|
||||
'实现一个跑马灯控制器,控制 LED 形成不同流动效果',
|
||||
'实现一个序列检测器,检测串行输入中是否出现指定比特序列',
|
||||
'实现一个LFSR 伪随机数发生器',
|
||||
'实现一个自动售货机,模拟一个简单售货逻辑',
|
||||
'实现一个交通灯控制器,控制两方向交通灯的切换',
|
||||
'实现一个先进先出的数据缓冲区',
|
||||
'单端口 RAM 读写控制器',
|
||||
'实现一个移位加法乘法器,不用 * 运算符'
|
||||
];
|
||||
|
||||
// 当前显示的示例文本
|
||||
let exampleTexts = ['生成一个SPI控制器', '生成一个GMII接口的以太网UDP通信模块'];
|
||||
|
||||
// 存储待发送的示例索引
|
||||
let pendingExampleIndex = -1;
|
||||
|
||||
// 节流控制
|
||||
let refreshing = false;
|
||||
|
||||
// 刷新示例
|
||||
function refreshExamples() {
|
||||
if (refreshing) return;
|
||||
refreshing = true;
|
||||
|
||||
const used = new Set();
|
||||
const newExamples = [];
|
||||
while (newExamples.length < 2) {
|
||||
const idx = Math.floor(Math.random() * allExamples.length);
|
||||
if (!used.has(idx)) {
|
||||
used.add(idx);
|
||||
newExamples.push(allExamples[idx]);
|
||||
}
|
||||
}
|
||||
exampleTexts = newExamples;
|
||||
updateExampleCards();
|
||||
|
||||
setTimeout(() => { refreshing = false; }, 500);
|
||||
}
|
||||
|
||||
// 更新示例卡片显示
|
||||
function updateExampleCards() {
|
||||
const container = document.querySelector('.example-cards');
|
||||
if (!container) return;
|
||||
container.innerHTML = exampleTexts.map((text, i) => \`
|
||||
<div class="example-card" onclick="sendExample(\${i})">
|
||||
<div class="example-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 9H9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="example-content">
|
||||
<div class="example-title">\${text}</div>
|
||||
</div>
|
||||
</div>
|
||||
\`).join('');
|
||||
}
|
||||
|
||||
// 直接发送示例消息
|
||||
function sendExample(index) {
|
||||
// 先检查邀请码验证状态
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
220
src/views/messageRenderer.ts
Normal file
220
src/views/messageRenderer.ts
Normal file
@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 消息渲染脚本模块
|
||||
* 功能:消息渲染、滚动控制、工具状态显示
|
||||
* 依赖:toolHelpers, textFormatter, waveformPreviewContent, agentCard, planCard, codeHighlight
|
||||
* 使用场景:webview 中的消息显示逻辑
|
||||
*/
|
||||
|
||||
import { collapseIconSvg } from "../constants/toolIcons";
|
||||
import { getWaveformPreviewScript } from "./waveformPreviewContent";
|
||||
import { getAgentCardScript } from "./agentCard";
|
||||
import { getPlanCardScript } from "./planCard";
|
||||
import { getCodeHighlightScript } from "../components/codeHighlight";
|
||||
|
||||
export function getMessageRendererScript(): string {
|
||||
return `
|
||||
${getAgentCardScript()}
|
||||
${getPlanCardScript()}
|
||||
|
||||
const toolCollapseStates = new Map();
|
||||
let shouldAutoScroll = true;
|
||||
let lastScrollHeight = 0;
|
||||
|
||||
function isUserNearBottom() {
|
||||
const threshold = 50;
|
||||
return messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < threshold;
|
||||
}
|
||||
|
||||
messagesEl.addEventListener('scroll', () => {
|
||||
const isAtBottom = isUserNearBottom();
|
||||
if (isAtBottom) {
|
||||
shouldAutoScroll = true;
|
||||
} else {
|
||||
if (messagesEl.scrollHeight === lastScrollHeight) {
|
||||
shouldAutoScroll = false;
|
||||
}
|
||||
}
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
});
|
||||
|
||||
function smartScrollToBottom() {
|
||||
if (shouldAutoScroll) {
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
lastScrollHeight = messagesEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(text, sender) {
|
||||
const div = document.createElement('div');
|
||||
div.className = \`message \${sender}-message\`;
|
||||
if (sender === 'bot') {
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const messageContent = document.createElement('span');
|
||||
messageContent.textContent = text;
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => copyMessage(text, copyBtn);
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
actionsDiv.appendChild(messageContent);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
div.appendChild(actionsDiv);
|
||||
} else {
|
||||
const parts = text.split(' ');
|
||||
const filePaths = [];
|
||||
const textParts = [];
|
||||
parts.forEach(part => {
|
||||
if (part.includes('/') || part.includes('\\\\') || /\\.[a-zA-Z0-9]+$/.test(part) || /:[0-9]+-[0-9]+$/.test(part)) {
|
||||
filePaths.push(part);
|
||||
} else {
|
||||
textParts.push(part);
|
||||
}
|
||||
});
|
||||
if (filePaths.length > 0) {
|
||||
div.innerHTML = filePaths.map(fp => window.createFilePathTag ? window.createFilePathTag(fp) : fp).join('') + ' ' + textParts.join(' ');
|
||||
} else {
|
||||
div.textContent = text;
|
||||
}
|
||||
hideHeaderIfNeeded();
|
||||
}
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
function hideHeaderIfNeeded() {
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
function copyMessage(text, button) {
|
||||
// 从按钮的父消息元素中获取实际文本内容
|
||||
const messageDiv = button.closest('.message');
|
||||
const messageContent = messageDiv?.querySelector('.message-content, div:not(.message-actions)');
|
||||
const textToCopy = messageContent ? messageContent.textContent : text;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474c-6.1-7.7-15.3-12.2-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1 0.4-12.8-6.3-12.8z" fill="currentColor"/></svg>\`;
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleLike(button) {
|
||||
const isActive = button.classList.contains('active');
|
||||
const parent = button.parentElement;
|
||||
parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
|
||||
if (!isActive) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDislike(button) {
|
||||
const isActive = button.classList.contains('active');
|
||||
const parent = button.parentElement;
|
||||
parent.querySelectorAll('.action-btn').forEach(btn => btn.classList.remove('active'));
|
||||
if (!isActive) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function updateOrCreateStreamingMessage(text) {
|
||||
hideLoadingIndicator();
|
||||
if (!currentStreamingMessage) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message bot-message streaming';
|
||||
const messageContent = document.createElement('div');
|
||||
messageContent.className = 'message-content';
|
||||
messageContent.textContent = text;
|
||||
div.appendChild(messageContent);
|
||||
messagesEl.appendChild(div);
|
||||
currentStreamingMessage = div;
|
||||
} else {
|
||||
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = text;
|
||||
}
|
||||
}
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function finalizeStreamingMessage(finalText) {
|
||||
if (currentStreamingMessage) {
|
||||
const messageContent = currentStreamingMessage.querySelector('.message-content');
|
||||
if (messageContent) {
|
||||
messageContent.textContent = finalText;
|
||||
}
|
||||
currentStreamingMessage.classList.remove('streaming');
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
copyBtn.onclick = () => copyMessage(finalText, copyBtn);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
currentStreamingMessage.appendChild(actionsDiv);
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function showLoadingIndicator(text) {
|
||||
hideLoadingIndicator();
|
||||
loadingIndicator = document.createElement('div');
|
||||
loadingIndicator.className = 'message bot-message loading-message';
|
||||
loadingIndicator.innerHTML = \`
|
||||
<div class="loading-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
<span class="loading-text">\${text}</span>
|
||||
\`;
|
||||
messagesEl.appendChild(loadingIndicator);
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function hideLoadingIndicator() {
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.remove();
|
||||
loadingIndicator = null;
|
||||
}
|
||||
}
|
||||
|
||||
function addToolStatus(toolName, status, detail) {
|
||||
const statusIcons = {
|
||||
start: '🔧',
|
||||
complete: '✅',
|
||||
error: '❌'
|
||||
};
|
||||
const statusTexts = {
|
||||
start: '正在执行',
|
||||
complete: '执行完成',
|
||||
error: '执行失败'
|
||||
};
|
||||
const div = document.createElement('div');
|
||||
div.className = \`message tool-status tool-\${status}\`;
|
||||
div.innerHTML = \`
|
||||
<span class="tool-icon">\${statusIcons[status]}</span>
|
||||
<span class="tool-name">\${getToolDisplayName(toolName)}</span>
|
||||
<span class="tool-status-text">\${statusTexts[status]}</span>
|
||||
\${detail ? \`<div class="tool-detail">\${detail}</div>\` : ''}
|
||||
\`;
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
|
||||
${getWaveformPreviewScript()}
|
||||
${getCodeHighlightScript()}
|
||||
`;
|
||||
}
|
||||
632
src/views/messageStyles.ts
Normal file
632
src/views/messageStyles.ts
Normal file
@ -0,0 +1,632 @@
|
||||
/**
|
||||
* 消息样式模块
|
||||
* 功能:提供消息区域的所有 CSS 样式
|
||||
* 依赖:agentCard, planCard, codeHighlight, waveformPreviewContent
|
||||
* 使用场景:webview 样式注入
|
||||
*/
|
||||
|
||||
import { getAgentCardStyles } from "./agentCard";
|
||||
import { getPlanCardStyles } from "./planCard";
|
||||
import { getCodeHighlightStyles } from "../components/codeHighlight";
|
||||
import { getWaveformPreviewContent } from "./waveformPreviewContent";
|
||||
|
||||
export function getMessageAreaStyles(): string {
|
||||
return `
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 15px;
|
||||
min-height: 0;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.user-message {
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
margin-left: auto;
|
||||
width: fit-content;
|
||||
max-width: 80%;
|
||||
}
|
||||
.bot-message {
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
color: var(--vscode-foreground);
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.message-actions {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
margin-left: 12px;
|
||||
opacity: 0.85;
|
||||
transition: opacity 0.2s ease;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
}
|
||||
.message-actions > span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.message-actions:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-foreground);
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
position: relative;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.action-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.action-btn.active {
|
||||
color: var(--vscode-button-background);
|
||||
opacity: 1;
|
||||
}
|
||||
.action-btn .action-tooltip {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background: #1e1e1e;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 4px 8px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(5px);
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;white-space: nowrap;pointer-events: none;
|
||||
}
|
||||
.action-btn .action-tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: #1e1e1e transparent transparent transparent;
|
||||
}
|
||||
.action-btn .action-tooltip::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent;
|
||||
z-index: -1;
|
||||
}
|
||||
.action-btn:hover .action-tooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.streaming .message-content {
|
||||
border-right: 2px solid var(--vscode-focusBorder);
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
0%, 50% { border-color: var(--vscode-focusBorder); }
|
||||
51%, 100% { border-color: transparent; }
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 4px;}
|
||||
.loading-dots span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-focusBorder);
|
||||
animation: loadingDot 1.4s infinite ease-in-out;
|
||||
}
|
||||
.loading-dots span:nth-child(1) { animation-delay: 0s; }
|
||||
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes loadingDot {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
.loading-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tool-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
}
|
||||
.tool-status.tool-start {
|
||||
border-left: 3px solid var(--vscode-charts-blue);
|
||||
}
|
||||
.tool-status.tool-complete {
|
||||
border-left: 3px solid var(--vscode-charts-green);
|
||||
}
|
||||
.tool-status.tool-error {
|
||||
border-left: 3px solid var(--vscode-charts-red);
|
||||
}
|
||||
.tool-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
.tool-name {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.tool-status-text {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.tool-detail {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
white-space: pre-wrap;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.question-message {
|
||||
padding: 16px;
|
||||
}
|
||||
.question-text {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.question-option {
|
||||
padding: 8px 16px;
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border: 1px solid #007ACC;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.question-option:hover {
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
}
|
||||
.question-option.selected {
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
.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 0;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.segment-text h1,
|
||||
.segment-text h2,
|
||||
.segment-text h3,
|
||||
.question-text h1,
|
||||
.question-text h2,
|
||||
.question-text h3 {
|
||||
margin: 0px 0 -10px 0;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.segment-text h1,
|
||||
.question-text h1 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.segment-text h2,
|
||||
.question-text h2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.segment-text h3,
|
||||
.question-text h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.segment-text ul,
|
||||
.segment-text ol,
|
||||
.question-text ul,
|
||||
.question-text ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.segment-text li,
|
||||
.question-text li {
|
||||
line-height: 1;
|
||||
}
|
||||
.segment-text strong,
|
||||
.question-text strong {
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.segment-text em,
|
||||
.question-text em {
|
||||
font-style: italic;
|
||||
}
|
||||
.segment-text a,
|
||||
.question-text a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
.segment-text a:hover,
|
||||
.question-text a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.segment-text p,
|
||||
.question-text p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.segment-text code,
|
||||
.question-text code {
|
||||
background: var(--vscode-textCodeBlock-background);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.segment-tool {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.segment-tool.low-profile {
|
||||
margin: 25px 0px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
.tool-segment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-segment-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
.tool-segment-name {
|
||||
font-weight: normal;
|
||||
}
|
||||
.tool-segment-result {
|
||||
display: inline;
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-left: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 500px;
|
||||
}
|
||||
.tool-collapse-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool-collapse-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.icon-expanded svg path {
|
||||
fill: #007ACC !important;
|
||||
}
|
||||
.tool-segment-header.collapsed .tool-collapse-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.tool-segment-header:not(.collapsed) .tool-collapse-icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
.tool-file-write-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-write-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-read-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-read-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-file-delete-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-file-delete-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-syntax-check-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-syntax-check-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-search-code-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-search-code-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-save-knowledge-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-save-knowledge-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-simulation-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-simulation-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-waveform-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-waveform-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-knowledge-load-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-knowledge-load-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-state-transition-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.tool-state-transition-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tool-segment-content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
.tool-segment-content.collapsed {
|
||||
max-height: 0;
|
||||
}
|
||||
.tool-segment-description {
|
||||
margin: 25px 0 0 0px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-header {
|
||||
opacity: 0.65;
|
||||
font-size: 12px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-icon {
|
||||
opacity: 0.55;
|
||||
font-size: 11px;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-name {
|
||||
font-weight: 300;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.segment-tool.low-profile .tool-segment-result {
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-question {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-radius: 6px;
|
||||
margin: 8px 0;
|
||||
padding: 12px 35px;
|
||||
border-left: 3px solid var(--vscode-charts-orange);
|
||||
}
|
||||
.segment-question .question-text {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.segment-question .question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.segment-question .question-option {
|
||||
padding: 8px 16px;
|
||||
background: #3d3f41;
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid #474747;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-question .question-option:hover {
|
||||
background: #005a9e;
|
||||
border-color: #005a9e;
|
||||
}
|
||||
.segment-question .question-option.selected {
|
||||
background: #007ACC;
|
||||
color: #ffffff;
|
||||
border-color: #007ACC;
|
||||
}
|
||||
.segment-question.answered .question-option:not(.selected) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.segment-question .custom-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.segment-question .custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.segment-question .custom-submit {
|
||||
padding: 8px 16px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.segment-question .custom-submit:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.segment-question.answered .custom-input-container {
|
||||
display: none;
|
||||
}
|
||||
.question-segment .question-text {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.question-segment .question-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.question-opt {
|
||||
padding: 4px 10px;
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;}
|
||||
|
||||
${getAgentCardStyles()}
|
||||
|
||||
${getPlanCardStyles()}
|
||||
|
||||
${getCodeHighlightStyles()}
|
||||
|
||||
${getWaveformPreviewContent()}
|
||||
`;
|
||||
}
|
||||
@ -59,7 +59,7 @@ export function getPlanCardStyles(): string {
|
||||
.plan-summary h2 { font-size: 16px; }
|
||||
.plan-summary h3 { font-size: 14px; }
|
||||
.plan-summary h4 { font-size: 13px; }
|
||||
.plan-summary p { margin: 8px 0; }
|
||||
.plan-summary p { margin: 8px 0; letter-spacing: 0.5px; }
|
||||
.plan-summary ul, .plan-summary ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 0;
|
||||
|
||||
@ -25,7 +25,7 @@ export function getProgressBarContent(): string {
|
||||
<span class="step-number">1</span>
|
||||
<span class="step-check">✓</span>
|
||||
</div>
|
||||
<div class="step-label">Spec</div>
|
||||
<div class="step-label">Specification</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-line"></div>
|
||||
|
||||
118
src/views/questionHandler.ts
Normal file
118
src/views/questionHandler.ts
Normal file
@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 问题处理脚本模块
|
||||
* 功能:用户问题交互逻辑
|
||||
* 依赖:textFormatter
|
||||
* 使用场景:webview 中的问题回答处理
|
||||
*/
|
||||
|
||||
export function getQuestionHandlerScript(): string {
|
||||
return `
|
||||
const answeredQuestions = new Map();
|
||||
|
||||
function handleQuestionAnswer(askId, answer, questionDiv) {
|
||||
console.log('[WebView] 用户选择答案:', askId, answer);
|
||||
questionDiv.classList.add('answered');
|
||||
const options = questionDiv.querySelectorAll('.question-option');
|
||||
options.forEach(opt => {
|
||||
if (opt.textContent === answer) {
|
||||
opt.classList.add('selected');
|
||||
}
|
||||
});
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
selected: [answer],
|
||||
customInput: answer
|
||||
});
|
||||
}
|
||||
|
||||
function handleQuestionAnswerInSegment(askId, answer, segmentDiv) {
|
||||
console.log('[WebView] 段落中用户选择答案:', askId, answer);
|
||||
answeredQuestions.set(askId, answer);
|
||||
segmentDiv.classList.add('answered');
|
||||
const options = segmentDiv.querySelectorAll('.question-option');
|
||||
options.forEach(opt => {
|
||||
if (opt.getAttribute('data-option') === answer) {
|
||||
opt.classList.add('selected');
|
||||
}
|
||||
});
|
||||
const customContainer = segmentDiv.querySelector('.custom-input-container');
|
||||
if (customContainer) {
|
||||
customContainer.style.display = 'none';
|
||||
}
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
selected: [answer],
|
||||
customInput: answer
|
||||
});
|
||||
}
|
||||
|
||||
function handleMultiQuestionAnswer(askId, answers, segmentDiv) {
|
||||
console.log('[WebView] 多问题答案提交:', askId, answers);
|
||||
answeredQuestions.set(askId, answers);
|
||||
segmentDiv.classList.add('answered');
|
||||
const inputs = segmentDiv.querySelectorAll('input');
|
||||
inputs.forEach(input => {
|
||||
input.disabled = true;
|
||||
if (input.checked) {
|
||||
const label = input.closest('.question-option');
|
||||
if (label) {
|
||||
label.classList.add('selected');
|
||||
}
|
||||
}
|
||||
});
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
if (submitBtn) {
|
||||
submitBtn.style.display = 'none';
|
||||
}
|
||||
vscode.postMessage({
|
||||
command: 'submitAnswer',
|
||||
askId: askId,
|
||||
answers: answers
|
||||
});
|
||||
}
|
||||
|
||||
function showQuestion(askId, question, options) {
|
||||
console.log('[WebView] showQuestion 被调用:', askId, question, options);
|
||||
const div = document.createElement('div');
|
||||
div.className = 'message bot-message question-message';
|
||||
div.setAttribute('data-ask-id', askId);
|
||||
const questionText = document.createElement('div');
|
||||
questionText.className = 'question-text';
|
||||
questionText.textContent = question;
|
||||
div.appendChild(questionText);
|
||||
const optionsContainer = document.createElement('div');
|
||||
optionsContainer.className = 'question-options';
|
||||
options.forEach((option, index) => {
|
||||
const optionBtn = document.createElement('button');
|
||||
optionBtn.className = 'question-option';
|
||||
optionBtn.textContent = option;
|
||||
optionBtn.onclick = () => handleQuestionAnswer(askId, option, div);
|
||||
optionsContainer.appendChild(optionBtn);
|
||||
});
|
||||
div.appendChild(optionsContainer);
|
||||
const customContainer = document.createElement('div');
|
||||
customContainer.className = 'custom-input-container';
|
||||
const customInput = document.createElement('input');
|
||||
customInput.type = 'text';
|
||||
customInput.className = 'custom-input';
|
||||
customInput.placeholder = '输入其他答案...';
|
||||
const customSubmit = document.createElement('button');
|
||||
customSubmit.className = 'custom-submit';
|
||||
customSubmit.textContent = '提交';
|
||||
customSubmit.onclick = () => {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswer(askId, customValue, div);
|
||||
}
|
||||
};
|
||||
customContainer.appendChild(customInput);
|
||||
customContainer.appendChild(customSubmit);
|
||||
div.appendChild(customContainer);
|
||||
messagesEl.appendChild(div);
|
||||
smartScrollToBottom();
|
||||
checkHeaderVisibility();
|
||||
}
|
||||
`;
|
||||
}
|
||||
272
src/views/segmentRenderer.ts
Normal file
272
src/views/segmentRenderer.ts
Normal file
@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 分段消息渲染脚本模块
|
||||
* 功能:实时更新分段消息、工具调用展示
|
||||
* 依赖:toolHelpers, textFormatter, waveformPreviewContent
|
||||
* 使用场景:webview 中的分段消息渲染
|
||||
*/
|
||||
|
||||
export function getSegmentRendererScript(): string {
|
||||
return `
|
||||
function updateSegmentsRealtime(segments, isComplete) {
|
||||
if (!isComplete && (!segments || segments.length === 0)) return;
|
||||
|
||||
if (!currentSegmentedMessage) {
|
||||
if (currentStreamingMessage) {
|
||||
currentStreamingMessage.remove();
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
|
||||
toolStatuses.forEach(el => el.remove());
|
||||
const lastSegmented = messagesEl.querySelector('.segmented-message:last-child');
|
||||
if (lastSegmented && !lastSegmented.querySelector('.message-actions')) {
|
||||
currentSegmentedMessage = lastSegmented;
|
||||
} else {
|
||||
currentSegmentedMessage = document.createElement('div');
|
||||
currentSegmentedMessage.className = 'message bot-message segmented-message';
|
||||
messagesEl.appendChild(currentSegmentedMessage);
|
||||
}
|
||||
renderedSegmentCount = 0;
|
||||
}
|
||||
|
||||
if (currentSegmentedMessage) {
|
||||
const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
|
||||
toolHeaders.forEach((header, idx) => {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
toolCollapseStates.set(idx, isCollapsed);
|
||||
});
|
||||
}
|
||||
|
||||
if (!isComplete) {
|
||||
currentSegmentedMessage.innerHTML = '';
|
||||
}
|
||||
|
||||
const mergedSegments = [];
|
||||
let i = 0;
|
||||
while (i < (segments?.length || 0)) {
|
||||
const segment = segments[i];
|
||||
if (segment.type === 'tool') {
|
||||
let count = 1;
|
||||
while (i + count < segments.length &&
|
||||
segments[i + count].type === 'tool' &&
|
||||
segments[i + count].toolName === segment.toolName) {
|
||||
count++;
|
||||
}
|
||||
mergedSegments.push({ ...segment, toolCount: count });
|
||||
i += count;
|
||||
} else {
|
||||
mergedSegments.push(segment);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
let toolIndex = 0;
|
||||
mergedSegments.forEach((segment, index) => {
|
||||
const segmentDiv = document.createElement('div');
|
||||
segmentDiv.className = 'message-segment segment-' + segment.type;
|
||||
|
||||
if (segment.type === 'text' && segment.content) {
|
||||
segmentDiv.className += ' segment-text';
|
||||
segmentDiv.innerHTML = formatText(segment.content);
|
||||
} else if (segment.type === 'tool') {
|
||||
if (segment.toolName === 'spawnExplorer') return;
|
||||
segmentDiv.className += ' low-profile';
|
||||
const toolResult = segment.toolResult || '';
|
||||
const toolCount = segment.toolCount || 1;
|
||||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||||
const toolDescription = segment.toolDescription || '';
|
||||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||||
const savedState = toolCollapseStates.get(toolIndex);
|
||||
const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
|
||||
const currentToolIndex = toolIndex;
|
||||
toolIndex++;
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
|
||||
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
|
||||
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
|
||||
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
|
||||
</div>
|
||||
\${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||||
\${toolDescription ? \`<p class="tool-segment-description">\${toolDescription}</p>\` : ''}
|
||||
\`;
|
||||
|
||||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||||
if (typeof createWaveformPreview === 'function') {
|
||||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||||
if (vcdPaths.length > 0) {
|
||||
vcdPaths.forEach(vcdInfo => {
|
||||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
});
|
||||
} else {
|
||||
let vcdPath = segment.vcdFilePath;
|
||||
if (!vcdPath && segment.toolResult) {
|
||||
const match = String(segment.toolResult).match(/(?:路径\\s*[::]\\s*|已生成[::]\\s*)(.+\\.vcd)/);
|
||||
if (match && match[1]) {
|
||||
vcdPath = match[1].trim();
|
||||
}
|
||||
}
|
||||
if (vcdPath) {
|
||||
const fileName = segment.fileName || vcdPath.split(/[\\\\\\/]/).pop() || 'waveform.vcd';
|
||||
const waveformPreview = createWaveformPreview(vcdPath, fileName);
|
||||
segmentDiv.appendChild(waveformPreview);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('[VCD Preview] createWaveformPreview function not found');
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCollapse) {
|
||||
setTimeout(() => {
|
||||
const header = segmentDiv.querySelector('.tool-segment-header');
|
||||
const content = segmentDiv.querySelector('.tool-segment-content');
|
||||
if (header && content) {
|
||||
header.addEventListener('click', function() {
|
||||
const isCollapsed = header.classList.contains('collapsed');
|
||||
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
|
||||
if (isCollapsed) {
|
||||
header.classList.remove('collapsed');
|
||||
content.classList.remove('collapsed');
|
||||
content.style.maxHeight = content.scrollHeight + 'px';
|
||||
toolCollapseStates.set(toolIdx, false);
|
||||
} else {
|
||||
header.classList.add('collapsed');
|
||||
content.classList.add('collapsed');
|
||||
content.style.maxHeight = '0';
|
||||
toolCollapseStates.set(toolIdx, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
} else if (segment.type === 'question') {
|
||||
segmentDiv.className += ' segment-question';
|
||||
const questions = segment.questions || (segment.question ? [{
|
||||
question: segment.question,
|
||||
options: segment.options || [],
|
||||
multiSelect: false
|
||||
}] : []);
|
||||
const isAnswered = answeredQuestions.has(segment.askId);
|
||||
const savedAnswers = answeredQuestions.get(segment.askId) || {};
|
||||
if (isAnswered) {
|
||||
segmentDiv.classList.add('answered');
|
||||
}
|
||||
|
||||
const questionsHtml = questions.map((q, qIndex) => {
|
||||
const inputType = q.multiSelect ? 'checkbox' : 'radio';
|
||||
const inputName = \`q\${qIndex}\`;
|
||||
const selectedAnswers = savedAnswers[qIndex] || [];
|
||||
let optionsHtml;
|
||||
if (!q.options || q.options.length === 0) {
|
||||
const savedText = selectedAnswers[0] || '';
|
||||
optionsHtml = \`<textarea class="question-text-input" name="\${inputName}" placeholder="请输入您的答案..." style="width:100%;min-height:80px;padding:8px;border:1px solid var(--vscode-input-border);border-radius:4px;background:var(--vscode-input-background);color:var(--vscode-input-foreground);resize:vertical;" \${isAnswered ? 'disabled' : ''}>\${savedText}</textarea>\`;
|
||||
} else {
|
||||
optionsHtml = q.options.map(opt => {
|
||||
const isSelected = selectedAnswers.includes(opt);
|
||||
return \`<label class="question-option\${isSelected ? ' selected' : ''}" style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 5px 5px 0;">
|
||||
<input type="\${inputType}" name="\${inputName}" value="\${opt}" \${isSelected ? 'checked' : ''} \${isAnswered ? 'disabled' : ''}>
|
||||
<span>\${opt}</span>
|
||||
</label>\`;
|
||||
}).join('');
|
||||
}
|
||||
return \`
|
||||
<div class="question-item" data-question-index="\${qIndex}" style="margin-bottom:12px;">
|
||||
<div class="question-text" style="margin-bottom:8px;">\${formatText(q.question)}</div>
|
||||
<div class="question-options">\${optionsHtml}</div>
|
||||
</div>
|
||||
\`;
|
||||
}).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
\${questionsHtml}
|
||||
<button class="custom-submit" style="display:\${isAnswered ? 'none' : 'block'};margin-top:8px;padding:8px 16px;background:var(--vscode-button-background);color:var(--vscode-button-foreground);border:none;border-radius:6px;cursor:pointer;">提交答案</button>
|
||||
\`;
|
||||
|
||||
if (!isAnswered) {
|
||||
setTimeout(() => {
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const answers = {};
|
||||
questions.forEach((q, qIndex) => {
|
||||
const textarea = segmentDiv.querySelector(\`textarea[name="q\${qIndex}"]\`);
|
||||
if (textarea) {
|
||||
const value = textarea.value.trim();
|
||||
answers[qIndex] = value ? [value] : [];
|
||||
} else {
|
||||
const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
|
||||
answers[qIndex] = Array.from(inputs).map(input => input.value);
|
||||
}
|
||||
});
|
||||
handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
} else if (segment.type === 'agent') {
|
||||
renderAgentCard(segment, segmentDiv);
|
||||
} else if (segment.type === 'plan') {
|
||||
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
|
||||
}
|
||||
|
||||
currentSegmentedMessage.appendChild(segmentDiv);
|
||||
});
|
||||
|
||||
if (isComplete) {
|
||||
currentSegmentedMessage = null;
|
||||
}
|
||||
|
||||
|
||||
smartScrollToBottom();
|
||||
}
|
||||
|
||||
function renderSegments(segments) {
|
||||
console.log('[WebView] renderSegments 被调用, segments:', segments);
|
||||
if (!segments || segments.length === 0) {
|
||||
console.log('[WebView] segments 为空,跳过渲染');
|
||||
return;
|
||||
}
|
||||
if (currentStreamingMessage) {
|
||||
console.log('[WebView] 移除流式消息');
|
||||
currentStreamingMessage.remove();
|
||||
currentStreamingMessage = null;
|
||||
}
|
||||
const toolStatuses = messagesEl.querySelectorAll('.tool-status');
|
||||
console.log('[WebView] 找到工具状态消息数量:', toolStatuses.length);
|
||||
toolStatuses.forEach(el => {
|
||||
console.log('[WebView] 移除工具状态消息:', el.className);
|
||||
el.remove();
|
||||
});
|
||||
updateSegmentsRealtime(segments, false);
|
||||
|
||||
// 历史消息渲染完成后添加操作按钮
|
||||
if (currentSegmentedMessage) {
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'message-actions';
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'action-btn';
|
||||
copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
copyBtn.onclick = () => {
|
||||
const textContent = segments.filter(s => s.type === 'text' && s.content).map(s => s.content).join('\\n');
|
||||
copyMessage(textContent, copyBtn);
|
||||
};
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'action-btn';
|
||||
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
likeBtn.onclick = () => toggleLike(likeBtn);
|
||||
const dislikeBtn = document.createElement('button');
|
||||
dislikeBtn.className = 'action-btn';
|
||||
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
actionsDiv.appendChild(likeBtn);
|
||||
actionsDiv.appendChild(dislikeBtn);
|
||||
currentSegmentedMessage.appendChild(actionsDiv);
|
||||
currentSegmentedMessage = null;
|
||||
}
|
||||
|
||||
smartScrollToBottom();
|
||||
}
|
||||
`;
|
||||
}
|
||||
56
src/views/textFormatter.ts
Normal file
56
src/views/textFormatter.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 文本格式化模块
|
||||
* 功能:Markdown 文本转 HTML
|
||||
* 依赖:无
|
||||
* 使用场景:消息内容格式化显示
|
||||
*/
|
||||
|
||||
export function formatText(text: string): string {
|
||||
if (!text) return "";
|
||||
|
||||
let html = text;
|
||||
|
||||
const codeBlocks: string[] = [];
|
||||
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
||||
const language = lang || "plaintext";
|
||||
const escapedCode = code
|
||||
.trim()
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
const placeholder = `___CODE_BLOCK_${codeBlocks.length}___`;
|
||||
codeBlocks.push(`<pre><code class="language-${language}">${escapedCode}</code></pre>`);
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
const inlineCodes: string[] = [];
|
||||
html = html.replace(/`([^`]+)`/g, (match, code) => {
|
||||
const escapedCode = code.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const placeholder = `___INLINE_CODE_${inlineCodes.length}___`;
|
||||
inlineCodes.push(`<code>${escapedCode}</code>`);
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
html = html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
html = html.replace(/^### (.+)$/gm, "<h3>$1</h3>");
|
||||
html = html.replace(/^## (.+)$/gm, "<h2>$1</h2>");
|
||||
html = html.replace(/^# (.+)$/gm, "<h1>$1</h1>");
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||||
html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
||||
html = html.replace(/^[\-\*] (.+)$/gm, "<li>$1</li>");
|
||||
html = html.replace(/(<li>.*<\/li>\n?)+/g, "<ul>$&</ul>");
|
||||
html = html.replace(/^\d+\. (.+)$/gm, "<li>$1</li>");
|
||||
html = html.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
html = html.replace(/\n/g, "<br>");
|
||||
|
||||
codeBlocks.forEach((block, index) => {
|
||||
html = html.replace(`___CODE_BLOCK_${index}___`, block);
|
||||
});
|
||||
|
||||
inlineCodes.forEach((code, index) => {
|
||||
html = html.replace(`___INLINE_CODE_${index}___`, code);
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
106
src/views/toolHelpers.ts
Normal file
106
src/views/toolHelpers.ts
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 工具辅助函数模块
|
||||
* 功能:工具图标、名称映射、VCD 路径解析
|
||||
* 依赖:toolIcons
|
||||
* 使用场景:工具调用显示
|
||||
*/
|
||||
|
||||
import {
|
||||
fileWriteIconSvg,
|
||||
fileReadIconSvg,
|
||||
fileDeleteIconSvg,
|
||||
syntaxCheckIconSvg,
|
||||
SearchCode,
|
||||
saveKnowledgeIconSvg,
|
||||
simulationIconSvg,
|
||||
waveformIconSvg,
|
||||
knowledgeLoadIconSvg,
|
||||
stateTransitionIconSvg,
|
||||
userQuestionIconSvg,
|
||||
updateStageIconSvg,
|
||||
successIconSvg,
|
||||
} from "../constants/toolIcons";
|
||||
|
||||
export function getToolIcon(toolName: string): string {
|
||||
const iconMap: Record<string, string> = {
|
||||
file_read: fileReadIconSvg,
|
||||
file_write: fileWriteIconSvg,
|
||||
file_delete: fileDeleteIconSvg,
|
||||
file_list: SearchCode,
|
||||
syntax_check: syntaxCheckIconSvg,
|
||||
simulation: simulationIconSvg,
|
||||
waveform_summary: waveformIconSvg,
|
||||
knowledge_save: saveKnowledgeIconSvg,
|
||||
knowledge_load: knowledgeLoadIconSvg,
|
||||
queryKnowledgeSummary: knowledgeLoadIconSvg,
|
||||
queryRules: knowledgeLoadIconSvg,
|
||||
setModule: fileWriteIconSvg,
|
||||
addSignal: fileWriteIconSvg,
|
||||
addSignalExample: fileWriteIconSvg,
|
||||
validateKnowledgeGraph: syntaxCheckIconSvg,
|
||||
querySignals: SearchCode,
|
||||
addPlan: fileWriteIconSvg,
|
||||
addEdge: fileWriteIconSvg,
|
||||
showPlan: SearchCode,
|
||||
addRule: fileWriteIconSvg,
|
||||
updateNode: fileWriteIconSvg,
|
||||
addStateTransition: stateTransitionIconSvg,
|
||||
askUser: userQuestionIconSvg,
|
||||
updatePhase: updateStageIconSvg,
|
||||
iverilog: successIconSvg,
|
||||
};
|
||||
return iconMap[toolName] || "";
|
||||
}
|
||||
|
||||
export function getToolDisplayName(toolName: string): string {
|
||||
const toolNameMap: Record<string, string> = {
|
||||
file_read: "已完成文件读取",
|
||||
file_write: "已完成文件写入",
|
||||
file_delete: "已完成文件删除",
|
||||
file_list: "已检索代码文件",
|
||||
syntax_check: "已完成语法检查",
|
||||
simulation: "已完成仿真",
|
||||
waveform_summary: "已完成波形分析",
|
||||
knowledge_save: "已保存知识库",
|
||||
knowledge_load: "已加载知识库",
|
||||
queryKnowledgeSummary: "已查询知识摘要",
|
||||
queryRules: "已查询规则",
|
||||
setModule: "已设置模块",
|
||||
addSignal: "信号分析完成",
|
||||
addSignalExample: "信号示例处理完成",
|
||||
validateKnowledgeGraph: "已验证知识图谱",
|
||||
querySignals: "已查询信号",
|
||||
addPlan: "已添加计划",
|
||||
addEdge: "已添加边",
|
||||
showPlan: "已显示计划",
|
||||
addRule: "已添加规则",
|
||||
updateNode: "已更新节点",
|
||||
addStateTransition: "已添加状态转换",
|
||||
spawnExplorer: "代码探索",
|
||||
spawnDebugger: "波形调试",
|
||||
askUser: "用户提问",
|
||||
updatePhase: "已更新阶段",
|
||||
iverilog: "已完成编译",
|
||||
};
|
||||
return toolNameMap[toolName] || toolName;
|
||||
}
|
||||
|
||||
export function parseMultiVcdPaths(toolResult: string): Array<{ name: string; path: string }> {
|
||||
if (!toolResult) return [];
|
||||
const result = String(toolResult);
|
||||
|
||||
const vcdListMatch = result.match(/VCD 文件列表:[\s\S]*?(?=\n\n|$)/);
|
||||
if (!vcdListMatch) return [];
|
||||
|
||||
const paths: Array<{ name: string; path: string }> = [];
|
||||
const lineRegex = /- (\w+): ([^\n]+)/g;
|
||||
let match;
|
||||
while ((match = lineRegex.exec(vcdListMatch[0])) !== null) {
|
||||
const name = match[1];
|
||||
const pathOrError = match[2].trim();
|
||||
if (!pathOrError.startsWith("失败")) {
|
||||
paths.push({ name: name + ".vcd", path: pathOrError });
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
@ -25,6 +25,7 @@ import {
|
||||
} from "./progressBar";
|
||||
import { getHighlightJsLinks } from "../components/codeHighlight";
|
||||
import { getCurrentEnv } from "../config/settings";
|
||||
import { taskCompleteIconSvg } from "../constants/toolIcons";
|
||||
import {
|
||||
getInvitationModalContent,
|
||||
getInvitationModalStyles,
|
||||
@ -304,7 +305,9 @@ export function getWebviewContent(
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
font-size:0.9rem
|
||||
font-size:0.9rem;
|
||||
color: var(--vscode-foreground);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.segment-tool {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
@ -375,16 +378,32 @@ export function getWebviewContent(
|
||||
font-size: 13px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.status-bar #statusText {
|
||||
background: linear-gradient(90deg,
|
||||
var(--vscode-descriptionForeground) 0%,
|
||||
var(--vscode-foreground) 50%,
|
||||
var(--vscode-descriptionForeground) 100%);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: textShimmer 2s linear infinite;
|
||||
}
|
||||
@keyframes textShimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-charts-blue);
|
||||
animation: statusPulse 1.5s ease-in-out infinite;
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
@keyframes statusPulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.8); }
|
||||
0%, 100% { opacity: 1; transform: scale(1.2); }
|
||||
50% { opacity: 0.3; transform: scale(0.6); }
|
||||
}
|
||||
.status-bar.working .status-indicator {
|
||||
background: var(--vscode-charts-orange);
|
||||
@ -529,6 +548,9 @@ export function getWebviewContent(
|
||||
const modeSelect = document.getElementById('modeSelect');
|
||||
const messagesEl = document.getElementById('messages');
|
||||
|
||||
// 图标常量
|
||||
const taskCompleteIconSvg = ${JSON.stringify(taskCompleteIconSvg)};
|
||||
|
||||
// 全局变量
|
||||
let currentStreamingMessage = null;
|
||||
let loadingIndicator = null;
|
||||
@ -760,6 +782,45 @@ export function getWebviewContent(
|
||||
// 隐藏加载指示器
|
||||
hideLoadingIndicator();
|
||||
break;
|
||||
case 'taskComplete':
|
||||
// 显示任务完成提示
|
||||
const taskDiv = document.createElement('div');
|
||||
taskDiv.className = 'message bot-message';
|
||||
const taskActionsDiv = document.createElement('div');
|
||||
taskActionsDiv.className = 'message-actions';
|
||||
const taskMessageContent = document.createElement('span');
|
||||
taskMessageContent.innerHTML = taskCompleteIconSvg + ' 任务完成';
|
||||
const taskCopyBtn = document.createElement('button');
|
||||
taskCopyBtn.className = 'action-btn';
|
||||
taskCopyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
|
||||
taskCopyBtn.onclick = () => {
|
||||
// 获取前一个 AI 消息的内容
|
||||
const prevMessage = taskDiv.previousElementSibling;
|
||||
if (prevMessage && prevMessage.classList.contains('bot-message')) {
|
||||
const textContent = prevMessage.textContent || '';
|
||||
copyMessage(textContent, taskCopyBtn);
|
||||
}
|
||||
};
|
||||
const taskLikeBtn = document.createElement('button');
|
||||
taskLikeBtn.className = 'action-btn';
|
||||
taskLikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
|
||||
taskLikeBtn.onclick = () => toggleLike(taskLikeBtn);
|
||||
const taskDislikeBtn = document.createElement('button');
|
||||
taskDislikeBtn.className = 'action-btn';
|
||||
taskDislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
|
||||
taskDislikeBtn.onclick = () => toggleDislike(taskDislikeBtn);
|
||||
taskActionsDiv.appendChild(taskMessageContent);
|
||||
taskActionsDiv.appendChild(taskCopyBtn);
|
||||
taskActionsDiv.appendChild(taskLikeBtn);
|
||||
taskActionsDiv.appendChild(taskDislikeBtn);
|
||||
taskDiv.appendChild(taskActionsDiv);
|
||||
messagesEl.appendChild(taskDiv);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
break;
|
||||
|
||||
case 'taskCompleteHistory':
|
||||
// 历史记录不显示任务完成提示
|
||||
break;
|
||||
|
||||
case 'workspaceStatus':
|
||||
// 更新工作区状态
|
||||
|
||||
Reference in New Issue
Block a user