@timbal-ai/timbal-react 0.1.1 → 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
@@ -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 |
@@ -80,23 +190,33 @@ The package includes a session/auth system backed by localStorage tokens. The AP
80
190
  Wrap your app with `SessionProvider` and protect routes with `AuthGuard`:
81
191
 
82
192
  ```tsx
193
+ // src/App.tsx
83
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";
84
197
 
198
+ // Auth is opt-in — only active when VITE_TIMBAL_PROJECT_ID is set
85
199
  const isAuthEnabled = !!import.meta.env.VITE_TIMBAL_PROJECT_ID;
86
200
 
87
- function App() {
201
+ export default function App() {
88
202
  return (
89
203
  <SessionProvider enabled={isAuthEnabled}>
90
204
  <TooltipProvider>
91
- <AuthGuard requireAuth enabled={isAuthEnabled}>
92
- <YourApp />
93
- </AuthGuard>
205
+ <BrowserRouter>
206
+ <AuthGuard requireAuth enabled={isAuthEnabled}>
207
+ <Routes>
208
+ <Route path="/" element={<Home />} />
209
+ </Routes>
210
+ </AuthGuard>
211
+ </BrowserRouter>
94
212
  </TooltipProvider>
95
213
  </SessionProvider>
96
214
  );
97
215
  }
98
216
  ```
99
217
 
218
+ When `enabled` is `false` (no project ID configured), both `SessionProvider` and `AuthGuard` are transparent — no redirects, no API calls.
219
+
100
220
  ### `SessionProvider` props
101
221
 
102
222
  | Prop | Type | Default | Description |
