@timbal-ai/timbal-react 0.1.1 → 0.2.1

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,6 +1,6 @@
1
1
  # @timbal-ai/timbal-react
2
2
 
3
- React components and runtime for building Timbal chat UIs. Provides a streaming chat interface that connects to Timbal workforce agents out of the box.
3
+ React components and runtime for building Timbal chat UIs. Drop in a single component to get a fully-featured streaming chat interface connected to a Timbal workforce agent.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,15 +10,15 @@ npm install @timbal-ai/timbal-react
10
10
  bun add @timbal-ai/timbal-react
11
11
  ```
12
12
 
13
- ### Peer dependencies
13
+ **Peer dependencies:**
14
14
 
15
15
  ```bash
16
16
  npm install react react-dom @assistant-ui/react @timbal-ai/timbal-sdk
17
17
  ```
18
18
 
19
- ### Required: Tailwind setup
19
+ ### Tailwind setup
20
20
 
21
- The package ships pre-built class names that Tailwind must scan. Add this `@source` line to your CSS entry file — **without it the components will be unstyled**:
21
+ The package ships pre-built Tailwind class names. Add this `@source` line to your CSS entry file — **without it the components will be unstyled**:
22
22
 
23
23
  ```css
24
24
  /* src/index.css */
@@ -29,7 +29,7 @@ The package ships pre-built class names that Tailwind must scan. Add this `@sour
29
29
 
30
30
  > Adjust the path if your CSS file lives at a different depth relative to `node_modules`.
31
31
 
32
- ### Required: CSS imports
32
+ ### CSS imports
33
33
 
34
34
  Import these stylesheets once in your app entry:
35
35
 
@@ -41,11 +41,74 @@ import "katex/dist/katex.min.css";
41
41
 
42
42
  ---
43
43
 
44
- ## Usage
44
+ ## Quick start
45
45
 
46
- ### Drop-in chat
46
+ ### Basic usage
47
47
 
48
- Wrap `TimbalRuntimeProvider` with a `workforceId` and render `<Thread />` inside it:
48
+ `TimbalChat` is a single component that handles everything runtime, streaming, messages, and the composer:
49
+
50
+ ```tsx
51
+ import { TimbalChat } from "@timbal-ai/timbal-react";
52
+
53
+ export default function App() {
54
+ return (
55
+ <div style={{ height: "100vh" }}>
56
+ <TimbalChat workforceId="your-workforce-id" />
57
+ </div>
58
+ );
59
+ }
60
+ ```
61
+
62
+ > `TimbalChat` requires a fixed height parent. Use `height: "100vh"` or `flex-1 min-h-0` depending on your layout.
63
+
64
+ ### Welcome screen and suggestions
65
+
66
+ ```tsx
67
+ <TimbalChat
68
+ workforceId="your-workforce-id"
69
+ welcome={{
70
+ heading: "Hi, I'm your assistant",
71
+ subheading: "Ask me anything about your data.",
72
+ }}
73
+ suggestions={[
74
+ { title: "Summarize this week", description: "Get a quick overview of recent activity" },
75
+ { title: "What can you help with?" },
76
+ { title: "Show me the latest report" },
77
+ ]}
78
+ />
79
+ ```
80
+
81
+ ### Placeholder and width
82
+
83
+ ```tsx
84
+ <TimbalChat
85
+ workforceId="your-workforce-id"
86
+ composerPlaceholder="Type a question..."
87
+ maxWidth="60rem"
88
+ className="my-custom-class"
89
+ />
90
+ ```
91
+
92
+ ### Switching agents dynamically
93
+
94
+ Pass `key` to fully reset the chat when the workforce changes:
95
+
96
+ ```tsx
97
+ const [workforceId, setWorkforceId] = useState("agent-a");
98
+
99
+ <select onChange={(e) => setWorkforceId(e.target.value)}>
100
+ <option value="agent-a">Agent A</option>
101
+ <option value="agent-b">Agent B</option>
102
+ </select>
103
+
104
+ <TimbalChat workforceId={workforceId} key={workforceId} />
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Splitting the runtime and UI
110
+
111
+ `TimbalChat` is a convenience wrapper around `TimbalRuntimeProvider` + `Thread`. Use them separately when you need to place the runtime above the chat — for example, to build a custom header that reads or controls chat state:
49
112
 
50
113
  ```tsx
51
114
  import { TimbalRuntimeProvider, Thread } from "@timbal-ai/timbal-react";
@@ -53,89 +116,292 @@ import { TimbalRuntimeProvider, Thread } from "@timbal-ai/timbal-react";
53
116
  export default function App() {
54
117
  return (
55
118
  <TimbalRuntimeProvider workforceId="your-workforce-id">
56
- <div style={{ height: "100vh" }}>
57
- <Thread />
119
+ <div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
120
+ <header>My App</header>
121
+ <Thread
122
+ composerPlaceholder="Ask anything..."
123
+ className="flex-1 min-h-0"
124
+ />
58
125
  </div>
59
126
  </TimbalRuntimeProvider>
60
127
  );
61
128
  }
62
129
  ```
63
130
 
