import { Button } from "@rakazo/ui-web"; import { useEffect, useMemo, useRef, useState } from "react"; import { type DemoBot, type DemoMessage, type DemoRoutine, type DemoRoutineRun, type DemoScreen, } from "../demo"; import { demoText, getDemoBots } from "../i18n/demo"; import type { Locale } from "../i18n/locales"; import { LandingBotAvatar } from "./LandingBotAvatar"; import { type DemoTrigger, defaultTrigger, describeTrigger, displayRoutineWhen, parseWhen, resolveRoutineWhen, } from "./product-demo-when"; type DemoTranslator = (source: string, values?: Record) => string; const BOT_COLORS = ["#3EC5A8", "#F5A03C", "#6A6BF5", "#9B5CF6", "#3B82F6", "#F2622A", "#D9508A"]; const FREQS = [ "Every hour", "Every day", "Weekdays", "Every week", "Every month", "Interval", "Advanced", ]; const UNITS = ["minutes", "hours", "days"]; const NUMBERS = [1, 2, 3, 5, 10, 15, 30, 45]; const TIMES = [ "6:00 AM", "7:00 AM", "8:00 AM", "9:00 AM", "12:00 PM", "3:00 PM", "6:00 PM", "9:00 PM", ]; const ONBOARD = [ { q: "What do you mainly want me helping with?", sub: "Pick whatever’s closest, or type your own.", opts: [ "Inbox & email", "Slack & messages", "Coding & repos", "Research & writing", "A bit of everything", ], ack: "{answer} is a sweet spot for me.", }, { q: "How do you want me to write?", sub: "I’ll match this unless you say otherwise on a specific piece.", opts: [ "Clear and tight", "Warm and conversational", "Polished / formal", "Match whatever I draft", ], ack: "Got it. {answer} it is.", }, { q: "Where does most of that work live?", sub: "So I know where to pull from and drop drafts.", opts: ["Google Docs", "Notion", "Just chat / paste here", "A mix"], ack: "Noted. I’ll pull from {answer} and leave drafts there too.", }, ]; type ExtraMessages = Record; type PanelMode = "computer" | "settings" | "routine"; type LiveBot = DemoBot & { title: string; description: string; onboarding: boolean; answers: string[]; }; type Trigger = DemoTrigger; type RoutineDraft = { index: number | null; name: string; instruction: string; active: boolean; triggers: Trigger[]; runs: DemoRoutineRun[]; /** Original English `when` from the bot; kept when triggers are unchanged. */ sourceWhen?: string; }; function cloneBots(source: DemoBot[]): LiveBot[] { return source.map((bot) => ({ ...bot, routines: bot.routines.map((routine) => ({ ...routine, runs: routine.runs?.map((run) => ({ ...run })), })), title: "", description: "", onboarding: false, answers: [], })); } function displayedWhen(when: string, text: DemoTranslator): string { return displayRoutineWhen(when, text); } function previewForBot(bot: LiveBot, extra: ExtraMessages) { const last = extra[bot.id]?.at(-1); if (last && "text" in last) { return last.text; } if (bot.onboarding && bot.answers.length > 0) { return bot.answers.at(-1) ?? bot.preview; } return bot.preview; } const BOOT_STEPS: Record = { 8: "Allocating a machine", 46: "Restoring the session", 82: "Opening the browser", 100: "Handing you the screen", }; function ComputerDesktop({ screen, large = false }: { screen: DemoScreen; large?: boolean }) { return (
{screen.host}
{screen.title}
{screen.lines.map((line) => (
{line}
))}
); } function Thread({ messages, text }: { messages: DemoMessage[]; text: DemoTranslator }) { return ( <> {messages.map((message, index) => { if (message.type === "time") { return (
{message.text}
); } if (message.type === "meta") { return (
{message.text}
); } if (message.type === "card") { return (
{message.lines.map((line) => (
✓ {line.k} → {line.v}
))}
); } if (message.type === "typing") { return (
{text("working…")}
); } return (
{message.text}
); })} ); } function OnboardThread({ answers, onAnswer, text, }: { answers: string[]; onAnswer: (value: string) => void; text: DemoTranslator; }) { return ( <>
{text("Today")}
{text("Hey Avery. Good to meet you.")}
{ONBOARD.map((step, index) => { const answer = answers[index]; if (answer !== undefined) { const letter = String.fromCharCode(65 + Math.max(0, step.opts.indexOf(answer))); return (
{text(step.q)}
{letter} {text(answer)} ✓
{text(step.ack, { answer: text(answer) })}
); } if (answers.length !== index) { return null; } return (
{text(step.q)}
{text(step.sub)}
{step.opts.map((opt, optIndex) => ( ))}
{text("Type your own answer")}
); })} {answers.length === ONBOARD.length ? (
{text( "That’s everything I need. Give me a first job whenever you’re ready. I’ll ask before anything leaves the building.", )}
) : null} ); } export function ProductDemo({ locale = "en" }: { locale?: Locale }) { const text: DemoTranslator = (source, values) => demoText(locale, source, values); const [bots, setBots] = useState(() => cloneBots(getDemoBots(locale))); const [activeId, setActiveId] = useState("inbox"); const [panelOpen, setPanelOpen] = useState(true); const [menuOpen, setMenuOpen] = useState(false); const [compact, setCompact] = useState(false); const [panelMode, setPanelMode] = useState("computer"); const [hasControl, setHasControl] = useState(false); const [takeover, setTakeover] = useState(false); const [bootPct, setBootPct] = useState(0); const [draft, setDraft] = useState(""); const [query, setQuery] = useState(""); const [extra, setExtra] = useState({}); const [routineDraft, setRoutineDraft] = useState(null); const scrollRef = useRef(null); const timersRef = useRef([]); const menuButtonRef = useRef(null); const widePanelRef = useRef(true); const booting = bootPct > 0; const active = bots.find((bot) => bot.id === activeId) ?? bots[0]; const messages = useMemo(() => { if (!active) { return []; } return active.thread.concat(extra[active.id] ?? []); }, [active, extra]); const filtered = useMemo(() => { const needle = query.trim().toLowerCase(); if (!needle) { return bots; } return bots.filter((bot) => `${bot.name} ${bot.preview}`.toLowerCase().includes(needle)); }, [bots, query]); const onboardingOpen = Boolean(active?.onboarding && active.answers.length < ONBOARD.length); useEffect(() => { setHasControl(false); setTakeover(false); setBootPct(0); }, [activeId]); useEffect(() => { const el = scrollRef.current; if (el) { el.scrollTop = el.scrollHeight; } }, [active?.id, messages.length, active?.answers.length]); useEffect(() => { return () => { for (const timer of timersRef.current) { window.clearTimeout(timer); } }; }, []); useEffect(() => { const query = window.matchMedia("(max-width: 860px)"); const sync = () => setCompact(query.matches); sync(); query.addEventListener("change", sync); return () => query.removeEventListener("change", sync); }, []); // Phone widths show one screen at a time, so the bot list starts hidden behind // the hamburger and the side panel stays closed until asked for. Growing the // window back restores however the panel was left at wide widths: this effect // only runs when `compact` flips, so it closes over that render's panelOpen. useEffect(() => { if (compact) { widePanelRef.current = panelOpen; setPanelOpen(false); } else { setMenuOpen(false); setPanelOpen(widePanelRef.current); } }, [compact]); useEffect(() => { if (!menuOpen) { return; } function onKey(event: KeyboardEvent) { if (event.key === "Escape") { closeMenu(); } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [menuOpen]); useEffect(() => { if (!takeover && !booting) { return; } function onKey(event: KeyboardEvent) { if (event.key === "Escape") { setTakeover(false); setBootPct(0); } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [takeover, booting]); if (!active) { return null; } function schedule(callback: () => void, delayMs: number) { const timer = window.setTimeout(() => { timersRef.current = timersRef.current.filter((pending) => pending !== timer); callback(); }, delayMs); timersRef.current.push(timer); } function closeMenu() { if (!menuOpen) { return; } setMenuOpen(false); // The drawer is hidden from the focus order once closed, so hand focus back // to the control that opened it. menuButtonRef.current?.focus(); } function openComputer() { setPanelOpen(true); setPanelMode("computer"); setRoutineDraft(null); } function toggleComputer() { if (panelOpen && panelMode === "computer") { setPanelOpen(false); return; } openComputer(); } function openSettings() { setPanelOpen(true); setPanelMode("settings"); setRoutineDraft(null); } function openRoutine(routine: DemoRoutine | null, index: number | null) { setPanelOpen(true); setPanelMode("routine"); setRoutineDraft({ index, name: routine?.name ?? "", instruction: routine?.instruction ?? "", active: routine?.active ?? true, triggers: routine ? [parseWhen(routine.when)] : [], runs: routine?.runs?.map((run) => ({ ...run })) ?? [], sourceWhen: routine?.when, }); } function patchActive(patch: Partial) { setBots((current) => current.map((bot) => (bot.id === activeId ? { ...bot, ...patch } : bot))); } function changeRoutine(patch: Partial) { setRoutineDraft((current) => { if (!current) return current; // Drop opaque sourceWhen once the user edits triggers so an explicit // daily 9:00 AM choice is not overwritten by the seeded custom label. if (patch.triggers !== undefined) { return { ...current, ...patch, sourceWhen: undefined }; } return { ...current, ...patch }; }); } function persistRoutine(draftState: RoutineDraft) { const index = draftState.index ?? active.routines.length; const next: DemoRoutine = { name: draftState.name.trim() || text("Untitled routine"), when: resolveRoutineWhen(draftState.triggers, draftState.sourceWhen), instruction: draftState.instruction, active: draftState.active, runs: draftState.runs, }; setBots((current) => current.map((bot) => { if (bot.id !== activeId) { return bot; } const routines = [...bot.routines]; if (index === routines.length) { routines.push(next); } else { routines[index] = next; } return { ...bot, routines }; }), ); return index; } function saveRoutine() { if (!routineDraft) { return; } persistRoutine(routineDraft); openComputer(); } function deleteRoutine() { if (routineDraft?.index !== null && routineDraft) { patchActive({ routines: active.routines.filter((_, index) => index !== routineDraft.index) }); } openComputer(); } function startNewBot() { const color = BOT_COLORS[bots.length % BOT_COLORS.length] ?? "#3EC5A8"; const bot: LiveBot = { id: `bot-${Date.now()}`, name: text("New bot"), color, time: text("Now"), preview: text("Say what you want this bot doing"), title: "", description: "", onboarding: true, answers: [], routines: [], screen: { host: "desktop", title: text("Computer is stopped"), lines: [] }, thread: [], reply: text("on it. tell me the job and i’ll get started."), }; setBots((current) => [bot, ...current]); setActiveId(bot.id); setDraft(""); closeMenu(); openSettings(); } function answerOnboard(value: string) { if (!active.onboarding || active.answers.length >= ONBOARD.length) { return; } patchActive({ answers: [...active.answers, value] }); } function closeOverlay() { for (const timer of timersRef.current.splice(0)) { window.clearTimeout(timer); } setTakeover(false); setBootPct(0); } function releaseControl() { setHasControl(false); closeOverlay(); } function takeControl() { if (booting) { return; } if (hasControl) { setTakeover(true); return; } for (const timer of timersRef.current.splice(0)) { window.clearTimeout(timer); } setBootPct(8); schedule(() => setBootPct(46), 450); schedule(() => setBootPct(82), 1100); schedule(() => setBootPct(100), 1750); schedule(() => { setBootPct(0); setHasControl(true); setTakeover(true); }, 2300); } function appendMessage(botId: string, message: DemoMessage) { setExtra((current) => ({ ...current, [botId]: [...(current[botId] ?? []), message], })); } function send() { const text = draft.trim(); if (!text) { return; } setDraft(""); if (onboardingOpen) { answerOnboard(text); return; } const botId = active.id; const reply = active.reply; appendMessage(botId, { type: "user", text }); schedule(() => appendMessage(botId, { type: "typing" }), 280); schedule(() => { setExtra((current) => { const withoutTyping = (current[botId] ?? []).filter((message) => message.type !== "typing"); return { ...current, [botId]: [...withoutTyping, { type: "bot", text: reply }] }; }); }, 1350); } function selectBot(id: string) { setActiveId(id); closeMenu(); if (panelMode === "routine") { setPanelMode("computer"); setRoutineDraft(null); } } function patchTrigger(index: number, patch: Partial) { changeRoutine({ triggers: (routineDraft?.triggers ?? []).map((trigger, triggerIndex) => triggerIndex === index ? { ...trigger, ...patch } : trigger, ), }); } function testRun() { if (!routineDraft?.name.trim()) { return; } const completedRun: DemoRoutineRun = { mark: "●", color: "#4ECB71", text: text("Completed"), time: text("Just now"), }; const index = persistRoutine({ ...routineDraft, runs: [...routineDraft.runs, completedRun], }); setRoutineDraft((current) => current ? { ...current, index, runs: [...current.runs, completedRun], } : current, ); appendMessage(active.id, { type: "meta", text: text("Routine ran · {name}", { name: routineDraft.name }), }); } return (
{menuOpen ? (
{active.onboarding ? ( ) : null} {messages.length === 0 && !active.onboarding ? (
{text("Message {name} to give it a first job.", { name: active.name })}
) : ( )}
setDraft(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); send(); } }} placeholder={ onboardingOpen ? text("Type your own answer") : text("Message {name}", { name: active.name }) } aria-label={ onboardingOpen ? text("Type your own answer") : text("Message {name}", { name: active.name }) } />
{panelOpen ? (