@@ -112,25 +232,47 @@ function App() {
112
232
 
113
233
  ### `useSession` hook
114
234
 
235
+ Access the current session anywhere inside `SessionProvider`:
236
+
115
237
  ```tsx
116
238
  import { useSession } from "@timbal-ai/timbal-react";
117
239
 
118
240
  function Header() {
119
241
  const { user, isAuthenticated, loading, logout } = useSession();
120
- // ...
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
+ );
121
257
  }
122
258
  ```
123
259
 
124
260
  ### `authFetch`
125
261
 
126
- 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:
127
263
 
128
264
  ```tsx
129
265
  import { authFetch } from "@timbal-ai/timbal-react";
130
266
 
267
+ // Fetch a list of workforce agents
131
268
  const res = await authFetch("/api/workforce");
269
+ if (res.ok) {
270
+ const agents = await res.json();
271
+ }
132
272
  ```
133
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
+
134
276
  ---
135
277
 
136
278
  ## Components
@@ -156,6 +298,71 @@ Re-exported Radix UI wrappers pre-styled to match the Timbal design system:
156
298
 
157
299
  ---
158
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
+
159
366
  ## Local development
160
367
 
161
368
  Install via a local path reference:
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,
@@ -1267,21 +1268,28 @@ ToolFallback.displayName = "ToolFallback";
1267
1268
  var import_react11 = require("@assistant-ui/react");
1268
1269
  var import_lucide_react5 = require("lucide-react");
1269
1270
  var import_jsx_runtime12 = require("react/jsx-runtime");
1270
- var Thread = () => {
1271
+ var Thread = ({
1272
+ className,
1273
+ maxWidth = "44rem",
1274
+ welcome,
1275
+ suggestions,
1276
+ composerPlaceholder = "Send a message..."
1277
+ }) => {
1271
1278
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1272
1279
  import_react11.ThreadPrimitive.Root,
1273
1280
  {
1274
- className: "aui-root aui-thread-root @container flex h-full flex-col bg-background",
1275
- style: {
1276
- ["--thread-max-width"]: "44rem"
1277
- },
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 },
1278
1286
  children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1279
1287
  import_react11.ThreadPrimitive.Viewport,
1280
1288
  {
1281
1289
  turnAnchor: "bottom",
1282
1290
  className: "aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4 pt-4",
1283
1291
  children: [
1284
- /* @__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 }) }),
1285
1293
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1286
1294
  import_react11.ThreadPrimitive.Messages,
1287
1295
  {
@@ -1294,7 +1302,7 @@ var Thread = () => {
1294
1302
  ),
1295
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: [
1296
1304
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadScrollToBottom, {}),
1297
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Composer, {})
1305
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Composer, { placeholder: composerPlaceholder })
1298
1306
  ] })
1299
1307
  ]
1300
1308
  }
@@ -1313,7 +1321,7 @@ var ThreadScrollToBottom = () => {
1313
1321
  }
1314
1322
  ) });
1315
1323
  };
1316
- var ThreadWelcome = () => {
1324
+ var ThreadWelcome = ({ config, suggestions }) => {
1317
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: [
1318
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: [
1319
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: [
@@ -1334,42 +1342,40 @@ var ThreadWelcome = () => {
1334
1342
  }
1335
1343
  )
1336
1344
  ] }),
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." })
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." })
1339
1347
  ] }) }),
1340
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadSuggestions, {})
1348
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadSuggestions, { suggestions })
1341
1349
  ] });
1342
1350
  };
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
- ) });
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)) });
1352
1353
  };
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)(
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)(
1355
1357
  Button,
1356
1358
  {
1357
1359
  variant: "ghost",
1358
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
+ }),
1359
1365
  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, {}) })
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 })
1362
1368
  ]
1363
1369
  }
1364
- ) }) });
1370
+ ) });
1365
1371
  };
1366
- var Composer = () => {
1372
+ var Composer = ({ placeholder }) => {
1367
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: [
1368
1374
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ComposerAttachments, {}),
1369
1375
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1370
1376
  import_react11.ComposerPrimitive.Input,
1371
1377
  {
1372
- placeholder: "Send a message...",
1378
+ placeholder: placeholder ?? "Send a message...",
1373
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",
1374
1380
  rows: 1,
1375
1381
  autoFocus: true,
@@ -1517,9 +1523,20 @@ var EditComposer = () => {
1517
1523
  ] }) });
1518
1524
  };
1519
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
+
1520
1537
  // src/auth/provider.tsx
1521
1538
  var import_react12 = require("react");
1522
- var import_jsx_runtime13 = require("react/jsx-runtime");
1539
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1523
1540
  var SessionContext = (0, import_react12.createContext)(void 0);
1524
1541
  var useSession = () => {
1525
1542
  const context = (0, import_react12.useContext)(SessionContext);
@@ -1583,7 +1600,7 @@ var SessionProvider = ({
1583
1600
  () => window.location.href = `/api/auth/login?return_to=${returnTo}`
1584
1601
  );
1585
1602
  }, []);
1586
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1603
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1587
1604
  SessionContext.Provider,
1588
1605
  {
1589
1606
  value: {
@@ -1599,7 +1616,7 @@ var SessionProvider = ({
1599
1616
 
1600
1617
  // src/auth/guard.tsx
1601
1618
  var import_lucide_react6 = require("lucide-react");
1602
- var import_jsx_runtime14 = require("react/jsx-runtime");
1619
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1603
1620
  var AuthGuard = ({
1604
1621
  children,
1605
1622
  requireAuth = false,
@@ -1610,7 +1627,7 @@ var AuthGuard = ({
1610
1627
  return children;
1611
1628
  }
1612
1629
  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" }) });
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" }) });
1614
1631
  }
1615
1632
  if (requireAuth && !isAuthenticated) {
1616
1633
  const returnTo = encodeURIComponent(
@@ -1642,6 +1659,7 @@ var AuthGuard = ({
1642
1659
  Shimmer,
1643
1660
  SyntaxHighlighter,
1644
1661
  Thread,
1662
+ TimbalChat,
1645
1663
  TimbalRuntimeProvider,
1646
1664
  ToolFallback,
1647
1665
  Tooltip,
package/dist/index.d.cts CHANGED
@@ -26,7 +26,30 @@ interface TimbalRuntimeProviderProps {
26
26
  }
27
27
  declare function TimbalRuntimeProvider({ workforceId, children, baseUrl, fetch: fetchFn, }: TimbalRuntimeProviderProps): react_jsx_runtime.JSX.Element;
28
28
 
29
- 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;
30
53
 
31
54
  declare const MarkdownText: React.MemoExoticComponent<() => react_jsx_runtime.JSX.Element>;
32
55
 
@@ -113,4 +136,4 @@ declare const Shimmer: React.MemoExoticComponent<({ children, as: Component, cla
113
136
 
114
137
  declare function cn(...inputs: ClassValue[]): string;
115
138
 
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 };
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
@@ -26,7 +26,30 @@ interface TimbalRuntimeProviderProps {
26
26
  }
27
27
  declare function TimbalRuntimeProvider({ workforceId, children, baseUrl, fetch: fetchFn, }: TimbalRuntimeProviderProps): react_jsx_runtime.JSX.Element;
28
28
 
29
- 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;
30
53
 
31
54
  declare const MarkdownText: React.MemoExoticComponent<() => react_jsx_runtime.JSX.Element>;
32
55
 
@@ -113,4 +136,4 @@ declare const Shimmer: React.MemoExoticComponent<({ children, as: Component, cla
113
136
 
114
137
  declare function cn(...inputs: ClassValue[]): string;
115
138
 
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 };
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
@@ -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,21 +1231,28 @@ 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
+ }) => {
1235
1241
  return /* @__PURE__ */ jsx12(
1236
1242
  ThreadPrimitive.Root,
1237
1243
  {
1238
- className: "aui-root aui-thread-root @container flex h-full flex-col bg-background",
1239
- style: {
1240
- ["--thread-max-width"]: "44rem"
1241
- },
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 },
1242
1249
  children: /* @__PURE__ */ jsxs7(
1243
1250
  ThreadPrimitive.Viewport,
1244
1251
  {
1245
1252
  turnAnchor: "bottom",
1246
1253
  className: "aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4 pt-4",
1247
1254
  children: [
1248
- /* @__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 }) }),
1249
1256
  /* @__PURE__ */ jsx12(
1250
1257
  ThreadPrimitive.Messages,
1251
1258
  {
@@ -1258,7 +1265,7 @@ var Thread = () => {
1258
1265
  ),
1259
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: [
1260
1267
  /* @__PURE__ */ jsx12(ThreadScrollToBottom, {}),
1261
- /* @__PURE__ */ jsx12(Composer, {})
1268
+ /* @__PURE__ */ jsx12(Composer, { placeholder: composerPlaceholder })
1262
1269
  ] })
1263
1270
  ]
1264
1271
  }
@@ -1277,7 +1284,7 @@ var ThreadScrollToBottom = () => {
1277
1284
  }
1278
1285
  ) });
1279
1286
  };
1280
- var ThreadWelcome = () => {
1287
+ var ThreadWelcome = ({ config, suggestions }) => {
1281
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: [
1282
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: [
1283
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: [
@@ -1298,42 +1305,40 @@ var ThreadWelcome = () => {
1298
1305
  }
1299
1306
  )
1300
1307
  ] }),
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." })
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." })
1303
1310
  ] }) }),
