// Loop MVP — Home + Meeting screens // HomeScreen, MeetingsScreen, BriefScreen, LiveMeetingScreen, RecapScreen const D = () => window.LOOP_DATA; // ───────────────────────────────────────────────────────────── // HomeScreen — orchestrated by Loop's AI coach layer. // Each module is ranked by relevance to today; the user can pin, // mute, or reorder anything from the orchestrator sheet. // ───────────────────────────────────────────────────────────── const HOME_MODULES = { next: { label: 'Next meeting', agent: 'Chief of staff', icon: 'calendar-clock' }, insights: { label: "Today's signals", agent: 'Chief of staff', icon: 'sparkles' }, weather: { label: 'Weather & outfit', agent: 'Daily ritual', icon: 'cloud-sun' }, spots: { label: 'Places near your day', agent: 'Chief of staff', icon: 'map-pin' }, health: { label: 'Health & recovery', agent: 'Health coach', icon: 'activity' }, kpis: { label: 'Company pulse', agent: 'Board & investor relations', icon: 'trending-up' }, trip: { label: 'Next trip', agent: 'Chief of staff', icon: 'plane' }, people: { label: 'My people', agent: 'People & hiring', icon: 'users' }, agenda: { label: 'Your day', agent: 'Chief of staff', icon: 'calendar' }, todos: { label: 'Follow-ups', agent: 'Chief of staff', icon: 'check-square' }, agents: { label: 'Agents running', agent: 'Orchestrator', icon: 'sparkles' }, notifs: { label: 'Signal not noise', agent: 'Orchestrator', icon: 'bell' }, }; const DEFAULT_ORDER = { principal: ['weather', 'next', 'insights', 'kpis', 'trip', 'spots', 'people', 'agenda', 'todos', 'agents', 'notifs'], moderator: ['weather', 'next', 'insights', 'kpis', 'people', 'agenda', 'spots', 'trip', 'todos', 'agents', 'notifs'], }; const DEFAULT_ENABLED = Object.keys(HOME_MODULES).reduce((a, k) => (a[k] = true, a), {}); function HomeScreen({ persona, go }) { const data = D(); const u = data[persona]; const meetings = data.meetings[persona]; const next = meetings.find(m => m.status === 'upcoming'); const todos = data.todos[persona].filter(t => t.priority === 'high').slice(0, 3); const notifs = data.notifications[persona].slice(0, 2); const ringPeople = data.people.filter(p => p.status === 'needs-attention' || p.status === 'warm').slice(0, 4); const trip = data.trip[persona]; const companies = data.companies[persona]; const [companyId, setCompanyId] = React.useState(companies[0].id); const company = companies.find(c => c.id === companyId) || companies[0]; const [enabled, setEnabled] = React.useState(DEFAULT_ENABLED); const [order, setOrder] = React.useState(DEFAULT_ORDER[persona]); const [sheetOpen, setSheetOpen] = React.useState(false); React.useEffect(() => { setOrder(DEFAULT_ORDER[persona]); setCompanyId(data.companies[persona][0].id); }, [persona]); const renderModule = (key) => { if (!enabled[key]) return null; switch (key) { case 'next': return next ? (
go('brief', { id: next.id })} style={{ padding: 24, position: 'relative', overflow: 'hidden' }}>
Next · {next.when}

{next.title}

