// --- START OF FILE chat.js --- // static/js/ui/chat.js import * as state from '../state.js'; import * as db from '../db.js'; import { dom } from './dom.js'; import { toggleHtmlPreviewModal, toggleSidebar, showHistoryMenu, showMessageMenu } from './modals.js'; import { createDeepThinkPanel, createReasoningPanel, hideDeepThinkPanel, hideReasoningPanel, updateDeepThinkPanel, updateReasoningPanel } from './tools.js'; export const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6InZSVUdlLzBlR0FzOHZJdXFZeWhER0E9PSIsInZhbHVlIjoiWFhqRXBLc29vSFpHdk9nYmRjZGVuWHRHRHVSZHRlTG1BUENLaE5mNXBNVVRGWFg3ZWN0djJ5K1dIY1RqTHJGaCIsIm1hYyI6IjIzYzFlZTMwYmVmMTdkYjQ0YTQ4YWMxNmFhN2RmNWQ2OTc1NDIyNGVlZGI3ZjJjMjhkNmQxNjM4MDFlZTIxNmUiLCJ0YWciOiIifQ==/20934991'; const MAX_TEXTAREA_HEIGHT = 150; export let minTextareaHeight = 0; const atomIconSVG = ``; const robotIconInBubbleSVG = `
`; window.toggleThinkingPanel = function(headElement) { const wrapper = headElement.closest('.thinking-panel-wrapper'); const body = wrapper.querySelector('.thinking-body'); const chevron = headElement.querySelector('.thinking-chevron'); if (body && chevron) { body.classList.toggle('collapsed'); chevron.classList.toggle('collapsed'); } }; export function startThinking(modelBubbleOuterDivElement) { const contentArea = modelBubbleOuterDivElement?.querySelector('.message-content'); if (!contentArea) return; const modelContent = `
${robotIconInBubbleSVG}
${atomIconSVG} افکار
`; contentArea.innerHTML = modelContent; } export function streamThought(text, modelBubbleOuterDivElement) { const thinkingBody = modelBubbleOuterDivElement.querySelector('.thinking-body'); if (!thinkingBody) return; const existingContent = thinkingBody.innerHTML; const newContent = DOMPurify.sanitize(marked.parse(thinkingBody.textContent + text, { breaks: true, gfm: true })); thinkingBody.innerHTML = newContent; thinkingBody.scrollTop = thinkingBody.scrollHeight; } function isScrolledToBottom() { const { chatWindow } = dom; const scrollThreshold = 15; return chatWindow.scrollHeight - chatWindow.clientHeight <= chatWindow.scrollTop + scrollThreshold; } export function escapeHTML(str) { const p = document.createElement("p"); p.textContent = str; return p.innerHTML; } function getFileIcon(mimeType) { if (mimeType.startsWith('image/')) return '🖼️'; if (mimeType.startsWith('video/')) return '🎬'; if (mimeType.startsWith('audio/')) return '🎵'; if (mimeType.startsWith('application/pdf')) return '📄'; if (mimeType.startsWith('text/')) return '📝'; return '📁'; } export function hideFilePreview() { dom.imagePreviewContainer.classList.add('hidden'); dom.imagePreview.src = ''; dom.fileInfoText.innerHTML = ''; dom.imageFileInput.value = ''; dom.generalFileInput.value = ''; } export function showFileUploading(fileName) { dom.imagePreview.src = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='animate-spin' fill='none' viewBox='0 0 24 24' stroke-width='2' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707'/%3E%3C/svg%3E`; dom.fileInfoText.innerHTML = `
${escapeHTML(fileName)}
در حال آپلود... 0%
`; dom.imagePreviewContainer.classList.remove('hidden'); } export function showFileReady(fileName, mimeType, url) { const icon = getFileIcon(mimeType); if(mimeType.startsWith('image/')) { dom.imagePreview.src = url; } else { dom.imagePreview.src = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z'%3E%3C/path%3E%3Cpolyline points='13 2 13 9 20 9'%3E%3C/polyline%3E%3C/svg%3E`; } dom.fileInfoText.innerHTML = `
${icon} ${escapeHTML(fileName)}فایل برای ارسال آماده است.
`; dom.imagePreviewContainer.classList.remove('hidden'); } export function showFileError(errorMessage) { dom.imagePreview.src = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' /%3E%3C/svg%3E`; dom.fileInfoText.innerHTML = `
خطا در آپلود${escapeHTML(errorMessage)}
`; dom.imagePreviewContainer.classList.remove('hidden'); setTimeout(hideFilePreview, 5000); } export function handleSuggestionClick(text) { dom.messageInput.value = text; dom.messageInput.dispatchEvent(new Event('input', { bubbles: true })); dom.messageInput.focus(); } export function runWelcomeAnimation() { const chatbotNameContainer = document.querySelector('.chatbot-name'); const mainTitle = document.querySelector('.main-title'); const suggestionsContainer = document.querySelector('.suggestions-container'); if (!chatbotNameContainer || !mainTitle || !suggestionsContainer) return; const textToType = "چت بات آلفا"; let charIndex = 0; const typingSpeed = 90; function typeChatbotName() { if (charIndex < textToType.length) { chatbotNameContainer.textContent += textToType.charAt(charIndex); charIndex++; setTimeout(typeChatbotName, typingSpeed); } else { chatbotNameContainer.style.opacity = '1'; setTimeout(() => { mainTitle.style.opacity = '1'; }, 300); setTimeout(() => { suggestionsContainer.style.opacity = '1'; suggestionsContainer.style.transform = 'translateY(0)'; }, 600); } } chatbotNameContainer.textContent = ''; typeChatbotName(); } export function setupCodeBlockActions(container) { container.querySelectorAll('pre').forEach(preElement => { preElement.setAttribute('dir', 'ltr'); if (preElement.querySelector('.code-button-container')) return; const codeElement = preElement.querySelector('code'); if (!codeElement) return; hljs.highlightElement(codeElement); const buttonContainer = document.createElement('div'); buttonContainer.className = 'code-button-container'; const copyButton = document.createElement('button'); copyButton.className = 'code-button'; copyButton.innerHTML = `کپی`; const copyTextSpan = copyButton.querySelector('.copy-text'); copyButton.onclick = () => { navigator.clipboard.writeText(codeElement.innerText).then(() => { copyTextSpan.textContent = 'کپی شد!'; copyButton.style.backgroundColor = '#4CAF50'; setTimeout(() => { copyTextSpan.textContent = 'کپی'; copyButton.style.backgroundColor = ''; }, 2000); }); }; buttonContainer.appendChild(copyButton); const languageClass = Array.from(codeElement.classList).find(cls => cls.startsWith('language-')); if (languageClass === 'language-html') { const runButton = document.createElement('button'); runButton.className = 'code-button'; runButton.innerHTML = `اجرا`; runButton.onclick = () => { toggleHtmlPreviewModal(true, codeElement.innerText); }; buttonContainer.appendChild(runButton); } preElement.appendChild(buttonContainer); }); } export function renderHistoryList() { dom.historyList.innerHTML = ''; const chatsToDisplay = state.chatSessions.filter(session => session.messages.length > 0 || session.id === state.activeChatId); if (chatsToDisplay.length > 0) { chatsToDisplay.forEach((session) => { const itemContainer = document.createElement('div'); itemContainer.className = 'history-item flex items-center justify-between rounded-lg'; const itemLink = document.createElement('a'); itemLink.href = '#'; itemLink.className = `flex-grow p-3 truncate transition-colors rounded-lg ${session.id === state.activeChatId ? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold' : 'hover:bg-slate-200/60 dark:hover:bg-slate-700/60 text-slate-700 dark:text-slate-300'}`; itemLink.textContent = session.title; itemLink.onclick = (e) => { e.preventDefault(); state.setActiveChatId(session.id); renderActiveChat(); renderHistoryList(); toggleSidebar(false); }; const menuButton = document.createElement('button'); menuButton.className = 'history-item-button p-2 ml-1 text-slate-500 dark:text-slate-400 hover:bg-slate-200/80 dark:hover:bg-slate-700/80 rounded-full flex-shrink-0'; menuButton.innerHTML = ''; menuButton.onclick = (e) => { e.preventDefault(); e.stopPropagation(); showHistoryMenu(e, session.id); }; itemContainer.appendChild(itemLink); itemContainer.appendChild(menuButton); dom.historyList.appendChild(itemContainer); }); } } export async function renderActiveChat() { dom.chatWindow.innerHTML = ''; const activeChat = state.getActiveChat(); if (activeChat && activeChat.messages.length === 0) { dom.chatWindow.innerHTML = `

چطور می‌توانم به شما کمک کنم؟

`; runWelcomeAnimation(); } else if (activeChat && activeChat.messages.length > 0) { const lastMessageIndex = activeChat.messages.length - 1; const lastUserMessageIndex = state.findLastIndex(activeChat.messages, msg => msg.role === 'user'); for (const [index, msg] of activeChat.messages.entries()) { if (msg.isTemporary) continue; const isLastUser = (index === lastUserMessageIndex); const isLastModel = (index === lastMessageIndex && msg.role === 'assistant'); await addMessageToUI(msg, index, { isLastUser, isLastModel, animate: false }); } } requestAnimationFrame(() => { dom.chatWindow.scrollTop = dom.chatWindow.scrollHeight; }); } export function createMessageActionsHtml(options) { const { role, isLastUser, isLastModel, messageObject } = options; let buttonsHtml = ''; const textContent = messageObject?.parts.find(p => p.text)?.text; const copyButtonHtml = ``; const menuButtonHtml = ``; if (role === 'user') { if (textContent) { buttonsHtml += copyButtonHtml; } if (isLastUser && textContent) { buttonsHtml += ``; } buttonsHtml += menuButtonHtml; } if (role === 'assistant') { const hasTextContent = messageObject?.parts.some(p => p.text); const isClarification = !!messageObject?.clarification; const isGpuGuide = !!messageObject?.isGpuGuide; if (hasTextContent) { buttonsHtml += ``; buttonsHtml += copyButtonHtml; } if (isLastModel && !isClarification && !isGpuGuide) { buttonsHtml += ``; } if (hasTextContent && !isClarification && !isGpuGuide) { buttonsHtml += ``; } buttonsHtml += menuButtonHtml; } return buttonsHtml ? `
${buttonsHtml}
` : ''; } function createFileContentHtml(filePart) { const { fileUrl, mimeType, name } = filePart; let fileHtml = ''; if (!fileUrl) { return `
خطا: فایل برای نمایش یافت نشد.
`; } if (mimeType.startsWith('image/')) { fileHtml = `${escapeHTML(name) || 'Uploaded image'}`; } else if (mimeType.startsWith('video/')) { fileHtml = ``; } else if (mimeType.startsWith('audio/')) { fileHtml = ``; } else { fileHtml = `
${escapeHTML(name)} دانلود فایل
`; } return fileHtml; } export async function addMessageToUI(message, index, options = {}, existingElement = null) { const { role, parts } = message; const { isLastUser = false, isLastModel = false, animate = true } = options; const isUser = role === 'user'; let finalElement = existingElement; // *** منطق جدید و دقیق برای پاکسازی دکمه‌ها *** if (!existingElement) { const activeChat = state.getActiveChat(); if (isUser) { // وقتی کاربر پیام جدید میده: // 1. دکمه ویرایش از تمام پیام‌های قبلی کاربر حذف بشه const prevUserMsgs = dom.chatWindow.querySelectorAll('.message-entry.user'); prevUserMsgs.forEach(el => { const idx = parseInt(el.dataset.index, 10); if (activeChat.messages[idx]) updateMessageActions(el, activeChat.messages[idx], false, false); }); // 2. دکمه تلاش مجدد از تمام پیام‌های قبلی هوش مصنوعی حذف بشه (چون بحث ادامه پیدا کرده) const prevModelMsgs = dom.chatWindow.querySelectorAll('.message-entry.model'); prevModelMsgs.forEach(el => { const idx = parseInt(el.dataset.index, 10); if (activeChat.messages[idx]) updateMessageActions(el, activeChat.messages[idx], false, false); }); } else { // وقتی هوش مصنوعی پیام جدید میده: // 1. دکمه تلاش مجدد از پیام‌های قبلی هوش مصنوعی حذف بشه const prevModelMsgs = dom.chatWindow.querySelectorAll('.message-entry.model'); prevModelMsgs.forEach(el => { const idx = parseInt(el.dataset.index, 10); if (activeChat.messages[idx]) updateMessageActions(el, activeChat.messages[idx], false, false); }); } } if (!finalElement) { finalElement = document.createElement('div'); const roleClass = isUser ? 'user' : 'model'; finalElement.className = `message-entry ${roleClass} mb-6 flex items-end gap-3 ${isUser ? 'justify-end' : 'justify-start'}`; finalElement.dataset.index = index; if (animate) finalElement.classList.add('message-entry'); const userIcon = `
`; const bubbleClasses = isUser ? 'bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-br-none' : 'bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-800 dark:text-slate-200 rounded-bl-none'; const messageBubbleHTML = `
`; finalElement.innerHTML = `
${messageBubbleHTML}
${isUser ? userIcon : ''} `; dom.chatWindow.appendChild(finalElement); } const contentArea = finalElement.querySelector('.message-content'); if (!isUser) { contentArea.classList.add('model-bubble'); contentArea.style.padding = '0'; } else { contentArea.innerHTML = ''; contentArea.style.padding = '1rem'; } if (isUser) { const textParts = parts.filter(p => p.text); const fileParts = parts.filter(p => p.id); const processedFileParts = await Promise.all( fileParts.map(async (part) => { if (part.id) { try { const file = await db.getFile(part.id); if (file) { const newBlobUrl = URL.createObjectURL(file); return { ...part, fileUrl: newBlobUrl, mimeType: file.type, name: file.name }; } } catch (error) { console.error(`Error retrieving file ${part.id} from DB:`, error); } } return { ...part, fileUrl: null }; }) ); const fileHtml = processedFileParts.map(p => createFileContentHtml(p)).join(''); const textHtml = textParts.map(p => `
${escapeHTML(p.text)}
`).join(''); if (processedFileParts.length > 0 && textParts.length > 0) { contentArea.classList.add('user-bubble-multipart'); contentArea.innerHTML = `
${fileHtml}
${textHtml}
`; } else { contentArea.classList.remove('user-bubble-multipart'); contentArea.innerHTML = fileHtml + textHtml; if (processedFileParts.length === 1 && (processedFileParts[0].mimeType?.startsWith('image/') || processedFileParts[0].mimeType?.startsWith('video/'))) { contentArea.style.padding = '0'; const filePartElement = contentArea.querySelector('.user-file-part'); if (filePartElement) filePartElement.classList.add('single'); } } } else if (message.isTemporary) { const activeTool = state.getActiveTool(); if (activeTool === 'deep-think') { createDeepThinkPanel(finalElement); } else if (activeTool === 'reasoning') { createReasoningPanel(finalElement); } else { const activeChat = state.getActiveChat(); if (activeChat && activeChat.showThoughts) { startThinking(finalElement); } else { showFreeWsLoadingIndicator(finalElement); } } } else { const allContent = parts?.filter(p => p.text).map(p => p.text).join('') || ''; if (message.toolUsed === 'deep-think') { createDeepThinkPanel(finalElement); hideDeepThinkPanel(finalElement); finalizeFinalText(finalElement, allContent); } else if (message.toolUsed === 'reasoning') { createReasoningPanel(finalElement); hideReasoningPanel(finalElement); finalizeFinalText(finalElement, allContent); } else if (message.wasGeneratedWithThoughts) { startThinking(finalElement); finalizeFinalText(finalElement, allContent); } else { finalizeFreeWsMessage(finalElement, allContent); } } updateMessageActions(finalElement, message, isLastUser, isLastModel); if (!existingElement && animate) { finalElement.scrollIntoView({ behavior: 'smooth', block: 'end' }); } return finalElement; } export function showLimitReachedUpgrade() { const message = "محدودیت پیام‌های روزانه شما به پایان رسیده است."; const modelBubbleOuterDivElement = document.createElement('div'); modelBubbleOuterDivElement.className = 'message-entry model mb-6 flex items-end gap-3 justify-start'; modelBubbleOuterDivElement.style.animation = 'fade-slide-in 300ms ease-out forwards'; const limitReachedHTML = `

محدودیت استفاده رایگان

${message} برای ادامه استفاده نامحدود، حساب خود را ارتقا دهید.

`; modelBubbleOuterDivElement.innerHTML = limitReachedHTML; dom.chatWindow.appendChild(modelBubbleOuterDivElement); const upgradeButton = modelBubbleOuterDivElement.querySelector('#limit-upgrade-btn'); if (upgradeButton) { upgradeButton.addEventListener('click', () => { parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*'); }); } modelBubbleOuterDivElement.scrollIntoView({ behavior: 'smooth', block: 'end' }); } export function streamFinalText(text, modelBubbleOuterDivElement) { const finalAnswerWrapper = modelBubbleOuterDivElement.querySelector('.final-answer-wrapper'); if (!finalAnswerWrapper) return; if (!finalAnswerWrapper.classList.contains('visible')) { finalAnswerWrapper.classList.add('visible'); } const shouldScroll = isScrolledToBottom(); if (finalAnswerWrapper.innerHTML.trim() === '') { finalAnswerWrapper.innerHTML = `
`; } const contentContainer = finalAnswerWrapper.querySelector('.p-4'); if (!contentContainer) return; const content = DOMPurify.sanitize(marked.parse(text + '▍' || " ", { breaks: true, gfm: true })); contentContainer.innerHTML = content; contentContainer.querySelectorAll('pre code').forEach(block => { hljs.highlightElement(block); }); if (shouldScroll) { requestAnimationFrame(() => { dom.chatWindow.scrollTop = dom.chatWindow.scrollHeight; }); } } export function finalizeFinalText(modelBubbleOuterDivElement, fullText) { const finalAnswerWrapper = modelBubbleOuterDivElement.querySelector('.final-answer-wrapper'); if (!finalAnswerWrapper) return; const shouldScroll = isScrolledToBottom(); if (!finalAnswerWrapper.classList.contains('visible')) { finalAnswerWrapper.classList.add('visible'); } if (finalAnswerWrapper.innerHTML.trim() === '') { finalAnswerWrapper.innerHTML = `
`; } const contentContainer = finalAnswerWrapper.querySelector('.p-4'); const content = DOMPurify.sanitize(marked.parse(fullText || " ", { breaks: true, gfm: true })); contentContainer.innerHTML = content; setupCodeBlockActions(finalAnswerWrapper); if (shouldScroll) { requestAnimationFrame(() => { dom.chatWindow.scrollTop = dom.chatWindow.scrollHeight; }); } } export function updateMessageActions(messageOuterDivElement, messageObject, isLastUser, isLastModel) { const messageWrapper = messageOuterDivElement.querySelector('.group'); if (!messageWrapper) return; let oldActionsContainer = messageWrapper.querySelector('.message-actions'); if (oldActionsContainer) { oldActionsContainer.remove(); } const newActionsHtml = createMessageActionsHtml({ role: messageObject.role, isLastUser: isLastUser, isLastModel: isLastModel, messageObject: messageObject }); if (newActionsHtml) { messageWrapper.insertAdjacentHTML('beforeend', newActionsHtml); } } export function adjustTextareaHeight(el) { el.style.height = 'auto'; el.style.height = `${el.scrollHeight}px`; } export function showCopyFeedback(button) { const copyIcon = button.querySelector('.copy-icon'); const checkIcon = button.querySelector('.check-icon'); const feedback = button.querySelector('.copy-feedback'); if (copyIcon && checkIcon && feedback) { copyIcon.classList.add('hidden'); checkIcon.classList.remove('hidden'); feedback.classList.add('visible'); setTimeout(() => { copyIcon.classList.remove('hidden'); checkIcon.classList.add('hidden'); feedback.classList.remove('visible'); }, 2000); } } export function handleLikeDislike(button, messageEntry) { const isActive = button.classList.toggle('active'); if (isActive) { button.classList.add('like-animation'); button.addEventListener('animationend', () => button.classList.remove('like-animation'), { once: true }); const action = button.dataset.action; const siblingAction = action === 'like' ? 'dislike' : 'like'; const siblingButton = messageEntry.querySelector(`[data-action="${siblingAction}"]`); if (siblingButton) siblingButton.classList.remove('active'); } } export function resetState() { state.setGenerating(false); dom.submitButton.classList.remove('is-loading'); dom.sendIcon.classList.remove('hidden'); dom.stopIcon.classList.add('hidden'); dom.submitButton.title = 'ارسال'; dom.submitButton.disabled = false; dom.messageInput.disabled = false; dom.attachFileButton.disabled = false; state.setGlobalAbortController(null); } export function setGeneratingState(generating) { state.setGenerating(generating); dom.submitButton.disabled = !generating; if (generating) { state.setGlobalAbortController(new AbortController()); dom.submitButton.classList.add('is-loading'); dom.sendIcon.classList.add('hidden'); dom.stopIcon.classList.remove('hidden'); dom.submitButton.title = 'توقف تولید'; dom.messageInput.disabled = true; dom.attachFileButton.disabled = true; } else { resetState(); } } export function displayError(modelBubbleOuterDivElement, errorMessage) { const messageBubbleContentDiv = modelBubbleOuterDivElement.querySelector('.message-content'); const messageWrapper = modelBubbleOuterDivElement.querySelector('.group'); let oldActionsContainer = messageWrapper.querySelector('.message-actions'); if (oldActionsContainer) { oldActionsContainer.remove(); } const errorIcon = ``; messageBubbleContentDiv.innerHTML = `
${errorIcon}

${escapeHTML(errorMessage)}

`; messageBubbleContentDiv.className = 'message-content model-bubble rounded-2xl shadow-sm relative bg-red-100 dark:bg-red-800/20 border border-red-200 dark:border-red-600/30 text-red-800 dark:text-red-300'; const regenerateButtonHtml = ``; const newActionsHtml = `
${regenerateButtonHtml}
`; if (messageWrapper) { messageWrapper.insertAdjacentHTML('beforeend', newActionsHtml); } resetState(); } export function setupMobileKeyboardFix() { if ('visualViewport' in window) { const handleViewportResize = () => { const vp = window.visualViewport; document.body.style.height = `${vp.height}px`; document.body.style.top = `${vp.offsetTop}px`; dom.mainFooter.scrollIntoView({ behavior: "instant", block: "end" }); }; window.visualViewport.addEventListener('resize', handleViewportResize); handleViewportResize(); } } export function showLoadingOnButton(button, isLoading) { const spinner = button.querySelector('.animate-spin'); const textSpan = button.querySelector('span'); if (isLoading) { button.disabled = true; if(textSpan) textSpan.style.opacity = '0.5'; if(spinner) spinner.classList.remove('hidden'); } else { button.disabled = false; if(textSpan) textSpan.style.opacity = '1'; if(spinner) spinner.classList.add('hidden'); } } export function applyTheme(theme) { if (theme === 'dark') { document.documentElement.classList.add('dark'); dom.themeToggle.checked = true; } else { document.documentElement.classList.remove('dark'); dom.themeToggle.checked = false; } } export function initTheme() { const savedTheme = localStorage.getItem('theme'); if (savedTheme) { applyTheme(savedTheme); } else { const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; applyTheme(systemPrefersDark ? 'dark' : 'light'); } } export function showFreeWsLoadingIndicator(modelBubbleOuterDivElement) { const contentArea = modelBubbleOuterDivElement.querySelector('.message-content'); if (!contentArea) return; contentArea.style.padding = '1rem'; contentArea.innerHTML = `
`; } export function streamFreeWsChunk(modelBubbleOuterDivElement, fullText) { const contentArea = modelBubbleOuterDivElement.querySelector('.message-content'); if (!contentArea) return; const shouldScroll = isScrolledToBottom(); const loadingIndicator = contentArea.querySelector('.ws-loading-container'); if (loadingIndicator) { contentArea.innerHTML = ''; contentArea.classList.add('prose', 'dark:prose-invert', 'max-w-none'); } contentArea.innerHTML = DOMPurify.sanitize(marked.parse(fullText + '▍', { breaks: true, gfm: true })); contentArea.querySelectorAll('pre code').forEach(block => { hljs.highlightElement(block); }); if (shouldScroll) { requestAnimationFrame(() => { dom.chatWindow.scrollTop = dom.chatWindow.scrollHeight; }); } } export function finalizeFreeWsMessage(modelBubbleOuterDivElement, fullText) { const contentArea = modelBubbleOuterDivElement.querySelector('.message-content'); if (!contentArea) return; const shouldScroll = isScrolledToBottom(); contentArea.classList.add('prose', 'dark:prose-invert', 'max-w-none'); contentArea.style.padding = '1rem'; contentArea.innerHTML = DOMPurify.sanitize(marked.parse(fullText || " ", { breaks: true, gfm: true })); setupCodeBlockActions(modelBubbleOuterDivElement); if (shouldScroll) { requestAnimationFrame(() => { dom.chatWindow.scrollTop = dom.chatWindow.scrollHeight; }); } } // --- END OF FILE chat.js ---