@june-ai/june-ui 0.2.7 → 0.2.8
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/CHANGELOG.md +44 -2
- package/README.md +633 -400
- package/dist/{chunk-VFZI3SEL.js → chunk-27XM6EVX.js} +8 -8
- package/dist/chunk-3NVO336D.cjs +4314 -0
- package/dist/chunk-G6YPSE6J.cjs +520 -0
- package/dist/chunk-H26BMR5S.js +4314 -0
- package/dist/{document-engine-RKD5SLXR.js → document-engine-BUTK73DW.js} +1 -1
- package/dist/document-engine-GXZH2LNW.cjs +12 -0
- package/dist/entry-core.cjs +148 -0
- package/dist/entry-core.d.cts +1005 -0
- package/dist/entry-core.d.ts +1005 -0
- package/dist/entry-core.js +149 -0
- package/dist/index.cjs +171 -4244
- package/dist/index.d.cts +81 -525
- package/dist/index.d.ts +81 -525
- package/dist/index.js +156 -3604
- package/dist/june.css +94 -0
- package/docs/architecture.md +95 -0
- package/docs/component-registration.md +115 -0
- package/docs/custom-features.md +68 -0
- package/docs/streaming.md +76 -0
- package/docs/theming.md +65 -0
- package/docs/troubleshooting.md +59 -0
- package/package.json +19 -5
package/dist/june.css
CHANGED
|
@@ -488,3 +488,97 @@
|
|
|
488
488
|
.june-doc__canvas { max-height: none; overflow: visible; padding: 0; background: none; }
|
|
489
489
|
.june-doc__sheet { width: auto; box-shadow: none; border: 0; background-image: none; padding: 0; }
|
|
490
490
|
}
|
|
491
|
+
|
|
492
|
+
/* Tool approval (features/tool-approval). Colors come from June theme variables, with fallbacks. */
|
|
493
|
+
.june-approval {
|
|
494
|
+
border: 1px solid var(--june-border, #e5e7eb);
|
|
495
|
+
border-radius: 10px;
|
|
496
|
+
padding: 12px 14px;
|
|
497
|
+
display: grid;
|
|
498
|
+
gap: 8px;
|
|
499
|
+
max-width: 100%;
|
|
500
|
+
min-width: 0;
|
|
501
|
+
}
|
|
502
|
+
.june-approval[data-risk="high"] {
|
|
503
|
+
border-color: var(--june-danger, #b91c1c);
|
|
504
|
+
}
|
|
505
|
+
.june-approval__header {
|
|
506
|
+
display: flex;
|
|
507
|
+
flex-wrap: wrap;
|
|
508
|
+
align-items: center;
|
|
509
|
+
gap: 6px;
|
|
510
|
+
}
|
|
511
|
+
.june-approval__tool {
|
|
512
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
513
|
+
overflow-wrap: anywhere;
|
|
514
|
+
}
|
|
515
|
+
.june-approval__badge {
|
|
516
|
+
font-size: 0.75em;
|
|
517
|
+
padding: 1px 8px;
|
|
518
|
+
border-radius: 999px;
|
|
519
|
+
border: 1px solid var(--june-border, #e5e7eb);
|
|
520
|
+
color: var(--june-muted, #6b7280);
|
|
521
|
+
}
|
|
522
|
+
.june-approval__badge[data-risk="high"] {
|
|
523
|
+
color: var(--june-danger, #b91c1c);
|
|
524
|
+
border-color: currentColor;
|
|
525
|
+
}
|
|
526
|
+
.june-approval__description,
|
|
527
|
+
.june-approval__status {
|
|
528
|
+
margin: 0;
|
|
529
|
+
color: var(--june-muted, #6b7280);
|
|
530
|
+
}
|
|
531
|
+
.june-approval__args,
|
|
532
|
+
.june-approval__editor {
|
|
533
|
+
margin: 0;
|
|
534
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
535
|
+
font-size: 0.85em;
|
|
536
|
+
max-height: 240px;
|
|
537
|
+
overflow: auto;
|
|
538
|
+
padding: 8px;
|
|
539
|
+
border-radius: 6px;
|
|
540
|
+
border: 1px solid var(--june-border, #e5e7eb);
|
|
541
|
+
white-space: pre-wrap;
|
|
542
|
+
overflow-wrap: anywhere;
|
|
543
|
+
}
|
|
544
|
+
.june-approval__editor {
|
|
545
|
+
width: 100%;
|
|
546
|
+
box-sizing: border-box;
|
|
547
|
+
background: transparent;
|
|
548
|
+
color: inherit;
|
|
549
|
+
resize: vertical;
|
|
550
|
+
}
|
|
551
|
+
.june-approval__actions {
|
|
552
|
+
display: flex;
|
|
553
|
+
flex-wrap: wrap;
|
|
554
|
+
gap: 8px;
|
|
555
|
+
}
|
|
556
|
+
.june-approval__button {
|
|
557
|
+
font: inherit;
|
|
558
|
+
padding: 4px 12px;
|
|
559
|
+
border-radius: 6px;
|
|
560
|
+
border: 1px solid var(--june-border, #e5e7eb);
|
|
561
|
+
background: transparent;
|
|
562
|
+
color: inherit;
|
|
563
|
+
cursor: pointer;
|
|
564
|
+
}
|
|
565
|
+
.june-approval__button[data-variant="primary"] {
|
|
566
|
+
border-color: var(--june-accent, #4f46e5);
|
|
567
|
+
background: var(--june-accent, #4f46e5);
|
|
568
|
+
color: #fff;
|
|
569
|
+
}
|
|
570
|
+
.june-approval__button[data-variant="danger"] {
|
|
571
|
+
color: var(--june-danger, #b91c1c);
|
|
572
|
+
}
|
|
573
|
+
.june-approval__button:disabled {
|
|
574
|
+
opacity: 0.5;
|
|
575
|
+
cursor: not-allowed;
|
|
576
|
+
}
|
|
577
|
+
.june-approval__error {
|
|
578
|
+
margin: 4px 0 0;
|
|
579
|
+
color: var(--june-danger, #b91c1c);
|
|
580
|
+
}
|
|
581
|
+
.june-approval-list {
|
|
582
|
+
display: grid;
|
|
583
|
+
gap: 8px;
|
|
584
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
June UI wraps OpenUI behind a small public API and keeps every concern in its own module. This page explains the folders, how a response flows through them, and the rules that keep them independent.
|
|
4
|
+
|
|
5
|
+
## Layers
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
flowchart TB
|
|
9
|
+
subgraph Public["Public API — src/index.ts, src/entry-core.ts"]
|
|
10
|
+
direction LR
|
|
11
|
+
JuneUI & createJuneUI & JuneUIProvider & defineComponent & useJuneStream
|
|
12
|
+
end
|
|
13
|
+
subgraph Core["core/ — composition"]
|
|
14
|
+
direction LR
|
|
15
|
+
C1[JuneUI] --- C2[createJuneUI] --- C3[JuneUIProvider] --- C4[default-library<br/>defaultRegistry]
|
|
16
|
+
end
|
|
17
|
+
subgraph Engine["engine/ — generic machinery"]
|
|
18
|
+
direction LR
|
|
19
|
+
P[parser<br/>pure text] --- R[renderer<br/>React] --- G[registry<br/>OpenUI library]
|
|
20
|
+
end
|
|
21
|
+
subgraph Features["features/ — independent capabilities"]
|
|
22
|
+
direction LR
|
|
23
|
+
F1[streaming] --- F2[actions] --- F3[document-editor] --- F4[tool-approval]
|
|
24
|
+
end
|
|
25
|
+
A[adapters/langgraph]
|
|
26
|
+
T[themes]
|
|
27
|
+
U[utils/errors]
|
|
28
|
+
Public --> Core --> Engine
|
|
29
|
+
Core --> Features
|
|
30
|
+
Engine --> Features
|
|
31
|
+
Engine --> T
|
|
32
|
+
A --> Features
|
|
33
|
+
Features --> U
|
|
34
|
+
Engine --> U
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Data flow
|
|
38
|
+
|
|
39
|
+
```mermaid
|
|
40
|
+
sequenceDiagram
|
|
41
|
+
participant Agent
|
|
42
|
+
participant Stream as features/streaming
|
|
43
|
+
participant JuneUI as core/JuneUI
|
|
44
|
+
participant Parser as engine/parser
|
|
45
|
+
participant Registry as engine/registry
|
|
46
|
+
participant Renderer as engine/renderer
|
|
47
|
+
participant OpenUI as OpenUI Renderer
|
|
48
|
+
Agent->>Stream: bytes (text, SSE, LangGraph)
|
|
49
|
+
Stream->>JuneUI: response (batched ~1 per frame)
|
|
50
|
+
JuneUI->>Registry: library (instance, provider or default)
|
|
51
|
+
JuneUI->>Renderer: props + library
|
|
52
|
+
Renderer->>Parser: resolveMixedContent(response)
|
|
53
|
+
Parser-->>Renderer: prose segments + one OpenUI program
|
|
54
|
+
Renderer->>OpenUI: program, library, isStreaming
|
|
55
|
+
OpenUI-->>Renderer: components (props validated by Zod)
|
|
56
|
+
Renderer-->>Agent: actions → whitelisted handlers / approval
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
1. **Streaming** decodes bytes safely (multi-byte characters across chunks), applies SSE/JSON/LangGraph extraction, and accumulates text. React state updates are batched to about one per frame.
|
|
60
|
+
2. **Parser** (`resolveMixedContent`) splits prose from statements, keeps a half-typed trailing identifier from flashing as prose, and — after the stream ends — repairs common model mistakes (`normalizeProgram`).
|
|
61
|
+
3. **Registry** supplies the OpenUI library: built-in components plus registered, overridden or removed ones. The reference only changes when the components change, because OpenUI memoizes parsing by library identity.
|
|
62
|
+
4. **Renderer** wraps OpenUI's `Renderer` in the theme root, the runtime context (`useJuneRuntime`), the document bridge and an error boundary, and routes actions and errors.
|
|
63
|
+
5. **OpenUI** parses incrementally, validates arguments against each component's Zod schema, and renders.
|
|
64
|
+
|
|
65
|
+
## Folders
|
|
66
|
+
|
|
67
|
+
| Folder | Owns | Must not |
|
|
68
|
+
| :--- | :--- | :--- |
|
|
69
|
+
| `core/` | `JuneUI`, `createJuneUI`, `JuneUIProvider`, `config.ts` (default props), `default-library.ts` (composition root: which features contribute components), runtime context, shared OpenUI-derived types. | Implement feature logic. |
|
|
70
|
+
| `engine/parser/` | `splitResponse`, `resolveMixedContent`, `normalizeProgram`. | Import React or anything with side effects. |
|
|
71
|
+
| `engine/renderer/` | `JuneRenderer`, `JuneErrorBoundary`, `MarkdownText`. | Fetch or read streams; know document internals (it mounts `DocumentBridge` only). |
|
|
72
|
+
| `engine/registry/` | `defineComponent`, `ComponentRegistry`, `useRegistryLibrary`, `createJuneLibrary`. | Import application or feature components. |
|
|
73
|
+
| `engine/openui-devtools.tsx` | Keeps OpenUI's dev launcher hidden. | — |
|
|
74
|
+
| `features/streaming/` | Stream sources, SSE, `useJuneStream`. | Know about parsing or rendering. |
|
|
75
|
+
| `features/actions/` | `defineJuneAction`, `runAction`. | Render UI. |
|
|
76
|
+
| `features/document-editor/` | Tiptap editor, store, merge, export, instructions, `DocumentBridge` (write-back of edits into the program). | Depend on the chat or the renderer. |
|
|
77
|
+
| `features/tool-approval/` | `ToolApproval`, `ToolApprovalList`, `useToolApproval`. | Depend on a specific agent framework. |
|
|
78
|
+
| `adapters/langgraph/` | Pure translation of LangGraph streams and interrupts. | Hold React state or perform network calls. |
|
|
79
|
+
| `themes/` | Tokens, palettes, `JuneThemeProvider`, theme context, `createTheme`. | Contain component behavior. |
|
|
80
|
+
| `utils/` | `JuneError`. | — |
|
|
81
|
+
| `styles/` | Base CSS; features keep their own CSS next to their code. | — |
|
|
82
|
+
|
|
83
|
+
## Rules (enforced)
|
|
84
|
+
|
|
85
|
+
- **No import cycles** — `npm run check:cycles`.
|
|
86
|
+
- **Public API only through the entries** — `src/entry-core.ts` (no default library) and `src/index.ts` (adds the default library and the `JuneUI` layer). `tests/core/public-api.test.ts` fails if a 0.2.7 export disappears.
|
|
87
|
+
- **`/core` stays light** — `npm run check:exports` fails if `entry-core` statically imports `@openuidev/react-ui`.
|
|
88
|
+
- **One copy of state** — the build uses shared chunks so React contexts and the default document store are identical across `.` and `./core` (checked by `check:exports`).
|
|
89
|
+
|
|
90
|
+
## Why these choices
|
|
91
|
+
|
|
92
|
+
- **Built on OpenUI's own extension API.** The registry uses `defineComponent`, `createLibrary` and `library.extend({ components: { add, override, remove } })`. June invents no parallel component model, so anything OpenUI accepts, June accepts.
|
|
93
|
+
- **Composition root in `core/default-library.ts`.** Features contribute components there; the engine never imports a feature's components. Adding a component-providing feature touches one line of core and nothing in the engine.
|
|
94
|
+
- **`DocumentBridge` instead of document logic in the renderer.** The renderer only mounts a provider; everything about stores, echoes and program rewriting lives in the document feature.
|
|
95
|
+
- **Pure parser.** Splitting and repair are synchronous functions, testable without React and reusable on the server.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Component registration
|
|
2
|
+
|
|
3
|
+
Registering a component makes a React component available to the model through OpenUI Lang. June's registry is a thin, validated layer over OpenUI's own `defineComponent` / `createLibrary` / `library.extend`.
|
|
4
|
+
|
|
5
|
+
## 1. Define
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { z } from "zod/v4";
|
|
9
|
+
import { defineComponent } from "@june-ai/june-ui";
|
|
10
|
+
|
|
11
|
+
export const ProductCard = defineComponent({
|
|
12
|
+
name: "ProductCard", // PascalCase, unique
|
|
13
|
+
description: "A product with price and stock. Use when showing a single product.",
|
|
14
|
+
schema: z.object({ // key order = argument order
|
|
15
|
+
title: z.string().describe("Product name"),
|
|
16
|
+
price: z.number().describe("Price in USD"),
|
|
17
|
+
inStock: z.boolean().optional().describe("Defaults to true"),
|
|
18
|
+
}),
|
|
19
|
+
component: ({ title, price, inStock = true }) => (
|
|
20
|
+
<article>
|
|
21
|
+
<h3>{title}</h3>
|
|
22
|
+
<p>${price.toFixed(2)} {inStock ? "" : "· sold out"}</p>
|
|
23
|
+
</article>
|
|
24
|
+
),
|
|
25
|
+
slots: ["Card"], // optional: may appear inside Card
|
|
26
|
+
metadata: { owner: "catalog-team" }, // optional: for your tooling
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The model writes `p = ProductCard("Desk lamp", 39.5, false)`.
|
|
31
|
+
|
|
32
|
+
### `component` or `render`
|
|
33
|
+
|
|
34
|
+
| Field | Receives | Use when |
|
|
35
|
+
| :--- | :--- | :--- |
|
|
36
|
+
| `component` | The validated props, as plain React props. | Almost always. |
|
|
37
|
+
| `render` | OpenUI's `{ props, renderNode, statementId }`. | You render child nodes (`renderNode(props.children)`) or need the statement id. |
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
const Section = defineComponent({
|
|
41
|
+
name: "Section",
|
|
42
|
+
description: "A titled section containing other components.",
|
|
43
|
+
schema: z.object({ title: z.string(), children: z.array(z.any()) }),
|
|
44
|
+
render: ({ props, renderNode }) => (
|
|
45
|
+
<section>
|
|
46
|
+
<h2>{props.title}</h2>
|
|
47
|
+
{renderNode(props.children)}
|
|
48
|
+
</section>
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Validation at definition time
|
|
54
|
+
|
|
55
|
+
`defineComponent` throws `JuneError("LIBRARY_ERROR")` with a fix in the message when: the name is not PascalCase, the description is empty, the schema is not a Zod 4 object, or not exactly one of `component`/`render` is given.
|
|
56
|
+
|
|
57
|
+
## 2. Register
|
|
58
|
+
|
|
59
|
+
| Where | Code | Scope |
|
|
60
|
+
| :--- | :--- | :--- |
|
|
61
|
+
| Instance (recommended) | `const june = createJuneUI({ components: [ProductCard] })` | `<JuneUI june={june}>` or `<JuneUIProvider june={june}>` |
|
|
62
|
+
| Default registry | `registerComponent(ProductCard)` | Every `<JuneUI>` without `june`/`library` |
|
|
63
|
+
| Standalone registry | `createComponentRegistry({ base, components })` | Pass `registry.library` to `JuneRenderer` (works from `/core`) |
|
|
64
|
+
|
|
65
|
+
`createJuneUI` options:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
createJuneUI({
|
|
69
|
+
components: [ProductCard], // add (names must be new)
|
|
70
|
+
overrides: [MyTextContent], // replace built-ins by name
|
|
71
|
+
remove: ["Modal", "DocumentEditor"],// drop built-ins (not the root)
|
|
72
|
+
baseLibrary: openuiLibrary, // start from another library (default: June's)
|
|
73
|
+
defaults: { theme: "system" }, // default <JuneUI> props
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Runtime changes
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
june.register(NewChart); // add
|
|
81
|
+
june.registry.override(BetterTable); // replace
|
|
82
|
+
june.registry.unregister("NewChart"); // remove / revert an override
|
|
83
|
+
june.registry.reset(); // back to the base library
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each change is atomic: it is validated immediately and, if OpenUI rejects it, the registry is left exactly as before and the error is thrown. Mounted `<JuneUI>` components re-render with the new library (`useRegistryLibrary` subscribes to the registry).
|
|
87
|
+
|
|
88
|
+
### Inspecting
|
|
89
|
+
|
|
90
|
+
`registry.names()`, `registry.has(name)`, `registry.get(name)`, `registry.customNames()`, `registry.getMeta(name)`.
|
|
91
|
+
|
|
92
|
+
## 3. Teach the model
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// server
|
|
96
|
+
import { june } from "./june";
|
|
97
|
+
const system = june.prompt({
|
|
98
|
+
preamble: "You are a shopping assistant.",
|
|
99
|
+
additionalRules: ["Show at most 6 products."],
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`prompt()` is OpenUI's `library.prompt()` for the current components, so the model sees your `description` and every `.describe()` text. Generate it on the server; it can be large.
|
|
104
|
+
|
|
105
|
+
## 4. Render and validate
|
|
106
|
+
|
|
107
|
+
OpenUI parses each statement and validates its arguments against the schema. Invalid statements are not rendered; June reports them through `onJuneError` as `PARSE_ERROR` with OpenUI's error list in `details`. While streaming, partially written statements are held until complete.
|
|
108
|
+
|
|
109
|
+
## Existing OpenUI components
|
|
110
|
+
|
|
111
|
+
Anything made with OpenUI's `defineComponent` (or the 0.2 alias `defineJuneComponent`) can be registered as is — June's definitions *are* OpenUI `DefinedComponent`s with an extra `june` field.
|
|
112
|
+
|
|
113
|
+
## Zod versions
|
|
114
|
+
|
|
115
|
+
OpenUI needs Zod 4 schemas. With Zod 3.25 import from `"zod/v4"`. `slots` clone parent schemas and need Zod 4.1+; on older versions June registers the component without slots and logs a development warning.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Custom features
|
|
2
|
+
|
|
3
|
+
Two audiences: **applications** extending June from the outside (no SDK changes), and **contributors** adding a feature to the SDK.
|
|
4
|
+
|
|
5
|
+
## In your application
|
|
6
|
+
|
|
7
|
+
Everything an app needs is public API. A typical feature is a folder in your app:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
src/june/
|
|
11
|
+
components/
|
|
12
|
+
product-card.tsx # React component + defineComponent(...)
|
|
13
|
+
order-status.tsx
|
|
14
|
+
actions.ts # defineJuneAction(...) map
|
|
15
|
+
index.ts # export const june = createJuneUI({ components, defaults: { actions } })
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// src/june/index.ts
|
|
20
|
+
import { createJuneUI } from "@june-ai/june-ui";
|
|
21
|
+
import { ProductCard } from "./components/product-card";
|
|
22
|
+
import { OrderStatus } from "./components/order-status";
|
|
23
|
+
import { actions } from "./actions";
|
|
24
|
+
|
|
25
|
+
export const june = createJuneUI({
|
|
26
|
+
components: [ProductCard, OrderStatus],
|
|
27
|
+
defaults: { actions, textFormat: "markdown" },
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Building blocks for feature code:
|
|
32
|
+
|
|
33
|
+
| Need | Use |
|
|
34
|
+
| :--- | :--- |
|
|
35
|
+
| Know if the answer is still streaming | `useJuneRuntime().isStreaming` |
|
|
36
|
+
| Current light/dark mode | `useJuneTheme()` |
|
|
37
|
+
| React to buttons and forms | `actions` + `defineJuneAction`, or `onAction` |
|
|
38
|
+
| Ask the user before something risky | `sensitive: true` + `useToolApproval()` |
|
|
39
|
+
| Persist UI state | `initialState` / `onStateUpdate` (forwarded to OpenUI) |
|
|
40
|
+
| Custom data fetching from programs | OpenUI `toolProvider` (proxy to your server) |
|
|
41
|
+
| Teach the model | `june.prompt({ additionalRules, examples })` |
|
|
42
|
+
|
|
43
|
+
## Inside the SDK
|
|
44
|
+
|
|
45
|
+
### Checklist
|
|
46
|
+
|
|
47
|
+
1. **Folder**: `src/features/<name>/` with `types.ts`, components, hooks, optional `<name>.css`, and `index.ts` exporting only the public API.
|
|
48
|
+
2. **Boundaries**: import from `../../utils`, `../../themes`, or another feature's **public types** only. No imports from `core/`. The parser and registry must not import your feature.
|
|
49
|
+
3. **Components for the model**: define them with `defineComponent` inside the feature and add them in `src/core/default-library.ts`, the composition root. The engine is not modified.
|
|
50
|
+
4. **Renderer integration** (rare): if every response needs a provider (like `DocumentBridge`), expose a single component from the feature and mount it in `engine/renderer/june-renderer.tsx`. Keep all logic in the feature.
|
|
51
|
+
5. **CSS**: add the file to `scripts/copy-css.mjs`; use `--june-*` tokens with fallbacks; prefix classes `june-<name>`.
|
|
52
|
+
6. **Exports**: `export * from "./features/<name>"` in `src/entry-core.ts`. Add the names to `NEW_EXPORTS` in `tests/core/public-api.test.ts`.
|
|
53
|
+
7. **Tests** in `tests/features/<name>/`; **docs** in the README and, if large, `docs/`; an example in `examples/` (type-checked by `npm run typecheck:examples`).
|
|
54
|
+
8. `npm run validate`.
|
|
55
|
+
|
|
56
|
+
### Example: how tool approval was added
|
|
57
|
+
|
|
58
|
+
- `features/tool-approval/types.ts` — `ToolApprovalRequest`, `ToolApprovalDecision`, statuses.
|
|
59
|
+
- `tool-approval.tsx` — presentational card, labels, edit-as-JSON.
|
|
60
|
+
- `use-tool-approval.ts` — promise-based state; exposes `confirmAction` matching the actions feature's `ConfirmAction` type, so it plugs into `<JuneUI confirmAction>` without either feature knowing the other's internals.
|
|
61
|
+
- `tool-approval.css` added to the stylesheet build.
|
|
62
|
+
- `adapters/langgraph` maps LangGraph interrupts to `ToolApprovalRequest` — the adapter depends on the feature's types, never the reverse.
|
|
63
|
+
|
|
64
|
+
No change to `core/` or `engine/` was needed.
|
|
65
|
+
|
|
66
|
+
### Adding an adapter
|
|
67
|
+
|
|
68
|
+
Adapters translate an external framework's wire format into June types. Keep them pure (no React state, no network), put them in `src/adapters/<framework>/`, and expose small composable functions (an `extract` for `sseToText`, mappers to June types).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Streaming
|
|
2
|
+
|
|
3
|
+
## How June streams
|
|
4
|
+
|
|
5
|
+
| Stage | Module | Behavior |
|
|
6
|
+
| :--- | :--- | :--- |
|
|
7
|
+
| Transport | `fetchStream` | `fetch` with structured errors: `NETWORK_ERROR` (rejected or non-2xx, `details.status`), `STREAM_ABORTED`, `STREAM_EMPTY`. |
|
|
8
|
+
| Framing | `sseToText` | Parses SSE frames (`data:` lines joined, comments skipped). `event: error` → `STREAM_ERROR`. Stops at `doneSentinel` (`"[DONE]"`, or `null` to wait for close). `extract(data, event)` maps a frame to text. |
|
|
9
|
+
| Decoding | `consumeStream` | UTF-8 decoding that survives multi-byte characters split across chunks; accumulates text; `onText(delta, full)`. |
|
|
10
|
+
| React | `useJuneStream` | Coalesces updates to about one per frame (16 ms), cancels the previous run on `start`, cancels on unmount, ignores late results. |
|
|
11
|
+
| Parsing | `resolveMixedContent` + OpenUI | Splits prose, holds back a half-typed trailing identifier; OpenUI parses statements incrementally and only renders complete ones. |
|
|
12
|
+
|
|
13
|
+
Rendering cost stays low because `JuneUI` memoizes the split per `response` string, OpenUI memoizes by library identity, and handlers passed to OpenUI keep a stable identity.
|
|
14
|
+
|
|
15
|
+
## Recipes
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// Plain text body
|
|
19
|
+
stream.startFetch("/api/generate", { method: "POST", body });
|
|
20
|
+
|
|
21
|
+
// SSE where each data line is text
|
|
22
|
+
stream.startFetch("/api/generate", { method: "POST", body, sse: true });
|
|
23
|
+
|
|
24
|
+
// OpenAI-compatible SSE
|
|
25
|
+
stream.startFetch("/api/proxy", {
|
|
26
|
+
method: "POST",
|
|
27
|
+
body,
|
|
28
|
+
sse: { extract: jsonFieldExtractor("choices.0.delta.content") },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Custom SSE payload
|
|
32
|
+
stream.startFetch("/api/agent", {
|
|
33
|
+
method: "POST",
|
|
34
|
+
body,
|
|
35
|
+
sse: {
|
|
36
|
+
extract: (data, event) => (event === "token" ? JSON.parse(data).text : null),
|
|
37
|
+
doneSentinel: null, // server just closes the stream
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Anything else: WebSocket bridge, SDK async iterator, ReadableStream
|
|
42
|
+
stream.start(async (signal) => myAsyncIterableOfStrings(signal));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## LangGraph
|
|
46
|
+
|
|
47
|
+
The LangGraph adapter reads the SSE produced by the LangGraph server API (`/threads/{id}/runs/stream`, `/runs/stream`):
|
|
48
|
+
|
|
49
|
+
| `stream_mode` | Event | Handling |
|
|
50
|
+
| :--- | :--- | :--- |
|
|
51
|
+
| `messages-tuple` | `messages` → `[chunk, metadata]` | Each AI chunk's text is a delta. Filter with `nodes` (matches `metadata.langgraph_node`). |
|
|
52
|
+
| `messages` | `messages/partial` → `[message…]` | Messages are cumulative; only the new part of each (by id) is emitted. |
|
|
53
|
+
| subgraphs | `messages|<ns>` | Same as above. |
|
|
54
|
+
| anything else | — | Ignored. |
|
|
55
|
+
|
|
56
|
+
Only AI messages are kept; tool and human messages are skipped. Text content blocks (`[{ type: "text", text }]`) are supported.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
stream.startFetch(url, { method: "POST", headers, body, ...langGraphStreamOptions({ nodes: ["agent"] }) });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`langGraphStreamOptions` creates a fresh, stateful extractor: call it for every run.
|
|
63
|
+
|
|
64
|
+
**Interrupts (human in the loop).** If you parse the stream yourself (e.g. with `stream_mode: ["messages-tuple", "updates"]`), `readLangGraphInterrupts(data, event)` returns interrupt values from `updates` events, and `interruptToApprovalRequests(value)` maps the `{ action_requests, review_configs }` shape (LangChain's human-in-the-loop middleware) to `ToolApprovalRequest`s for `useToolApproval().request(...)`. For your own interrupt payloads, build the requests yourself.
|
|
65
|
+
|
|
66
|
+
## Status and errors
|
|
67
|
+
|
|
68
|
+
| `status` | `isStreaming` | `error` | `response` |
|
|
69
|
+
| :--- | :--- | :--- | :--- |
|
|
70
|
+
| `idle` | false | null | `""` |
|
|
71
|
+
| `streaming` | true | null | grows |
|
|
72
|
+
| `done` | false | null | full text |
|
|
73
|
+
| `aborted` | false | null | partial text |
|
|
74
|
+
| `error` | false | `JuneError` | partial text |
|
|
75
|
+
|
|
76
|
+
Pass `isStreaming` to `<JuneUI>` unchanged. While it is true, components can read `useJuneRuntime().isStreaming` to defer expensive work (the document editor shows a placeholder until the stream ends).
|
package/docs/theming.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Theming
|
|
2
|
+
|
|
3
|
+
June's theming has one rule: **inherit by default, opt in to color**. Without a `theme` prop June sets no colors, fonts or `color-scheme`; your app's styles apply.
|
|
4
|
+
|
|
5
|
+
## Modes
|
|
6
|
+
|
|
7
|
+
| `theme` | Effect on `.june-ui-root` |
|
|
8
|
+
| :--- | :--- |
|
|
9
|
+
| omitted | No `data-june-theme`, no palette. `themeVars` still apply. |
|
|
10
|
+
| `"light"` / `"dark"` | `data-june-theme`, palette as CSS variables, `color: var(--june-foreground)`, `color-scheme`. |
|
|
11
|
+
| `"system"` | Same, resolved from `prefers-color-scheme` and updated live (SSR renders light). |
|
|
12
|
+
|
|
13
|
+
## Tokens
|
|
14
|
+
|
|
15
|
+
| Token | Light | Dark |
|
|
16
|
+
| :--- | :--- | :--- |
|
|
17
|
+
| `--june-foreground` | `#111827` | `#f3f4f6` |
|
|
18
|
+
| `--june-muted` | `#6b7280` | `#9ca3af` |
|
|
19
|
+
| `--june-border` | `#e5e7eb` | `#1f2937` |
|
|
20
|
+
| `--june-accent` | `#4f46e5` | `#818cf8` |
|
|
21
|
+
| `--june-danger` | `#b91c1c` | `#f87171` |
|
|
22
|
+
|
|
23
|
+
June's prose, root, tool-approval cards and error alert read these tokens (with fallbacks). The document editor uses its own `--jd-*` variables, which follow the June theme context automatically (`theme="auto"`).
|
|
24
|
+
|
|
25
|
+
## Overrides
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// both modes
|
|
29
|
+
<JuneUI theme="dark" themeVars={{ "--june-accent": "var(--brand)" }} response={r} />
|
|
30
|
+
|
|
31
|
+
// per mode, starting from June's palettes
|
|
32
|
+
const brand = createTheme({
|
|
33
|
+
light: { "--june-accent": "#0ea5e9" },
|
|
34
|
+
dark: { "--june-accent": "#38bdf8" },
|
|
35
|
+
});
|
|
36
|
+
<JuneUI theme="system" themeVars={brand} response={r} />
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`themeVars` are inline custom properties and win over the palette. Any `--name` is passed through, so you can set your own variables for custom components in the same place.
|
|
40
|
+
|
|
41
|
+
## Custom components
|
|
42
|
+
|
|
43
|
+
Read the resolved mode with `useJuneTheme()` and style with the tokens:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
function Badge({ label }: { label: string }) {
|
|
47
|
+
const { theme } = useJuneTheme();
|
|
48
|
+
return <span style={{ color: "var(--june-accent)", border: "1px solid var(--june-border)" }} data-mode={theme}>{label}</span>;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Keep styling in CSS or tokens; keep logic in the component. Never hard-code a palette inside a registered component.
|
|
53
|
+
|
|
54
|
+
## OpenUI components
|
|
55
|
+
|
|
56
|
+
Cards, charts, inputs and tables come from `@openuidev/react-ui` and use OpenUI's tokens from `components.css`. Restyle them with OpenUI's `ThemeProvider` (wrap your app), pointing its tokens at your design system. June's `theme` prop does not replace it.
|
|
57
|
+
|
|
58
|
+
## Global setup
|
|
59
|
+
|
|
60
|
+
Set a theme once for all messages:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
const june = createJuneUI({ defaults: { theme: "system", themeVars: brand } });
|
|
64
|
+
<JuneUIProvider june={june}>…</JuneUIProvider>
|
|
65
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Start by passing `onJuneError={(e) => console.log(e.code, e.message, e.details)}` — most problems show up there.
|
|
4
|
+
|
|
5
|
+
## Nothing renders
|
|
6
|
+
|
|
7
|
+
| Check | Why |
|
|
8
|
+
| :--- | :--- |
|
|
9
|
+
| `response` contains a `root = …` statement | OpenUI draws only what `root` references. |
|
|
10
|
+
| `mixedContent` is not `false` for prose | With it off, prose is handed to OpenUI and ignored. |
|
|
11
|
+
| `onJuneError` shows `PARSE_ERROR` | A statement is invalid; `details` lists OpenUI's errors (often wrong argument order or type). |
|
|
12
|
+
| `emptyFallback` / `loadingFallback` | Shown when there is no UI and no prose. |
|
|
13
|
+
|
|
14
|
+
## Custom component issues
|
|
15
|
+
|
|
16
|
+
| Symptom | Fix |
|
|
17
|
+
| :--- | :--- |
|
|
18
|
+
| `LIBRARY_ERROR: Component name … is invalid` | Use PascalCase: `ProductCard`. |
|
|
19
|
+
| `… needs a Zod 4 object schema` | `import { z } from "zod/v4"` (Zod 3.25) or upgrade to Zod 4. |
|
|
20
|
+
| `… already registered` | Name taken by a built-in or an earlier registration. Rename, or `{ override: true }`. Often caused by registering inside a component body: move it to module scope. |
|
|
21
|
+
| Component missing in `<JuneUI>` | It was registered on a different instance. `createJuneUI` instances are isolated from `registerComponent`. Check `june.registry.has("Name")`. |
|
|
22
|
+
| Arguments land in the wrong props | Positional arguments follow the schema's key order. |
|
|
23
|
+
| Model ignores the component | Regenerate the system prompt from the same instance (`june.prompt()`); improve `description` and `.describe()` texts. |
|
|
24
|
+
| Dev warning: slots skipped | `slots` need Zod 4.1+. The component still works at the top level. |
|
|
25
|
+
|
|
26
|
+
## Streaming issues
|
|
27
|
+
|
|
28
|
+
| Symptom | Fix |
|
|
29
|
+
| :--- | :--- |
|
|
30
|
+
| UI appears only at the end | The backend buffers. Disable proxy buffering (e.g. `X-Accel-Buffering: no`), flush per token, avoid compression middleware on the stream. |
|
|
31
|
+
| Raw `data: {...}` text in the UI | Use `sse: true` or an `extract` function. |
|
|
32
|
+
| Stream never finishes | The server doesn't send `[DONE]` and doesn't close; send it, or close the response. |
|
|
33
|
+
| `NETWORK_ERROR` with `details.status` | The HTTP call failed; check the backend. |
|
|
34
|
+
| `STREAM_EMPTY` | The body was empty. Use `allowEmpty: true` if that is valid. |
|
|
35
|
+
| LangGraph shows nothing | Use `stream_mode: ["messages-tuple"]` (or `"messages"`), and check the `nodes` filter matches your graph node names. |
|
|
36
|
+
|
|
37
|
+
## Performance
|
|
38
|
+
|
|
39
|
+
- Create `createJuneUI(...)`, libraries, `actions` maps and definitions at **module scope**; memoize `documents` and `themeVars` objects. A new library object every render forces OpenUI to re-parse.
|
|
40
|
+
- Use `useJuneRuntime().isStreaming` in heavy custom components to defer work until the stream ends.
|
|
41
|
+
- For long chats, render finished messages with `isStreaming={false}` so they are parsed once.
|
|
42
|
+
|
|
43
|
+
## Styling
|
|
44
|
+
|
|
45
|
+
| Symptom | Fix |
|
|
46
|
+
| :--- | :--- |
|
|
47
|
+
| Unstyled cards and charts | Import `@openuidev/react-ui/components.css`. |
|
|
48
|
+
| Unstyled document editor or approval cards | Import `@june-ai/june-ui/styles.css`. |
|
|
49
|
+
| Colors don't change with `themeVars` | Token names start with `--`; per-mode sets must be `{ light, dark }`. |
|
|
50
|
+
| Text color ignores the theme | Set `theme`; without it June inherits your page on purpose. |
|
|
51
|
+
|
|
52
|
+
## Build and module resolution
|
|
53
|
+
|
|
54
|
+
| Symptom | Fix |
|
|
55
|
+
| :--- | :--- |
|
|
56
|
+
| `Cannot find module '@june-ai/june-ui/core'` | Use `moduleResolution: "bundler"` or `"node16"`; upgrade from 0.2.7, where `/core` was not built. |
|
|
57
|
+
| Hooks error in Next.js | Render June from a `"use client"` file. |
|
|
58
|
+
| Two copies of React | Ensure a single `react` / `react-dom` (`npm ls react`). |
|
|
59
|
+
| OpenUI's devtools logo appears | Import June before any other OpenUI package in your entry. |
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@june-ai/june-ui",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "A React
|
|
3
|
+
"version": "0.2.8",
|
|
4
|
+
"description": "A modular React SDK for generative UI: Markdown, streaming OpenUI Lang, custom components and documents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
|
8
8
|
"**/*.css",
|
|
9
9
|
"./dist/block-automount.js",
|
|
10
|
-
"./src/openui
|
|
10
|
+
"./src/engine/openui-devtools.tsx"
|
|
11
11
|
],
|
|
12
12
|
"main": "./dist/index.cjs",
|
|
13
13
|
"module": "./dist/index.js",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
|
+
"docs",
|
|
41
42
|
"README.md",
|
|
42
43
|
"CHANGELOG.md",
|
|
43
44
|
"LICENSE"
|
|
@@ -45,10 +46,14 @@
|
|
|
45
46
|
"scripts": {
|
|
46
47
|
"build": "tsup && node scripts/copy-css.mjs && node scripts/prepare-devtools-block.mjs",
|
|
47
48
|
"typecheck": "tsc --noEmit",
|
|
49
|
+
"typecheck:examples": "tsc --noEmit -p tsconfig.examples.json",
|
|
48
50
|
"test": "vitest run",
|
|
49
51
|
"test:watch": "vitest",
|
|
52
|
+
"check:cycles": "node scripts/check-cycles.mjs",
|
|
53
|
+
"check:exports": "node scripts/check-exports.mjs",
|
|
50
54
|
"pack:check": "npm pack --dry-run",
|
|
51
|
-
"
|
|
55
|
+
"validate": "npm run typecheck && npm run typecheck:examples && npm run check:cycles && npm test && npm run build && npm run check:exports",
|
|
56
|
+
"prepublishOnly": "npm run validate"
|
|
52
57
|
},
|
|
53
58
|
"peerDependencies": {
|
|
54
59
|
"@openuidev/react-headless": ">=0.14.0 <0.19.0",
|
|
@@ -91,5 +96,14 @@
|
|
|
91
96
|
"vitest": "^3.0.0",
|
|
92
97
|
"zod": "^4.0.0",
|
|
93
98
|
"zustand": "^4.5.5"
|
|
94
|
-
}
|
|
99
|
+
},
|
|
100
|
+
"keywords": [
|
|
101
|
+
"react",
|
|
102
|
+
"openui",
|
|
103
|
+
"generative-ui",
|
|
104
|
+
"llm",
|
|
105
|
+
"streaming",
|
|
106
|
+
"langgraph",
|
|
107
|
+
"sdk"
|
|
108
|
+
]
|
|
95
109
|
}
|