@timbal-ai/timbal-react 0.1.0 → 0.2.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
@@ -16,9 +16,9 @@ bun add @timbal-ai/timbal-react
16
16
  npm install react react-dom @assistant-ui/react @timbal-ai/timbal-sdk
17
17
  ```
18
18
 
19
- ## Tailwind setup
19
+ ### Required: Tailwind setup
20
20
 
21
- The package ships pre-built class names that Tailwind must scan. Add this line to your CSS entry file:
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**:
22
22
 
23
23
  ```css
24
24
  /* src/index.css */
@@ -29,9 +29,9 @@ The package ships pre-built class names that Tailwind must scan. Add this line t
29
29
 
30
30
  > Adjust the path if your CSS file lives at a different depth relative to `node_modules`.
31
31
 
32
- ## CSS imports
32
+ ### Required: CSS imports
33
33
 
34
- Some components require stylesheets from their dependencies. Import these once in your app entry:
34
+ Import these stylesheets once in your app entry:
35
35
 
36
36
  ```ts
37
37
  // src/main.tsx
@@ -43,9 +43,70 @@ import "katex/dist/katex.min.css";
43
43
 
44
44
  ## Usage
45
45
 
46
- ### Drop-in chat
46
+ ### One-liner
47
47
 
48
- Wrap `TimbalRuntimeProvider` with a `workforceId` and render `<Thread />` inside it:
48
+ The simplest way to embed a chat UI. `TimbalChat` handles the runtime and the thread in one component:
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
+ #### With welcome screen and suggestions
63
+
64
+ ```tsx
65
+ <TimbalChat
66
+ workforceId="your-workforce-id"
67
+ welcome={{
68
+ heading: "Hi, I'm your assistant",
69
+ subheading: "Ask me anything about your data.",
70
+ }}
71
+ suggestions={[
72
+ { title: "Summarize this week", description: "Get a quick overview of recent activity" },
73
+ { title: "What can you help with?" },
74
+ { title: "Show me the latest report" },
75
+ ]}
76
+ />
77
+ ```
78
+
79
+ #### With a custom placeholder and width
80
+
81
+ ```tsx
82
+ <TimbalChat
83
+ workforceId="your-workforce-id"
84
+ composerPlaceholder="Type a question..."
85
+ maxWidth="60rem"
86
+ className="my-custom-class"
87
+ />
88
+ ```
89
+
90
+ #### Switching agents dynamically
91
+
92
+ Use `key` to reset the chat when the workforce changes:
93
+
94
+ ```tsx
95
+ const [workforceId, setWorkforceId] = useState("agent-a");
96
+
97
+ <select onChange={(e) => setWorkforceId(e.target.value)}>
98
+ <option value="agent-a">Agent A</option>
99
+ <option value="agent-b">Agent B</option>
100
+ </select>
101
+
102
+ <TimbalChat workforceId={workforceId} key={workforceId} />
103
+ ```
104
+
105
+ ---
106
+
107
+ ### Compose manually
108
+
109
+ Use `TimbalRuntimeProvider` + `Thread` separately when you need to place the runtime above the chat UI — for example, to build a custom header that reads or controls chat state:
49
110
 
50
111
  ```tsx
51
112
  import { TimbalRuntimeProvider, Thread } from "@timbal-ai/timbal-react";
@@ -53,14 +114,63 @@ import { TimbalRuntimeProvider, Thread } from "@timbal-ai/timbal-react";
53
114
  export default function App() {
54
115
  return (
55
116
  <TimbalRuntimeProvider workforceId="your-workforce-id">
56
- <div style={{ height: "100vh" }}>
57
- <Thread />
117
+ <div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
118
+ <header>My App</header>
119
+ <Thread
120
+ composerPlaceholder="Ask anything..."
121
+ className="flex-1 min-h-0"
122
+ />
58
123
  </div>
59
124
  </TimbalRuntimeProvider>
60
125
  );
61
126
  }
62
127
  ```
63
128
 
129
+ #### With a custom API base URL
130
+
131
+ Useful when your API is mounted at a subpath (e.g. behind a reverse proxy):
132
+
133
+ ```tsx
134
+ <TimbalRuntimeProvider workforceId="your-workforce-id" baseUrl="/api">
135
+ <Thread />
136
+ </TimbalRuntimeProvider>
137
+ ```
138
+
139
+ #### With a custom fetch function
140
+
141
+ Pass your own `fetch` to add headers, inject tokens, or proxy requests:
142
+
143
+ ```tsx
144
+ import { TimbalRuntimeProvider, Thread } from "@timbal-ai/timbal-react";
145
+
146
+ const myFetch: typeof fetch = (url, options) => {
147
+ return fetch(url, {
148
+ ...options,
149
+ headers: {
150
+ ...options?.headers,
151
+ "X-My-Header": "value",
152
+ },
153
+ });
154
+ };
155
+
156
+ <TimbalRuntimeProvider workforceId="your-workforce-id" fetch={myFetch}>
157
+ <Thread />
158
+ </TimbalRuntimeProvider>
159
+ ```
160
+
161
+ ---
162
+
163
+ ### `TimbalChat` / `Thread` props
164
+
165
+ | Prop | Type | Default | Description |
166
+ |---|---|---|---|
167
+ | `welcome.heading` | `string` | `"How can I help you today?"` | Welcome screen heading |
168
+ | `welcome.subheading` | `string` | `"Send a message to start a conversation."` | Welcome screen subheading |
169
+ | `suggestions` | `{ title: string; description?: string }[]` | — | Suggestion chips on the welcome screen |
170
+ | `composerPlaceholder` | `string` | `"Send a message..."` | Composer input placeholder |
171
+ | `maxWidth` | `string` | `"44rem"` | Max width of the message column |
172
+ | `className` | `string` | — | Extra classes on the root element |
173
+
64
174
  ### `TimbalRuntimeProvider` props
65
175
 
66
176
  | Prop | Type | Default | Description |
@@ -68,8 +178,6 @@ export default function App() {
68
178
  | `workforceId` | `string` | — | ID of the workforce to stream from |
69
179
  | `baseUrl` | `string` | `"/api"` | Base URL for API calls. Posts to `{baseUrl}/workforce/{workforceId}/stream` |
70
180
  | `fetch` | `(url, options?) => Promise<Response>` | `authFetch` | Custom fetch function. Defaults to the built-in auth-aware fetch (Bearer token + auto-refresh) |
71
- | `devFakeStream` | `boolean` | `false` | Enable fake streaming for local dev/testing without a backend |
72
- | `devFakeStreamDelayMs` | `number` | `75` | Token delay in ms for fake streaming |
73
181
 
74
182
  ---
75
183
 
@@ -82,23 +190,33 @@ The package includes a session/auth system backed by localStorage tokens. The AP
82
190
  Wrap your app with `SessionProvider` and protect routes with `AuthGuard`:
83
191
 
84
192
  ```tsx
