juneau 0.1.0 → 0.1.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
@@ -3,11 +3,13 @@
3
3
  React component library for building AI chat interfaces. Streaming-first, adapter-based, fully themeable.
4
4
 
5
5
  ```tsx
6
- import { JuneauProvider, AiSidebar } from 'juneau';
6
+ import { JuneauProvider, AiSidebar, createSseAdapter } from 'juneau';
7
7
  import 'juneau/dist/style.css';
8
8
 
9
+ const adapter = createSseAdapter('/api/chat');
10
+
9
11
  <JuneauProvider>
10
- <AiSidebar adapter={myAdapter} />
12
+ <AiSidebar adapter={adapter} />
11
13
  </JuneauProvider>
12
14
  ```
13
15
 
@@ -19,13 +21,15 @@ import 'juneau/dist/style.css';
19
21
  npm install juneau
20
22
  ```
21
23
 
22
- Peer dependencies: `react ^18 || ^19`, `react-dom ^18 || ^19`
24
+ **Peer dependencies:** `react ^18 || ^19`, `react-dom ^18 || ^19`
23
25
 
24
26
  ---
25
27
 
26
28
  ## Core concept — the adapter
27
29
 
28
- Juneau never calls any AI API directly. You provide an **adapter** that implements one method:
30
+ Juneau never calls any AI API directly. **You own the network layer.** Juneau provides the UI, state, and streaming machinery — you provide an adapter that connects it to your backend.
31
+
32
+ An adapter is just one method:
29
33
 
30
34
  ```ts
