// Modals.jsx — all secondary flows (new key, recovery codes, upgrade, photo upload, confirms, change pw, totp, export, system prompt edit)
function ModalShell({ eb, title, desc, children, footer, width, onClose }) {
React.useEffect(() => {
const onKey = e => { if (e.key === 'Escape') onClose && onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
return (
e.stopPropagation()}>
{eb &&
{eb}
}
{title}
{desc &&
{desc}
}
{children}
{footer}
);
}
/* ---- 1. New API key (multi-step: name+scopes → reveal) ---- */
function NewKeyModal({ onClose }) {
const [step, setStep] = React.useState(1);
const [name, setName] = React.useState('production-2');
const [scopes, setScopes] = React.useState({read: true, write: true, verify: true, billing: false});
const tog = (k) => setScopes({...scopes, [k]: !scopes[k]});
const key = 'zg-live-a8f3c91d4e7b8s2nCAAB2901F40137';
if (step === 1) return (
>}>
Scopes
{[
{k: 'read', t: 'Read', s: 'List threads, models, and certificates'},
{k: 'write', t: 'Write', s: 'Create threads and submit statements'},
{k: 'verify', t: 'Verify', s: 'Run kernel verification, fetch certificates'},
{k: 'billing', t: 'Billing', s: 'Read invoices and update payment methods'},
].map(o => (
))}
);
return (
>}>
{key}
Copy
Store this in a secure secrets manager. If lost, you'll need to create a new key. Treat it like a password.
name · {name}
scopes · {Object.keys(scopes).filter(k => scopes[k]).join(', ')}
created · {new Date().toISOString().slice(0, 16).replace('T', ' ')}
);
}
/* ---- 2. Recovery codes ---- */
function RecoveryCodesModal({ onClose }) {
const codes = ['8h2d-fw3k', 'p1qa-ne7r', 'm4xv-2tcb', 'gz9o-rk1y', 'aa3e-d8u2', 'sd0n-rm4p', '4er8-lo2k', 'b7yu-fwn9', 'cc1a-vb6q', '7l9z-hk4n'];
return (
10 codes · 0 used
>}>
Store these somewhere safe and offline. They are equivalent to your password.
);
}
/* ---- 3. Change password ---- */
function ChangePasswordModal({ onClose }) {
const [done, setDone] = React.useState(false);
if (done) return (
}>
Your password has been changed. All other sessions have been signed out.
);
return (
>}>
);
}
/* ---- 4. Reconfigure TOTP ---- */
function TotpModal({ onClose }) {
const secret = 'JBSWY3DPEHPK3PXPSXNJWGGMTQTQM';
return (
>}>
);
}
/* ---- 5. Upgrade plan ---- */
function UpgradeModal({ onClose }) {
const [sel, setSel] = React.useState('atlas');
return (
Billed monthly · prorated today
>}>
setSel('atlas')}>
Atlas
$1,499/mo
10M tokens · 25k proofs · SSO · audit log · 99.95% SLA · email + slack support
setSel('enterprise')}>
Enterprise
Talk to us
Dedicated kernel · on-prem option · custom retention · designated SRE · MSA
);
}
/* ---- 6. Photo upload ---- */
function PhotoUploadModal({ onClose }) {
const ref = React.useRef(null);
return (
>}>
ref.current && ref.current.click()}>
↑
Drag an image here, or click to browse
jpg · png · webp · 4 MB max
);
}
/* ---- 7. Confirm destructive ---- */
function ConfirmModal({ eb, title, desc, confirmText = 'Confirm', requirePhrase, onClose, onConfirm }) {
const [phrase, setPhrase] = React.useState('');
const ok = !requirePhrase || phrase === requirePhrase;
return (
>}>
This action cannot be undone.
{requirePhrase && (
)}
);
}
/* ---- 8. Generic info modal ---- */
function ExportModal({ onClose }) {
return (
>}>
);
}
/* ---- 9. Generic value-edit (radio-style selector) ---- */
const VALUE_PRESETS = {
timezone: { eb: 'Account · Profile', title: 'Change timezone', desc: 'Used for timestamps, scheduled sends, and daily digest delivery.',
options: ['Asia/Seoul · UTC+09', 'Asia/Tokyo · UTC+09', 'America/Los_Angeles · UTC-08', 'Europe/London · UTC+00', 'UTC'] },
locale: { eb: 'Account · Profile', title: 'Change locale', desc: 'Affects UI language and number/date formatting.',
options: ['한국어 / English', 'English (US)', '日本語', '中文 (简体)', 'Deutsch'] },
'billing-email': { eb: 'Account · Plan & billing', title: 'Billing email', desc: 'Invoices and dunning notices are sent here.', input: true, current: 'billing@gbr.ai' },
'pay-method': { eb: 'Account · Plan & billing', title: 'Update payment method',
desc: 'We never store your full PAN. Stripe Elements handles the card field.',
options: ['Visa ···· 4128 (default)', 'Mastercard ···· 7733', '+ Add new card', '+ Add bank transfer / wire'] },
invoices: { eb: 'Account · Plan & billing', title: 'Download invoice history',
desc: 'Choose a range — we\u2019ll email the CSV bundle within 30 minutes.',
options: ['Last 30 days', 'Last 90 days', 'Last 12 months', 'All time'] },
'math-render': { eb: 'Settings · Appearance', title: 'Math rendering engine',
desc: 'KaTeX is faster; MathJax has wider symbol support.',
options: ['KaTeX · italic serif (default)', 'KaTeX · upright sans', 'MathJax · classical', 'Plain text · monospace'] },
'code-theme': { eb: 'Settings · Appearance', title: 'Code block theme',
options: ['GBR Gold (default)', 'Classic dark', 'Solarized dark', 'Monokai', 'High contrast'] },
'default-model': { eb: 'Settings · Chat defaults', title: 'Default model',
desc: 'Applied to every new conversation. You can swap per-thread.',
options: ['proof-α-2.4 (recommended)', 'proof-α-2.4 · thinking', 'proof-β-1.1 · fast', 'kernel-only · no LLM'] },
retention: { eb: 'Settings · Data & privacy', title: 'Conversation retention',
desc: 'After this period, threads are permanently destroyed. Certificates are kept indefinitely.',
options: ['30 days', '90 days (default)', '180 days', '1 year', 'Forever (until manually deleted)'] },
};
function ValueEditModal({ presetKey, onClose, onSave }) {
const p = VALUE_PRESETS[presetKey] || { eb: 'Settings', title: 'Edit value', options: [] };
const [val, setVal] = React.useState(p.input ? (p.current || '') : (p.options ? p.options[0] : ''));
return (
>}>
{p.input ? (
setVal(e.target.value)} style={{width:'100%',background:'var(--bg-canvas)',border:'1px solid var(--border-1)',borderRadius:'var(--r-2)',padding:'10px 12px',color:'var(--fg-1)',fontFamily:'var(--font-mono)',fontSize:13,outline:'none'}}/>
) : (
{p.options.map(o => (
))}
)}
);
}
/* ---- 10. Share thread ---- */
function ShareModal({ onClose, threadTitle = 'Riemann zeta — non-trivial zeros' }) {
const [scope, setScope] = React.useState('link');
const [expiry, setExpiry] = React.useState('7d');
const [allowVerify, setAllowVerify] = React.useState(true);
const [copied, setCopied] = React.useState(false);
const link = 'https://gbr.ai/s/' + (Math.random().toString(36).slice(2, 10));
const scopes = [
{ id: 'link', t: 'Anyone with the link', s: 'Read-only. No sign-in required.' },
{ id: 'workspace', t: 'Workspace members', s: 'ZIGU LABS · 14 members' },
{ id: 'specific', t: 'Specific people', s: 'Invite by email; they\u2019ll need a GBR account.' },
{ id: 'off', t: 'Off · only me', s: 'Disable any existing share links.' },
];
const expiries = ['24 hours', '7 days', '30 days', 'No expiry'];
return (
cert · 0xCAAB29F4…8E2A
>}>
{link}
Who can view
{scopes.map(o => (
))}
Link expires
{expiries.map(e => )}
Options
);
}
/* ---- 11. Export thread ---- */
function ExportThreadModal({ onClose, threadTitle = 'thread' }) {
const [fmt, setFmt] = React.useState('md');
const formats = [
{ id: 'md', t: 'Markdown', s: 'Plain .md · code fences preserved', size: '12.4 KB', ic: 'M↓' },
{ id: 'json', t: 'JSON', s: 'Structured messages + metadata', size: '38.1 KB', ic: '{}' },
{ id: 'pdf', t: 'PDF', s: 'Printable · GBR Gold theme · 1-column', size: '410 KB', ic: '📄' },
{ id: 'cbor', t: 'Proof bundle', s: 'Signed CBOR · re-verifiable offline', size: '4.2 MB', ic: '⛨' },
];
return (
>}>
{formats.map(f => (
))}
);
}
/* ---- 12. Search threads (command palette) ---- */
function SearchModal({ onClose, threads = [], onSelect }) {
const [q, setQ] = React.useState('');
const ref = React.useRef(null);
React.useEffect(() => { ref.current && ref.current.focus(); }, []);
const filtered = !q ? threads : threads.filter(t => t.title.toLowerCase().includes(q.toLowerCase()) || t.model.toLowerCase().includes(q.toLowerCase()));
return (
e.stopPropagation()}>
⌕
setQ(e.target.value)}/>
esc
{!q &&
Jump to
}
{!q && [
{ ic: '+', t: 'New conversation', kbd: '⌘ N' },
{ ic: '⚙', t: 'Settings', kbd: '⌘ ,' },
{ ic: '⌘', t: 'Switch model', kbd: '⌘ ⇧ M' },
{ ic: '✓', t: 'Verify last proof', kbd: '⌘ ⇧ V' },
].map((c, i) => (
{c.ic}{c.t}{c.kbd}
))}
{q ? 'Threads · ' + filtered.length + ' match' + (filtered.length === 1 ? '' : 'es') : 'Recent threads'}
{filtered.length === 0 &&
No threads match "{q}". Try a model name, a date, or a partial proof statement.
}
{filtered.map(t => (
{ onSelect && onSelect(t.id); onClose(); }}>
∎
{t.title}{t.model} · {t.time}
↵
))}
↑↓ navigate↵ open⌘ ↵ open in new tabesc close
);
}
/* ---- 13. Rename thread ---- */
function RenameThreadModal({ onClose, current = '', onSave }) {
const [v, setV] = React.useState(current);
return (
>}>
setV(e.target.value)} style={{width:'100%',background:'var(--bg-canvas)',border:'1px solid var(--border-1)',borderRadius:'var(--r-2)',padding:'12px 14px',color:'var(--fg-1)',fontFamily:'var(--font-sans)',fontSize:14,outline:'none'}}/>
);
}
/* ---- 14. Move to project ---- */
function MoveToProjectModal({ onClose }) {
const [sel, setSel] = React.useState('p2');
const projects = [
{ id: 'p1', t: 'Riemann hypothesis · 2026', s: '12 threads · last update 2 hr ago' },
{ id: 'p2', t: 'Internal · proof-α evals', s: '38 threads · pinned' },
{ id: 'p3', t: 'Sandbox', s: '6 threads · draft' },
{ id: 'new', t: '+ New project', s: 'Create a project from this thread.' },
];
return (
>}>
{projects.map(p => (
))}
);
}
Object.assign(window, { ModalShell, NewKeyModal, RecoveryCodesModal, ChangePasswordModal, TotpModal, UpgradeModal, PhotoUploadModal, ConfirmModal, ExportModal, ValueEditModal, ShareModal, ExportThreadModal, SearchModal, RenameThreadModal, MoveToProjectModal });