Chat / static /js /ui /chat.js
Jan2000's picture
Update chat.js
24d2009 unverified
raw
history blame
42.4 kB
// --- 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 = `<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;
});
}
}
// --- END OF FILE chat.js ---