chat / js /modals.js
incognitolm's picture
Initial
5383ef0 verified
Raw
History Blame
19.2 kB
// modals.js β€” All modal dialogs
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);
// Close on overlay click
o.onclick = (e) => { if (e.target === o) closeModal(); };
// Close on Escape
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);
}
// ── Auth modal ────────────────────────────────────────────────────────────
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) {
// Tab switching
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';
});
});
// Sign in
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 = '';
}
});
// Sign up
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());
// Enter key
[['#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 = ''; }
});
}
});
}
// ── Share modal ───────────────────────────────────────────────────────────
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!';
});
});
});
}
});
}
// ── Tool call modal ───────────────────────────────────────────────────────
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>
`);
}
// ── Image modal ───────────────────────────────────────────────────────────
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);
});
}
});
}
// ── Chat limit modal ──────────────────────────────────────────────────────
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'); });
}
});
}
// ── Device session detail modal ───────────────────────────────────────────
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();
});
}
}
});
}
// ── Pasted content editor ─────────────────────────────────────────────────
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();
});
}
});
}
// Auto-handle limit event
on('chat:limitReached', () => openLimitModal());
export { closeModal, openModal };