/** * 消息渲染脚本模块 * 功能:消息渲染、滚动控制、工具状态显示 * 依赖: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 = \`复制\`; copyBtn.onclick = () => copyMessage(text, 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(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 = \`\`; 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 = ''; 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 = \`