@tinyweb_dev/agent-framework-chat-ui
v0.4.0
Published
Drop-in React 19 chat component for [`tinyweb-agent-framework`](../../) agents. Streams [`AgentEvent`](../protocol/src/events.ts) from any NDJSON endpoint, renders assistant markdown, tool results (image / exec / generic JSON), and context-condense pills
Readme
@tinyweb-agent-framework/chat-ui
Drop-in React 19 chat component for tinyweb-agent-framework agents.
Streams AgentEvent from any NDJSON endpoint,
renders assistant markdown, tool results (image / exec / generic JSON), and
context-condense pills — all with zero runtime CSS dependencies.
Install
bun add @tinyweb-agent-framework/chat-ui
# peer deps
bun add react react-dom @tinyweb-agent-framework/protocolThen import the stylesheet once (e.g. in your root layout.tsx):
import "@tinyweb-agent-framework/chat-ui/styles.css"The stylesheet is fully scoped under .tinyweb-chat, so it will not bleed
into the host app's styles.
Quick start — 5 lines
import { TinywebChat } from "@tinyweb-agent-framework/chat-ui"
import "@tinyweb-agent-framework/chat-ui/styles.css"
export default function ChatPage() {
return <TinywebChat endpoint="/api/chat" />
}Your /api/chat POST handler must return NDJSON
AgentEvent objects (one JSON per \n-terminated line). See
packages/agent for the agent runtime that emits them.
Custom tool renderer
TinywebChat ships with image, exec, and JSON fallback renderers. To add
your own — say, a chart preview for a analytics.query tool — pass
toolRenderers:
import { TinywebChat, type ToolRenderer } from "@tinyweb-agent-framework/chat-ui"
const ChartRenderer: ToolRenderer = {
match: (name) => name === "analytics.query",
render: ({ result }) => <ChartPreview data={result as ChartData} />,
}
export default function Page() {
return (
<TinywebChat
endpoint="/api/chat"
toolRenderers={[ChartRenderer]}
/>
)
}Renderers are evaluated first-match-wins in array order, and the built-in image / exec / JSON renderers run AFTER your list — so you can override them by matching the same shapes.
Passing extra body / headers
Use extraBody (object or function) when the agent route needs more than
just prompt — e.g. session id, owner id, device kind:
<TinywebChat
endpoint="/api/chat"
extraBody={() => ({
sessionId: useSessionStore.getState().id,
ownerId: useOwnerStore.getState().id,
})}
headers={() => ({
authorization: `Bearer ${useAuthStore.getState().token}`,
})}
/>Both extraBody and headers accept a plain object OR a function. The
function form is invoked once per turn, so tokens stay fresh.
Props reference
| Prop | Type | Default |
|---|---|---|
| endpoint | string | — (required) |
| extraBody | Record<string, unknown> \| (() => Record<string, unknown>) | undefined |
| headers | HeadersInit \| (() => HeadersInit) | undefined |
| title | string | "Tinyweb Agent" |
| hero | { title?, subtitle? } \| null | default copy, null to hide |
| sidebar | ReactNode | undefined (hidden) |
| placeholder | string | "Ask me anything…" |
| theme | Partial<ChatTheme> | dark preset |
| toolRenderers | ToolRenderer[] | [] |
| onEvent | (event: AgentEvent) => void | undefined |
| onFinish | (event: FinishEvent) => void | undefined |
| disabled | boolean | false |
| initialMessages | ChatMessage[] | [] |
Advanced — bring your own shell
useAgentChat is exported so you can build a custom layout while reusing
the state machine. The hook returns { nodes, status, send, abort, reset }.
import { useAgentChat } from "@tinyweb-agent-framework/chat-ui"
function MyChat() {
const { nodes, status, send } = useAgentChat({ endpoint: "/api/chat" })
return /* render however you like */
}renderMarkdown(src) is also exported if you want to reuse the same
XSS-safe markdown parser elsewhere.
Compatibility
- React:
>= 19.0.0(usesuseEffect/useLayoutEffectonly — no server components / RSC features). - Next.js: 15.x with App Router.
<TinywebChat />is a Client Component ("use client"directive baked in). - Bun / Node: source ships as
.tsx. Both Bun and Next 15 transpile on the fly; no build step required for now.
Voice call UI
When TinywebChatProps.realtime is provided, the
composer renders a Call button that opens RealtimeCallOverlay
inside the same chat column. The voice panel features a calm dark
navy stage with a central black "void" disc surrounded by three
translucent purple halo rings that breathe and react in real time to
whoever is talking (mic input vs agent TTS output).
The animation is CSS-only (no Canvas, no WebGL). A single RAF loop
inside RealtimeOrb reads the
analyser nodes exposed by useRealtimeCall
and writes one CSS custom property (--rt-level) plus a data-speaker
attribute on the orb root. Children react via pure CSS.
Overlay props
<RealtimeCallOverlay
tokenEndpoint="/api/realtime/token"
onClose={() => setOpen(false)}
hero="How can I help you today?"
statusLabels={{
connecting: "Đang kết nối…",
"active-listening": "Đang lắng nghe",
"active-speaking": "Đang nói…",
"active-muted": "Đã tắt mic",
}}
/>| Prop | Type | Description |
|---|---|---|
| hero | string | Greeting line shown under the orb. Defaults to "How can I help you today?". Pass "" to hide. |
| statusLabels | Partial<Record<RealtimeStatusLabelKey, string>> | i18n / branding overrides for the top status pill. Beyond the lifecycle CallStatus keys, the "active" bucket exposes three richer sub-state keys: active-listening, active-speaking, active-muted. |
Theming
The voice panel exposes a dedicated set of CSS custom properties under
the .tinyweb-chat root. Host apps can override them either directly
in their stylesheet or via ChatTheme.realtime:
<TinywebChat
theme={{
realtime: {
bgFrom: "#0a0e2a",
bgTo: "#050818",
halo1: "rgba(146, 130, 220, 0.34)",
halo2: "rgba(146, 130, 220, 0.22)",
halo3: "rgba(146, 130, 220, 0.14)",
core: "#06070d",
accent: "#b9aef0",
micBg: "#0d1024",
micFg: "#ffffff",
},
}}
/>| Theme key | CSS variable | Purpose |
|---|---|---|
| bgFrom | --rt-bg-from | Top of the dark navy gradient. |
| bgTo | --rt-bg-to | Bottom of the gradient. |
| halo1 | --rt-halo-1 | Innermost halo ring color. |
| halo2 | --rt-halo-2 | Middle halo ring color. |
| halo3 | --rt-halo-3 | Outer halo ring color. |
| core | --rt-core | Inner black "void" disc color. |
| accent | --rt-accent | Text accent + ripple stroke color. |
| micBg | --rt-mic-bg | Mic FAB background. |
| micFg | --rt-mic-fg | Mic icon foreground. |
Accessibility
- Status pill is an
aria-live="polite"region. - All icon buttons carry explicit
aria-labels. - The mic FAB uses
aria-pressedto convey muted state. prefers-reduced-motion: reduceis honoured: the orb skips its RAF loop and pins--rt-levelat a small static value, and the mic ripple animation is disabled.
See plan 0019 for the full design and animation maths.
Not included
This package is intentionally focused on the chat surface. The
device-pilot demo's device pairing UI
(Owner / Pair / Devices panel) is not part of v0 — it lives in the
upcoming plan 0012-next-chat-example
consumer.
