| |
| import { send, on } from './ws.js'; |
| import { escHtml } from './ui.js'; |
| import { isAuthenticated, loginWithEmail, signUpWithEmail, loginWithOAuth, logout, currentUser, userProfile, userSettings } from './auth.js'; |
|
|
| let overlay, box; |
| function getOverlay() { return overlay || (overlay = document.getElementById('modal-overlay')); } |
| function getBox() { return box || (box = document.getElementById('modal-box')); } |
|
|
| export function openModal(html, opts = {}) { |
| const o = getOverlay(), b = getBox(); |
| b.className = 'modal-box' + (opts.wide ? ' wide' : ''); |
| b.innerHTML = html; |
| o.classList.remove('hidden'); |
| if (opts.onOpen) opts.onOpen(b); |
| |
| o.onclick = (e) => { if (e.target === o) closeModal(); }; |
| |
| document.addEventListener('keydown', escHandler); |
| } |
|
|
| const escHandler = (e) => { if (e.key === 'Escape') closeModal(); }; |
|
|
| export function closeModal() { |
| getOverlay().classList.add('hidden'); |
| getBox().innerHTML = ''; |
| document.removeEventListener('keydown', escHandler); |
| } |
|
|
| |
|
|
| export function openAuthModal(initialTab = 'signin') { |
| openModal(` |
| <div class="modal-header"> |
| <span class="modal-title">Sign in to InferencePort AI</span> |
| <button class="modal-close" onclick="import('/js/modals.js').then(m=>m.closeModal())">Γ</button> |
| </div> |
| <div class="modal-body"> |
| <div class="auth-tabs"> |
| <button class="auth-tab ${initialTab==='signin'?'active':''}" data-tab="signin">Sign In</button> |
| <button class="auth-tab ${initialTab==='signup'?'active':''}" data-tab="signup">Create Account</button> |
| </div> |
| |
| <div id="auth-signin" style="${initialTab!=='signin'?'display:none':''}"> |
| <div style="display:flex;flex-direction:column;gap:8px;margin-bottom:14px;"> |
| <button class="social-btn" id="github-btn"> |
| <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.7 7.7 0 012.01-.27c.68 0 1.36.09 2.01.27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0016 8c0-4.42-3.58-8-8-8z"/></svg> |
| Continue with GitHub |
| </button> |
| <button class="social-btn" id="google-btn"> |
| <svg width="16" height="16" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.02 1.53 7.4 2.8l5.4-5.4C33.52 3.7 29.1 1.5 24 1.5 14.64 1.5 6.58 6.88 2.66 14.7l6.64 5.15C11.2 13.6 17.08 9.5 24 9.5z"/><path fill="#4285F4" d="M46.5 24c0-1.64-.15-3.22-.43-4.74H24v9h12.7c-.55 2.95-2.21 5.45-4.7 7.12l7.23 5.6C43.38 36.9 46.5 31.1 46.5 24z"/><path fill="#FBBC05" d="M9.3 28.85A14.4 14.4 0 0 1 8.5 24c0-1.68.3-3.3.8-4.85l-6.64-5.15A23.96 23.96 0 0 0 1.5 24c0 3.9.94 7.58 2.66 10.7l6.64-5.15z"/><path fill="#34A853" d="M24 46.5c6.48 0 11.92-2.14 15.9-5.82l-7.23-5.6c-2.01 1.35-4.58 2.15-8.67 2.15-6.92 0-12.8-4.1-14.7-10.05l-6.64 5.15C6.58 41.12 14.64 46.5 24 46.5z"/></svg> |
| Continue with Google |
| </button> |
| </div> |
| <div class="auth-divider">or</div> |
| <div class="form-group"> |
| <label class="form-label">Email</label> |
| <input class="form-input" id="signin-email" type="email" placeholder="you@example.com" /> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Password</label> |
| <input class="form-input" id="signin-password" type="password" placeholder="β’β’β’β’β’β’β’β’" /> |
| </div> |
| <div id="signin-error" class="form-error" style="display:none;margin-bottom:8px;"></div> |
| <button class="btn-primary" id="signin-submit" style="width:100%;">Sign In</button> |
| <div style="margin-top:10px;text-align:center;"> |
| <button style="font-size:13px;color:var(--blue-bright);" id="forgot-pw">Forgot password?</button> |
| </div> |
| </div> |
| |
| <div id="auth-signup" style="${initialTab!=='signup'?'display:none':''}"> |
| <div class="form-group"> |
| <label class="form-label">Email</label> |
| <input class="form-input" id="signup-email" type="email" placeholder="you@example.com" /> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Password</label> |
| <input class="form-input" id="signup-password" type="password" placeholder="Min 6 characters" /> |
| </div> |
| <div id="signup-error" class="form-error" style="display:none;margin-bottom:8px;"></div> |
| <button class="btn-primary" id="signup-submit" style="width:100%;">Create Account</button> |
| </div> |
| </div> |
| `, { |
| onOpen(b) { |
| |
| b.querySelectorAll('.auth-tab').forEach(tab => { |
| tab.addEventListener('click', () => { |
| b.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active')); |
| tab.classList.add('active'); |
| const name = tab.dataset.tab; |
| b.querySelector('#auth-signin').style.display = name === 'signin' ? '' : 'none'; |
| b.querySelector('#auth-signup').style.display = name === 'signup' ? '' : 'none'; |
| }); |
| }); |
|
|
| |
| b.querySelector('#signin-submit').addEventListener('click', async () => { |
| const email = b.querySelector('#signin-email').value.trim(); |
| const pass = b.querySelector('#signin-password').value; |
| const errEl = b.querySelector('#signin-error'); |
| errEl.style.display = 'none'; |
| try { |
| await loginWithEmail(email, pass); |
| closeModal(); |
| } catch (e) { |
| errEl.textContent = e.message; errEl.style.display = ''; |
| } |
| }); |
|
|
| |
| b.querySelector('#signup-submit').addEventListener('click', async () => { |
| const email = b.querySelector('#signup-email').value.trim(); |
| const pass = b.querySelector('#signup-password').value; |
| const errEl = b.querySelector('#signup-error'); |
| errEl.style.display = 'none'; |
| try { |
| const result = await signUpWithEmail(email, pass); |
| if (result.access_token) { |
| closeModal(); |
| } else { |
| errEl.textContent = 'Check your email to confirm your account.'; errEl.style.display = ''; |
| } |
| } catch (e) { |
| errEl.textContent = e.message; errEl.style.display = ''; |
| } |
| }); |
|
|
| b.querySelector('#github-btn').addEventListener('click', () => loginWithOAuth('github')); |
| b.querySelector('#google-btn').addEventListener('click', () => loginWithOAuth('google')); |
| b.querySelector('#forgot-pw').addEventListener('click', () => openForgotPasswordModal()); |
|
|
| |
| [['#signin-email','#signin-password','#signin-submit'], |
| ['#signup-email','#signup-password','#signup-submit']].forEach(([e, p, s]) => { |
| [e, p].forEach(sel => { |
| b.querySelector(sel)?.addEventListener('keydown', ev => { |
| if (ev.key === 'Enter') b.querySelector(s)?.click(); |
| }); |
| }); |
| }); |
| } |
| }); |
| } |
|
|
| function openForgotPasswordModal() { |
| openModal(` |
| <div class="modal-header"> |
| <span class="modal-title">Reset Password</span> |
| <button class="modal-close" onclick="import('/js/modals.js').then(m=>m.closeModal())">Γ</button> |
| </div> |
| <div class="modal-body"> |
| <div class="form-group"> |
| <label class="form-label">Email</label> |
| <input class="form-input" id="reset-email" type="email" placeholder="you@example.com" /> |
| </div> |
| <div id="reset-msg" style="font-size:13px;margin-bottom:8px;display:none;"></div> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn-ghost" onclick="import('/js/modals.js').then(m=>m.closeModal())">Cancel</button> |
| <button class="btn-primary" id="reset-submit">Send Reset Link</button> |
| </div> |
| `, { |
| onOpen(b) { |
| b.querySelector('#reset-submit').addEventListener('click', async () => { |
| const email = b.querySelector('#reset-email').value.trim(); |
| const msgEl = b.querySelector('#reset-msg'); |
| const SUPABASE_URL = 'https://dpixehhdbtzsbckfektd.supabase.co'; |
| const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRwaXhlaGhkYnR6c2Jja2Zla3RkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjExNDI0MjcsImV4cCI6MjA3NjcxODQyN30.nR1KCSRQj1E_evQWnE2VaZzg7PgLp2kqt4eDKP2PkpE'; |
| try { |
| await fetch(`${SUPABASE_URL}/auth/v1/recover`, { |
| method: 'POST', headers: { 'Content-Type':'application/json','apikey':SUPABASE_KEY }, |
| body: JSON.stringify({ email }), |
| }); |
| msgEl.textContent = 'Reset link sent. Check your email.'; |
| msgEl.style.color = 'var(--plan-core)'; msgEl.style.display = ''; |
| } catch { msgEl.textContent = 'Error. Try again.'; msgEl.style.display = ''; } |
| }); |
| } |
| }); |
| } |
|
|
| |
|
|
| export function showShareModal(sessionId) { |
| if (!isAuthenticated()) return openAuthModal('signin'); |
|
|
| openModal(` |
| <div class="modal-header"> |
| <span class="modal-title">Share Chat</span> |
| <button class="modal-close" onclick="import('/js/modals.js').then(m=>m.closeModal())">Γ</button> |
| </div> |
| <div class="modal-body"> |
| <div class="share-warning"> |
| <span style="font-size:18px">β οΈ</span> |
| <span>You are about to share this whole session. Anyone with the link can import it into their account.</span> |
| </div> |
| <div id="share-url-wrap" style="display:none;"> |
| <div class="form-label" style="margin-bottom:6px;">Share link</div> |
| <div style="display:flex;gap:8px;"> |
| <input class="form-input" id="share-url-input" readonly style="flex:1;" /> |
| <button class="btn-ghost" id="share-copy-btn">Copy</button> |
| </div> |
| </div> |
| <div id="share-loading" style="font-size:13px;color:var(--text-muted);">Generating linkβ¦</div> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn-ghost" onclick="import('/js/modals.js').then(m=>m.closeModal())">Close</button> |
| <button class="btn-primary" id="share-generate-btn">Generate Link</button> |
| </div> |
| `, { |
| onOpen(b) { |
| b.querySelector('#share-generate-btn').addEventListener('click', () => { |
| b.querySelector('#share-loading').style.display = ''; |
| b.querySelector('#share-generate-btn').disabled = true; |
| send({ type: 'sessions:share', sessionId }); |
|
|
| on('sessions:shareUrl', function handler(msg) { |
| if (msg.sessionId !== sessionId) return; |
| import('./ws.js').then(({ off }) => off('sessions:shareUrl', handler)); |
| b.querySelector('#share-loading').style.display = 'none'; |
| b.querySelector('#share-url-wrap').style.display = ''; |
| const input = b.querySelector('#share-url-input'); |
| input.value = msg.url; |
|
|
| b.querySelector('#share-copy-btn').addEventListener('click', async () => { |
| await navigator.clipboard.writeText(msg.url).catch(() => {}); |
| b.querySelector('#share-copy-btn').textContent = 'Copied!'; |
| }); |
| }); |
| }); |
| } |
| }); |
| } |
|
|
| |
|
|
| export function showToolCallModal(call) { |
| const names = { |
| ollama_search: 'Web Search', read_web_page: 'Read Web Page', |
| generate_image: 'Image Generation', generate_video: 'Video Generation', generate_audio: 'Audio Generation', |
| }; |
| const displayName = names[call.name] || call.name; |
|
|
| let argsDisplay = call.args || call.arguments || '{}'; |
| if (typeof argsDisplay !== 'string') argsDisplay = JSON.stringify(argsDisplay, null, 2); |
|
|
| let resultDisplay = call.result || 'β'; |
| if (typeof resultDisplay !== 'string') resultDisplay = JSON.stringify(resultDisplay, null, 2); |
|
|
| openModal(` |
| <div class="modal-header"> |
| <span class="modal-title">π§ ${escHtml(displayName)}</span> |
| <button class="modal-close" onclick="import('/js/modals.js').then(m=>m.closeModal())">Γ</button> |
| </div> |
| <div class="modal-body"> |
| <div class="tool-detail-section"> |
| <div class="tool-detail-label">Tool</div> |
| <div class="tool-detail-content" style="font-family:var(--font-sans);">${escHtml(call.name)}</div> |
| </div> |
| <div class="tool-detail-section"> |
| <div class="tool-detail-label">Request</div> |
| <div class="tool-detail-content">${escHtml(argsDisplay)}</div> |
| </div> |
| <div class="tool-detail-section"> |
| <div class="tool-detail-label">Response</div> |
| <div class="tool-detail-content">${escHtml(resultDisplay.slice(0, 4000))}</div> |
| </div> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn-ghost" onclick="import('/js/modals.js').then(m=>m.closeModal())">Close</button> |
| </div> |
| `); |
| } |
|
|
| |
|
|
| export function openImageModal(src) { |
| openModal(` |
| <div class="modal-header" style="border-bottom:none;"> |
| <span></span> |
| <button class="modal-close" onclick="import('/js/modals.js').then(m=>m.closeModal())">Γ</button> |
| </div> |
| <div class="modal-body" style="padding-top:0;text-align:center;"> |
| <img src="${escHtml(src)}" style="max-width:100%;max-height:70vh;border-radius:8px;" alt="Image" /> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn-ghost" onclick="import('/js/modals.js').then(m=>m.closeModal())">Close</button> |
| <button class="btn-primary" id="img-dl-btn">Download</button> |
| </div> |
| `, { |
| onOpen(b) { |
| b.querySelector('#img-dl-btn').addEventListener('click', () => { |
| const a = document.createElement('a'); |
| a.href = src; a.download = `image-${Date.now()}.png`; |
| document.body.appendChild(a); a.click(); document.body.removeChild(a); |
| }); |
| } |
| }); |
| } |
|
|
| |
|
|
| export function openLimitModal() { |
| openModal(` |
| <div class="modal-body" style="padding-top:28px;"> |
| <div class="limit-modal-inner"> |
| <div class="limit-icon">π¬</div> |
| <div class="limit-title">Daily limit reached</div> |
| <div class="limit-desc">Sign in or create a free account to keep chatting.<br>Guest usage resets every 24 hours.</div> |
| <div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;"> |
| <button class="btn-primary" id="limit-signin">Sign In</button> |
| <button class="btn-ghost" id="limit-signup">Create Account</button> |
| </div> |
| </div> |
| </div> |
| `, { |
| onOpen(b) { |
| b.querySelector('#limit-signin').addEventListener('click', () => { closeModal(); openAuthModal('signin'); }); |
| b.querySelector('#limit-signup').addEventListener('click', () => { closeModal(); openAuthModal('signup'); }); |
| } |
| }); |
| } |
|
|
| |
|
|
| export function openDeviceSessionModal(session, isCurrentSession) { |
| openModal(` |
| <div class="modal-header"> |
| <span class="modal-title">Session Details</span> |
| <button class="modal-close" onclick="import('/js/modals.js').then(m=>m.closeModal())">Γ</button> |
| </div> |
| <div class="modal-body"> |
| <div class="form-group"> |
| <div class="form-label">IP Address</div> |
| <div style="font-size:14px;">${escHtml(session.ip || 'Unknown')}</div> |
| </div> |
| <div class="form-group"> |
| <div class="form-label">Last seen</div> |
| <div style="font-size:14px;">${escHtml(session.lastSeen ? new Date(session.lastSeen).toLocaleString() : 'β')}</div> |
| </div> |
| <div class="form-group"> |
| <div class="form-label">First seen</div> |
| <div style="font-size:14px;">${escHtml(session.createdAt ? new Date(session.createdAt).toLocaleString() : 'β')}</div> |
| </div> |
| <div class="form-group"> |
| <div class="form-label">User Agent</div> |
| <div style="font-size:12px;word-break:break-all;color:var(--text-dim);">${escHtml(session.userAgent || 'Unknown')}</div> |
| </div> |
| ${isCurrentSession ? '<div style="font-size:12px;color:var(--plan-core);margin-top:4px;">This is your current session.</div>' : ''} |
| </div> |
| <div class="modal-footer"> |
| <button class="btn-ghost" onclick="import('/js/modals.js').then(m=>m.closeModal())">Close</button> |
| ${!isCurrentSession ? `<button class="btn-danger" id="revoke-session-btn">Log Out This Session</button>` : ''} |
| </div> |
| `, { |
| onOpen(b) { |
| if (!isCurrentSession) { |
| b.querySelector('#revoke-session-btn')?.addEventListener('click', () => { |
| send({ type: 'account:revokeSession', token: session.token }); |
| closeModal(); |
| }); |
| } |
| } |
| }); |
| } |
|
|
| |
|
|
| export function openPasteEditor(content, onSave) { |
| openModal(` |
| <div class="modal-header"> |
| <span class="modal-title">Edit Pasted Content</span> |
| <button class="modal-close" onclick="import('/js/modals.js').then(m=>m.closeModal())">Γ</button> |
| </div> |
| <div class="modal-body"> |
| <textarea id="paste-editor" style="width:100%;min-height:300px;background:var(--input-bg);border:1px solid var(--input-border);border-radius:8px;padding:10px;color:var(--text);font-size:13px;font-family:var(--font-mono);resize:vertical;">${escHtml(content)}</textarea> |
| </div> |
| <div class="modal-footer"> |
| <button class="btn-ghost" onclick="import('/js/modals.js').then(m=>m.closeModal())">Cancel</button> |
| <button class="btn-primary" id="paste-save">Save</button> |
| </div> |
| `, { |
| onOpen(b) { |
| b.querySelector('#paste-save').addEventListener('click', () => { |
| const val = b.querySelector('#paste-editor').value; |
| onSave(val); |
| closeModal(); |
| }); |
| } |
| }); |
| } |
|
|
| |
| on('chat:limitReached', () => openLimitModal()); |
|
|
| export { closeModal, openModal }; |
|
|