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 +64 -3
- package/fesm2022/ngx-json-render-devtools.mjs +262 -0
- package/fesm2022/ngx-json-render-devtools.mjs.map +1 -0
- package/fesm2022/ngx-json-render.mjs +86 -3
- package/fesm2022/ngx-json-render.mjs.map +1 -1
- package/package.json +10 -2
- package/schematics/ng-add/index.cjs +1 -1
- package/types/ngx-json-render-devtools.d.ts +67 -0
- package/types/ngx-json-render.d.ts +32 -2
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/)** —
|
|
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 [](https://stackblitz.com/github/shteynu/ngx-json-render)
|
|
12
12
|
|
|
13
13
|

|
|
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
|
|
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
|
|
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(() =>
|
|
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(() =>
|
|
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
|