| |
|
|
| import * as state from '../state.js'; |
| import * as db from '../db.js'; |
| import { dom } from './dom.js'; |
| import { toggleHtmlPreviewModal, toggleSidebar } 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) => { |
| import('./modals.js').then(modals => modals.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; |
| }); |
| } |
| } |
|
|