Components
A production-grade prompt input: typed variable chips, attachment queue with upload states, template menu, model picker and a live token budget that warns before you overflow.

"use client";
import * as React from "react";
import { PromptComposer } from "@/components/ui/prompt-composer";
const VARIABLES = [
{ key: "customer_name", label: "customer_name", sample: "Dana" },
{ key: "order_id", label: "order_id", sample: "AC-4192" },
{ key: "product", label: "product", sample: "Atlas Pro" },
{ key: "tone", label: "tone", sample: "warm, direct" },
];
const TEMPLATES = [
{
id: "refund",
label: "Refund approved",
description: "Confirm, set expectations, close warmly",
body: "Draft a reply to {{customer_name}} confirming the refund for {{order_id}}. Keep it {{tone}}.",
},
{
id: "bug",
label: "Bug acknowledgement",
description: "Own it, give a timeline, offer a workaround",
body: "Reply to {{customer_name}} about the issue with {{product}}. Acknowledge, give a timeline, offer a workaround.",
},
];
const MODELS = [
{ id: "atlas-lg", name: "Atlas Large", caption: "Best quality · 200k context" },
{ id: "atlas-sm", name: "Atlas Small", caption: "Fastest · 32k context" },
{ id: "atlas-xl", name: "Atlas XL", caption: "Preview", disabled: true, disabledReason: "Not enabled on this workspace" },
];
export default function PromptComposerDemo() {
const [value, setValue] = React.useState(
"Draft a reply to {{customer_name}} about their issue with {{product}}.",
);
const [attachments, setAttachments] = React.useState([
{ id: "a1", name: "error-summary.txt", kind: "file" as const, meta: "412 KB", status: "ready" as const },
{ id: "a2", name: "ticket-AC-4192.json", kind: "data" as const, meta: "3 KB", status: "ready" as const },
{ id: "a3", name: "screenshot.png", kind: "image" as const, meta: "1.2 MB", status: "uploading" as const },
]);
const [modelId, setModelId] = React.useState("atlas-lg");
const [status, setStatus] = React.useState<"idle" | "streaming">("idle");
// Rough stand-in for a tokenizer, so the budget meter moves as you type.
const tokenCount = Math.ceil(value.length / 4) + attachments.length * 120;
return (
<div className="flex w-full items-center justify-center p-8">
<div className="w-full max-w-2xl">
<PromptComposer
label="Reply drafter"
placeholder="Describe the reply you want…"
value={value}
onValueChange={setValue}
variables={VARIABLES}
templates={TEMPLATES}
models={MODELS}
selectedModelId={modelId}
onModelChange={setModelId}
attachments={attachments}
onRemoveAttachment={(a) => setAttachments((prev) => prev.filter((x) => x.id !== a.id))}
onAddAttachment={() =>
setAttachments((prev) => [
...prev,
{ id: `a${prev.length + 1}`, name: "transcript.md", kind: "file", meta: "8 KB", status: "ready" },
])
}
tokenCount={tokenCount}
maxTokens={2000}
status={status}
onSubmit={() => {
setStatus("streaming");
window.setTimeout(() => setStatus("idle"), 1800);
}}
onStop={() => setStatus("idle")}
/>
<p className="mt-4 text-center text-sm text-[var(--motiq-muted)]">
Demo data — nothing is sent anywhere from this surface.
</p>
</div>
</div>
);
}
Part of Motiq — browse the full library on 21st.dev.