64
- ### `TimbalRuntimeProvider` props
131
+ ### Custom API base URL
132
+
133
+ Useful when your API is mounted at a subpath (e.g. behind a reverse proxy):
134
+
135
+ ```tsx
136
+ <TimbalRuntimeProvider workforceId="your-workforce-id" baseUrl="/api">
137
+ <Thread />
138
+ </TimbalRuntimeProvider>
139
+ ```
140
+
141
+ ### Custom fetch function
142
+
143
+ Pass your own `fetch` to add headers, inject tokens, or proxy requests:
144
+
145
+ ```tsx
146
+ const myFetch: typeof fetch = (url, options) => {
147
+ return fetch(url, {
148
+ ...options,
149
+ headers: { ...options?.headers, "X-My-Header": "value" },
150
+ });
151
+ };
152
+
153
+ <TimbalRuntimeProvider workforceId="your-workforce-id" fetch={myFetch}>
154
+ <Thread />
155
+ </TimbalRuntimeProvider>
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Customizing the UI
161
+
162
+ Use the `components` prop on `TimbalChat` or `Thread` to replace any part of the interface while keeping everything else as the default.
163
+
164
+ ### Available slots
165
+
166
+ | Slot | Props forwarded | Default |
167
+ |---|---|---|
168
+ | `UserMessage` | none | built-in user bubble |
169
+ | `AssistantMessage` | none | built-in assistant bubble |
170
+ | `EditComposer` | none | built-in inline edit composer |
171
+ | `Composer` | `placeholder` | built-in composer bar |
172
+ | `Welcome` | `config`, `suggestions` | built-in welcome screen |
173
+ | `ScrollToBottom` | none | built-in scroll button |
174
+
175
+ Custom slot components read their data via hooks — no props are passed automatically except where noted above.
176
+
177
+ ### Custom user message
178
+
179
+ ```tsx
180
+ import { TimbalChat, MessagePrimitive } from "@timbal-ai/timbal-react";
181
+
182
+ const CompactUserMessage = () => (
183
+ <MessagePrimitive.Root className="flex justify-end px-4 py-2">
184
+ <div className="bg-primary text-primary-foreground rounded-2xl px-4 py-2 text-sm max-w-[75%]">
185
+ <MessagePrimitive.Parts />
186
+ </div>
187
+ </MessagePrimitive.Root>
188
+ );
189
+
190
+ <TimbalChat workforceId="..." components={{ UserMessage: CompactUserMessage }} />
191
+ ```
192
+
193
+ ### Custom composer
194
+
195
+ The `Composer` slot receives `placeholder` from the `composerPlaceholder` prop:
196
+
197
+ ```tsx
198
+ import { TimbalChat, ComposerPrimitive } from "@timbal-ai/timbal-react";
199
+
200
+ const MinimalComposer = ({ placeholder }: { placeholder?: string }) => (
201
+ <ComposerPrimitive.Root className="flex items-center gap-2 border rounded-full px-4 py-2">
202
+ <ComposerPrimitive.Input
203
+ placeholder={placeholder ?? "Type here..."}
204
+ className="flex-1 bg-transparent text-sm outline-none"
205
+ rows={1}
206
+ />
207
+ <ComposerPrimitive.Send className="text-primary font-medium text-sm">
208
+ Send
209
+ </ComposerPrimitive.Send>
210
+ </ComposerPrimitive.Root>
211
+ );
212
+
213
+ <TimbalChat workforceId="..." components={{ Composer: MinimalComposer }} />
214
+ ```
215
+
216
+ ### Custom welcome screen
217
+
218
+ The `Welcome` slot is always mounted and controls its own visibility. Use `useThread` to replicate the default "show only when the thread is empty" behaviour:
219
+
220
+ ```tsx
221
+ import { TimbalChat, useThread, useThreadRuntime, type ThreadWelcomeProps } from "@timbal-ai/timbal-react";
222
+
223
+ const BrandedWelcome = ({ suggestions }: ThreadWelcomeProps) => {
224
+ const isEmpty = useThread((s) => s.isEmpty);
225
+ const runtime = useThreadRuntime();
226
+ if (!isEmpty) return null;
227
+ return (
228
+ <div className="flex flex-col items-center justify-center h-full gap-4">
229
+ <img src="/logo.svg" className="h-12" />
230
+ <h2 className="text-xl font-semibold">Welcome to Acme AI</h2>
231
+ <div className="flex gap-2 flex-wrap justify-center">
232
+ {suggestions?.map((s) => (
233
+ <button
234
+ key={s.title}
235
+ onClick={() => runtime.append({ role: "user", content: [{ type: "text", text: s.title }] })}
236
+ className="border rounded-full px-4 py-1.5 text-sm hover:bg-muted"
237
+ >
238
+ {s.title}
239
+ </button>
240
+ ))}
241
+ </div>
242
+ </div>
243
+ );
244
+ };
245
+
246
+ <TimbalChat
247
+ workforceId="..."
248
+ suggestions={[{ title: "Get started" }, { title: "Show me an example" }]}
249
+ components={{ Welcome: BrandedWelcome }}
250
+ />
251
+ ```
252
+
253
+ ### Mixing slots
254
+
255
+ Override any combination — slots are independent of each other:
256
+
257
+ ```tsx
258
+ <TimbalChat
259
+ workforceId="..."
260
+ components={{
261
+ UserMessage: CompactUserMessage,
262
+ Composer: MinimalComposer,
263
+ }}
264
+ />
265
+ ```
266
+
267
+ ### Hooks and primitives
268
+
269
+ These are re-exported from `@assistant-ui/react` for use inside custom slot components:
270
+
271
+ | Export | Use inside |
272
+ |---|---|
273
+ | `ThreadPrimitive` | Any slot |
274
+ | `MessagePrimitive` | `UserMessage`, `AssistantMessage`, `EditComposer` |
275
+ | `ComposerPrimitive` | `Composer`, `EditComposer` |
276
+ | `ActionBarPrimitive` | `UserMessage`, `AssistantMessage` |
277
+ | `useThread` | Any slot — subscribe to thread state (e.g. `isRunning`, `isEmpty`) |
278
+ | `useThreadRuntime` | Any slot — call actions (e.g. `runtime.append(...)`) |
279
+ | `useMessageRuntime` | `UserMessage`, `AssistantMessage` — edit, reload, branch |
280
+ | `useComposerRuntime` | `Composer`, `EditComposer` — access composer state |
281
+
282
+ ---
283
+
284
+ ## API reference
285
+
286
+ ### `TimbalChat` props
287
+
288
+ `TimbalChat` accepts all `TimbalRuntimeProvider` props plus all `Thread` props.
65
289
 
66
290
  | Prop | Type | Default | Description |
67
291
  |---|---|---|---|
68
- | `workforceId` | `string` | | ID of the workforce to stream from |
292
+ | `workforceId` | `string` | **required** | ID of the workforce to stream from |
69
293
  | `baseUrl` | `string` | `"/api"` | Base URL for API calls. Posts to `{baseUrl}/workforce/{workforceId}/stream` |
70
- | `fetch` | `(url, options?) => Promise<Response>` | `authFetch` | Custom fetch function. Defaults to the built-in auth-aware fetch (Bearer token + auto-refresh) |
294
+ | `fetch` | `(url, options?) => Promise<Response>` | `authFetch` | Custom fetch. Defaults to the built-in auth-aware fetch (Bearer token + auto-refresh) |
295
+ | `welcome.heading` | `string` | `"How can I help you today?"` | Welcome screen heading |
296
+ | `welcome.subheading` | `string` | `"Send a message to start a conversation."` | Welcome screen subheading |
297
+ | `suggestions` | `{ title: string; description?: string }[]` | — | Suggestion chips on the welcome screen |
298
+ | `composerPlaceholder` | `string` | `"Send a message..."` | Composer input placeholder |
299
+ | `components` | `ThreadComponents` | — | Override individual UI slots |
300
+ | `maxWidth` | `string` | `"44rem"` | Max width of the message column |
301
+ | `className` | `string` | — | Extra classes on the root element |
302
+
303
+ ### `Thread` props
304
+
305
+ Same as `TimbalChat` minus `workforceId`, `baseUrl`, and `fetch` (those live on `TimbalRuntimeProvider`).
306
+
307
+ ### `TimbalRuntimeProvider` props
308
+
309
+ | Prop | Type | Default | Description |
310
+ |---|---|---|---|
311
+ | `workforceId` | `string` | **required** | ID of the workforce to stream from |
312
+ | `baseUrl` | `string` | `"/api"` | Base URL for API calls |
313
+ | `fetch` | `(url, options?) => Promise<Response>` | `authFetch` | Custom fetch function |
71
314
 
72
315
  ---
73
316
 
74
317
  ## Auth
75
318
 
76
- The package includes a session/auth system backed by localStorage tokens. The API is expected to expose `/api/auth/login`, `/api/auth/logout`, and `/api/auth/refresh`.
319
+ The package includes an optional session/auth system backed by localStorage tokens. The API is expected to expose `/api/auth/login`, `/api/auth/logout`, and `/api/auth/refresh`.
320
+
321
+ Auth is **opt-in** — it only activates when `VITE_TIMBAL_PROJECT_ID` is set in your environment.
77
322
 
78
323
  ### Setup
79
324
 
80
325
  Wrap your app with `SessionProvider` and protect routes with `AuthGuard`:
81
326
 
82
327
  ```tsx
328
+ // src/App.tsx
83
329
  import { SessionProvider, AuthGuard, TooltipProvider } from "@timbal-ai/timbal-react";
330
+ import { BrowserRouter, Routes, Route } from "react-router-dom";
331
+ import Home from "./pages/Home";
84
332
 
85
333
  const isAuthEnabled = !!import.meta.env.VITE_TIMBAL_PROJECT_ID;
86
334
 
