@june-ai/june-ui 0.2.2 → 0.2.4
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 +598 -31
- package/dist/chunk-HFHRSNIL.js +174 -0
- package/dist/document-engine-C6ILFMRP.js +12 -0
- package/dist/index.cjs +1461 -85
- package/dist/index.d.cts +253 -10
- package/dist/index.d.ts +253 -10
- package/dist/index.js +1199 -61
- package/dist/june.css +119 -1
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -1,66 +1,633 @@
|
|
|
1
1
|
# @june-ai/june-ui
|
|
2
2
|
|
|
3
|
-
React
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
React SDK for rendering [OpenUI Lang](https://openui.com/docs/openui-lang) as the model streams it. June supplies the React component, streaming, themes, registered actions, and error recovery. [OpenUI](https://github.com/thesysdev/openui) parses the language and draws the components.
|
|
4
|
+
|
|
5
|
+
A response can be plain OpenUI Lang, or prose with OpenUI Lang in the middle (including inside ` ``` ` fences). June separates the prose and hands the program to OpenUI.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<JuneUI response={response} isStreaming={isStreaming} onJuneError={console.error} />
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Contents
|
|
12
|
+
|
|
13
|
+
- [Requirements](#requirements)
|
|
14
|
+
- [Install](#install)
|
|
15
|
+
- [Set up a Vite app](#set-up-a-vite-app)
|
|
16
|
+
- [Set up a Next.js app](#set-up-a-nextjs-app)
|
|
17
|
+
- [Render a static response](#render-a-static-response)
|
|
18
|
+
- [Stream from your backend](#stream-from-your-backend)
|
|
19
|
+
- [Server-sent events](#server-sent-events)
|
|
20
|
+
- [Themes](#themes)
|
|
21
|
+
- [Chat actions](#chat-actions)
|
|
22
|
+
- [Registered actions](#registered-actions)
|
|
23
|
+
- [Your own component library](#your-own-component-library)
|
|
24
|
+
- [Errors and fallbacks](#errors-and-fallbacks)
|
|
25
|
+
- [Smaller bundle](#smaller-bundle)
|
|
26
|
+
- [Props](#props)
|
|
27
|
+
- [Streaming reference](#streaming-reference)
|
|
28
|
+
- [Theme tokens](#theme-tokens)
|
|
29
|
+
- [Errors](#errors)
|
|
30
|
+
- [Security](#security)
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
| Package | Range |
|
|
35
|
+
| :--- | :--- |
|
|
36
|
+
| `react` | `^18.3.1` or `^19.0.0` |
|
|
37
|
+
| `react-dom` | `^18.0.0` or `^19.0.0` |
|
|
38
|
+
| `@openuidev/react-lang` | `>=1.0.0 <2.0.0` |
|
|
39
|
+
| `@openuidev/react-headless` | `>=0.14.0 <0.19.0` |
|
|
40
|
+
| `zod` | `^3.25.0` or `^4.0.0` |
|
|
41
|
+
| `zustand` | `^4.5.5` |
|
|
42
|
+
|
|
43
|
+
`@openuidev/react-ui` is a direct dependency. You do not install it yourself. Component styles still have to be imported once (see below).
|
|
44
|
+
|
|
45
|
+
The browser calls your backend. The backend calls the model. Do not put model API keys in the client.
|
|
6
46
|
|
|
7
47
|
## Install
|
|
8
48
|
|
|
9
49
|
```bash
|
|
10
|
-
npm install @june-ai/june-ui
|
|
50
|
+
npm install @june-ai/june-ui @openuidev/react-lang @openuidev/react-headless zod zustand
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`react` and `react-dom` are already in a Vite or Next.js app. pnpm and yarn use the same package names.
|
|
54
|
+
|
|
55
|
+
Import both stylesheets once, at the app entry:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import "@openuidev/react-ui/components.css";
|
|
59
|
+
import "@june-ai/june-ui/styles.css";
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`components.css` is the OpenUI component library (cards, forms, charts, tables). `styles.css` is June's root layout and, when you opt into a theme, its text color.
|
|
63
|
+
|
|
64
|
+
## Set up a Vite app
|
|
65
|
+
|
|
66
|
+
### 1. Create the project
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm create vite@latest my-june-app -- --template react-ts
|
|
70
|
+
cd my-june-app
|
|
71
|
+
npm install
|
|
72
|
+
npm install @june-ai/june-ui @openuidev/react-lang @openuidev/react-headless zod zustand
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2. Import styles
|
|
76
|
+
|
|
77
|
+
`src/main.tsx`:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { StrictMode } from "react";
|
|
81
|
+
import { createRoot } from "react-dom/client";
|
|
82
|
+
import "@openuidev/react-ui/components.css";
|
|
83
|
+
import "@june-ai/june-ui/styles.css";
|
|
84
|
+
import App from "./App";
|
|
85
|
+
|
|
86
|
+
createRoot(document.getElementById("root")!).render(
|
|
87
|
+
<StrictMode>
|
|
88
|
+
<App />
|
|
89
|
+
</StrictMode>,
|
|
90
|
+
);
|
|
11
91
|
```
|
|
12
92
|
|
|
13
|
-
|
|
93
|
+
### 3. Point `/api` at your backend during development
|
|
94
|
+
|
|
95
|
+
`vite.config.ts`:
|
|
14
96
|
|
|
15
97
|
```ts
|
|
16
|
-
import
|
|
17
|
-
import "@
|
|
98
|
+
import { defineConfig } from "vite";
|
|
99
|
+
import react from "@vitejs/plugin-react";
|
|
100
|
+
|
|
101
|
+
export default defineConfig({
|
|
102
|
+
plugins: [react()],
|
|
103
|
+
server: {
|
|
104
|
+
proxy: { "/api": "http://localhost:8000" },
|
|
105
|
+
},
|
|
106
|
+
});
|
|
18
107
|
```
|
|
19
108
|
|
|
20
|
-
|
|
109
|
+
### 4. Render
|
|
110
|
+
|
|
111
|
+
`src/App.tsx`:
|
|
21
112
|
|
|
22
113
|
```tsx
|
|
23
114
|
import { JuneUI } from "@june-ai/june-ui";
|
|
24
115
|
|
|
25
|
-
|
|
116
|
+
const response = `root = Stack([title])
|
|
117
|
+
title = TextContent("Hello", "large-heavy")`;
|
|
118
|
+
|
|
119
|
+
export default function App() {
|
|
120
|
+
return <JuneUI response={response} isStreaming={false} />;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm run dev
|
|
26
126
|
```
|
|
27
127
|
|
|
28
|
-
|
|
128
|
+
Open the local URL Vite prints. The page shows the text "Hello".
|
|
129
|
+
|
|
130
|
+
## Set up a Next.js app
|
|
131
|
+
|
|
132
|
+
June uses hooks and the browser, so the component that renders `JuneUI` is a Client Component.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx create-next-app@latest my-june-app --ts --app --eslint
|
|
136
|
+
cd my-june-app
|
|
137
|
+
npm install @june-ai/june-ui @openuidev/react-lang @openuidev/react-headless zod zustand
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`app/june-preview.tsx`:
|
|
29
141
|
|
|
30
142
|
```tsx
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
143
|
+
"use client";
|
|
144
|
+
|
|
145
|
+
import "@openuidev/react-ui/components.css";
|
|
146
|
+
import "@june-ai/june-ui/styles.css";
|
|
147
|
+
import { JuneUI } from "@june-ai/june-ui";
|
|
148
|
+
|
|
149
|
+
const response = `root = Stack([title])
|
|
150
|
+
title = TextContent("Hello", "large-heavy")`;
|
|
151
|
+
|
|
152
|
+
export function JunePreview() {
|
|
153
|
+
return <JuneUI response={response} isStreaming={false} />;
|
|
154
|
+
}
|
|
34
155
|
```
|
|
35
156
|
|
|
36
|
-
|
|
157
|
+
`app/page.tsx`:
|
|
37
158
|
|
|
38
159
|
```tsx
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
160
|
+
import { JunePreview } from "./june-preview";
|
|
161
|
+
|
|
162
|
+
export default function Page() {
|
|
163
|
+
return <JunePreview />;
|
|
164
|
+
}
|
|
42
165
|
```
|
|
43
166
|
|
|
44
|
-
|
|
167
|
+
Import the two CSS files from one client module. Importing them again from other routes is harmless, but one import is enough for the app.
|
|
168
|
+
|
|
169
|
+
## Render a static response
|
|
170
|
+
|
|
171
|
+
`response` is the full string. `null` or `""` renders nothing unless you pass `emptyFallback`.
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
import { JuneUI } from "@june-ai/june-ui";
|
|
175
|
+
|
|
176
|
+
export function Dashboard() {
|
|
177
|
+
return (
|
|
178
|
+
<JuneUI
|
|
179
|
+
response={`root = Stack([title, chart])
|
|
180
|
+
title = TextContent("Q4 Revenue", "large-heavy")
|
|
181
|
+
chart = BarChart(labels, [series])
|
|
182
|
+
labels = ["Oct", "Nov", "Dec"]
|
|
183
|
+
series = Series("Product A", [120, 150, 180])`}
|
|
184
|
+
isStreaming={false}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Prose around the program is rendered as paragraphs. This is the default (`mixedContent`):
|
|
45
191
|
|
|
46
192
|
```tsx
|
|
47
|
-
|
|
48
|
-
|
|
193
|
+
<JuneUI
|
|
194
|
+
response={`Sales rose this quarter.
|
|
195
|
+
|
|
196
|
+
root = Stack([title])
|
|
197
|
+
title = TextContent("Sales", "large-heavy")
|
|
198
|
+
|
|
199
|
+
Compared with last year.`}
|
|
200
|
+
/>
|
|
49
201
|
```
|
|
50
|
-
Model output never executes code. Only actions you registered by name can run, and sensitive
|
|
51
|
-
actions are denied unless confirmed.
|
|
52
202
|
|
|
53
|
-
|
|
203
|
+
Pass `mixedContent={false}` when the string is already pure OpenUI Lang and must be given to OpenUI unchanged. A sentence with no `name = ...` statement will not render in that mode.
|
|
54
204
|
|
|
55
|
-
|
|
56
|
-
import OpenUI's built-in component library.
|
|
205
|
+
`isStreaming` tells OpenUI the program is still arriving. Leave it `true` until the stream finishes so a half-written statement is not treated as the final UI. Forms stay non-interactive while it is `true`.
|
|
57
206
|
|
|
58
|
-
|
|
207
|
+
A few shapes the built-in library does not draw directly are rewritten before render, and only when `mixedContent` is on and the stream has finished:
|
|
59
208
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
209
|
+
- `Grid([a, b])` becomes a stack of those children.
|
|
210
|
+
- `Card("Total Revenue", "$43,000")` becomes a title-and-value card.
|
|
211
|
+
- A chart or table that was declared but left off `root` is attached so it still appears.
|
|
63
212
|
|
|
64
|
-
|
|
213
|
+
A program that is already valid is left as it was.
|
|
214
|
+
|
|
215
|
+
## Stream from your backend
|
|
216
|
+
|
|
217
|
+
`useJuneStream` collects chunks into one string, about once per frame. Starting a new stream cancels the one in progress. Unmount cancels it too.
|
|
218
|
+
|
|
219
|
+
The backend should write the OpenUI Lang (or prose plus OpenUI Lang) as a plain text body. Chunked `fetch` is enough. No special framing.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import { JuneUI, useJuneStream } from "@june-ai/june-ui";
|
|
223
|
+
import { useState } from "react";
|
|
224
|
+
|
|
225
|
+
export function Generator() {
|
|
226
|
+
const [prompt, setPrompt] = useState("Show a signup form");
|
|
227
|
+
const stream = useJuneStream({
|
|
228
|
+
onComplete: (text) => console.log("finished", text.length),
|
|
229
|
+
onError: (error) => console.error(error.code, error.message),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const send = () => {
|
|
233
|
+
void stream.startFetch("/api/generate", {
|
|
234
|
+
method: "POST",
|
|
235
|
+
headers: { "Content-Type": "application/json" },
|
|
236
|
+
body: JSON.stringify({ prompt }),
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div>
|
|
242
|
+
<input value={prompt} onChange={(e) => setPrompt(e.target.value)} />
|
|
243
|
+
<button type="button" onClick={send} disabled={stream.isStreaming}>
|
|
244
|
+
Generate
|
|
245
|
+
</button>
|
|
246
|
+
<button type="button" onClick={stream.abort} disabled={!stream.isStreaming}>
|
|
247
|
+
Stop
|
|
248
|
+
</button>
|
|
249
|
+
<p>status: {stream.status}</p>
|
|
250
|
+
{stream.error && (
|
|
251
|
+
<p role="alert">
|
|
252
|
+
{stream.error.code}: {stream.error.message}
|
|
253
|
+
</p>
|
|
254
|
+
)}
|
|
255
|
+
<JuneUI response={stream.response} isStreaming={stream.isStreaming} />
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`stream.status` is `"idle"`, `"streaming"`, `"done"`, `"error"`, or `"aborted"`. Stop keeps the text received so far and sets `error` to `null`. A network or HTTP failure sets `error` and also keeps the partial text.
|
|
262
|
+
|
|
263
|
+
`startFetch` returns the full string, or `null` if the run was aborted, superseded, or failed.
|
|
264
|
+
|
|
265
|
+
If you already have a `ReadableStream` or an async iterable, use `start` instead of `startFetch`:
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
await stream.start(async (signal) => {
|
|
269
|
+
const response = await fetch("/api/generate", { method: "POST", body, signal });
|
|
270
|
+
if (!response.ok || !response.body) throw new Error(String(response.status));
|
|
271
|
+
return response.body;
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Prefer `startFetch` unless you need that control. It maps network failures, non-2xx responses, and an empty body to a `JuneError`.
|
|
276
|
+
|
|
277
|
+
## Server-sent events
|
|
278
|
+
|
|
279
|
+
Pass `sse: true` when each event's `data` field is the next piece of text. A `data: [DONE]` frame ends the stream. An `event: error` frame fails it.
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
void stream.startFetch("/api/generate", {
|
|
283
|
+
method: "POST",
|
|
284
|
+
headers: { "Content-Type": "application/json" },
|
|
285
|
+
body: JSON.stringify({ prompt }),
|
|
286
|
+
sse: true,
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
OpenAI-style streams put the token under a JSON path. `jsonFieldExtractor` reads that path and skips frames that do not have a string there:
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
import { jsonFieldExtractor, useJuneStream } from "@june-ai/june-ui";
|
|
294
|
+
|
|
295
|
+
void stream.startFetch("/api/generate", {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: { "Content-Type": "application/json" },
|
|
298
|
+
body: JSON.stringify({ prompt }),
|
|
299
|
+
sse: { extract: jsonFieldExtractor("choices.0.delta.content") },
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`extract` receives the raw `data` string and the event name. Return `""` or `null` to ignore a frame. Set `doneSentinel: null` if the server never sends `[DONE]` and just closes the response.
|
|
304
|
+
|
|
305
|
+
Outside React, the same helpers work in a script or a test:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { consumeStream, fetchStream } from "@june-ai/june-ui";
|
|
309
|
+
|
|
310
|
+
const source = await fetchStream("/api/generate", { method: "POST", body, sse: true });
|
|
311
|
+
const text = await consumeStream(source, {
|
|
312
|
+
onText: (_delta, full) => process.stdout.write(full),
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Themes
|
|
317
|
+
|
|
318
|
+
June does not paint a background, a font, or a radius. With no `theme` prop it also does not set a text color, so the UI inherits the page.
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
<JuneUI response={response} />
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Pass `theme` when you want June's own text colors:
|
|
325
|
+
|
|
326
|
+
| Value | Result |
|
|
327
|
+
| :--- | :--- |
|
|
328
|
+
| omitted | Host color, font, and `color-scheme` are left alone. |
|
|
329
|
+
| `"light"` | Dark text on the assumption of a light page. |
|
|
330
|
+
| `"dark"` | Light text on the assumption of a dark page. |
|
|
331
|
+
| `"system"` | Follows `prefers-color-scheme`, and updates when the OS changes. |
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
<JuneUI response={response} theme="system" />
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`theme` colors June prose (`.june-text`) and the root. Cards, charts, inputs, and tables still use OpenUI's own tokens from `components.css`. To restyle those, wrap the tree in OpenUI's `ThemeProvider` (`@openuidev/react-ui`) and point its tokens at your CSS variables. June's `theme` does not replace that.
|
|
338
|
+
|
|
339
|
+
Override individual tokens with `themeVars`. They are written as inline custom properties and win over the built-in palette:
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
<JuneUI
|
|
343
|
+
response={response}
|
|
344
|
+
theme="system"
|
|
345
|
+
themeVars={{
|
|
346
|
+
"--june-foreground": "var(--text)",
|
|
347
|
+
"--june-accent": "var(--brand)",
|
|
348
|
+
"--june-danger": "#b91c1c",
|
|
349
|
+
}}
|
|
350
|
+
className="assistant-ui"
|
|
351
|
+
/>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`className` and `style` land on the `.june-ui-root` element. `useJuneTheme()` inside that tree returns `{ mode, theme }` where `theme` is the resolved `"light"` or `"dark"`.
|
|
355
|
+
|
|
356
|
+
`lightTheme` and `darkTheme` are the palettes June applies. Read them if you need the same colors elsewhere. Do not mutate them.
|
|
357
|
+
|
|
358
|
+
## Chat actions
|
|
359
|
+
|
|
360
|
+
Buttons and forms call `onAction` with OpenUI's event. June does not run those for you. Two built-in types matter for a chat:
|
|
361
|
+
|
|
362
|
+
| `event.type` | Produced by | What to do |
|
|
363
|
+
| :--- | :--- | :--- |
|
|
364
|
+
| `"continue_conversation"` | `Button("Submit", @ToAssistant("Submit"))` | Send `event.humanFriendlyMessage` as the next user turn. `event.formState` holds the form fields. |
|
|
365
|
+
| `"open_url"` | `Button("Docs", @OpenUrl("https://example.com"))` | Open `event.params.url` after you check the scheme. |
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
<JuneUI
|
|
369
|
+
response={stream.response}
|
|
370
|
+
isStreaming={stream.isStreaming}
|
|
371
|
+
onAction={(event) => {
|
|
372
|
+
if (event.type === "open_url") {
|
|
373
|
+
const url = String(event.params?.url ?? "");
|
|
374
|
+
if (/^https?:\/\//i.test(url)) window.open(url, "_blank", "noopener,noreferrer");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (event.type === "continue_conversation" && event.humanFriendlyMessage) {
|
|
378
|
+
send(event.humanFriendlyMessage);
|
|
379
|
+
}
|
|
380
|
+
}}
|
|
381
|
+
/>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`onAction` runs for every event, including ones you also handle with the `actions` map below.
|
|
385
|
+
|
|
386
|
+
Form values can be restored on the next render with `initialState`, and read as the user types with `onStateUpdate`. Both are forwarded to OpenUI:
|
|
387
|
+
|
|
388
|
+
```tsx
|
|
389
|
+
<JuneUI
|
|
390
|
+
response={response}
|
|
391
|
+
initialState={saved}
|
|
392
|
+
onStateUpdate={(state) => setSaved(state)}
|
|
393
|
+
/>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`Query()` and `Mutation()` in a program call `toolProvider`. Pass `toolProvider={null}` to disable them. If you supply a provider, it must call your backend. A tool that talks to a model or a database from the browser will leak credentials.
|
|
397
|
+
|
|
398
|
+
## Registered actions
|
|
399
|
+
|
|
400
|
+
`onAction` is a callback. The `actions` prop is a whitelist. A handler runs only when `event.type` matches a name you registered. Model text is never evaluated as code.
|
|
401
|
+
|
|
402
|
+
A custom OpenUI action step arrives as `event.type` equal to the action name and `event.params` equal to its parameters. Register that name:
|
|
403
|
+
|
|
404
|
+
```tsx
|
|
405
|
+
import { defineJuneAction, JuneUI } from "@june-ai/june-ui";
|
|
406
|
+
import { z } from "zod";
|
|
407
|
+
|
|
408
|
+
const actions = {
|
|
409
|
+
place_order: defineJuneAction({
|
|
410
|
+
description: "Place an order",
|
|
411
|
+
sensitive: true,
|
|
412
|
+
schema: z.object({
|
|
413
|
+
sku: z.string(),
|
|
414
|
+
qty: z.number().int().positive(),
|
|
415
|
+
}),
|
|
416
|
+
handler: async ({ sku, qty }) => {
|
|
417
|
+
const response = await fetch("/api/orders", {
|
|
418
|
+
method: "POST",
|
|
419
|
+
headers: { "Content-Type": "application/json" },
|
|
420
|
+
body: JSON.stringify({ sku, qty }),
|
|
421
|
+
});
|
|
422
|
+
if (!response.ok) throw new Error(`Order failed (${response.status})`);
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
export function Checkout({ response }: { response: string }) {
|
|
428
|
+
return (
|
|
429
|
+
<JuneUI
|
|
430
|
+
response={response}
|
|
431
|
+
actions={actions}
|
|
432
|
+
confirmAction={({ name, description }) => window.confirm(description ?? name)}
|
|
433
|
+
onJuneError={(error) => console.error(error.code, error.message)}
|
|
434
|
+
/>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Create the map once, at module scope, so its identity stays stable.
|
|
440
|
+
|
|
441
|
+
What the runner does, in order:
|
|
442
|
+
|
|
443
|
+
1. Unknown names, including `"constructor"`, are ignored. Outcome `"unhandled"`.
|
|
444
|
+
2. `schema.safeParse` runs when you passed a schema. Zod objects work. A failed parse reports `ACTION_VALIDATION_ERROR` and does not call the handler.
|
|
445
|
+
3. `sensitive: true`, or a per-action `confirm`, must approve the call. With neither a per-action `confirm` nor the `confirmAction` prop, a sensitive action is rejected (`ACTION_REJECTED`) and the handler does not run. A user declining is also `"rejected"` and is not an error.
|
|
446
|
+
4. The handler receives the parsed payload and `{ name, event }`. A throw becomes `ACTION_ERROR`.
|
|
447
|
+
|
|
448
|
+
`defineJuneAction` throws `LIBRARY_ERROR`'s sibling `ACTION_VALIDATION_ERROR` immediately if `handler` is not a function. That is a setup bug, not a runtime action failure.
|
|
449
|
+
|
|
450
|
+
The default parser reads the name from `type`, then `name`, then `action`, and the payload from `params`, then `payload`, then `data`. Pass `parseActionEvent` if your event shape differs.
|
|
451
|
+
|
|
452
|
+
## Your own component library
|
|
453
|
+
|
|
454
|
+
`JuneUI` uses OpenUI's built-in library (`Stack`, `TextContent`, `Form`, `Table`, `BarChart`, and the rest). Pass `library` to replace it.
|
|
455
|
+
|
|
456
|
+
Build the library once at module scope. `createJuneLibrary` checks that every component has a unique non-empty name, then applies `allow` and `deny`. `allow` lists the only names to keep. `deny` removes names after that. The `root` component must still be present. A bad library throws `LIBRARY_ERROR` when the module loads, which is what you want.
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
import { createJuneLibrary, defineJuneComponent, JuneUI } from "@june-ai/june-ui";
|
|
460
|
+
import { z } from "zod/v4";
|
|
461
|
+
|
|
462
|
+
const Greeting = defineJuneComponent({
|
|
463
|
+
name: "Greeting",
|
|
464
|
+
description: "Greets a person by name",
|
|
465
|
+
props: z.object({
|
|
466
|
+
name: z.string().describe("The person's name"),
|
|
467
|
+
}),
|
|
468
|
+
component: ({ props }) => <p>Hello, {props.name}</p>,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const library = createJuneLibrary({
|
|
472
|
+
root: "Greeting",
|
|
473
|
+
components: [Greeting],
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
export function Custom() {
|
|
477
|
+
return <JuneUI library={library} response={'root = Greeting("Ada")'} />;
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
`defineJuneComponent` is OpenUI's `defineComponent`. Component schemas use Zod 4 (`zod/v4`). Props, functions, and actions follow [OpenUI's library guide](https://openui.com/docs/openui-lang). `createJuneLibrary` adds the name checks and the allow/deny lists on top of `createLibrary`. The `root` name has to be one of the components you pass.
|
|
482
|
+
|
|
483
|
+
To teach the model the built-in components, generate the system prompt on the server from the same library the client renders:
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
import { openuiLibrary } from "@openuidev/react-ui";
|
|
487
|
+
|
|
488
|
+
const system = openuiLibrary.prompt({});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Send `system` with the user prompt from your backend. Keep that call off the client if the prompt is large or if you add private rules to it.
|
|
492
|
+
|
|
493
|
+
## Errors and fallbacks
|
|
494
|
+
|
|
495
|
+
Three channels, for three kinds of failure:
|
|
496
|
+
|
|
497
|
+
| Prop | When |
|
|
498
|
+
| :--- | :--- |
|
|
499
|
+
| `onError` | OpenUI's own list. Called with `[]` when the errors clear. Warnings stay in the list; filter `severity !== "warning"` if you only want failures. |
|
|
500
|
+
| `onJuneError` | A `JuneError`: parse failures (warnings excluded), a render crash, or an action failure. |
|
|
501
|
+
| `errorFallback` | The render crash boundary. The default is a short alert that does not echo the model output. |
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
<JuneUI
|
|
505
|
+
response={stream.response}
|
|
506
|
+
isStreaming={stream.isStreaming}
|
|
507
|
+
loadingFallback={<p>Drawing…</p>}
|
|
508
|
+
emptyFallback={<p>Nothing to show yet.</p>}
|
|
509
|
+
errorFallback={({ error, reset }) => (
|
|
510
|
+
<div role="alert">
|
|
511
|
+
<p>This reply could not be drawn.</p>
|
|
512
|
+
<button type="button" onClick={reset}>
|
|
513
|
+
Try again
|
|
514
|
+
</button>
|
|
515
|
+
</div>
|
|
516
|
+
)}
|
|
517
|
+
onJuneError={(error) => {
|
|
518
|
+
if (error.code === "PARSE_ERROR") console.table(error.details);
|
|
519
|
+
}}
|
|
520
|
+
/>
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
`loadingFallback` shows while `isStreaming` is true and there is no UI yet. `emptyFallback` shows when the stream is finished and there is still no UI and no prose. `resetKeys` clears a caught render error when any value changes. The default key is the `response` string, so the next chunk retries the render.
|
|
524
|
+
|
|
525
|
+
If you pass neither `onError` nor `onJuneError`, parser errors are written with `console.warn`.
|
|
526
|
+
|
|
527
|
+
## Smaller bundle
|
|
528
|
+
|
|
529
|
+
The main entry imports OpenUI's built-in component library, which is large. `@june-ai/june-ui/core` exports the same streaming, actions, themes, and `JuneRenderer`, and does not import that library. You pass `library` yourself.
|
|
530
|
+
|
|
531
|
+
```tsx
|
|
532
|
+
import { JuneRenderer } from "@june-ai/june-ui/core";
|
|
533
|
+
import { library } from "./library";
|
|
534
|
+
import "@openuidev/react-ui/components.css";
|
|
535
|
+
import "@june-ai/june-ui/styles.css";
|
|
536
|
+
|
|
537
|
+
export function View({ response, isStreaming }: { response: string | null; isStreaming: boolean }) {
|
|
538
|
+
return <JuneRenderer library={library} response={response} isStreaming={isStreaming} />;
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
`JuneUI` is `JuneRenderer` with the built-in library as the default. Props are the same except `library` is required on `JuneRenderer`.
|
|
543
|
+
|
|
544
|
+
## Props
|
|
545
|
+
|
|
546
|
+
`JuneUI` and `JuneRenderer` accept the props below. Any other prop of OpenUI's `Renderer` is forwarded and typed from the installed `@openuidev/react-lang`. That includes `onStateUpdate`, `initialState`, `toolProvider`, `queryLoader`, and `onParseResult`.
|
|
547
|
+
|
|
548
|
+
| Prop | Type | Default | |
|
|
549
|
+
| :--- | :--- | :--- | :--- |
|
|
550
|
+
| `response` | `string \| null` | — | Accumulated model output. |
|
|
551
|
+
| `isStreaming` | `boolean` | `false` | The model is still writing. |
|
|
552
|
+
| `library` | OpenUI library | built-in (`JuneUI` only) | Required on `JuneRenderer`. |
|
|
553
|
+
| `mixedContent` | `boolean` | `true` | Draw prose around the program. `false` passes `response` through. |
|
|
554
|
+
| `renderText` | `(text, index) => ReactNode` | `<p className="june-text">` | How a prose segment is drawn. |
|
|
555
|
+
| `theme` | `"light" \| "dark" \| "system"` | omitted | Opt in to June text colors. |
|
|
556
|
+
| `themeVars` | `Record<string, string>` | — | CSS variable overrides. |
|
|
557
|
+
| `className` | `string` | — | Added on `.june-ui-root`. |
|
|
558
|
+
| `style` | `CSSProperties` | — | Inline style on `.june-ui-root`. |
|
|
559
|
+
| `actions` | `JuneActionMap` | — | Whitelist of handlers, keyed by action name. |
|
|
560
|
+
| `confirmAction` | `(args) => boolean \| Promise<boolean>` | — | Approval for sensitive actions. |
|
|
561
|
+
| `parseActionEvent` | `(event) => { name, payload } \| null` | built-in | Override how an event is read. |
|
|
562
|
+
| `onAction` | `(event) => void` | — | Every OpenUI action, before the whitelist. |
|
|
563
|
+
| `onError` | `(errors: unknown[]) => void` | — | Raw OpenUI errors. |
|
|
564
|
+
| `onJuneError` | `(error: JuneError) => void` | — | Parse, render, and action failures. |
|
|
565
|
+
| `loadingFallback` | `ReactNode` | `null` | Shown while streaming with nothing to draw. |
|
|
566
|
+
| `emptyFallback` | `ReactNode` | `null` | Shown when finished with nothing to draw. |
|
|
567
|
+
| `errorFallback` | `ReactNode` or `({ error, reset }) => ReactNode` | generic alert | Render-crash UI. |
|
|
568
|
+
| `resetKeys` | `readonly unknown[]` | `[response]` | Change clears a caught render error. |
|
|
569
|
+
|
|
570
|
+
## Streaming reference
|
|
571
|
+
|
|
572
|
+
`useJuneStream(options?)` returns:
|
|
573
|
+
|
|
574
|
+
| Field | |
|
|
575
|
+
| :--- | :--- |
|
|
576
|
+
| `response` | Text so far. Kept after an error or an abort. |
|
|
577
|
+
| `isStreaming` | `true` only while a run is in progress. |
|
|
578
|
+
| `status` | `"idle" \| "streaming" \| "done" \| "error" \| "aborted"`. |
|
|
579
|
+
| `error` | `JuneError` or `null`. `null` after a clean stop. |
|
|
580
|
+
| `start(source)` | `source` is a `ReadableStream`, an async iterable, or `(signal) =>` either of those. Returns the full text, or `null`. |
|
|
581
|
+
| `startFetch(input, init)` | `fetch` plus the same return. `init.sse` enables SSE parsing. |
|
|
582
|
+
| `abort()` | Cancels the current run. |
|
|
583
|
+
| `reset()` | Cancels and clears back to idle. |
|
|
584
|
+
|
|
585
|
+
Options: `allowEmpty` resolves an empty body to `""` instead of `STREAM_EMPTY`. `onComplete(text)` and `onError(error)` fire for that hook instance.
|
|
586
|
+
|
|
587
|
+
`fetchStream(input, init)` is the non-hook version of the request. It throws `NETWORK_ERROR` when `fetch` rejects or the status is not OK, `STREAM_ABORTED` when the signal already fired, and `STREAM_EMPTY` when the body is missing.
|
|
588
|
+
|
|
589
|
+
`consumeStream(source, { signal, onText, allowEmpty })` decodes the source to one string. `onText(delta, accumulated)` sees every chunk.
|
|
590
|
+
|
|
591
|
+
`sseToText(source, { extract, doneSentinel, signal })` turns an SSE byte stream into an async iterable of strings. The default `doneSentinel` is `"[DONE]"`.
|
|
592
|
+
|
|
593
|
+
## Theme tokens
|
|
594
|
+
|
|
595
|
+
Set on `.june-ui-root` when `theme` is `"light"`, `"dark"`, or `"system"`:
|
|
596
|
+
|
|
597
|
+
| Variable | Light | Dark |
|
|
598
|
+
| :--- | :--- | :--- |
|
|
599
|
+
| `--june-foreground` | `#111827` | `#f3f4f6` |
|
|
600
|
+
| `--june-muted` | `#6b7280` | `#9ca3af` |
|
|
601
|
+
| `--june-border` | `#e5e7eb` | `#1f2937` |
|
|
602
|
+
| `--june-accent` | `#4f46e5` | `#818cf8` |
|
|
603
|
+
| `--june-danger` | `#b91c1c` | `#f87171` |
|
|
604
|
+
|
|
605
|
+
`color` on the root uses `--june-foreground` only when a theme was requested (the element has `data-june-theme="light"` or `"dark"`). `color-scheme` is set to the same value. Fonts and backgrounds stay yours.
|
|
606
|
+
|
|
607
|
+
## Errors
|
|
608
|
+
|
|
609
|
+
Every structured failure is a `JuneError`. `error.code` is one of:
|
|
610
|
+
|
|
611
|
+
| Code | |
|
|
612
|
+
| :--- | :--- |
|
|
613
|
+
| `PARSE_ERROR` | OpenUI reported one or more non-warning errors. `details` is that list. |
|
|
614
|
+
| `RENDER_ERROR` | A component threw. `details.componentStack` is the React stack. |
|
|
615
|
+
| `STREAM_ERROR` | The stream failed, including an SSE `event: error` frame. |
|
|
616
|
+
| `STREAM_ABORTED` | The request was aborted. `useJuneStream` treats this as a stop, not `error`. |
|
|
617
|
+
| `STREAM_EMPTY` | The response had no body, or the body decoded to nothing. |
|
|
618
|
+
| `NETWORK_ERROR` | `fetch` failed, or the status was not OK. `details.status` is set for HTTP errors. |
|
|
619
|
+
| `ACTION_ERROR` | A registered handler threw. |
|
|
620
|
+
| `ACTION_VALIDATION_ERROR` | The payload failed the schema, or `defineJuneAction` was called without a handler. |
|
|
621
|
+
| `ACTION_REJECTED` | A sensitive action had no confirm function. A user's "no" is not this code. |
|
|
622
|
+
| `LIBRARY_ERROR` | `createJuneLibrary` rejected the library. |
|
|
623
|
+
|
|
624
|
+
`isJuneError(value)` narrows an unknown catch. `toJuneError(error, fallbackCode)` wraps anything else.
|
|
625
|
+
|
|
626
|
+
## Security
|
|
65
627
|
|
|
66
|
-
|
|
628
|
+
- The model chooses components and arguments. It does not choose which JavaScript runs. Only names in your `actions` map have handlers.
|
|
629
|
+
- Mark anything that spends money, deletes data, or leaves the account as `sensitive: true`, and pass `confirmAction`.
|
|
630
|
+
- Validate payloads with `schema`. Treat `event.params` as untrusted input even after that.
|
|
631
|
+
- Open URLs only when the scheme is `https:` (or `http:` in local development).
|
|
632
|
+
- Call the model, your database, and MCP tools from the server. `toolProvider` on the client should be a thin proxy to that server.
|
|
633
|
+
- The render boundary does not print the thrown message by default. Keep raw model text out of error UI.
|