/** * 分段消息渲染脚本模块 * 功能:实时更新分段消息、工具调用展示 * 依赖:toolHelpers, textFormatter, waveformPreviewContent * 使用场景:webview 中的分段消息渲染 */ export function getSegmentRendererScript(): string { return ` function updateSegmentsRealtime(segments, isComplete) { if (isComplete && (!segments || segments.length === 0)) { currentSegmentedMessage = null; return; } if (!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); }); } 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 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 = \`
\${shouldCollapse ? \`\${collapseIconSvg}\` : getToolIcon(segment.toolName)} \${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix} \${toolResult && !shouldCollapse ? \`\${toolResult}\` : ''}
\${shouldCollapse ? \`
\${toolResult}
\` : ''} \${toolDescription ? \`

\${toolDescription}

\` : ''} \`; 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 = \`\`; } else { optionsHtml = q.options.map(opt => { const isSelected = selectedAnswers.includes(opt); return \`\`; }).join(''); } return \`
\${formatText(q.question)}
\${optionsHtml}
\`; }).join(''); segmentDiv.innerHTML = \` \${questionsHtml} \`; 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) { console.log('[WebView] 对话完成,添加操作按钮'); const actionsDiv = document.createElement('div'); actionsDiv.className = 'message-actions'; const copyBtn = document.createElement('button'); copyBtn.className = 'action-btn'; copyBtn.innerHTML = \`复制\`; 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 = \`点赞\`; likeBtn.onclick = () => toggleLike(likeBtn); const dislikeBtn = document.createElement('button'); dislikeBtn.className = 'action-btn'; dislikeBtn.innerHTML = \`点踩\`; 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'; updateSegmentsRealtime(segments, true); messagesEl.appendChild(container); smartScrollToBottom(); } `; }