Create chat.js
Browse files- static/js/ui/chat.js +752 -0
static/js/ui/chat.js
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/ui/chat.js
|
| 2 |
+
|
| 3 |
+
import * as state from '../state.js';
|
| 4 |
+
import * as db from '../db.js';
|
| 5 |
+
import { dom } from './dom.js';
|
| 6 |
+
import { toggleHtmlPreviewModal, toggleSidebar } from './modals.js';
|
| 7 |
+
import { createDeepThinkPanel, createReasoningPanel, hideDeepThinkPanel, hideReasoningPanel, updateDeepThinkPanel, updateReasoningPanel } from './tools.js';
|
| 8 |
+
|
| 9 |
+
export const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6InZSVUdlLzBlR0FzOHZJdXFZeWhER0E9PSIsInZhbHVlIjoiWFhqRXBLc29vSFpHdk9nYmRjZGVuWHRHRHVSZHRlTG1BUENLaE5mNXBNVVRGWFg3ZWN0djJ5K1dIY1RqTHJGaCIsIm1hYyI6IjIzYzFlZTMwYmVmMTdkYjQ0YTQ4YWMxNmFhN2RmNWQ2OTc1NDIyNGVlZGI3ZjJjMjhkNmQxNjM4MDFlZTIxNmUiLCJ0YWciOiIifQ==/20934991';
|
| 10 |
+
|
| 11 |
+
const MAX_TEXTAREA_HEIGHT = 150;
|
| 12 |
+
export let minTextareaHeight = 0;
|
| 13 |
+
|
| 14 |
+
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>`;
|
| 15 |
+
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>`;
|
| 16 |
+
|
| 17 |
+
window.toggleThinkingPanel = function(headElement) {
|
| 18 |
+
const wrapper = headElement.closest('.thinking-panel-wrapper');
|
| 19 |
+
const body = wrapper.querySelector('.thinking-body');
|
| 20 |
+
const chevron = headElement.querySelector('.thinking-chevron');
|
| 21 |
+
if (body && chevron) {
|
| 22 |
+
body.classList.toggle('collapsed');
|
| 23 |
+
chevron.classList.toggle('collapsed');
|
| 24 |
+
}
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
export function startThinking(modelBubbleOuterDivElement) {
|
| 28 |
+
const contentArea = modelBubbleOuterDivElement?.querySelector('.message-content');
|
| 29 |
+
if (!contentArea) return;
|
| 30 |
+
|
| 31 |
+
const modelContent = `
|
| 32 |
+
<div class="thinking-header-area">
|
| 33 |
+
${robotIconInBubbleSVG}
|
| 34 |
+
<div class="thinking-panel-wrapper">
|
| 35 |
+
<div class="thinking-head" onclick="toggleThinkingPanel(this)">
|
| 36 |
+
${atomIconSVG}
|
| 37 |
+
<span class="thinking-label">افکار</span>
|
| 38 |
+
<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>
|
| 39 |
+
</div>
|
| 40 |
+
<div class="thinking-body collapsed custom-scrollbar whitespace-pre-wrap"></div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
<div class="final-answer-wrapper"></div>
|
| 44 |
+
`;
|
| 45 |
+
contentArea.innerHTML = modelContent;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export function streamThought(text, modelBubbleOuterDivElement) {
|
| 49 |
+
const thinkingBody = modelBubbleOuterDivElement.querySelector('.thinking-body');
|
| 50 |
+
if (!thinkingBody) return;
|
| 51 |
+
|
| 52 |
+
const existingContent = thinkingBody.innerHTML;
|
| 53 |
+
const newContent = DOMPurify.sanitize(marked.parse(thinkingBody.textContent + text, { breaks: true, gfm: true }));
|
| 54 |
+
thinkingBody.innerHTML = newContent;
|
| 55 |
+
|
| 56 |
+
thinkingBody.scrollTop = thinkingBody.scrollHeight;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
function isScrolledToBottom() {
|
| 60 |
+
const { chatWindow } = dom;
|
| 61 |
+
const scrollThreshold = 15;
|
| 62 |
+
return chatWindow.scrollHeight - chatWindow.clientHeight <= chatWindow.scrollTop + scrollThreshold;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
export function escapeHTML(str) {
|
| 66 |
+
const p = document.createElement("p");
|
| 67 |
+
p.textContent = str;
|
| 68 |
+
return p.innerHTML;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function getFileIcon(mimeType) {
|
| 72 |
+
if (mimeType.startsWith('image/')) return '🖼️';
|
| 73 |
+
if (mimeType.startsWith('video/')) return '🎬';
|
| 74 |
+
if (mimeType.startsWith('audio/')) return '🎵';
|
| 75 |
+
if (mimeType.startsWith('application/pdf')) return '📄';
|
| 76 |
+
if (mimeType.startsWith('text/')) return '📝';
|
| 77 |
+
return '📁';
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
export function hideFilePreview() {
|
| 81 |
+
dom.imagePreviewContainer.classList.add('hidden');
|
| 82 |
+
dom.imagePreview.src = '';
|
| 83 |
+
dom.fileInfoText.innerHTML = '';
|
| 84 |
+
dom.imageFileInput.value = '';
|
| 85 |
+
dom.generalFileInput.value = '';
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
export function showFileUploading(fileName) {
|
| 89 |
+
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`;
|
| 90 |
+
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>`;
|
| 91 |
+
dom.imagePreviewContainer.classList.remove('hidden');
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
export function showFileReady(fileName, mimeType, url) {
|
| 95 |
+
const icon = getFileIcon(mimeType);
|
| 96 |
+
if(mimeType.startsWith('image/')) {
|
| 97 |
+
dom.imagePreview.src = url;
|
| 98 |
+
} else {
|
| 99 |
+
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`;
|
| 100 |
+
}
|
| 101 |
+
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>`;
|
| 102 |
+
dom.imagePreviewContainer.classList.remove('hidden');
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
export function showFileError(errorMessage) {
|
| 106 |
+
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`;
|
| 107 |
+
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>`;
|
| 108 |
+
dom.imagePreviewContainer.classList.remove('hidden');
|
| 109 |
+
setTimeout(hideFilePreview, 5000);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
export function handleSuggestionClick(text) {
|
| 113 |
+
dom.messageInput.value = text;
|
| 114 |
+
dom.messageInput.dispatchEvent(new Event('input', { bubbles: true }));
|
| 115 |
+
dom.messageInput.focus();
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
export function runWelcomeAnimation() {
|
| 119 |
+
const chatbotNameContainer = document.querySelector('.chatbot-name');
|
| 120 |
+
const mainTitle = document.querySelector('.main-title');
|
| 121 |
+
const suggestionsContainer = document.querySelector('.suggestions-container');
|
| 122 |
+
if (!chatbotNameContainer || !mainTitle || !suggestionsContainer) return;
|
| 123 |
+
|
| 124 |
+
const textToType = "چت بات آلفا";
|
| 125 |
+
let charIndex = 0;
|
| 126 |
+
const typingSpeed = 90;
|
| 127 |
+
|
| 128 |
+
function typeChatbotName() {
|
| 129 |
+
if (charIndex < textToType.length) {
|
| 130 |
+
chatbotNameContainer.textContent += textToType.charAt(charIndex);
|
| 131 |
+
charIndex++;
|
| 132 |
+
setTimeout(typeChatbotName, typingSpeed);
|
| 133 |
+
} else {
|
| 134 |
+
chatbotNameContainer.style.opacity = '1';
|
| 135 |
+
setTimeout(() => { mainTitle.style.opacity = '1'; }, 300);
|
| 136 |
+
setTimeout(() => { suggestionsContainer.style.opacity = '1'; suggestionsContainer.style.transform = 'translateY(0)'; }, 600);
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
chatbotNameContainer.textContent = '';
|
| 140 |
+
typeChatbotName();
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
export function setupCodeBlockActions(container) {
|
| 144 |
+
container.querySelectorAll('pre').forEach(preElement => {
|
| 145 |
+
preElement.setAttribute('dir', 'ltr');
|
| 146 |
+
|
| 147 |
+
if (preElement.querySelector('.code-button-container')) return;
|
| 148 |
+
const codeElement = preElement.querySelector('code');
|
| 149 |
+
if (!codeElement) return;
|
| 150 |
+
|
| 151 |
+
hljs.highlightElement(codeElement);
|
| 152 |
+
|
| 153 |
+
const buttonContainer = document.createElement('div');
|
| 154 |
+
buttonContainer.className = 'code-button-container';
|
| 155 |
+
|
| 156 |
+
const copyButton = document.createElement('button');
|
| 157 |
+
copyButton.className = 'code-button';
|
| 158 |
+
copyButton.innerHTML = `<span class="copy-text">کپی</span>`;
|
| 159 |
+
const copyTextSpan = copyButton.querySelector('.copy-text');
|
| 160 |
+
|
| 161 |
+
copyButton.onclick = () => {
|
| 162 |
+
navigator.clipboard.writeText(codeElement.innerText).then(() => {
|
| 163 |
+
copyTextSpan.textContent = 'کپی شد!';
|
| 164 |
+
copyButton.style.backgroundColor = '#4CAF50';
|
| 165 |
+
setTimeout(() => {
|
| 166 |
+
copyTextSpan.textContent = 'کپی';
|
| 167 |
+
copyButton.style.backgroundColor = '';
|
| 168 |
+
}, 2000);
|
| 169 |
+
});
|
| 170 |
+
};
|
| 171 |
+
buttonContainer.appendChild(copyButton);
|
| 172 |
+
|
| 173 |
+
const languageClass = Array.from(codeElement.classList).find(cls => cls.startsWith('language-'));
|
| 174 |
+
if (languageClass === 'language-html') {
|
| 175 |
+
const runButton = document.createElement('button');
|
| 176 |
+
runButton.className = 'code-button';
|
| 177 |
+
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>`;
|
| 178 |
+
runButton.onclick = () => { toggleHtmlPreviewModal(true, codeElement.innerText); };
|
| 179 |
+
buttonContainer.appendChild(runButton);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
preElement.appendChild(buttonContainer);
|
| 183 |
+
});
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
export function renderHistoryList() {
|
| 187 |
+
dom.historyList.innerHTML = '';
|
| 188 |
+
const chatsToDisplay = state.chatSessions.filter(session => session.messages.length > 0 || session.id === state.activeChatId);
|
| 189 |
+
if (chatsToDisplay.length > 0) {
|
| 190 |
+
chatsToDisplay.forEach((session) => {
|
| 191 |
+
const itemContainer = document.createElement('div');
|
| 192 |
+
itemContainer.className = 'history-item flex items-center justify-between rounded-lg';
|
| 193 |
+
const itemLink = document.createElement('a');
|
| 194 |
+
itemLink.href = '#';
|
| 195 |
+
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'}`;
|
| 196 |
+
itemLink.textContent = session.title;
|
| 197 |
+
itemLink.onclick = (e) => { e.preventDefault(); state.setActiveChatId(session.id); renderActiveChat(); renderHistoryList(); toggleSidebar(false); };
|
| 198 |
+
const menuButton = document.createElement('button');
|
| 199 |
+
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';
|
| 200 |
+
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>';
|
| 201 |
+
menuButton.onclick = (e) => {
|
| 202 |
+
import('./modals.js').then(modals => modals.showHistoryMenu(e, session.id));
|
| 203 |
+
};
|
| 204 |
+
itemContainer.appendChild(itemLink);
|
| 205 |
+
itemContainer.appendChild(menuButton);
|
| 206 |
+
dom.historyList.appendChild(itemContainer);
|
| 207 |
+
});
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
export async function renderActiveChat() {
|
| 212 |
+
dom.chatWindow.innerHTML = '';
|
| 213 |
+
const activeChat = state.getActiveChat();
|
| 214 |
+
|
| 215 |
+
if (activeChat && activeChat.messages.length === 0) {
|
| 216 |
+
dom.chatWindow.innerHTML = `
|
| 217 |
+
<div class="welcome-screen">
|
| 218 |
+
<div class="welcome-container">
|
| 219 |
+
<div class="chatbot-name"></div>
|
| 220 |
+
<h1 class="main-title">چطور میتوانم به شما کمک کنم؟</h1>
|
| 221 |
+
|
| 222 |
+
<div class="suggestions-container">
|
| 223 |
+
<button class="suggestion-button" onclick="handleSuggestionClick('یک برنامه بنویس برای ')">
|
| 224 |
+
<span>برنامه بچین</span>
|
| 225 |
+
<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>
|
| 226 |
+
</button>
|
| 227 |
+
<button class="suggestion-button" onclick="handleSuggestionClick('بهم مشاوره بده در مورد ')">
|
| 228 |
+
<span>مشاوره بده</span>
|
| 229 |
+
<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>
|
| 230 |
+
</button>
|
| 231 |
+
<button class="suggestion-button" onclick="handleSuggestionClick('این تصویر رو آنالیز کن')">
|
| 232 |
+
<span>آنالیز تصاویر</span>
|
| 233 |
+
<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>
|
| 234 |
+
</button>
|
| 235 |
+
<button class="suggestion-button" onclick="handleSuggestionClick('سورپرایزم کن')">
|
| 236 |
+
<span>سورپرایزم کن</span>
|
| 237 |
+
<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>
|
| 238 |
+
</button>
|
| 239 |
+
<button class="suggestion-button" onclick="handleSuggestionClick('تحلیل کن ')">
|
| 240 |
+
<span>تحلیل کن</span>
|
| 241 |
+
<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>
|
| 242 |
+
</button>
|
| 243 |
+
<button class="suggestion-button" onclick="handleSuggestionClick('کمک کن بنویسم در مورد ')">
|
| 244 |
+
<span>کمک کن بنویسم</span>
|
| 245 |
+
<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>
|
| 246 |
+
</button>
|
| 247 |
+
<button class="suggestion-button" onclick="handleSuggestionClick('خلاصه متن ')">
|
| 248 |
+
<span>خلاصه کن</span>
|
| 249 |
+
<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>
|
| 250 |
+
</button>
|
| 251 |
+
<button class="suggestion-button" onclick="handleSuggestionClick('ایده بده در مورد ')">
|
| 252 |
+
<span>ایده بده</span>
|
| 253 |
+
<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>
|
| 254 |
+
</button>
|
| 255 |
+
</div>
|
| 256 |
+
</div>
|
| 257 |
+
</div>`;
|
| 258 |
+
runWelcomeAnimation();
|
| 259 |
+
} else if (activeChat && activeChat.messages.length > 0) {
|
| 260 |
+
const lastMessageIndex = activeChat.messages.length - 1;
|
| 261 |
+
const lastUserMessageIndex = state.findLastIndex(activeChat.messages, msg => msg.role === 'user');
|
| 262 |
+
|
| 263 |
+
for (const [index, msg] of activeChat.messages.entries()) {
|
| 264 |
+
if (msg.isTemporary) continue;
|
| 265 |
+
const isLastUser = (index === lastUserMessageIndex);
|
| 266 |
+
const isLastModel = (index === lastMessageIndex && msg.role === 'assistant');
|
| 267 |
+
await addMessageToUI(msg, index, { isLastUser, isLastModel, animate: false });
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
requestAnimationFrame(() => { dom.chatWindow.scrollTop = dom.chatWindow.scrollHeight; });
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
export function createMessageActionsHtml(options) {
|
| 275 |
+
const { role, isLastUser, isLastModel, messageObject } = options;
|
| 276 |
+
let buttonsHtml = '';
|
| 277 |
+
const textContent = messageObject?.parts.find(p => p.text)?.text;
|
| 278 |
+
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>`;
|
| 279 |
+
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>`;
|
| 280 |
+
|
| 281 |
+
if (role === 'user') {
|
| 282 |
+
if (textContent) {
|
| 283 |
+
buttonsHtml += copyButtonHtml;
|
| 284 |
+
}
|
| 285 |
+
if (isLastUser && textContent) {
|
| 286 |
+
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>`;
|
| 287 |
+
}
|
| 288 |
+
buttonsHtml += menuButtonHtml;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
if (role === 'assistant') {
|
| 292 |
+
const hasTextContent = messageObject?.parts.some(p => p.text);
|
| 293 |
+
const isClarification = !!messageObject?.clarification;
|
| 294 |
+
const isGpuGuide = !!messageObject?.isGpuGuide;
|
| 295 |
+
|
| 296 |
+
if (hasTextContent) {
|
| 297 |
+
buttonsHtml += `<button data-action="speak" title="پخش صدا" class="action-button">
|
| 298 |
+
<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>
|
| 299 |
+
<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>
|
| 300 |
+
<div class="loading-spinner"></div>
|
| 301 |
+
</button>`;
|
| 302 |
+
buttonsHtml += copyButtonHtml;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
if (isLastModel && !isClarification && !isGpuGuide) {
|
| 306 |
+
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>`;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
if (hasTextContent && !isClarification && !isGpuGuide) {
|
| 310 |
+
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>`;
|
| 311 |
+
}
|
| 312 |
+
buttonsHtml += menuButtonHtml;
|
| 313 |
+
}
|
| 314 |
+
return buttonsHtml ? `<div class="message-actions"><div class="flex items-center gap-1.5">${buttonsHtml}</div></div>` : '';
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
function createFileContentHtml(filePart) {
|
| 319 |
+
const { fileUrl, mimeType, name } = filePart;
|
| 320 |
+
let fileHtml = '';
|
| 321 |
+
|
| 322 |
+
if (!fileUrl) {
|
| 323 |
+
return `<div class="p-3 text-red-500">خطا: فایل برای نمایش یافت نشد.</div>`;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
if (mimeType.startsWith('image/')) {
|
| 327 |
+
fileHtml = `<img src="${fileUrl}" alt="${escapeHTML(name) || 'Uploaded image'}">`;
|
| 328 |
+
} else if (mimeType.startsWith('video/')) {
|
| 329 |
+
fileHtml = `<video controls src="${fileUrl}"></video>`;
|
| 330 |
+
} else if (mimeType.startsWith('audio/')) {
|
| 331 |
+
fileHtml = `<audio controls src="${fileUrl}" class="w-full"></audio>`;
|
| 332 |
+
} else {
|
| 333 |
+
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">
|
| 334 |
+
<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>
|
| 335 |
+
<div class="flex flex-col overflow-hidden">
|
| 336 |
+
<span class="font-semibold truncate">${escapeHTML(name)}</span>
|
| 337 |
+
<a href="${fileUrl}" target="_blank" rel="noopener noreferrer" class="text-xs text-blue-600 hover:underline">دانلود فایل</a>
|
| 338 |
+
</div>
|
| 339 |
+
</div>`;
|
| 340 |
+
}
|
| 341 |
+
return fileHtml;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
export async function addMessageToUI(message, index, options = {}, existingElement = null) {
|
| 345 |
+
const { role, parts } = message;
|
| 346 |
+
const { isLastUser = false, isLastModel = false, animate = true } = options;
|
| 347 |
+
const isUser = role === 'user';
|
| 348 |
+
|
| 349 |
+
let finalElement = existingElement;
|
| 350 |
+
|
| 351 |
+
if (!finalElement) {
|
| 352 |
+
finalElement = document.createElement('div');
|
| 353 |
+
const roleClass = isUser ? 'user' : 'model';
|
| 354 |
+
finalElement.className = `message-entry ${roleClass} mb-6 flex items-end gap-3 ${isUser ? 'justify-end' : 'justify-start'}`;
|
| 355 |
+
finalElement.dataset.index = index;
|
| 356 |
+
if (animate) finalElement.classList.add('message-entry');
|
| 357 |
+
|
| 358 |
+
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>`;
|
| 359 |
+
|
| 360 |
+
const bubbleClasses = isUser
|
| 361 |
+
? 'bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-br-none'
|
| 362 |
+
: 'bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-800 dark:text-slate-200 rounded-bl-none';
|
| 363 |
+
|
| 364 |
+
const messageBubbleHTML = `<div class="message-content p-4 rounded-2xl shadow-md ${bubbleClasses}"></div>`;
|
| 365 |
+
|
| 366 |
+
finalElement.innerHTML = `
|
| 367 |
+
<div class="relative group w-full">
|
| 368 |
+
${messageBubbleHTML}
|
| 369 |
+
</div>
|
| 370 |
+
${isUser ? userIcon : ''}
|
| 371 |
+
`;
|
| 372 |
+
dom.chatWindow.appendChild(finalElement);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
const contentArea = finalElement.querySelector('.message-content');
|
| 376 |
+
|
| 377 |
+
if (!isUser) {
|
| 378 |
+
contentArea.classList.add('model-bubble');
|
| 379 |
+
contentArea.style.padding = '0';
|
| 380 |
+
} else {
|
| 381 |
+
contentArea.innerHTML = '';
|
| 382 |
+
contentArea.style.padding = '1rem';
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
if (isUser) {
|
| 386 |
+
const textParts = parts.filter(p => p.text);
|
| 387 |
+
const fileParts = parts.filter(p => p.id);
|
| 388 |
+
|
| 389 |
+
// *** START: MODIFIED - منطق جدید و اصلاح شده برای نمایش فایل ***
|
| 390 |
+
const processedFileParts = await Promise.all(
|
| 391 |
+
fileParts.map(async (part) => {
|
| 392 |
+
// همیشه فایل را از IndexedDB بر اساس ID میخوانیم تا URL تازه بسازیم
|
| 393 |
+
if (part.id) {
|
| 394 |
+
try {
|
| 395 |
+
const file = await db.getFile(part.id);
|
| 396 |
+
if (file) {
|
| 397 |
+
const newBlobUrl = URL.createObjectURL(file);
|
| 398 |
+
// یک آبجکت جدید با URL تازه برای رندر برمیگردانیم
|
| 399 |
+
return { ...part, fileUrl: newBlobUrl, mimeType: file.type, name: file.name };
|
| 400 |
+
}
|
| 401 |
+
} catch (error) {
|
| 402 |
+
console.error(`Error retrieving file ${part.id} from DB:`, error);
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
// در صورت خطا یا نبودن ID، پارت اصلی را برمیگردانیم تا پیام خطا نمایش داده شود
|
| 406 |
+
return { ...part, fileUrl: null };
|
| 407 |
+
})
|
| 408 |
+
);
|
| 409 |
+
// *** END: MODIFIED ***
|
| 410 |
+
|
| 411 |
+
const fileHtml = processedFileParts.map(p => createFileContentHtml(p)).join('');
|
| 412 |
+
const textHtml = textParts.map(p => `<div class="whitespace-pre-wrap">${escapeHTML(p.text)}</div>`).join('');
|
| 413 |
+
|
| 414 |
+
if (processedFileParts.length > 0 && textParts.length > 0) {
|
| 415 |
+
contentArea.classList.add('user-bubble-multipart');
|
| 416 |
+
contentArea.innerHTML = `<div class="user-file-part">${fileHtml}</div><div class="user-text-part">${textHtml}</div>`;
|
| 417 |
+
} else {
|
| 418 |
+
contentArea.classList.remove('user-bubble-multipart');
|
| 419 |
+
contentArea.innerHTML = fileHtml + textHtml;
|
| 420 |
+
if (processedFileParts.length === 1 && (processedFileParts[0].mimeType?.startsWith('image/') || processedFileParts[0].mimeType?.startsWith('video/'))) {
|
| 421 |
+
contentArea.style.padding = '0';
|
| 422 |
+
const filePartElement = contentArea.querySelector('.user-file-part');
|
| 423 |
+
if (filePartElement) filePartElement.classList.add('single');
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
} else if (message.isTemporary) {
|
| 428 |
+
const activeTool = state.getActiveTool();
|
| 429 |
+
|
| 430 |
+
if (activeTool === 'deep-think') {
|
| 431 |
+
createDeepThinkPanel(finalElement);
|
| 432 |
+
} else if (activeTool === 'reasoning') {
|
| 433 |
+
createReasoningPanel(finalElement);
|
| 434 |
+
} else {
|
| 435 |
+
const activeChat = state.getActiveChat();
|
| 436 |
+
if (activeChat && activeChat.showThoughts) {
|
| 437 |
+
startThinking(finalElement);
|
| 438 |
+
} else {
|
| 439 |
+
showFreeWsLoadingIndicator(finalElement);
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
} else {
|
| 443 |
+
const allContent = parts?.filter(p => p.text).map(p => p.text).join('') || '';
|
| 444 |
+
|
| 445 |
+
if (message.toolUsed === 'deep-think') {
|
| 446 |
+
createDeepThinkPanel(finalElement);
|
| 447 |
+
hideDeepThinkPanel(finalElement);
|
| 448 |
+
finalizeFinalText(finalElement, allContent);
|
| 449 |
+
} else if (message.toolUsed === 'reasoning') {
|
| 450 |
+
createReasoningPanel(finalElement);
|
| 451 |
+
hideReasoningPanel(finalElement);
|
| 452 |
+
finalizeFinalText(finalElement, allContent);
|
| 453 |
+
} else if (message.wasGeneratedWithThoughts) {
|
| 454 |
+
startThinking(finalElement);
|
| 455 |
+
finalizeFinalText(finalElement, allContent);
|
| 456 |
+
} else {
|
| 457 |
+
finalizeFreeWsMessage(finalElement, allContent);
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
updateMessageActions(finalElement, message, isLastUser, isLastModel);
|
| 462 |
+
|
| 463 |
+
if (!existingElement && animate) {
|
| 464 |
+
finalElement.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
| 465 |
+
}
|
| 466 |
+
return finalElement;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
export function showLimitReachedUpgrade() {
|
| 470 |
+
const message = "محدودیت پیامهای روزانه شما به پایان رسیده است.";
|
| 471 |
+
const modelBubbleOuterDivElement = document.createElement('div');
|
| 472 |
+
modelBubbleOuterDivElement.className = 'message-entry model mb-6 flex items-end gap-3 justify-start';
|
| 473 |
+
modelBubbleOuterDivElement.style.animation = 'fade-slide-in 300ms ease-out forwards';
|
| 474 |
+
|
| 475 |
+
const limitReachedHTML = `
|
| 476 |
+
<div class="relative group w-full">
|
| 477 |
+
<div class="message-content w-full rounded-2xl shadow-md bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
|
| 478 |
+
<div class="p-4 flex flex-col items-center text-center">
|
| 479 |
+
<svg class="w-12 h-12 text-orange-400 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
| 480 |
+
<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" />
|
| 481 |
+
</svg>
|
| 482 |
+
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-2">محدودیت استفاده رایگان</h3>
|
| 483 |
+
<p class="text-slate-600 dark:text-slate-300 mb-6 text-sm">${message} برای ادامه استفاده نامحدود، حساب خود را ارتقا دهید.</p>
|
| 484 |
+
<button id="limit-upgrade-btn" class="beautiful-upgrade-btn">
|
| 485 |
+
✨ ارتقا به نسخه کامل
|
| 486 |
+
</button>
|
| 487 |
+
</div>
|
| 488 |
+
</div>
|
| 489 |
+
</div>
|
| 490 |
+
`;
|
| 491 |
+
modelBubbleOuterDivElement.innerHTML = limitReachedHTML;
|
| 492 |
+
dom.chatWindow.appendChild(modelBubbleOuterDivElement);
|
| 493 |
+
const upgradeButton = modelBubbleOuterDivElement.querySelector('#limit-upgrade-btn');
|
| 494 |
+
if (upgradeButton) {
|
| 495 |
+
upgradeButton.addEventListener('click', () => {
|
| 496 |
+
parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*');
|
| 497 |
+
});
|
| 498 |
+
}
|
| 499 |
+
modelBubbleOuterDivElement.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
export function streamFinalText(text, modelBubbleOuterDivElement) {
|
| 503 |
+
const finalAnswerWrapper = modelBubbleOuterDivElement.querySelector('.final-answer-wrapper');
|
| 504 |
+
if (!finalAnswerWrapper) return;
|
| 505 |
+
|
| 506 |
+
if (!finalAnswerWrapper.classList.contains('visible')) {
|
| 507 |
+
finalAnswerWrapper.classList.add('visible');
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
const shouldScroll = isScrolledToBottom();
|
| 511 |
+
|
| 512 |
+
if (finalAnswerWrapper.innerHTML.trim() === '') {
|
| 513 |
+
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>`;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
const contentContainer = finalAnswerWrapper.querySelector('.p-4');
|
| 517 |
+
if (!contentContainer) return;
|
| 518 |
+
|
| 519 |
+
const content = DOMPurify.sanitize(marked.parse(text + '▍' || " ", { breaks: true, gfm: true }));
|
| 520 |
+
contentContainer.innerHTML = content;
|
| 521 |
+
|
| 522 |
+
contentContainer.querySelectorAll('pre code').forEach(block => {
|
| 523 |
+
hljs.highlightElement(block);
|
| 524 |
+
});
|
| 525 |
+
|
| 526 |
+
if (shouldScroll) {
|
| 527 |
+
requestAnimationFrame(() => {
|
| 528 |
+
dom.chatWindow.scrollTop = dom.chatWindow.scrollHeight;
|
| 529 |
+
});
|
| 530 |
+
}
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
export function finalizeFinalText(modelBubbleOuterDivElement, fullText) {
|
| 534 |
+
const finalAnswerWrapper = modelBubbleOuterDivElement.querySelector('.final-answer-wrapper');
|
| 535 |
+
if (!finalAnswerWrapper) return;
|
| 536 |
+
|
| 537 |
+
const shouldScroll = isScrolledToBottom();
|
| 538 |
+
|
| 539 |
+
if (!finalAnswerWrapper.classList.contains('visible')) {
|
| 540 |
+
finalAnswerWrapper.classList.add('visible');
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
if (finalAnswerWrapper.innerHTML.trim() === '') {
|
| 544 |
+
finalAnswerWrapper.innerHTML = `<div class="p-4 prose dark:prose-invert max-w-none"></div>`;
|
| 545 |
+
}
|
| 546 |
+
const contentContainer = finalAnswerWrapper.querySelector('.p-4');
|
| 547 |
+
|
| 548 |
+
const content = DOMPurify.sanitize(marked.parse(fullText || " ", { breaks: true, gfm: true }));
|
| 549 |
+
contentContainer.innerHTML = content;
|
| 550 |
+
|
| 551 |
+
setupCodeBlockActions(finalAnswerWrapper);
|
| 552 |
+
|
| 553 |
+
if (shouldScroll) {
|
| 554 |
+
requestAnimationFrame(() => {
|
| 555 |
+
dom.chatWindow.scrollTop = dom.chatWindow.scrollHeight;
|
| 556 |
+
});
|
| 557 |
+
}
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
export function updateMessageActions(messageOuterDivElement, messageObject, isLastUser, isLastModel) {
|
| 561 |
+
const messageWrapper = messageOuterDivElement.querySelector('.group');
|
| 562 |
+
if (!messageWrapper) return;
|
| 563 |
+
let oldActionsContainer = messageWrapper.querySelector('.message-actions');
|
| 564 |
+
if (oldActionsContainer) { oldActionsContainer.remove(); }
|
| 565 |
+
const newActionsHtml = createMessageActionsHtml({ role: messageObject.role, isLastUser: isLastUser, isLastModel: isLastModel, messageObject: messageObject });
|
| 566 |
+
if (newActionsHtml) { messageWrapper.insertAdjacentHTML('beforeend', newActionsHtml); }
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
export function adjustTextareaHeight(el) {
|
| 570 |
+
el.style.height = 'auto';
|
| 571 |
+
el.style.height = `${el.scrollHeight}px`;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
export function showCopyFeedback(button) {
|
| 575 |
+
const copyIcon = button.querySelector('.copy-icon');
|
| 576 |
+
const checkIcon = button.querySelector('.check-icon');
|
| 577 |
+
const feedback = button.querySelector('.copy-feedback');
|
| 578 |
+
if (copyIcon && checkIcon && feedback) {
|
| 579 |
+
copyIcon.classList.add('hidden');
|
| 580 |
+
checkIcon.classList.remove('hidden');
|
| 581 |
+
feedback.classList.add('visible');
|
| 582 |
+
setTimeout(() => {
|
| 583 |
+
copyIcon.classList.remove('hidden');
|
| 584 |
+
checkIcon.classList.add('hidden');
|
| 585 |
+
feedback.classList.remove('visible');
|
| 586 |
+
}, 2000);
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
export function handleLikeDislike(button, messageEntry) {
|
| 591 |
+
const isActive = button.classList.toggle('active');
|
| 592 |
+
if (isActive) {
|
| 593 |
+
button.classList.add('like-animation');
|
| 594 |
+
button.addEventListener('animationend', () => button.classList.remove('like-animation'), { once: true });
|
| 595 |
+
const action = button.dataset.action;
|
| 596 |
+
const siblingAction = action === 'like' ? 'dislike' : 'like';
|
| 597 |
+
const siblingButton = messageEntry.querySelector(`[data-action="${siblingAction}"]`);
|
| 598 |
+
if (siblingButton) siblingButton.classList.remove('active');
|
| 599 |
+
}
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
export function resetState() {
|
| 603 |
+
state.setGenerating(false);
|
| 604 |
+
dom.submitButton.classList.remove('is-loading');
|
| 605 |
+
dom.sendIcon.classList.remove('hidden');
|
| 606 |
+
dom.stopIcon.classList.add('hidden');
|
| 607 |
+
dom.submitButton.title = 'ارسال';
|
| 608 |
+
dom.submitButton.disabled = false;
|
| 609 |
+
dom.messageInput.disabled = false;
|
| 610 |
+
dom.attachFileButton.disabled = false;
|
| 611 |
+
state.setGlobalAbortController(null);
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
export function setGeneratingState(generating) {
|
| 615 |
+
state.setGenerating(generating);
|
| 616 |
+
dom.submitButton.disabled = !generating;
|
| 617 |
+
if (generating) {
|
| 618 |
+
state.setGlobalAbortController(new AbortController());
|
| 619 |
+
dom.submitButton.classList.add('is-loading');
|
| 620 |
+
dom.sendIcon.classList.add('hidden');
|
| 621 |
+
dom.stopIcon.classList.remove('hidden');
|
| 622 |
+
dom.submitButton.title = 'توقف تولید';
|
| 623 |
+
dom.messageInput.disabled = true;
|
| 624 |
+
dom.attachFileButton.disabled = true;
|
| 625 |
+
} else {
|
| 626 |
+
resetState();
|
| 627 |
+
}
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
export function displayError(modelBubbleOuterDivElement, errorMessage) {
|
| 631 |
+
const messageBubbleContentDiv = modelBubbleOuterDivElement.querySelector('.message-content');
|
| 632 |
+
const messageWrapper = modelBubbleOuterDivElement.querySelector('.group');
|
| 633 |
+
|
| 634 |
+
let oldActionsContainer = messageWrapper.querySelector('.message-actions');
|
| 635 |
+
if (oldActionsContainer) { oldActionsContainer.remove(); }
|
| 636 |
+
|
| 637 |
+
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>`;
|
| 638 |
+
messageBubbleContentDiv.innerHTML = `<div class="p-4 flex items-center">${errorIcon}<p class="whitespace-pre-wrap">${escapeHTML(errorMessage)}</p></div>`;
|
| 639 |
+
|
| 640 |
+
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';
|
| 641 |
+
|
| 642 |
+
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>`;
|
| 643 |
+
const newActionsHtml = `<div class="message-actions"><div class="flex items-center gap-1.5">${regenerateButtonHtml}</div></div>`;
|
| 644 |
+
if (messageWrapper) {
|
| 645 |
+
messageWrapper.insertAdjacentHTML('beforeend', newActionsHtml);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
resetState();
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
export function setupMobileKeyboardFix() {
|
| 653 |
+
if ('visualViewport' in window) {
|
| 654 |
+
const handleViewportResize = () => {
|
| 655 |
+
const vp = window.visualViewport;
|
| 656 |
+
document.body.style.height = `${vp.height}px`;
|
| 657 |
+
document.body.style.top = `${vp.offsetTop}px`;
|
| 658 |
+
dom.mainFooter.scrollIntoView({ behavior: "instant", block: "end" });
|
| 659 |
+
};
|
| 660 |
+
window.visualViewport.addEventListener('resize', handleViewportResize);
|
| 661 |
+
handleViewportResize();
|
| 662 |
+
}
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
export function showLoadingOnButton(button, isLoading) {
|
| 666 |
+
const spinner = button.querySelector('.animate-spin');
|
| 667 |
+
const textSpan = button.querySelector('span');
|
| 668 |
+
if (isLoading) {
|
| 669 |
+
button.disabled = true;
|
| 670 |
+
if(textSpan) textSpan.style.opacity = '0.5';
|
| 671 |
+
if(spinner) spinner.classList.remove('hidden');
|
| 672 |
+
} else {
|
| 673 |
+
button.disabled = false;
|
| 674 |
+
if(textSpan) textSpan.style.opacity = '1';
|
| 675 |
+
if(spinner) spinner.classList.add('hidden');
|
| 676 |
+
}
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
export function applyTheme(theme) {
|
| 680 |
+
if (theme === 'dark') {
|
| 681 |
+
document.documentElement.classList.add('dark');
|
| 682 |
+
dom.themeToggle.checked = true;
|
| 683 |
+
} else {
|
| 684 |
+
document.documentElement.classList.remove('dark');
|
| 685 |
+
dom.themeToggle.checked = false;
|
| 686 |
+
}
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
export function initTheme() {
|
| 690 |
+
const savedTheme = localStorage.getItem('theme');
|
| 691 |
+
if (savedTheme) {
|
| 692 |
+
applyTheme(savedTheme);
|
| 693 |
+
} else {
|
| 694 |
+
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
| 695 |
+
applyTheme(systemPrefersDark ? 'dark' : 'light');
|
| 696 |
+
}
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
export function showFreeWsLoadingIndicator(modelBubbleOuterDivElement) {
|
| 700 |
+
const contentArea = modelBubbleOuterDivElement.querySelector('.message-content');
|
| 701 |
+
if (!contentArea) return;
|
| 702 |
+
contentArea.style.padding = '1rem';
|
| 703 |
+
contentArea.innerHTML = `<div class="ws-loading-container">
|
| 704 |
+
<div class="dots">
|
| 705 |
+
<div class="dot"></div>
|
| 706 |
+
<div class="dot"></div>
|
| 707 |
+
<div class="dot"></div>
|
| 708 |
+
</div>
|
| 709 |
+
</div>`;
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
export function streamFreeWsChunk(modelBubbleOuterDivElement, fullText) {
|
| 713 |
+
const contentArea = modelBubbleOuterDivElement.querySelector('.message-content');
|
| 714 |
+
if (!contentArea) return;
|
| 715 |
+
|
| 716 |
+
const shouldScroll = isScrolledToBottom();
|
| 717 |
+
|
| 718 |
+
const loadingIndicator = contentArea.querySelector('.ws-loading-container');
|
| 719 |
+
if (loadingIndicator) {
|
| 720 |
+
contentArea.innerHTML = '';
|
| 721 |
+
contentArea.classList.add('prose', 'dark:prose-invert', 'max-w-none');
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
contentArea.innerHTML = DOMPurify.sanitize(marked.parse(fullText + '▍', { breaks: true, gfm: true }));
|
| 725 |
+
|
| 726 |
+
contentArea.querySelectorAll('pre code').forEach(block => {
|
| 727 |
+
hljs.highlightElement(block);
|
| 728 |
+
});
|
| 729 |
+
|
| 730 |
+
if (shouldScroll) {
|
| 731 |
+
requestAnimationFrame(() => {
|
| 732 |
+
dom.chatWindow.scrollTop = dom.chatWindow.scrollHeight;
|
| 733 |
+
});
|
| 734 |
+
}
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
export function finalizeFreeWsMessage(modelBubbleOuterDivElement, fullText) {
|
| 738 |
+
const contentArea = modelBubbleOuterDivElement.querySelector('.message-content');
|
| 739 |
+
if (!contentArea) return;
|
| 740 |
+
|
| 741 |
+
const shouldScroll = isScrolledToBottom();
|
| 742 |
+
contentArea.classList.add('prose', 'dark:prose-invert', 'max-w-none');
|
| 743 |
+
contentArea.style.padding = '1rem';
|
| 744 |
+
contentArea.innerHTML = DOMPurify.sanitize(marked.parse(fullText || " ", { breaks: true, gfm: true }));
|
| 745 |
+
setupCodeBlockActions(modelBubbleOuterDivElement);
|
| 746 |
+
|
| 747 |
+
if (shouldScroll) {
|
| 748 |
+
requestAnimationFrame(() => {
|
| 749 |
+
dom.chatWindow.scrollTop = dom.chatWindow.scrollHeight;
|
| 750 |
+
});
|
| 751 |
+
}
|
| 752 |
+
}
|