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

Add files via upload

Browse files
static/js/ui/dom.js ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // static/js/ui/dom.js
2
+
3
+ export const dom = {
4
+ appContainer: document.getElementById('app-container'),
5
+ chatWindow: document.getElementById('chat-window'),
6
+ mainHeader: document.getElementById('main-header'),
7
+ mainFooter: document.getElementById('main-footer'),
8
+ messageForm: document.getElementById('message-form'),
9
+ messageInput: document.getElementById('message-input'),
10
+ submitButton: document.getElementById('submit-button'),
11
+ sendIcon: document.getElementById('send-icon'),
12
+ stopIcon: document.getElementById('stop-icon'),
13
+ menuButton: document.getElementById('menu-button'),
14
+ newChatButton: document.getElementById('new-chat-button'),
15
+ historySidebar: document.getElementById('history-sidebar'),
16
+ sidebarOverlay: document.getElementById('sidebar-overlay'),
17
+ historyList: document.getElementById('history-list'),
18
+ deleteAllChatsButton: document.getElementById('delete-all-chats'),
19
+ attachFileButton: document.getElementById('attach-file-button'),
20
+ imageFileInput: document.getElementById('image-file-input'),
21
+ generalFileInput: document.getElementById('general-file-input'),
22
+ imagePreviewContainer: document.getElementById('image-preview-container'),
23
+ imagePreview: document.getElementById('image-preview'),
24
+ removeImageButton: document.getElementById('remove-image-button'),
25
+ fileInfoText: document.getElementById('file-info-text'),
26
+ historyItemMenu: document.getElementById('history-item-menu'),
27
+ messageItemMenu: document.getElementById('message-item-menu'),
28
+ messageItemMenuOverlay: document.getElementById('message-item-menu-overlay'),
29
+ messageItemMenuContent: document.getElementById('message-item-menu-content'),
30
+ confirmModal: document.getElementById('confirm-modal'),
31
+ confirmModalOverlay: document.getElementById('confirm-modal-overlay'),
32
+ confirmModalContent: document.getElementById('confirm-modal-content'),
33
+ confirmModalMessage: document.getElementById('confirm-modal-message'),
34
+ confirmModalConfirmBtn: document.getElementById('confirm-modal-confirm-btn'),
35
+ confirmModalCancelBtn: document.getElementById('confirm-modal-cancel-btn'),
36
+ renameModal: document.getElementById('rename-modal'),
37
+ renameModalOverlay: document.getElementById('rename-modal-overlay'),
38
+ renameModalContent: document.getElementById('rename-modal-content'),
39
+ renameInput: document.getElementById('rename-input'),
40
+ renameModalConfirmBtn: document.getElementById('rename-modal-confirm-btn'),
41
+ renameModalCancelBtn: document.getElementById('rename-modal-cancel-btn'),
42
+ editModal: document.getElementById('edit-modal'),
43
+ editModalOverlay: document.getElementById('edit-modal-overlay'),
44
+ editModalContent: document.getElementById('edit-modal-content'),
45
+ editInput: document.getElementById('edit-input'),
46
+ editModalConfirmBtn: document.getElementById('edit-modal-confirm-btn'),
47
+ editModalCancelBtn: document.getElementById('edit-modal-cancel-btn'),
48
+ htmlPreviewModal: document.getElementById('html-preview-modal'),
49
+ htmlPreviewOverlay: document.getElementById('html-preview-overlay'),
50
+ htmlPreviewContent: document.getElementById('html-preview-content'),
51
+ htmlPreviewIframe: document.getElementById('html-preview-iframe'),
52
+ htmlPreviewCloseBtn: document.getElementById('html-preview-close-btn'),
53
+ selectImageOption: document.getElementById('select-image-option'),
54
+ selectFileOption: document.getElementById('select-file-option'),
55
+ imageGalleryModal: document.getElementById('image-gallery-modal'),
56
+ imageGalleryContent: document.getElementById('image-gallery-content'),
57
+ galleryCloseBtn: document.getElementById('gallery-close-btn'),
58
+ galleryMainImage: document.getElementById('gallery-main-image'),
59
+ galleryPrevBtn: document.getElementById('gallery-prev-btn'),
60
+ galleryNextBtn: document.getElementById('gallery-next-btn'),
61
+ galleryThumbnails: document.getElementById('gallery-thumbnails'),
62
+ galleryDownloadBtn: document.getElementById('gallery-download-btn'),
63
+ settingsButton: document.getElementById('settings-button'),
64
+ settingsModal: document.getElementById('settings-modal'),
65
+ settingsModalContent: document.getElementById('settings-modal-content'),
66
+ themeToggle: document.getElementById('theme-toggle'),
67
+ settingsUserTier: document.getElementById('settings-user-tier'),
68
+ premiumFeatureModal: document.getElementById('premium-feature-modal'),
69
+ premiumModalIconContainer: document.getElementById('premium-modal-icon-container'),
70
+ premiumModalCloseBtn: document.getElementById('premium-modal-close-btn'),
71
+ premiumModalUpgradeBtn: document.getElementById('premium-modal-upgrade-btn'),
72
+ plusRequiredModal: document.getElementById('plus-required-modal'),
73
+ plusModalIconContainer: document.getElementById('plus-modal-icon-container'),
74
+ plusModalCloseBtn: document.getElementById('plus-modal-close-btn'),
75
+ toolsButton: document.getElementById('tools-button'),
76
+ toolsMenu: document.getElementById('tools-menu'),
77
+ filePopupMenu: document.getElementById('file-popup-menu'),
78
+ toolsButtonText: document.getElementById('tools-button-text'),
79
+ toolsDefaultIcon: document.getElementById('tools-default-icon'),
80
+ clearToolSelection: document.getElementById('clear-tool-selection'),
81
+ globalAudioPlayer: document.getElementById('global-audio-player'),
82
+ globalAudioElement: document.getElementById('global-audio-element'),
83
+ globalPlayerPlayPause: document.getElementById('global-player-play-pause'),
84
+ globalPlayerPlayIcon: document.getElementById('global-player-play-icon'),
85
+ globalPlayerPauseIcon: document.getElementById('global-player-pause-icon'),
86
+ globalPlayerText: document.getElementById('global-player-text'),
87
+ globalPlayerCurrentTime: document.getElementById('global-player-current-time'),
88
+ globalPlayerTotalTime: document.getElementById('global-player-total-time'),
89
+ waveformCanvas: document.getElementById('waveform-canvas'),
90
+ globalPlayerLoading: document.getElementById('global-player-loading'),
91
+ globalPlayerClose: document.getElementById('global-player-close'),
92
+ };
static/js/ui/modals.js ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // static/js/ui/modals.js
2
+
3
+ import { dom } from './dom.js';
4
+
5
+ let currentGalleryImages = [];
6
+ let currentGalleryIndex = 0;
7
+
8
+ const createMenuItem = (options) => {
9
+ const { action, format = '', text, icon, isDanger = false, type = 'button' } = options;
10
+ const element = document.createElement(type);
11
+ element.className = `menu-item ${isDanger ? 'danger' : ''}`;
12
+ element.dataset.action = action;
13
+ if (format) element.dataset.format = format;
14
+ element.innerHTML = `${icon}<span>${text}</span><div class="hidden w-4 h-4 border-2 border-slate-300 border-t-blue-500 rounded-full animate-spin ml-auto"></div>`;
15
+ return element;
16
+ };
17
+
18
+ const getConversionMenuItems = (action) => `
19
+ ${createMenuItem({ action, format: 'pdf', text: 'تبدیل به PDF', icon: `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><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>` }).outerHTML}
20
+ ${createMenuItem({ action, format: 'docx', text: 'تبدیل به Word', icon: `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><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.25m2.25 0H15M2.25 3h1.5M2.25 6h1.5M2.25 9h1.5M2.25 12h1.5M2.25 15h1.5M2.25 18h1.5M4.5 21h15a2.25 2.25 0 002.25-2.25V5.25A2.25 2.25 0 0019.5 3h-15A2.25 2.25 0 002.25 5.25v13.5A2.25 2.25 0 004.5 21z" /></svg>` }).outerHTML}
21
+ ${createMenuItem({ action, format: 'txt', text: 'تبدیل به Text', icon: `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" /></svg>` }).outerHTML}
22
+ ${createMenuItem({ action, format: 'html', text: 'تبدیل به HTML', icon: `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" /></svg>` }).outerHTML}
23
+ `;
24
+
25
+ function showImageInGallery(index) {
26
+ if (index < 0 || index >= currentGalleryImages.length) return;
27
+ currentGalleryIndex = index;
28
+ const newImageUrl = currentGalleryImages[index];
29
+
30
+ dom.galleryMainImage.style.opacity = '0';
31
+ setTimeout(() => {
32
+ dom.galleryMainImage.src = newImageUrl;
33
+ dom.galleryMainImage.style.opacity = '1';
34
+ }, 150);
35
+
36
+ const thumbnails = dom.galleryThumbnails.querySelectorAll('.gallery-thumb');
37
+ thumbnails.forEach((thumb, i) => {
38
+ thumb.classList.toggle('active', i === index);
39
+ if (i === index) {
40
+ thumb.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
41
+ }
42
+ });
43
+ }
44
+
45
+ const handleGalleryKeyDown = (e) => {
46
+ if (e.key === 'ArrowRight') showImageInGallery((currentGalleryIndex + 1) % currentGalleryImages.length);
47
+ else if (e.key === 'ArrowLeft') showImageInGallery((currentGalleryIndex - 1 + currentGalleryImages.length) % currentGalleryImages.length);
48
+ else if (e.key === 'Escape') closeImageGallery();
49
+ };
50
+
51
+ export function openImageGallery(imageUrlsString, startIndex) {
52
+ try {
53
+ currentGalleryImages = JSON.parse(imageUrlsString);
54
+ } catch (e) {
55
+ console.error("Failed to parse image URLs for gallery:", e);
56
+ return;
57
+ }
58
+
59
+ if (!currentGalleryImages || currentGalleryImages.length === 0) return;
60
+
61
+ dom.galleryThumbnails.innerHTML = '';
62
+ currentGalleryImages.forEach((url, index) => {
63
+ const thumb = document.createElement('img');
64
+ thumb.src = url;
65
+ thumb.className = 'gallery-thumb';
66
+ thumb.onclick = () => showImageInGallery(index);
67
+ dom.galleryThumbnails.appendChild(thumb);
68
+ });
69
+
70
+ showImageInGallery(startIndex);
71
+
72
+ dom.imageGalleryModal.classList.remove('hidden');
73
+ requestAnimationFrame(() => {
74
+ dom.imageGalleryModal.classList.add('visible');
75
+ });
76
+
77
+ dom.galleryCloseBtn.onclick = closeImageGallery;
78
+ dom.imageGalleryModal.onclick = (e) => { if (e.target === dom.imageGalleryModal) closeImageGallery(); };
79
+ dom.galleryNextBtn.onclick = () => showImageInGallery((currentGalleryIndex + 1) % currentGalleryImages.length);
80
+ dom.galleryPrevBtn.onclick = () => showImageInGallery((currentGalleryIndex - 1 + currentGalleryImages.length) % currentGalleryImages.length);
81
+ window.addEventListener('keydown', handleGalleryKeyDown);
82
+ }
83
+
84
+ function closeImageGallery() {
85
+ dom.imageGalleryModal.classList.remove('visible');
86
+ setTimeout(() => {
87
+ dom.imageGalleryModal.classList.add('hidden');
88
+ dom.galleryMainImage.src = '';
89
+ }, 300);
90
+ window.removeEventListener('keydown', handleGalleryKeyDown);
91
+ }
92
+
93
+ export function toggleSidebar(show) {
94
+ if (show) {
95
+ dom.sidebarOverlay.classList.remove('hidden');
96
+ requestAnimationFrame(() => {
97
+ dom.sidebarOverlay.style.opacity = '1';
98
+ dom.historySidebar.style.transform = 'translateX(0)';
99
+ });
100
+ } else {
101
+ dom.sidebarOverlay.style.opacity = '0';
102
+ dom.historySidebar.style.transform = 'translateX(100%)';
103
+ setTimeout(() => dom.sidebarOverlay.classList.add('hidden'), 300);
104
+ }
105
+ }
106
+
107
+ export function toggleEditModal(show) {
108
+ if (show) {
109
+ dom.editModal.classList.remove('hidden');
110
+ requestAnimationFrame(() => {
111
+ dom.editModalOverlay.style.opacity = '1';
112
+ dom.editModalContent.style.opacity = '1';
113
+ dom.editModalContent.style.transform = 'scale(1)';
114
+ dom.editInput.focus();
115
+ });
116
+ } else {
117
+ dom.editModalOverlay.style.opacity = '0';
118
+ dom.editModalContent.style.opacity = '0';
119
+ dom.editModalContent.style.transform = 'scale(0.95)';
120
+ setTimeout(() => dom.editModal.classList.add('hidden'), 300);
121
+ }
122
+ }
123
+
124
+ export function toggleHtmlPreviewModal(show, htmlContent = '') {
125
+ if (show) {
126
+ dom.htmlPreviewIframe.srcdoc = htmlContent;
127
+ dom.htmlPreviewModal.classList.remove('hidden');
128
+ requestAnimationFrame(() => {
129
+ dom.htmlPreviewOverlay.style.opacity = '1';
130
+ dom.htmlPreviewContent.style.opacity = '1';
131
+ dom.htmlPreviewContent.style.transform = 'scale(1)';
132
+ });
133
+ } else {
134
+ dom.htmlPreviewOverlay.style.opacity = '0';
135
+ dom.htmlPreviewContent.style.opacity = '0';
136
+ dom.htmlPreviewContent.style.transform = 'scale(0.95)';
137
+ setTimeout(() => {
138
+ dom.htmlPreviewModal.classList.add('hidden');
139
+ dom.htmlPreviewIframe.srcdoc = '';
140
+ }, 300);
141
+ }
142
+ }
143
+
144
+ export function showHistoryMenu(event, sessionId) {
145
+ event.stopPropagation();
146
+ const menu = dom.historyItemMenu;
147
+
148
+ menu.innerHTML = `
149
+ ${createMenuItem({ action: 'rename', text: 'تغییر نام گفتگو', icon: `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125" /></svg>` }).outerHTML}
150
+ <div class="menu-divider"></div>
151
+ ${getConversionMenuItems('convert-chat')}
152
+ <div class="menu-divider"></div>
153
+ ${createMenuItem({ action: 'delete', text: 'حذف گفتگو', icon: `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /></svg>`, isDanger: true }).outerHTML}
154
+ `;
155
+
156
+ menu.dataset.sessionId = sessionId;
157
+
158
+ const buttonRect = event.currentTarget.getBoundingClientRect();
159
+ const menuHeight = 220;
160
+ const margin = 8;
161
+
162
+ let top = buttonRect.bottom + margin;
163
+ let right = window.innerWidth - buttonRect.right;
164
+
165
+ if (top + menuHeight > window.innerHeight) {
166
+ top = buttonRect.top - menuHeight - margin;
167
+ }
168
+
169
+ menu.style.top = `${top}px`;
170
+ menu.style.right = `${right}px`;
171
+ menu.style.left = 'auto';
172
+ menu.style.transformOrigin = (top > buttonRect.top) ? 'bottom right' : 'top right';
173
+
174
+ menu.classList.add('visible');
175
+ const closeMenu = () => {
176
+ menu.classList.remove('visible');
177
+ window.removeEventListener('click', closeMenu);
178
+ };
179
+ window.addEventListener('click', closeMenu, { once: true });
180
+ }
181
+
182
+ export function showMessageMenu(event, messageIndex, activeChat, escapeHTML) {
183
+ event.stopPropagation();
184
+ const menu = dom.messageItemMenu;
185
+ const menuContent = dom.messageItemMenuContent;
186
+ const message = activeChat.messages[messageIndex];
187
+ if (!message) return;
188
+
189
+ const textPart = message.parts.find(p => p.text);
190
+ const textContent = textPart ? textPart.text : '[محتوای غیر متنی]';
191
+
192
+ let menuItemsHtml = '';
193
+ if (message.role === 'assistant' && textPart) {
194
+ menuItemsHtml += getConversionMenuItems('convert-message');
195
+ menuItemsHtml += '<div class="menu-divider"></div>';
196
+ }
197
+
198
+ menuItemsHtml += createMenuItem({
199
+ action: 'delete-message',
200
+ text: 'حذف پیام',
201
+ icon: `<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /></svg>`,
202
+ isDanger: true
203
+ }).outerHTML;
204
+
205
+ const escapedContent = escapeHTML(textContent);
206
+
207
+ menuContent.innerHTML = `
208
+ <div class="message-preview-container">
209
+ <p class="message-preview-text">${escapedContent}</p>
210
+ </div>
211
+ ${menuItemsHtml}
212
+ `;
213
+
214
+ menu.dataset.messageIndex = messageIndex;
215
+
216
+ menu.classList.remove('hidden');
217
+ requestAnimationFrame(() => {
218
+ menu.classList.add('visible');
219
+ });
220
+ }
221
+
222
+
223
+ export function showConfirmModal(message, onConfirm) {
224
+ dom.confirmModalMessage.textContent = message;
225
+ dom.confirmModal.classList.remove('hidden');
226
+ requestAnimationFrame(() => {
227
+ dom.confirmModalOverlay.style.opacity = '1';
228
+ dom.confirmModalContent.style.opacity = '1';
229
+ dom.confirmModalContent.style.transform = 'scale(1)';
230
+ });
231
+ const hide = () => {
232
+ dom.confirmModalOverlay.style.opacity = '0';
233
+ dom.confirmModalContent.style.opacity = '0';
234
+ dom.confirmModalContent.style.transform = 'scale(0.95)';
235
+ setTimeout(() => dom.confirmModal.classList.add('hidden'), 300);
236
+ };
237
+ dom.confirmModalConfirmBtn.onclick = () => { onConfirm(); hide(); };
238
+ dom.confirmModalCancelBtn.onclick = hide;
239
+ dom.confirmModalOverlay.onclick = hide;
240
+ }
241
+
242
+ export function showRenameModal(currentTitle, onConfirm) {
243
+ dom.renameInput.value = currentTitle;
244
+ dom.renameModal.classList.remove('hidden');
245
+ requestAnimationFrame(() => {
246
+ dom.renameModalOverlay.style.opacity = '1';
247
+ dom.renameModalContent.style.opacity = '1';
248
+ dom.renameModalContent.style.transform = 'scale(1)';
249
+ dom.renameInput.focus();
250
+ dom.renameInput.select();
251
+ });
252
+ const hide = () => {
253
+ dom.renameModalOverlay.style.opacity = '0';
254
+ dom.renameModalContent.style.opacity = '0';
255
+ dom.renameModalContent.style.transform = 'scale(0.95)';
256
+ setTimeout(() => dom.renameModal.classList.add('hidden'), 300);
257
+ };
258
+ dom.renameModalContent.onsubmit = (e) => {
259
+ e.preventDefault();
260
+ const newTitle = dom.renameInput.value.trim();
261
+ if (newTitle) { onConfirm(newTitle); }
262
+ hide();
263
+ };
264
+ dom.renameModalCancelBtn.onclick = hide;
265
+ dom.renameModalOverlay.onclick = hide;
266
+ }
267
+
268
+ export function showEditModal(currentText, onConfirm) {
269
+ dom.editInput.value = currentText;
270
+ toggleEditModal(true);
271
+ dom.editModalContent.onsubmit = (e) => {
272
+ e.preventDefault();
273
+ const newText = dom.editInput.value.trim();
274
+ if (newText === '') {
275
+ showConfirmModal('متن پیام شما خالی است. آیا مایل به حذف پیام هستید؟', () => { onConfirm(''); });
276
+ } else if (newText !== currentText) {
277
+ onConfirm(newText);
278
+ }
279
+ toggleEditModal(false);
280
+ };
281
+ dom.editModalCancelBtn.onclick = () => toggleEditModal(false);
282
+ dom.editModalOverlay.onclick = () => toggleEditModal(false);
283
+ }
284
+
285
+ export function toggleSettingsModal(show) {
286
+ const modal = dom.settingsModal;
287
+ const content = dom.settingsModalContent;
288
+ if (show) {
289
+ modal.classList.remove('hidden');
290
+ requestAnimationFrame(() => {
291
+ modal.style.opacity = '1';
292
+ content.style.opacity = '1';
293
+ content.style.transform = 'scale(1)';
294
+ });
295
+ } else {
296
+ modal.style.opacity = '0';
297
+ content.style.opacity = '0';
298
+ content.style.transform = 'scale(0.95)';
299
+ setTimeout(() => modal.classList.add('hidden'), 200);
300
+ }
301
+ }
302
+
303
+ export function updateSettingsUI(isPremium) {
304
+ if (!dom.settingsUserTier) return;
305
+
306
+ let content = '';
307
+ if (isPremium) {
308
+ const premiumIcon = `<svg class="settings-tier-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="premium-gradient" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#818cf8"/><stop offset="100%" stop-color="#c084fc"/></linearGradient></defs><path fill="url(#premium-gradient)" d="M50,5 C74.85,5 95,25.15 95,50 C95,74.85 74.85,95 50,95 C25.15,95 5,74.85 5,50 C5,25.15 25.15,5 50,5 Z M50,15 C30.67,15 15,30.67 15,50 C15,69.33 30.67,85 50,85 C69.33,85 85,69.33 85,50 C85,30.67 69.33,15 50,15 Z" /><text x="50" y="62" font-family="Arial" font-size="30" fill="white" text-anchor="middle" font-weight="bold">∞</text></svg>`;
309
+ content = `
310
+ <div class="settings-tier-container">
311
+ ${premiumIcon}
312
+ <div class="settings-tier-text text-slate-800 dark:text-slate-200">
313
+ نسخه: <span class="tier-name-premium">نامحدود</span>
314
+ </div>
315
+ </div>`;
316
+ } else {
317
+ const freeIcon = `<svg class="settings-tier-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 13.8214 2.48697 15.5291 3.33782 17" stroke="#64748b" stroke-width="2" stroke-linecap="round"/><path d="M19 9L12 16L9.5 13.5" stroke="#64748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
318
+ content = `
319
+ <div class="settings-tier-container">
320
+ ${freeIcon}
321
+ <div class="settings-tier-text">
322
+ نسخه: <span class="tier-name-free">رایگان</span>
323
+ </div>
324
+ </div>`;
325
+ }
326
+ dom.settingsUserTier.innerHTML = content;
327
+ }
328
+
329
+ export function togglePremiumFeatureModal(show) {
330
+ if (show) {
331
+ const icon = `<svg width="80" height="80" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
332
+ <defs>
333
+ <linearGradient id="icon-gradient" x1="0" y1="0" x2="1" y2="1">
334
+ <stop offset="0%" stop-color="#818cf8" />
335
+ <stop offset="100%" stop-color="#c084fc" />
336
+ </linearGradient>
337
+ </defs>
338
+ <path d="M12 2.5C12.41 2.5 12.75 2.84 12.75 3.25V4.44C15.96 4.97 18.5 7.79 18.5 11.06V17L20 18.5V19.5H4V18.5L5.5 17V11.06C5.5 7.79 8.04 4.97 11.25 4.44V3.25C11.25 2.84 11.59 2.5 12 2.5ZM12 22C13.1 22 14 21.1 14 20H10C10 21.1 10.9 22 12 22Z" fill="url(#icon-gradient)"/>
339
+ </svg>`;
340
+ dom.premiumModalIconContainer.innerHTML = icon;
341
+ dom.premiumFeatureModal.classList.remove('hidden');
342
+ requestAnimationFrame(() => {
343
+ dom.premiumFeatureModal.classList.add('visible');
344
+ });
345
+ } else {
346
+ dom.premiumFeatureModal.classList.remove('visible');
347
+ setTimeout(() => {
348
+ dom.premiumFeatureModal.classList.add('hidden');
349
+ }, 300);
350
+ }
351
+ }
352
+
353
+ export function togglePlusRequiredModal(show) {
354
+ const modal = dom.plusRequiredModal;
355
+ if (!modal) return;
356
+
357
+ if (show) {
358
+ const icon = `<svg width="80" height="80" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
359
+ <defs>
360
+ <linearGradient id="plus-icon-gradient" x1="0" y1="0" x2="1" y2="1">
361
+ <stop offset="0%" stop-color="#3b82f6" />
362
+ <stop offset="100%" stop-color="#14b8a6" />
363
+ </linearGradient>
364
+ </defs>
365
+ <path d="M11 11V3H13V11H21V13H13V21H11V13H3V11H11Z" fill="url(#plus-icon-gradient)"/>
366
+ <path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.58 20 4 16.42 4 12C4 7.58 7.58 4 12 4C16.42 4 20 7.58 20 12C20 16.42 16.42 20 12 20Z" fill="url(#plus-icon-gradient)" fill-opacity="0.6"/>
367
+ </svg>`;
368
+ dom.plusModalIconContainer.innerHTML = icon;
369
+ modal.classList.remove('hidden');
370
+ requestAnimationFrame(() => {
371
+ modal.classList.add('visible');
372
+ });
373
+ } else {
374
+ modal.classList.remove('visible');
375
+ setTimeout(() => {
376
+ modal.classList.add('hidden');
377
+ }, 300);
378
+ }
379
+ }
static/js/ui/tools.js ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // static/js/ui/tools.js
2
+
3
+ import { dom } from './dom.js';
4
+ import { escapeHTML } from './chat.js';
5
+
6
+ export function toggleFilePopupMenu(show) {
7
+ dom.toolsMenu.classList.remove('active');
8
+ if (show) {
9
+ dom.filePopupMenu.classList.add('active');
10
+ } else {
11
+ dom.filePopupMenu.classList.remove('active');
12
+ }
13
+ }
14
+
15
+ export function toggleToolsMenu(show) {
16
+ dom.filePopupMenu.classList.remove('active');
17
+ if (show) {
18
+ dom.toolsMenu.classList.add('active');
19
+ } else {
20
+ dom.toolsMenu.classList.remove('active');
21
+ }
22
+ }
23
+
24
+ export function updateToolsButton(toolName) {
25
+ if (toolName) {
26
+ dom.toolsButtonText.textContent = toolName;
27
+ dom.toolsDefaultIcon.style.display = 'none';
28
+ dom.clearToolSelection.style.display = 'flex';
29
+ dom.toolsButton.classList.add('tool-selected');
30
+ } else {
31
+ dom.toolsButtonText.textContent = 'ابزارها';
32
+ dom.toolsDefaultIcon.style.display = 'flex';
33
+ dom.clearToolSelection.style.display = 'none';
34
+ dom.toolsButton.classList.remove('tool-selected');
35
+ dom.messageInput.placeholder = 'هر چی میخوای بپرس';
36
+ }
37
+ }
38
+
39
+ function getDeepThinkTVHTML() {
40
+ return `
41
+ <div class="deep-think-tv-container">
42
+ <section class="deep-think-tv">
43
+ <div class="screen scanlines gloss">
44
+ <div class="ui">
45
+ <div class="header">
46
+ <div class="topbar">
47
+ <div class="badge"><span class="dot"></span>تفکر عمیق چت بات‌آلفا</div>
48
+ </div>
49
+ <div class="topic"><span>موضوع:</span><strong id="tv-topic">...</strong></div>
50
+ </div>
51
+ <div class="steps">
52
+ <div class="step active" data-step="analyzing"><span class="mini"></span>تحقیق و بررسی</div>
53
+ </div>
54
+ <div class="progress"><div class="bar" id="tv-bar" style="width:0%"></div></div>
55
+ <div id="tv-log" class="log custom-scrollbar"></div>
56
+ </div>
57
+ </div>
58
+ </section>
59
+ </div>
60
+ <div class="final-answer-wrapper"></div>
61
+ `;
62
+ }
63
+
64
+ export function createDeepThinkPanel(modelBubbleOuterDivElement) {
65
+ const contentArea = modelBubbleOuterDivElement.querySelector('.message-content');
66
+ if (!contentArea || contentArea.querySelector('.deep-think-tv')) return;
67
+ contentArea.innerHTML = getDeepThinkTVHTML();
68
+ const tv = contentArea.querySelector('.deep-think-tv');
69
+ if (document.documentElement.classList.contains('dark')) {
70
+ tv.classList.add('dark');
71
+ }
72
+ }
73
+
74
+ export function updateDeepThinkPanel(data, modelBubbleOuterDivElement) {
75
+ const tv = modelBubbleOuterDivElement.querySelector('.deep-think-tv');
76
+ if (!tv) return;
77
+
78
+ if (data.topic) {
79
+ const topicEl = tv.querySelector('#tv-topic');
80
+ if (topicEl) topicEl.textContent = data.topic;
81
+ }
82
+ if (data.log) {
83
+ const logEl = tv.querySelector('#tv-log');
84
+ if (logEl) {
85
+ const row = document.createElement('div');
86
+ row.className = 'row';
87
+ row.innerHTML = `<div class="icon">✓</div><div>${escapeHTML(data.log)}</div>`;
88
+ logEl.appendChild(row);
89
+ logEl.scrollTop = logEl.scrollHeight;
90
+ }
91
+ }
92
+ if (data.progress) {
93
+ const barEl = tv.querySelector('#tv-bar');
94
+ if (barEl) barEl.style.width = `${data.progress}%`;
95
+ }
96
+ }
97
+
98
+ export function hideDeepThinkPanel(modelBubbleOuterDivElement) {
99
+ const tvContainer = modelBubbleOuterDivElement.querySelector('.deep-think-tv-container');
100
+ const finalAnswerWrapper = modelBubbleOuterDivElement.querySelector('.final-answer-wrapper');
101
+ if (tvContainer) {
102
+ tvContainer.style.transition = 'opacity 0.5s, max-height 0.5s, padding 0.5s, margin 0.5s';
103
+ tvContainer.style.opacity = '0';
104
+ tvContainer.style.maxHeight = '0';
105
+ tvContainer.style.padding = '0';
106
+ tvContainer.style.margin = '0';
107
+ tvContainer.style.overflow = 'hidden';
108
+ }
109
+ if(finalAnswerWrapper) {
110
+ finalAnswerWrapper.classList.add('visible');
111
+ }
112
+ }
113
+
114
+ function getReasoningHUDHTML() {
115
+ return `
116
+ <div class="reasoning-hud-container">
117
+ <section class="reasoning-hud">
118
+ <div class="grid-bg"></div>
119
+ <div class="ui-content">
120
+ <div class="header">
121
+ <div class="badge"><span class="dot"></span>استدلال منطقی</div>
122
+ <div class="topic"><span>موضوع:</span><strong id="reasoning-topic">...</strong></div>
123
+ </div>
124
+ <div class="progress"><div class="bar" id="reasoning-bar" style="width:0%"></div></div>
125
+ <div id="reasoning-log" class="log custom-scrollbar"></div>
126
+ </div>
127
+ </section>
128
+ </div>
129
+ <div class="final-answer-wrapper"></div>
130
+ `;
131
+ }
132
+
133
+ export function createReasoningPanel(modelBubbleOuterDivElement) {
134
+ const contentArea = modelBubbleOuterDivElement.querySelector('.message-content');
135
+ if (!contentArea || contentArea.querySelector('.reasoning-hud')) return;
136
+ contentArea.innerHTML = getReasoningHUDHTML();
137
+ const hud = contentArea.querySelector('.reasoning-hud');
138
+ if (document.documentElement.classList.contains('dark')) {
139
+ hud.classList.add('dark');
140
+ }
141
+ }
142
+
143
+ export function updateReasoningPanel(data, modelBubbleOuterDivElement) {
144
+ const hud = modelBubbleOuterDivElement.querySelector('.reasoning-hud');
145
+ if (!hud) return;
146
+
147
+ if (data.topic) {
148
+ const topicEl = hud.querySelector('#reasoning-topic');
149
+ if (topicEl) topicEl.textContent = data.topic;
150
+ }
151
+ if (data.log) {
152
+ const logEl = hud.querySelector('#reasoning-log');
153
+ if (logEl) {
154
+ const row = document.createElement('div');
155
+ row.className = 'row';
156
+ row.innerHTML = `<div class="icon"><svg fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg></div><div>${escapeHTML(data.log)}</div>`;
157
+ logEl.appendChild(row);
158
+ logEl.scrollTop = logEl.scrollHeight;
159
+ }
160
+ }
161
+ if (data.progress) {
162
+ const barEl = hud.querySelector('#reasoning-bar');
163
+ if (barEl) barEl.style.width = `${data.progress}%`;
164
+ }
165
+ }
166
+
167
+ export function hideReasoningPanel(modelBubbleOuterDivElement) {
168
+ const hudContainer = modelBubbleOuterDivElement.querySelector('.reasoning-hud-container');
169
+ const finalAnswerWrapper = modelBubbleOuterDivElement.querySelector('.final-answer-wrapper');
170
+ if (hudContainer) {
171
+ hudContainer.style.transition = 'opacity 0.5s, max-height 0.5s, padding 0.5s, margin 0.5s';
172
+ hudContainer.style.opacity = '0';
173
+ hudContainer.style.maxHeight = '0';
174
+ hudContainer.style.padding = '0';
175
+ hudContainer.style.margin = '0';
176
+ hudContainer.style.overflow = 'hidden';
177
+ }
178
+ if(finalAnswerWrapper) {
179
+ finalAnswerWrapper.classList.add('visible');
180
+ }
181
+ }
static/js/ui/tts.js ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // static/js/ui/tts.js
2
+
3
+ import { dom } from './dom.js';
4
+
5
+ const ttsCache = new Map();
6
+
7
+ const ttsState = {
8
+ currentMessageIndex: null,
9
+ animationFrameId: null,
10
+ activeButton: null,
11
+ };
12
+
13
+ const ttsStreamManager = {
14
+ HF_WEBSOCKET_URL: "wss://ezmary-ttslive.hf.space/ws",
15
+ socket: null,
16
+ activeStreamController: null,
17
+ pendingRequest: null, // برای نگهداری درخواست در زمان اتصال مجدد
18
+
19
+ connectWebSocket() {
20
+ // فقط اگر اتصالی وجود ندارد یا قطع شده، اتصال جدید بساز
21
+ if (this.socket && this.socket.readyState !== WebSocket.CLOSED) {
22
+ return;
23
+ }
24
+
25
+ this.socket = new WebSocket(this.HF_WEBSOCKET_URL);
26
+
27
+ this.socket.onopen = () => {
28
+ console.log("اتصال WebSocket به سرور TTS برقرار شد.");
29
+ // اگر درخواستی در صف انتظار بود، آن را ارسال کن
30
+ if (this.pendingRequest) {
31
+ const { messageIndex, text, button } = this.pendingRequest;
32
+ this.pendingRequest = null;
33
+ this.stream(messageIndex, text, button, true); // ارسال مجدد درخواست
34
+ }
35
+ };
36
+
37
+ this.socket.onmessage = (event) => {
38
+ if (this.activeStreamController && this.activeStreamController.handleMessage) {
39
+ this.activeStreamController.handleMessage(event);
40
+ }
41
+ };
42
+
43
+ this.socket.onclose = () => {
44
+ console.warn("اتصال WebSocket قطع شد.");
45
+ this.socket = null; // سوکت را null کن تا اتصال بعدی دوباره برقرار شود
46
+ if (this.activeStreamController) {
47
+ this.activeStreamController.stop();
48
+ }
49
+ };
50
+
51
+ this.socket.onerror = (error) => {
52
+ console.error("خطای WebSocket:", error);
53
+ if (this.socket) this.socket.close();
54
+ };
55
+ },
56
+
57
+ _createStreamController(messageIndex, button) {
58
+ // ... (این تابع داخلی بدون تغییر باقی می‌ماند)
59
+ const controller = {
60
+ messageIndex: messageIndex, button: button, audioContext: null, audioQueue: [], receivedPcmChunks: [], sourceNodes: [], isStopped: false, isPlaying: false, nextStartTime: 0, timerInterval: null, startTime: 0, elapsedTime: 0,
61
+ _initializeAudio() { if (!this.audioContext || this.audioContext.state === 'closed') { this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 }); } if(this.audioContext.state === 'suspended') { this.audioContext.resume(); } this.nextStartTime = this.audioContext.currentTime; },
62
+ async handleMessage(event) { if (this.isStopped) return; if (typeof event.data === 'string') { const message = JSON.parse(event.data); if (message.event === "STREAM_ENDED") this._handleStreamEnd(); else if (message.event === "ERROR") { console.error(`خطا از سرور TTS: ${message.message}`); this.stop(); } } else { const arrayBuffer = await event.data.arrayBuffer(); const pcmData = new Int16Array(arrayBuffer); this.audioQueue.push(pcmData); this.receivedPcmChunks.push(pcmData); if (!this.isPlaying) this._playFromQueue(); } },
63
+ async _playFromQueue() { if (this.audioQueue.length === 0 || this.isStopped) { this.isPlaying = false; return; } this.isPlaying = true; if (!dom.globalAudioPlayer.classList.contains('visible')) { showGlobalPlayer(false, true); this.startTime = Date.now(); this.timerInterval = setInterval(() => updateGlobalPlayerUI(this), 250); ttsState.animationFrameId = requestAnimationFrame(drawWaveform); if(this.button) { this.button.classList.remove('loading'); this.button.classList.add('playing'); } } while (this.audioQueue.length > 0) { if (this.isStopped) break; const pcmData = this.audioQueue.shift(); const float32Data = new Float32Array(pcmData.length); for (let i = 0; i < pcmData.length; i++) float32Data[i] = pcmData[i] / 32768.0; if (this.audioContext.state === 'closed') return; const audioBuffer = this.audioContext.createBuffer(1, float32Data.length, this.audioContext.sampleRate); audioBuffer.getChannelData(0).set(float32Data); const source = this.audioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(this.audioContext.destination); const currentTime = this.audioContext.currentTime; this.nextStartTime = Math.max(this.nextStartTime, currentTime); source.start(this.nextStartTime); this.sourceNodes.push(source); this.nextStartTime += audioBuffer.duration; } this.isPlaying = false; },
64
+ _handleStreamEnd() { this._finalizeAndCacheAudio(); const checkPlaybackEnd = setInterval(() => { if (this.audioQueue.length === 0 && this.audioContext && this.audioContext.currentTime > this.nextStartTime - 0.1) { if(!this.isStopped) this.stop(); clearInterval(checkPlaybackEnd); } }, 100); },
65
+ _finalizeAndCacheAudio() { if (this.receivedPcmChunks.length === 0) return; const totalLength = this.receivedPcmChunks.reduce((acc, val) => acc + val.length, 0); const concatenatedPcm = new Int16Array(totalLength); let offset = 0; for (const chunk of this.receivedPcmChunks) { concatenatedPcm.set(chunk, offset); offset += chunk.length; } const wavBlob = this._createWavBlob(concatenatedPcm, this.audioContext.sampleRate); ttsCache.set(this.messageIndex, wavBlob); this.receivedPcmChunks = []; },
66
+ _createWavBlob(pcmData, sampleRate) { const numChannels = 1, bitsPerSample = 16; const blockAlign = (numChannels * bitsPerSample) / 8; const byteRate = sampleRate * blockAlign; const dataSize = pcmData.length * (bitsPerSample / 8); const buffer = new ArrayBuffer(44 + dataSize); const view = new DataView(buffer); function writeString(view, offset, string) { for (let i = 0; i < string.length; i++) view.setUint8(offset + i, string.charCodeAt(i)); } writeString(view, 0, 'RIFF'); view.setUint32(4, 36 + dataSize, true); writeString(view, 8, 'WAVE'); writeString(view, 12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, byteRate, true); view.setUint16(32, blockAlign, true); view.setUint16(34, bitsPerSample, true); writeString(view, 36, 'data'); view.setUint32(40, dataSize, true); for (let i = 0; i < pcmData.length; i++) view.setInt16(44 + i * 2, pcmData[i], true); return new Blob([view], { type: 'audio/wav' }); },
67
+ stop() { this.isStopped = true; if (this.timerInterval) clearInterval(this.timerInterval); this.timerInterval = null; this.sourceNodes.forEach(source => { try { source.stop(); } catch(e) {} }); if (this.audioContext && this.audioContext.state !== 'closed') { this.audioContext.close(); } if (ttsStreamManager.activeStreamController === this) { ttsStreamManager.activeStreamController = null; hideGlobalPlayer(); if (this.button) this.button.classList.remove('playing', 'loading'); ttsState.activeButton = null; } },
68
+ pause() { if (!this.audioContext || this.audioContext.state !== 'running') return; this.audioContext.suspend().then(() => { clearInterval(this.timerInterval); this.timerInterval = null; this.elapsedTime += (Date.now() - this.startTime) / 1000; showGlobalPlayer(false, false); if(this.button) this.button.classList.remove('playing'); }); },
69
+ resume() { if (!this.audioContext || this.audioContext.state !== 'suspended') return; this.audioContext.resume().then(() => { this.startTime = Date.now(); this.timerInterval = setInterval(() => updateGlobalPlayerUI(this), 250); showGlobalPlayer(false, true); if(this.button) this.button.classList.add('playing'); }); }
70
+ };
71
+ controller._initializeAudio();
72
+ return controller;
73
+ },
74
+
75
+ stopCurrentStream() {
76
+ if (this.activeStreamController) {
77
+ this.activeStreamController.stop();
78
+ this.activeStreamController = null;
79
+ }
80
+ // *** مهم: اتصال وب‌سوکت را قطع کن ***
81
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
82
+ this.socket.close();
83
+ console.log("اتصال WebSocket به دلیل توقف توسط کاربر، عمداً قطع شد.");
84
+ }
85
+ this.pendingRequest = null; // هر درخواست در حال انتظاری را لغو کن
86
+ },
87
+
88
+ stream(messageIndex, text, button, isRetrying = false) {
89
+ if (!isRetrying) {
90
+ this.stopCurrentStream(); // همیشه پخش قبلی را کاملا متوقف کن
91
+ }
92
+
93
+ // اگر اتصال برقرار نیست، برقرار کن و درخواست را در صف بگذار
94
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
95
+ this.pendingRequest = { messageIndex, text, button };
96
+ setSpeakButtonLoading(button, true);
97
+ this.connectWebSocket(); // این تابع اتصال جدید را شروع می‌کند
98
+ return;
99
+ }
100
+
101
+ this.activeStreamController = this._createStreamController(messageIndex, button);
102
+ ttsState.currentMessageIndex = messageIndex;
103
+ ttsState.activeButton = button;
104
+
105
+ this.socket.send(text);
106
+ setSpeakButtonLoading(button, true);
107
+ },
108
+
109
+ async playFromCache(messageIndex, button) {
110
+ const audioBlob = ttsCache.get(messageIndex);
111
+ if (!audioBlob) return;
112
+
113
+ this.stopCurrentStream();
114
+ this.activeStreamController = this._createStreamController(messageIndex, button);
115
+ ttsState.currentMessageIndex = messageIndex;
116
+ ttsState.activeButton = button;
117
+
118
+ try {
119
+ const controller = this.activeStreamController;
120
+ const arrayBuffer = await audioBlob.arrayBuffer();
121
+ if (controller.audioContext.state === 'closed') return;
122
+ const decodedBuffer = await controller.audioContext.decodeAudioData(arrayBuffer);
123
+ const source = controller.audioContext.createBufferSource();
124
+ source.buffer = decodedBuffer;
125
+ source.connect(controller.audioContext.destination);
126
+
127
+ showGlobalPlayer(false, true);
128
+ button.classList.add('playing');
129
+
130
+ controller.startTime = Date.now();
131
+ controller.timerInterval = setInterval(() => updateGlobalPlayerUI(controller), 250);
132
+ ttsState.animationFrameId = requestAnimationFrame(drawWaveform);
133
+
134
+ source.start(0);
135
+ source.onended = () => {
136
+ if (controller.audioContext && controller.audioContext.state !== 'closed') {
137
+ controller.stop();
138
+ }
139
+ };
140
+ controller.sourceNodes.push(source);
141
+ } catch (error) {
142
+ console.error("خطا در پخش از کش:", error);
143
+ this.stopCurrentStream();
144
+ }
145
+ }
146
+ };
147
+
148
+ export function clearCacheForMessage(messageIndex) { ttsCache.delete(messageIndex); }
149
+ export function clearAllCache() { ttsCache.clear(); }
150
+ export function hasCacheForMessage(messageIndex) { return ttsCache.has(messageIndex); }
151
+ export function getAudioState() { const controller = ttsStreamManager.activeStreamController; if (!controller || !controller.audioContext) return { status: 'idle', messageIndex: null }; return { status: controller.audioContext.state, messageIndex: controller.messageIndex }; }
152
+ export function togglePauseResumeAudio() { const controller = ttsStreamManager.activeStreamController; if (!controller) return; if (controller.audioContext.state === 'running') controller.pause(); else if (controller.audioContext.state === 'suspended') controller.resume(); }
153
+ export function stopAudio() { ttsStreamManager.stopCurrentStream(); }
154
+ export function stream(messageIndex, text, button) { ttsStreamManager.stream(messageIndex, text, button); }
155
+ export function playFromCache(messageIndex, button) { ttsStreamManager.playFromCache(messageIndex, button); }
156
+ export function initTtsPlayer() { dom.globalPlayerPlayPause.addEventListener('click', togglePauseResumeAudio); dom.globalPlayerClose.addEventListener('click', stopAudio); }
157
+ function formatTime(s) { if (isNaN(s) || s < 0) return '0:00'; const minutes = Math.floor(s / 60); const seconds = Math.floor(s % 60).toString().padStart(2, '0'); return `${minutes}:${seconds}`; }
158
+ function drawWaveform() { if (!dom.waveformCanvas || !ttsStreamManager.activeStreamController) { if (ttsState.animationFrameId) { cancelAnimationFrame(ttsState.animationFrameId); ttsState.animationFrameId = null; } return; } const canvas = dom.waveformCanvas; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); if (canvas.width !== rect.width * dpr || canvas.height !== rect.height * dpr) { canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.scale(dpr, dpr); } const width = canvas.width / dpr, height = canvas.height / dpr; ctx.clearRect(0, 0, width, height); const barWidth = 2, barGap = 1.5, totalBarWidth = barWidth + barGap; const numBars = Math.floor(width / totalBarWidth); const offset = (width - numBars * totalBarWidth) / 2; const inactiveColor = getComputedStyle(document.documentElement).classList.contains('dark') ? '#4b5563' : '#d1d5db'; const controller = ttsStreamManager.activeStreamController; for (let i = 0; i < numBars; i++) { let barHeight = height * 0.1; if(controller && controller.audioContext && controller.audioContext.state === 'running') { barHeight = (Math.sin((i + Date.now() / 200) * 0.2) + 1) / 2 * height * 0.7 + height * 0.1; } const x = offset + i * totalBarWidth, y = (height - barHeight) / 2; ctx.fillStyle = inactiveColor; ctx.fillRect(x, y, barWidth, barHeight); } ttsState.animationFrameId = requestAnimationFrame(drawWaveform); }
159
+ function updateGlobalPlayerUI(controller) { if (!controller || !controller.audioContext || controller.audioContext.state !== 'running') return; const currentTime = controller.elapsedTime + (Date.now() - controller.startTime) / 1000; dom.globalPlayerCurrentTime.textContent = formatTime(currentTime); }
160
+ function showGlobalPlayer(isLoading = false, isPlaying = false) { dom.globalAudioPlayer.classList.add('visible'); dom.globalPlayerText.classList.add('hidden'); dom.globalPlayerTotalTime.classList.add('hidden'); dom.globalPlayerPlayPause.style.display = isLoading ? 'none' : 'flex'; dom.globalPlayerLoading.classList.toggle('hidden', !isLoading); dom.globalPlayerPlayIcon.classList.toggle('hidden', isPlaying); dom.globalPlayerPauseIcon.classList.toggle('hidden', !isPlaying); }
161
+ export function hideGlobalPlayer() { dom.globalAudioPlayer.classList.remove('visible'); ttsState.currentMessageIndex = null; if (ttsState.animationFrameId) { cancelAnimationFrame(ttsState.animationFrameId); ttsState.animationFrameId = null; } }
162
+ export function setSpeakButtonLoading(button, isLoading) { if (button) { button.classList.toggle('loading', isLoading); if(isLoading) button.classList.remove('playing'); } }