@jxsuite/studio 0.33.0 → 0.35.0
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/dist/iframe-entry.js +6238 -0
- package/dist/iframe-entry.js.map +35 -0
- package/dist/studio.js +30972 -17222
- package/dist/studio.js.map +328 -83
- package/package.json +9 -9
- package/src/browse/browse.ts +11 -4
- package/src/canvas/canvas-helpers.ts +2 -56
- package/src/canvas/canvas-live-render.ts +102 -435
- package/src/canvas/canvas-origin.ts +66 -0
- package/src/canvas/canvas-patcher.ts +63 -403
- package/src/canvas/canvas-render.ts +70 -212
- package/src/canvas/canvas-utils.ts +37 -65
- package/src/canvas/iframe-channel.ts +154 -0
- package/src/canvas/iframe-drop.ts +484 -0
- package/src/canvas/iframe-entry.ts +600 -0
- package/src/canvas/iframe-host.ts +1373 -0
- package/src/canvas/iframe-inline-edit.ts +367 -0
- package/src/canvas/iframe-insert.ts +164 -0
- package/src/canvas/iframe-interaction.ts +176 -0
- package/src/canvas/iframe-keys.ts +85 -0
- package/src/canvas/iframe-overlay.ts +218 -0
- package/src/canvas/iframe-patch.ts +363 -0
- package/src/canvas/iframe-protocol.ts +361 -0
- package/src/canvas/iframe-render.ts +458 -0
- package/src/canvas/iframe-slash.ts +114 -0
- package/src/canvas/iframe-subtree.ts +113 -0
- package/src/canvas/path-mapping.ts +86 -0
- package/src/canvas/serialize-scope.ts +65 -0
- package/src/editor/canvas-context-menu.ts +40 -0
- package/src/editor/canvas-slash-bridge.ts +21 -0
- package/src/editor/context-menu.ts +2 -1
- package/src/editor/inline-edit-apply.ts +183 -0
- package/src/editor/inline-edit.ts +99 -21
- package/src/editor/inline-link.ts +89 -0
- package/src/editor/insert-zone-action.ts +35 -0
- package/src/editor/merge-tags.ts +26 -2
- package/src/editor/repeater-scope.ts +144 -0
- package/src/editor/shortcuts.ts +14 -28
- package/src/editor/slash-menu.ts +73 -42
- package/src/files/files.ts +5 -1
- package/src/new-project/design-fields.ts +260 -0
- package/src/new-project/import-tab.ts +322 -0
- package/src/new-project/new-project-modal.ts +472 -89
- package/src/new-project/templates.ts +61 -0
- package/src/packages/ensure-deps.ts +6 -0
- package/src/packages/pull-package-sync.ts +339 -0
- package/src/page-params.ts +383 -0
- package/src/panels/ai-chat/attached-context.ts +49 -0
- package/src/panels/ai-chat/chat-markdown.ts +38 -0
- package/src/panels/ai-chat/chat-view.ts +250 -0
- package/src/panels/ai-chat/composer.ts +315 -0
- package/src/panels/ai-chat/sessions-view.ts +98 -0
- package/src/panels/ai-panel.ts +189 -457
- package/src/panels/block-action-bar.ts +296 -138
- package/src/panels/canvas-dnd-bridge.ts +397 -0
- package/src/panels/component-preview.ts +56 -0
- package/src/panels/dnd.ts +41 -17
- package/src/panels/drag-ghost.ts +62 -0
- package/src/panels/editors.ts +1 -1
- package/src/panels/git-panel.ts +16 -1
- package/src/panels/overlays.ts +10 -125
- package/src/panels/properties-panel.ts +210 -0
- package/src/panels/right-panel.ts +21 -7
- package/src/panels/signals-panel.ts +136 -22
- package/src/panels/stylebook-doc.ts +373 -0
- package/src/panels/stylebook-panel.ts +46 -689
- package/src/panels/tab-bar.ts +159 -13
- package/src/panels/toolbar.ts +3 -2
- package/src/platforms/devserver.ts +45 -1
- package/src/services/agent-seed.ts +91 -0
- package/src/services/ai-models.ts +64 -0
- package/src/services/ai-session-store.ts +250 -0
- package/src/services/ai-settings.ts +5 -0
- package/src/services/automation.ts +140 -0
- package/src/services/document-assistant.ts +98 -38
- package/src/services/import-client.ts +134 -0
- package/src/services/monaco-setup.ts +12 -0
- package/src/services/render-critic.ts +9 -9
- package/src/services/settings-store.ts +93 -0
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/store.ts +4 -62
- package/src/studio.ts +115 -40
- package/src/tabs/doc-op-apply.ts +89 -0
- package/src/tabs/patch-ops.ts +6 -2
- package/src/tabs/tab.ts +23 -4
- package/src/tabs/transact.ts +2 -74
- package/src/types.ts +98 -18
- package/src/ui/ai-credentials-form.ts +205 -0
- package/src/ui/jx-theme.ts +63 -0
- package/src/ui/media-picker.ts +6 -4
- package/src/ui/spectrum.ts +13 -0
- package/src/utils/canvas-media.ts +0 -137
- package/src/utils/edit-display.ts +23 -3
- package/src/utils/geometry.ts +43 -0
- package/src/utils/link-target.ts +93 -0
- package/src/utils/strip-events.ts +54 -0
- package/src/view.ts +0 -23
- package/src/workspace/workspace.ts +14 -1
- package/src/canvas/canvas-subtree-render.ts +0 -113
- package/src/editor/component-inline-edit.ts +0 -349
- package/src/editor/content-inline-edit.ts +0 -207
- package/src/editor/insertion-helper.ts +0 -308
- package/src/panels/canvas-dnd.ts +0 -329
- package/src/panels/panel-events.ts +0 -306
- package/src/panels/preview-render.ts +0 -132
- package/src/panels/pseudo-preview.ts +0 -75
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Dynamic route-param helpers for the tab-bar's $params picker.
|
|
4
|
+
*
|
|
5
|
+
* A page like `pages/products/[sku].json` declares its candidate values in a `$paths` block (spec
|
|
6
|
+
* §4.3). At build time the compiler expands those into concrete routes (`resolvePathEntries` in
|
|
7
|
+
* `@jxsuite/compiler`); in the studio the same shapes are resolved live so the author can pick one
|
|
8
|
+
* value to render. The chosen values are substituted into the canvas render document
|
|
9
|
+
* (`substitutePreviewParams`), which mirrors what the compiler's context injection produces for the
|
|
10
|
+
* matching built page.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getPlatform } from "./platform";
|
|
14
|
+
import type { JxMutableNode, JxPathsDef } from "@jxsuite/schema/types";
|
|
15
|
+
|
|
16
|
+
/** Param name → candidate values, in `$paths` declaration order. */
|
|
17
|
+
export type ParamValues = Record<string, string[]>;
|
|
18
|
+
|
|
19
|
+
/** Bracket route segments: `[param]` (named) and `[...param]` (catch-all). */
|
|
20
|
+
const PARAM_SEGMENT = /\[\.\.\.(\w+)\]|\[(\w+)\]/g;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Param names declared by a document path's bracket segments, e.g. `pages/products/[sku].json` →
|
|
24
|
+
* `["sku"]`. Mirrors the compiler's `fileToRoute` parsing.
|
|
25
|
+
*
|
|
26
|
+
* @param {string | null | undefined} documentPath
|
|
27
|
+
* @returns {string[]}
|
|
28
|
+
*/
|
|
29
|
+
export function dynamicRouteParams(documentPath: string | null | undefined) {
|
|
30
|
+
if (!documentPath) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const params: string[] = [];
|
|
34
|
+
for (const match of documentPath.matchAll(PARAM_SEGMENT)) {
|
|
35
|
+
params.push((match[1] ?? match[2])!);
|
|
36
|
+
}
|
|
37
|
+
return params;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* URL pattern for a page document path, mirroring the compiler's `fileToRoute`:
|
|
42
|
+
* `pages/products/[sku].json` → `/products/:sku`, `pages/index.json` → `/`.
|
|
43
|
+
*
|
|
44
|
+
* @param {string | null | undefined} documentPath
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function documentUrlPattern(documentPath: string | null | undefined) {
|
|
48
|
+
if (!documentPath) {
|
|
49
|
+
return "/";
|
|
50
|
+
}
|
|
51
|
+
let urlPath = documentPath
|
|
52
|
+
.replace(/^\.\//, "")
|
|
53
|
+
.replace(/^pages\//, "")
|
|
54
|
+
.replace(/\.[^/.]+$/, "");
|
|
55
|
+
if (urlPath.endsWith("/index")) {
|
|
56
|
+
urlPath = urlPath.slice(0, -6) || "/";
|
|
57
|
+
} else if (urlPath === "index") {
|
|
58
|
+
urlPath = "/";
|
|
59
|
+
}
|
|
60
|
+
if (!urlPath.startsWith("/")) {
|
|
61
|
+
urlPath = `/${urlPath}`;
|
|
62
|
+
}
|
|
63
|
+
return urlPath.replaceAll(PARAM_SEGMENT, (_match, spread: string, named: string) =>
|
|
64
|
+
spread ? "*" : `:${named}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The `$paths` declaration of an open document. JSON pages carry it on the document root;
|
|
70
|
+
* format-class pages (markdown) keep it in the frontmatter.
|
|
71
|
+
*
|
|
72
|
+
* @param {{ document?: unknown; frontmatter?: Record<string, unknown> }} doc
|
|
73
|
+
* @returns {JxPathsDef | null}
|
|
74
|
+
*/
|
|
75
|
+
export function pagePathsDef(doc: {
|
|
76
|
+
document?: JxMutableNode | null;
|
|
77
|
+
frontmatter?: Record<string, unknown> | null;
|
|
78
|
+
}) {
|
|
79
|
+
const fromDoc = (doc.document as { $paths?: JxPathsDef } | null | undefined)?.$paths;
|
|
80
|
+
const fromFrontmatter = doc.frontmatter?.$paths as JxPathsDef | undefined;
|
|
81
|
+
return fromDoc ?? fromFrontmatter ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const valueCache = new Map<string, Promise<ParamValues>>();
|
|
85
|
+
|
|
86
|
+
/** Drop all cached enumerations (e.g. after content files change). */
|
|
87
|
+
export function invalidateParamValues() {
|
|
88
|
+
valueCache.clear();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Enumerate candidate values for each param declared by a `$paths` block. Results are cached per
|
|
93
|
+
* (documentPath, $paths) pair; failures are not cached so a transient backend error retries on the
|
|
94
|
+
* next call.
|
|
95
|
+
*
|
|
96
|
+
* @param {string | null | undefined} documentPath - Cache key component
|
|
97
|
+
* @param {JxPathsDef | null} pathsDef
|
|
98
|
+
* @returns {Promise<ParamValues>}
|
|
99
|
+
*/
|
|
100
|
+
export function loadParamValues(
|
|
101
|
+
documentPath: string | null | undefined,
|
|
102
|
+
pathsDef: JxPathsDef | null,
|
|
103
|
+
) {
|
|
104
|
+
if (!pathsDef) {
|
|
105
|
+
return Promise.resolve({} as ParamValues);
|
|
106
|
+
}
|
|
107
|
+
const key = `${documentPath ?? ""}::${JSON.stringify(pathsDef)}`;
|
|
108
|
+
let pending = valueCache.get(key);
|
|
109
|
+
if (!pending) {
|
|
110
|
+
pending = resolveParamValues(pathsDef).catch((error: unknown) => {
|
|
111
|
+
valueCache.delete(key);
|
|
112
|
+
console.warn("page-params: failed to enumerate $paths values:", error);
|
|
113
|
+
return {} as ParamValues;
|
|
114
|
+
});
|
|
115
|
+
valueCache.set(key, pending);
|
|
116
|
+
}
|
|
117
|
+
return pending;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve one `$paths` shape to values, mirroring the compiler's `resolvePathEntries`.
|
|
122
|
+
*
|
|
123
|
+
* @param {JxPathsDef} pathsDef
|
|
124
|
+
* @returns {Promise<ParamValues>}
|
|
125
|
+
*/
|
|
126
|
+
async function resolveParamValues(pathsDef: JxPathsDef): Promise<ParamValues> {
|
|
127
|
+
// Legacy: array of param objects — group values per param key
|
|
128
|
+
if (Array.isArray(pathsDef)) {
|
|
129
|
+
const out: ParamValues = {};
|
|
130
|
+
for (const entry of pathsDef) {
|
|
131
|
+
for (const [param, value] of Object.entries(entry)) {
|
|
132
|
+
pushValue(out, param, value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Content type-based: { contentType: "blog", param: "slug", field: "id" } — resolved through the
|
|
139
|
+
// Backend's ContentCollection pipeline (the same one the canvas's ContentEntry uses), so every
|
|
140
|
+
// Offered value is guaranteed to resolve in preview.
|
|
141
|
+
if ("contentType" in pathsDef && pathsDef.contentType) {
|
|
142
|
+
const param = pathsDef.param ?? "slug";
|
|
143
|
+
const field = pathsDef.field ?? "id";
|
|
144
|
+
const entries = (await resolveContentCollection(pathsDef.contentType)) as {
|
|
145
|
+
id?: unknown;
|
|
146
|
+
data?: Record<string, unknown>;
|
|
147
|
+
}[];
|
|
148
|
+
const out: ParamValues = { [param]: [] };
|
|
149
|
+
if (!Array.isArray(entries)) {
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
pushValue(out, param, field === "id" ? entry.id : (entry.data?.[field] ?? entry.id));
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Explicit values: { values: ["en", "fr"], param: "lang" }
|
|
159
|
+
if ("values" in pathsDef && Array.isArray(pathsDef.values)) {
|
|
160
|
+
const param = pathsDef.param ?? "value";
|
|
161
|
+
const out: ParamValues = { [param]: [] };
|
|
162
|
+
for (const value of pathsDef.values) {
|
|
163
|
+
pushValue(out, param, value);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Data file ref: { "$ref": "./data/products.json", param: "id", field: "sku" }
|
|
169
|
+
if ("$ref" in pathsDef && pathsDef.$ref) {
|
|
170
|
+
const param = pathsDef.param ?? "id";
|
|
171
|
+
const field = pathsDef.field ?? "id";
|
|
172
|
+
const content = await getPlatform().readFile(pathsDef.$ref.replace(/^\.\//, ""));
|
|
173
|
+
const data = JSON.parse(content) as unknown;
|
|
174
|
+
const out: ParamValues = { [param]: [] };
|
|
175
|
+
if (!Array.isArray(data)) {
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
for (const item of data as Record<string, unknown>[]) {
|
|
179
|
+
pushValue(out, param, item?.[field] ?? item?.id ?? item);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve a ContentCollection for a content type via the PAL, falling back to a plain dev-proxy
|
|
189
|
+
* fetch on platforms that predate `resolveClass`.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} contentType
|
|
192
|
+
* @returns {Promise<unknown>}
|
|
193
|
+
*/
|
|
194
|
+
function resolveContentCollection(contentType: string) {
|
|
195
|
+
const body = {
|
|
196
|
+
$prototype: "ContentCollection",
|
|
197
|
+
$src: "@jxsuite/parser/ContentCollection.class.json",
|
|
198
|
+
contentType,
|
|
199
|
+
};
|
|
200
|
+
const platform = getPlatform();
|
|
201
|
+
if (platform.resolveClass) {
|
|
202
|
+
return platform.resolveClass(body);
|
|
203
|
+
}
|
|
204
|
+
return fetch("/__jx_resolve__", {
|
|
205
|
+
body: JSON.stringify(body),
|
|
206
|
+
headers: { "content-type": "application/json" },
|
|
207
|
+
method: "POST",
|
|
208
|
+
}).then((res) => {
|
|
209
|
+
if (!res.ok) {
|
|
210
|
+
throw new Error(`Class resolution failed: ${res.status}`);
|
|
211
|
+
}
|
|
212
|
+
return res.json() as Promise<unknown>;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {ParamValues} out
|
|
218
|
+
* @param {string} param
|
|
219
|
+
* @param {unknown} value
|
|
220
|
+
*/
|
|
221
|
+
function pushValue(out: ParamValues, param: string, value: unknown) {
|
|
222
|
+
if (value === null || value === undefined || value === "") {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const str = String(value);
|
|
226
|
+
const list = (out[param] ??= []);
|
|
227
|
+
if (!list.includes(str)) {
|
|
228
|
+
list.push(str);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Substitute chosen param values into a canvas render document:
|
|
234
|
+
*
|
|
235
|
+
* - Every `{ "$ref": "#/$params/<name>" }` value with a chosen `<name>` becomes the literal string
|
|
236
|
+
* (the exact dereference `ContentEntry.resolve()` performs against `route._pathParams`), and
|
|
237
|
+
* - `state.$page` is injected mirroring the compiler's `injectContext`, so `${state.$page.params.x}`
|
|
238
|
+
* template expressions render.
|
|
239
|
+
*
|
|
240
|
+
* Pure: returns a rebuilt tree. The design/edit render doc shares node references with the tab's
|
|
241
|
+
* source document (stripEventHandlers/prepareForEditMode shallow-rebuild), so in-place mutation
|
|
242
|
+
* here would corrupt the document being edited and saved.
|
|
243
|
+
*
|
|
244
|
+
* @param {JxMutableNode} renderDoc
|
|
245
|
+
* @param {Record<string, string>} params
|
|
246
|
+
* @param {string | null | undefined} documentPath
|
|
247
|
+
* @returns {JxMutableNode}
|
|
248
|
+
*/
|
|
249
|
+
export function substitutePreviewParams(
|
|
250
|
+
renderDoc: JxMutableNode,
|
|
251
|
+
params: Record<string, string>,
|
|
252
|
+
documentPath: string | null | undefined,
|
|
253
|
+
): JxMutableNode {
|
|
254
|
+
if (Object.keys(params).length === 0) {
|
|
255
|
+
return renderDoc;
|
|
256
|
+
}
|
|
257
|
+
const doc = substituteParamRefs(renderDoc, params) as JxMutableNode & {
|
|
258
|
+
state?: Record<string, unknown>;
|
|
259
|
+
title?: unknown;
|
|
260
|
+
};
|
|
261
|
+
doc.state ??= {};
|
|
262
|
+
doc.state.$page = {
|
|
263
|
+
params: { ...params },
|
|
264
|
+
title: typeof doc.title === "string" ? doc.title : "",
|
|
265
|
+
url: documentUrlPattern(documentPath),
|
|
266
|
+
};
|
|
267
|
+
return doc;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* State keys whose entries are class prototypes ($prototype + $src) referencing a route param.
|
|
272
|
+
* These are the entries `substitutePreviewParams` rewrites, and the ones `resolveParamBoundState`
|
|
273
|
+
* bakes.
|
|
274
|
+
*
|
|
275
|
+
* @param {Record<string, unknown> | null | undefined} state - The ORIGINAL document state (with
|
|
276
|
+
* `$ref`s intact — substitution removes the marker)
|
|
277
|
+
* @returns {string[]}
|
|
278
|
+
*/
|
|
279
|
+
export function paramBoundStateKeys(state: Record<string, unknown> | null | undefined) {
|
|
280
|
+
if (!state) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
return Object.keys(state).filter((key) => {
|
|
284
|
+
const entry = state[key] as Record<string, unknown> | null;
|
|
285
|
+
return (
|
|
286
|
+
entry &&
|
|
287
|
+
typeof entry === "object" &&
|
|
288
|
+
typeof entry.$prototype === "string" &&
|
|
289
|
+
typeof entry.$src === "string" &&
|
|
290
|
+
JSON.stringify(entry).includes('"#/$params/')
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resolve substituted class-prototype state entries through the backend and bake the values into
|
|
297
|
+
* `renderDoc.state` — compiler parity: the built site resolves ContentEntry/ContentCollection
|
|
298
|
+
* BEFORE templates run. Without baking, the iframe runtime resolves these entries asynchronously
|
|
299
|
+
* (null on the first render pass), so preview templates like `${state.product.data.title}`
|
|
300
|
+
* dereference null and abort the whole render.
|
|
301
|
+
*
|
|
302
|
+
* Failures leave the entry in place (the runtime's own async resolution remains the fallback).
|
|
303
|
+
*
|
|
304
|
+
* @param {JxMutableNode} renderDoc - The substituted render doc (mutated in place — it is the fresh
|
|
305
|
+
* rebuild from substitutePreviewParams, never the source doc)
|
|
306
|
+
* @param {string[]} keys - From paramBoundStateKeys(originalDoc.state)
|
|
307
|
+
* @param {string | undefined} docBase - Document base URL for relative-$src rebasing
|
|
308
|
+
*/
|
|
309
|
+
export async function resolveParamBoundState(
|
|
310
|
+
renderDoc: JxMutableNode,
|
|
311
|
+
keys: string[],
|
|
312
|
+
docBase?: string,
|
|
313
|
+
) {
|
|
314
|
+
if (keys.length === 0) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const platform = getPlatform();
|
|
318
|
+
if (!platform.resolveClass) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const { state } = renderDoc as { state?: Record<string, unknown> };
|
|
322
|
+
if (!state) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
await Promise.all(
|
|
326
|
+
keys.map(async (key) => {
|
|
327
|
+
const entry = state[key];
|
|
328
|
+
if (!entry || typeof entry !== "object") {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
state[key] = await platform.resolveClass!({
|
|
333
|
+
...(entry as Record<string, unknown>),
|
|
334
|
+
...(docBase ? { $base: docBase } : {}),
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.warn(`page-params: failed to resolve state.${key} for preview:`, error);
|
|
338
|
+
}
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {unknown} node
|
|
345
|
+
* @param {Record<string, string>} params
|
|
346
|
+
* @returns {unknown} — a rebuilt copy with matched param refs replaced by literals
|
|
347
|
+
*/
|
|
348
|
+
function substituteParamRefs(node: unknown, params: Record<string, string>): unknown {
|
|
349
|
+
const replaced = paramRefValue(node, params);
|
|
350
|
+
if (replaced !== null) {
|
|
351
|
+
return replaced;
|
|
352
|
+
}
|
|
353
|
+
if (!node || typeof node !== "object") {
|
|
354
|
+
return node;
|
|
355
|
+
}
|
|
356
|
+
if (Array.isArray(node)) {
|
|
357
|
+
return node.map((item) => substituteParamRefs(item, params));
|
|
358
|
+
}
|
|
359
|
+
const out: Record<string, unknown> = {};
|
|
360
|
+
for (const [key, value] of Object.entries(node)) {
|
|
361
|
+
out[key] = substituteParamRefs(value, params);
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The chosen literal for a `{ "$ref": "#/$params/<name>" }` value, or null when the value is not a
|
|
368
|
+
* param ref (or no value was chosen for it).
|
|
369
|
+
*
|
|
370
|
+
* @param {unknown} value
|
|
371
|
+
* @param {Record<string, string>} params
|
|
372
|
+
* @returns {string | null}
|
|
373
|
+
*/
|
|
374
|
+
function paramRefValue(value: unknown, params: Record<string, string>) {
|
|
375
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
const ref = (value as { $ref?: unknown }).$ref;
|
|
379
|
+
if (typeof ref !== "string" || !ref.startsWith("#/$params/")) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
return params[ref.slice("#/$params/".length)] ?? null;
|
|
383
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attached-context.js — the composer's "attach context" message-content convention.
|
|
3
|
+
*
|
|
4
|
+
* Attached context (current page, selected element) is embedded into the user message
|
|
5
|
+
* content after a delimiter line: it's the only channel the LLM sees (the streaming
|
|
6
|
+
* payload carries message content only) and it persists with the session for free.
|
|
7
|
+
* The chat view splits the content back apart to show chips instead of the raw block.
|
|
8
|
+
*
|
|
9
|
+
* The delimiter is a soft convention — a model echoing it would at worst render stray
|
|
10
|
+
* chips (no security impact; assistant markdown is sanitized separately).
|
|
11
|
+
*
|
|
12
|
+
* @license MIT
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const ATTACHED_CONTEXT_DELIMITER = "---- attached context ----";
|
|
16
|
+
|
|
17
|
+
export interface ContextChip {
|
|
18
|
+
kind: "page" | "selection";
|
|
19
|
+
/** Short chip label, e.g. the page path or `<h1>`. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** The context line embedded into the message content. */
|
|
22
|
+
detail: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Serialize the composer text plus attached-context chips into one message content. */
|
|
26
|
+
export function buildMessageWithContext(text: string, chips: ContextChip[]): string {
|
|
27
|
+
if (chips.length === 0) {
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
const lines = chips.map((c) => c.detail).join("\n");
|
|
31
|
+
return `${text}\n\n${ATTACHED_CONTEXT_DELIMITER}\n${lines}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Split a user message back into its typed body and any attached-context lines. */
|
|
35
|
+
export function splitAttachedContext(content: string): { body: string; contextLines: string[] } {
|
|
36
|
+
const idx = content.indexOf(`\n\n${ATTACHED_CONTEXT_DELIMITER}\n`);
|
|
37
|
+
if (idx === -1) {
|
|
38
|
+
return { body: content, contextLines: [] };
|
|
39
|
+
}
|
|
40
|
+
const body = content.slice(0, idx);
|
|
41
|
+
const rest = content.slice(idx + ATTACHED_CONTEXT_DELIMITER.length + 3);
|
|
42
|
+
return {
|
|
43
|
+
body,
|
|
44
|
+
contextLines: rest
|
|
45
|
+
.split("\n")
|
|
46
|
+
.map((l) => l.trim())
|
|
47
|
+
.filter((l) => l.length > 0),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-markdown.js — memoized markdown rendering for assistant chat messages.
|
|
3
|
+
*
|
|
4
|
+
* Wraps @jxsuite/parser/md-html (sanitized markdown → HTML) with a per-message cache
|
|
5
|
+
* keyed by message id + content length, so re-renders during streaming only re-parse
|
|
6
|
+
* the message that actually grew. The HTML goes through unsafeHTML, which is safe here
|
|
7
|
+
* because md-html sanitizes (raw HTML dropped, javascript: URLs stripped).
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { html } from "lit-html";
|
|
13
|
+
import type { TemplateResult } from "lit-html";
|
|
14
|
+
import { unsafeHTML } from "lit-html/directives/unsafe-html.js";
|
|
15
|
+
import { markdownToHtml } from "@jxsuite/parser/md-html";
|
|
16
|
+
|
|
17
|
+
const cache = new Map<string, { len: number; html: string }>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render a message's markdown content, memoized by message id.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} id - Stable message id (cache key).
|
|
23
|
+
* @param {string} content
|
|
24
|
+
* @returns {TemplateResult}
|
|
25
|
+
*/
|
|
26
|
+
export function renderMarkdown(id: string, content: string): TemplateResult {
|
|
27
|
+
let entry = cache.get(id);
|
|
28
|
+
if (!entry || entry.len !== content.length) {
|
|
29
|
+
entry = { len: content.length, html: markdownToHtml(content) };
|
|
30
|
+
cache.set(id, entry);
|
|
31
|
+
}
|
|
32
|
+
return html`<div class="ai-msg-md">${unsafeHTML(entry.html)}</div>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Drop all cached renders (call on session switch / new chat). */
|
|
36
|
+
export function clearMarkdownCache() {
|
|
37
|
+
cache.clear();
|
|
38
|
+
}
|