193
+ // src/App.tsx
85
194
  import { SessionProvider, AuthGuard, TooltipProvider } from "@timbal-ai/timbal-react";
195
+ import { BrowserRouter, Routes, Route } from "react-router-dom";
196
+ import Home from "./pages/Home";
86
197
 
198
+ // Auth is opt-in — only active when VITE_TIMBAL_PROJECT_ID is set
87
199
  const isAuthEnabled = !!import.meta.env.VITE_TIMBAL_PROJECT_ID;
88
200
 
89
- function App() {
201
+ export default function App() {
90
202
  return (
91
203
  <SessionProvider enabled={isAuthEnabled}>
92
204
  <TooltipProvider>
93
- <AuthGuard requireAuth enabled={isAuthEnabled}>
94
- <YourApp />
95
- </AuthGuard>
205
+ <BrowserRouter>
206
+ <AuthGuard requireAuth enabled={isAuthEnabled}>
207
+ <Routes>
208
+ <Route path="/" element={<Home />} />
209
+ </Routes>
210
+ </AuthGuard>
211
+ </BrowserRouter>
96
212
  </TooltipProvider>
97
213
  </SessionProvider>
98
214
  );
99
215
  }
100
216
  ```
101
217
 
218
+ When `enabled` is `false` (no project ID configured), both `SessionProvider` and `AuthGuard` are transparent — no redirects, no API calls.
219
+
102
220
  ### `SessionProvider` props
103
221
 
104
222
  | Prop | Type | Default | Description |
@@ -114,25 +232,47 @@ function App() {
114
232
 
115
233
  ### `useSession` hook
116
234
 
235
+ Access the current session anywhere inside `SessionProvider`:
236
+
117
237
  ```tsx
118
238
  import { useSession } from "@timbal-ai/timbal-react";
119
239
 
120
240
  function Header() {
121
241
  const { user, isAuthenticated, loading, logout } = useSession();
122
- // ...
242
+
243
+ if (loading) return null;
244
+
245
+ return (
246
+ <header>
247
+ {isAuthenticated ? (
248
+ <>
249
+ <span>{user?.email}</span>
250
+ <button onClick={logout}>Log out</button>
251
+ </>
252
+ ) : (
253
+ <a href="/login">Log in</a>
254
+ )}
255
+ </header>
256
+ );
123
257
  }
124
258
  ```
125
259
 
126
260
  ### `authFetch`
127
261
 
128
- A drop-in replacement for `fetch` that attaches the Bearer token and auto-refreshes on 401:
262
+ A drop-in replacement for `fetch` that attaches the Bearer token from localStorage and auto-refreshes on 401:
129
263
 
130
264
  ```tsx
131
265
  import { authFetch } from "@timbal-ai/timbal-react";
132
266
 
267
+ // Fetch a list of workforce agents
133
268
  const res = await authFetch("/api/workforce");
269
+ if (res.ok) {
270
+ const agents = await res.json();
271
+ }
134
272
  ```
135
273
 
274
+ It's also the default `fetch` used by `TimbalRuntimeProvider` — you only need to import it directly for your own API calls (e.g. loading workforce lists, metadata, etc.).
275
+
136
276
  ---
137
277
 
138
278
  ## Components
@@ -158,9 +298,74 @@ Re-exported Radix UI wrappers pre-styled to match the Timbal design system:
158
298
 
159
299
  ---
160
300
 
301
+ ## Full example
302
+
303
+ A complete page with agent switching, auth, and a custom header:
304
+
305
+ ```tsx
306
+ // src/pages/Home.tsx
307
+ import { useEffect, useState } from "react";
308
+ import type { WorkforceItem } from "@timbal-ai/timbal-sdk";
309
+ import {
310
+ TimbalChat,
311
+ Button,
312
+ authFetch,
313
+ useSession,
314
+ } from "@timbal-ai/timbal-react";
315
+ import { LogOut } from "lucide-react";
316
+
317
+ const isAuthEnabled = !!import.meta.env.VITE_TIMBAL_PROJECT_ID;
318
+
319
+ export default function Home() {
320
+ const { logout } = useSession();
321
+ const [workforces, setWorkforces] = useState<WorkforceItem[]>([]);
322
+ const [selectedId, setSelectedId] = useState("");
323
+
324
+ useEffect(() => {
325
+ authFetch("/api/workforce")
326
+ .then((r) => r.json())
327
+ .then((data: WorkforceItem[]) => {
328
+ setWorkforces(data);
329
+ const agent = data.find((w) => w.type === "agent") ?? data[0];
330
+ if (agent) setSelectedId(agent.id ?? agent.name ?? "");
331
+ })
332
+ .catch(() => {});
333
+ }, []);
334
+
335
+ return (
336
+ <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
337
+ <header style={{ display: "flex", justifyContent: "space-between", padding: "0.5rem 1.25rem" }}>
338
+ <select value={selectedId} onChange={(e) => setSelectedId(e.target.value)}>
339
+ {workforces.map((w) => (
340
+ <option key={w.id ?? w.name} value={w.id ?? w.name ?? ""}>
341
+ {w.name}
342
+ </option>
343
+ ))}
344
+ </select>
345
+
346
+ {isAuthEnabled && (
347
+ <Button variant="ghost" size="icon" onClick={logout}>
348
+ <LogOut />
349
+ </Button>
350
+ )}
351
+ </header>
352
+
353
+ <TimbalChat
354
+ workforceId={selectedId}
355
+ key={selectedId}
356
+ className="flex-1 min-h-0"
357
+ welcome={{ heading: "How can I help you today?" }}
358
+ />
359
+ </div>
360
+ );
361
+ }
362
+ ```
363
+
364
+ ---
365
+
161
366
  ## Local development
162
367
 
163
- The package isn't published yet. Install it via a local path reference:
368
+ Install via a local path reference:
164
369
 
165
370
  ```json
166
371
  // package.json
package/dist/index.cjs CHANGED
@@ -49,6 +49,7 @@ __export(index_exports, {
49
49
  Shimmer: () => Shimmer,
50
50
  SyntaxHighlighter: () => syntax_highlighter_default,
51
51
  Thread: () => Thread,
52
+ TimbalChat: () => TimbalChat,
52
53
  TimbalRuntimeProvider: () => TimbalRuntimeProvider,
53
54
  ToolFallback: () => ToolFallback,
54
55
  Tooltip: () => Tooltip,
@@ -155,7 +156,6 @@ var fetchCurrentUser = async () => {
155
156
 
156
157
  // src/runtime/provider.tsx
157
158
  var import_jsx_runtime = require("react/jsx-runtime");
158
- var parseLine = import_timbal_sdk.parseSSELine;
159
159
  var convertMessage = (message) => ({
160
160
  role: message.role,
161
161
  content: message.content,
@@ -175,65 +175,11 @@ function getTextFromMessage(message) {
175
175
  const part = message.content.find((c) => c.type === "text");
176
176
  return part?.type === "text" ? part.text : null;
177
177
  }
178
- function waitWithAbort(ms, signal) {
179
- if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
180
- return new Promise((resolve, reject) => {
181
- const timeoutId = setTimeout(() => {
182
- signal.removeEventListener("abort", onAbort);
183
- resolve();
184
- }, ms);
185
- const onAbort = () => {
186
- clearTimeout(timeoutId);
187
- reject(new DOMException("The operation was aborted.", "AbortError"));
188
- };
189
- signal.addEventListener("abort", onAbort, { once: true });
190
- });
191
- }
192
- function buildFakeLongResponse(input) {
193
- const safeInput = input.trim() || "your request";
194
- const base = [
195
- `Fake streaming fallback enabled. You asked: "${safeInput}".`,
196
- "",
197
- "This is a deliberately long response used to test rendering, scrolling, cancellation, and streaming UX behavior.",
198
- "",
199
- "What this stream is exercising:",
200
- "- Frequent tiny token updates",
201
- "- Long markdown paragraphs",
202
- "- Bullet list rendering",
203
- "- UI action bar behavior while running",
204
- "- Stop button and abort flow",
205
- "",
206
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse vitae mi at augue pulvinar porta. Praesent ullamcorper felis at nibh tincidunt, id sagittis mauris interdum. Integer nec semper dui. Curabitur sed fermentum libero. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.",
207
- "",
208
- "Aliquam luctus purus non bibendum faucibus. Donec at elit eget massa feugiat ultricies. Quisque condimentum, libero in egestas varius, purus justo aliquam sem, vitae feugiat nunc lorem a justo. Sed non tempor est. In hac habitasse platea dictumst.",
209
- "",
210
- "If you can read this arriving progressively, the fallback is working as intended."
211
- ].join("\n");
212
- return `${base}
213
-
214
- ---
215
-
216
- ${base}`;
217
- }
218
- async function streamFakeLongResponse(input, delayMs, signal, onDelta) {
219
- const fullResponse = buildFakeLongResponse(input);
220
- let cursor = 0;
221
- while (cursor < fullResponse.length) {
222
- if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
223
- const chunkSize = Math.min(fullResponse.length - cursor, Math.floor(Math.random() * 12) + 2);
224
- const delta = fullResponse.slice(cursor, cursor + chunkSize);
225
- cursor += chunkSize;
226
- onDelta(delta);
227
- await waitWithAbort(delayMs, signal);
228
- }
229
- }
230
178
  function TimbalRuntimeProvider({
231
179
  workforceId,
232
180
  children,
233
181
  baseUrl = "/api",
234
- fetch: fetchFn,
235
- devFakeStream = false,
236
- devFakeStreamDelayMs = 75
182
+ fetch: fetchFn
237
183
  }) {
238
184
  const [messages, setMessages] = (0, import_react.useState)([]);
239
185
  const [isRunning, setIsRunning] = (0, import_react.useState)(false);
@@ -268,20 +214,6 @@ function TimbalRuntimeProvider({
268
214
  );
269
215
  };
270
216
  try {
271
- if (devFakeStream) {
272
- const fakeId = `call_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
273
- parts.push({ type: "tool-call", toolCallId: fakeId, toolName: "get_datetime", argsText: "{}" });
274
- flush();
275
- await waitWithAbort(2e3, signal);
276
- parts[0].result = `Current datetime (from tool): ${(/* @__PURE__ */ new Date()).toISOString()}`;
277
- flush();
278
- await waitWithAbort(300, signal);
279
- await streamFakeLongResponse(input, devFakeStreamDelayMs, signal, (delta) => {
280
- lastTextPart().text += delta;
281
- flush();
282
- });
283
- return;
284
- }
285
217
  const res = await fetchFnRef.current(`${baseUrl}/workforce/${workforceId}/stream`, {
286
218
  method: "POST",
287
219
  headers: { "Content-Type": "application/json" },
@@ -303,7 +235,7 @@ function TimbalRuntimeProvider({
303
235
  const lines = buffer.split("\n");
304
236
  buffer = lines.pop() ?? "";
305
237
  for (const line of lines) {
306
- const event = parseLine(line);
238
+ const event = (0, import_timbal_sdk.parseSSELine)(line);
307
239
  if (!event) continue;
308
240
  if (!capturedRunId && isTopLevelStart(event)) {
309
241
  capturedRunId = event.run_id;
@@ -373,7 +305,7 @@ function TimbalRuntimeProvider({
373
305
  }
374
306
  }
375
307
  if (buffer.trim()) {
376
- const event = parseLine(buffer);
308
+ const event = (0, import_timbal_sdk.parseSSELine)(buffer);
377
309
  if (event?.type === "OUTPUT" && parts.length === 0 && event.output) {
378
310
  const text = typeof event.output === "string" ? event.output : JSON.stringify(event.output);
379
311
  parts.push({ type: "text", text });
@@ -390,7 +322,7 @@ function TimbalRuntimeProvider({
390
322
  abortRef.current = null;
391
323
  }
392
324
  },
393
- [workforceId, baseUrl, devFakeStream, devFakeStreamDelayMs]
325
+ [workforceId, baseUrl]
394
326
  );
395
327
  const onNew = (0, import_react.useCallback)(
396
328
  async (message) => {
@@ -1336,21 +1268,28 @@ ToolFallback.displayName = "ToolFallback";
1336
1268
  var import_react11 = require("@assistant-ui/react");
1337
1269
  var import_lucide_react5 = require("lucide-react");
1338
1270
  var import_jsx_runtime12 = require("react/jsx-runtime");
1339
- var Thread = () => {
1271
+ var Thread = ({
1272
+ className,
1273
+ maxWidth = "44rem",
1274
+ welcome,
1275
+ suggestions,
1276
+ composerPlaceholder = "Send a message..."
1277
+ }) => {
1340
1278
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1341
1279
  import_react11.ThreadPrimitive.Root,
1342
1280
  {
1343
- className: "aui-root aui-thread-root @container flex h-full flex-col bg-background",
1344
- style: {
1345
- ["--thread-max-width"]: "44rem"
1346
- },
1281
+ className: cn(
1282
+ "aui-root aui-thread-root @container flex h-full flex-col bg-background",
1283
+ className
1284
+ ),
1285
+ style: { ["--thread-max-width"]: maxWidth },
1347
1286
  children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1348
1287
  import_react11.ThreadPrimitive.Viewport,
1349
1288
  {
1350
1289
  turnAnchor: "bottom",
1351
1290
  className: "aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4 pt-4",
1352
1291
  children: [
1353
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react11.AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadWelcome, {}) }),
1292
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react11.AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadWelcome, { config: welcome, suggestions }) }),
1354
1293
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1355
1294
  import_react11.ThreadPrimitive.Messages,
1356
1295
  {
@@ -1363,7 +1302,7 @@ var Thread = () => {
1363
1302
  ),
1364
1303
  /* @__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: [
1365
1304
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadScrollToBottom, {}),
1366
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Composer, {})
1305
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Composer, { placeholder: composerPlaceholder })
1367
1306
  ] })
1368
1307
  ]
1369
1308
  }
@@ -1382,7 +1321,7 @@ var ThreadScrollToBottom = () => {
1382
1321
  }
1383
1322
  ) });
1384
1323
  };
1385
- var ThreadWelcome = () => {
1324
+ var ThreadWelcome = ({ config, suggestions }) => {
1386
1325
  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: [
1387
1326
  /* @__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: [
1388
1327
  /* @__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: [
@@ -1403,42 +1342,40 @@ var ThreadWelcome = () => {
1403
1342
  }
1404
1343
  )
1405
1344
  ] }),
1406
- /* @__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?" }),
1407
- /* @__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." })
1345
+ /* @__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?" }),
1346
+ /* @__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." })
1408
1347
  ] }) }),
1409
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadSuggestions, {})
1348
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadSuggestions, { suggestions })
1410
1349
  ] });
1411
1350
  };
1412
- var ThreadSuggestions = () => {
1413
- 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)(
1414
- import_react11.ThreadPrimitive.Suggestions,
1415
- {
1416
- components: {
1417
- Suggestion: ThreadSuggestionItem
1418
- }
1419
- }
1420
- ) });
1351
+ var ThreadSuggestions = ({ suggestions }) => {
1352
+ 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)) });
1421
1353
  };
1422
- var ThreadSuggestionItem = () => {
1423
- 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)(
1354
+ var ThreadSuggestionItem = ({ title, description }) => {
1355
+ const runtime = (0, import_react11.useThreadRuntime)();
1356
+ 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)(
1424
1357
  Button,
1425
1358
  {
1426
1359
  variant: "ghost",
1427
1360
  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",
1361
+ onClick: () => runtime.append({
1362
+ role: "user",
1363
+ content: [{ type: "text", text: title }]
1364
+ }),
1428
1365
  children: [
1429
- /* @__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, {}) }),
1430
- /* @__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, {}) })
1366
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "aui-thread-welcome-suggestion-text-1 font-medium", children: title }),
1367
+ description && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "aui-thread-welcome-suggestion-text-2 text-muted-foreground", children: description })
1431
1368
  ]
1432
1369
  }
1433
- ) }) });
1370
+ ) });
1434
1371
  };
1435
- var Composer = () => {
1372
+ var Composer = ({ placeholder }) => {
1436
1373
  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: [
1437
1374
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ComposerAttachments, {}),
1438
1375
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1439
1376
  import_react11.ComposerPrimitive.Input,
1440
1377
  {
1441
- placeholder: "Send a message...",
1378
+ placeholder: placeholder ?? "Send a message...",
1442
1379
  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",
1443
1380
  rows: 1,
1444
1381
  autoFocus: true,
@@ -1586,9 +1523,20 @@ var EditComposer = () => {
1586
1523
  ] }) });
1587
1524
  };
1588
1525
 
1526
+ // src/components/chat.tsx
1527
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1528
+ function TimbalChat({
1529
+ workforceId,
1530
+ baseUrl,
1531
+ fetch: fetch2,
1532
+ ...threadProps
1533
+ }) {
1534
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TimbalRuntimeProvider, { workforceId, baseUrl, fetch: fetch2, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Thread, { ...threadProps }) });
1535
+ }
1536
+
1589
1537
  // src/auth/provider.tsx
1590
1538
  var import_react12 = require("react");
1591
- var import_jsx_runtime13 = require("react/jsx-runtime");
1539
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1592
1540
  var SessionContext = (0, import_react12.createContext)(void 0);
1593
1541
  var useSession = () => {
1594
1542
  const context = (0, import_react12.useContext)(SessionContext);
@@ -1652,7 +1600,7 @@ var SessionProvider = ({
1652
1600
  () => window.location.href = `/api/auth/login?return_to=${returnTo}`
1653
1601
  );
1654
1602
  }, []);
1655
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1603
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1656
1604
  SessionContext.Provider,
1657
1605
  {
1658
1606
  value: {
@@ -1668,7 +1616,7 @@ var SessionProvider = ({
1668
1616
 
1669
1617
  // src/auth/guard.tsx
1670
1618
  var import_lucide_react6 = require("lucide-react");
1671
- var import_jsx_runtime14 = require("react/jsx-runtime");
1619
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1672
1620
  var AuthGuard = ({
1673
1621
  children,
1674
1622
  requireAuth = false,
@@ -1679,7 +1627,7 @@ var AuthGuard = ({
1679
1627
  return children;
1680
1628
  }
1681
1629
  if (loading) {
1682
- 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" }) });
1630
+ 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" }) });
1683
1631
  }
1684
1632
  if (requireAuth && !isAuthenticated) {
1685
1633
  const returnTo = encodeURIComponent(
@@ -1711,6 +1659,7 @@ var AuthGuard = ({
1711
1659
  Shimmer,
1712
1660
  SyntaxHighlighter,
1713
1661
  Thread,
1662
+ TimbalChat,
1714
1663
  TimbalRuntimeProvider,
1715
1664
  ToolFallback,
1716
1665
  Tooltip,
package/dist/index.d.cts CHANGED
@@ -23,14 +23,33 @@ interface TimbalRuntimeProviderProps {
23
23
  * attaches Bearer tokens from localStorage and auto-refreshes on 401.
24
24
  */
25
25
  fetch?: FetchFn;
26
- /** Enable fake streaming for development/testing. Default: false */
27
- devFakeStream?: boolean;
28
- /** Token delay in ms for fake streaming. Default: 75 */
29
- devFakeStreamDelayMs?: number;
30
26
  }
31
- declare function TimbalRuntimeProvider({ workforceId, children, baseUrl, fetch: fetchFn, devFakeStream, devFakeStreamDelayMs, }: TimbalRuntimeProviderProps): react_jsx_runtime.JSX.Element;
27
+ declare function TimbalRuntimeProvider({ workforceId, children, baseUrl, fetch: fetchFn, }: TimbalRuntimeProviderProps): react_jsx_runtime.JSX.Element;
32
28
 
33
- declare const Thread: FC;
29
+ interface ThreadSuggestion {
30
+ title: string;
31
+ description?: string;
32
+ }
33
+ interface ThreadWelcomeConfig {
34
+ heading?: string;
35
+ subheading?: string;
36
+ }
37
+ interface ThreadProps {
38
+ className?: string;
39
+ /** Max width of the message column. Default: "44rem" */
40
+ maxWidth?: string;
41
+ /** Welcome screen text */
42
+ welcome?: ThreadWelcomeConfig;
43
+ /** Suggestion chips shown on the welcome screen */
44
+ suggestions?: ThreadSuggestion[];
45
+ /** Composer input placeholder. Default: "Send a message..." */
46
+ composerPlaceholder?: string;
47
+ }
48
+ declare const Thread: FC<ThreadProps>;
49
+
50
+ interface TimbalChatProps extends Omit<TimbalRuntimeProviderProps, "children">, ThreadProps {
51
+ }
52
+ declare function TimbalChat({ workforceId, baseUrl, fetch, ...threadProps }: TimbalChatProps): react_jsx_runtime.JSX.Element;
34
53
 
35
54
  declare const MarkdownText: React.MemoExoticComponent<() => react_jsx_runtime.JSX.Element>;
36
55
 
@@ -117,4 +136,4 @@ declare const Shimmer: React.MemoExoticComponent<({ children, as: Component, cla
117
136
 
118
137
  declare function cn(...inputs: ClassValue[]): string;
119
138
 
120
- 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 };
139
+ 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 ThreadProps, type ThreadSuggestion, type ThreadWelcomeConfig, 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
@@ -23,14 +23,33 @@ interface TimbalRuntimeProviderProps {
23
23
  * attaches Bearer tokens from localStorage and auto-refreshes on 401.
24
24
  */
25
25
  fetch?: FetchFn;
26
- /** Enable fake streaming for development/testing. Default: false */
27
- devFakeStream?: boolean;
28
- /** Token delay in ms for fake streaming. Default: 75 */
29
- devFakeStreamDelayMs?: number;
30
26
  }
31
- declare function TimbalRuntimeProvider({ workforceId, children, baseUrl, fetch: fetchFn, devFakeStream, devFakeStreamDelayMs, }: TimbalRuntimeProviderProps): react_jsx_runtime.JSX.Element;
27
+ declare function TimbalRuntimeProvider({ workforceId, children, baseUrl, fetch: fetchFn, }: TimbalRuntimeProviderProps): react_jsx_runtime.JSX.Element;
32
28
 
33
- declare const Thread: FC;
29
+ interface ThreadSuggestion {
30
+ title: string;
31
+ description?: string;
32
+ }
33
+ interface ThreadWelcomeConfig {
34
+ heading?: string;
35
+ subheading?: string;
36
+ }
37
+ interface ThreadProps {
38
+ className?: string;
39
+ /** Max width of the message column. Default: "44rem" */
40
+ maxWidth?: string;
41
+ /** Welcome screen text */
42
+ welcome?: ThreadWelcomeConfig;
43
+ /** Suggestion chips shown on the welcome screen */
44
+ suggestions?: ThreadSuggestion[];
45
+ /** Composer input placeholder. Default: "Send a message..." */
46
+ composerPlaceholder?: string;
47
+ }
48
+ declare const Thread: FC<ThreadProps>;
49
+
50
+ interface TimbalChatProps extends Omit<TimbalRuntimeProviderProps, "children">, ThreadProps {
51
+ }
52
+ declare function TimbalChat({ workforceId, baseUrl, fetch, ...threadProps }: TimbalChatProps): react_jsx_runtime.JSX.Element;
34
53
 
35
54
  declare const MarkdownText: React.MemoExoticComponent<() => react_jsx_runtime.JSX.Element>;
36
55
 
@@ -117,4 +136,4 @@ declare const Shimmer: React.MemoExoticComponent<({ children, as: Component, cla
117
136
 
118
137
  declare function cn(...inputs: ClassValue[]): string;
119
138
 
120
- 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 };
139
+ 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 ThreadProps, type ThreadSuggestion, type ThreadWelcomeConfig, 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
@@ -87,7 +87,6 @@ var fetchCurrentUser = async () => {
87
87
 
88
88
  // src/runtime/provider.tsx
89
89
  import { jsx } from "react/jsx-runtime";
90
- var parseLine = parseSSELine;
91
90
  var convertMessage = (message) => ({
92
91
  role: message.role,
93
92
  content: message.content,
@@ -107,65 +106,11 @@ function getTextFromMessage(message) {
107
106
  const part = message.content.find((c) => c.type === "text");
108
107
  return part?.type === "text" ? part.text : null;
109
108
  }
110
- function waitWithAbort(ms, signal) {
111
- if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
112
- return new Promise((resolve, reject) => {
113
- const timeoutId = setTimeout(() => {
114
- signal.removeEventListener("abort", onAbort);
115
- resolve();
116
- }, ms);
117
- const onAbort = () => {
118
- clearTimeout(timeoutId);
119
- reject(new DOMException("The operation was aborted.", "AbortError"));
120
- };
121
- signal.addEventListener("abort", onAbort, { once: true });
122
- });
123
- }
124
- function buildFakeLongResponse(input) {
125
- const safeInput = input.trim() || "your request";
126
- const base = [
127
- `Fake streaming fallback enabled. You asked: "${safeInput}".`,
128
- "",
129
- "This is a deliberately long response used to test rendering, scrolling, cancellation, and streaming UX behavior.",
130
- "",
131
- "What this stream is exercising:",
132
- "- Frequent tiny token updates",
133
- "- Long markdown paragraphs",
134
- "- Bullet list rendering",
135
- "- UI action bar behavior while running",
136
- "- Stop button and abort flow",
137
- "",
138
- "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse vitae mi at augue pulvinar porta. Praesent ullamcorper felis at nibh tincidunt, id sagittis mauris interdum. Integer nec semper dui. Curabitur sed fermentum libero. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.",
139
- "",
140
- "Aliquam luctus purus non bibendum faucibus. Donec at elit eget massa feugiat ultricies. Quisque condimentum, libero in egestas varius, purus justo aliquam sem, vitae feugiat nunc lorem a justo. Sed non tempor est. In hac habitasse platea dictumst.",
141
- "",
142
- "If you can read this arriving progressively, the fallback is working as intended."
143
- ].join("\n");
144
- return `${base}
145
-
146
- ---
147
-
148
- ${base}`;
149
- }
150
- async function streamFakeLongResponse(input, delayMs, signal, onDelta) {
151
- const fullResponse = buildFakeLongResponse(input);
152
- let cursor = 0;
153
- while (cursor < fullResponse.length) {
154
- if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
155
- const chunkSize = Math.min(fullResponse.length - cursor, Math.floor(Math.random() * 12) + 2);
156
- const delta = fullResponse.slice(cursor, cursor + chunkSize);
157
- cursor += chunkSize;
158
- onDelta(delta);
159
- await waitWithAbort(delayMs, signal);
160
- }
161
- }
162
109
  function TimbalRuntimeProvider({
163
110
  workforceId,
164
111
  children,
165
112
  baseUrl = "/api",
166
- fetch: fetchFn,
167
- devFakeStream = false,
168
- devFakeStreamDelayMs = 75
113
+ fetch: fetchFn
169
114
  }) {
170
115
  const [messages, setMessages] = useState([]);
171
116
  const [isRunning, setIsRunning] = useState(false);
@@ -200,20 +145,6 @@ function TimbalRuntimeProvider({
200
145
  );
201
146
  };
202
147
  try {
203
- if (devFakeStream) {
204
- const fakeId = `call_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
205
- parts.push({ type: "tool-call", toolCallId: fakeId, toolName: "get_datetime", argsText: "{}" });
206
- flush();
207
- await waitWithAbort(2e3, signal);
208
- parts[0].result = `Current datetime (from tool): ${(/* @__PURE__ */ new Date()).toISOString()}`;
209
- flush();
210
- await waitWithAbort(300, signal);
211
- await streamFakeLongResponse(input, devFakeStreamDelayMs, signal, (delta) => {
212
- lastTextPart().text += delta;
213
- flush();
214
- });
215
- return;
216
- }
217
148
  const res = await fetchFnRef.current(`${baseUrl}/workforce/${workforceId}/stream`, {
218
149
  method: "POST",
219
150
  headers: { "Content-Type": "application/json" },
@@ -235,7 +166,7 @@ function TimbalRuntimeProvider({
235
166
  const lines = buffer.split("\n");
236
167
  buffer = lines.pop() ?? "";
237
168
  for (const line of lines) {
238
- const event = parseLine(line);
169
+ const event = parseSSELine(line);
239
170
  if (!event) continue;
240
171
  if (!capturedRunId && isTopLevelStart(event)) {
241
172
  capturedRunId = event.run_id;
@@ -305,7 +236,7 @@ function TimbalRuntimeProvider({
305
236
  }
306
237
  }
307
238
  if (buffer.trim()) {
308
- const event = parseLine(buffer);
239
+ const event = parseSSELine(buffer);
309
240
  if (event?.type === "OUTPUT" && parts.length === 0 && event.output) {
310
241
  const text = typeof event.output === "string" ? event.output : JSON.stringify(event.output);
311
242
  parts.push({ type: "text", text });
@@ -322,7 +253,7 @@ function TimbalRuntimeProvider({
322
253
  abortRef.current = null;
323
254
  }
324
255
  },
325
- [workforceId, baseUrl, devFakeStream, devFakeStreamDelayMs]
256
+ [workforceId, baseUrl]
326
257
  );
327
258
  const onNew = useCallback(
328
259
  async (message) => {
@@ -1285,8 +1216,8 @@ import {
1285
1216
  ComposerPrimitive as ComposerPrimitive2,
1286
1217
  ErrorPrimitive,
1287
1218
  MessagePrimitive as MessagePrimitive2,
1288
- SuggestionPrimitive,
1289
- ThreadPrimitive
1219
+ ThreadPrimitive,
1220
+ useThreadRuntime
1290
1221
  } from "@assistant-ui/react";
1291
1222
  import {
1292
1223
  ArrowDownIcon,
@@ -1300,21 +1231,28 @@ import {
1300
1231
  SquareIcon
1301
1232
  } from "lucide-react";
1302
1233
  import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
1303
- var Thread = () => {
1234
+ var Thread = ({
1235
+ className,
1236
+ maxWidth = "44rem",
1237
+ welcome,
1238
+ suggestions,
1239
+ composerPlaceholder = "Send a message..."
1240
+ }) => {
1304
1241
  return /* @__PURE__ */ jsx12(
1305
1242
  ThreadPrimitive.Root,
1306
1243
  {
1307
- className: "aui-root aui-thread-root @container flex h-full flex-col bg-background",
1308
- style: {
1309
- ["--thread-max-width"]: "44rem"
1310
- },
1244
+ className: cn(
1245
+ "aui-root aui-thread-root @container flex h-full flex-col bg-background",
1246
+ className
1247
+ ),
1248
+ style: { ["--thread-max-width"]: maxWidth },
1311
1249
  children: /* @__PURE__ */ jsxs7(
1312
1250
  ThreadPrimitive.Viewport,
1313
1251
  {
1314
1252
  turnAnchor: "bottom",
1315
1253
  className: "aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4 pt-4",
1316
1254
  children: [
1317
- /* @__PURE__ */ jsx12(AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ jsx12(ThreadWelcome, {}) }),
1255
+ /* @__PURE__ */ jsx12(AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ jsx12(ThreadWelcome, { config: welcome, suggestions }) }),
1318
1256
  /* @__PURE__ */ jsx12(
1319
1257
  ThreadPrimitive.Messages,
1320
1258
  {
@@ -1327,7 +1265,7 @@ var Thread = () => {
1327
1265
  ),
1328
1266
  /* @__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: [
1329
1267
  /* @__PURE__ */ jsx12(ThreadScrollToBottom, {}),
1330
- /* @__PURE__ */ jsx12(Composer, {})
1268
+ /* @__PURE__ */ jsx12(Composer, { placeholder: composerPlaceholder })
1331
1269
  ] })
1332
1270
  ]
1333
1271
  }
@@ -1346,7 +1284,7 @@ var ThreadScrollToBottom = () => {
1346
1284
  }
1347
1285
  ) });
1348
1286
  };
1349
- var ThreadWelcome = () => {
1287
+ var ThreadWelcome = ({ config, suggestions }) => {
1350
1288
  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: [
1351
1289
  /* @__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: [
1352
1290
  /* @__PURE__ */ jsxs7("div", { className: "fade-in animate-in fill-mode-both relative mb-6 flex size-14 items-center justify-center duration-300", children: [
@@ -1367,42 +1305,40 @@ var ThreadWelcome = () => {
1367
1305
  }
1368
1306
  )
1369
1307
  ] }),
1370
- /* @__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?" }),
1371
- /* @__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." })
1308
+ /* @__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?" }),
1309
+ /* @__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." })
1372
1310
  ] }) }),
1373
- /* @__PURE__ */ jsx12(ThreadSuggestions, {})
1311
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx12(ThreadSuggestions, { suggestions })
1374
1312
  ] });
1375
1313
  };
1376
- var ThreadSuggestions = () => {
1377
- return /* @__PURE__ */ jsx12("div", { className: "aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4", children: /* @__PURE__ */ jsx12(
1378
- ThreadPrimitive.Suggestions,
1379
- {
1380
- components: {
1381
- Suggestion: ThreadSuggestionItem
1382
- }
1383
- }
1384
- ) });
1314
+ var ThreadSuggestions = ({ suggestions }) => {
1315
+ 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)) });
1385
1316
  };
1386
- var ThreadSuggestionItem = () => {
1387
- 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(
1317
+ var ThreadSuggestionItem = ({ title, description }) => {
1318
+ const runtime = useThreadRuntime();
1319
+ 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(
1388
1320
  Button,
1389
1321
  {
1390
1322
  variant: "ghost",
1391
1323
  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",
1324
+ onClick: () => runtime.append({
1325
+ role: "user",
1326
+ content: [{ type: "text", text: title }]
1327
+ }),
1392
1328
  children: [
1393
- /* @__PURE__ */ jsx12("span", { className: "aui-thread-welcome-suggestion-text-1 font-medium", children: /* @__PURE__ */ jsx12(SuggestionPrimitive.Title, {}) }),
1394
- /* @__PURE__ */ jsx12("span", { className: "aui-thread-welcome-suggestion-text-2 text-muted-foreground", children: /* @__PURE__ */ jsx12(SuggestionPrimitive.Description, {}) })
1329
+ /* @__PURE__ */ jsx12("span", { className: "aui-thread-welcome-suggestion-text-1 font-medium", children: title }),
1330
+ description && /* @__PURE__ */ jsx12("span", { className: "aui-thread-welcome-suggestion-text-2 text-muted-foreground", children: description })
1395
1331
  ]
1396
1332
  }
1397
- ) }) });
1333
+ ) });
1398
1334
  };
1399
- var Composer = () => {
1335
+ var Composer = ({ placeholder }) => {
1400
1336
  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: [
1401
1337
  /* @__PURE__ */ jsx12(ComposerAttachments, {}),
1402
1338
  /* @__PURE__ */ jsx12(
1403
1339
  ComposerPrimitive2.Input,
1404
1340
  {
1405
- placeholder: "Send a message...",
1341
+ placeholder: placeholder ?? "Send a message...",
1406
1342
  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",
1407
1343
  rows: 1,
1408
1344
  autoFocus: true,
@@ -1550,6 +1486,17 @@ var EditComposer = () => {
1550
1486
  ] }) });
1551
1487
  };
1552
1488
 
1489
+ // src/components/chat.tsx
1490
+ import { jsx as jsx13 } from "react/jsx-runtime";
1491
+ function TimbalChat({
1492
+ workforceId,
1493
+ baseUrl,
1494
+ fetch: fetch2,
1495
+ ...threadProps
1496
+ }) {
1497
+ return /* @__PURE__ */ jsx13(TimbalRuntimeProvider, { workforceId, baseUrl, fetch: fetch2, children: /* @__PURE__ */ jsx13(Thread, { ...threadProps }) });
1498
+ }
1499
+
1553
1500
  // src/auth/provider.tsx
1554
1501
  import {
1555
1502
  createContext,
@@ -1558,7 +1505,7 @@ import {
1558
1505
  useEffect as useEffect4,
1559
1506
  useState as useState5
1560
1507
  } from "react";
1561
- import { jsx as jsx13 } from "react/jsx-runtime";
1508
+ import { jsx as jsx14 } from "react/jsx-runtime";
1562
1509
  var SessionContext = createContext(void 0);
1563
1510
  var useSession = () => {
1564
1511
  const context = useContext(SessionContext);
@@ -1622,7 +1569,7 @@ var SessionProvider = ({
1622
1569
  () => window.location.href = `/api/auth/login?return_to=${returnTo}`
1623
1570
  );
1624
1571
  }, []);
1625
- return /* @__PURE__ */ jsx13(
1572
+ return /* @__PURE__ */ jsx14(
1626
1573
  SessionContext.Provider,
1627
1574
  {
1628
1575
  value: {
@@ -1638,7 +1585,7 @@ var SessionProvider = ({
1638
1585
 
1639
1586
  // src/auth/guard.tsx
1640
1587
  import { Loader2 } from "lucide-react";
1641
- import { jsx as jsx14 } from "react/jsx-runtime";
1588
+ import { jsx as jsx15 } from "react/jsx-runtime";
1642
1589
  var AuthGuard = ({
1643
1590
  children,
1644
1591
  requireAuth = false,
@@ -1649,7 +1596,7 @@ var AuthGuard = ({
1649
1596
  return children;
1650
1597
  }
1651
1598
  if (loading) {
1652
- return /* @__PURE__ */ jsx14("div", { className: "flex items-center justify-center h-screen", children: /* @__PURE__ */ jsx14(Loader2, { className: "w-8 h-8 animate-spin" }) });
1599
+ return /* @__PURE__ */ jsx15("div", { className: "flex items-center justify-center h-screen", children: /* @__PURE__ */ jsx15(Loader2, { className: "w-8 h-8 animate-spin" }) });
1653
1600
  }
1654
1601
  if (requireAuth && !isAuthenticated) {
1655
1602
  const returnTo = encodeURIComponent(
@@ -1680,6 +1627,7 @@ export {
1680
1627
  Shimmer,
1681
1628
  syntax_highlighter_default as SyntaxHighlighter,
1682
1629
  Thread,
1630
+ TimbalChat,
1683
1631
  TimbalRuntimeProvider,
1684
1632
  ToolFallback,
1685
1633
  Tooltip,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timbal-ai/timbal-react",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "React components and runtime for building Timbal chat UIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",