AI Interface Pack
The complete AI Interface Pack — installs the AI Agent Workspace block and its four components (AI Response Stream, Tool Call Activity, Source Citation Rail, Agent Run Timeline) as editable source, deduping shared utilities.
registry/blocks/ai-agent-workspace.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
getStatusMeta,
statusVars,
useControllableState,
type StatusTone,
} from "@/lib/motiq";
import {
AgentRunTimeline,
type AgentRun,
type RunStep,
type RunStage,
type StepStatus,
} from "@/components/motiq/agent-run-timeline";
import {
ToolCallActivity,
type ToolCall,
type ToolCallStatus,
} from "@/components/motiq/tool-call-activity";
import {
AiResponseStream,
type ResponseSegment,
type StreamState,
type StreamSource,
} from "@/components/motiq/ai-response-stream";
import {
SourceCitationRail,
CitationMarker,
type CitationSource,
} from "@/components/motiq/source-citation-rail";
/**
* AiAgentWorkspace — a composed, full-page AI agent workspace built from four
* released Motiq components:
* • AgentRunTimeline — the run, step-by-step, with approval + retry controls
* • ToolCallActivity — the same run seen as a live tool-call inspector
* • AiResponseStream — the streamed answer, with inline citations
* • SourceCitationRail — interactive, claim-level source attribution
*
* It is strictly a *presentation* composition. It never talks to a model, never
* executes an agent, never retrieves anything, and never fabricates a result.
* The application owns the workspace state end to end; this block simply renders
* whatever `phase` + `dataset` it is given and reports user intent
* (approve / reject / retry / cancel / stop) back by moving the phase. It ships
* with clearly-fictional inline demo data so it renders standalone, and every
* override is a prop so a host app can drive the exact same UI from real state.
*
* The demo state machine has six required states — Idle · Running · Waiting for
* approval · Completed · Failed · Cancelled — each of which fully determines the
* state of all four child components. Callbacks transition between phases:
* approve → completed reject → cancelled
* retry → running cancel → cancelled stop → cancelled
*
* Hydration-safe: there is no `Date.now()` / `Math.random()` / `new Date()` at
* module scope, during render, or in a state initializer — every timestamp is a
* fixed epoch constant, so the server and client render identical markup.
* Reduced-motion, forced-colors, and dark mode are all inherited from the
* children. Clean-room original.
*/
/* -------------------------------------------------------------------------- */
/* Types */
/* -------------------------------------------------------------------------- */
export type WorkspacePhase =
| "idle"
| "running"
| "waiting"
| "completed"
| "failed"
| "cancelled";
/** A single unit of work — rendered both as a run step and as a tool call. */
export interface WorkspaceStepSeed {
id: string;
/** Step / call display name, e.g. "Search knowledge base". */
title: string;
/** One-line description shown on the timeline step. */
description: string;
/** Tool-activity category (drives the glyph): search | read | code | approve | web. */
category: string;
/** Tool / function name, e.g. "kb.search". */
tool: string;
/** Arguments the tool was invoked with (rendered read-only). */
args: Record<string, unknown>;
/** Result the tool returned (rendered read-only, only when completed). */
result?: Record<string, unknown>;
/** Short summary shown on a completed step. */
summary?: string;
/** Error surfaced when this unit fails. */
errorText?: string;
/** Optional nested sub-stages for the timeline step. */
stages?: { id: string; label: string }[];
}
/** The fictional demo data. Every field is overridable so a host app can drive it. */
export interface WorkspaceDataset {
runTitle: string;
steps: WorkspaceStepSeed[];
/** The full, resolved answer. */
fullAnswer: ResponseSegment[];
/** A partial answer, used while streaming / after an interruption. */
partialAnswer: ResponseSegment[];
/** Compact source list rendered in the stream footer. */
streamSources: StreamSource[];
/** Rich, interactive sources for the citation rail. */
citationSources: CitationSource[];
/** Claim-level attribution rendered as the citation-rail body. */
claims: { text: string; sourceId: string }[];
/** Run-level summary shown once the run completes. */
runSummary: string;
/** Message shown in the answer error banner when the run fails. */
errorMessage: string;
}
export interface AiAgentWorkspaceProps {
/** Controlled workspace phase. Pair with `onPhaseChange`. */
phase?: WorkspacePhase;
/** Uncontrolled initial phase. Defaults to `"running"` so the demo looks alive. */
defaultPhase?: WorkspacePhase;
/** Notified whenever the phase changes (control click or a child callback). */
onPhaseChange?: (phase: WorkspacePhase) => void;
/** The prompt shown in the workspace header. */
prompt?: string;
/** The assistant / model display name shown on the answer. */
assistantName?: string;
/** Override the fictional demo data with application-owned data. */
dataset?: WorkspaceDataset;
/** Hide the built-in state controls when the host app supplies its own. */
showStateControls?: boolean;
className?: string;
}
/* -------------------------------------------------------------------------- */
/* Per-phase specification */
/* -------------------------------------------------------------------------- */
interface PhaseSpec {
runStatus: AgentRun["status"];
/** Status for each of the six steps, in order. */
steps: StepStatus[];
current?: string;
stream: StreamState;
answer: "none" | "partial" | "full";
/** Whether the citation rail shows the grounded claims. */
claims: boolean;
}
const PHASE_META: Record<WorkspacePhase, PhaseSpec> = {
idle: {
runStatus: "queued",
steps: ["pending", "pending", "pending", "pending", "pending", "pending"],
current: "s1",
stream: "stopped",
answer: "none",
claims: false,
},
running: {
runStatus: "running",
steps: ["completed", "completed", "active", "pending", "pending", "pending"],
current: "s3",
stream: "streaming",
answer: "partial",
claims: true,
},
waiting: {
runStatus: "waiting",
steps: ["completed", "completed", "completed", "completed", "waiting_approval", "pending"],
current: "s5",
stream: "complete",
answer: "full",
claims: true,
},
completed: {
runStatus: "completed",
steps: ["completed", "completed", "completed", "completed", "completed", "completed"],
current: undefined,
stream: "complete",
answer: "full",
claims: true,
},
failed: {
runStatus: "failed",
steps: ["completed", "completed", "failed", "pending", "pending", "pending"],
current: "s3",
stream: "error",
answer: "partial",
claims: false,
},
cancelled: {
runStatus: "cancelled",
steps: ["completed", "completed", "cancelled", "cancelled", "cancelled", "cancelled"],
current: undefined,
stream: "stopped",
answer: "partial",
claims: false,
},
};
const PHASES: { id: WorkspacePhase; label: string }[] = [
{ id: "idle", label: "Idle" },
{ id: "running", label: "Running" },
{ id: "waiting", label: "Waiting for approval" },
{ id: "completed", label: "Completed" },
{ id: "failed", label: "Failed" },
{ id: "cancelled", label: "Cancelled" },
];
const STEP_TO_CALL: Record<StepStatus, ToolCallStatus> = {
pending: "queued",
active: "running",
completed: "completed",
failed: "failed",
skipped: "cancelled",
waiting_approval: "waiting_approval",
cancelled: "cancelled",
};
/* Fixed, deterministic timing so server + client render identically. */
const T0 = 1_720_000_000_000;
const STEP_START = [0, 1_800, 2_600, 5_200, 7_400, 9_100];
const STEP_DURATION = [1_600, 700, 2_400, 3_100, 0, 1_200];
/* -------------------------------------------------------------------------- */
/* Snapshot builders */
/* -------------------------------------------------------------------------- */
function buildStages(seed: WorkspaceStepSeed, status: StepStatus): RunStage[] | undefined {
if (!seed.stages) return undefined;
return seed.stages.map((s, j) => {
let sub: StepStatus;
if (status === "completed") sub = "completed";
else if (status === "active") sub = j === 0 ? "active" : "pending";
else if (status === "failed") sub = j === 0 ? "completed" : j === 1 ? "failed" : "pending";
else if (status === "cancelled") sub = "cancelled";
else sub = "pending";
return { id: s.id, label: s.label, status: sub };
});
}
function buildStep(seed: WorkspaceStepSeed, i: number, status: StepStatus): RunStep {
const started = status !== "pending";
return {
id: seed.id,
title: seed.title,
description: seed.description,
status,
startedAt: started ? T0 + STEP_START[i] : undefined,
durationMs: status === "completed" ? STEP_DURATION[i] : undefined,
toolCall: {
name: seed.tool,
arguments: seed.args,
result: status === "completed" ? seed.result : undefined,
},
stages: buildStages(seed, status),
summary: status === "completed" ? seed.summary : undefined,
error: status === "failed" ? seed.errorText : undefined,
attempts: status === "failed" ? 1 : undefined,
maxAttempts: status === "failed" ? 3 : undefined,
};
}
function buildRun(spec: PhaseSpec, ds: WorkspaceDataset): AgentRun {
return {
title: ds.runTitle,
status: spec.runStatus,
startedAt: spec.runStatus === "queued" ? undefined : T0,
currentStepId: spec.current,
steps: ds.steps.map((seed, i) => buildStep(seed, i, spec.steps[i])),
summary: spec.runStatus === "completed" ? ds.runSummary : undefined,
};
}
function buildCall(seed: WorkspaceStepSeed, i: number, status: ToolCallStatus): ToolCall {
return {
id: seed.id,
name: seed.title,
category: seed.category,
status,
startedAt: status !== "queued" ? T0 + STEP_START[i] : undefined,
durationMs: status === "completed" ? STEP_DURATION[i] : undefined,
input: seed.args,
output: status === "completed" ? seed.result : undefined,
error: status === "failed" ? seed.errorText : undefined,
progress: status === "running" ? 0.62 : undefined,
};
}
function buildCalls(spec: PhaseSpec, ds: WorkspaceDataset): ToolCall[] {
return ds.steps.map((seed, i) => buildCall(seed, i, STEP_TO_CALL[spec.steps[i]]));
}
/* -------------------------------------------------------------------------- */
/* Default fictional demo data — clearly not a real product or model */
/* -------------------------------------------------------------------------- */
const PUB_1 = 1_719_100_000_000;
const PUB_2 = 1_719_500_000_000;
const PUB_3 = 1_719_700_000_000;
const DEFAULT_STEPS: WorkspaceStepSeed[] = [
{
id: "s1",
title: "Search knowledge base",
description: "Find every internal doc touching the Q3 refund policy.",
category: "search",
tool: "kb.search",
args: { query: "refund policy Q3 changes", scopes: ["policy", "changelog", "support"], top_k: 8 },
result: { hits: 3, indices: ["policy", "changelog", "support"] },
summary: "3 relevant documents matched across policy, changelog, and support.",
},
{
id: "s2",
title: "Read policy document",
description: "Open Refund Policy v3 and extract the changed clauses.",
category: "read",
tool: "docs.read",
args: { doc: "policy/refunds", version: "v3", sections: ["window", "proration", "add-ons"] },
result: { clauses: 3, effectiveDate: "Q3", reversible: true },
summary: "Extracted 3 changed clauses from Refund Policy v3.",
},
{
id: "s3",
title: "Compare policy versions",
description: "Diff v2 against v3 to isolate the customer-visible changes.",
category: "code",
tool: "policy.diff",
args: { from: "v2", to: "v3", ignore: ["formatting", "internal-notes"] },
result: { changed: 3, added: 1, removed: 0, flagged: ["add-ons"] },
summary: "3 clauses changed; add-on handling flagged for review.",
errorText: "policy.diff exited with code 1: schema drift between v2 and v3 exceeded the safe-diff threshold.",
stages: [
{ id: "s3a", label: "Normalize both versions" },
{ id: "s3b", label: "Compute clause-level diff" },
{ id: "s3c", label: "Classify customer impact" },
],
},
{
id: "s4",
title: "Draft the summary",
description: "Write a customer-facing summary grounded in the diff.",
category: "generate",
tool: "compose.draft",
args: { audience: "customer", tone: "plain", maxWords: 120, requireCitations: true },
result: { words: 92, citations: 3, readingGrade: 7 },
summary: "92-word draft produced with 3 inline citations.",
},
{
id: "s5",
title: "Wait for compliance approval",
description: "A human reviewer must approve before anything is published.",
category: "approve",
tool: "gate.approval",
args: { reviewer: "compliance", blockOn: "publish", policy: "customer-comms" },
},
{
id: "s6",
title: "Publish to help center",
description: "Update the public refund article once approved.",
category: "web",
tool: "helpcenter.publish",
args: { article: "refunds", channel: "public", notify: ["support"] },
result: { url: "help.acme.internal/refunds", revision: 47 },
summary: "Published revision 47 of the public refund article.",
},
];
const DEFAULT_FULL_ANSWER: ResponseSegment[] = [
{
type: "text",
text: "Q3 ships three customer-facing changes to the refund policy. First, the standard refund window on annual plans moves from 14 to 30 days ",
},
{ type: "citation", sourceId: "1" },
{
type: "text",
text: ". Second, mid-cycle downgrades now issue a prorated refund automatically, instead of a credit applied to the next invoice ",
},
{ type: "citation", sourceId: "2" },
{
type: "text",
text: ".\n\nThe effective policy the agent extracted looks like this:",
},
{
type: "code",
lang: "json",
filename: "refund-policy.q3.json",
code: '{\n "annualRefundWindowDays": 30,\n "proration": "automatic",\n "addonRefunds": "follow-parent"\n}',
},
{
type: "text",
text: "Finally, add-on refunds now follow the parent subscription's status rather than a separate track, and the support macro was updated to match ",
},
{ type: "citation", sourceId: "3" },
{ type: "text", text: "." },
];
const DEFAULT_PARTIAL_ANSWER: ResponseSegment[] = [
{
type: "text",
text: "Q3 ships three customer-facing changes to the refund policy. First, the standard refund window on annual plans moves from 14 to 30 days ",
},
{ type: "citation", sourceId: "1" },
{ type: "text", text: ". Second, mid-cycle downgrades now issue a prorated" },
];
const DEFAULT_STREAM_SOURCES: StreamSource[] = [
{
id: "1",
title: "Refund Policy v3 - Q3 2026",
url: "https://docs.acme.internal/policy/refunds",
snippet: "docs.acme.internal · §2.1 Refund window",
},
{
id: "2",
title: "Payments changelog - Q3 release",
url: "https://changelog.acme.internal/q3",
snippet: "changelog.acme.internal · Downgrades",
},
{
id: "3",
title: "Support macro - refund requests",
url: "https://help.acme.internal/macros/refunds",
snippet: "help.acme.internal · Add-ons",
},
];
const DEFAULT_CITATION_SOURCES: CitationSource[] = [
{
id: "1",
index: 1,
title: "Refund Policy v3 - Q3 2026",
domain: "docs.acme.internal",
url: "https://docs.acme.internal/policy/refunds",
type: "docs",
author: "Policy Ops",
publishedAt: PUB_1,
retrievedAt: T0,
relevance: 0.96,
location: "§2.1",
verified: true,
excerpt:
"Annual plans are eligible for a full refund within 30 days of the invoice date, extended from the previous 14-day window.",
},
{
id: "2",
index: 2,
title: "Payments changelog - Q3 release",
domain: "changelog.acme.internal",
url: "https://changelog.acme.internal/q3",
type: "article",
author: "Payments",
publishedAt: PUB_2,
retrievedAt: T0,
relevance: 0.88,
location: "Downgrades",
verified: true,
excerpt:
"Mid-cycle downgrades now trigger an automatic prorated refund rather than a credit applied to the next invoice.",
},
{
id: "3",
index: 3,
title: "Support macro - refund requests",
domain: "help.acme.internal",
url: "https://help.acme.internal/macros/refunds",
type: "web",
author: "Support Enablement",
publishedAt: PUB_3,
retrievedAt: T0,
relevance: 0.71,
location: "Add-ons",
verified: false,
excerpt:
"Add-on refunds inherit the status of the parent subscription; agents should no longer process them on a separate track.",
},
];
const DEFAULT_CLAIMS: { text: string; sourceId: string }[] = [
{ text: "Annual plans get a 30-day refund window, up from 14 days.", sourceId: "1" },
{ text: "Mid-cycle downgrades are now refunded automatically, prorated.", sourceId: "2" },
{ text: "Add-on refunds follow the parent subscription's status.", sourceId: "3" },
];
export const DEFAULT_WORKSPACE_DATASET: WorkspaceDataset = {
runTitle: "Summarize Q3 refund-policy changes",
steps: DEFAULT_STEPS,
fullAnswer: DEFAULT_FULL_ANSWER,
partialAnswer: DEFAULT_PARTIAL_ANSWER,
streamSources: DEFAULT_STREAM_SOURCES,
citationSources: DEFAULT_CITATION_SOURCES,
claims: DEFAULT_CLAIMS,
runSummary:
"Draft approved by Compliance and published to the help center. 6 steps completed, 3 sources cited, 1 add-on clause flagged for follow-up.",
errorMessage:
"The run stopped while comparing policy versions - the answer was drafted only up to the point the agent reached.",
};
const DEFAULT_PROMPT =
"Summarize our Q3 refund-policy changes for a customer-facing help article, and cite the exact sources.";
const DEFAULT_ASSISTANT = "Acme Agent";
/* -------------------------------------------------------------------------- */
/* Small presentational pieces */
/* -------------------------------------------------------------------------- */
const segBtn =
"rounded-lg border px-2.5 py-1.5 text-[12.5px] font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[var(--color-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface)]";
const segBtnOn =
"border-transparent bg-[var(--color-accent)] text-[var(--color-accent-fg)]";
const segBtnOff =
"border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg)] hover:border-[var(--color-accent)] hover:text-[var(--color-accent)]";
const primaryBtn =
"inline-flex items-center gap-1.5 rounded-lg border border-transparent bg-[var(--color-accent)] px-3 py-1.5 text-[13px] font-medium text-[var(--color-accent-fg)] outline-none transition-colors hover:bg-[var(--color-accent-hover)] focus-visible:ring-2 focus-visible:ring-[var(--color-focus)]";
function DemoPill() {
return (
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-[var(--color-warning)] bg-[color-mix(in_oklab,var(--color-warning)_12%,transparent)] px-2 py-0.5 text-[11px] font-medium text-[var(--color-warning)]">
<span className="h-1.5 w-1.5 rounded-full bg-current" aria-hidden />
Demo data
</span>
);
}
function OverallStatusChip({ status }: { status: AgentRun["status"] }) {
const meta = getStatusMeta(status);
const vars = statusVars(meta.tone as StatusTone);
return (
<span
className="inline-flex shrink-0 items-center gap-1.5 rounded-full border px-2.5 py-1 text-[12px] font-medium"
style={{ color: vars.color, borderColor: vars.border, background: vars.bg }}
>
<span className="h-1.5 w-1.5 rounded-full bg-current" aria-hidden />
{meta.label}
</span>
);
}
function ClaimsBody({ claims }: { claims: { text: string; sourceId: string }[] }) {
return (
<div>
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[var(--color-muted)]">
Key claims
</p>
<ul className="flex flex-col gap-2.5">
{claims.map((c, i) => (
<li
key={i}
className="flex items-start gap-2 text-[13.5px] leading-relaxed text-[var(--color-fg)]"
>
<span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--color-accent)]" aria-hidden />
<span>
{c.text} <CitationMarker source={c.sourceId} />
</span>
</li>
))}
</ul>
<p className="mt-3 text-[11.5px] leading-relaxed text-[var(--color-muted)]">
Select a citation or a source to sync the two. Verification badges are provided by the application, not
inferred by the component.
</p>
</div>
);
}
function PlaceholderBody() {
return (
<p className="text-[13.5px] leading-relaxed text-[var(--color-muted)]">
The grounded answer and its citations appear here once the agent produces a response.
</p>
);
}
function IdleAnswer({ onStart }: { onStart: () => void }) {
return (
<section className="flex w-full flex-col items-start gap-3 rounded-2xl border border-dashed border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-8 shadow-[var(--shadow-md)]">
<span
className="grid h-10 w-10 place-items-center rounded-xl bg-[color-mix(in_oklab,var(--color-accent)_14%,transparent)] text-[var(--color-accent)]"
aria-hidden
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M8 5v14l11-7z" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" />
</svg>
</span>
<p className="text-[14px] font-semibold text-[var(--color-fg)]">Ready to run</p>
<p className="max-w-[46ch] text-[13px] leading-relaxed text-[var(--color-muted)]">
Start the run and the agent will search the knowledge base, draft an answer, and stream it here with inline
citations.
</p>
<button type="button" onClick={onStart} className={primaryBtn}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden>
<path d="M8 5v14l11-7z" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" />
</svg>
Start run
</button>
</section>
);
}
/* -------------------------------------------------------------------------- */
/* Component */
/* -------------------------------------------------------------------------- */
export function AiAgentWorkspace({
phase: phaseProp,
defaultPhase = "running",
onPhaseChange,
prompt = DEFAULT_PROMPT,
assistantName = DEFAULT_ASSISTANT,
dataset = DEFAULT_WORKSPACE_DATASET,
showStateControls = true,
className,
}: AiAgentWorkspaceProps) {
const [phase, setPhase] = useControllableState<WorkspacePhase>({
value: phaseProp,
defaultValue: defaultPhase,
onChange: onPhaseChange,
});
const spec = PHASE_META[phase];
const run = React.useMemo(() => buildRun(spec, dataset), [spec, dataset]);
const calls = React.useMemo(() => buildCalls(spec, dataset), [spec, dataset]);
const segments =
spec.answer === "full" ? dataset.fullAnswer : spec.answer === "partial" ? dataset.partialAnswer : [];
/* Phase transitions wired to the children's callbacks. The app owns the real
work; here each intent simply advances the fictional demo state machine. */
const goRunning = React.useCallback(() => setPhase("running"), [setPhase]);
const goCompleted = React.useCallback(() => setPhase("completed"), [setPhase]);
const goCancelled = React.useCallback(() => setPhase("cancelled"), [setPhase]);
return (
<div
data-phase={phase}
className={cn(
"flex w-full flex-col overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-bg)] shadow-[var(--shadow-lg)]",
className,
)}
>
{/* Workspace top bar: prompt + state controls -------------------------- */}
<header className="border-b border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-3.5 sm:px-5">
<div className="flex flex-wrap items-start gap-3">
<span
className="mt-0.5 grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-[linear-gradient(135deg,var(--color-accent),color-mix(in_oklab,var(--color-accent)_55%,#000))] text-[var(--color-accent-fg)]"
aria-hidden
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<rect x="4" y="7" width="16" height="12" rx="3" stroke="currentColor" strokeWidth="1.7" />
<path d="M12 7V4M9.5 12h.01M14.5 12h.01M9.5 15.5h5" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
</svg>
</span>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-[15px] font-semibold text-[var(--color-fg)]">Agent workspace</h2>
<DemoPill />
</div>
<p className="mt-1 max-w-[68ch] text-[13px] leading-relaxed text-[var(--color-muted)]">
<span className="text-[var(--color-fg)]">Prompt:</span> “{prompt}”
</p>
</div>
<OverallStatusChip status={run.status} />
</div>
{showStateControls ? (
<div className="mt-3.5 flex flex-col gap-2 border-t border-[var(--color-border)] pt-3.5">
<span className="text-[11px] font-medium uppercase tracking-wide text-[var(--color-muted)]">
Workspace state
</span>
<div role="group" aria-label="Workspace state" className="flex flex-wrap gap-1.5">
{PHASES.map((p) => (
<button
key={p.id}
type="button"
aria-pressed={phase === p.id}
onClick={() => setPhase(p.id)}
className={cn(segBtn, phase === p.id ? segBtnOn : segBtnOff)}
>
{p.label}
</button>
))}
</div>
</div>
) : null}
</header>
{/* Body: run column + answer column ----------------------------------- */}
<div className="grid gap-4 p-4 sm:p-5 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,1fr)] lg:items-start">
{/* Run column ------------------------------------------------------- */}
<div className="flex min-w-0 flex-col gap-4">
<AgentRunTimeline
run={run}
followActive
compactCompleted
onApprove={goCompleted}
onReject={goCancelled}
onRetryStep={goRunning}
onCancelRun={goCancelled}
onResumeRun={goRunning}
/>
<ToolCallActivity
calls={calls}
title="Tool call activity"
onApprove={goCompleted}
onReject={goCancelled}
onRetry={goRunning}
/>
</div>
{/* Answer column ---------------------------------------------------- */}
<div className="flex min-w-0 flex-col gap-4">
{spec.answer === "none" ? (
<IdleAnswer onStart={goRunning} />
) : (
<AiResponseStream
segments={segments}
state={spec.stream}
sources={dataset.streamSources}
assistantName={assistantName}
errorMessage={dataset.errorMessage}
onStop={goCancelled}
onRetry={goRunning}
/>
)}
<SourceCitationRail
sources={dataset.citationSources}
layout="list"
title="Cited sources"
showExcerpts
>
{spec.claims ? <ClaimsBody claims={dataset.claims} /> : <PlaceholderBody />}
</SourceCitationRail>
</div>
</div>
{/* Footer note -------------------------------------------------------- */}
<footer className="border-t border-[var(--color-border)] bg-[var(--color-bg-secondary)] px-4 py-3 sm:px-5">
<p className="text-[11.5px] leading-relaxed text-[var(--color-muted)]">
Demo data - a clearly-fictional agent run driven from local state. No model is involved and nothing is
executed; the workspace only renders the phase and data it is given, and reports approve / reject / retry /
cancel / stop back to the application.
</p>
</footer>
</div>
);
}
export default AiAgentWorkspace;
- Where it goes
- data-table
- Use cases
- dashboard
DependenciesBundle weight · medium
- animation
- motion