@agno-hq/chat-react 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,17 @@
1
1
  # @agno-hq/chat-react
2
2
 
3
- Headless React hook (`useAgnoChat`) plus drop-in UI components for streaming **any
4
- Agno agent, team, or workflow** from an [AgentOS](https://docs.agno.com) backend.
3
+ Composable, embeddable React components for [AgentOS](https://docs.agno.com)
4
+ a headless hook, a provider for building your own layout, and drop-in embeds for
5
+ streaming **any Agno agent, team, or workflow**.
6
+
7
+ | Subpath | Contents |
8
+ | --- | --- |
9
+ | `@agno-hq/chat-react` | Everything below. |
10
+ | `@agno-hq/chat-react/core` | `AgnoClient`, run streaming, event helpers, types. No React rendering. |
11
+ | `@agno-hq/chat-react/chat` | `useAgnoChat`, `ChatProvider`, `AgnoChat`, and every building block. |
12
+ | `@agno-hq/chat-react/styles.css` | Tokens + component styles. |
13
+ | `@agno-hq/chat-react/tokens.css` | CSS variables only. |
14
+ | `@agno-hq/chat-react/fonts.css` | Inter + DM Mono (optional). |
5
15
 
6
16
  It speaks the AgentOS HTTP run protocol directly: it POSTs to the run endpoint,
7
17
  parses the streamed events, and accumulates them into render-ready messages —
@@ -9,16 +19,69 @@ content, tool calls, reasoning, citations, media — while exposing the raw even
9
19
  feed, live status, and human-in-the-loop pauses.
10
20
 
11
21
  ```tsx
12
- import { AgnoChat } from '@agno-hq/chat-react'
22
+ import { AgnoChat } from '@agno-hq/chat-react/chat'
13
23
  import '@agno-hq/chat-react/styles.css'
24
+ import '@agno-hq/chat-react/fonts.css' // optional — Inter + DM Mono
14
25
 
15
26
  export default function Page() {
16
- return <AgnoChat baseUrl="http://localhost:7777" showEventLog allowFiles />
27
+ return <AgnoChat baseUrl="http://localhost:7777" allowFiles />
17
28
  }
18
29
  ```
19
30
 
20
- That's the zero-wiring widget: it discovers the agents/teams/workflows on your
21
- AgentOS, shows a selector, and runs a full chat. For full control, use the hook.
31
+ That's the zero-wiring embed: it discovers the agents/teams/workflows on your
32
+ AgentOS and runs a full chat. It renders **no header** and fills its container,
33
+ so it drops into a page, a card, a drawer or a modal without fighting your
34
+ layout — put your own chrome in the `header` slot.
35
+
36
+ Support chat on every page is the same component in launcher mode:
37
+
38
+ ```tsx
39
+ <AgnoChat mode="launcher" baseUrl="..." launcherLabel="Chat with us" />
40
+ ```
41
+
42
+ ### Compose it instead
43
+
44
+ `<ChatProvider>` runs the chat and shares it through context, so every component
45
+ below reads it with no props:
46
+
47
+ ```tsx
48
+ import { ChatProvider, MessageList, ChatInput } from '@agno-hq/chat-react/chat'
49
+
50
+ <ChatProvider baseUrl="http://localhost:7777" entity={agent}>
51
+ <MyOwnHeader />
52
+ <MessageList />
53
+ <ChatInput />
54
+ </ChatProvider>
55
+ ```
56
+
57
+ Reach for `useChatContext()` to drive it from your own components, or call
58
+ `useAgnoChat` directly and render nothing of ours.
59
+
60
+ ### Customize it
61
+
62
+ Three independent levers:
63
+
64
+ ```tsx
65
+ /* 1. Tokens — palette, radii and shadows from agno-os/globals.css */
66
+ :root { --color-brand-brand: 99, 102, 241; --agno-radius-lg: 1.25rem; }
67
+
68
+ /* 2. Slots — every element is addressable by name */
69
+ <AgnoChat classNames={{ root: 'rounded-2xl', textarea: 'font-mono' }} />
70
+
71
+ /* 3. Replacement — swap any piece for your own */
72
+ <ChatWindow composer={<MyComposer />} header={<MyBar />} />
73
+ ```
74
+
75
+ The built-in classes stay on the elements, so Tailwind utilities layer on top
76
+ rather than replacing them. Import only `tokens.css` (or nothing) to style from
77
+ scratch.
78
+
79
+ ### Storybook
80
+
81
+ `npm run storybook` runs the component gallery locally; every story streams
82
+ against a scripted in-memory backend, so it exercises the real hook and the real
83
+ event reducers without an AgentOS running. Pushes to `main` publish it to GitHub
84
+ Pages via `.github/workflows/storybook.yml`.
22
85
 
23
86
  ---
24
87
 
@@ -89,32 +152,248 @@ agent/team/workflow id — the same hook drives all three.
89
152
 
90
153
  ## Components
91
154
 
92
- All components are styled by `@agno-hq/chat-react/styles.css` (dark by default; add
93
- the `agno-light` class on a wrapper for light mode). Every piece is exported so
94
- you can compose your own layout.
155
+ All components are styled by `@agno-hq/chat-react/styles.css`, which carries the
156
+ Agno OS design system the same colour tokens, radii, Inter/DM Mono type scale
157
+ and chat layout as the AgentOS chat page. It is dark by default; add the
158
+ `agno-light` class on a wrapper for light mode (`agno-dark` is accepted too, for
159
+ apps that toggle both). Every piece is exported so you can compose your own
160
+ layout.
161
+
162
+ Theme it by overriding the tokens on your wrapper — they are the AgentOS ones,
163
+ as `R, G, B` triples:
164
+
165
+ ```css
166
+ .agno-root.my-brand {
167
+ --color-brand-brand: 255, 64, 23;
168
+ --color-background: 17, 17, 19;
169
+ --color-background-secondary: 39, 39, 42;
170
+ }
171
+ ```
95
172
 
96
173
  | Component | Purpose |
97
174
  |---|---|
98
- | `<AgnoChat>` | All-in-one widget: discovery, selector, chat, optional event log. |
175
+ | `<AgnoChat>` | All-in-one widget: discovery, selector, chat, optional per-run event feed. |
99
176
  | `<ChatWindow chat={chat}>` | Full chat surface built from a `useAgnoChat` result. |
177
+ | `<ChatLauncher>` | Floating support-widget shell: a corner bubble that opens any chat surface. |
178
+ | `<QuickPrompts>` | Suggested-prompt chips. |
100
179
  | `<MessageList>` | Auto-scrolling transcript with live status + footer slot. |
101
- | `<Message>` | A single message: content, tools, reasoning, media, citations. |
180
+ | `<Message>` | A single message: content, behind-the-scenes panel, media, citations, follow-ups. |
181
+ | `<BehindTheScenes>` | One collapsible timeline over a run's reasoning, tools, member turns, steps and events — doubles as the live status line. |
102
182
  | `<ChatInput>` | Multiline input with file attach, send, and stop. |
103
183
  | `<ToolCalls>` | Collapsible tool-call cards (name, args, result, status). |
104
184
  | `<Reasoning>` | Collapsible reasoning-steps panel. |
105
- | `<Citations>` | References / source URLs. |
185
+ | `<Citations>` | The sources behind an answer, as linked cards. |
186
+ | `<Followups>` | "Related questions" — the follow-up prompts an agent suggests after its answer. |
106
187
  | `<Multimedia>` | Images, video, and audio attachments. |
107
188
  | `<StatusIndicator>` | Animated "what is it doing now" line. |
108
- | `<EventLog>` | Developer feed of every raw run event. |
189
+ | `<EventLog>` | Developer feed of raw run events — render it yourself from `chat.events`. |
109
190
  | `<HumanInput>` | Human-in-the-loop panel (confirm / reject / input). |
110
191
  | `<EntitySelector>` | Dropdown of agents, teams, and workflows. |
111
192
  | `<SessionList>` | Sidebar of past sessions — click to load, trash to delete. |
112
- | `<Markdown>` | The built-in lightweight Markdown renderer. |
193
+ | `<Markdown>` | Markdown, rendered by Streamdown — GFM, and repair for half-written text mid-stream. |
194
+
195
+ ### Inline, or a floating support widget
196
+
197
+ `<AgnoChat>` renders inline by default — a panel wherever you put it, sized by
198
+ `width` and `height`:
199
+
200
+ ```tsx
201
+ <AgnoChat baseUrl="http://localhost:7777" width={720} height={560} />
202
+ ```
203
+
204
+ `mode="launcher"` turns it into the support-chat layout instead: a bubble pinned
205
+ to a corner of the viewport that opens the chat. Mount it once, anywhere in your
206
+ app — it renders into `document.body`, so no wrapper can clip it.
207
+
208
+ ```tsx
209
+ <AgnoChat
210
+ baseUrl="http://localhost:7777"
211
+ mode="launcher"
212
+ position="bottom-right"
213
+ launcherLabel="Chat with us"
214
+ width={400}
215
+ height={620}
216
+ />
217
+ ```
218
+
219
+ | Prop | Default | Description |
220
+ |---|---|---|
221
+ | `mode` | `'inline'` | `'inline'` renders in place; `'launcher'` pins a bubble to a corner. |
222
+ | `position` | `'bottom-right'` | Corner to pin to: also `bottom-left`, `top-right`, `top-left`. |
223
+ | `offset` | `24` | Distance from both edges of that corner. Number means px. |
224
+ | `panel` | `'popover'` | How far it opens: a corner popover, or `'fullscreen'` over the page. |
225
+ | `width` / `height` | `400` / `620` | Size of the popover (and of the inline panel). |
226
+ | `launcherLabel` | — | Text beside the bubble glyph. Icon-only without it. |
227
+ | `launcherIcon` | speech bubble | Replaces the default glyph. |
228
+ | `defaultOpen` | `false` | Open on first render. |
229
+ | `open` / `onOpenChange` | — | Control the open state yourself (e.g. from your own "Help" button). |
230
+
231
+ A `'fullscreen'` panel covers the page and closes from an X in the chat header;
232
+ a popover closes from the bubble, which becomes an X. Escape closes either, and
233
+ below 480px wide a popover goes full-screen on its own. The conversation is kept
234
+ across close/open — a run that is still streaming keeps streaming.
235
+
236
+ To give your own layout the same behaviour, wrap it in `<ChatLauncher>`:
237
+
238
+ ```tsx
239
+ <ChatLauncher position="bottom-left" panel="fullscreen" label="Support">
240
+ <ChatWindow chat={chat} />
241
+ </ChatLauncher>
242
+ ```
243
+
244
+ ### Errors
245
+
246
+ A failed run leaves its message on `chat.error`, but nothing is rendered by
247
+ default — raw backend errors are long, and they crowd a narrow window. Opt in
248
+ with `showErrors` on `<AgnoChat>` or `<ChatWindow>`:
249
+
250
+ ```tsx
251
+ <AgnoChat baseUrl="http://localhost:7777" showErrors />
252
+ ```
253
+
254
+ Or render it wherever suits your layout — a toast, say:
255
+
256
+ ```tsx
257
+ {chat.error && <Toast>{chat.error}</Toast>}
258
+ ```
113
259
 
114
- ### Bring your own Markdown
260
+ ### Quick prompts
115
261
 
116
- The built-in renderer covers code blocks, inline code, bold/italic, links,
117
- headings and lists. For full GFM (tables, etc.), pass `renderMarkdown`:
262
+ `quickPrompts` offers suggested prompts while the transcript is empty. Clicking
263
+ one sends it. A chip is either the prompt itself, or a short label that sends
264
+ something longer:
265
+
266
+ ```tsx
267
+ <AgnoChat
268
+ baseUrl="http://localhost:7777"
269
+ quickPrompts={[
270
+ 'What can you do?',
271
+ { label: 'Pricing', prompt: 'How does pricing work?' },
272
+ { label: 'Talk to a human', prompt: 'I would like to talk to a human.' },
273
+ ]}
274
+ />
275
+ ```
276
+
277
+ `<ChatWindow>` takes the same prop, plus `onQuickPrompt` to do something other
278
+ than send the text. Render `<QuickPrompts>` yourself to place the chips
279
+ elsewhere.
280
+
281
+ ### Run events
282
+
283
+ Every run keeps the events that produced it on its own message
284
+ (`message.events`) — live, and for runs restored from session history when the
285
+ agent stores them. `<BehindTheScenes>` builds its steps from those events the
286
+ way the AgentOS chat does: content deltas, model-request and reasoning-delta
287
+ events are dropped, started/completed pairs collapse into one step, and run
288
+ lifecycle events read as "Run Started" / "Run Completed" with the reported
289
+ duration. A message with no events falls back to what it does carry (tools,
290
+ reasoning, member turns, workflow steps).
291
+
292
+ The raw feed is not part of that panel. `chat.events` holds the latest run's
293
+ events, and every message keeps its own in `message.events` — render either
294
+ with `<EventLog>` wherever you want it:
295
+
296
+ ```tsx
297
+ <EventLog events={chat.events} maxHeight={240} />
298
+ ```
299
+
300
+ ### Citations, sources and link previews
301
+
302
+ Whatever a run cites — knowledge-base chunks in `references`, browsed links in
303
+ `citations.urls` — is flattened into one numbered list and rendered as cards
304
+ under the answer: the site's favicon and the title, linking to the source. A
305
+ `[1]` in the answer text becomes a chip pointing at card one, and hovering
306
+ either the chip or a link opens a preview with the URL and the cited passage:
307
+
308
+ ```tsx
309
+ <Message message={message} /> // markers, cards and previews, no props
310
+ <Message message={message} hideSources /> // just the answer
311
+ ```
312
+
313
+ The hostname and favicon come from the URL itself, and the title and passage
314
+ from the citation payload, so previews need no network of their own. To fill in
315
+ OpenGraph metadata, hand the provider a `resolveLinkPreview` — the browser can't
316
+ read another origin's meta tags, so back it with an endpoint of your own. It is
317
+ called once per URL, on first hover:
318
+
319
+ ```tsx
320
+ <ChatProvider
321
+ baseUrl="http://localhost:7777"
322
+ resolveLinkPreview={async (url) => {
323
+ const res = await fetch(`/api/og?url=${encodeURIComponent(url)}`)
324
+ return res.ok ? res.json() : null // { title, description, image, siteName, favicon }
325
+ }}
326
+ >
327
+ <ChatWindow />
328
+ </ChatProvider>
329
+ ```
330
+
331
+ Plenty of runs carry no `references` or `citations` at all — the model cites in
332
+ the answer itself, as numbered links, and lists the titles at the end:
333
+
334
+ ```text
335
+ Call `Agent.run()` in production. [1](https://docs.agno.com/agents/building-agents)
336
+
337
+ Sources: [Building Agents](https://docs.agno.com/agents/building-agents)
338
+ ```
339
+
340
+ That reads the same way: the numbered link becomes the chip, the labelled link
341
+ names the card, and the numbering stays exactly as the model wrote it. Links in
342
+ prose are left alone — only numbered ones count as citations.
343
+
344
+ Rendering sources yourself? `collectSources(references, citations)` and
345
+ `sourcesFromMarkdown(content)` are the same numbering the components use, and
346
+ `<SourceCard>`, `<LinkPreviewCard>` and `<HoverPreview>` are exported to build
347
+ with.
348
+
349
+ ### Follow-ups
350
+
351
+ An agent built with `followups=True` suggests what to ask next once it has
352
+ answered. They sit above the composer as "Related questions", one line each —
353
+ the full text is on hover — and clicking one sends it:
354
+
355
+ ```tsx
356
+ <AgnoChat baseUrl="http://localhost:7777" /> // on by default
357
+ <AgnoChat baseUrl="http://localhost:7777" hideFollowups /> // off
358
+ <AgnoChat baseUrl="http://localhost:7777" followupsTitle="Ask next" /> // your own heading
359
+ ```
360
+
361
+ Only the latest answer offers them; earlier ones have been answered by the
362
+ conversation moving on. They arrive as a `FollowupsCompleted` event and are
363
+ stored on the run, so a reloaded session still shows them. To put them under
364
+ the answer instead, pass `<Message onFollowup={chat.sendMessage}>`; to place
365
+ them anywhere else, render `<Followups>` yourself from `message.followups` —
366
+ `lines` lets them wrap instead of clamping.
367
+
368
+ ### Markdown
369
+
370
+ Answers are rendered by [Streamdown](https://streamdown.ai), which is built for
371
+ text that arrives a token at a time: mid-stream it repairs the unterminated
372
+ bold, the half-written link and the unclosed fence instead of showing you the
373
+ raw characters. GFM comes with it — tables, task lists, strikethrough — along
374
+ with a sanitising pass over the HTML.
375
+
376
+ Streamdown styles itself with Tailwind utility classes. Every element is mapped
377
+ back onto this package's own `agno-md-*` classes, so **Tailwind is not
378
+ required** and the shipped stylesheet and tokens still govern the look.
379
+
380
+ Fenced code blocks get a header naming the language, a line-number gutter and
381
+ syntax highlighting by [Shiki](https://shiki.style). The highlighter loads on
382
+ the first fenced block, not before, and grammars load one at a time as
383
+ languages appear; a language Shiki doesn't know renders plain. Colours follow
384
+ the theme class like everything else. `--agno-code-max-height` (default `32rem`)
385
+ caps a block before it scrolls.
386
+
387
+ The header also holds a copy button. Turn it off per-renderer or for a whole
388
+ tree:
389
+
390
+ ```tsx
391
+ <Markdown content={answer} codeCopy={false} />
392
+ <ChatProvider codeCopy={false}>…</ChatProvider>
393
+ <AgnoChat codeCopy={false} />
394
+ ```
395
+
396
+ Prefer a different renderer? `renderMarkdown` replaces it entirely:
118
397
 
119
398
  ```tsx
120
399
  import ReactMarkdown from 'react-markdown'
@@ -126,6 +405,10 @@ import remarkGfm from 'remark-gfm'
126
405
  />
127
406
  ```
128
407
 
408
+ Note that `streamdown` is ESM-only, as is the remark/rehype ecosystem under it.
409
+ Bundlers resolve that fine; a bare `require()` of the CJS entry needs Node
410
+ 22.12+.
411
+
129
412
  ---
130
413
 
131
414
  ## Human-in-the-loop
@@ -161,12 +444,20 @@ await chat.loadSession(sessionId) // load a transcript into the chat
161
444
  await chat.deleteSession(sessionId)
162
445
  ```
163
446
 
447
+ The list updates optimistically, as the AgentOS chat does: the moment a run
448
+ reports its session id, that session moves to the top of `chat.sessions` —
449
+ created on the spot, named after the message that started it, if the backend
450
+ just made it — rather than only appearing after the next `refreshSessions()`.
451
+ A later refresh keeps the in-flight session even if the backend has not
452
+ persisted it yet.
453
+
164
454
  Render them with `<SessionList>`:
165
455
 
166
456
  ```tsx
167
457
  <SessionList
168
458
  sessions={chat.sessions}
169
459
  activeSessionId={chat.sessionId}
460
+ streamingSessionIds={chat.streamingSessionIds}
170
461
  loading={chat.sessionsLoading}
171
462
  onSelect={chat.loadSession}
172
463
  onDelete={chat.deleteSession}
@@ -174,10 +465,22 @@ Render them with `<SessionList>`:
174
465
  />
175
466
  ```
176
467
 
468
+ `streamingSessionIds` marks the sessions with a run in flight — each shows a
469
+ spinner in place of the delete button.
470
+
471
+ ### Switching session does not stop the run
472
+
473
+ Selecting another session (or starting a new chat) while a run is streaming
474
+ *parks* that run instead of cancelling it, as the AgentOS chat does: it keeps
475
+ streaming in the background, stays listed in `chat.streamingSessionIds`, and
476
+ `loadSession` restores it — mid-stream or finished — when you switch back,
477
+ rather than refetching a transcript that is still being written. Only
478
+ `chat.cancel()` stops a run.
479
+
177
480
  The all-in-one widget shows this sidebar with `showSessions`:
178
481
 
179
482
  ```tsx
180
- <AgnoChat baseUrl="http://localhost:7777" showSessions showEventLog />
483
+ <AgnoChat baseUrl="http://localhost:7777" showSessions />
181
484
  ```
182
485
 
183
486
  ---
@@ -202,8 +505,9 @@ normalised events) if you want to bypass the hook entirely.
202
505
 
203
506
  ## Running the example
204
507
 
205
- The `example/` folder is a Vite app demonstrating both the widget and the hook
206
- (with a live status panel + event log). It imports the library from source.
508
+ The `example/` folder is a Vite app demonstrating the widget, the floating
509
+ launcher, every component, and the hook (with a live status panel + event log).
510
+ It imports the library from source.
207
511
 
208
512
  ```bash
209
513
  cd example