| |
| import { send, on } from './ws.js'; |
| import { isAuthenticated, logout, getTempId, getClientId } from './auth.js'; |
| import { createNewSession, switchSession, currentSessionId, onSessionChange } from './sessions.js'; |
| import { submitMessage, renderSession, setActiveSession, getIsStreaming } from './chat.js'; |
| import { openAuthModal, closeModal, openPasteEditor } from './modals.js'; |
| import { openSettings, applyTheme } from './settings.js'; |
| import { showNotification, autoResize, escHtml } from './ui.js'; |
|
|
| |
|
|
| const sidebar = document.getElementById('sidebar'); |
| const toggleBtn = document.getElementById('toggle-sidebar-btn'); |
|
|
| function expandSidebar() { sidebar?.classList.remove('collapsed'); sidebar?.classList.add('expanded'); } |
| function collapseSidebar(){ sidebar?.classList.remove('expanded'); sidebar?.classList.add('collapsed'); } |
| function toggleSidebar() { sidebar?.classList.contains('expanded') ? collapseSidebar() : expandSidebar(); } |
|
|
| toggleBtn?.addEventListener('click', toggleSidebar); |
| if (window.innerWidth >= 768) expandSidebar(); |
|
|
| |
|
|
| document.getElementById('new-chat-btn')?.addEventListener('click', () => { |
| createNewSession(); |
| if (window.innerWidth < 768) collapseSidebar(); |
| }); |
|
|
| |
|
|
| onSessionChange((event, data) => { |
| if (event === 'switched') { |
| setActiveSession(data); |
| if (!data) { |
| document.getElementById('welcome-view')?.classList.remove('hidden'); |
| document.getElementById('chat-view')?.classList.add('hidden'); |
| document.getElementById('bottom-input-bar')?.classList.add('hidden'); |
| } |
| } |
| if (event === 'data') { |
| if (data.id === currentSessionId) renderSession(data); |
| } |
| if (event === 'created') { |
| switchSession(data.id); |
| if (window.innerWidth < 768) collapseSidebar(); |
| } |
| }); |
|
|
| |
|
|
| const centerInput = document.getElementById('center-input'); |
| const centerSendBtn = document.getElementById('center-send-btn'); |
|
|
| centerInput?.addEventListener('input', () => autoResize(centerInput, 6)); |
| centerInput?.addEventListener('keydown', e => { |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); triggerCenterSend(); } |
| }); |
| centerSendBtn?.addEventListener('click', triggerCenterSend); |
|
|
| |
| document.querySelectorAll('#center-tool-search, #center-tool-image, #center-tool-video, #center-tool-audio') |
| .forEach(btn => btn.addEventListener('click', () => btn.classList.toggle('active'))); |
|
|
| function triggerCenterSend() { |
| const text = centerInput?.value.trim(); |
| const attachments = pendingAttachments.splice(0); |
| if (!text && attachments.length === 0) return; |
| if (centerInput) { centerInput.value = ''; autoResize(centerInput, 6); } |
| clearFilePreviewRow(); |
|
|
| if (!currentSessionId) { |
| |
| const pendingText = text, pendingAttach = attachments; |
| const unsub = onSessionChange((ev, s) => { |
| if (ev !== 'created') return; |
| unsub(); |
| doSend(pendingText, pendingAttach); |
| }); |
| createNewSession(); |
| } else { |
| doSend(text, attachments); |
| } |
| } |
|
|
| |
|
|
| const bottomInput = document.getElementById('bottom-input'); |
| const bottomSendBtn = document.getElementById('bottom-send-btn'); |
|
|
| bottomInput?.addEventListener('input', () => autoResize(bottomInput, 6)); |
| bottomInput?.addEventListener('keydown', e => { |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); triggerBottomSend(); } |
| }); |
| bottomSendBtn?.addEventListener('click', () => { |
| if (getIsStreaming()) { send({ type: 'chat:stop' }); return; } |
| triggerBottomSend(); |
| }); |
|
|
| function triggerBottomSend() { |
| const text = bottomInput?.value.trim(); |
| const attachments = pendingAttachments.splice(0); |
| if (!text && attachments.length === 0) return; |
| if (bottomInput) { bottomInput.value = ''; autoResize(bottomInput, 6); } |
| clearFilePreviewRow(); |
| doSend(text || '', attachments); |
| } |
|
|
| document.querySelectorAll('.tool-btn-sm').forEach(btn => |
| btn.addEventListener('click', () => btn.classList.toggle('active'))); |
|
|
| |
|
|
| function doSend(text, attachments = []) { |
| submitMessage(text, attachments); |
| } |
|
|
| |
|
|
| let pendingAttachments = []; |
| const LARGE_PASTE_THRESHOLD = 10000; |
|
|
| function openAttachMenu(e, triggerEl) { |
| e.preventDefault(); e.stopPropagation(); |
| const menu = document.getElementById('attach-context-menu'); |
| if (!menu) return; |
| menu.innerHTML = ''; |
|
|
| for (const item of [ |
| { label: 'π Upload file', onClick: () => document.getElementById('file-input')?.click() }, |
| { label: 'πΌοΈ Upload image', onClick: () => document.getElementById('image-input')?.click() }, |
| ]) { |
| const el = document.createElement('div'); |
| el.className = 'context-item'; el.textContent = item.label; |
| el.addEventListener('click', () => { menu.classList.add('hidden'); item.onClick(); }); |
| menu.appendChild(el); |
| } |
|
|
| menu.classList.remove('hidden'); |
| |
| const rect = triggerEl.getBoundingClientRect(); |
| const mh = menu.getBoundingClientRect().height || 80; |
| menu.style.left = `${Math.max(8, rect.left)}px`; |
| menu.style.top = `${rect.top - mh - 8}px`; |
|
|
| setTimeout(() => document.addEventListener('click', () => menu.classList.add('hidden'), { once: true }), 0); |
| } |
|
|
| document.getElementById('center-attach-btn')?.addEventListener('click', e => |
| openAttachMenu(e, document.getElementById('center-attach-btn'))); |
| document.getElementById('bottom-attach-btn')?.addEventListener('click', e => |
| openAttachMenu(e, document.getElementById('bottom-attach-btn'))); |
|
|
| |
| document.getElementById('file-input')?.addEventListener('change', async function() { |
| for (const file of this.files) { |
| const text = await file.text(); |
| pendingAttachments.push({ type: 'text', name: file.name, content: text }); |
| } |
| this.value = ''; |
| renderFilePreviewRow(); |
| }); |
|
|
| document.getElementById('image-input')?.addEventListener('change', async function() { |
| for (const file of this.files) await addImageFile(file); |
| this.value = ''; |
| }); |
|
|
| async function addImageFile(file) { |
| const dataUrl = await new Promise((res, rej) => { |
| const reader = new FileReader(); |
| reader.onload = () => res(reader.result); |
| reader.onerror = rej; |
| reader.readAsDataURL(file); |
| }); |
| const comma = dataUrl.indexOf(','); |
| const mimeType = dataUrl.slice(5, dataUrl.indexOf(';')); |
| const base64 = dataUrl.slice(comma + 1); |
| pendingAttachments.push({ type: 'image', name: file.name, base64, mimeType }); |
| renderFilePreviewRow(); |
| } |
|
|
| function renderFilePreviewRow() { |
| const row = document.getElementById('file-preview-row'); |
| if (!row) return; |
| row.innerHTML = ''; |
| pendingAttachments.forEach((a, i) => { |
| const wrap = document.createElement('div'); |
| wrap.style.cssText = 'position:relative;display:inline-flex;align-items:center;'; |
|
|
| if (a.type === 'image') { |
| const img = document.createElement('img'); |
| img.src = `data:${a.mimeType};base64,${a.base64}`; |
| img.style.cssText = 'width:48px;height:48px;object-fit:cover;border-radius:6px;display:block;'; |
| wrap.appendChild(img); |
| } else { |
| const chip = document.createElement('div'); |
| chip.className = 'pasted-chip'; |
| const icon = document.createElement('span'); icon.textContent = 'π'; |
| const name = document.createElement('span'); name.textContent = a.name; |
| chip.appendChild(icon); chip.appendChild(name); |
| chip.addEventListener('click', () => openPasteEditor(a.content, nc => { pendingAttachments[i].content = nc; })); |
| wrap.appendChild(chip); |
| } |
|
|
| const rm = document.createElement('button'); |
| rm.textContent = 'Γ'; |
| rm.style.cssText = 'position:absolute;top:-5px;right:-5px;width:18px;height:18px;border-radius:50%;' + |
| 'background:var(--bg-raised);border:1px solid var(--border-bright);cursor:pointer;' + |
| 'display:flex;align-items:center;justify-content:center;font-size:11px;color:var(--text);z-index:1;'; |
| rm.addEventListener('click', e => { e.stopPropagation(); pendingAttachments.splice(i, 1); renderFilePreviewRow(); }); |
| wrap.appendChild(rm); |
| row.appendChild(wrap); |
| }); |
| } |
|
|
| function clearFilePreviewRow() { |
| pendingAttachments = []; |
| const row = document.getElementById('file-preview-row'); |
| if (row) row.innerHTML = ''; |
| } |
|
|
| |
|
|
| function handlePaste(e) { |
| const items = Array.from(e.clipboardData?.items || []); |
|
|
| |
| const imgItem = items.find(i => i.kind === 'file' && i.type.startsWith('image/')); |
| if (imgItem) { e.preventDefault(); addImageFile(imgItem.getAsFile()); return; } |
|
|
| |
| const textItem = items.find(i => i.kind === 'string' && i.type === 'text/plain'); |
| if (textItem) { |
| textItem.getAsString(text => { |
| if (text.length > LARGE_PASTE_THRESHOLD) { |
| e.preventDefault(); |
| pendingAttachments.push({ type: 'text', name: `pasted-${Date.now()}.txt`, content: text }); |
| renderFilePreviewRow(); |
| } |
| }); |
| } |
| } |
|
|
| [centerInput, bottomInput].forEach(input => { |
| input?.addEventListener('paste', handlePaste); |
| input?.addEventListener('dragover', e => e.preventDefault()); |
| input?.addEventListener('drop', async e => { |
| e.preventDefault(); |
| for (const file of e.dataTransfer.files) |
| if (file.type.startsWith('image/')) await addImageFile(file); |
| }); |
| }); |
|
|
| |
|
|
| document.getElementById('signin-btn')?.addEventListener('click', () => openAuthModal('signin')); |
| document.getElementById('settings-btn')?.addEventListener('click', () => openSettings('chat')); |
| document.getElementById('settings-btn-guest')?.addEventListener('click', () => openSettings('chat')); |
|
|
| document.getElementById('user-profile-btn')?.addEventListener('click', e => { |
| const btn = e.currentTarget; |
| const rect = btn.getBoundingClientRect(); |
| const menu = document.getElementById('user-context-menu'); |
| if (!menu) return; |
|
|
| const menuItems = [ |
| { label: 'βοΈ Settings', onClick: () => openSettings('chat') }, |
| { label: 'π€ Account', onClick: () => openSettings('account') }, |
| { label: 'π³ Billing Portal', onClick: () => window.open('https://sharktide-lightning.hf.space/portal', '_blank') }, |
| { sep: true }, |
| { label: 'ποΈ Clear All Chats', danger: true, |
| onClick: () => { if (confirm('Delete all chats? This cannot be undone.')) send({ type: 'sessions:deleteAll' }); }}, |
| { sep: true }, |
| { label: 'πͺ Sign Out', danger: true, onClick: () => logout() }, |
| ]; |
|
|
| menu.innerHTML = ''; |
| for (const item of menuItems) { |
| if (item.sep) { |
| const s = document.createElement('div'); s.style.cssText = 'height:1px;background:var(--border);margin:3px 0;'; |
| menu.appendChild(s); continue; |
| } |
| const el = document.createElement('div'); |
| el.className = 'context-item' + (item.danger ? ' danger' : ''); |
| el.textContent = item.label; |
| el.addEventListener('click', () => { menu.classList.add('hidden'); item.onClick(); }); |
| menu.appendChild(el); |
| } |
|
|
| menu.classList.remove('hidden'); |
| const mw = 200; |
| let left = rect.left, top = rect.top; |
| const mh = menu.scrollHeight || 200; |
| top = rect.top - mh - 8; |
| if (left + mw > window.innerWidth - 8) left = window.innerWidth - mw - 8; |
| menu.style.left = `${Math.max(8, left)}px`; |
| menu.style.top = `${Math.max(8, top)}px`; |
|
|
| setTimeout(() => document.addEventListener('click', () => menu.classList.add('hidden'), { once: true }), 0); |
| }); |
|
|
| |
|
|
| function checkShareParam() { |
| const params = new URLSearchParams(location.search); |
| const token = params.get('share'); |
| if (!token) return; |
|
|
| const banner = document.getElementById('share-import-banner'); |
| const bannerText= document.getElementById('share-banner-text'); |
| const importBtn = document.getElementById('share-import-btn'); |
| const dismissBtn= document.getElementById('share-dismiss-btn'); |
|
|
| fetch(`/api/share/${encodeURIComponent(token)}`) |
| .then(r => r.ok ? r.json() : null) |
| .then(data => { |
| if (!data) return; |
| if (bannerText) bannerText.textContent = `Import shared chat: "${data.name}"?`; |
| banner?.classList.remove('hidden'); |
| }) |
| .catch(() => {}); |
|
|
| importBtn?.addEventListener('click', () => { |
| if (!isAuthenticated()) { openAuthModal('signin'); return; } |
| send({ type: 'sessions:import', token }); |
| banner?.classList.add('hidden'); |
| history.replaceState({}, '', '/'); |
| }); |
| dismissBtn?.addEventListener('click', () => { |
| banner?.classList.add('hidden'); |
| history.replaceState({}, '', '/'); |
| }); |
| } |
|
|
| |
|
|
| let wasDisconnected = false; |
| on('ws:disconnected', () => { |
| wasDisconnected = true; |
| showNotification({ type: 'warning', message: 'Connection lost β reconnectingβ¦', duration: 3000 }); |
| }); |
| on('ws:connected', () => { |
| if (wasDisconnected) showNotification({ type: 'success', message: 'Reconnected', duration: 2000 }); |
| wasDisconnected = false; |
| }); |
|
|
| |
|
|
| document.addEventListener('DOMContentLoaded', () => { |
| checkShareParam(); |
| applyTheme('dark'); |
| }); |
|
|