AI
Package: @zuilib/ai · React 18 · Headless UI v2 · Tailwind v4 (optional at runtime)
@zuilib/ai is the AI layer of ZUI: the interface patterns an
assistant-in-the-app needs and a chat box does not give you, plus the
event model, hooks and adapters that tie them to a provider. None of the
components talk to a model. Each is a presentational component with a
small amount of state: you stream events in and wire the callbacks out,
to whichever provider, SDK or in-house gateway you run.
Why these exist
An assistant that lives inside an enterprise app is not a chatbot. It knows where the user is, offers the few things worth doing there, shows its work as it runs tools, asks before it changes anything, and hands the result back into the product. That is a set of UI patterns, not a text box:
| Pattern | Component |
|---|---|
| A dock that knows the page and the selection, runs visible tasks, applies results | AssistantDock |
| An answer that grows as tokens arrive, in real markdown | StreamingMarkdown |
| A tool invocation the human can inspect: arguments, status, result, duration | ToolCallCard |
| Propose, then confirm, with a risk badge and keyboard approval | ApprovalCard |
| A proposed change to text, reviewed hunk by hunk | DiffReview |
| Model output as an editable form the human corrects | StructuredOutputForm |
Inline [1] chips and a source list | Citation |
| "Working on it", politely announced | Reasoning |
| A whole conversation, every part rendered by its card | Thread |
| A provider's stream or messages as events | Adapters |
Every visual decision reads a token from @zuilib/tokens,
every control is a @zuilib/primitives
primitive, every part carries a data-slot, and every file starts with
'use client'.
Install
pnpm add @zuilib/ai
React and React DOM are the required peers. Headless UI, primitives and tokens install transitively.
/* your Tailwind entry */
@import "tailwindcss";
@import "@zuilib/primitives/tailwind.css";
@import "@zuilib/ai/tailwind.css";
import AssistantDock from '@zuilib/ai/assistant-dock'
import StreamingMarkdown from '@zuilib/ai/streaming-markdown'
One import per component, like @zuilib/primitives. The pure helpers
the components are built on are exported too: @zuilib/ai/markdown-parser
(the parser), @zuilib/ai/diff-engine (the line diff) and
@zuilib/ai/thread-reducer (folding events into message parts); the
hooks are @zuilib/ai/use-thread, /use-tool-call, /use-approval and
/use-diff-decisions, the event types @zuilib/ai/stream-events, the
adapters @zuilib/ai/adapters/ai-sdk, /anthropic and /sse.
Overview
Composing a thread
Everything composes through one event model:
provider stream → adapter → AssistantEvent → useThread / AssistantDock → Message.parts → Thread → cards
An AssistantEvent (@zuilib/ai/stream-events) is text (a delta),
tool-call, tool-result, approval, diff, citation, custom
(your own part type, rendered by a
renderers entry), error or done. useThread folds them into Messages
(@zuilib/ai/message-parts: {id, role, parts} with MessagePart the settled
twin of each event), and Thread renders each part
with its card: text → StreamingMarkdown, tool-call → ToolCallCard,
approval → ApprovalCard, diff → DiffReview, citation →
CitationList. The AssistantDock runs the same
fold inside each task. An adapter produces the events
from an AI SDK stream, an Anthropic Messages stream or a generic
text/event-stream.
import {useThread} from '@zuilib/ai/use-thread'
import Thread from '@zuilib/ai/thread'
import {fromSSE} from '@zuilib/ai/adapters/sse'
function Chat() {
const thread = useThread()
const ask = (text: string) => {
thread.appendUserMessage(text)
thread.consume(fromSSE(fetch('/api/chat', {method: 'POST', body: text})))
}
return <Thread messages={thread.messages} onApprovalDecision={thread.decideApproval} onDecisionsChange={thread.decideDiff} />
}
An approval in the stream pauses consume until thread.decideApproval(id, outcome)
is called, which the ApprovalCard inside the thread does for you.
Generating whole views
Text is not the only thing a model can stream into the product:
@zuilib/apps generates entire consoles and screens
as a JSON ViewSpec — the registry teaches the model its vocabulary, the
spec streams into the renderer as it generates, and the model edits a
view with the same patches users make. Pair it with the dock (an action
that yields the spec, an ApprovalCard before the patch applies) when
"the answer" is a chart or a screen rather than a paragraph.
The console on the home page runs the dock over a full back office.
Small screens and touch
Below sm (640px) AssistantDock opens as a
full-width bottom sheet (a top sheet for the top-* positions), capped at
85dvh and padded past the safe-area inset, with a close button in its
header (data-slot="assistant-dock-close"); width, height and
resizable apply from sm up. DiffReview with
mode="split" renders the unified layout when its container is narrower
than 384px, and data-mode reports the layout actually shown.
ApprovalCard and
StructuredOutputForm stack their buttons
full width below sm; PromptInput's textarea
renders at 16px on phones, and every AI control has a 44px target on
coarse pointers. Streaming markdown code blocks
and tables scroll horizontally inside themselves.