|
|
|
|
|
|
|
|
|
|
|
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 = `<svg class="thinking-atom-icon w-5 h-5" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="chatbot-gradient" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#3b82f6;" /><stop offset="100%" style="stop-color:#8b5cf6;" /></linearGradient></defs><g><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="8s" repeatCount="indefinite"/><g stroke-width="8" stroke-linecap="round"><circle cx="50" cy="50" r="10" fill="url(#chatbot-gradient)" stroke="none"/><g fill="none" stroke="url(#chatbot-gradient)"><ellipse cx="50" cy="50" rx="22" ry="45"/><ellipse cx="50" cy="50" rx="45" ry="22"/></g></g></g></svg>`; |
|
|
const robotIconInBubbleSVG = `<div class="model-icon-in-bubble"><svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.5 14.5L2 12l7.5-2.5L12 2l2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5z"></path></svg></div>`; |
|
|
|
|
|
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 = ` |
|
|
<div class="thinking-header-area"> |
|
|
${robotIconInBubbleSVG} |
|
|
<div class="thinking-panel-wrapper"> |
|
|
<div class="thinking-head" onclick="toggleThinkingPanel(this)"> |
|
|
${atomIconSVG} |
|
|
<span class="thinking-label">افکار</span> |
|
|
<svg class="thinking-chevron collapsed w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /></svg> |
|
|
</div> |
|
|
<div class="thinking-body collapsed custom-scrollbar whitespace-pre-wrap"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="final-answer-wrapper"></div> |
|
|
`; |
|
|
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 = `<div class='flex flex-col'><span class='font-semibold'>${escapeHTML(fileName)}</span><div class='text-xs text-slate-500 dark:text-slate-400'>در حال آپلود... <span class='upload-progress'>0%</span></div></div>`; |
|
|
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 = `<div class='flex flex-col'><span class='font-semibold'>${icon} ${escapeHTML(fileName)}</span><span class='text-xs text-green-600 dark:text-green-500'>فایل برای ارسال آماده است.</span></div>`; |
|
|
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 = `<div class='flex flex-col'><span class='font-semibold text-red-600'>خطا در آپلود</span><span class='text-xs text-red-500'>${escapeHTML(errorMessage)}</span></div>`; |
|
|
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 = `<span class="copy-text">کپی</span>`; |
|
|
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 = `<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"></path></svg><span>اجرا</span>`; |
|
|
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 = '<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" /></svg>'; |
|
|
|
|
|
|
|
|
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 = ` |
|
|
<div class="welcome-screen"> |
|
|
<div class="welcome-container"> |
|
|
<div class="chatbot-name"></div> |
|
|
<h1 class="main-title">چطور میتوانم به شما کمک کنم؟</h1> |
|
|
|
|
|
<div class="suggestions-container"> |
|
|
<button class="suggestion-button" onclick="handleSuggestionClick('یک برنامه بنویس برای ')"> |
|
|
<span>برنامه بچین</span> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2196F3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> |
|
|
</button> |
|
|
<button class="suggestion-button" onclick="handleSuggestionClick('بهم مشاوره بده در مورد ')"> |
|
|
<span>مشاوره بده</span> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#4CAF50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10v6M2 10l10-5 10 5-10 5z"></path><path d="M6 12v5c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2v-5"></path></svg> |
|
|
</button> |
|
|
<button class="suggestion-button" onclick="handleSuggestionClick('این تصویر رو آنالیز کن')"> |
|
|
<span>آنالیز تصاویر</span> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#673AB7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg> |
|
|
</button> |
|
|
<button class="suggestion-button" onclick="handleSuggestionClick('سورپرایزم کن')"> |
|
|
<span>سورپرایزم کن</span> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#009688" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 12 20 22 4 22 4 12"></polyline><rect x="2" y="7" width="20" height="5"></rect><line x1="12" y1="22" x2="12" y2="7"></line><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path></svg> |
|
|
</button> |
|
|
<button class="suggestion-button" onclick="handleSuggestionClick('تحلیل کن ')"> |
|
|
<span>تحلیل کن</span> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#009688" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V10"></path><path d="M18 20V4"></path><path d="M6 20V16"></path></svg> |
|
|
</button> |
|
|
<button class="suggestion-button" onclick="handleSuggestionClick('کمک کن بنویسم در مورد ')"> |
|
|
<span>کمک کن بنویسم</span> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#E91E63" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"></path><path d="m19 12-7-7"></path></svg> |
|
|
</button> |
|
|
<button class="suggestion-button" onclick="handleSuggestionClick('خلاصه متن ')"> |
|
|
<span>خلاصه کن</span> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#FF9800" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6h13"></path><path d="M8 12h13"></path><path d="M8 18h13"></path><path d="M3 6h.01"></path><path d="M3 12h.01"></path><path d="M3 18h.01"></path></svg> |
|
|
</button> |
|
|
<button class="suggestion-button" onclick="handleSuggestionClick('ایده بده در مورد ')"> |
|
|
<span>ایده بده</span> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#FFC107" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 16a5 5 0 1 1 6 0a3.5 3.5 0 0 0 -1 3a2 2 0 0 1 -4 0a3.5 3.5 0 0 0 -1 -3" /><line x1="9.7" y1="17" x2="14.3" y2="17" /></svg> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div>`; |
|
|
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 = `<button data-action="copy" title="کپی" class="action-button relative"><svg class="w-4 h-4 copy-icon" fill="currentColor" viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2z"/></svg><svg class="w-4 h-4 check-icon hidden text-green-500" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg><span class="copy-feedback">کپی شد!</span></button>`; |
|
|
const menuButtonHtml = `<button data-action="show-message-menu" title="گزینههای بیشتر" class="action-button"><svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg></button>`; |
|
|
|
|
|
if (role === 'user') { |
|
|
if (textContent) { |
|
|
buttonsHtml += copyButtonHtml; |
|
|
} |
|
|
if (isLastUser && textContent) { |
|
|
buttonsHtml += `<button data-action="edit" title="ویرایش" class="action-button"><svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg></button>`; |
|
|
} |
|
|
buttonsHtml += menuButtonHtml; |
|
|
} |
|
|
|
|
|
if (role === 'assistant') { |
|
|
const hasTextContent = messageObject?.parts.some(p => p.text); |
|
|
const isClarification = !!messageObject?.clarification; |
|
|
const isGpuGuide = !!messageObject?.isGpuGuide; |
|
|
|
|
|
if (hasTextContent) { |
|
|
buttonsHtml += `<button data-action="speak" title="پخش صدا" class="action-button"> |
|
|
<svg class="w-4 h-4 speak-icon" fill="currentColor" viewBox="0 0 20 20"><path d="M8.25 3.75a.75.75 0 00-1.5 0v12.5a.75.75 0 001.5 0V3.75zM11.75 3.75a.75.75 0 00-1.5 0v12.5a.75.75 0 001.5 0V3.75zM4 6a.75.75 0 01.75.75v6.5a.75.75 0 01-1.5 0V6.75A.75.75 0 014 6zM16 6a.75.75 0 01.75.75v6.5a.75.75 0 01-1.5 0V6.75A.75.75 0 0116 6z"></path></svg> |
|
|
<svg class="w-4 h-4 pause-icon hidden" fill="currentColor" viewBox="0 0 20 20"><path d="M5.75 4.5a.75.75 0 00-.75.75v10.5a.75.75 0 001.5 0V5.25a.75.75 0 00-.75-.75zM14.25 4.5a.75.75 0 00-.75.75v10.5a.75.75 0 001.5 0V5.25a.75.75 0 00-.75-.75z"></path></svg> |
|
|
<div class="loading-spinner"></div> |
|
|
</button>`; |
|
|
buttonsHtml += copyButtonHtml; |
|
|
} |
|
|
|
|
|
if (isLastModel && !isClarification && !isGpuGuide) { |
|
|
buttonsHtml += `<button data-action="regenerate" title="تولید مجدد" class="action-button"><svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z"/></svg></button>`; |
|
|
} |
|
|
|
|
|
if (hasTextContent && !isClarification && !isGpuGuide) { |
|
|
buttonsHtml += `<button data-action="like" title="پسندیدم" class="action-button"><svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></button><button data-action="dislike" title="نپسندیدم" class="action-button"><svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14-.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41-.17-.79-.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg></button>`; |
|
|
} |
|
|
buttonsHtml += menuButtonHtml; |
|
|
} |
|
|
return buttonsHtml ? `<div class="message-actions"><div class="flex items-center gap-1.5">${buttonsHtml}</div></div>` : ''; |
|
|
} |
|
|
|
|
|
|
|
|
function createFileContentHtml(filePart) { |
|
|
const { fileUrl, mimeType, name } = filePart; |
|
|
let fileHtml = ''; |
|
|
|
|
|
if (!fileUrl) { |
|
|
return `<div class="p-3 text-red-500">خطا: فایل برای نمایش یافت نشد.</div>`; |
|
|
} |
|
|
|
|
|
if (mimeType.startsWith('image/')) { |
|
|
fileHtml = `<img src="${fileUrl}" alt="${escapeHTML(name) || 'Uploaded image'}">`; |
|
|
} else if (mimeType.startsWith('video/')) { |
|
|
fileHtml = `<video controls src="${fileUrl}"></video>`; |
|
|
} else if (mimeType.startsWith('audio/')) { |
|
|
fileHtml = `<audio controls src="${fileUrl}" class="w-full"></audio>`; |
|
|
} else { |
|
|
fileHtml = `<div class="flex items-center gap-3 p-3 bg-slate-100 dark:bg-slate-700/50 rounded-lg border border-slate-200 dark:border-slate-600 text-slate-700 dark:text-slate-200 text-sm"> |
|
|
<svg class="w-8 h-8 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m.75 12l3 3m0 0l3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg> |
|
|
<div class="flex flex-col overflow-hidden"> |
|
|
<span class="font-semibold truncate">${escapeHTML(name)}</span> |
|
|
<a href="${fileUrl}" target="_blank" rel="noopener noreferrer" class="text-xs text-blue-600 hover:underline">دانلود فایل</a> |
|
|
</div> |
|
|
</div>`; |
|
|
} |
|
|
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 (!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 = `<div class="w-9 h-9 rounded-full flex items-center justify-center flex-shrink-0 bg-blue-600 text-white"><svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"></path></svg></div>`; |
|
|
|
|
|
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 = `<div class="message-content p-4 rounded-2xl shadow-md ${bubbleClasses}"></div>`; |
|
|
|
|
|
finalElement.innerHTML = ` |
|
|
<div class="relative group w-full"> |
|
|
${messageBubbleHTML} |
|
|
</div> |
|
|
${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 => `<div class="whitespace-pre-wrap">${escapeHTML(p.text)}</div>`).join(''); |
|
|
|
|
|
if (processedFileParts.length > 0 && textParts.length > 0) { |
|
|
contentArea.classList.add('user-bubble-multipart'); |
|
|
contentArea.innerHTML = `<div class="user-file-part">${fileHtml}</div><div class="user-text-part">${textHtml}</div>`; |
|
|
} 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 = ` |
|
|
<div class="relative group w-full"> |
|
|
<div class="message-content w-full rounded-2xl shadow-md bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700"> |
|
|
<div class="p-4 flex flex-col items-center text-center"> |
|
|
<svg class="w-12 h-12 text-orange-400 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> |
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /> |
|
|
</svg> |
|
|
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-2">محدودیت استفاده رایگان</h3> |
|
|
<p class="text-slate-600 dark:text-slate-300 mb-6 text-sm">${message} برای ادامه استفاده نامحدود، حساب خود را ارتقا دهید.</p> |
|
|
<button id="limit-upgrade-btn" class="beautiful-upgrade-btn"> |
|
|
✨ ارتقا به نسخه کامل |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
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 = `<div class="border-t border-slate-200 dark:border-slate-700 mt-4 p-4 prose dark:prose-invert max-w-none"></div>`; |
|
|
} |
|
|
|
|
|
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 = `<div class="p-4 prose dark:prose-invert max-w-none"></div>`; |
|
|
} |
|
|
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 = `<svg class="w-6 h-6 mr-3 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"></path></svg>`; |
|
|
messageBubbleContentDiv.innerHTML = `<div class="p-4 flex items-center">${errorIcon}<p class="whitespace-pre-wrap">${escapeHTML(errorMessage)}</p></div>`; |
|
|
|
|
|
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 = `<button data-action="regenerate" title="تلاش مجدد" class="action-button"><svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z"/></path></svg></button>`; |
|
|
const newActionsHtml = `<div class="message-actions"><div class="flex items-center gap-1.5">${regenerateButtonHtml}</div></div>`; |
|
|
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 = `<div class="ws-loading-container"> |
|
|
<div class="dots"> |
|
|
<div class="dot"></div> |
|
|
<div class="dot"></div> |
|
|
<div class="dot"></div> |
|
|
</div> |
|
|
</div>`; |
|
|
} |
|
|
|
|
|
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; |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|