{next.time} · {next.duration} · {next.location}
{next.participants && (
({ initials: p.initials, tint: '#CF4500', photo: (findPerson(p.name) || findPerson(p.initials) || {}).photo, }))} size={32} /> { e.stopPropagation(); go('brief', { id: next.id }); }} />
)}
) : null; case 'insights': return
; case 'weather': // When both weather + health are enabled, render them as a paired briefing. // Otherwise this slot renders the full weather module. if (enabled.health) { return
; } return
; case 'spots': return
; case 'health': // Already rendered alongside weather as a paired briefing if (enabled.weather) return null; return
; case 'kpis': return (
{company.name} } action={
{company.role}
}> {/* Company switcher — horizontal scroller of company chips */}
{companies.map(c => { const active = c.id === companyId; if (c.isAdd) { return ( ); } return ( ); })}
{company.meta}
{company.kpis.map(k => )}
); case 'trip': return (
Itinerary}>
); case 'people': return (
go('people')} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: 'var(--ink)', textDecoration: 'underline' }}>All}>
{ringPeople.map((p, i) => (
go('person', { id: p.id })} style={{ flexShrink: 0, width: 130, cursor: 'pointer', transform: i % 2 === 0 ? 'translateY(8px)' : 'translateY(-12px)' }}>
{p.name.split(' ')[0]}
{p.last}
))}
); case 'agenda': return (
go('meetings')} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: 'var(--ink)', textDecoration: 'underline' }}>All}> {meetings.slice(0, 4).map((m, i) => (
{m.time}
{m.duration}
} title={{m.title}} subtitle={`${m.location} · ${m.status === 'done' ? 'done' : m.when}`} trailing={} onClick={() => go(m.status === 'done' ? 'recap' : 'brief', { id: m.id })} /> ))}
); case 'todos': return (
go('tasks')} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: 'var(--ink)', textDecoration: 'underline' }}>All}> {todos.map((t, i) => (
} title={t.body} subtitle={`${t.from} · ${t.due}`} trailing={} onClick={() => go('tasks')} /> ))}
); case 'agents': return (
go('agents')} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 14, color: 'var(--ink)', textDecoration: 'underline' }}>All}>
{data.agents[persona].slice(0, 3).map(a => ( go('agent-chat', { id: a.id })} style={{ minWidth: 260, padding: 18 }}>
Live
{a.name}
{a.role}
{a.skills} skills · {a.runs}
))}
); case 'notifs': return (
{notifs.map((n, i) => ( } title={n.body} subtitle={n.when} onClick={() => go('notifications')} /> ))}
); default: return null; } }; return ( {/* Greeting block */}
{data.today}

{u.greeting}.

{u.contextLine}.

{order.map(k => renderModule(k))}
setSheetOpen(false)} enabled={enabled} setEnabled={setEnabled} order={order} setOrder={setOrder} modules={HOME_MODULES} /> ); } // ───────────────────────────────────────────────────────────── // KPI card // ───────────────────────────────────────────────────────────── function KPICard({ k }) { const trendColor = { up: 'var(--success)', down: 'var(--danger)', 'down-good': 'var(--success)', flat: 'var(--slate)', }[k.trend] || 'var(--slate)'; const trendIcon = { up: 'trending-up', down: 'trending-down', 'down-good': 'trending-down', flat: 'minus', }[k.trend] || 'minus'; return (
{k.label}
{k.value}
{k.delta} · {k.period}
); } // ───────────────────────────────────────────────────────────── // FlightCard — private jet headline, Qatar-airways inspired // Compact summary by default; expand to reveal full metadata. // ───────────────────────────────────────────────────────────── function FlightCard({ flight, persona }) { const [open, setOpen] = React.useState(false); return (
{/* Hero — fixed compact height, hero image + airport codes */} {/* Expanded meta */} {open && (
{flight.crew && flight.crew !== '—' && } {flight.catering && }
)}
); } function KV({ label, value }) { return (
{label}
{value}
); } // ───────────────────────────────────────────────────────────── // AccommodationCard — compact summary + expandable meta // ───────────────────────────────────────────────────────────── function AccommodationCard({ accom }) { const [open, setOpen] = React.useState(false); return (
{open && (
Concierge · {accom.concierge}
)}
); } // ───────────────────────────────────────────────────────────── // MeetingsScreen — full agenda list // ───────────────────────────────────────────────────────────── function MeetingsScreen({ persona, go }) { const meetings = D().meetings[persona]; const [filter, setFilter] = React.useState('today'); const filters = [ { key: 'today', label: 'Today' }, { key: 'week', label: 'This week' }, { key: 'past', label: 'Past' }, ]; return (
} />
{filters.map(f => ( ))}
{meetings.map((m, i) => { const past = m.status === 'done'; return (
go(past ? 'recap' : 'brief', { id: m.id })} style={{ display: 'flex', gap: 16, padding: '20px 0', borderBottom: i < meetings.length - 1 ? '1px solid rgba(20,20,19,0.08)' : 'none', cursor: 'pointer' }}>
{m.time}
{m.duration}
{m.kind} · {m.location}
{m.title}
{m.participants && (
({ initials: p.initials, tint: 'var(--ink)', photo: (findPerson(p.name) || findPerson(p.initials) || {}).photo, }))} size={24} /> {past ? 'recap ready' : m.when}
)}
); })}
); } function MeetingKindDot({ kind }) { const map = { investor: '#CF4500', board: '#9A3A0A', hiring: '#3860BE', interview: '#3860BE', internal: '#1F7A4D', sales: '#F37338', }; return ; } // ───────────────────────────────────────────────────────────── // BriefScreen — pre-meeting one-pager // ───────────────────────────────────────────────────────────── function BriefScreen({ persona, go, params }) { const data = D(); const m = data.meetings[persona].find(x => x.id === params?.id) || data.meetings[persona][0]; if (!m.agenda) { // not enough data for this meeting return (
go('meetings')} title="Brief" eyebrow={`${m.time} · ${m.location}`} />
Coming up

