// 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 (
{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."}