// Kayser v2 — Quick menu (avatar dropdown) + Settings page (full surface) // Option C: separate "account stuff" from "how Kayser works." // QuickMenu = small dropdown anchored to the top-right ⋯ // SettingsPage = full takeover (mobile full-screen, desktop right pane) // Two-letter monogram from a display name. Handles single-word names // ("Madonna" → "MA"), email-derived fallbacks ("isaiah" → "IS"), and // missing names ("" → "·"). Used by the quick menu's avatar fallback // when /me hasn't returned a picture URL yet. function kxInitialsFromName(name) { if (!name) return '·'; const cleaned = String(name).trim(); if (!cleaned) return '·'; const parts = cleaned.split(/\s+/).filter(Boolean); if (parts.length >= 2) { return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); } // Single word — take the first two letters (handles "Isaiah" → "IS"). return cleaned.slice(0, 2).toUpperCase(); } // ============ QUICK MENU ============ // Small floating dropdown. Anchored to the ⋯ button. Closes on outside-click. // // `me` is the /api/v1/me response shape (or an empty object when the // fetch hasn't landed). When non-empty we render the operator's real // Google profile (name, business, picture); otherwise fall through to // the prototype's mock copy so the standalone demo still looks // presentable. function KxV2QuickMenu({ open, onClose, anchorRef, me, onOpenSettings, onOpenBilling, onOpenHelp, onSignOut }) { const ref = React.useRef(null); React.useEffect(() => { if (!open) return; const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) { onClose?.(); } }; // Use mouseup (after the button's onClick fires) so we don't immediately close document.addEventListener('mouseup', onDoc); return () => document.removeEventListener('mouseup', onDoc); }, [open, onClose]); if (!open) return null; const isDesktop = !!window.__kxDesktop; // Resolve identity. Empty `me` (no token, or /me still in flight) // gets the prototype defaults so the menu always looks intentional. const live = !!(me && me.email); const displayName = live ? (me.name || me.email.split('@')[0]) : 'Wes Kayser'; const businessName = live ? (me.business || '') : 'Kayser Foundation Repair'; const pictureUrl = live ? me.picture : null; const initials = kxInitialsFromName(displayName); return (
{/* Profile header */}
{pictureUrl ? ( // Real Google profile picture. `referrerPolicy=no-referrer` is // required — Google's CDN 403s requests carrying our origin. ) : (
{initials}
)}
{displayName}
{businessName}
{/* PRO badge: only render in prototype mode. We don't have a real billing read yet, so showing it for every signed-in operator would be a lie. Comes back in the billing wire-up. */} {!live && ( PRO )}
); } function QuickMenuRow({ icon, label, hint, onClick, danger }) { return ( ); } // ============ SETTINGS PAGE ============ // Full settings surface. On desktop it takes over the right pane (renders inside it). // On mobile it opens as a full-screen sheet (no rounded top — covers the whole app). // Sections inside open their own sub-sheets (Trust, Memory, Conversations, Automations) // so we reuse existing UIs. const KXV2_SETTINGS_SECTIONS = [ { title: 'How I work', rows: [ { id: 'trust', icon: '◐', label: 'Trust & autonomy', hint: 'Adjust how much I do alone', tag: null }, { id: 'memory', icon: '◈', label: 'What I know about you', hint: 'Memory & profile · 11 facts', tag: null }, { id: 'automations', icon: '↗', label: 'Automations', hint: 'Routines you taught me · 4', tag: null }, { id: 'conversations', icon: '◉', label: 'Conversations', hint: 'Browse by customer or job', tag: null }, ], }, { title: 'Connections', rows: [ { id: 'gmail', icon: 'G', label: 'Gmail', hint: 'wes@kayserrepair.com · synced 4m ago', tag: 'OK' }, { id: 'twilio', icon: 'T', label: 'Twilio SMS', hint: '+1 817 555 0142 · A2P approved', tag: 'OK' }, { id: 'quickbooks', icon: 'Q', label: 'QuickBooks', hint: 'Synced 4m ago', tag: 'OK' }, { id: 'calendar', icon: 'C', label: 'Google Calendar', hint: 'Synced 1h ago', tag: 'OK' }, { id: 'stripe', icon: 'S', label: 'Stripe', hint: 'Connect to track payments', tag: 'CONNECT' }, ], }, { title: 'Notifications', rows: [ { id: 'push', icon: '◌', label: 'Push notifications', hint: 'On — banners + sound', tag: null }, { id: 'quiet', icon: '◷', label: 'Quiet hours', hint: "Don't ping me 7pm – 7am", tag: null }, { id: 'priority', icon: '▲', label: 'What to interrupt me for', hint: 'Anything over $5K or new leads', tag: null }, ], }, { title: 'Account', rows: [ { id: 'profile', icon: '◉', label: 'Profile', hint: 'Wes Kayser · Kayser Foundation Repair', tag: null }, { id: 'industry', icon: '◆', label: 'Industry', hint: 'Foundation repair (affects vocab)', tag: null }, { id: 'team', icon: '◑', label: 'Team', hint: '5 people · 2 leads', tag: null }, { id: 'billing', icon: '$', label: 'Billing', hint: 'Pro · $49/mo · renews May 12', tag: null }, ], }, { title: 'Danger zone', rows: [ { id: 'pause', icon: '⊘', label: 'Pause Kayser', hint: 'Take a break from automations', tag: null, danger: true }, { id: 'reset', icon: '↺', label: 'Reset memory', hint: 'Forget what I know about you', tag: null, danger: true }, { id: 'delete', icon: '✕', label: 'Delete account', hint: null, tag: null, danger: true }, ], }, ]; function KxV2SettingsPage({ open, onClose, onOpen, me }) { const [section, setSection] = React.useState('how-i-work'); if (!open) return null; const isDesktop = !!window.__kxDesktop; // Per-row hint overrides driven by /me. Without these, the // "Profile" row reads "Wes Kayser · Kayser Foundation Repair" for // every operator. Other rows (industry, team, billing, memory count, // automation count, connections sync state) need their own backends // before we can replace their hardcoded hints — flagged for the next // truthfulness pass. const live = !!(me && me.email); const displayName = live ? (me.name || me.email.split('@')[0]) : null; const businessName = live ? me.business : null; const dynamicHints = live ? { profile: [displayName, businessName].filter(Boolean).join(' · '), } : {}; const body = (
{/* Header */}
Settings
{/* Body — list of sections w/ rows */}
{KXV2_SETTINGS_SECTIONS.map((s, si) => (
{s.title}
{s.rows.map(r => ( onOpen?.(r.id)} /> ))}
))}
Kayser · v2.4 · Pro plan
Your data lives on your dedicated VPS. Nothing here is used to train other models.
); if (isDesktop) { // Right-side drawer on desktop return (
e.stopPropagation()} style={{ width: 'min(560px, 60vw)', height: '100%', borderLeft: `1px solid ${KX2.glassLine}`, animation: 'kxv2SlideInRight 220ms cubic-bezier(.2,.8,.2,1)', }}>{body}
); } // Mobile / tablet: full-screen overlay return (
{body}
); } function SettingsRow({ row, onClick }) { return ( ); } Object.assign(window, { KxV2QuickMenu, KxV2SettingsPage, KXV2_SETTINGS_SECTIONS, KxV2BillingSheet, KxV2PauseSheet, KxV2ServiceSheet, }); // ============ BILLING SHEET ============ function KxV2BillingSheet({ open, onClose }) { return (
{/* Current plan */}
Kayser Pro ACTIVE
$49/month · billed monthly
Next charge May 12 · Visa ending 4242
{/* This month usage */}
This month
{[ { k: 'Actions handled', v: '124', sub: 'unlimited on Pro' }, { k: 'AI tokens', v: '~1.2M', sub: 'unlimited on Pro' }, { k: 'Connected services', v: '4', sub: 'unlimited on Pro' }, { k: 'Storage', v: '2.1 GB', sub: 'of 50 GB' }, ].map(s => (
{s.k.toUpperCase()}
{s.v}
{s.sub}
))}
{/* Actions */}
Billing handled by Stripe · receipts emailed to your Google account
); } // ============ PAUSE SHEET ============ function KxV2PauseSheet({ open, onClose, paused, onTogglePause }) { return (
{paused ? '▶' : '⏸'}
{paused ? 'Pick up where I left off?' : 'Take a break from automations?'}
{paused ? "I'll resume watching email, calendar, and texts. Any backlog from while I was paused will surface as a recap." : "I'll stop watching for things and stop sending. You can still chat with me. Turn me back on anytime."}
{paused ? 'When I resume:' : 'While paused:'} {' '} {paused ? "I'll show you everything that happened, ask before catching up, and start fresh." : "No auto-handled actions. No proactive pings. No scheduled sends. Already-sent stuff stays sent."}
); } // ============ CONNECTED SERVICE DETAIL SHEET ============ const KXV2_SERVICE_DETAILS = { gmail: { name: 'Gmail', account: 'wes@kayserrepair.com', status: 'Connected', syncing: 'Last synced 4m ago', permissions: ['Read inbound messages', 'Draft and send replies (on approval)', 'Apply labels to organize'], }, twilio: { name: 'Twilio SMS', account: '+1 817 555 0142', status: 'Connected · A2P approved', syncing: 'Last message 14m ago', permissions: ['Receive inbound texts', 'Send texts (on approval)', 'Track delivery and reads'], }, quickbooks: { name: 'QuickBooks', account: 'Kayser Foundation Repair, LLC', status: 'Connected', syncing: 'Last synced 4m ago', permissions: ['Read invoices, bills, customers', 'Create draft invoices (on approval)', 'Log expenses to jobs'], }, calendar: { name: 'Google Calendar', account: 'wes@kayserrepair.com · Primary', status: 'Connected', syncing: 'Last synced 1h ago', permissions: ['Read events and busy times', 'Create and reschedule events (on approval)', 'Send invitations'], }, stripe: { name: 'Stripe', account: null, status: 'Not connected', syncing: null, permissions: ['Read payments, payouts, fees', 'Reconcile payments to invoices'], }, }; function KxV2ServiceSheet({ open, service, onClose }) { const d = service ? KXV2_SERVICE_DETAILS[service] : null; if (!d) return null; const connected = d.status !== 'Not connected'; return (
{d.status}
{d.account &&
{d.account}
} {d.syncing &&
{d.syncing}
}
What I can do
{d.permissions.map((p, i) => (
{p}
))}
{connected ? (
) : ( )}
); }