| |
| import { send, on, off } from './ws.js'; |
| import { openModal, closeModal, openDeviceSessionModal } from './modals.js'; |
| import { isAuthenticated, currentUser, userProfile, userSettings } from './auth.js'; |
| import { deleteAllSessions } from './sessions.js'; |
| import { escHtml, showNotification } from './ui.js'; |
|
|
| export function applyTheme(theme) { |
| document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark'); |
| } |
|
|
| export function openSettings(tab = 'chat') { |
| |
| if (isAuthenticated()) { |
| send({ type: 'settings:get' }); |
| |
| const handler = (msg) => { |
| off('settings:data', handler); |
| const freshSettings = msg.settings || userSettings || _defaultSettings(); |
| _openSettingsModal(tab, freshSettings); |
| }; |
| on('settings:data', handler); |
| |
| setTimeout(() => { |
| off('settings:data', handler); |
| _openSettingsModal(tab, userSettings || _defaultSettings()); |
| }, 1500); |
| } else { |
| |
| const stored = (() => { |
| try { return JSON.parse(localStorage.getItem('ipai_settings') || '{}'); } catch { return {}; } |
| })(); |
| _openSettingsModal(tab, { ..._defaultSettings(), ...stored }); |
| } |
| } |
|
|
| function _defaultSettings() { |
| return { theme: 'dark', webSearch: true, imageGen: true, videoGen: true, audioGen: true }; |
| } |
|
|
| function _openSettingsModal(activeTab, settings) { |
| openModal(buildSettingsHtml(activeTab, settings), { |
| wide: true, |
| onOpen(b) { |
| setupSettingsTabs(b); |
| setupChatSettings(b); |
| if (isAuthenticated()) { |
| setupAccountSettings(b); |
| } |
| b.querySelectorAll('[data-disabled]').forEach(btn => { |
| btn.disabled = true; |
| btn.title = 'Coming soon'; |
| btn.style.opacity = '0.45'; |
| }); |
| } |
| }); |
| } |
|
|
| function buildSettingsHtml(activeTab, settings) { |
| const authed = isAuthenticated(); |
|
|
| return ` |
| <div class="modal-header"> |
| <span class="modal-title">Settings</span> |
| <button class="modal-close" id="settings-close">×</button> |
| </div> |
| <div class="settings-tabs"> |
| <button class="settings-tab ${activeTab==='chat'?'active':''}" data-tab="chat">Chat</button> |
| ${authed ? `<button class="settings-tab ${activeTab==='account'?'active':''}" data-tab="account">Account</button>` : ''} |
| </div> |
| |
| <!-- Chat Settings --> |
| <div class="settings-pane ${activeTab==='chat'?'active':''}" data-pane="chat" style="padding:0 24px 20px;"> |
| <div class="setting-row"> |
| <div> |
| <div class="setting-label">Theme</div> |
| <div class="setting-desc">Light or dark interface</div> |
| </div> |
| <div style="display:flex;gap:8px;"> |
| <button class="btn-ghost${settings.theme==='light'?' active-theme':''}" data-theme-btn="light" style="font-size:13px;padding:5px 12px;">☀ Light</button> |
| <button class="btn-ghost${settings.theme!=='light'?' active-theme':''}" data-theme-btn="dark" style="font-size:13px;padding:5px 12px;">🌙 Dark</button> |
| </div> |
| </div> |
| |
| <div style="margin-top:16px;margin-bottom:8px;font-size:12px;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-muted);">Available Tools</div> |
| ${buildToolToggle('webSearch', 'Web Search', 'Search the web for current information', settings.webSearch !== false)} |
| ${buildToolToggle('imageGen', 'Image Generation','Generate images from prompts', settings.imageGen !== false)} |
| ${buildToolToggle('videoGen', 'Video Generation','Generate videos from prompts', settings.videoGen !== false)} |
| ${buildToolToggle('audioGen', 'Audio / SFX', 'Generate music and sound effects', settings.audioGen !== false)} |
| </div> |
| |
| ${authed ? buildAccountPane(activeTab) : ''} |
| |
| <div class="modal-footer" style="border-top:1px solid var(--border);padding-top:12px;"> |
| <button class="btn-ghost" id="settings-cancel">Cancel</button> |
| <button class="btn-primary" id="settings-apply">Apply</button> |
| </div> |
| `; |
| } |
|
|
| function buildToolToggle(key, label, desc, enabled) { |
| return ` |
| <div class="setting-row"> |
| <div> |
| <div class="setting-label">${escHtml(label)}</div> |
| <div class="setting-desc">${escHtml(desc)}</div> |
| </div> |
| <label class="toggle-switch" data-toggle="${key}"> |
| <input type="checkbox" ${enabled ? 'checked' : ''} /> |
| <div class="toggle-track"></div> |
| <div class="toggle-thumb"></div> |
| </label> |
| </div>`; |
| } |
|
|
| function buildAccountPane(activeTab) { |
| const u = currentUser; |
| const p = userProfile; |
| const email = u?.email || ''; |
| const username = p?.username || ''; |
|
|
| return ` |
| <div class="settings-pane ${activeTab==='account'?'active':''}" data-pane="account" style="padding:0 24px 8px;"> |
| <!-- Username --> |
| <div class="form-group" style="margin-top:4px;"> |
| <label class="form-label">Username</label> |
| <div style="display:flex;gap:8px;"> |
| <input class="form-input" id="username-input" value="${escHtml(username)}" placeholder="Choose a username" style="flex:1;" /> |
| <button class="btn-ghost" id="username-save" style="font-size:13px;padding:6px 14px;white-space:nowrap;">Save</button> |
| </div> |
| <div id="username-msg" class="form-hint" style="display:none;"></div> |
| </div> |
| |
| <!-- Plan info --> |
| <div id="plan-section" style="padding:12px;border-radius:var(--radius-md);background:var(--bg-raised);border:1px solid var(--border);margin-bottom:16px;"> |
| <div style="font-size:12px;color:var(--text-muted);margin-bottom:4px;">Current Plan</div> |
| <div id="plan-name-display" style="font-weight:600;font-size:15px;">Loading…</div> |
| <button class="btn-ghost" id="billing-portal-btn" style="margin-top:8px;font-size:12px;padding:5px 12px;">Manage Billing ↗</button> |
| </div> |
| |
| <!-- Data management --> |
| <div style="margin-bottom:8px;font-size:12px;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-muted);">Data</div> |
| <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px;"> |
| <button class="btn-ghost" data-disabled style="font-size:13px;">Manage Memories</button> |
| <button class="btn-ghost" data-disabled style="font-size:13px;">Delete All Memories</button> |
| <button class="btn-danger" id="delete-sessions-btn" style="font-size:13px;">Delete All Chats</button> |
| </div> |
| |
| <!-- Active sessions --> |
| <div style="margin-bottom:8px;font-size:12px;text-transform:uppercase;letter-spacing:0.05em;color:var(--text-muted);">Devices & Sessions</div> |
| <div id="device-sessions-list" style="margin-bottom:10px;"> |
| <div style="font-size:13px;color:var(--text-muted);">Loading sessions…</div> |
| </div> |
| <button class="btn-danger" id="revoke-all-btn" style="font-size:13px;margin-bottom:16px;">Log Out All Other Devices</button> |
| |
| <!-- Delete account --> |
| <div style="border-top:1px solid var(--border);padding-top:14px;"> |
| <button class="btn-danger" id="delete-account-btn" style="font-size:13px;">Delete Account</button> |
| </div> |
| </div>`; |
| } |
|
|
| function setupSettingsTabs(b) { |
| b.querySelector('#settings-close')?.addEventListener('click', closeModal); |
| b.querySelector('#settings-cancel')?.addEventListener('click', closeModal); |
|
|
| b.querySelectorAll('.settings-tab').forEach(tab => { |
| tab.addEventListener('click', () => { |
| b.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active')); |
| b.querySelectorAll('.settings-pane').forEach(p => p.classList.remove('active')); |
| tab.classList.add('active'); |
| b.querySelector(`[data-pane="${tab.dataset.tab}"]`)?.classList.add('active'); |
| }); |
| }); |
|
|
| b.querySelector('#settings-apply')?.addEventListener('click', () => applySettings(b)); |
| } |
|
|
| function setupChatSettings(b) { |
| b.querySelectorAll('[data-theme-btn]').forEach(btn => { |
| btn.addEventListener('click', () => { |
| b.querySelectorAll('[data-theme-btn]').forEach(t => t.classList.remove('active-theme')); |
| btn.classList.add('active-theme'); |
| applyTheme(btn.dataset.themeBtn); |
| }); |
| }); |
| } |
|
|
| function setupAccountSettings(b) { |
| |
| b.querySelector('#username-save')?.addEventListener('click', async () => { |
| const val = b.querySelector('#username-input')?.value; |
| const msgEl = b.querySelector('#username-msg'); |
| send({ type: 'account:setUsername', username: val }); |
| const handler = (msg) => { |
| off('account:usernameResult', handler); |
| msgEl.style.display = ''; |
| if (msg.success) { |
| msgEl.textContent = `Username set to @${msg.username}`; |
| msgEl.style.color = 'var(--plan-core)'; |
| } else { |
| msgEl.textContent = msg.error || 'Failed'; |
| msgEl.style.color = '#f87171'; |
| } |
| }; |
| on('account:usernameResult', handler); |
| }); |
|
|
| |
| b.querySelector('#billing-portal-btn')?.addEventListener('click', () => { |
| window.open('https://sharktide-lightning.hf.space/portal', '_blank'); |
| }); |
|
|
| |
| b.querySelector('#delete-sessions-btn')?.addEventListener('click', () => { |
| if (confirm('Delete all chats? This cannot be undone.')) { |
| deleteAllSessions(); |
| closeModal(); |
| } |
| }); |
|
|
| |
| b.querySelector('#revoke-all-btn')?.addEventListener('click', () => { |
| if (confirm('Log out all other devices?')) { |
| send({ type: 'account:revokeAllOthers' }); |
| showNotification({ type: 'success', message: 'Other sessions logged out', duration: 2500 }); |
| } |
| }); |
|
|
| |
| b.querySelector('#delete-account-btn')?.addEventListener('click', async () => { |
| if (!confirm('Delete your account permanently? This cannot be undone.')) return; |
| const auth = JSON.parse(localStorage.getItem('ipai_auth_v1') || '{}'); |
| if (!auth.access_token) return; |
| const res = await fetch('https://dpixehhdbtzsbckfektd.supabase.co/functions/v1/delete_account', { |
| method: 'POST', headers: { Authorization: `Bearer ${auth.access_token}` }, |
| }); |
| if (res.ok) { |
| closeModal(); |
| import('./auth.js').then(a => a.logout()); |
| } else { |
| const d = await res.json().catch(() => ({})); |
| showNotification({ type: 'error', message: d.error || 'Delete failed', duration: 4000 }); |
| } |
| }); |
|
|
| |
| send({ type: 'account:getSubscription' }); |
| send({ type: 'account:getSessions' }); |
|
|
| const subHandler = (msg) => { |
| off('account:subscription', subHandler); |
| const planEl = b.querySelector('#plan-name-display'); |
| if (planEl && msg.info) { |
| const pKey = msg.info.planKey || 'free'; |
| const pName = msg.info.planName || 'Free Tier'; |
| planEl.innerHTML = `<span style="color:var(--plan-${pKey})">${escHtml(pName)}</span>`; |
| } |
| }; |
| on('account:subscription', subHandler); |
|
|
| const sessHandler = (msg) => { |
| off('account:deviceSessions', sessHandler); |
| const listEl = b.querySelector('#device-sessions-list'); |
| if (!listEl) return; |
| const sessions = msg.sessions || []; |
| const currentToken = msg.currentToken; |
| if (sessions.length === 0) { |
| listEl.innerHTML = '<div style="font-size:13px;color:var(--text-muted);">No sessions found.</div>'; |
| return; |
| } |
| listEl.innerHTML = sessions.map(s => ` |
| <div class="device-session-item" data-token="${escHtml(s.token)}"> |
| <div class="device-badge">💻</div> |
| <div class="device-info"> |
| <div class="device-name">${escHtml(s.userAgent?.slice(0,50) || 'Unknown device')}</div> |
| <div class="device-meta">${escHtml(s.ip || '—')} · Last seen ${escHtml(s.lastSeen ? new Date(s.lastSeen).toLocaleDateString() : '—')}</div> |
| ${s.token === currentToken ? '<div class="device-current">Current session</div>' : ''} |
| </div> |
| </div>`).join(''); |
|
|
| listEl.querySelectorAll('.device-session-item').forEach(el => { |
| el.addEventListener('click', () => { |
| const token = el.dataset.token; |
| const session = sessions.find(s => s.token === token); |
| if (session) openDeviceSessionModal(session, token === currentToken); |
| }); |
| }); |
| }; |
| on('account:deviceSessions', sessHandler); |
| } |
|
|
| function applySettings(b) { |
| const theme = b.querySelector('[data-theme-btn].active-theme')?.dataset.themeBtn |
| || (document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark'); |
|
|
| const tools = {}; |
| b.querySelectorAll('[data-toggle]').forEach(label => { |
| const key = label.dataset.toggle; |
| const checked = label.querySelector('input[type="checkbox"]').checked; |
| tools[key] = checked; |
| }); |
|
|
| const newSettings = { theme, ...tools }; |
| applyTheme(theme); |
|
|
| |
| document.querySelectorAll('[data-tool]').forEach(btn => { |
| const t = btn.dataset.tool; |
| if (t in tools) { |
| btn.classList.toggle('active', !!tools[t]); |
| btn.style.display = ''; |
| } |
| }); |
|
|
| |
| if (!isAuthenticated()) { |
| try { localStorage.setItem('ipai_settings', JSON.stringify(newSettings)); } catch {} |
| } |
|
|
| if (isAuthenticated()) { |
| send({ type: 'settings:save', settings: newSettings }); |
| } |
|
|
| closeModal(); |
| } |
|
|
| |
| on('auth:ok', (msg) => { |
| if (msg.settings?.theme) applyTheme(msg.settings.theme); |
| }); |
| on('auth:guestOk', () => { |
| const stored = (() => { |
| try { return JSON.parse(localStorage.getItem('ipai_settings') || '{}'); } catch { return {}; } |
| })(); |
| applyTheme(stored.theme || 'dark'); |
| }); |