{m.title}

Loop is preparing the brief. It will be ready 30 minutes before the meeting.

); } return (
go('meetings')} title="Brief" eyebrow={`${m.time} · ${m.duration}`} trailing={} />
{m.kind} · {m.location}

{m.title}

{m.when} · prepared by Loop · 3 min read
{/* Participants — circular portraits per design system */}
{m.participants.map((p, i) => { const person = findPerson(p.name) || findPerson(p.initials); return (
person && go('person', { id: person.id })} style={{ flexShrink: 0, width: 170, cursor: 'pointer' }}>
{p.name}
{p.role}
{p.last}
); })}
{/* Talking points — the principal's edge */}
{m.talkingPoints.map((tp, i) => (
{String(i + 1).padStart(2, '0')}
{tp}
))}
{/* Agenda */}
{m.agenda.map((a, i) => ( {i + 1}.
} title={a} trailing={null} /> ))} {/* Open threads */}
{m.threads.map((t, i) => ( {t.label}
{t.body}
))}
{/* Files */}
{m.files.map((f, i) => (
} title={f.name} subtitle={f.meta} trailing={} /> ))}
{/* Floating bottom CTA */}
); } // ───────────────────────────────────────────────────────────── // LiveMeetingScreen — capture + live notes // ───────────────────────────────────────────────────────────── function LiveMeetingScreen({ persona, go, params }) { const data = D(); const m = data.meetings[persona].find(x => x.id === params?.id) || data.meetings[persona][0]; const [elapsed, setElapsed] = React.useState(247); // 4:07 const [notes, setNotes] = React.useState([]); const [tab, setTab] = React.useState('notes'); React.useEffect(() => { const id = setInterval(() => setElapsed(e => e + 1), 1000); return () => clearInterval(id); }, []); React.useEffect(() => { // Stream in fake notes const seed = persona === 'principal' ? [ { t: 0, kind: 'decision', who: 'Roelof', body: 'Series C target post-money: $420M. Comfortable with $400M floor.' }, { t: 4, kind: 'action', who: 'You', body: 'Send updated cap table by end of day.' }, { t: 9, kind: 'topic', who: 'Jess', body: 'Two board candidates in the next ten days — operator profile, no investors.' }, { t: 14, kind: 'decision', who: 'Room', body: 'Timeline target: signed term sheet by mid-August.' }, { t: 19, kind: 'action', who: 'Roelof', body: 'Roelof to send a model term sheet on Friday.' }, ] : [ { t: 0, kind: 'topic', who: 'Mae', body: 'Distributed inventory — walked the room through Stripe Issuing primitives.' }, { t: 5, kind: 'decision', who: 'Panel', body: 'Strong technical depth. 8/10 against rubric.' }, { t: 10, kind: 'risk', who: 'Devon', body: 'Some hesitation on cross-functional conflict resolution.' }, { t: 15, kind: 'action', who: 'You', body: 'Share EU GM context doc and schedule final with Aurélien.' }, { t: 20, kind: 'topic', who: 'Mae', body: 'Comp expectation: $340 base, 1.0% equity — open to negotiate.' }, ]; let i = 0; const id = setInterval(() => { if (i >= seed.length) return clearInterval(id); setNotes(n => [...n, seed[i]]); i++; }, 1800); return () => clearInterval(id); }, [persona]); const fmt = (s) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; const kindMeta = { decision: { tone: 'ink', label: 'Decision', icon: 'check-circle' }, action: { tone: 'signal', label: 'Action', icon: 'arrow-right-circle' }, topic: { tone: 'default', label: 'Topic', icon: 'message-square' }, risk: { tone: 'warning', label: 'Risk', icon: 'alert-triangle' }, }; return (
{/* Live header */}
Recording
{m.kind} · {m.location}

{m.title}