87
- function App() {
335
+ export default function App() {
88
336
  return (
89
337
  <SessionProvider enabled={isAuthEnabled}>
90
338
  <TooltipProvider>
91
- <AuthGuard requireAuth enabled={isAuthEnabled}>
92
- <YourApp />
93
- </AuthGuard>
339
+ <BrowserRouter>
340
+ <AuthGuard requireAuth enabled={isAuthEnabled}>
341
+ <Routes>
342
+ <Route path="/" element={<Home />} />
343
+ </Routes>
344
+ </AuthGuard>
345
+ </BrowserRouter>
94
346
  </TooltipProvider>
95
347
  </SessionProvider>
96
348
  );
97
349
  }
98
350
  ```
99
351
 
100
- ### `SessionProvider` props
101
-
102
- | Prop | Type | Default | Description |
103
- |---|---|---|---|
104
- | `enabled` | `boolean` | `true` | When `false`, session is always `null` and no API calls are made |
105
-
106
- ### `AuthGuard` props
107
-
108
- | Prop | Type | Default | Description |
109
- |---|---|---|---|
110
- | `requireAuth` | `boolean` | `false` | Redirect to login if not authenticated |
111
- | `enabled` | `boolean` | `true` | When `false`, renders children unconditionally |
352
+ When `enabled` is `false`, both `SessionProvider` and `AuthGuard` are transparent — no redirects, no API calls.
112
353
 
113
354
  ### `useSession` hook
114
355
 
356
+ Access the current session anywhere inside `SessionProvider`:
357
+
115
358
  ```tsx
116
359
  import { useSession } from "@timbal-ai/timbal-react";
117
360
 
118
361
  function Header() {
119
362
  const { user, isAuthenticated, loading, logout } = useSession();
120
- // ...
363
+ if (loading) return null;
364
+ return (
365
+ <header>
366
+ {isAuthenticated ? (
367
+ <>
368
+ <span>{user?.email}</span>
369
+ <button onClick={logout}>Log out</button>
370
+ </>
371
+ ) : (
372
+ <a href="/login">Log in</a>
373
+ )}
374
+ </header>
375
+ );
121
376
  }
122
377
  ```
123
378
 
124
379
  ### `authFetch`
125
380
 
126
- A drop-in replacement for `fetch` that attaches the Bearer token and auto-refreshes on 401:
381
+ A drop-in replacement for `fetch` that attaches the Bearer token from localStorage and auto-refreshes on 401. It's also the default `fetch` used by `TimbalRuntimeProvider`, so you only need to import it directly for your own API calls (e.g. loading workforce lists):
127
382
 
128
383
  ```tsx
129
384
  import { authFetch } from "@timbal-ai/timbal-react";
130
385
 
131
386
  const res = await authFetch("/api/workforce");
387
+ if (res.ok) {
388
+ const agents = await res.json();
389
+ }
132
390
  ```
133
391
 
392
+ ### Auth prop reference
393
+
394
+ | Component | Prop | Type | Default | Description |
395
+ |---|---|---|---|---|
396
+ | `SessionProvider` | `enabled` | `boolean` | `true` | When `false`, session is always `null` and no API calls are made |
397
+ | `AuthGuard` | `requireAuth` | `boolean` | `false` | Redirect to login if not authenticated |
398
+ | `AuthGuard` | `enabled` | `boolean` | `true` | When `false`, renders children unconditionally |
399
+
134
400
  ---
135
401
 
136
- ## Components
402
+ ## Other exports
137
403
 
138
- All components accept `className` for Tailwind overrides.
404
+ ### Components
139
405
 
140
406
  | Export | Description |
141
407
  |---|---|
@@ -156,12 +422,71 @@ Re-exported Radix UI wrappers pre-styled to match the Timbal design system:
156
422
 
157
423
  ---
158
424
 
425
+ ## Full example
426
+
427
+ A complete page with agent switching, auth, and a custom header:
428
+
429
+ ```tsx
430
+ // src/pages/Home.tsx
431
+ import { useEffect, useState } from "react";
432
+ import type { WorkforceItem } from "@timbal-ai/timbal-sdk";
433
+ import { TimbalChat, Button, authFetch, useSession } from "@timbal-ai/timbal-react";
434
+ import { LogOut } from "lucide-react";
435
+
436
+ const isAuthEnabled = !!import.meta.env.VITE_TIMBAL_PROJECT_ID;
437
+
438
+ export default function Home() {
439
+ const { logout } = useSession();
440
+ const [workforces, setWorkforces] = useState<WorkforceItem[]>([]);
441
+ const [selectedId, setSelectedId] = useState("");
442
+
443
+ useEffect(() => {
444
+ authFetch("/api/workforce")
445
+ .then((r) => r.json())
446
+ .then((data: WorkforceItem[]) => {
447
+ setWorkforces(data);
448
+ const agent = data.find((w) => w.type === "agent") ?? data[0];
449
+ if (agent) setSelectedId(agent.id ?? agent.name ?? "");
450
+ })
451
+ .catch(() => {});
452
+ }, []);
453
+
454
+ return (
455
+ <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
456
+ <header style={{ display: "flex", justifyContent: "space-between", padding: "0.5rem 1.25rem" }}>
457
+ <select value={selectedId} onChange={(e) => setSelectedId(e.target.value)}>
458
+ {workforces.map((w) => (
459
+ <option key={w.id ?? w.name} value={w.id ?? w.name ?? ""}>
460
+ {w.name}
461
+ </option>
462
+ ))}
463
+ </select>
464
+
465
+ {isAuthEnabled && (
466
+ <Button variant="ghost" size="icon" onClick={logout}>
467
+ <LogOut />
468
+ </Button>
469
+ )}
470
+ </header>
471
+
472
+ <TimbalChat
473
+ workforceId={selectedId}
474
+ key={selectedId}
475
+ className="flex-1 min-h-0"
476
+ welcome={{ heading: "How can I help you today?" }}
477
+ />
478
+ </div>
479
+ );
480
+ }
481
+ ```
482
+
483
+ ---
484
+
159
485
  ## Local development
160
486
 
161
487
  Install via a local path reference:
162
488
 
163
489
  ```json
164
- // package.json
165
490
  {
166
491
  "dependencies": {
167
492
  "@timbal-ai/timbal-react": "file:../../timbal-react"
@@ -171,7 +496,7 @@ Install via a local path reference:
171
496
 
172
497
  Adjust the relative path to where `timbal-react` lives on your machine.
173
498
 
174
- After editing source files, rebuild the package:
499
+ After editing source files, rebuild:
175
500
 
176
501
  ```bash
177
502
  cd timbal-react
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ ActionBarPrimitive: () => import_react13.ActionBarPrimitive,
33
34
  AuthGuard: () => AuthGuard,
34
35
  Avatar: () => Avatar,
35
36
  AvatarFallback: () => AvatarFallback,
@@ -37,6 +38,7 @@ __export(index_exports, {
37
38
  Button: () => Button,
38
39
  ComposerAddAttachment: () => ComposerAddAttachment,
39
40
  ComposerAttachments: () => ComposerAttachments,
41
+ ComposerPrimitive: () => import_react13.ComposerPrimitive,
40
42
  Dialog: () => Dialog,
41
43
  DialogClose: () => DialogClose,
42
44
  DialogContent: () => DialogContent,
@@ -45,10 +47,13 @@ __export(index_exports, {
45
47
  DialogTitle: () => DialogTitle,
46
48
  DialogTrigger: () => DialogTrigger,
47
49
  MarkdownText: () => MarkdownText,
50
+ MessagePrimitive: () => import_react13.MessagePrimitive,
48
51
  SessionProvider: () => SessionProvider,
49
52
  Shimmer: () => Shimmer,
50
53
  SyntaxHighlighter: () => syntax_highlighter_default,
51
54
  Thread: () => Thread,
55
+ ThreadPrimitive: () => import_react13.ThreadPrimitive,
56
+ TimbalChat: () => TimbalChat,
52
57
  TimbalRuntimeProvider: () => TimbalRuntimeProvider,
53
58
  ToolFallback: () => ToolFallback,
54
59
  Tooltip: () => Tooltip,
@@ -65,7 +70,11 @@ __export(index_exports, {
65
70
  getAccessToken: () => getAccessToken,
66
71
  getRefreshToken: () => getRefreshToken,
67
72
  refreshAccessToken: () => refreshAccessToken,
68
- useSession: () => useSession
73
+ useComposerRuntime: () => import_react13.useComposerRuntime,
74
+ useMessageRuntime: () => import_react13.useMessageRuntime,
75
+ useSession: () => useSession,
76
+ useThread: () => import_react13.useThread,
77
+ useThreadRuntime: () => import_react13.useThreadRuntime
69
78
  });
70
79
  module.exports = __toCommonJS(index_exports);
71
80
 
@@ -1267,34 +1276,48 @@ ToolFallback.displayName = "ToolFallback";
1267
1276
  var import_react11 = require("@assistant-ui/react");
1268
1277
  var import_lucide_react5 = require("lucide-react");
1269
1278
  var import_jsx_runtime12 = require("react/jsx-runtime");
1270
- var Thread = () => {
1279
+ var Thread = ({
1280
+ className,
1281
+ maxWidth = "44rem",
1282
+ welcome,
1283
+ suggestions,
1284
+ composerPlaceholder = "Send a message...",
1285
+ components
1286
+ }) => {
1287
+ const WelcomeSlot = components?.Welcome ?? ThreadWelcome;
1288
+ const ComposerSlot = components?.Composer ?? Composer;
1289
+ const UserMessageSlot = components?.UserMessage ?? UserMessage;
1290
+ const AssistantMessageSlot = components?.AssistantMessage ?? AssistantMessage;
1291
+ const EditComposerSlot = components?.EditComposer ?? EditComposer;
1292
+ const ScrollToBottomSlot = components?.ScrollToBottom ?? ThreadScrollToBottom;
1271
1293
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1272
1294
  import_react11.ThreadPrimitive.Root,
1273
1295
  {
1274
- className: "aui-root aui-thread-root @container flex h-full flex-col bg-background",
1275
- style: {
1276
- ["--thread-max-width"]: "44rem"
1277
- },
1296
+ className: cn(
1297
+ "aui-root aui-thread-root @container flex h-full flex-col bg-background",
1298
+ className
1299
+ ),
1300
+ style: { ["--thread-max-width"]: maxWidth },
1278
1301
  children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1279
1302
  import_react11.ThreadPrimitive.Viewport,
1280
1303
  {
1281
1304
  turnAnchor: "bottom",
1282
1305
  className: "aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4 pt-4",
1283
1306
  children: [
1284
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react11.AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadWelcome, {}) }),
1307
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(WelcomeSlot, { config: welcome, suggestions }),
1285
1308
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1286
1309
  import_react11.ThreadPrimitive.Messages,
1287
1310
  {
1288
1311
  components: {
1289
- UserMessage,
1290
- EditComposer,
1291
- AssistantMessage
1312
+ UserMessage: UserMessageSlot,
1313
+ EditComposer: EditComposerSlot,
1314
+ AssistantMessage: AssistantMessageSlot
1292
1315
  }
1293
1316
  }
1294
1317
  ),
1295
1318
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react11.ThreadPrimitive.ViewportFooter, { className: "aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6", children: [
1296
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadScrollToBottom, {}),
1297
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Composer, {})
1319
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ScrollToBottomSlot, {}),
1320
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ComposerSlot, { placeholder: composerPlaceholder })
1298
1321
  ] })
1299
1322
  ]
1300
1323
  }
@@ -1313,8 +1336,8 @@ var ThreadScrollToBottom = () => {
1313
1336
  }
1314
1337
  ) });
1315
1338
  };
1316
- var ThreadWelcome = () => {
1317
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col", children: [
1339
+ var ThreadWelcome = ({ config, suggestions }) => {
1340
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react11.AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col", children: [
1318
1341
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "aui-thread-welcome-center flex w-full grow flex-col items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "aui-thread-welcome-message flex size-full flex-col items-center justify-center px-4 text-center", children: [
1319
1342
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "fade-in animate-in fill-mode-both relative mb-6 flex size-14 items-center justify-center duration-300", children: [
1320
1343
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "animate-ai-ring-glow absolute inset-0 rounded-2xl bg-gradient-to-br from-primary/15 to-primary/5 ring-1 ring-primary/15" }),
@@ -1334,42 +1357,40 @@ var ThreadWelcome = () => {
1334
1357
  }
1335
1358
  )
1336
1359
  ] }),
1337
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h1", { className: "aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both font-semibold text-2xl duration-200", children: "How can I help you today?" }),
1338
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground mt-2 delay-75 duration-200", children: "Send a message to start a conversation." })
1360
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h1", { className: "aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both font-semibold text-2xl duration-200", children: config?.heading ?? "How can I help you today?" }),
1361
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground mt-2 delay-75 duration-200", children: config?.subheading ?? "Send a message to start a conversation." })
1339
1362
  ] }) }),
1340
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadSuggestions, {})
1341
- ] });
1363
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadSuggestions, { suggestions })
1364
+ ] }) });
1342
1365
  };
1343
- var ThreadSuggestions = () => {
1344
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1345
- import_react11.ThreadPrimitive.Suggestions,
1346
- {
1347
- components: {
1348
- Suggestion: ThreadSuggestionItem
1349
- }
1350
- }
1351
- ) });
1366
+ var ThreadSuggestions = ({ suggestions }) => {
1367
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4", children: suggestions.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadSuggestionItem, { title: s.title, description: s.description }, i)) });
1352
1368
  };
1353
- var ThreadSuggestionItem = () => {
1354
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-200", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react11.SuggestionPrimitive.Trigger, { send: true, asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1369
+ var ThreadSuggestionItem = ({ title, description }) => {
1370
+ const runtime = (0, import_react11.useThreadRuntime)();
1371
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 animate-in fill-mode-both duration-200", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1355
1372
  Button,
1356
1373
  {
1357
1374
  variant: "ghost",
1358
1375
  className: "aui-thread-welcome-suggestion h-auto w-full @md:flex-col flex-wrap items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-left text-sm transition-colors hover:bg-muted",
1376
+ onClick: () => runtime.append({
1377
+ role: "user",
1378
+ content: [{ type: "text", text: title }]
1379
+ }),
1359
1380
  children: [
1360
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "aui-thread-welcome-suggestion-text-1 font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react11.SuggestionPrimitive.Title, {}) }),
1361
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "aui-thread-welcome-suggestion-text-2 text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react11.SuggestionPrimitive.Description, {}) })
1381
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "aui-thread-welcome-suggestion-text-1 font-medium", children: title }),
1382
+ description && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "aui-thread-welcome-suggestion-text-2 text-muted-foreground", children: description })
1362
1383
  ]
1363
1384
  }
1364
- ) }) });
1385
+ ) });
1365
1386
  };
1366
- var Composer = () => {
1387
+ var Composer = ({ placeholder }) => {
1367
1388
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react11.ComposerPrimitive.Root, { className: "aui-composer-root relative mt-3 flex w-full flex-col", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react11.ComposerPrimitive.AttachmentDropzone, { className: "aui-composer-attachment-dropzone flex w-full flex-col rounded-2xl border border-input bg-background px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50", children: [
1368
1389
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ComposerAttachments, {}),
1369
1390
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1370
1391
  import_react11.ComposerPrimitive.Input,
1371
1392
  {
1372
- placeholder: "Send a message...",
1393
+ placeholder: placeholder ?? "Send a message...",
1373
1394
  className: "aui-composer-input mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0",
1374
1395
  rows: 1,
1375
1396
  autoFocus: true,
@@ -1517,9 +1538,23 @@ var EditComposer = () => {
1517
1538
  ] }) });
1518
1539
  };
1519
1540
 
1541
+ // src/components/chat.tsx
1542
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1543
+ function TimbalChat({
1544
+ workforceId,
1545
+ baseUrl,
1546
+ fetch: fetch2,
1547
+ ...threadProps
1548
+ }) {
1549
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TimbalRuntimeProvider, { workforceId, baseUrl, fetch: fetch2, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Thread, { ...threadProps }) });
1550
+ }
1551
+
1552
+ // src/index.ts
1553
+ var import_react13 = require("@assistant-ui/react");
1554
+
1520
1555
  // src/auth/provider.tsx
1521
1556
  var import_react12 = require("react");
1522
- var import_jsx_runtime13 = require("react/jsx-runtime");
1557
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1523
1558
  var SessionContext = (0, import_react12.createContext)(void 0);
1524
1559
  var useSession = () => {
1525
1560
  const context = (0, import_react12.useContext)(SessionContext);
@@ -1583,7 +1618,7 @@ var SessionProvider = ({
1583
1618
  () => window.location.href = `/api/auth/login?return_to=${returnTo}`
1584
1619
  );
1585
1620
  }, []);
1586
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1621
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1587
1622
  SessionContext.Provider,
1588
1623
  {
1589
1624
  value: {
@@ -1599,7 +1634,7 @@ var SessionProvider = ({
1599
1634
 
1600
1635
  // src/auth/guard.tsx
1601
1636
  var import_lucide_react6 = require("lucide-react");
1602
- var import_jsx_runtime14 = require("react/jsx-runtime");
1637
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1603
1638
  var AuthGuard = ({
1604
1639
  children,
1605
1640
  requireAuth = false,
@@ -1610,7 +1645,7 @@ var AuthGuard = ({
1610
1645
  return children;
1611
1646
  }
1612
1647
  if (loading) {
1613
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex items-center justify-center h-screen", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react6.Loader2, { className: "w-8 h-8 animate-spin" }) });
1648
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex items-center justify-center h-screen", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.Loader2, { className: "w-8 h-8 animate-spin" }) });
1614
1649
  }
1615
1650
  if (requireAuth && !isAuthenticated) {
1616
1651
  const returnTo = encodeURIComponent(
@@ -1623,6 +1658,7 @@ var AuthGuard = ({
1623
1658
  };
1624
1659
  // Annotate the CommonJS export names for ESM import in node:
1625
1660
  0 && (module.exports = {
1661
+ ActionBarPrimitive,
1626
1662
  AuthGuard,
1627
1663
  Avatar,
1628
1664
  AvatarFallback,
@@ -1630,6 +1666,7 @@ var AuthGuard = ({
1630
1666
  Button,
1631
1667
  ComposerAddAttachment,
1632
1668
  ComposerAttachments,
1669
+ ComposerPrimitive,
1633
1670
  Dialog,
1634
1671
  DialogClose,
1635
1672
  DialogContent,
@@ -1638,10 +1675,13 @@ var AuthGuard = ({
1638
1675
  DialogTitle,
1639
1676
  DialogTrigger,
1640
1677
  MarkdownText,
1678
+ MessagePrimitive,
1641
1679
  SessionProvider,
1642
1680
  Shimmer,
1643
1681
  SyntaxHighlighter,
1644
1682
  Thread,
1683
+ ThreadPrimitive,
1684
+ TimbalChat,
1645
1685
  TimbalRuntimeProvider,
1646
1686
  ToolFallback,
1647
1687
  Tooltip,
@@ -1658,5 +1698,9 @@ var AuthGuard = ({
1658
1698
  getAccessToken,
1659
1699
  getRefreshToken,
1660
1700
  refreshAccessToken,
1661
- useSession
1701
+ useComposerRuntime,
1702
+ useMessageRuntime,
1703
+ useSession,
1704
+ useThread,
1705
+ useThreadRuntime
1662
1706
  });
package/dist/index.d.cts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
- import React__default, { ReactNode, FC, ComponentPropsWithRef, ElementType } from 'react';
3
+ import React__default, { ReactNode, FC, ComponentType, ComponentPropsWithRef, ElementType } from 'react';
4
4
  import { ToolCallMessagePartComponent } from '@assistant-ui/react';
5
+ export { ActionBarPrimitive, ComposerPrimitive, MessagePrimitive, ThreadPrimitive, useComposerRuntime, useMessageRuntime, useThread, useThreadRuntime } from '@assistant-ui/react';
5
6
  import * as class_variance_authority_types from 'class-variance-authority/types';
6
7
  import { VariantProps } from 'class-variance-authority';
7
8
  import { SyntaxHighlighterProps } from '@assistant-ui/react-markdown';
@@ -26,7 +27,52 @@ interface TimbalRuntimeProviderProps {
26
27
  }
27
28
  declare function TimbalRuntimeProvider({ workforceId, children, baseUrl, fetch: fetchFn, }: TimbalRuntimeProviderProps): react_jsx_runtime.JSX.Element;
28
29
 
29
- declare const Thread: FC;
30
+ interface ThreadSuggestion {
31
+ title: string;
32
+ description?: string;
33
+ }
34
+ interface ThreadWelcomeConfig {
35
+ heading?: string;
36
+ subheading?: string;
37
+ }
38
+ interface ThreadWelcomeProps {
39
+ config?: ThreadWelcomeConfig;
40
+ suggestions?: ThreadSuggestion[];
41
+ }
42
+ interface ThreadComponents {
43
+ /** Replace the user message bubble. Access message content via `MessagePrimitive.Parts`. */
44
+ UserMessage?: ComponentType;
45
+ /** Replace the assistant message bubble. Access message content via `MessagePrimitive.Parts`. */
46
+ AssistantMessage?: ComponentType;
47
+ /** Replace the inline edit composer. */
48
+ EditComposer?: ComponentType;
49
+ /** Replace the composer (input bar). Receives `placeholder` from `composerPlaceholder`. */
50
+ Composer?: ComponentType<{
51
+ placeholder?: string;
52
+ }>;
53
+ /** Replace the welcome / empty state. Receives `config` and `suggestions` props. Controls its own visibility — use `useThread(s => s.isEmpty)` to replicate the default behaviour. */
54
+ Welcome?: ComponentType<ThreadWelcomeProps>;
55
+ /** Replace the scroll-to-bottom button. */
56
+ ScrollToBottom?: ComponentType;
57
+ }
58
+ interface ThreadProps {
59
+ className?: string;
60
+ /** Max width of the message column. Default: "44rem" */
61
+ maxWidth?: string;
62
+ /** Welcome screen text */
63
+ welcome?: ThreadWelcomeConfig;
64
+ /** Suggestion chips shown on the welcome screen */
65
+ suggestions?: ThreadSuggestion[];
66
+ /** Composer input placeholder. Default: "Send a message..." */
67
+ composerPlaceholder?: string;
68
+ /** Override individual UI slots while keeping the rest as defaults. */
69
+ components?: ThreadComponents;
70
+ }
71
+ declare const Thread: FC<ThreadProps>;
72
+
73
+ interface TimbalChatProps extends Omit<TimbalRuntimeProviderProps, "children">, ThreadProps {
74
+ }
75
+ declare function TimbalChat({ workforceId, baseUrl, fetch, ...threadProps }: TimbalChatProps): react_jsx_runtime.JSX.Element;
30
76
 
31
77
  declare const MarkdownText: React.MemoExoticComponent<() => react_jsx_runtime.JSX.Element>;
32
78
 
@@ -113,4 +159,4 @@ declare const Shimmer: React.MemoExoticComponent<({ children, as: Component, cla
113
159
 
114
160
  declare function cn(...inputs: ClassValue[]): string;
115
161
 
116
- export { AuthGuard, Avatar, AvatarFallback, AvatarImage, Button, ComposerAddAttachment, ComposerAttachments, Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, MarkdownText, SessionProvider, Shimmer, ShikiSyntaxHighlighter as SyntaxHighlighter, type TextShimmerProps, Thread, TimbalRuntimeProvider, type TimbalRuntimeProviderProps, ToolFallback, Tooltip, TooltipContent, TooltipIconButton, type TooltipIconButtonProps, TooltipProvider, TooltipTrigger, UserMessageAttachments, authFetch, buttonVariants, clearTokens, cn, fetchCurrentUser, getAccessToken, getRefreshToken, refreshAccessToken, useSession };
162
+ export { AuthGuard, Avatar, AvatarFallback, AvatarImage, Button, ComposerAddAttachment, ComposerAttachments, Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, MarkdownText, SessionProvider, Shimmer, ShikiSyntaxHighlighter as SyntaxHighlighter, type TextShimmerProps, Thread, type ThreadComponents, type ThreadProps, type ThreadSuggestion, type ThreadWelcomeConfig, type ThreadWelcomeProps, TimbalChat, type TimbalChatProps, TimbalRuntimeProvider, type TimbalRuntimeProviderProps, ToolFallback, Tooltip, TooltipContent, TooltipIconButton, type TooltipIconButtonProps, TooltipProvider, TooltipTrigger, UserMessageAttachments, authFetch, buttonVariants, clearTokens, cn, fetchCurrentUser, getAccessToken, getRefreshToken, refreshAccessToken, useSession };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
- import React__default, { ReactNode, FC, ComponentPropsWithRef, ElementType } from 'react';
3
+ import React__default, { ReactNode, FC, ComponentType, ComponentPropsWithRef, ElementType } from 'react';
4
4
  import { ToolCallMessagePartComponent } from '@assistant-ui/react';
5
+ export { ActionBarPrimitive, ComposerPrimitive, MessagePrimitive, ThreadPrimitive, useComposerRuntime, useMessageRuntime, useThread, useThreadRuntime } from '@assistant-ui/react';
5
6
  import * as class_variance_authority_types from 'class-variance-authority/types';
6
7
  import { VariantProps } from 'class-variance-authority';
7
8
  import { SyntaxHighlighterProps } from '@assistant-ui/react-markdown';
@@ -26,7 +27,52 @@ interface TimbalRuntimeProviderProps {
26
27
  }
27
28
  declare function TimbalRuntimeProvider({ workforceId, children, baseUrl, fetch: fetchFn, }: TimbalRuntimeProviderProps): react_jsx_runtime.JSX.Element;
28
29
 
29
- declare const Thread: FC;
30
+ interface ThreadSuggestion {
31
+ title: string;
32
+ description?: string;
33
+ }
34
+ interface ThreadWelcomeConfig {
35
+ heading?: string;
36
+ subheading?: string;
37
+ }
38
+ interface ThreadWelcomeProps {
39
+ config?: ThreadWelcomeConfig;
40
+ suggestions?: ThreadSuggestion[];
41
+ }
42
+ interface ThreadComponents {
43
+ /** Replace the user message bubble. Access message content via `MessagePrimitive.Parts`. */
44
+ UserMessage?: ComponentType;
45
+ /** Replace the assistant message bubble. Access message content via `MessagePrimitive.Parts`. */
46
+ AssistantMessage?: ComponentType;
47
+ /** Replace the inline edit composer. */
48
+ EditComposer?: ComponentType;
49
+ /** Replace the composer (input bar). Receives `placeholder` from `composerPlaceholder`. */
50
+ Composer?: ComponentType<{
51
+ placeholder?: string;
52
+ }>;
53
+ /** Replace the welcome / empty state. Receives `config` and `suggestions` props. Controls its own visibility — use `useThread(s => s.isEmpty)` to replicate the default behaviour. */
54
+ Welcome?: ComponentType<ThreadWelcomeProps>;
55
+ /** Replace the scroll-to-bottom button. */
56
+ ScrollToBottom?: ComponentType;
57
+ }
58
+ interface ThreadProps {
59
+ className?: string;
60
+ /** Max width of the message column. Default: "44rem" */
61
+ maxWidth?: string;
62
+ /** Welcome screen text */
63
+ welcome?: ThreadWelcomeConfig;
64
+ /** Suggestion chips shown on the welcome screen */
65
+ suggestions?: ThreadSuggestion[];
66
+ /** Composer input placeholder. Default: "Send a message..." */
67
+ composerPlaceholder?: string;
68
+ /** Override individual UI slots while keeping the rest as defaults. */
69
+ components?: ThreadComponents;
70
+ }
71
+ declare const Thread: FC<ThreadProps>;
72
+
73
+ interface TimbalChatProps extends Omit<TimbalRuntimeProviderProps, "children">, ThreadProps {
74
+ }
75
+ declare function TimbalChat({ workforceId, baseUrl, fetch, ...threadProps }: TimbalChatProps): react_jsx_runtime.JSX.Element;
30
76
 
31
77
  declare const MarkdownText: React.MemoExoticComponent<() => react_jsx_runtime.JSX.Element>;
32
78
 
@@ -113,4 +159,4 @@ declare const Shimmer: React.MemoExoticComponent<({ children, as: Component, cla
113
159
 
114
160
  declare function cn(...inputs: ClassValue[]): string;
115
161
 
116
- export { AuthGuard, Avatar, AvatarFallback, AvatarImage, Button, ComposerAddAttachment, ComposerAttachments, Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, MarkdownText, SessionProvider, Shimmer, ShikiSyntaxHighlighter as SyntaxHighlighter, type TextShimmerProps, Thread, TimbalRuntimeProvider, type TimbalRuntimeProviderProps, ToolFallback, Tooltip, TooltipContent, TooltipIconButton, type TooltipIconButtonProps, TooltipProvider, TooltipTrigger, UserMessageAttachments, authFetch, buttonVariants, clearTokens, cn, fetchCurrentUser, getAccessToken, getRefreshToken, refreshAccessToken, useSession };
162
+ export { AuthGuard, Avatar, AvatarFallback, AvatarImage, Button, ComposerAddAttachment, ComposerAttachments, Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, MarkdownText, SessionProvider, Shimmer, ShikiSyntaxHighlighter as SyntaxHighlighter, type TextShimmerProps, Thread, type ThreadComponents, type ThreadProps, type ThreadSuggestion, type ThreadWelcomeConfig, type ThreadWelcomeProps, TimbalChat, type TimbalChatProps, TimbalRuntimeProvider, type TimbalRuntimeProviderProps, ToolFallback, Tooltip, TooltipContent, TooltipIconButton, type TooltipIconButtonProps, TooltipProvider, TooltipTrigger, UserMessageAttachments, authFetch, buttonVariants, clearTokens, cn, fetchCurrentUser, getAccessToken, getRefreshToken, refreshAccessToken, useSession };
package/dist/index.esm.js CHANGED
@@ -1216,8 +1216,8 @@ import {
1216
1216
  ComposerPrimitive as ComposerPrimitive2,
1217
1217
  ErrorPrimitive,
1218
1218
  MessagePrimitive as MessagePrimitive2,
1219
- SuggestionPrimitive,
1220
- ThreadPrimitive
1219
+ ThreadPrimitive,
1220
+ useThreadRuntime
1221
1221
  } from "@assistant-ui/react";
1222
1222
  import {
1223
1223
  ArrowDownIcon,
@@ -1231,34 +1231,48 @@ import {
1231
1231
  SquareIcon
1232
1232
  } from "lucide-react";
1233
1233
  import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
1234
- var Thread = () => {
1234
+ var Thread = ({
1235
+ className,
1236
+ maxWidth = "44rem",
1237
+ welcome,
1238
+ suggestions,
1239
+ composerPlaceholder = "Send a message...",
1240
+ components
1241
+ }) => {
1242
+ const WelcomeSlot = components?.Welcome ?? ThreadWelcome;
1243
+ const ComposerSlot = components?.Composer ?? Composer;
1244
+ const UserMessageSlot = components?.UserMessage ?? UserMessage;
1245
+ const AssistantMessageSlot = components?.AssistantMessage ?? AssistantMessage;
1246
+ const EditComposerSlot = components?.EditComposer ?? EditComposer;
1247
+ const ScrollToBottomSlot = components?.ScrollToBottom ?? ThreadScrollToBottom;
1235
1248
  return /* @__PURE__ */ jsx12(
1236
1249
  ThreadPrimitive.Root,
1237
1250
  {
1238
- className: "aui-root aui-thread-root @container flex h-full flex-col bg-background",
1239
- style: {
1240
- ["--thread-max-width"]: "44rem"
1241
- },
1251
+ className: cn(
1252
+ "aui-root aui-thread-root @container flex h-full flex-col bg-background",
1253
+ className
1254
+ ),
1255
+ style: { ["--thread-max-width"]: maxWidth },
1242
1256
  children: /* @__PURE__ */ jsxs7(
1243
1257
  ThreadPrimitive.Viewport,
1244
1258
  {
1245
1259
  turnAnchor: "bottom",
1246
1260
  className: "aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4 pt-4",
1247
1261
  children: [
1248
- /* @__PURE__ */ jsx12(AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ jsx12(ThreadWelcome, {}) }),
1262
+ /* @__PURE__ */ jsx12(WelcomeSlot, { config: welcome, suggestions }),
1249
1263
  /* @__PURE__ */ jsx12(
1250
1264
  ThreadPrimitive.Messages,
1251
1265
  {
1252
1266
  components: {
1253
- UserMessage,
1254
- EditComposer,
1255
- AssistantMessage
1267
+ UserMessage: UserMessageSlot,
1268
+ EditComposer: EditComposerSlot,
1269
+ AssistantMessage: AssistantMessageSlot
1256
1270
  }
1257
1271
  }
1258
1272
  ),
1259
1273
  /* @__PURE__ */ jsxs7(ThreadPrimitive.ViewportFooter, { className: "aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6", children: [
1260
- /* @__PURE__ */ jsx12(ThreadScrollToBottom, {}),
1261
- /* @__PURE__ */ jsx12(Composer, {})
1274
+ /* @__PURE__ */ jsx12(ScrollToBottomSlot, {}),
1275
+ /* @__PURE__ */ jsx12(ComposerSlot, { placeholder: composerPlaceholder })
1262
1276
  ] })
1263
1277
  ]
1264
1278
  }
@@ -1277,8 +1291,8 @@ var ThreadScrollToBottom = () => {
1277
1291
  }
1278
1292
  ) });
1279
1293
  };
1280
- var ThreadWelcome = () => {
1281
- return /* @__PURE__ */ jsxs7("div", { className: "aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col", children: [
1294
+ var ThreadWelcome = ({ config, suggestions }) => {
1295
+ return /* @__PURE__ */ jsx12(AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ jsxs7("div", { className: "aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col", children: [
1282
1296
  /* @__PURE__ */ jsx12("div", { className: "aui-thread-welcome-center flex w-full grow flex-col items-center justify-center", children: /* @__PURE__ */ jsxs7("div", { className: "aui-thread-welcome-message flex size-full flex-col items-center justify-center px-4 text-center", children: [
1283
1297
  /* @__PURE__ */ jsxs7("div", { className: "fade-in animate-in fill-mode-both relative mb-6 flex size-14 items-center justify-center duration-300", children: [
1284
1298
  /* @__PURE__ */ jsx12("div", { className: "animate-ai-ring-glow absolute inset-0 rounded-2xl bg-gradient-to-br from-primary/15 to-primary/5 ring-1 ring-primary/15" }),
@@ -1298,42 +1312,40 @@ var ThreadWelcome = () => {
1298
1312
  }
1299
1313
  )
1300
1314
  ] }),
1301
- /* @__PURE__ */ jsx12("h1", { className: "aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both font-semibold text-2xl duration-200", children: "How can I help you today?" }),
1302
- /* @__PURE__ */ jsx12("p", { className: "aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground mt-2 delay-75 duration-200", children: "Send a message to start a conversation." })
1315
+ /* @__PURE__ */ jsx12("h1", { className: "aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both font-semibold text-2xl duration-200", children: config?.heading ?? "How can I help you today?" }),
1316
+ /* @__PURE__ */ jsx12("p", { className: "aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground mt-2 delay-75 duration-200", children: config?.subheading ?? "Send a message to start a conversation." })
1303
1317
  ] }) }),
1304
- /* @__PURE__ */ jsx12(ThreadSuggestions, {})
1305
- ] });
1318
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx12(ThreadSuggestions, { suggestions })
1319
+ ] }) });
1306
1320
  };
1307
- var ThreadSuggestions = () => {
1308
- return /* @__PURE__ */ jsx12("div", { className: "aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4", children: /* @__PURE__ */ jsx12(
1309
- ThreadPrimitive.Suggestions,
1310
- {
1311
- components: {
1312
- Suggestion: ThreadSuggestionItem
1313
- }
1314
- }
1315
- ) });
1321
+ var ThreadSuggestions = ({ suggestions }) => {
1322
+ return /* @__PURE__ */ jsx12("div", { className: "aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4", children: suggestions.map((s, i) => /* @__PURE__ */ jsx12(ThreadSuggestionItem, { title: s.title, description: s.description }, i)) });
1316
1323
  };
1317
- var ThreadSuggestionItem = () => {
1318
- return /* @__PURE__ */ jsx12("div", { className: "aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-200", children: /* @__PURE__ */ jsx12(SuggestionPrimitive.Trigger, { send: true, asChild: true, children: /* @__PURE__ */ jsxs7(
1324
+ var ThreadSuggestionItem = ({ title, description }) => {
1325
+ const runtime = useThreadRuntime();
1326
+ return /* @__PURE__ */ jsx12("div", { className: "aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 animate-in fill-mode-both duration-200", children: /* @__PURE__ */ jsxs7(
1319
1327
  Button,
1320
1328
  {
1321
1329
  variant: "ghost",
1322
1330
  className: "aui-thread-welcome-suggestion h-auto w-full @md:flex-col flex-wrap items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-left text-sm transition-colors hover:bg-muted",
1331
+ onClick: () => runtime.append({
1332
+ role: "user",
1333
+ content: [{ type: "text", text: title }]
1334
+ }),
1323
1335
  children: [
1324
- /* @__PURE__ */ jsx12("span", { className: "aui-thread-welcome-suggestion-text-1 font-medium", children: /* @__PURE__ */ jsx12(SuggestionPrimitive.Title, {}) }),
1325
- /* @__PURE__ */ jsx12("span", { className: "aui-thread-welcome-suggestion-text-2 text-muted-foreground", children: /* @__PURE__ */ jsx12(SuggestionPrimitive.Description, {}) })
1336
+ /* @__PURE__ */ jsx12("span", { className: "aui-thread-welcome-suggestion-text-1 font-medium", children: title }),
1337
+ description && /* @__PURE__ */ jsx12("span", { className: "aui-thread-welcome-suggestion-text-2 text-muted-foreground", children: description })
1326
1338
  ]
1327
1339
  }
1328
- ) }) });
1340
+ ) });
1329
1341
  };
1330
- var Composer = () => {
1342
+ var Composer = ({ placeholder }) => {
1331
1343
  return /* @__PURE__ */ jsx12(ComposerPrimitive2.Root, { className: "aui-composer-root relative mt-3 flex w-full flex-col", children: /* @__PURE__ */ jsxs7(ComposerPrimitive2.AttachmentDropzone, { className: "aui-composer-attachment-dropzone flex w-full flex-col rounded-2xl border border-input bg-background px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50", children: [
1332
1344
  /* @__PURE__ */ jsx12(ComposerAttachments, {}),
1333
1345
  /* @__PURE__ */ jsx12(
1334
1346
  ComposerPrimitive2.Input,
1335
1347
  {
1336
- placeholder: "Send a message...",
1348
+ placeholder: placeholder ?? "Send a message...",
1337
1349
  className: "aui-composer-input mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0",
1338
1350
  rows: 1,
1339
1351
  autoFocus: true,
@@ -1481,6 +1493,29 @@ var EditComposer = () => {
1481
1493
  ] }) });
1482
1494
  };
1483
1495
 
1496
+ // src/components/chat.tsx
1497
+ import { jsx as jsx13 } from "react/jsx-runtime";
1498
+ function TimbalChat({
1499
+ workforceId,
1500
+ baseUrl,
1501
+ fetch: fetch2,
1502
+ ...threadProps
1503
+ }) {
1504
+ return /* @__PURE__ */ jsx13(TimbalRuntimeProvider, { workforceId, baseUrl, fetch: fetch2, children: /* @__PURE__ */ jsx13(Thread, { ...threadProps }) });
1505
+ }
1506
+
1507
+ // src/index.ts
1508
+ import {
1509
+ ThreadPrimitive as ThreadPrimitive2,
1510
+ MessagePrimitive as MessagePrimitive3,
1511
+ ComposerPrimitive as ComposerPrimitive3,
1512
+ ActionBarPrimitive as ActionBarPrimitive2,
1513
+ useThread,
1514
+ useThreadRuntime as useThreadRuntime2,
1515
+ useMessageRuntime,
1516
+ useComposerRuntime
1517
+ } from "@assistant-ui/react";
1518
+
1484
1519
  // src/auth/provider.tsx
1485
1520
  import {
1486
1521
  createContext,
@@ -1489,7 +1524,7 @@ import {
1489
1524
  useEffect as useEffect4,
1490
1525
  useState as useState5
1491
1526
  } from "react";
1492
- import { jsx as jsx13 } from "react/jsx-runtime";
1527
+ import { jsx as jsx14 } from "react/jsx-runtime";
1493
1528
  var SessionContext = createContext(void 0);
1494
1529
  var useSession = () => {
1495
1530
  const context = useContext(SessionContext);
@@ -1553,7 +1588,7 @@ var SessionProvider = ({
1553
1588
  () => window.location.href = `/api/auth/login?return_to=${returnTo}`
1554
1589
  );
1555
1590
  }, []);
1556
- return /* @__PURE__ */ jsx13(
1591
+ return /* @__PURE__ */ jsx14(
1557
1592
  SessionContext.Provider,
1558
1593
  {
1559
1594
  value: {
@@ -1569,7 +1604,7 @@ var SessionProvider = ({
1569
1604
 
1570
1605
  // src/auth/guard.tsx
1571
1606
  import { Loader2 } from "lucide-react";
1572
- import { jsx as jsx14 } from "react/jsx-runtime";
1607
+ import { jsx as jsx15 } from "react/jsx-runtime";
1573
1608
  var AuthGuard = ({
1574
1609
  children,
1575
1610
  requireAuth = false,
@@ -1580,7 +1615,7 @@ var AuthGuard = ({
1580
1615
  return children;
1581
1616
  }
1582
1617
  if (loading) {
1583
- return /* @__PURE__ */ jsx14("div", { className: "flex items-center justify-center h-screen", children: /* @__PURE__ */ jsx14(Loader2, { className: "w-8 h-8 animate-spin" }) });
1618
+ return /* @__PURE__ */ jsx15("div", { className: "flex items-center justify-center h-screen", children: /* @__PURE__ */ jsx15(Loader2, { className: "w-8 h-8 animate-spin" }) });
1584
1619
  }
1585
1620
  if (requireAuth && !isAuthenticated) {
1586
1621
  const returnTo = encodeURIComponent(
@@ -1592,6 +1627,7 @@ var AuthGuard = ({
1592
1627
  return children;
1593
1628
  };
1594
1629
  export {
1630
+ ActionBarPrimitive2 as ActionBarPrimitive,
1595
1631
  AuthGuard,
1596
1632
  Avatar,
1597
1633
  AvatarFallback,
@@ -1599,6 +1635,7 @@ export {
1599
1635
  Button,
1600
1636
  ComposerAddAttachment,
1601
1637
  ComposerAttachments,
1638
+ ComposerPrimitive3 as ComposerPrimitive,
1602
1639
  Dialog,
1603
1640
  DialogClose,
1604
1641
  DialogContent,
@@ -1607,10 +1644,13 @@ export {
1607
1644
  DialogTitle,
1608
1645
  DialogTrigger,
1609
1646
  MarkdownText,
1647
+ MessagePrimitive3 as MessagePrimitive,
1610
1648
  SessionProvider,
1611
1649
  Shimmer,
1612
1650
  syntax_highlighter_default as SyntaxHighlighter,
1613
1651
  Thread,
1652
+ ThreadPrimitive2 as ThreadPrimitive,
1653
+ TimbalChat,
1614
1654
  TimbalRuntimeProvider,
1615
1655
  ToolFallback,
1616
1656
  Tooltip,
@@ -1627,5 +1667,9 @@ export {
1627
1667
  getAccessToken,
1628
1668
  getRefreshToken,
1629
1669
  refreshAccessToken,
1630
- useSession
1670
+ useComposerRuntime,
1671
+ useMessageRuntime,
1672
+ useSession,
1673
+ useThread,
1674
+ useThreadRuntime2 as useThreadRuntime
1631
1675
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timbal-ai/timbal-react",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "React components and runtime for building Timbal chat UIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -22,6 +22,8 @@
22
22
  "build": "tsup",
23
23
  "build:watch": "tsup --watch",
24
24
  "clean": "rm -rf dist",
25
+ "test": "bun test",
26
+ "test:watch": "bun test --watch",
25
27
  "typecheck": "tsc --noEmit",
26
28
  "prepublishOnly": "bun run build && bun run typecheck"
27
29
  },
@@ -52,9 +54,13 @@
52
54
  },
53
55
  "devDependencies": {
54
56
  "@assistant-ui/react": "^0.12.10",
57
+ "@testing-library/jest-dom": "^6.9.1",
58
+ "@testing-library/react": "^16.3.2",
59
+ "@testing-library/user-event": "^14.6.1",
55
60
  "@timbal-ai/timbal-sdk": "0.4.9",
56
61
  "@types/react": "^19.2.4",
57
62
  "@types/react-dom": "^19.2.3",
63
+ "happy-dom": "^20.8.9",
58
64
  "react": "^19.2.0",
59
65
  "react-dom": "^19.2.0",
60
66
  "tsup": "^8.5.0",