@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 CHANGED
@@ -1,66 +1,633 @@
1
1
  # @june-ai/june-ui
2
2
 
3
- React Generative UI SDK built on [OpenUI](https://github.com/thesysdev/openui). June adds streaming
4
- helpers, themes, safe actions, error recovery, and library validation. OpenUI does all parsing and
5
- rendering.
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
- Import styles once in your app entry:
93
+ ### 3. Point `/api` at your backend during development
94
+
95
+ `vite.config.ts`:
14
96
 
15
97
  ```ts
16
- import "@openuidev/react-ui/components.css"; // OpenUI built-in components
17
- import "@june-ai/june-ui/styles.css"; // June theme variables
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
- ## Usage
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
- <JuneUI response={response} isStreaming={isStreaming} theme="system" onJuneError={console.error} />
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
- ### Streaming (works with any backend)
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
- const stream = useJuneStream();
32
- stream.startFetch("/api/generate", { method: "POST", body, sse: false }); // sse: true for SSE
33
- <JuneUI response={stream.response} isStreaming={stream.isStreaming} />
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
- ### Custom library
157
+ `app/page.tsx`:
37
158
 
38
159
  ```tsx
39
- import { createJuneLibrary } from "@june-ai/june-ui";
40
- const library = createJuneLibrary({ root: "Stack", components: [Stack, Card] }); // module scope
41
- <JuneUI library={library} response={response} />
160
+ import { JunePreview } from "./june-preview";
161
+
162
+ export default function Page() {
163
+ return <JunePreview />;
164
+ }
42
165
  ```
43
166
 
44
- ### Actions
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
- const actions = { submit: defineJuneAction({ schema, sensitive: true, handler }) };
48
- <JuneUI actions={actions} confirmAction={({ name }) => window.confirm(name)} ... />
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
- ### Lightweight entry
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
- `@june-ai/june-ui/core` exports `JuneRenderer`, which requires your own `library` and does not
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
- ## OpenUI features
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
- Tool providers, query loaders, state callbacks, and parse-result callbacks are forwarded to
61
- OpenUI's `Renderer` as props. See OpenUI's docs for their behaviour. Never put API keys in the
62
- browser; do tool/MCP calls in your backend.
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
- ## Requirements
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
- React 18.3.1+ or 19, plus the OpenUI peer dependencies listed in package.json.
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.