1679 lines
65 KiB
TypeScript
1679 lines
65 KiB
TypeScript
/**
|
||
* 消息区域模块
|
||
*
|
||
* 功能说明:
|
||
* - 负责聊天消息的显示和渲染
|
||
* - 支持用户消息和 AI 消息的不同样式
|
||
* - 提供消息操作功能(复制、点赞、点踩)
|
||
* - 支持流式消息实时更新
|
||
* - 支持分段消息渲染(文本、工具调用、用户问题)
|
||
* - 显示工具执行状态和加载指示器
|
||
*/
|
||
|
||
import {
|
||
collapseIconSvg,
|
||
fileWriteIconSvg,
|
||
fileReadIconSvg,
|
||
fileDeleteIconSvg,
|
||
syntaxCheckIconSvg,
|
||
SearchCode,
|
||
agentIconSvg,
|
||
saveKnowledgeIconSvg,
|
||
simulationIconSvg,
|
||
waveformIconSvg,
|
||
knowledgeLoadIconSvg,
|
||
stateTransitionIconSvg,
|
||
userQuestionIconSvg,
|
||
} from "../constants/toolIcons";
|
||
import {
|
||
getWaveformPreviewContent,
|
||
getWaveformPreviewScript,
|
||
} from "./waveformPreviewContent";
|
||
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
|
||
import { getPlanCardStyles, getPlanCardScript } from "./planCard";
|
||
import {
|
||
getCodeHighlightStyles,
|
||
getCodeHighlightScript,
|
||
} from "../components/codeHighlight";
|
||
|
||
/**
|
||
* 获取消息区域的 HTML 内容
|
||
*/
|
||
export function getMessageAreaContent(): string {
|
||
return `<div id="messages" class="messages"></div>`;
|
||
}
|
||
|
||
/**
|
||
* 获取消息区域的样式
|
||
*/
|
||
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: flex;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
margin-left: 10px;
|
||
opacity: 0.85;
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
.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;
|
||
}
|
||
.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: 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 0;
|
||
}
|
||
.segment-text {
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* Markdown 样式 */
|
||
.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: 2px 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;
|
||
}
|
||
.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;
|
||
}
|
||
/* 低调显示的工具调用样式 */
|
||
.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: 10px;
|
||
}
|
||
.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: 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;
|
||
font-size: 13px;
|
||
}
|
||
.segment-question .question-option:hover {
|
||
background: var(--vscode-button-secondaryHoverBackground);
|
||
}
|
||
.segment-question .question-option.selected {
|
||
background: var(--vscode-button-background);
|
||
color: var(--vscode-button-foreground);
|
||
}
|
||
.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()}
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 获取消息区域的脚本
|
||
*/
|
||
export function getMessageAreaScript(): string {
|
||
return `
|
||
// 工具图标定义
|
||
const collapseIconSvg = \`${collapseIconSvg}\`;
|
||
const fileWriteIconSvg = \`${fileWriteIconSvg}\`;
|
||
const fileReadIconSvg = \`${fileReadIconSvg}\`;
|
||
const fileDeleteIconSvg = \`${fileDeleteIconSvg}\`;
|
||
const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`;
|
||
const searchCodeIconSvg = \`${SearchCode}\`;
|
||
const saveKnowledgeIconSvg = \`${saveKnowledgeIconSvg}\`;
|
||
const simulationIconSvg = \`${simulationIconSvg}\`;
|
||
const waveformIconSvg = \`${waveformIconSvg}\`;
|
||
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
|
||
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
|
||
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
|
||
|
||
${getAgentCardScript()}
|
||
|
||
${getPlanCardScript()}
|
||
|
||
// 解析多 VCD 文件路径
|
||
function parseMultiVcdPaths(toolResult) {
|
||
if (!toolResult) return [];
|
||
const result = String(toolResult);
|
||
|
||
// 匹配 "- moduleName: path" 格式
|
||
const vcdListMatch = result.match(/VCD 文件列表:[\\s\\S]*?(?=\\n\\n|$)/);
|
||
if (!vcdListMatch) return [];
|
||
|
||
const paths = [];
|
||
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;
|
||
}
|
||
|
||
// 获取工具图标
|
||
function getToolIcon(toolName) {
|
||
const iconMap = {
|
||
'file_read': fileReadIconSvg,
|
||
'file_write': fileWriteIconSvg,
|
||
'file_delete': fileDeleteIconSvg,
|
||
'file_list': searchCodeIconSvg,
|
||
'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': searchCodeIconSvg,
|
||
'addPlan': fileWriteIconSvg,
|
||
'addEdge': fileWriteIconSvg,
|
||
'showPlan': searchCodeIconSvg,
|
||
'addRule': fileWriteIconSvg,
|
||
'updateNode': fileWriteIconSvg,
|
||
'addStateTransition': stateTransitionIconSvg,
|
||
'askUser': userQuestionIconSvg,
|
||
};
|
||
return iconMap[toolName] || '';
|
||
}
|
||
|
||
// 工具名称映射
|
||
function getToolDisplayName(toolName) {
|
||
const toolNameMap = {
|
||
'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': '用户提问',
|
||
};
|
||
return toolNameMap[toolName] || toolName;
|
||
}
|
||
|
||
// 自动滚动控制标志
|
||
let shouldAutoScroll = true;
|
||
let lastScrollHeight = 0;
|
||
|
||
// 检查用户是否在底部附近(允许50px的误差)
|
||
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 messageContent = document.createElement('div');
|
||
messageContent.textContent = text;
|
||
div.appendChild(messageContent);
|
||
|
||
// 创建操作按钮容器
|
||
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 = () => 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(copyBtn);
|
||
actionsDiv.appendChild(likeBtn);
|
||
actionsDiv.appendChild(dislikeBtn);
|
||
|
||
div.appendChild(actionsDiv);
|
||
} else {
|
||
div.textContent = text;
|
||
// 当添加用户消息时,隐藏 header
|
||
hideHeaderIfNeeded();
|
||
}
|
||
|
||
messagesEl.appendChild(div);
|
||
smartScrollToBottom();
|
||
|
||
// 添加消息后检查 header 显示状态
|
||
checkHeaderVisibility();
|
||
}
|
||
|
||
// 检查是否需要隐藏 header
|
||
function hideHeaderIfNeeded() {
|
||
checkHeaderVisibility();
|
||
}
|
||
|
||
// 复制消息
|
||
function copyMessage(text, button) {
|
||
navigator.clipboard.writeText(text).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');
|
||
// 移除所有同级按钮的 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');
|
||
// 移除所有同级按钮的 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;
|
||
}
|
||
}
|
||
|
||
// 存储已回答问题的状态
|
||
const answeredQuestions = new Map(); // askId -> answer
|
||
|
||
// 存储工具展开/折叠状态
|
||
const toolCollapseStates = new Map(); // index -> isCollapsed
|
||
|
||
// 实时更新分段消息(按后端返回顺序)
|
||
function updateSegmentsRealtime(segments, isComplete) {
|
||
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
|
||
|
||
if (!segments || segments.length === 0) {
|
||
console.log('[WebView] segments 为空,跳过渲染');
|
||
return;
|
||
}
|
||
|
||
// 如果没有当前分段消息容器,创建一个
|
||
if (!currentSegmentedMessage) {
|
||
console.log('[WebView] 创建新的分段消息容器');
|
||
// 移除流式消息(如果有)
|
||
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();
|
||
});
|
||
|
||
currentSegmentedMessage = document.createElement('div');
|
||
currentSegmentedMessage.className = 'message bot-message segmented-message';
|
||
messagesEl.appendChild(currentSegmentedMessage);
|
||
}
|
||
|
||
// 保存当前所有工具的展开/折叠状态
|
||
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);
|
||
});
|
||
}
|
||
|
||
// 清空容器并重新渲染所有段落
|
||
currentSegmentedMessage.innerHTML = '';
|
||
|
||
// 合并连续相同的工具调用
|
||
const mergedSegments = [];
|
||
let i = 0;
|
||
while (i < segments.length) {
|
||
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 statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||
const toolResult = segment.toolResult || '';
|
||
const toolCount = segment.toolCount || 1;
|
||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||
|
||
// 检查工具结果是否过长(超过一行显示不下)
|
||
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>\` : ''}
|
||
\`;
|
||
|
||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||
// 尝试解析多个 VCD 文件(多 VCD 模式)
|
||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||
|
||
if (vcdPaths.length > 0) {
|
||
// 多 VCD 模式:为每个文件创建预览
|
||
vcdPaths.forEach(vcdInfo => {
|
||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||
segmentDiv.appendChild(waveformPreview);
|
||
});
|
||
} else {
|
||
// 单 VCD 模式(兼容旧逻辑)
|
||
let vcdPath = segment.vcdFilePath;
|
||
if (!vcdPath && segment.toolResult) {
|
||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加折叠/展开事件监听
|
||
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 isAnswered = answeredQuestions.has(segment.askId);
|
||
const selectedAnswer = answeredQuestions.get(segment.askId);
|
||
|
||
if (isAnswered) {
|
||
segmentDiv.classList.add('answered');
|
||
}
|
||
|
||
// 检查是否有选项
|
||
const hasOptions = segment.options && segment.options.length > 0;
|
||
|
||
const optionsHtml = hasOptions
|
||
? (segment.options || []).map(opt => {
|
||
const isSelected = isAnswered && opt === selectedAnswer;
|
||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
||
}).join('')
|
||
: '';
|
||
|
||
segmentDiv.innerHTML = \`
|
||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
||
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
|
||
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
|
||
<button class="custom-submit">提交</button>
|
||
</div>
|
||
\`;
|
||
|
||
// 只在未回答时添加事件监听
|
||
if (!isAnswered) {
|
||
setTimeout(() => {
|
||
if (hasOptions) {
|
||
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
||
optionButtons.forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const option = this.getAttribute('data-option');
|
||
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
||
});
|
||
});
|
||
}
|
||
|
||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||
const customInput = segmentDiv.querySelector('.custom-input');
|
||
if (submitBtn && customInput) {
|
||
submitBtn.addEventListener('click', function() {
|
||
const customValue = customInput.value.trim();
|
||
if (customValue) {
|
||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||
}
|
||
});
|
||
|
||
// 支持回车提交
|
||
customInput.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
const customValue = customInput.value.trim();
|
||
if (customValue) {
|
||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}, 0);
|
||
}
|
||
} else if (segment.type === 'agent') {
|
||
// 智能体卡片渲染
|
||
renderAgentCard(segment, segmentDiv);
|
||
} else if (segment.type === 'plan') {
|
||
// 计划卡片渲染(使用独立组件)
|
||
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
|
||
}
|
||
|
||
currentSegmentedMessage.appendChild(segmentDiv);
|
||
});
|
||
|
||
// 如果对话完成,添加操作按钮
|
||
if (isComplete) {
|
||
console.log('[WebView] 对话完成,添加操作按钮');
|
||
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();
|
||
}
|
||
|
||
// 渲染分段消息(兼容旧代码)
|
||
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();
|
||
});
|
||
|
||
// 创建消息容器
|
||
const container = document.createElement('div');
|
||
container.className = 'message bot-message segmented-message';
|
||
|
||
// 合并连续相同的工具调用
|
||
const mergedSegments = [];
|
||
let i = 0;
|
||
while (i < segments.length) {
|
||
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++;
|
||
}
|
||
}
|
||
|
||
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 statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
|
||
const toolResult = segment.toolResult || '';
|
||
const toolCount = segment.toolCount || 1;
|
||
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
|
||
|
||
// 检查工具结果是否过长(超过一行显示不下)
|
||
const shouldCollapse = toolResult && toolResult.length > 60;
|
||
|
||
segmentDiv.innerHTML = \`
|
||
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
|
||
\${shouldCollapse ? \`<span class="icon-collapsed" style="display:block;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span><span class="icon-expanded" style="display:none;width:16px;height:16px;flex-shrink:0;">\${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 collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
|
||
\`;
|
||
|
||
// 如果是仿真工具且成功完成,尝试添加波形预览
|
||
if (segment.toolName === 'simulation' && segment.toolStatus === 'success') {
|
||
// 尝试解析多个 VCD 文件(多 VCD 模式)
|
||
const vcdPaths = parseMultiVcdPaths(segment.toolResult);
|
||
|
||
if (vcdPaths.length > 0) {
|
||
// 多 VCD 模式:为每个文件创建预览
|
||
vcdPaths.forEach(vcdInfo => {
|
||
const waveformPreview = createWaveformPreview(vcdInfo.path, vcdInfo.name);
|
||
segmentDiv.appendChild(waveformPreview);
|
||
});
|
||
} else {
|
||
// 单 VCD 模式(兼容旧逻辑)
|
||
let vcdPath = segment.vcdFilePath;
|
||
if (!vcdPath && segment.toolResult) {
|
||
const match = String(segment.toolResult).match(/路径\s*:\s*(.+)/);
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加折叠/展开事件监听
|
||
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.innerHTML = \`
|
||
<div class="question-segment">
|
||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
||
<div class="question-options">
|
||
\${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')}
|
||
</div>
|
||
</div>
|
||
\`;
|
||
} else if (segment.type === 'agent') {
|
||
// 智能体卡片渲染
|
||
renderAgentCard(segment, segmentDiv);
|
||
} else if (segment.type === 'plan') {
|
||
// 计划卡片渲染(使用独立组件)
|
||
renderPlanCardStatic(segment, segmentDiv);
|
||
}
|
||
|
||
container.appendChild(segmentDiv);
|
||
});
|
||
|
||
// 添加操作按钮
|
||
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);
|
||
container.appendChild(actionsDiv);
|
||
|
||
messagesEl.appendChild(container);
|
||
smartScrollToBottom();
|
||
}
|
||
|
||
// 格式化文本(支持 Markdown)
|
||
function formatText(text) {
|
||
if (!text) return '';
|
||
|
||
let html = text;
|
||
|
||
// 先提取并处理代码块(避免被转义)
|
||
const codeBlocks = [];
|
||
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
|
||
const language = lang || 'plaintext';
|
||
// 转义代码内容
|
||
const escapedCode = code.trim()
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
// 不再手动高亮,让 highlight.js 处理
|
||
const placeholder = \`___CODE_BLOCK_\${codeBlocks.length}___\`;
|
||
codeBlocks.push('<pre><code class="language-' + language + '">' + escapedCode + '</code></pre>');
|
||
return placeholder;
|
||
});
|
||
|
||
// 提取行内代码(避免被转义)
|
||
const inlineCodes = [];
|
||
html = html.replace(/\`([^\`]+)\`/g, function(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 = html
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
|
||
// 处理标题 ### Title
|
||
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
||
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
||
|
||
// 处理粗体 **text**
|
||
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
||
|
||
// 处理斜体 *text*
|
||
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
||
|
||
// 处理无序列表 - item 或 * item
|
||
html = html.replace(/^[\\-\\*] (.+)$/gm, '<li>$1</li>');
|
||
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
|
||
|
||
// 处理有序列表 1. item
|
||
html = html.replace(/^\\d+\\. (.+)$/gm, '<li>$1</li>');
|
||
|
||
// 处理链接 [text](url)
|
||
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;
|
||
}
|
||
|
||
// 添加工具状态消息
|
||
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();
|
||
|
||
// 添加消息后检查 header 显示状态
|
||
checkHeaderVisibility();
|
||
}
|
||
|
||
// 显示用户问题
|
||
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();
|
||
|
||
// 添加消息后检查 header 显示状态
|
||
checkHeaderVisibility();
|
||
}
|
||
|
||
// 处理问题回答
|
||
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);
|
||
|
||
// 保存答案到 Map 中
|
||
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
|
||
});
|
||
}
|
||
|
||
${getWaveformPreviewScript()}
|
||
|
||
${getCodeHighlightScript()}
|
||
`;
|
||
}
|