221 lines
11 KiB
TypeScript
221 lines
11 KiB
TypeScript
/**
|
||
* 消息渲染脚本模块
|
||
* 功能:消息渲染、滚动控制、工具状态显示
|
||
* 依赖: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()}
|
||
`;
|
||
}
|