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 +316 -118
- package/dist/adapters/createFetchStreamAdapter.d.ts +88 -0
- package/dist/adapters/createFetchStreamAdapter.d.ts.map +1 -0
- package/dist/adapters/createSseAdapter.d.ts +94 -0
- package/dist/adapters/createSseAdapter.d.ts.map +1 -0
- package/dist/index.cjs +39 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3006 -2862
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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={
|
|
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
|
|
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
|
|
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
|
-
|
|
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 }
|
|
41
|
-
| { type: 'part'; part: AiTablePart | AiProposalPart | AiErrorPart }
|
|
42
|
-
| { type: 'done' }
|
|
43
|
-
| { type: 'error'; message: string }
|
|
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
|
-
|
|
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
|
|
156
|
+
import type { AiBackendAdapter } from 'juneau';
|
|
50
157
|
|
|
51
|
-
export const
|
|
52
|
-
async *sendMessage({ messages }
|
|
53
|
-
const
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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={
|
|
233
|
+
adapter={adapter}
|
|
111
234
|
title="AI Assistant"
|
|
112
|
-
onProposalConfirm={(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
|
|
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
|
|
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
|
|
125
|
-
| `onProposalCancel` | `(id) => void` | Called when
|
|
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
|
|
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.
|
|
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
|
-
|
|
258
|
+
The parent is responsible for calling `useAiChat` and passing results down as props.
|
|
136
259
|
|
|
137
260
|
```tsx
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
156
|
-
| `input` | `string` | Current
|
|
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.
|
|
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
|
|
165
|
-
| `actions` | `AiInputAction[]` | Toolbar
|
|
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
|
|
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,
|
|
178
|
-
input,
|
|
179
|
-
setInput,
|
|
180
|
-
sendMessage,
|
|
181
|
-
sendMessageWithText, // (text: string) => Promise<void> — send programmatically
|
|
182
|
-
sendGreeting,
|
|
183
|
-
stop,
|
|
184
|
-
isLoading,
|
|
185
|
-
error,
|
|
186
|
-
reset,
|
|
187
|
-
confirmProposal,
|
|
188
|
-
cancelProposal,
|
|
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,
|
|
192
|
-
onProposalConfirm,
|
|
193
|
-
onProposalCancel,
|
|
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**.
|
|
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 |
|
|
358
|
+
| Part type | Emitted as | Rendered by |
|
|
204
359
|
|---|---|---|
|
|
205
|
-
| `text` |
|
|
206
|
-
| `table` |
|
|
207
|
-
| `proposal` |
|
|
208
|
-
| `error` |
|
|
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 },
|
|
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-`.
|
|
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:
|
|
238
|
-
colorAccent:
|
|
239
|
-
radiusLg:
|
|
240
|
-
fontFamily:
|
|
406
|
+
colorPrimary: '#1d4ed8',
|
|
407
|
+
colorAccent: '#7c3aed',
|
|
408
|
+
radiusLg: '16px',
|
|
409
|
+
fontFamily: '"Inter", sans-serif',
|
|
241
410
|
}}>
|
|
242
411
|
```
|
|
243
412
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
|
249
|
-
|
|
250
|
-
| `
|
|
251
|
-
| `
|
|
252
|
-
| `
|
|
253
|
-
| `
|
|
254
|
-
| `
|
|
255
|
-
| `
|
|
413
|
+
Only keys you specify are overridden — everything 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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
|
277
|
-
|
|
278
|
-
| `
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
282
|
-
| `
|
|
283
|
-
| `
|
|
284
|
-
| `
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
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
|
-
|
|
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={
|
|
485
|
+
adapter={adapter}
|
|
301
486
|
actions={[
|
|
302
|
-
{
|
|
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={
|
|
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
|
-
|
|
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
|
|