1304
- /* @__PURE__ */ jsx12(ThreadSuggestions, {})
1311
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx12(ThreadSuggestions, { suggestions })
1305
1312
  ] });
1306
1313
  };
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
- ) });
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)) });
1316
1316
  };
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(
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(
1319
1320
  Button,
1320
1321
  {
1321
1322
  variant: "ghost",
1322
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
+ }),
1323
1328
  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, {}) })
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 })
1326
1331
  ]
1327
1332
  }
1328
- ) }) });
1333
+ ) });
1329
1334
  };
1330
- var Composer = () => {
1335
+ var Composer = ({ placeholder }) => {
1331
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: [
1332
1337
  /* @__PURE__ */ jsx12(ComposerAttachments, {}),
1333
1338
  /* @__PURE__ */ jsx12(
1334
1339
  ComposerPrimitive2.Input,
1335
1340
  {
1336
- placeholder: "Send a message...",
1341
+ placeholder: placeholder ?? "Send a message...",
1337
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",
1338
1343
  rows: 1,
1339
1344
  autoFocus: true,
@@ -1481,6 +1486,17 @@ var EditComposer = () => {
1481
1486
  ] }) });
1482
1487
  };
1483
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
+
1484
1500
  // src/auth/provider.tsx
1485
1501
  import {
1486
1502
  createContext,
@@ -1489,7 +1505,7 @@ import {
1489
1505
  useEffect as useEffect4,
1490
1506
  useState as useState5
1491
1507
  } from "react";
1492
- import { jsx as jsx13 } from "react/jsx-runtime";
1508
+ import { jsx as jsx14 } from "react/jsx-runtime";
1493
1509
  var SessionContext = createContext(void 0);
1494
1510
  var useSession = () => {
1495
1511
  const context = useContext(SessionContext);
@@ -1553,7 +1569,7 @@ var SessionProvider = ({
1553
1569
  () => window.location.href = `/api/auth/login?return_to=${returnTo}`
1554
1570
  );
1555
1571
  }, []);
1556
- return /* @__PURE__ */ jsx13(
1572
+ return /* @__PURE__ */ jsx14(
1557
1573
  SessionContext.Provider,
1558
1574
  {
1559
1575
  value: {
@@ -1569,7 +1585,7 @@ var SessionProvider = ({
1569
1585
 
1570
1586
  // src/auth/guard.tsx
1571
1587
  import { Loader2 } from "lucide-react";
1572
- import { jsx as jsx14 } from "react/jsx-runtime";
1588
+ import { jsx as jsx15 } from "react/jsx-runtime";
1573
1589
  var AuthGuard = ({
1574
1590
  children,
1575
1591
  requireAuth = false,
@@ -1580,7 +1596,7 @@ var AuthGuard = ({
1580
1596
  return children;
1581
1597
  }
1582
1598
  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" }) });
1599
+ return /* @__PURE__ */ jsx15("div", { className: "flex items-center justify-center h-screen", children: /* @__PURE__ */ jsx15(Loader2, { className: "w-8 h-8 animate-spin" }) });
1584
1600
  }
1585
1601
  if (requireAuth && !isAuthenticated) {
1586
1602
  const returnTo = encodeURIComponent(
@@ -1611,6 +1627,7 @@ export {
1611
1627
  Shimmer,
1612
1628
  syntax_highlighter_default as SyntaxHighlighter,
1613
1629
  Thread,
1630
+ TimbalChat,
1614
1631
  TimbalRuntimeProvider,
1615
1632
  ToolFallback,
1616
1633
  Tooltip,
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.0",
4
4
  "description": "React components and runtime for building Timbal chat UIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",