Jan2000 commited on
Commit
07ea0f7
·
unverified ·
1 Parent(s): 2717123

Create chat.js

Browse files
Files changed (1) hide show
  1. 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
+ }