juneau 0.1.0 → 0.1.3
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 +394 -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/components/AiChat/AiChat.d.ts +2 -1
- package/dist/components/AiChat/AiChat.d.ts.map +1 -1
- package/dist/components/AiMessageList/AiMessageList.d.ts +3 -1
- package/dist/components/AiMessageList/AiMessageList.d.ts.map +1 -1
- package/dist/components/AiSidebar/AiSidebar.d.ts.map +1 -1
- package/dist/components/AiTypingIndicator/AiTypingIndicator.d.ts +6 -1
- package/dist/components/AiTypingIndicator/AiTypingIndicator.d.ts.map +1 -1
- package/dist/core/stream.d.ts +16 -1
- package/dist/core/stream.d.ts.map +1 -1
- package/dist/hooks/useAiChat.d.ts +6 -0
- package/dist/hooks/useAiChat.d.ts.map +1 -1
- package/dist/i18n/locales/cs.d.ts.map +1 -1
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/types.d.ts +2 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.cjs +39 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3459 -3292
- package/dist/index.js.map +1 -1
- package/dist/style.css +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
|
|
48
|
+
```
|
|
49
|
+
|
|
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
|
+
});
|
|
44
139
|
```
|
|
45
140
|
|
|
46
|
-
|
|
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,218 @@ 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 (`sendGreeting`):**
|
|
332
|
+
|
|
333
|
+
`sendGreeting(hint)` sends the hint as a `system` role message to the adapter and streams the response as an assistant-only message — no user bubble is added to the conversation. Perfect for page-load summaries where the AI speaks first.
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
// In your component:
|
|
337
|
+
const chat = useAiChat({ adapter });
|
|
338
|
+
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
chat.sendGreeting(
|
|
341
|
+
'The user is viewing the Invoices page. ' +
|
|
342
|
+
'Briefly introduce what you can help with on this page.'
|
|
343
|
+
);
|
|
344
|
+
}, []);
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The hint arrives in your adapter as `input.messages[0]` with `role: 'system'`:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
// In your adapter:
|
|
351
|
+
async *sendMessage({ messages }) {
|
|
352
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
353
|
+
const hint = systemMsg ? getMessageText(systemMsg) : '';
|
|
354
|
+
// → "The user is viewing the Invoices page. Briefly introduce…"
|
|
355
|
+
|
|
356
|
+
// Forward to your backend / OpenAI system prompt:
|
|
357
|
+
const res = await fetch('/api/chat', {
|
|
358
|
+
method: 'POST',
|
|
359
|
+
body: JSON.stringify({
|
|
360
|
+
messages: [{ role: 'system', content: hint }],
|
|
361
|
+
stream: true,
|
|
362
|
+
}),
|
|
363
|
+
});
|
|
364
|
+
// …stream response
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Trigger from outside the chat (e.g. clicking a data row):**
|
|
369
|
+
```ts
|
|
370
|
+
chat.sendMessageWithText(`Summarise order #${order.id} for me`);
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Pass page context to every request:**
|
|
374
|
+
```tsx
|
|
375
|
+
<AiSidebar
|
|
376
|
+
adapter={adapter}
|
|
377
|
+
context={{ pageId: 'invoices', entityId: invoice.id, userRole: 'admin' }}
|
|
378
|
+
/>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The `context` object lands in `input.context` inside every `adapter.sendMessage` call — use it to inject page-level data without polluting the message history.
|
|
382
|
+
|
|
383
|
+
**Reading message text in your adapter:**
|
|
384
|
+
|
|
385
|
+
Don't dig into `parts` manually — use the exported `getMessageText` helper:
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { getMessageText } from 'juneau';
|
|
389
|
+
|
|
390
|
+
async *sendMessage({ messages }) {
|
|
391
|
+
const lastUser = [...messages].reverse().find(m => m.role === 'user');
|
|
392
|
+
const text = lastUser ? getMessageText(lastUser) : '';
|
|
393
|
+
// → plain string, all text parts concatenated
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Auth — bearer token from React context:**
|
|
398
|
+
```ts
|
|
399
|
+
const adapter = createSseAdapter('/api/chat', {
|
|
400
|
+
getHeaders: async () => ({
|
|
401
|
+
Authorization: `Bearer ${await getAccessToken()}`,
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Auth — session cookie (no extra config needed):**
|
|
407
|
+
|
|
408
|
+
Cookies are sent automatically by `fetch` to same-origin URLs. Just use `createSseAdapter('/api/chat')` with no headers — the browser attaches the session cookie for you.
|
|
409
|
+
|
|
410
|
+
For cross-origin backends, add `credentials: 'include'` by writing a custom adapter:
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
const adapter: AiBackendAdapter = {
|
|
414
|
+
async *sendMessage({ messages }) {
|
|
415
|
+
const res = await fetch('https://api.example.com/chat', {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
credentials: 'include', // sends cookies cross-origin
|
|
418
|
+
headers: { 'Content-Type': 'application/json' },
|
|
419
|
+
body: JSON.stringify({ messages }),
|
|
420
|
+
});
|
|
421
|
+
// …stream response
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Stop button feedback:**
|
|
427
|
+
|
|
428
|
+
When `onStop` is provided, the send button becomes a stop button while streaming. After the user hits stop, `isLoading` goes false immediately and the existing partial response stays visible in the conversation — there's no error state, the stream just ends cleanly where it was cut.
|
|
429
|
+
|
|
197
430
|
---
|
|
198
431
|
|
|
199
432
|
## Message parts
|
|
200
433
|
|
|
201
|
-
Assistant messages are composed of typed **parts**.
|
|
434
|
+
Assistant messages are composed of typed **parts**. Text is streamed chunk by chunk; rich UI blocks are emitted as complete `part` events.
|
|
202
435
|
|
|
203
|
-
| Part type |
|
|
436
|
+
| Part type | Emitted as | Rendered by |
|
|
204
437
|
|---|---|---|
|
|
205
|
-
| `text` |
|
|
206
|
-
| `table` |
|
|
207
|
-
| `proposal` |
|
|
208
|
-
| `error` |
|
|
209
|
-
|
|
210
|
-
Emit a rich part from your adapter:
|
|
438
|
+
| `text` | `{ type: 'text', text: string }` stream events, accumulated | Markdown via `react-markdown` — safe against XSS |
|
|
439
|
+
| `table` | `{ type: 'part', part: { type: 'table', ... } }` | `AiTablePart` |
|
|
440
|
+
| `proposal` | `{ type: 'part', part: { type: 'proposal', ... } }` | `AiProposalCard` |
|
|
441
|
+
| `error` | `{ type: 'error', message: string }` or `{ type: 'part', part: { type: 'error', ... } }` | Inline error in bubble |
|
|
211
442
|
|
|
443
|
+
**Emitting a proposal from your adapter:**
|
|
212
444
|
```ts
|
|
213
445
|
yield {
|
|
214
446
|
type: 'part',
|
|
215
447
|
part: {
|
|
216
448
|
type: 'proposal',
|
|
217
449
|
proposal: {
|
|
218
|
-
id: 'confirm-delete',
|
|
450
|
+
id: 'confirm-delete', // passed back to onProposalConfirm
|
|
219
451
|
title: 'Delete this item?',
|
|
220
452
|
description: 'This cannot be undone.',
|
|
221
|
-
confirmLabel: 'Delete',
|
|
222
|
-
cancelLabel: 'Keep',
|
|
223
|
-
payload: { itemId: 42 },
|
|
453
|
+
confirmLabel: 'Delete', // optional, falls back to labels.proposalConfirm
|
|
454
|
+
cancelLabel: 'Keep', // optional, falls back to labels.proposalCancel
|
|
455
|
+
payload: { itemId: 42 }, // anything — you get it back in onProposalConfirm
|
|
224
456
|
},
|
|
225
457
|
},
|
|
226
458
|
};
|
|
227
459
|
```
|
|
228
460
|
|
|
461
|
+
**Emitting a table:**
|
|
462
|
+
```ts
|
|
463
|
+
yield {
|
|
464
|
+
type: 'part',
|
|
465
|
+
part: {
|
|
466
|
+
type: 'table',
|
|
467
|
+
columns: ['Name', 'Amount', 'Status'],
|
|
468
|
+
rows: [
|
|
469
|
+
{ Name: 'Item A', Amount: '$1,200', Status: 'Paid' },
|
|
470
|
+
{ Name: 'Item B', Amount: '$840', Status: 'Pending' },
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
```
|
|
475
|
+
|
|
229
476
|
---
|
|
230
477
|
|
|
231
478
|
## Theming
|
|
232
479
|
|
|
233
|
-
All visual values are CSS custom properties prefixed `--juneau-`.
|
|
480
|
+
All visual values are CSS custom properties prefixed `--juneau-`. Override them via `JuneauProvider`:
|
|
234
481
|
|
|
235
482
|
```tsx
|
|
236
483
|
<JuneauProvider theme={{
|
|
237
|
-
colorPrimary:
|
|
238
|
-
colorAccent:
|
|
239
|
-
radiusLg:
|
|
240
|
-
fontFamily:
|
|
484
|
+
colorPrimary: '#1d4ed8',
|
|
485
|
+
colorAccent: '#7c3aed',
|
|
486
|
+
radiusLg: '16px',
|
|
487
|
+
fontFamily: '"Inter", sans-serif',
|
|
241
488
|
}}>
|
|
242
489
|
```
|
|
243
490
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
|
249
|
-
|
|
250
|
-
| `
|
|
251
|
-
| `
|
|
252
|
-
| `
|
|
253
|
-
| `
|
|
254
|
-
| `
|
|
255
|
-
| `
|
|
491
|
+
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.
|
|
492
|
+
|
|
493
|
+
### Full theme reference
|
|
494
|
+
|
|
495
|
+
| Key | CSS variable | Default | Usage |
|
|
496
|
+
|---|---|---|---|
|
|
497
|
+
| `colorPrimary` | `--juneau-color-primary` | `#000000` | Send button, user bubble |
|
|
498
|
+
| `colorPrimaryDark` | `--juneau-color-primary-dark` | `#252528` | Primary hover state |
|
|
499
|
+
| `colorPrimaryLight` | `--juneau-color-primary-light` | `#ECECEC` | Primary tinted backgrounds |
|
|
500
|
+
| `colorAccent` | `--juneau-color-accent` | `#FF49A4` | Header bg, proposal confirm, avatar |
|
|
501
|
+
| `colorAccentDark` | `--juneau-color-accent-dark` | `#FF6BB3` | Accent hover |
|
|
502
|
+
| `colorAccentLight` | `--juneau-color-accent-light` | `#FFF0F7` | Proposal card background |
|
|
503
|
+
| `colorSurface` | `--juneau-color-surface` | `#FFFFFF` | Cards, sidebar, input background |
|
|
504
|
+
| `colorSurfaceRaised` | `--juneau-color-surface-raised` | `#F4F4F6` | Page background, table headers |
|
|
505
|
+
| `colorSurfaceHover` | `--juneau-color-surface-hover` | `#ECECEC` | Row hover |
|
|
506
|
+
| `colorBorder` | `--juneau-color-border` | `#E8E8EC` | Default borders |
|
|
507
|
+
| `colorTextPrimary` | `--juneau-color-text-primary` | `#000000` | Main body text |
|
|
508
|
+
| `colorTextSecondary` | `--juneau-color-text-secondary` | `#474747` | Supporting text |
|
|
509
|
+
| `colorTextMuted` | `--juneau-color-text-muted` | `#474747` | Labels, hints |
|
|
510
|
+
| `colorTextFaint` | `--juneau-color-text-faint` | `#9090A0` | Empty states |
|
|
511
|
+
| `colorTextInverse` | `--juneau-color-text-inverse` | `#FFFFFF` | Text on dark backgrounds |
|
|
512
|
+
| `colorAssistantAvatar` | `--juneau-color-assistant-avatar` | `#FF49A4` | Assistant avatar circle |
|
|
513
|
+
| `radiusSm` | `--juneau-radius-sm` | `6px` | Buttons, small elements |
|
|
514
|
+
| `radiusMd` | `--juneau-radius-md` | `8px` | Inputs, cards |
|
|
515
|
+
| `radiusLg` | `--juneau-radius-lg` | `12px` | Panels, large cards |
|
|
516
|
+
| `fontFamily` | `--juneau-font-family` | system-ui | Font used across all components |
|
|
256
517
|
|
|
257
518
|
---
|
|
258
519
|
|
|
@@ -271,47 +532,62 @@ Pass any `Partial<JuneauLabels>` — omitted keys fall back to English:
|
|
|
271
532
|
<JuneauProvider labels={{ sidebarTitle: 'Ask AI', sendMessage: 'Send' }}>
|
|
272
533
|
```
|
|
273
534
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
|
277
|
-
|
|
278
|
-
| `
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
282
|
-
| `
|
|
283
|
-
| `
|
|
284
|
-
| `
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
535
|
+
### Full label reference
|
|
536
|
+
|
|
537
|
+
| Key | Default (EN) | Used in |
|
|
538
|
+
|---|---|---|
|
|
539
|
+
| `sidebarTitle` | `AI Assistant` | `AiChatHeader` title |
|
|
540
|
+
| `clearConversation` | `Clear conversation` | `AiChatHeader` reset button |
|
|
541
|
+
| `inputPlaceholder` | `Ask a question… (Enter to send)` | `AiInput` textarea |
|
|
542
|
+
| `sendMessage` | `Send message` | `AiInput` send button |
|
|
543
|
+
| `stopMessage` | `Stop` | `AiInput` stop button (while streaming) |
|
|
544
|
+
| `emptyStateText` | `How can I help you today?` | `AiMessageList` empty state |
|
|
545
|
+
| `emptyStateHint` | `Try: "Show me the data" or "Can you suggest something?"` | `AiMessageList` empty state hint |
|
|
546
|
+
| `proposalConfirm` | `Confirm` | `AiProposalCard` fallback confirm label |
|
|
547
|
+
| `proposalCancel` | `Cancel` | `AiProposalCard` fallback cancel label |
|
|
548
|
+
| `errorDismiss` | `Dismiss` | `AiError` dismiss button |
|
|
549
|
+
| `actionAddFile` | `Add file` | `AiInput` toolbar |
|
|
550
|
+
| `actionQuickActions` | `Quick actions` | `AiInput` toolbar |
|
|
551
|
+
| `actionNew` | `New` | `AiInput` toolbar |
|
|
552
|
+
| `actionHistory` | `History` | `AiInput` toolbar |
|
|
553
|
+
| `actionRules` | `Rules` | `AiInput` toolbar |
|
|
291
554
|
|
|
292
555
|
---
|
|
293
556
|
|
|
294
557
|
## Custom toolbar actions
|
|
295
558
|
|
|
296
|
-
|
|
297
|
-
import { IconBolt } from './my-icons'; // any ReactNode
|
|
559
|
+
The toolbar buttons left of the send button are fully configurable:
|
|
298
560
|
|
|
561
|
+
```tsx
|
|
299
562
|
<AiSidebar
|
|
300
|
-
adapter={
|
|
563
|
+
adapter={adapter}
|
|
301
564
|
actions={[
|
|
302
|
-
{
|
|
565
|
+
{
|
|
566
|
+
icon: <MyAttachIcon />,
|
|
567
|
+
label: 'Attach file',
|
|
568
|
+
onClick: () => openFilePicker(),
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
icon: <MyTemplatesIcon />,
|
|
572
|
+
label: 'Templates',
|
|
573
|
+
onClick: () => openTemplateMenu(),
|
|
574
|
+
},
|
|
303
575
|
]}
|
|
304
576
|
/>
|
|
305
577
|
|
|
306
|
-
// Hide toolbar entirely:
|
|
307
|
-
<AiSidebar adapter={
|
|
578
|
+
// Hide the toolbar entirely:
|
|
579
|
+
<AiSidebar adapter={adapter} actions={[]} />
|
|
308
580
|
```
|
|
309
581
|
|
|
582
|
+
Each action: `{ icon: ReactNode, label: string, onClick?: () => void }`
|
|
583
|
+
|
|
310
584
|
---
|
|
311
585
|
|
|
312
586
|
## Security
|
|
313
587
|
|
|
314
|
-
|
|
588
|
+
- **No API keys in the library** — all AI calls happen in your adapter, which calls your backend. Juneau never sees credentials.
|
|
589
|
+
- **XSS-safe markdown** — text parts are rendered via `react-markdown`, which never uses `dangerouslySetInnerHTML`. Malicious model output cannot inject scripts.
|
|
590
|
+
- **No data storage** — Juneau holds conversation state in React memory only. Nothing is persisted or sent anywhere by the library itself.
|
|
315
591
|
|
|
316
592
|
---
|
|
317
593
|
|