31
35
  interface AiBackendAdapter {
@@ -33,37 +37,139 @@ interface AiBackendAdapter {
33
37
  }
34
38
  ```
35
39
 
36
- The adapter receives the full conversation history and streams back events:
40
+ It receives the conversation history and streams back typed events:
37
41
 
38
42
  ```ts
39
43
  type AiStreamEvent =
40
- | { type: 'text'; text: string } // streamed text chunk
41
- | { type: 'part'; part: AiTablePart | AiProposalPart | AiErrorPart } // rich content block
42
- | { type: 'done' } // stream finished
43
- | { type: 'error'; message: string } // stream failed
44
+ | { type: 'text'; text: string } // streamed text chunk — append to current bubble
45
+ | { type: 'part'; part: AiTablePart | AiProposalPart | AiErrorPart } // rich UI block
46
+ | { type: 'done' } // stream finished cleanly
47
+ | { type: 'error'; message: string } // stream failed
44
48
  ```
45
49
 
46
- ### Minimal adapter example (OpenAI)
50
+ This means your API keys stay on your backend, you control auth, rate limiting, model selection — Juneau just renders whatever comes back.
51
+
52
+ ---
53
+
54
+ ## Built-in adapters
55
+
56
+ Juneau ships two factory functions so you don't have to write boilerplate streaming code.
57
+
58
+ ### `createSseAdapter(url, options?)` — recommended
59
+
60
+ For backends that stream **Server-Sent Events (SSE)** — the format used by OpenAI, Anthropic, and most AI API proxies.
61
+
62
+ The default parser understands OpenAI's streaming format out of the box (`choices[0].delta.content`).
63
+
64
+ ```ts
65
+ import { createSseAdapter } from 'juneau';
66
+
67
+ // OpenAI-compatible backend — zero config needed:
68
+ const adapter = createSseAdapter('/api/chat');
69
+
70
+ // With dynamic auth header:
71
+ const adapter = createSseAdapter('/api/chat', {
72
+ getHeaders: async () => ({
73
+ Authorization: `Bearer ${await getSessionToken()}`,
74
+ }),
75
+ });
76
+
77
+ // Custom request body:
78
+ const adapter = createSseAdapter('/api/chat', {
79
+ getBody: ({ messages, context }) => ({
80
+ messages,
81
+ model: 'gpt-4o',
82
+ stream: true,
83
+ temperature: 0.7,
84
+ }),
85
+ });
86
+
87
+ // Custom SSE event schema (backend streams { text: "..." } instead of OpenAI format):
88
+ const adapter = createSseAdapter('/api/chat', {
89
+ parseEvent: (data) => {
90
+ try {
91
+ const json = JSON.parse(data);
92
+ return json.text ? [{ type: 'text', text: json.text }] : [];
93
+ } catch {
94
+ return [];
95
+ }
96
+ },
97
+ });
98
+ ```
99
+
100
+ | Option | Type | Default | Description |
101
+ |---|---|---|---|
102
+ | `method` | `string` | `'POST'` | HTTP method |
103
+ | `headers` | `Record<string, string>` | `{}` | Static headers, merged with Content-Type |
104
+ | `getHeaders` | `(input) => Record<string, string>` | — | Dynamic headers, called per request. Merged on top of `headers`. |
105
+ | `getBody` | `(input) => unknown` | `{ messages, context }` | Override request body |
106
+ | `parseEvent` | `(data: string) => AiStreamEvent[]` | OpenAI parser | Parse each `data: ...` SSE line into events |
107
+
108
+ ---
109
+
110
+ ### `createFetchStreamAdapter(url, options?)` — for non-SSE backends
111
+
112
+ For backends that stream **newline-delimited JSON (NDJSON)** or plain text — raw chunked HTTP without SSE formatting.
113
+
114
+ The default parser expects `{ "text": "..." }` or `{ "done": true }` JSON lines.
115
+
116
+ ```ts
117
+ import { createFetchStreamAdapter } from 'juneau';
118
+
119
+ // NDJSON backend (streams { text: "..." } lines):
120
+ const adapter = createFetchStreamAdapter('/api/chat');
121
+
122
+ // Plain text — treat every chunk as raw text:
123
+ const adapter = createFetchStreamAdapter('/api/chat', {
124
+ parseChunk: (chunk) => chunk ? [{ type: 'text', text: chunk }] : [],
125
+ });
126
+
127
+ // Custom JSON lines schema:
128
+ const adapter = createFetchStreamAdapter('/api/chat', {
129
+ parseChunk: (chunk) => {
130
+ try {
131
+ const json = JSON.parse(chunk);
132
+ if (json.error) return [{ type: 'error', message: json.error }];
133
+ if (json.done) return [{ type: 'done' }];
134
+ if (json.delta) return [{ type: 'text', text: json.delta }];
135
+ return [];
136
+ } catch { return []; }
137
+ },
138
+ });
139
+ ```
140
+
141
+ | Option | Type | Default | Description |
142
+ |---|---|---|---|
143
+ | `method` | `string` | `'POST'` | HTTP method |
144
+ | `headers` | `Record<string, string>` | `{}` | Static headers |
145
+ | `getHeaders` | `(input) => Record<string, string>` | — | Dynamic headers, called per request |
146
+ | `getBody` | `(input) => unknown` | `{ messages, context }` | Override request body |
147
+ | `parseChunk` | `(chunk: string) => AiStreamEvent[]` | NDJSON parser | Parse each newline-delimited chunk into events |
148
+
149
+ ---
150
+
151
+ ### Writing your own adapter
152
+
153
+ If neither factory fits, implementing the interface directly takes about 10 lines:
47
154
 
48
155
  ```ts
49
- import type { AiBackendAdapter, AiAdapterInput } from 'juneau';
156
+ import type { AiBackendAdapter } from 'juneau';
50
157
 
51
- export const openAiAdapter: AiBackendAdapter = {
52
- async *sendMessage({ messages }: AiAdapterInput) {
53
- const response = await fetch('/api/chat', {
158
+ export const myAdapter: AiBackendAdapter = {
159
+ async *sendMessage({ messages, context }) {
160
+ const res = await fetch('/api/chat', {
54
161
  method: 'POST',
55
162
  headers: { 'Content-Type': 'application/json' },
56
163
  body: JSON.stringify({ messages }),
57
164
  });
58
165
 
59
- const reader = response.body!.getReader();
166
+ const reader = res.body!.getReader();
60
167
  const decoder = new TextDecoder();
61
168
 
62
169
  while (true) {
63
170
  const { done, value } = await reader.read();
64
171
  if (done) break;
65
- const chunk = decoder.decode(value);
66
- yield { type: 'text', text: chunk };
172
+ yield { type: 'text', text: decoder.decode(value) };
67
173
  }
68
174
 
69
175
  yield { type: 'done' };
@@ -71,7 +177,24 @@ export const openAiAdapter: AiBackendAdapter = {
71
177
  };
72
178
  ```
73
179
 
74
- A `mockAdapter` is exported for development — it responds to keywords like `"show"`, `"suggest"`, `"help"`, `"error"`.
180
+ ---
181
+
182
+ ### `mockAdapter` — for development
183
+
184
+ Shipped for local development. No backend needed — responds to keywords in the message:
185
+
186
+ | Say... | Gets you... |
187
+ |---|---|
188
+ | `"show"`, `"list"`, `"data"`, `"table"` | A rendered data table |
189
+ | `"suggest"`, `"recommend"`, `"proposal"` | A proposal card with confirm/cancel |
190
+ | `"help"`, `"what can you do"` | Capability overview |
191
+ | `"error"`, `"fail"` | Simulated error response |
192
+ | anything else | Explains the available triggers |
193
+
194
+ ```ts
195
+ import { mockAdapter } from 'juneau';
196
+ <AiSidebar adapter={mockAdapter} />
197
+ ```
75
198
 
76
199
  ---
77
200
 
@@ -79,7 +202,7 @@ A `mockAdapter` is exported for development — it responds to keywords like `"s
79
202
 
80
203
  ### `<JuneauProvider>`
81
204
 
82
- Wrap your app (or subtree) once. Provides theme and labels.
205
+ Wrap your app (or any subtree) once. Provides theme tokens and UI labels to all Juneau components below it.
83
206
 
84
207
  ```tsx
85
208
  import { JuneauProvider, juneauCs } from 'juneau';
@@ -103,66 +226,75 @@ import { JuneauProvider, juneauCs } from 'juneau';
103
226
 
104
227
  ### `<AiSidebar>`
105
228
 
106
- Self-contained chat panel. Manages its own state via `useAiChat` internally.
229
+ The fastest path to a working chat UI. Self-contained manages its own state internally via `useAiChat`.
107
230
 
108
231
  ```tsx
109
232
  <AiSidebar
110
- adapter={myAdapter}
233
+ adapter={adapter}
111
234
  title="AI Assistant"
112
- onProposalConfirm={(id, payload) => console.log('confirmed', id, payload)}
235
+ onProposalConfirm={(id, payload) => handleAction(id, payload)}
113
236
  />
114
237
  ```
115
238
 
116
239
  | Prop | Type | Description |
117
240
  |---|---|---|
118
- | `adapter` | `AiBackendAdapter` | **Required.** Your adapter implementation. |
241
+ | `adapter` | `AiBackendAdapter` | **Required.** Your adapter. |
119
242
  | `title` | `string` | Overrides the `sidebarTitle` label for this instance. |
120
- | `context` | `Record<string, unknown>` | Passed through to every `adapter.sendMessage` call. |
243
+ | `context` | `Record<string, unknown>` | Passed through to every `adapter.sendMessage` call. Useful for page-level context (current entity ID, user role, etc). |
121
244
  | `icon` | `ReactNode` | Override the header + avatar icon. Defaults to a brain icon. |
122
- | `actions` | `AiInputAction[]` | Toolbar action buttons. Pass `[]` to hide the toolbar entirely. |
245
+ | `actions` | `AiInputAction[]` | Toolbar buttons left of send. Pass `[]` to hide entirely. |
123
246
  | `sendIcon` | `ReactNode` | Override the send button icon. |
124
- | `onProposalConfirm` | `(id, payload) => void` | Called when the user confirms a proposal. |
125
- | `onProposalCancel` | `(id) => void` | Called when the user cancels a proposal. |
247
+ | `onProposalConfirm` | `(id, payload) => void` | Called when user confirms a proposal card. |
248
+ | `onProposalCancel` | `(id) => void` | Called when user cancels a proposal card. |
126
249
  | `className` | `string` | Added to the `<aside>` element. |
127
- | `style` | `CSSProperties` | Inline styles on the `<aside>` element. |
250
+ | `style` | `CSSProperties` | Inline styles on the `<aside>`. |
128
251
 
129
252
  ---
130
253
 
131
254
  ### `<AiChat>`
132
255
 
133
- Headless chat body — message list + input + error banner, no surrounding chrome. Accepts all state as props; the parent calls `useAiChat` and passes results down.
256
+ Headless chat body — message list + input bar + error banner, no surrounding chrome. Use this when you want to embed chat inside your own layout (dashboard panel, modal, full-page view).
134
257
 
135
- Use this when embedding chat in your own layout (dashboard card, modal, full-page).
258
+ The parent is responsible for calling `useAiChat` and passing results down as props.
136
259
 
137
260
  ```tsx
138
- const chat = useAiChat({ adapter: myAdapter });
139
-
140
- <AiChat
141
- messages={chat.messages}
142
- input={chat.input}
143
- isLoading={chat.isLoading}
144
- error={chat.error}
145
- onInputChange={chat.setInput}
146
- onSend={chat.sendMessage}
147
- onStop={chat.stop}
148
- onProposalConfirm={chat.confirmProposal}
149
- onProposalCancel={chat.cancelProposal}
150
- />
261
+ import { useAiChat, AiChat } from 'juneau';
262
+
263
+ function MyPage() {
264
+ const chat = useAiChat({ adapter });
265
+
266
+ return (
267
+ <div className="my-layout">
268
+ <MySidebar />
269
+ <AiChat
270
+ messages={chat.messages}
271
+ input={chat.input}
272
+ isLoading={chat.isLoading}
273
+ error={chat.error}
274
+ onInputChange={chat.setInput}
275
+ onSend={chat.sendMessage}
276
+ onStop={chat.stop}
277
+ onProposalConfirm={chat.confirmProposal}
278
+ onProposalCancel={chat.cancelProposal}
279
+ />
280
+ </div>
281
+ );
282
+ }
151
283
  ```
152
284
 
153
285
  | Prop | Type | Description |
154
286
  |---|---|---|
155
- | `messages` | `AiMessage[]` | Conversation history from `useAiChat`. |
156
- | `input` | `string` | Current input value. |
287
+ | `messages` | `AiMessage[]` | Conversation history. |
288
+ | `input` | `string` | Current textarea value. |
157
289
  | `isLoading` | `boolean` | Whether a stream is in progress. |
158
290
  | `error` | `string \| null` | Last error message, or `null`. |
159
291
  | `onInputChange` | `(value: string) => void` | Input change handler. |
160
292
  | `onSend` | `() => void` | Send the current input. |
161
- | `onStop` | `() => void` | Abort the in-flight stream. Shows a stop button while loading. |
293
+ | `onStop` | `() => void` | Abort the in-flight stream. Renders a stop button while loading. |
162
294
  | `onProposalConfirm` | `(id, payload) => void` | Proposal confirmed. |
163
295
  | `onProposalCancel` | `(id) => void` | Proposal cancelled. |
164
- | `assistantIcon` | `ReactNode` | Override assistant avatar icon in all message bubbles. |
165
- | `actions` | `AiInputAction[]` | Toolbar action buttons. |
296
+ | `assistantIcon` | `ReactNode` | Override the assistant avatar in all bubbles. |
297
+ | `actions` | `AiInputAction[]` | Toolbar buttons. |
166
298
  | `sendIcon` | `ReactNode` | Override send button icon. |
167
299
  | `placeholder` | `string` | Input placeholder text. |
168
300
 
@@ -170,89 +302,140 @@ const chat = useAiChat({ adapter: myAdapter });
170
302
 
171
303
  ## `useAiChat` hook
172
304
 
173
- For full control. Returns everything you need to wire up a custom layout.
305
+ For full control over layout and behaviour. Returns everything needed to build a custom chat UI.
174
306
 
175
307
  ```ts
176
308
  const {
177
- messages, // AiMessage[] — full conversation
178
- input, // string — current textarea value
179
- setInput, // (value: string) => void
180
- sendMessage, // () => Promise<void> — sends current input
181
- sendMessageWithText, // (text: string) => Promise<void> — send programmatically
182
- sendGreeting, // (contextHint: string) => Promise<void> — assistant-only message, no user bubble
183
- stop, // () => void — abort in-flight stream, keep messages
184
- isLoading, // boolean
185
- error, // string | null
186
- reset, // () => void — clear all messages and abort stream
187
- confirmProposal, // (id: string, payload: unknown) => void
188
- cancelProposal, // (id: string) => void
309
+ messages, // AiMessage[] — full conversation history
310
+ input, // string — current textarea value
311
+ setInput, // (value: string) => void
312
+ sendMessage, // () => Promise<void> — sends current input as user message
313
+ sendMessageWithText, // (text: string) => Promise<void> — send programmatically, no input state change
314
+ sendGreeting, // (contextHint: string) => Promise<void> — assistant speaks first, no user bubble shown
315
+ stop, // () => void — abort in-flight stream, keep existing messages
316
+ isLoading, // boolean — true while streaming
317
+ error, // string | null — last error, cleared on next send
318
+ reset, // () => void — clear all messages and abort any stream
319
+ confirmProposal, // (id: string, payload: unknown) => void
320
+ cancelProposal, // (id: string) => void
189
321
  } = useAiChat({
190
- adapter,
191
- context, // optional — passed through to every adapter call
192
- onProposalConfirm, // optional callback
193
- onProposalCancel, // optional callback
322
+ adapter, // required
323
+ context, // optional — forwarded to every adapter.sendMessage call
324
+ onProposalConfirm, // optional — called by confirmProposal
325
+ onProposalCancel, // optional — called by cancelProposal
194
326
  });
195
327
  ```
196
328
 
329
+ ### Useful patterns
330
+
331
+ **Proactive greeting on mount:**
332
+ ```ts
333
+ useEffect(() => {
334
+ chat.sendGreeting('The user just opened the dashboard. Greet them briefly.');
335
+ }, []);
336
+ ```
337
+
338
+ **Trigger from outside the chat (e.g. clicking a data row):**
339
+ ```ts
340
+ chat.sendMessageWithText(`Explain invoice #${invoice.id}`);
341
+ ```
342
+
343
+ **Pass page context to every request:**
344
+ ```tsx
345
+ <AiSidebar
346
+ adapter={adapter}
347
+ context={{ pageId: 'invoices', entityId: invoice.id, userRole: 'admin' }}
348
+ />
349
+ ```
350
+ The `context` object lands in `input.context` inside your adapter's `sendMessage`.
351
+
197
352
  ---
198
353
 
199
354
  ## Message parts
200
355
 
201
- Assistant messages are composed of typed **parts**. The adapter emits them as stream events and `useAiChat` assembles them into `AiMessage.parts`:
356
+ Assistant messages are composed of typed **parts**. Text is streamed chunk by chunk; rich UI blocks are emitted as complete `part` events.
202
357
 
203
- | Part type | Description | Rendered by |
358
+ | Part type | Emitted as | Rendered by |
204
359
  |---|---|---|
205
- | `text` | Markdown string | `AiMessagePartRenderer` (via `react-markdown`) |
206
- | `table` | Columns + rows | `AiTablePart` |
207
- | `proposal` | Action card with confirm/cancel | `AiProposalCard` |
208
- | `error` | Inline error in the message | `AiMessagePartRenderer` |
209
-
210
- Emit a rich part from your adapter:
360
+ | `text` | `{ type: 'text', text: string }` stream events, accumulated | Markdown via `react-markdown` — safe against XSS |
361
+ | `table` | `{ type: 'part', part: { type: 'table', ... } }` | `AiTablePart` |
362
+ | `proposal` | `{ type: 'part', part: { type: 'proposal', ... } }` | `AiProposalCard` |
363
+ | `error` | `{ type: 'error', message: string }` or `{ type: 'part', part: { type: 'error', ... } }` | Inline error in bubble |
211
364
 
365
+ **Emitting a proposal from your adapter:**
212
366
  ```ts
213
367
  yield {
214
368
  type: 'part',
215
369
  part: {
216
370
  type: 'proposal',
217
371
  proposal: {
218
- id: 'confirm-delete',
372
+ id: 'confirm-delete', // passed back to onProposalConfirm
219
373
  title: 'Delete this item?',
220
374
  description: 'This cannot be undone.',
221
- confirmLabel: 'Delete',
222
- cancelLabel: 'Keep',
223
- payload: { itemId: 42 }, // passed back to onProposalConfirm
375
+ confirmLabel: 'Delete', // optional, falls back to labels.proposalConfirm
376
+ cancelLabel: 'Keep', // optional, falls back to labels.proposalCancel
377
+ payload: { itemId: 42 }, // anything — you get it back in onProposalConfirm
224
378
  },
225
379
  },
226
380
  };
227
381
  ```
228
382
 
383
+ **Emitting a table:**
384
+ ```ts
385
+ yield {
386
+ type: 'part',
387
+ part: {
388
+ type: 'table',
389
+ columns: ['Name', 'Amount', 'Status'],
390
+ rows: [
391
+ { Name: 'Item A', Amount: '$1,200', Status: 'Paid' },
392
+ { Name: 'Item B', Amount: '$840', Status: 'Pending' },
393
+ ],
394
+ },
395
+ };
396
+ ```
397
+
229
398
  ---
230
399
 
231
400
  ## Theming
232
401
 
233
- All visual values are CSS custom properties prefixed `--juneau-`. Pass overrides via `JuneauProvider`:
402
+ All visual values are CSS custom properties prefixed `--juneau-`. Override them via `JuneauProvider`:
234
403
 
235
404
  ```tsx
236
405
  <JuneauProvider theme={{
237
- colorPrimary: '#1d4ed8',
238
- colorAccent: '#7c3aed',
239
- radiusLg: '16px',
240
- fontFamily: '"Inter", sans-serif',
406
+ colorPrimary: '#1d4ed8',
407
+ colorAccent: '#7c3aed',
408
+ radiusLg: '16px',
409
+ fontFamily: '"Inter", sans-serif',
241
410
  }}>
242
411
  ```
243
412
 
244
- All theme keys are optionalonly what you provide is overridden.
245
-
246
- | Key | CSS variable | Default |
247
- |---|---|---|
248
- | `colorPrimary` | `--juneau-color-primary` | `#000000` |
249
- | `colorAccent` | `--juneau-color-accent` | `#FF49A4` |
250
- | `colorSurface` | `--juneau-color-surface` | `#FFFFFF` |
251
- | `colorSurfaceRaised` | `--juneau-color-surface-raised` | `#F4F4F6` |
252
- | `colorBorder` | `--juneau-color-border` | `#E8E8EC` |
253
- | `colorTextPrimary` | `--juneau-color-text-primary` | `#000000` |
254
- | `radiusSm` / `radiusMd` / `radiusLg` | `--juneau-radius-*` | `6px / 8px / 12px` |
255
- | `fontFamily` | `--juneau-font-family` | system-ui stack |
413
+ Only keys you specify are overriddeneverything else keeps its default. Overrides are scoped to the provider's subtree so multiple providers with different themes can coexist.
414
+
415
+ ### Full theme reference
416
+
417
+ | Key | CSS variable | Default | Usage |
418
+ |---|---|---|---|
419
+ | `colorPrimary` | `--juneau-color-primary` | `#000000` | Send button, user bubble |
420
+ | `colorPrimaryDark` | `--juneau-color-primary-dark` | `#252528` | Primary hover state |
421
+ | `colorPrimaryLight` | `--juneau-color-primary-light` | `#ECECEC` | Primary tinted backgrounds |
422
+ | `colorAccent` | `--juneau-color-accent` | `#FF49A4` | Header bg, proposal confirm, avatar |
423
+ | `colorAccentDark` | `--juneau-color-accent-dark` | `#FF6BB3` | Accent hover |
424
+ | `colorAccentLight` | `--juneau-color-accent-light` | `#FFF0F7` | Proposal card background |
425
+ | `colorSurface` | `--juneau-color-surface` | `#FFFFFF` | Cards, sidebar, input background |
426
+ | `colorSurfaceRaised` | `--juneau-color-surface-raised` | `#F4F4F6` | Page background, table headers |
427
+ | `colorSurfaceHover` | `--juneau-color-surface-hover` | `#ECECEC` | Row hover |
428
+ | `colorBorder` | `--juneau-color-border` | `#E8E8EC` | Default borders |
429
+ | `colorTextPrimary` | `--juneau-color-text-primary` | `#000000` | Main body text |
430
+ | `colorTextSecondary` | `--juneau-color-text-secondary` | `#474747` | Supporting text |
431
+ | `colorTextMuted` | `--juneau-color-text-muted` | `#474747` | Labels, hints |
432
+ | `colorTextFaint` | `--juneau-color-text-faint` | `#9090A0` | Empty states |
433
+ | `colorTextInverse` | `--juneau-color-text-inverse` | `#FFFFFF` | Text on dark backgrounds |
434
+ | `colorAssistantAvatar` | `--juneau-color-assistant-avatar` | `#FF49A4` | Assistant avatar circle |
435
+ | `radiusSm` | `--juneau-radius-sm` | `6px` | Buttons, small elements |
436
+ | `radiusMd` | `--juneau-radius-md` | `8px` | Inputs, cards |
437
+ | `radiusLg` | `--juneau-radius-lg` | `12px` | Panels, large cards |
438
+ | `fontFamily` | `--juneau-font-family` | system-ui | Font used across all components |
256
439
 
257
440
  ---
258
441
 
@@ -271,47 +454,62 @@ Pass any `Partial<JuneauLabels>` — omitted keys fall back to English:
271
454
  <JuneauProvider labels={{ sidebarTitle: 'Ask AI', sendMessage: 'Send' }}>
272
455
  ```
273
456
 
274
- | Key | Default (EN) |
275
- |---|---|
276
- | `sidebarTitle` | `AI Assistant` |
277
- | `clearConversation` | `Clear conversation` |
278
- | `inputPlaceholder` | `Ask a question… (Enter to send)` |
279
- | `sendMessage` | `Send message` |
280
- | `stopMessage` | `Stop` |
281
- | `emptyStateText` | `How can I help you today?` |
282
- | `emptyStateHint` | `Try: "Show me the data" or "Can you suggest something?"` |
283
- | `proposalConfirm` | `Confirm` |
284
- | `proposalCancel` | `Cancel` |
285
- | `errorDismiss` | `Dismiss` |
286
- | `actionAddFile` | `Add file` |
287
- | `actionQuickActions` | `Quick actions` |
288
- | `actionNew` | `New` |
289
- | `actionHistory` | `History` |
290
- | `actionRules` | `Rules` |
457
+ ### Full label reference
458
+
459
+ | Key | Default (EN) | Used in |
460
+ |---|---|---|
461
+ | `sidebarTitle` | `AI Assistant` | `AiChatHeader` title |
462
+ | `clearConversation` | `Clear conversation` | `AiChatHeader` reset button |
463
+ | `inputPlaceholder` | `Ask a question… (Enter to send)` | `AiInput` textarea |
464
+ | `sendMessage` | `Send message` | `AiInput` send button |
465
+ | `stopMessage` | `Stop` | `AiInput` stop button (while streaming) |
466
+ | `emptyStateText` | `How can I help you today?` | `AiMessageList` empty state |
467
+ | `emptyStateHint` | `Try: "Show me the data" or "Can you suggest something?"` | `AiMessageList` empty state hint |
468
+ | `proposalConfirm` | `Confirm` | `AiProposalCard` fallback confirm label |
469
+ | `proposalCancel` | `Cancel` | `AiProposalCard` fallback cancel label |
470
+ | `errorDismiss` | `Dismiss` | `AiError` dismiss button |
471
+ | `actionAddFile` | `Add file` | `AiInput` toolbar |
472
+ | `actionQuickActions` | `Quick actions` | `AiInput` toolbar |
473
+ | `actionNew` | `New` | `AiInput` toolbar |
474
+ | `actionHistory` | `History` | `AiInput` toolbar |
475
+ | `actionRules` | `Rules` | `AiInput` toolbar |
291
476
 
292
477
  ---
293
478
 
294
479
  ## Custom toolbar actions
295
480
 
296
- ```tsx
297
- import { IconBolt } from './my-icons'; // any ReactNode
481
+ The toolbar buttons left of the send button are fully configurable:
298
482
 
483
+ ```tsx
299
484
  <AiSidebar
300
- adapter={myAdapter}
485
+ adapter={adapter}
301
486
  actions={[
302
- { icon: <IconBolt />, label: 'Quick actions', onClick: () => openMenu() },
487
+ {
488
+ icon: <MyAttachIcon />,
489
+ label: 'Attach file',
490
+ onClick: () => openFilePicker(),
491
+ },
492
+ {
493
+ icon: <MyTemplatesIcon />,
494
+ label: 'Templates',
495
+ onClick: () => openTemplateMenu(),
496
+ },
303
497
  ]}
304
498
  />
305
499
 
306
- // Hide toolbar entirely:
307
- <AiSidebar adapter={myAdapter} actions={[]} />
500
+ // Hide the toolbar entirely:
501
+ <AiSidebar adapter={adapter} actions={[]} />
308
502
  ```
309
503
 
504
+ Each action: `{ icon: ReactNode, label: string, onClick?: () => void }`
505
+
310
506
  ---
311
507
 
312
508
  ## Security
313
509
 
314
- Juneau never handles API keys — all AI calls happen in your adapter. Text parts are rendered via `react-markdown`, which never uses `dangerouslySetInnerHTML` and is safe against XSS by default.
510
+ - **No API keys in the library** — all AI calls happen in your adapter, which calls your backend. Juneau never sees credentials.
511
+ - **XSS-safe markdown** — text parts are rendered via `react-markdown`, which never uses `dangerouslySetInnerHTML`. Malicious model output cannot inject scripts.
512
+ - **No data storage** — Juneau holds conversation state in React memory only. Nothing is persisted or sent anywhere by the library itself.
315
513
 
316
514
  ---
317
515