({ initials: p.initials, tint: '#CF4500', photo: (findPerson(p.name) || findPerson(p.initials) || {}).photo, })) || []} size={28} />
{fmt(elapsed)}
{/* Tabs */}
{['notes', 'transcript', 'people'].map(t => ( ))}
{tab === 'notes' && notes.map((n, i) => { const meta = kindMeta[n.kind]; return (
{meta.label} {n.who}
{n.body}
); })} {tab === 'notes' && notes.length === 0 && (
Listening…
)} {tab === 'transcript' && (

{m.participants[0].name}: Thanks for making time. We've been tracking the numbers — Q1 was strong.

You: 41% QoQ, came in ahead of plan. Burn is steady. Runway sits at 22 months.

{m.participants[0].name}: Good. On the Series C — I want to push for a September close. Founders Fund is circling.

You: I'd like two more weeks on diligence before we commit a date.

· Loop captured this as a decision pending. ·

)} {tab === 'people' && (
{m.participants.map((p, i) => { const person = findPerson(p.name) || findPerson(p.initials); return ( person && go('person', { id: person.id })}>
{p.name}
{p.role}
{p.last}
); })}
)}
{/* Floating control deck */}
Tap to add note ·
); } // ───────────────────────────────────────────────────────────── // RecapScreen — post-meeting summary // ───────────────────────────────────────────────────────────── function RecapScreen({ persona, go, params }) { const data = D(); const all = data.meetings[persona]; const m = all.find(x => x.id === params?.id) || all.find(x => x.status === 'done') || all[0]; const recap = persona === 'principal' ? { summary: 'Sequoia is anchored on a $420M post and a 6-week close. Roelof will send a model term sheet Friday; Jess will surface two operator-profile board candidates inside 10 days. You did not commit to a September close — that\'s yours to decide by Tuesday.', decisions: [ 'Series C target post-money: $420M, floor $400M.', 'Sequoia to lead. Roelof to send draft term sheet Friday.', 'Timeline: signed by mid-August. Aurélien to confirm by Tuesday.', ], actions: [ { who: 'You', body: 'Send updated cap table to Roelof — by today 18:00.', status: 'open' }, { who: 'You', body: 'Reply with September close decision — by Tuesday.', status: 'open' }, { who: 'Roelof', body: 'Send model term sheet — by Friday.', status: 'tracked' }, { who: 'Jess', body: 'Surface 2 board candidates — within 10 days.', status: 'tracked' }, ], sentiment: 'Warm. Roelof is leaning in. Jess is curious. Neither pushed on dilution.', crmWrite: 'Salesforce account "Sequoia · Series C" updated with stage, next step, deal size, and your sentiment read.', } : { summary: 'Mae cleared the technical bar (8/10 systems design). Soft on cross-functional conflict — Devon flagged hesitation. She is at final stage at Anthropic. You need to move within 48 hours or lose her. Comp ask sits inside our band.', decisions: [ 'Mae advances to final round with Aurélien.', 'Comp framework: $340 base + 1.0% equity is acceptable starting point.', 'Decision-by date: Monday EOD.', ], actions: [ { who: 'You', body: 'Send EU GM context doc to Mae — by 09:30.', status: 'open' }, { who: 'You', body: 'Schedule final with Aurélien — by EOD today.', status: 'open' }, { who: 'Devon', body: 'Add round 2 notes to Greenhouse — by EOD.', status: 'tracked' }, ], sentiment: 'Warm — Mae is bought into the mission. Comp negotiation will be friendly.', crmWrite: 'Greenhouse candidate "Mae Chen" moved to Final Round. Scorecard v3 attached.', }; return (
go('meetings')} title="Recap" eyebrow={`${m.time} · ${m.duration}`} trailing={} />
Done

{m.title}

Captured by Loop · 24-minute call · 4 decisions · 7 actions
{/* Executive summary */}

{recap.summary}

{/* Decisions */}
{recap.decisions.map((d, i) => (
{d}
))}
{/* Actions assigned */}
{recap.actions.map((a, i) => ( {a.who === 'You' ? 'You' : a.who[0]}
} title={a.body} subtitle={a.who === 'You' ? 'on your to-do list' : `tracked on ${a.who}`} trailing={{a.status}} /> ))} {/* Sentiment */}
{recap.sentiment}
{/* CRM writeback */}
Synced 2 minutes ago
{recap.crmWrite}
); } Object.assign(window, { HomeScreen, MeetingsScreen, BriefScreen, LiveMeetingScreen, RecapScreen });