"use client";
import { useEffect, useRef, useState } from "react";
import { Composer, type ComposerDraft, type ComposerReceipt } from "@/components/heidi-agent-ui/composer";
import { ConversationViewport } from "@/components/heidi-agent-ui/conversation-viewport";
import type { AgentInteractionScenarioId } from "../_lib/agent-interaction-scenarios";
import styles from "./interactions.module.css";
const RESPONSE = [
"A small studio. A considered beginning.",
"Today we’re opening Fieldwork, a design studio for people making useful things. We work on identities, websites, and the details that make a product feel like itself.",
"Our first collection brings together three recent projects: a neighbourhood bookshop, a tool for independent makers, and a new home for a ceramics practice.",
"Each started with the same question: what does this actually need? The answers are different. The care is the same.",
"If you’re working on something and want a thoughtful partner, we’d love to hear about it.",
"See the work. Tell us what you’re making."
].join("\n\n");
type Message = { id: number; role: "you" | "assistant"; text: string };
/** This fixture drives the GUI and browser contracts; it never calls a model. */
export function InteractionExample({ scenario }: { scenario: AgentInteractionScenarioId }) {
const [messages, setMessages] = useState<Message[]>([
{ id: 1, role: "you", text: "Write a launch note for our design studio. Keep it warm and direct." },
{ id: 2, role: "assistant", text: scenario === "long-content" ? RESPONSE : "Here’s a first direction. Give it room to breathe, and let the work do the talking." }
]);
const [streaming, setStreaming] = useState(false);
const [announcement, setAnnouncement] = useState<{ id: string; text: string } | null>(null);
const [waiting, setWaiting] = useState(false);
const [rejectedOnce, setRejectedOnce] = useState(false);
const sequence = useRef(2);
const pending = useRef<{ accept: (receipt: ComposerReceipt) => void; draft: ComposerDraft } | null>(null);
const timer = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => () => { if (timer.current != null) clearInterval(timer.current); }, []);
function addMessage(text: string) {
const id = ++sequence.current;
setMessages(current => [...current, { id, role: "you", text }]);
return id;
}
function startStream() {
if (timer.current != null) return;
const id = ++sequence.current;
let length = 0;
setMessages(current => [...current, { id, role: "assistant", text: "" }]);
setStreaming(true);
setAnnouncement({ id: `start-${id}`, text: "Response started." });
timer.current = setInterval(() => {
length = Math.min(RESPONSE.length, length + 18);
setMessages(current => current.map(message => message.id === id ? { ...message, text: RESPONSE.slice(0, length) } : message));
if (length === RESPONSE.length) {
clearInterval(timer.current!);
timer.current = null;
setStreaming(false);
setAnnouncement({ id: `end-${id}`, text: "Response ready." });
}
}, 90);
}
function submit(draft: ComposerDraft): ComposerReceipt | Promise<ComposerReceipt> {
if (scenario === "rejected-send" && !rejectedOnce) {
setRejectedOnce(true);
return new Promise(resolve => {
pending.current = { accept: resolve, draft };
setWaiting(true);
});
}
if (scenario === "delayed-receipt") {
return new Promise(resolve => {
pending.current = { accept: resolve, draft };
setWaiting(true);
});
}
addMessage(draft.text);
return { status: "accepted" };
}
function resolveSubmission() {
const request = pending.current;
if (request == null) return;
pending.current = null;
setWaiting(false);
if (scenario === "rejected-send") {
request.accept({ message: "The connection failed. Your message is safe here.", status: "rejected" });
} else {
addMessage(request.draft.text);
request.accept({ status: "accepted" });
}
}
const initialDraft = scenario === "long-content"
? "A quieter headline, please.\n\nKeep the introduction to two sentences.\nMake the invitation feel personal.\nAvoid a sales pitch.\nUse concrete words.\nKeep the same sense of care.\n\nThen show me a second direction."
: scenario === "streaming" ? "" : "Make the opening a little more personal.";
return (
<div className={styles.example} data-interaction-example={scenario}>
<div className={styles.demoControls}>
<span>{scenario === "delayed-receipt" ? "Send, then write your next draft." : scenario === "rejected-send" ? "Send, edit your next draft, then reject the first." : "Try scrolling while the response grows."}</span>
{waiting ? <button onClick={resolveSubmission} type="button">{scenario === "rejected-send" ? "Reject earlier message" : "Accept earlier message"}</button> : null}
{(scenario === "streaming" || scenario === "long-content") ? <button disabled={streaming} onClick={startStream} type="button">{streaming ? "Streaming…" : "Stream a response"}</button> : null}
</div>
<div className={styles.workspace}>
<ConversationViewport announcement={announcement} className={styles.conversation} label="Launch note conversation">
<ol className={styles.messages}>
{messages.map(message => (
<li className={message.role === "you" ? styles.userMessage : styles.assistantMessage} data-message-id={message.id} key={message.id}>
<span className={styles.messageAuthor}>{message.role === "you" ? "You" : "Assistant"}</span>
<p>{message.text || "Preparing a response…"}</p>
</li>
))}
{scenario === "long-content" ? <li className={styles.assistantMessage}>
<span className={styles.messageAuthor}>Reference</span>
<p>{`https://fieldwork.example/work/${"a-considered-beginning".repeat(18)}`}</p>
<pre><code>{`const collection = ${JSON.stringify({ name: "Fieldwork", description: "A considered beginning. ".repeat(15) })};`}</code></pre>
</li> : null}
</ol>
</ConversationViewport>
<div className={styles.composerDock}>
<Composer defaultValue={initialDraft} label="Your message" onSubmit={submit} placeholder="Make it your own…" />
</div>
</div>
</div>
);
}