"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
ReasoningThread,
type ThreadMessage,
} from "@/components/ui/reasoning-thread";
const settings = {
autoplay: true,
question: "Which requests are waiting for my approval?",
toolMs: 700,
wordMs: 45,
};
const REASONING =
"The question is about approvals. I will search the approvals that wait for a decision, check which one is risky, then answer with the list.";
const STEPS = [
{ detail: "3 approvals pending", id: "t1", title: "Searching the approvals" },
{
detail: "1 with a high risk: production data access",
id: "t2",
title: "Checking the risk notes",
},
];
const ANSWER =
"**3** requests wait for your decision.\n\n- Laptop for the new analyst, $1,840 (Finance)\n- Design licence renewal, $620 (Team lead)\n- Production database access for contractors (Security)\n\nThe database access is the risky one: ask for a masked replica first.";
type Timer = ReturnType<typeof setTimeout>;
const NO_MESSAGES: ThreadMessage[] = [];
const newTimers = (): Timer[] => [];
/** A scripted turn: the reasoning streams, two tool steps run, then the answer streams. */
export default function Demo(props: Partial<typeof settings>) {
const s = { ...settings, ...props };
const [messages, setMessages] = useState(NO_MESSAGES);
const timers = useRef(newTimers());
/** Each replay is a new turn with new ids, so it starts from a folded, fresh state. */
const turn = useRef(0);
const clear = useCallback(() => {
for (const timer of timers.current) {
clearTimeout(timer);
}
timers.current = [];
}, []);
const play = useCallback(() => {
clear();
turn.current += 1;
const at = (
ms: number,
update: (reply: ThreadMessage) => ThreadMessage
) => {
timers.current.push(
setTimeout(
() =>
setMessages(([asked, last]) =>
asked && last ? [asked, update(last)] : []
),
ms
)
);
};
setMessages([
{ id: `q${turn.current}`, role: "user", text: s.question },
{
id: `a${turn.current}`,
reasoning: { text: "" },
role: "assistant",
steps: [],
streaming: true,
text: "",
},
]);
const words = REASONING.split(" ");
for (let i = 0; i < words.length; i += 1) {
const text = words.slice(0, i + 1).join(" ");
at((i + 1) * s.wordMs, (m) =>
m.role === "assistant" ? { ...m, reasoning: { text } } : m
);
}
let t = (words.length + 1) * s.wordMs;
at(t, (m) =>
m.role === "assistant"
? { ...m, reasoning: { seconds: 2, text: REASONING } }
: m
);
for (const step of STEPS) {
at(t, (m) =>
m.role === "assistant"
? { ...m, steps: [...(m.steps ?? []), { ...step, state: "running" }] }
: m
);
t += s.toolMs;
at(t, (m) =>
m.role === "assistant"
? {
...m,
steps: (m.steps ?? []).map((x) =>
x.id === step.id
? { ...x, durationMs: s.toolMs, state: "done" }
: x
),
}
: m
);
}
const reply = ANSWER.split(" ");
for (let i = 0; i < reply.length; i += 1) {
const text = reply.slice(0, i + 1).join(" ");
const streaming = i < reply.length - 1;
at(t + (i + 1) * s.wordMs, (m) =>
m.role === "assistant" ? { ...m, streaming, text } : m
);
}
}, [clear, s.question, s.toolMs, s.wordMs]);
useEffect(() => {
if (s.autoplay) {
play();
}
return clear;
}, [clear, play, s.autoplay]);
return (
<div className="flex min-h-[560px] w-full flex-col items-center gap-4 bg-background p-6">
<ReasoningThread messages={messages} />
<button
className="min-h-11 rounded-lg border border-border px-4 py-2 font-medium text-sm hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={play}
type="button"
>
{messages.length === 0 ? "Ask" : "Ask again"}
</button>
</div>
);
}