Add files via upload
Browse files- static/js/ui/dom.js +92 -0
- static/js/ui/modals.js +379 -0
- static/js/ui/tools.js +181 -0
- static/js/ui/tts.js +162 -0
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'); } }
|