"use client"; import { AnimatePresence, LayoutGroup, MotionConfig, motion, useReducedMotion } from "motion/react"; import { ArrowUpIcon, CalendarIcon, CheckIcon, CircleDashedIcon, FileTextIcon, GaugeIcon, HandIcon, LoaderCircleIcon, MegaphoneIcon, MessageSquareQuoteIcon, RotateCcwIcon, SendIcon, SparklesIcon, UsersIcon, WalletIcon, WorkflowIcon, } from "lucide-react"; import { type ComponentType, type KeyboardEvent, type ReactNode, useEffect, useId, useReducer, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export type AgentTone = "orange" | "teal" | "violet" | "info" | "rose"; export interface SubAgent { id: string; name: string; role: string; tone: AgentTone; actions: string[]; result?: string; } export interface BriefField { key: string; label: string; kind: "text" | "textarea" | "chips"; placeholder?: string; } export interface ConductorProps { agentName?: string; briefTitle?: string; fields?: BriefField[]; agents?: SubAgent[]; channelOptions?: string[]; autoPlay?: boolean; className?: string; } type Status = "queued" | "working" | "waiting" | "done"; interface Msg { id: string; role: "user" | "agent"; text: string; shown: number; spawned?: string[]; updated?: number; ask?: boolean; } interface FieldState { value: string | string[]; target?: string; by?: "agent" | "you"; flash?: number; } interface AgentState { status: Status; progress: number; step: number; } interface State { msgs: Msg[]; fields: Record; agents: Record; cue: number; seq: number; waiting: boolean; done: boolean; } const tone: Record = { orange: { dot: "bg-orange-500", soft: "bg-orange-500/14 text-orange-700 dark:text-orange-300", stroke: "stroke-orange-500" }, teal: { dot: "bg-teal-500", soft: "bg-teal-500/14 text-teal-700 dark:text-teal-300", stroke: "stroke-teal-500" }, violet: { dot: "bg-violet-500", soft: "bg-violet-500/16 text-violet-700 dark:text-violet-300", stroke: "stroke-violet-500" }, rose: { dot: "bg-rose-500", soft: "bg-rose-500/14 text-rose-700 dark:text-rose-300", stroke: "stroke-rose-500" }, info: { dot: "bg-info", soft: "bg-info/14 text-info-foreground", stroke: "stroke-info" }, }; const fieldIcons: Record> = { date: CalendarIcon, audience: UsersIcon, budget: WalletIcon, metric: GaugeIcon, tone: MessageSquareQuoteIcon, channels: MegaphoneIcon, }; const defaultAgents: SubAgent[] = [ { id: "scout", name: "Scout", role: "Research", tone: "teal", actions: ["Reading 12 agency launch posts", "Comparing 4 calendar tools", "Summarising what worked"], result: "3 launch patterns from 12 posts" }, { id: "ledger", name: "Ledger", role: "Budget", tone: "violet", actions: ["Splitting $40k across channels", "Checking cost-per-trial benchmarks", "Drafting the budget table"], result: "Budget table, $18 per trial" }, { id: "quill", name: "Quill", role: "Copy", tone: "orange", actions: ["Drafting six taglines", "Writing launch email v1", "Needs channels to finish"], result: "6 taglines and launch email v1" }, { id: "iris", name: "Iris", role: "Design", tone: "info", actions: ["Sizing assets per channel", "Pulling a warm palette", "Exporting the moodboard"], result: "14 assets sized for 2 channels" }, ]; const defaultFields: BriefField[] = [ { key: "project", label: "Project", kind: "text", placeholder: "Untitled launch" }, { key: "date", label: "Launch date", kind: "text", placeholder: "Empty" }, { key: "audience", label: "Audience", kind: "chips" }, { key: "budget", label: "Budget", kind: "text", placeholder: "Empty" }, { key: "goal", label: "Goal", kind: "textarea", placeholder: "One sentence on why this matters" }, { key: "metric", label: "Target", kind: "text", placeholder: "Empty" }, { key: "tone", label: "Voice", kind: "text", placeholder: "Empty" }, { key: "channels", label: "Channels", kind: "chips" }, ]; const defaultChannels = ["Newsletter", "Product Hunt", "LinkedIn", "Partner agencies", "Podcasts"]; type Cue = | { wait: number; user: string } | { wait: number; agent: string; spawned?: string[]; updated?: number; ask?: boolean } | { wait: number; fill: Record } | { wait: number; start: string[] }; const script: Cue[] = [ { wait: 500, user: "We're launching Orbit Calendar on Nov 18. Need a launch plan aimed at small agencies, budget around $40k." }, { wait: 900, agent: "On it. I've opened a brief and filled in what you gave me. Scout is reading how similar tools launched, and Ledger is sketching a budget split.", spawned: ["scout", "ledger"], updated: 4 }, { wait: 200, fill: { project: "Orbit Calendar launch", date: "Nov 18, 2026", audience: ["Small agencies", "Freelance studios"], budget: "$40,000" } }, { wait: 300, start: ["scout", "ledger"] }, { wait: 4200, user: "Main goal is 2,000 trial signups in the first month. Keep it warm, not salesy." }, { wait: 900, agent: "Got it. Goal, metric and voice are in. Quill is drafting taglines in that tone.", spawned: ["quill"], updated: 3 }, { wait: 200, fill: { goal: "Get small agencies to try Orbit Calendar as the one planner the whole team shares.", metric: "2,000 trial signups in 30 days", tone: "Warm and plain-spoken, no hype" } }, { wait: 300, start: ["quill"] }, { wait: 4800, agent: "Scout found that agency tools do best through partner referrals and a short newsletter run. Which channels should we commit to?", ask: true }, ]; const finish: Cue[] = [ { wait: 700, agent: "Perfect. Channels are set, Iris is sizing assets for each, and Quill can finish the copy. The brief is complete whenever you are.", spawned: ["iris"], updated: 1 }, { wait: 200, start: ["iris"] }, ]; type Action = | { type: "cue"; cue: Cue } | { type: "tick"; agents: SubAgent[] } | { type: "type" } | { type: "edit"; key: string; value: string | string[] } | { type: "say"; text: string } | { type: "channels"; picks: string[] } | { type: "reset"; fields: BriefField[]; agents: SubAgent[] }; const ease = [0.23, 1, 0.32, 1] as const; function initial(fields: BriefField[], agents: SubAgent[]): State { return { msgs: [], fields: Object.fromEntries(fields.map((f) => [f.key, { value: f.kind === "chips" ? [] : "" }])), agents: Object.fromEntries(agents.map((a) => [a.id, { status: "queued" as Status, progress: 0, step: 0 }])), cue: 0, seq: 0, waiting: false, done: false, }; } function apply(state: State, cue: Cue): State { const seq = state.seq + 1; const id = `m${seq}`; if ("user" in cue) { return { ...state, seq, msgs: [...state.msgs, { id, role: "user", text: cue.user, shown: cue.user.length }] }; } if ("agent" in cue) { const msg: Msg = { id, role: "agent", text: cue.agent, shown: 0, spawned: cue.spawned, updated: cue.updated, ask: cue.ask }; return { ...state, seq, msgs: [...state.msgs, msg], waiting: Boolean(cue.ask) }; } if ("fill" in cue) { const fields = { ...state.fields }; for (const [k, v] of Object.entries(cue.fill)) { fields[k] = Array.isArray(v) ? { value: v, by: "agent", flash: seq } : { value: "", target: v, by: "agent", flash: seq }; } return { ...state, seq, fields }; } const agents = { ...state.agents }; for (const aid of cue.start) agents[aid] = { ...agents[aid], status: "working" }; return { ...state, agents }; } function reducer(state: State, action: Action): State { switch (action.type) { case "cue": return { ...apply(state, action.cue), cue: state.cue + 1 }; case "type": { let changed = false; const msgs = state.msgs.map((m) => { if (m.shown >= m.text.length) return m; changed = true; return { ...m, shown: Math.min(m.text.length, m.shown + 3) }; }); const fields = { ...state.fields }; for (const [k, f] of Object.entries(fields)) { if (f.target && typeof f.value === "string" && f.value.length < f.target.length) { changed = true; fields[k] = { ...f, value: f.target.slice(0, f.value.length + 2) }; } } return changed ? { ...state, msgs, fields } : state; } case "tick": { let changed = false; const agents = { ...state.agents }; for (const a of action.agents) { const s = agents[a.id]; if (!s || s.status !== "working") continue; changed = true; const progress = Math.min(100, s.progress + 3 + ((s.progress * 7 + a.id.length) % 5)); const step = Math.min(a.actions.length - 1, Math.floor((progress / 100) * a.actions.length)); const needsChannels = a.id === "quill" && (state.fields.channels?.value as string[]).length === 0; if (needsChannels && progress >= 78) { agents[a.id] = { status: "waiting", progress: 78, step: a.actions.length - 1 }; } else { agents[a.id] = { status: progress >= 100 ? "done" : "working", progress, step }; } } return changed ? { ...state, agents } : state; } case "edit": return { ...state, fields: { ...state.fields, [action.key]: { value: action.value, by: "you" } } }; case "say": return { ...state, seq: state.seq + 2, msgs: [ ...state.msgs, { id: `m${state.seq + 1}`, role: "user", text: action.text, shown: action.text.length }, { id: `m${state.seq + 2}`, role: "agent", text: "Noted. I'll pass that to the helpers so they pick it up on their next pass.", shown: 0, }, ], }; case "channels": { const agents = { ...state.agents }; if (agents.quill?.status === "waiting") agents.quill = { ...agents.quill, status: "working" }; const text = action.picks.join(", "); const seq = state.seq + 1; return { ...state, seq, waiting: false, done: true, agents, msgs: [...state.msgs, { id: `m${seq}`, role: "user", text, shown: text.length }], fields: { ...state.fields, channels: { value: action.picks, by: "agent", flash: seq } }, }; } case "reset": return initial(action.fields, action.agents); } } function statusLabel(s: AgentState, a: SubAgent) { if (s.status === "done") return a.result ?? "Finished"; if (s.status === "queued") return "Queued"; return a.actions[s.step]; } function AgentMark({ agent, state, size = "md" }: { agent: SubAgent; state: AgentState; size?: "sm" | "md" }) { const reduce = useReducedMotion(); const t = tone[agent.tone]; const sm = size === "sm"; return ( {!sm && ( )} {agent.name[0]} {!sm && state.status === "done" && ( )} ); } function AgentPanel({ agents, state }: { agents: SubAgent[]; state: State }) { const active = agents.filter((a) => state.agents[a.id]?.status !== "queued" || state.msgs.some((m) => m.spawned?.includes(a.id))); const working = agents.filter((a) => state.agents[a.id]?.status === "working").length; const waiting = agents.filter((a) => state.agents[a.id]?.status === "waiting").length; const done = active.length > 0 && active.every((a) => state.agents[a.id]?.status === "done"); const summary = working ? `${working} working` : waiting ? `${waiting} needs you` : done ? `All ${active.length} done` : active.length ? "Idle" : "None yet"; return (
Helpers {summary}
{active.length === 0 ? (
Helpers show up here as work is handed out.
) : (
    {active.map((a) => { const s = state.agents[a.id]; return (
    {a.name} {a.role} {s.status === "waiting" ? "Needs you" : s.status === "done" ? "Done" : s.status === "queued" ? "" : `${s.progress}%`}
    {statusLabel(s, a)}
    ); })}
)}
); } function Flash({ seq }: { seq?: number }) { const mounted = useRef(seq); return ( {seq && seq !== mounted.current ? ( ) : null} ); } function Provenance({ by, agentName }: { by?: "agent" | "you"; agentName: string }) { if (by !== "agent") return null; return ( Filled by {agentName} ); } function BriefDoc({ title, fields, state, onEdit, agentName, }: { title: string; fields: BriefField[]; state: State; onEdit: (key: string, value: string | string[]) => void; agentName: string; }) { const id = useId(); const filled = fields.filter((f) => { const v = state.fields[f.key]?.value; return Array.isArray(v) ? v.length > 0 : Boolean(v); }).length; const pct = filled / fields.length; const head = fields.find((f) => f.kind === "text"); const props = fields.filter((f) => f !== head && f.kind !== "textarea"); const sections = fields.filter((f) => f.kind === "textarea"); const headState = head ? state.fields[head.key] : undefined; return (
{title} {filled} of {fields.length}
e.preventDefault()}> {head && (
onEdit(head.key, e.target.value)} placeholder={head.placeholder} value={(headState?.value as string) ?? ""} />
)}

{agentName} fills this in as you talk. Anything you edit stays yours.

{props.map((f) => { const s = state.fields[f.key] ?? { value: "" }; const fid = `${id}-${f.key}`; const Icon = fieldIcons[f.key] ?? CircleDashedIcon; return (
{s.by === "agent" && , filled by {agentName}}
{f.kind === "chips" ? ( onEdit(f.key, v)} placeholder={f.key === "channels" ? "Waiting on your pick" : "Add and press Enter"} value={Array.isArray(s.value) ? s.value : []} /> ) : (
onEdit(f.key, e.target.value)} placeholder={f.placeholder ?? "Empty"} value={s.value as string} />
)}
); })}
{sections.map((f) => { const s = state.fields[f.key] ?? { value: "" }; const fid = `${id}-${f.key}`; return (