ngx-json-render 0.8.0 → 0.8.2

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
@@ -8,7 +8,7 @@ Built on `@json-render/core` (the same spec format, expressions, state store, ac
8
8
 
9
9
  This is an Angular **adapter over the official core**, not a second implementation of it — `@json-render/core` is a peer dependency, and the spec your model emits is the same one the React, Vue, Solid and Svelte renderers consume. A catalog and a spec written here move to another framework unchanged.
10
10
 
11
- **[Live demo](https://shteynu.github.io/ngx-json-render/)** — interactive spec (bindings, repeat, confirm, watch), a SpecStream rendering progressively that you can stop mid-generation, and a chat where prose and UI patches arrive in one reply; both streaming tabs replay a recording, or call a real model on your own key ([source](https://github.com/shteynu/ngx-json-render/tree/main/projects/demo)) — or [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/shteynu/ngx-json-render)
11
+ **[Live demo](https://shteynu.github.io/ngx-json-render/)** — opens on a sales dashboard streaming in patch by patch, which you can stop mid-generation; plus a playground, an interactive spec (bindings, repeat, confirm, watch), and a chat where prose and UI patches arrive in one reply. Both streaming tabs replay a recording, or call a real model on your own key ([source](https://github.com/shteynu/ngx-json-render/tree/main/projects/demo)) — or [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/shteynu/ngx-json-render)
12
12
 
13
13
  ![A SpecStream of RFC 6902 patches rendering progressively into an Angular dashboard](https://raw.githubusercontent.com/shteynu/ngx-json-render/main/docs/streaming.gif)
14
14
 
@@ -62,7 +62,7 @@ the Material catalog has one too (`--skill ngx-json-render-material`).
62
62
 
63
63
  Writing a catalog is the honest first step, but you do not have to take it to
64
64
  see a spec render. [`ngx-json-render-material`](https://www.npmjs.com/package/ngx-json-render-material)
65
- ships 28 Angular Material components already registered, so a generated spec
65
+ ships 29 Angular Material components already registered, so a generated spec
66
66
  renders with nothing else wired up:
67
67
 
68
68
  ```bash
@@ -119,7 +119,7 @@ against `@hashbrownai/angular` 0.6.1, as published on 2026-09-30:
119
119
  | What the model emits | A json-render spec, streamed as RFC 6902 patches — the format the React, Vue, Svelte and Solid renderers read, so a catalog and a spec move between frameworks unchanged | Hashbrown's own JSON, generated against a schema built from the components you expose; read by Hashbrown's Angular and React clients |
120
120
  | Server | Anything that streams text: the [AI SDK](https://ai-sdk.dev) in a few lines, or your own endpoint; `catalog.prompt()` writes the system prompt | An [AG-UI](https://docs.ag-ui.com) endpoint, built with Hashbrown's adapters (OpenAI, Anthropic, Google, Azure, Bedrock, Ollama) or your own mapping |
121
121
  | Angular | 20, 21 and 22, each tested in CI | 22 only since 0.6 (0.5 supported 20 and 21) |
122
- | Components | Your own, or 28 ready-made Angular Material ones ([`ngx-json-render-material`](https://www.npmjs.com/package/ngx-json-render-material)) | Your own (`exposeComponent`, UI kits) |
122
+ | Components | Your own, or 29 ready-made Angular Material ones ([`ngx-json-render-material`](https://www.npmjs.com/package/ngx-json-render-material)) | Your own (`exposeComponent`, UI kits) |
123
123
  | Beyond rendering | Rendering-focused: state and two-way bindings in the spec, actions, visibility, repeat, watch, confirmation dialogs | An agent toolkit: client-side tools, a QuickJS sandbox for model-written code, shared agent state, interrupts, streaming Markdown |
124
124
  | License | Apache-2.0 | MIT |
125
125
 
@@ -306,6 +306,40 @@ app.post('/api/generate', async (req, res) => {
306
306
 
307
307
  The patches apply to the `spec` signal as each line arrives, so the UI assembles on screen while the model is still generating — exactly what the [demo's Streaming tab](https://shteynu.github.io/ngx-json-render/) replays. Prefer structured output? `catalog.jsonSchema()` exports a JSON Schema for `streamObject`/tool calls, and `checkSpec(spec, 'strict', { catalog })` checks a finished spec against the catalog, props included — see [Checking what the model produced](#checking-what-the-model-produced) for why that and not `catalog.validate(spec)` alone.
308
308
 
309
+ ### With Genkit
310
+
311
+ On Firebase, or anywhere else [Genkit](https://genkit.dev) already runs, the route has the same shape. `ai.generateStream` streams the model's text, and that text is the JSONL the system prompt asked for, so it goes to the browser unchanged:
312
+
313
+ ```ts
314
+ // server.ts — Express with Genkit and its Google AI plugin
315
+ import express from 'express';
316
+ import { genkit } from 'genkit';
317
+ import { googleAI } from '@genkit-ai/google-genai';
318
+ import { buildUserPrompt } from '@json-render/core';
319
+ import { catalog } from './catalog';
320
+
321
+ const ai = genkit({ plugins: [googleAI()] }); // reads GEMINI_API_KEY
322
+
323
+ const app = express();
324
+ app.use(express.json());
325
+
326
+ app.post('/api/generate', async (req, res) => {
327
+ const { prompt, currentSpec } = req.body; // and `context`, yours to use
328
+
329
+ const { stream } = ai.generateStream({
330
+ model: googleAI.model('gemini-3.5-flash'),
331
+ system: catalog.prompt(),
332
+ prompt: buildUserPrompt({ prompt, currentSpec }), // currentSpec: refine, not restart
333
+ });
334
+
335
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
336
+ for await (const chunk of stream) res.write(chunk.text);
337
+ res.end();
338
+ });
339
+ ```
340
+
341
+ `buildUserPrompt` comes from `@json-render/core`. Given a `currentSpec`, it asks the model for patches to that spec, so `ui.send(prompt, { previousSpec })` refines the UI instead of regenerating it; without one, it adds a reminder of the order to stream in. Another model is another plugin and `model` line, such as `vertexAI` from the same package. Serve the text from a plain route like this one rather than through `expressHandler` from `@genkit-ai/express`: that wraps every chunk in its own server-sent event (`data: {"message": …}`), which `injectUIStream` does not read.
342
+
309
343
  ### Chat + GenUI
310
344
 
311
345
  When the model answers in prose _and_ renders a UI in the same turn, reach for
@@ -346,6 +380,14 @@ readonly msg = jsonRenderMessage(() => this.message().parts);
346
380
  // template: {{ msg.text() }} @if (msg.hasSpec()) { <json-render [spec]="msg.spec()" ... /> }
347
381
  ```
348
382
 
383
+ With `@ai-sdk/angular`, hand that component a copy of the message being
384
+ streamed. The SDK's `Chat` writes every chunk into one message object and
385
+ passes that same object back each time, so a signal input holding it never
386
+ changes, and the message renders empty even after the stream ends.
387
+ [`examples/ai-sdk-chat`](https://github.com/shteynu/ngx-json-render/tree/main/examples/ai-sdk-chat)
388
+ is the whole path, server to rendered message, and makes the copy in
389
+ `app.ts`.
390
+
349
391
  Two things about the parts themselves, both the SDK's semantics rather than
350
392
  this package's, and both silent when you get them wrong:
351
393
 
@@ -536,6 +578,23 @@ The spec renders while the model is still writing the tool call, from the host's
536
578
 
537
579
  The server side is upstream's `@json-render/mcp`, which serves the view as a single HTML resource. [`projects/mcp-app`](https://github.com/shteynu/ngx-json-render/tree/main/projects/mcp-app) is a complete example with the Material catalog: the view, the build that inlines it into one page, and a server that works around two `createMcpApp` problems.
538
580
 
581
+ ## Devtools
582
+
583
+ `ngx-json-render/devtools` is the Angular counterpart of `@json-render/devtools-react` (and its Vue, Svelte and Solid siblings): a floating panel with the spec tree, the live state, every action dispatched, every stream patch with its token usage, and the catalog, plus a picker that finds the element under the pointer. The panel itself is upstream's `@json-render/devtools`, an optional peer. It pins `@json-render/core` exactly, so install the version that matches your core:
584
+
585
+ ```bash
586
+ npm install -D @json-render/devtools@0.21
587
+ ```
588
+
589
+ ```html
590
+ <json-render #renderer [spec]="ui.spec()" [registry]="registry" />
591
+ <json-render-devtools [renderer]="renderer" [catalog]="catalog" />
592
+ ```
593
+
594
+ Angular has no provider above `<json-render>` for the panel to read, so the renderer is passed in by template reference. Streams need no wiring: every `injectUIStream` and `injectChatUI` generation in the app shows up in the Stream tab. Pass `[chat]="chat"` and each assistant reply with a spec becomes a generation you can switch between. `Cmd/Ctrl+Shift+J` toggles the panel (`hotkey`), and `(event)` hands every recorded event to your own logging.
595
+
596
+ It renders nothing outside dev mode, and loads the panel with a dynamic `import()` that a production build removes, so you can leave the tag in your template.
597
+
539
598
  ## Testing
540
599
 
541
600
  `ngx-json-render/testing` is a separate entry point, so nothing in it can reach
@@ -907,6 +966,8 @@ Schema alone (`ngx-json-render/schema`): `schema`, `AngularSchema`, `AngularSpec
907
966
 
908
967
  MCP Apps (`ngx-json-render/mcp`): `injectJsonRenderApp`, `parseSpecFromToolResult`, `messageText`, and the types `JsonRenderApp`, `JsonRenderAppOptions`. Needs the optional peers `@modelcontextprotocol/ext-apps` and `@modelcontextprotocol/sdk`.
909
968
 
969
+ Devtools (`ngx-json-render/devtools`): `JsonRenderDevtools` (`<json-render-devtools>`), and the types `DevtoolsEvent`, `PanelPosition`. Needs the optional peer `@json-render/devtools`.
970
+
910
971
  Testing (`ngx-json-render/testing`): `renderSpec`, `renderComponent`, `recordedTransport`, `specStream`, `usageLine`.
911
972
 
912
973
  Everything from `@json-render/core` (types, `createStateStore`, `nestedToFlat`, prompt builders, spec validators, SpecStream compiler) composes with this package; the most common symbols are re-exported.
@@ -0,0 +1,262 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, output, inject, DestroyRef, isDevMode, afterNextRender, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { registerActionObserver, markDevtoolsActive } from '@json-render/core';
4
+ import { ɵregisterStreamObserver as _registerStreamObserver } from 'ngx-json-render';
5
+
6
+ /**
7
+ * A floating json-render devtools panel for an Angular app — the counterpart
8
+ * of `<JsonRenderDevtools />` in `@json-render/devtools-react`, `-vue`,
9
+ * `-svelte` and `-solid`, with the same tabs (Spec, State, Actions, Stream,
10
+ * Catalog) and the same element picker.
11
+ *
12
+ * Angular has no provider above `<json-render>` to read the renderer from, so
13
+ * the renderer is passed in by template reference. Streams need no wiring:
14
+ * every `injectUIStream` and `injectChatUI` generation in the app reaches the
15
+ * Stream tab.
16
+ *
17
+ * Renders nothing outside dev mode, and loads the panel itself
18
+ * (`@json-render/devtools`) with a dynamic `import()`, so a production build
19
+ * never fetches it.
20
+ *
21
+ * @example
22
+ * ```html
23
+ * <json-render #renderer [spec]="ui.spec()" [registry]="registry" />
24
+ * <json-render-devtools [renderer]="renderer" [catalog]="catalog" />
25
+ * ```
26
+ */
27
+ class JsonRenderDevtools {
28
+ /** The renderer to inspect: its spec, state store and catalog. */
29
+ renderer = input(null, ...(ngDevMode ? [{ debugName: "renderer" }] : /* istanbul ignore next */ []));
30
+ /** Spec to show instead of the renderer's. */
31
+ spec = input(undefined, ...(ngDevMode ? [{ debugName: "spec" }] : /* istanbul ignore next */ []));
32
+ /** Catalog for the Catalog tab, if the renderer was not given one. */
33
+ catalog = input(undefined, ...(ngDevMode ? [{ debugName: "catalog" }] : /* istanbul ignore next */ []));
34
+ /**
35
+ * A chat from `injectChatUI`: every assistant reply with a spec becomes a
36
+ * generation in the Spec tab's switcher.
37
+ */
38
+ chat = input(null, ...(ngDevMode ? [{ debugName: "chat" }] : /* istanbul ignore next */ []));
39
+ initialOpen = input(false, ...(ngDevMode ? [{ debugName: "initialOpen" }] : /* istanbul ignore next */ []));
40
+ position = input('bottom-right', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
41
+ /** Toggle shortcut; `false` disables it. */
42
+ hotkey = input('mod+shift+j', ...(ngDevMode ? [{ debugName: "hotkey" }] : /* istanbul ignore next */ []));
43
+ /** Events kept in the ring buffer. Read once, at creation. */
44
+ bufferSize = input(500, ...(ngDevMode ? [{ debugName: "bufferSize" }] : /* istanbul ignore next */ []));
45
+ /** Pad the page while the panel is open instead of overlaying it. */
46
+ reserveSpace = input(true, ...(ngDevMode ? [{ debugName: "reserveSpace" }] : /* istanbul ignore next */ []));
47
+ /** Let the viewer flip the panel between bottom and right dock. */
48
+ allowDockToggle = input(true, ...(ngDevMode ? [{ debugName: "allowDockToggle" }] : /* istanbul ignore next */ []));
49
+ /** Every event the panel records, for an app's own logging. */
50
+ event = output();
51
+ destroyRef = inject(DestroyRef);
52
+ events = null;
53
+ panel = null;
54
+ destroyed = false;
55
+ constructor() {
56
+ if (!isDevMode())
57
+ return;
58
+ this.destroyRef.onDestroy(() => (this.destroyed = true));
59
+ afterNextRender(() => {
60
+ void this.mount();
61
+ });
62
+ // The panel reads everything through getters, so it only needs telling
63
+ // when to re-read: whenever the spec, the chat or the renderer changes.
64
+ effect(() => {
65
+ this.currentSpec();
66
+ this.chat()?.messages();
67
+ this.renderer();
68
+ untracked(() => this.panel?.refresh());
69
+ });
70
+ // A state write becomes an event, which redraws the State tab — the
71
+ // store has no subscribe of the shape the panel's StateStore expects.
72
+ effect((onCleanup) => {
73
+ const store = this.renderer()?.stateStore;
74
+ if (!store)
75
+ return;
76
+ onCleanup(store.subscribeChanges((changes) => {
77
+ for (const change of changes) {
78
+ this.events?.push({
79
+ kind: 'state-set',
80
+ at: Date.now(),
81
+ path: change.path,
82
+ prev: undefined,
83
+ next: change.value,
84
+ });
85
+ }
86
+ }));
87
+ });
88
+ }
89
+ currentSpec() {
90
+ const override = this.spec();
91
+ if (override !== undefined)
92
+ return override;
93
+ const chat = this.chat();
94
+ if (chat) {
95
+ const replies = chat.messages().filter((m) => m.spec);
96
+ return replies.at(-1)?.spec ?? null;
97
+ }
98
+ return this.renderer()?.spec() ?? null;
99
+ }
100
+ currentCatalog() {
101
+ const own = this.catalog();
102
+ if (own !== undefined)
103
+ return own;
104
+ // The renderer's catalog input is typed structurally; a catalog made with
105
+ // `schema.createCatalog` is a core Catalog, which is what the tab reads.
106
+ const fromRenderer = this.renderer()?.catalog();
107
+ return fromRenderer && 'data' in fromRenderer
108
+ ? fromRenderer
109
+ : null;
110
+ }
111
+ generations() {
112
+ const chat = this.chat();
113
+ if (!chat) {
114
+ const spec = this.currentSpec();
115
+ return spec ? [{ id: 'current', label: 'Current', spec }] : [];
116
+ }
117
+ return chat
118
+ .messages()
119
+ .filter((m) => m.role === 'assistant' && m.spec)
120
+ .map((m, i) => ({
121
+ id: m.id,
122
+ label: `Generation ${i + 1}`,
123
+ spec: m.spec,
124
+ }));
125
+ }
126
+ stateStore() {
127
+ const store = this.renderer()?.stateStore;
128
+ if (!store)
129
+ return null;
130
+ return {
131
+ get: (path) => store.get(path),
132
+ set: (path, value) => store.set(path, value),
133
+ update: (updates) => store.update(updates),
134
+ getSnapshot: () => store.getSnapshot(),
135
+ subscribe: () => () => { },
136
+ };
137
+ }
138
+ async mount() {
139
+ // A production build defines `ngDevMode` as false, so the bundler drops
140
+ // the import below: nothing references the panel chunk, even if esbuild
141
+ // still writes it to disk.
142
+ if (typeof ngDevMode !== 'undefined' && !ngDevMode)
143
+ return;
144
+ const devtools = await import('@json-render/devtools');
145
+ if (this.destroyed)
146
+ return;
147
+ const events = devtools.createEventStore({
148
+ bufferSize: this.bufferSize(),
149
+ });
150
+ this.events = events;
151
+ const unsubscribeEvents = events.subscribe(() => {
152
+ const last = events.snapshot().at(-1);
153
+ if (last)
154
+ this.event.emit(last);
155
+ });
156
+ // Both observers come from the app's own `@json-render/core` and
157
+ // `ngx-json-render`, never through `@json-render/devtools`: the registries
158
+ // are module singletons, and devtools may carry a nested copy of core.
159
+ const unsubscribeActions = registerActionObserver({
160
+ onDispatch: (e) => events.push({
161
+ kind: 'action-dispatched',
162
+ at: e.at,
163
+ id: e.id,
164
+ name: e.name,
165
+ params: e.params,
166
+ }),
167
+ onSettle: (e) => events.push({
168
+ kind: 'action-settled',
169
+ at: e.at,
170
+ id: e.id,
171
+ ok: e.ok,
172
+ durationMs: e.durationMs,
173
+ result: e.result,
174
+ error: e.error !== undefined ? String(e.error) : undefined,
175
+ }),
176
+ });
177
+ const unsubscribeStreams = _registerStreamObserver({
178
+ onStart: () => events.push({
179
+ kind: 'stream-lifecycle',
180
+ at: Date.now(),
181
+ phase: 'start',
182
+ }),
183
+ onPatch: (patch) => events.push({
184
+ kind: 'stream-patch',
185
+ at: Date.now(),
186
+ patch,
187
+ source: 'json',
188
+ }),
189
+ onText: (text) => events.push({ kind: 'stream-text', at: Date.now(), text }),
190
+ onUsage: (usage) => devtools.recordUsage(events, usage),
191
+ onEnd: (ok) => events.push({
192
+ kind: 'stream-lifecycle',
193
+ at: Date.now(),
194
+ phase: 'end',
195
+ ok,
196
+ }),
197
+ });
198
+ const selection = devtools.createSelectionBus();
199
+ this.panel = devtools.createPanel({
200
+ context: {
201
+ events,
202
+ getSpec: () => this.currentSpec(),
203
+ getSpecs: () => this.generations(),
204
+ getCatalog: () => this.currentCatalog(),
205
+ getStateStore: () => this.stateStore(),
206
+ startPicker: (options) => devtools.startPicker(options),
207
+ selection,
208
+ activateTab: () => { },
209
+ },
210
+ tabs: [
211
+ devtools.specTab(),
212
+ devtools.stateTab(),
213
+ devtools.actionsTab(),
214
+ devtools.streamTab(),
215
+ devtools.catalogTab(),
216
+ ],
217
+ initialOpen: this.initialOpen(),
218
+ position: this.position(),
219
+ hotkey: this.hotkey(),
220
+ reserveSpace: this.reserveSpace(),
221
+ allowDockToggle: this.allowDockToggle(),
222
+ });
223
+ const unsubscribeSelection = selection.subscribe((key) => {
224
+ if (key)
225
+ devtools.highlightElement(key);
226
+ });
227
+ // Makes every renderer wrap its elements in `data-jr-key` spans, which is
228
+ // what the picker and the highlight look for.
229
+ const releaseActive = markDevtoolsActive();
230
+ this.destroyRef.onDestroy(() => {
231
+ releaseActive();
232
+ unsubscribeSelection();
233
+ this.panel?.destroy();
234
+ this.panel = null;
235
+ unsubscribeStreams();
236
+ unsubscribeActions();
237
+ unsubscribeEvents();
238
+ this.events = null;
239
+ });
240
+ }
241
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JsonRenderDevtools, deps: [], target: i0.ɵɵFactoryTarget.Component });
242
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.22", type: JsonRenderDevtools, isStandalone: true, selector: "json-render-devtools", inputs: { renderer: { classPropertyName: "renderer", publicName: "renderer", isSignal: true, isRequired: false, transformFunction: null }, spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: false, transformFunction: null }, catalog: { classPropertyName: "catalog", publicName: "catalog", isSignal: true, isRequired: false, transformFunction: null }, chat: { classPropertyName: "chat", publicName: "chat", isSignal: true, isRequired: false, transformFunction: null }, initialOpen: { classPropertyName: "initialOpen", publicName: "initialOpen", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, hotkey: { classPropertyName: "hotkey", publicName: "hotkey", isSignal: true, isRequired: false, transformFunction: null }, bufferSize: { classPropertyName: "bufferSize", publicName: "bufferSize", isSignal: true, isRequired: false, transformFunction: null }, reserveSpace: { classPropertyName: "reserveSpace", publicName: "reserveSpace", isSignal: true, isRequired: false, transformFunction: null }, allowDockToggle: { classPropertyName: "allowDockToggle", publicName: "allowDockToggle", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { event: "event" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
243
+ }
244
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: JsonRenderDevtools, decorators: [{
245
+ type: Component,
246
+ args: [{
247
+ selector: 'json-render-devtools',
248
+ template: '',
249
+ changeDetection: ChangeDetectionStrategy.OnPush,
250
+ }]
251
+ }], ctorParameters: () => [], propDecorators: { renderer: [{ type: i0.Input, args: [{ isSignal: true, alias: "renderer", required: false }] }], spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: false }] }], catalog: [{ type: i0.Input, args: [{ isSignal: true, alias: "catalog", required: false }] }], chat: [{ type: i0.Input, args: [{ isSignal: true, alias: "chat", required: false }] }], initialOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialOpen", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], hotkey: [{ type: i0.Input, args: [{ isSignal: true, alias: "hotkey", required: false }] }], bufferSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "bufferSize", required: false }] }], reserveSpace: [{ type: i0.Input, args: [{ isSignal: true, alias: "reserveSpace", required: false }] }], allowDockToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDockToggle", required: false }] }], event: [{ type: i0.Output, args: ["event"] }] } });
252
+
253
+ /*
254
+ * Public API Surface of ngx-json-render/devtools
255
+ */
256
+
257
+ /**
258
+ * Generated bundle index. Do not edit.
259
+ */
260
+
261
+ export { JsonRenderDevtools };
262
+ //# sourceMappingURL=ngx-json-render-devtools.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngx-json-render-devtools.mjs","sources":["../../../projects/ngx-json-render/devtools/src/json-render-devtools.ts","../../../projects/ngx-json-render/devtools/src/public-api.ts","../../../projects/ngx-json-render/devtools/src/ngx-json-render-devtools.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n DestroyRef,\n afterNextRender,\n effect,\n inject,\n input,\n isDevMode,\n output,\n untracked,\n} from '@angular/core';\nimport {\n type Catalog,\n type Spec,\n type StateStore,\n markDevtoolsActive,\n registerActionObserver,\n} from '@json-render/core';\nimport type {\n DevtoolsEvent,\n EventStore,\n PanelHandle,\n PanelPosition,\n SpecEntry,\n} from '@json-render/devtools';\nimport {\n type ChatUIReturn,\n type JsonRenderer,\n ɵregisterStreamObserver,\n} from 'ngx-json-render';\n\n/**\n * A floating json-render devtools panel for an Angular app — the counterpart\n * of `<JsonRenderDevtools />` in `@json-render/devtools-react`, `-vue`,\n * `-svelte` and `-solid`, with the same tabs (Spec, State, Actions, Stream,\n * Catalog) and the same element picker.\n *\n * Angular has no provider above `<json-render>` to read the renderer from, so\n * the renderer is passed in by template reference. Streams need no wiring:\n * every `injectUIStream` and `injectChatUI` generation in the app reaches the\n * Stream tab.\n *\n * Renders nothing outside dev mode, and loads the panel itself\n * (`@json-render/devtools`) with a dynamic `import()`, so a production build\n * never fetches it.\n *\n * @example\n * ```html\n * <json-render #renderer [spec]=\"ui.spec()\" [registry]=\"registry\" />\n * <json-render-devtools [renderer]=\"renderer\" [catalog]=\"catalog\" />\n * ```\n */\n@Component({\n selector: 'json-render-devtools',\n template: '',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class JsonRenderDevtools {\n /** The renderer to inspect: its spec, state store and catalog. */\n readonly renderer = input<JsonRenderer | null>(null);\n /** Spec to show instead of the renderer's. */\n readonly spec = input<Spec | null | undefined>(undefined);\n /** Catalog for the Catalog tab, if the renderer was not given one. */\n readonly catalog = input<Catalog | null | undefined>(undefined);\n /**\n * A chat from `injectChatUI`: every assistant reply with a spec becomes a\n * generation in the Spec tab's switcher.\n */\n readonly chat = input<ChatUIReturn | null>(null);\n readonly initialOpen = input(false);\n readonly position = input<PanelPosition>('bottom-right');\n /** Toggle shortcut; `false` disables it. */\n readonly hotkey = input<string | false>('mod+shift+j');\n /** Events kept in the ring buffer. Read once, at creation. */\n readonly bufferSize = input(500);\n /** Pad the page while the panel is open instead of overlaying it. */\n readonly reserveSpace = input(true);\n /** Let the viewer flip the panel between bottom and right dock. */\n readonly allowDockToggle = input(true);\n /** Every event the panel records, for an app's own logging. */\n readonly event = output<DevtoolsEvent>();\n\n private readonly destroyRef = inject(DestroyRef);\n private events: EventStore | null = null;\n private panel: PanelHandle | null = null;\n private destroyed = false;\n\n constructor() {\n if (!isDevMode()) return;\n\n this.destroyRef.onDestroy(() => (this.destroyed = true));\n afterNextRender(() => {\n void this.mount();\n });\n\n // The panel reads everything through getters, so it only needs telling\n // when to re-read: whenever the spec, the chat or the renderer changes.\n effect(() => {\n this.currentSpec();\n this.chat()?.messages();\n this.renderer();\n untracked(() => this.panel?.refresh());\n });\n\n // A state write becomes an event, which redraws the State tab — the\n // store has no subscribe of the shape the panel's StateStore expects.\n effect((onCleanup) => {\n const store = this.renderer()?.stateStore;\n if (!store) return;\n onCleanup(\n store.subscribeChanges((changes) => {\n for (const change of changes) {\n this.events?.push({\n kind: 'state-set',\n at: Date.now(),\n path: change.path,\n prev: undefined,\n next: change.value,\n });\n }\n }),\n );\n });\n }\n\n private currentSpec(): Spec | null {\n const override = this.spec();\n if (override !== undefined) return override;\n const chat = this.chat();\n if (chat) {\n const replies = chat.messages().filter((m) => m.spec);\n return replies.at(-1)?.spec ?? null;\n }\n return this.renderer()?.spec() ?? null;\n }\n\n private currentCatalog(): Catalog | null {\n const own = this.catalog();\n if (own !== undefined) return own;\n // The renderer's catalog input is typed structurally; a catalog made with\n // `schema.createCatalog` is a core Catalog, which is what the tab reads.\n const fromRenderer = this.renderer()?.catalog();\n return fromRenderer && 'data' in fromRenderer\n ? (fromRenderer as unknown as Catalog)\n : null;\n }\n\n private generations(): SpecEntry[] {\n const chat = this.chat();\n if (!chat) {\n const spec = this.currentSpec();\n return spec ? [{ id: 'current', label: 'Current', spec }] : [];\n }\n return chat\n .messages()\n .filter((m) => m.role === 'assistant' && m.spec)\n .map((m, i) => ({\n id: m.id,\n label: `Generation ${i + 1}`,\n spec: m.spec as Spec,\n }));\n }\n\n private stateStore(): StateStore | null {\n const store = this.renderer()?.stateStore;\n if (!store) return null;\n return {\n get: (path) => store.get(path),\n set: (path, value) => store.set(path, value),\n update: (updates) => store.update(updates),\n getSnapshot: () => store.getSnapshot(),\n subscribe: () => () => {},\n };\n }\n\n private async mount(): Promise<void> {\n // A production build defines `ngDevMode` as false, so the bundler drops\n // the import below: nothing references the panel chunk, even if esbuild\n // still writes it to disk.\n if (typeof ngDevMode !== 'undefined' && !ngDevMode) return;\n const devtools = await import('@json-render/devtools');\n if (this.destroyed) return;\n\n const events = devtools.createEventStore({\n bufferSize: this.bufferSize(),\n });\n this.events = events;\n\n const unsubscribeEvents = events.subscribe(() => {\n const last = events.snapshot().at(-1);\n if (last) this.event.emit(last);\n });\n\n // Both observers come from the app's own `@json-render/core` and\n // `ngx-json-render`, never through `@json-render/devtools`: the registries\n // are module singletons, and devtools may carry a nested copy of core.\n const unsubscribeActions = registerActionObserver({\n onDispatch: (e) =>\n events.push({\n kind: 'action-dispatched',\n at: e.at,\n id: e.id,\n name: e.name,\n params: e.params,\n }),\n onSettle: (e) =>\n events.push({\n kind: 'action-settled',\n at: e.at,\n id: e.id,\n ok: e.ok,\n durationMs: e.durationMs,\n result: e.result,\n error: e.error !== undefined ? String(e.error) : undefined,\n }),\n });\n\n const unsubscribeStreams = ɵregisterStreamObserver({\n onStart: () =>\n events.push({\n kind: 'stream-lifecycle',\n at: Date.now(),\n phase: 'start',\n }),\n onPatch: (patch) =>\n events.push({\n kind: 'stream-patch',\n at: Date.now(),\n patch,\n source: 'json',\n }),\n onText: (text) =>\n events.push({ kind: 'stream-text', at: Date.now(), text }),\n onUsage: (usage) => devtools.recordUsage(events, usage),\n onEnd: (ok) =>\n events.push({\n kind: 'stream-lifecycle',\n at: Date.now(),\n phase: 'end',\n ok,\n }),\n });\n\n const selection = devtools.createSelectionBus();\n this.panel = devtools.createPanel({\n context: {\n events,\n getSpec: () => this.currentSpec(),\n getSpecs: () => this.generations(),\n getCatalog: () => this.currentCatalog(),\n getStateStore: () => this.stateStore(),\n startPicker: (options) => devtools.startPicker(options),\n selection,\n activateTab: () => {},\n },\n tabs: [\n devtools.specTab(),\n devtools.stateTab(),\n devtools.actionsTab(),\n devtools.streamTab(),\n devtools.catalogTab(),\n ],\n initialOpen: this.initialOpen(),\n position: this.position(),\n hotkey: this.hotkey(),\n reserveSpace: this.reserveSpace(),\n allowDockToggle: this.allowDockToggle(),\n });\n\n const unsubscribeSelection = selection.subscribe((key) => {\n if (key) devtools.highlightElement(key);\n });\n // Makes every renderer wrap its elements in `data-jr-key` spans, which is\n // what the picker and the highlight look for.\n const releaseActive = markDevtoolsActive();\n\n this.destroyRef.onDestroy(() => {\n releaseActive();\n unsubscribeSelection();\n this.panel?.destroy();\n this.panel = null;\n unsubscribeStreams();\n unsubscribeActions();\n unsubscribeEvents();\n this.events = null;\n });\n }\n}\n","/*\n * Public API Surface of ngx-json-render/devtools\n */\n\nexport { JsonRenderDevtools } from './json-render-devtools';\nexport type { DevtoolsEvent, PanelPosition } from '@json-render/devtools';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":["ɵregisterStreamObserver"],"mappings":"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"}
@@ -3139,6 +3139,67 @@ function applyPatch(spec, patch) {
3139
3139
  return newSpec;
3140
3140
  }
3141
3141
 
3142
+ const observers = new Set();
3143
+ /**
3144
+ * Register an observer for every stream in the app. Returns the function that
3145
+ * removes it. Private API for `ngx-json-render/devtools`.
3146
+ */
3147
+ function ɵregisterStreamObserver(observer) {
3148
+ observers.add(observer);
3149
+ return () => {
3150
+ observers.delete(observer);
3151
+ };
3152
+ }
3153
+ /**
3154
+ * Tell every observer. An observer that throws is a devtools bug, and it must
3155
+ * not become a failed generation in the app it is inspecting.
3156
+ */
3157
+ function notifyStreamObservers(event, ...args) {
3158
+ if (observers.size === 0)
3159
+ return;
3160
+ for (const observer of observers) {
3161
+ try {
3162
+ observer[event]?.(...args);
3163
+ }
3164
+ catch (error) {
3165
+ console.error('[ngx-json-render] A stream observer threw:', error);
3166
+ }
3167
+ }
3168
+ }
3169
+ /**
3170
+ * The start and end of one hook's generations, as observers see them.
3171
+ *
3172
+ * A superseded or stopped request only settles once its abort has gone
3173
+ * through, after the next generation may already have started. Reporting its
3174
+ * end from there would close the new generation instead of the old one, so a
3175
+ * generation ends here the moment it is superseded or stopped, and its own
3176
+ * late end is dropped.
3177
+ */
3178
+ function createObservedRuns() {
3179
+ let endOpen = null;
3180
+ return {
3181
+ start() {
3182
+ endOpen?.();
3183
+ let ended = false;
3184
+ const end = (ok) => {
3185
+ if (ended)
3186
+ return;
3187
+ ended = true;
3188
+ if (endOpen === stop)
3189
+ endOpen = null;
3190
+ notifyStreamObservers('onEnd', ok);
3191
+ };
3192
+ const stop = () => end(true);
3193
+ endOpen = stop;
3194
+ notifyStreamObservers('onStart');
3195
+ return end;
3196
+ },
3197
+ stop() {
3198
+ endOpen?.();
3199
+ },
3200
+ };
3201
+ }
3202
+
3142
3203
  /**
3143
3204
  * Convert a flat element list to a Spec.
3144
3205
  * Input elements use key/parentKey to establish identity and relationships.
@@ -3435,9 +3496,11 @@ function injectUIStream(options) {
3435
3496
  catalog: options.catalog ?? null,
3436
3497
  };
3437
3498
  const session = createStreamSession();
3499
+ const runs = createObservedRuns();
3438
3500
  inject(DestroyRef).onDestroy(() => session.cancel());
3439
3501
  const stop = () => {
3440
3502
  session.cancel();
3503
+ runs.stop();
3441
3504
  isStreaming.set(false);
3442
3505
  };
3443
3506
  const clear = () => {
@@ -3462,6 +3525,8 @@ function injectUIStream(options) {
3462
3525
  ? { ...previousSpec, elements: { ...previousSpec.elements } }
3463
3526
  : { root: '', elements: {} };
3464
3527
  spec.set(currentSpec);
3528
+ const end = runs.start();
3529
+ let failed = false;
3465
3530
  const handleLine = (line) => {
3466
3531
  const parsed = parseLine(line);
3467
3532
  if (!parsed)
@@ -3469,11 +3534,13 @@ function injectUIStream(options) {
3469
3534
  gate.commit(() => {
3470
3535
  if (parsed.type === 'usage') {
3471
3536
  usage.set(parsed.usage);
3537
+ notifyStreamObservers('onUsage', parsed.usage);
3472
3538
  return;
3473
3539
  }
3474
3540
  rawLines.update((prev) => [...prev, line]);
3475
3541
  currentSpec = applyPatch(currentSpec, parsed.patch);
3476
3542
  spec.set(currentSpec);
3543
+ notifyStreamObservers('onPatch', parsed.patch);
3477
3544
  });
3478
3545
  };
3479
3546
  try {
@@ -3504,6 +3571,7 @@ function injectUIStream(options) {
3504
3571
  // not want rendered.
3505
3572
  if (check.blocked || (validate === 'strict' && check.hasErrors)) {
3506
3573
  const invalid = new Error(`Generated spec failed validation:\n${formatSpecCheckIssues(check.issues)}`);
3574
+ failed = true;
3507
3575
  error.set(invalid);
3508
3576
  options.onError?.(invalid);
3509
3577
  return;
@@ -3514,6 +3582,7 @@ function injectUIStream(options) {
3514
3582
  catch (err) {
3515
3583
  if (isAbortError(err))
3516
3584
  return;
3585
+ failed = true;
3517
3586
  const resolvedError = err instanceof Error ? err : new Error(String(err));
3518
3587
  gate.commit(() => {
3519
3588
  error.set(resolvedError);
@@ -3522,6 +3591,7 @@ function injectUIStream(options) {
3522
3591
  }
3523
3592
  finally {
3524
3593
  gate.commit(() => isStreaming.set(false));
3594
+ end(!failed);
3525
3595
  }
3526
3596
  };
3527
3597
  return {
@@ -3565,9 +3635,11 @@ function injectChatUI(options) {
3565
3635
  catalog: options.catalog ?? null,
3566
3636
  };
3567
3637
  const session = createStreamSession();
3638
+ const runs = createObservedRuns();
3568
3639
  inject(DestroyRef).onDestroy(() => session.cancel());
3569
3640
  const stop = () => {
3570
3641
  session.cancel();
3642
+ runs.stop();
3571
3643
  isStreaming.set(false);
3572
3644
  };
3573
3645
  const clear = () => {
@@ -3622,6 +3694,8 @@ function injectChatUI(options) {
3622
3694
  messages.update((prev) => [...prev, userMessage, assistantMessage]);
3623
3695
  isStreaming.set(true);
3624
3696
  error.set(null);
3697
+ const end = runs.start();
3698
+ let failed = false;
3625
3699
  // Mutable state for accumulating the assistant response
3626
3700
  let accumulatedText = '';
3627
3701
  let currentSpec = { root: '', elements: {} };
@@ -3633,12 +3707,18 @@ function injectChatUI(options) {
3633
3707
  // touch, so the message can hold it directly — no snapshot copy.
3634
3708
  currentSpec = applyPatch(currentSpec, patch);
3635
3709
  const snapshot = currentSpec;
3636
- gate.commit(() => messages.update((prev) => prev.map((m) => m.id === assistantId ? { ...m, spec: snapshot } : m)));
3710
+ gate.commit(() => {
3711
+ messages.update((prev) => prev.map((m) => m.id === assistantId ? { ...m, spec: snapshot } : m));
3712
+ notifyStreamObservers('onPatch', patch);
3713
+ });
3637
3714
  },
3638
3715
  onText(line) {
3639
3716
  accumulatedText += (accumulatedText ? '\n' : '') + line;
3640
3717
  const snapshot = accumulatedText;
3641
- gate.commit(() => messages.update((prev) => prev.map((m) => m.id === assistantId ? { ...m, text: snapshot } : m)));
3718
+ gate.commit(() => {
3719
+ messages.update((prev) => prev.map((m) => m.id === assistantId ? { ...m, text: snapshot } : m));
3720
+ notifyStreamObservers('onText', line);
3721
+ });
3642
3722
  },
3643
3723
  });
3644
3724
  try {
@@ -3672,6 +3752,7 @@ function injectChatUI(options) {
3672
3752
  // not want rendered.
3673
3753
  if (check.blocked || (validate === 'strict' && check.hasErrors)) {
3674
3754
  const invalid = new Error(`Generated spec failed validation:\n${formatSpecCheckIssues(check.issues)}`);
3755
+ failed = true;
3675
3756
  error.set(invalid);
3676
3757
  options.onError?.(invalid);
3677
3758
  return;
@@ -3687,6 +3768,7 @@ function injectChatUI(options) {
3687
3768
  dropEmptyPlaceholder();
3688
3769
  if (isAbortError(err))
3689
3770
  return;
3771
+ failed = true;
3690
3772
  const resolvedError = err instanceof Error ? err : new Error(String(err));
3691
3773
  gate.commit(() => {
3692
3774
  error.set(resolvedError);
@@ -3695,6 +3777,7 @@ function injectChatUI(options) {
3695
3777
  }
3696
3778
  finally {
3697
3779
  gate.commit(() => isStreaming.set(false));
3780
+ end(!failed);
3698
3781
  }
3699
3782
  };
3700
3783
  return {
@@ -3726,5 +3809,5 @@ function injectChatUI(options) {
3726
3809
  * Generated bundle index. Do not edit.
3727
3810
  */
3728
3811
 
3729
- export { CONFIRM_CONTEXT, ELEMENT_KEY, JR_CONFIRM_DIALOG, JR_CONFIRM_LABELS, JrChildren, JrConfirmDialog, JrElement, JrRepeatScope, JsonRenderActionsService, JsonRenderRootContext, JsonRenderStateService, JsonRenderValidationService, JsonRenderer, RENDER_CONTEXT, REPEAT_SCOPE, applyPatch, buildSpecFromParts, checkSpec, createStoreSetState, defineRegistry, flatToTree, formatSpecCheckIssues, getTextFromParts, injectAction, injectActions, injectBoundProp, injectChatUI, injectConfirmContext, injectDevtoolsActive, injectElementKey, injectFieldValidation, injectRenderContext, injectRepeatScope, injectStateBinding, injectStateStore, injectStateValue, injectUIStream, injectValidation, isActionCancelled, jsonRenderMessage };
3812
+ export { CONFIRM_CONTEXT, ELEMENT_KEY, JR_CONFIRM_DIALOG, JR_CONFIRM_LABELS, JrChildren, JrConfirmDialog, JrElement, JrRepeatScope, JsonRenderActionsService, JsonRenderRootContext, JsonRenderStateService, JsonRenderValidationService, JsonRenderer, RENDER_CONTEXT, REPEAT_SCOPE, applyPatch, buildSpecFromParts, checkSpec, createStoreSetState, defineRegistry, flatToTree, formatSpecCheckIssues, getTextFromParts, injectAction, injectActions, injectBoundProp, injectChatUI, injectConfirmContext, injectDevtoolsActive, injectElementKey, injectFieldValidation, injectRenderContext, injectRepeatScope, injectStateBinding, injectStateStore, injectStateValue, injectUIStream, injectValidation, isActionCancelled, jsonRenderMessage, ɵregisterStreamObserver };
3730
3813
  //# sourceMappingURL=ngx-json-render.mjs.map