Skip to main content

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:

PatternComponent
A dock that knows the page and the selection, runs visible tasks, applies resultsAssistantDock
An answer that grows as tokens arrive, in real markdownStreamingMarkdown
A tool invocation the human can inspect: arguments, status, result, durationToolCallCard
Propose, then confirm, with a risk badge and keyboard approvalApprovalCard
A proposed change to text, reviewed hunk by hunkDiffReview
Model output as an editable form the human correctsStructuredOutputForm
Inline [1] chips and a source listCitation
"Working on it", politely announcedReasoning
A whole conversation, every part rendered by its cardThread
A provider's stream or messages as eventsAdapters

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

Loading example
Loading example
Loading example

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: textStreamingMarkdown, tool-callToolCallCard, approvalApprovalCard, diffDiffReview, citationCitationList. 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.