@elytracms/studio 0.0.57 → 0.0.59
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/components/ai/ai-chat-panel.js +44 -6
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/chat-ui.d.ts +1 -1
- package/dist/components/ai/chat-ui.js +11 -2
- package/dist/components/ai/chat-ui.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +30 -7
- package/dist/components/ai/proposal-card.js +81 -21
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/builder/blocks-model.js +12 -7
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +59 -0
- package/dist/components/builder/canvas-dnd.js +207 -0
- package/dist/components/builder/canvas-dnd.js.map +1 -0
- package/dist/components/builder/canvas-dom.d.ts +11 -0
- package/dist/components/builder/canvas-dom.js +17 -0
- package/dist/components/builder/canvas-dom.js.map +1 -0
- package/dist/components/builder/canvas-inline-editing.d.ts +91 -0
- package/dist/components/builder/canvas-inline-editing.js +242 -0
- package/dist/components/builder/canvas-inline-editing.js.map +1 -0
- package/dist/components/builder/canvas-render-context.d.ts +83 -0
- package/dist/components/builder/canvas-render-context.js +196 -0
- package/dist/components/builder/canvas-render-context.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +4 -1
- package/dist/components/builder/canvas.js +32 -567
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +14 -4
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +5 -2
- package/dist/components/builder/insert-context.js +1 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/node-factory.d.ts +12 -0
- package/dist/components/builder/node-factory.js +18 -1
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/cms/asset-field.d.ts +22 -0
- package/dist/components/cms/asset-field.js +300 -0
- package/dist/components/cms/asset-field.js.map +1 -0
- package/dist/components/cms/detail-editor.js +12 -0
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +12 -1
- package/dist/components/cms/document-diff-view.js +119 -42
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/field-control-lib.d.ts +43 -0
- package/dist/components/cms/field-control-lib.js +98 -0
- package/dist/components/cms/field-control-lib.js.map +1 -0
- package/dist/components/cms/field-control.d.ts +7 -35
- package/dist/components/cms/field-control.js +13 -556
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/object-array-field.d.ts +29 -0
- package/dist/components/cms/object-array-field.js +205 -0
- package/dist/components/cms/object-array-field.js.map +1 -0
- package/dist/components/cms/rich-text-preview.d.ts +15 -0
- package/dist/components/cms/rich-text-preview.js +81 -0
- package/dist/components/cms/rich-text-preview.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +26 -0
- package/dist/components/cms/version-compare-text.js +60 -0
- package/dist/components/cms/version-compare-text.js.map +1 -1
- package/dist/components/project-session.d.ts +19 -0
- package/dist/components/project-session.js +29 -2
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/use-hydration-phase.d.ts +6 -4
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/lib/ai/apply-content.js +52 -10
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +19 -3
- package/dist/lib/ai/chat-context.js +33 -4
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +6 -3
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +60 -5
- package/dist/lib/ai/fake-provider.js +74 -6
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/rich-text-input.d.ts +27 -0
- package/dist/lib/ai/rich-text-input.js +25 -0
- package/dist/lib/ai/rich-text-input.js.map +1 -0
- package/dist/lib/ai/shape.d.ts +12 -1
- package/dist/lib/ai/shape.js +46 -20
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +88 -13
- package/dist/lib/ai/tools.js +253 -14
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/types.d.ts +20 -0
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +68 -0
- package/dist/lib/ai/update-fields.js +296 -0
- package/dist/lib/ai/update-fields.js.map +1 -0
- package/dist/lib/ai-current-document.d.ts +53 -0
- package/dist/lib/ai-current-document.js +54 -0
- package/dist/lib/ai-current-document.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +6 -6
- package/dist/lib/data/collection-content.d.ts +12 -7
- package/dist/lib/data/collection-content.js +6 -4
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +10 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +4 -1
- package/dist/lib/operations/session-hydration.js +4 -1
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/index.js +13 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +1 -1
- package/dist/lib/workspace/index.js +1 -1
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +17 -22
- package/dist/lib/workspace/workspace-source.js +1 -49
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `buildUpdateProposal` — the EC-378 update-proposal builder (the edit-flow
|
|
3
|
+
* counterpart of `applyContentWithShape`, minus the generation step).
|
|
4
|
+
*
|
|
5
|
+
* The MODEL authors the field changes directly in the `updateFields` tool input
|
|
6
|
+
* (it has the conversation + `getDocument` output as context); this module turns
|
|
7
|
+
* those changes into a reviewable proposal with the SAME engine discipline the
|
|
8
|
+
* create flow has:
|
|
9
|
+
*
|
|
10
|
+
* - **merge semantics**: changes apply per field over the EXISTING document —
|
|
11
|
+
* never a whole-document rewrite; the patch is the merged document, the diff
|
|
12
|
+
* (`diffDocuments`) is what the editor reviews (changed fields only).
|
|
13
|
+
* - **targeting is addressed with the engine's existing conventions** — field
|
|
14
|
+
* name + optional locale bucket (the diff / `RestoreScope` shape) and, inside
|
|
15
|
+
* a `blocks` field, `blockId` + `prop` (the `BlockChange`/`PropChange` shape,
|
|
16
|
+
* root blocks by stable node id). No new path grammar.
|
|
17
|
+
* - **leashes hold, loudly**: `ai.exclude` fields are untouchable; relation AND
|
|
18
|
+
* asset ids (EC-381) must come from searched candidates (`searchDocuments` /
|
|
19
|
+
* `listAssets`) or already be on the document; every targeting failure is an
|
|
20
|
+
* explicit issue — never a silent drop (unlike the create shape, which makes bad
|
|
21
|
+
* ids structurally unrepresentable, an update change is imperative — so it FAILS
|
|
22
|
+
* instead of being filtered).
|
|
23
|
+
* - **validation parity**: the merged document runs through `validateDocument`
|
|
24
|
+
* with the studio's injected leashes. Only errors the change INTRODUCES block
|
|
25
|
+
* the proposal — pre-existing brokenness on the document is not the
|
|
26
|
+
* assistant's doing and must not force a whole-document repair (the editor's
|
|
27
|
+
* own save path tolerates it the same way; errors gate publish, not drafts).
|
|
28
|
+
*
|
|
29
|
+
* Locale-awareness: a localized field's change lands in the change's `locale`,
|
|
30
|
+
* defaulting to the editor's working locale (`input.locale`). Non-localized
|
|
31
|
+
* fields reject a `locale` loudly.
|
|
32
|
+
*
|
|
33
|
+
* Pure and deterministic — no provider, no I/O; the executor (`tools.ts`) wires
|
|
34
|
+
* it to `getDocument` + the candidate store and hands the result to the review
|
|
35
|
+
* sink.
|
|
36
|
+
*/
|
|
37
|
+
import { assetNode, cmsIssue, isFieldLocalized, readAssetId, readDocumentRef, referenceNode, validateDocument, } from '@elytracms/core/cms-core';
|
|
38
|
+
import { blockRootsOf, staticProp } from '@elytracms/core/project-graph';
|
|
39
|
+
import { diffDocuments } from '@elytracms/core/content';
|
|
40
|
+
import { richTextValueFromModelInput } from './rich-text-input.js';
|
|
41
|
+
function isNonEmptyString(value) {
|
|
42
|
+
return typeof value === 'string' && value.length > 0;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The present/absent-key field write — the same semantics as the editor's
|
|
46
|
+
* single-sourced `setFieldValue` (`lib/locale/fallback`): a resolved `locale`
|
|
47
|
+
* writes the `localized[locale]` bucket, clearing DELETES the key. Local here
|
|
48
|
+
* (not imported) because this module takes the locale as an already-resolved
|
|
49
|
+
* argument per change; the semantics are pinned by tests on both sides.
|
|
50
|
+
*/
|
|
51
|
+
function writeFieldValue(doc, fieldName, locale, value) {
|
|
52
|
+
if (locale !== undefined) {
|
|
53
|
+
const perLocale = { ...(doc.localized?.[locale] ?? {}) };
|
|
54
|
+
if (value === undefined)
|
|
55
|
+
delete perLocale[fieldName];
|
|
56
|
+
else
|
|
57
|
+
perLocale[fieldName] = value;
|
|
58
|
+
return { ...doc, localized: { ...doc.localized, [locale]: perLocale } };
|
|
59
|
+
}
|
|
60
|
+
const values = { ...doc.values };
|
|
61
|
+
if (value === undefined)
|
|
62
|
+
delete values[fieldName];
|
|
63
|
+
else
|
|
64
|
+
values[fieldName] = value;
|
|
65
|
+
return { ...doc, values };
|
|
66
|
+
}
|
|
67
|
+
/** Relation ids CURRENTLY on the document for a field (any bucket) — re-usable without a search. */
|
|
68
|
+
function currentRelationIds(doc, fieldName) {
|
|
69
|
+
const ids = new Set();
|
|
70
|
+
const collect = (value) => {
|
|
71
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
72
|
+
const ref = readDocumentRef(entry);
|
|
73
|
+
if (ref)
|
|
74
|
+
ids.add(ref.id);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
collect(doc.values[fieldName]);
|
|
78
|
+
for (const bucket of Object.values(doc.localized ?? {}))
|
|
79
|
+
collect(bucket[fieldName]);
|
|
80
|
+
return ids;
|
|
81
|
+
}
|
|
82
|
+
/** Asset ids CURRENTLY on the document for a field (any bucket) — re-usable without a search (EC-381). */
|
|
83
|
+
function currentAssetIds(doc, fieldName) {
|
|
84
|
+
const ids = new Set();
|
|
85
|
+
const collect = (value) => {
|
|
86
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
87
|
+
const id = readAssetId(entry);
|
|
88
|
+
if (id)
|
|
89
|
+
ids.add(id);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
collect(doc.values[fieldName]);
|
|
93
|
+
for (const bucket of Object.values(doc.localized ?? {}))
|
|
94
|
+
collect(bucket[fieldName]);
|
|
95
|
+
return ids;
|
|
96
|
+
}
|
|
97
|
+
/** Map a change's raw value into the stored value shape (envelopes; loud leash). */
|
|
98
|
+
function mapUpdateValue(field, change, document, input, issue) {
|
|
99
|
+
const raw = change.value;
|
|
100
|
+
if (raw === undefined || raw === null)
|
|
101
|
+
return { value: undefined }; // clear
|
|
102
|
+
switch (field.type) {
|
|
103
|
+
case 'relation': {
|
|
104
|
+
const ids = Array.isArray(raw) ? raw : [raw];
|
|
105
|
+
if (!ids.every(isNonEmptyString)) {
|
|
106
|
+
return issue('invalid-field-value', `Relation "${field.name}" takes document id string(s) from searchDocuments results.`);
|
|
107
|
+
}
|
|
108
|
+
// The id leash (same stance as the create enum, but LOUD): only searched
|
|
109
|
+
// candidate ids or ids already on the document are usable.
|
|
110
|
+
const allowed = new Set([
|
|
111
|
+
...(input.candidates?.[field.name] ?? []).map((c) => c.id),
|
|
112
|
+
...currentRelationIds(document, field.name),
|
|
113
|
+
]);
|
|
114
|
+
const unknown = ids.filter((id) => !allowed.has(id));
|
|
115
|
+
if (unknown.length > 0) {
|
|
116
|
+
return issue('unknown-relation-target', `Relation "${field.name}" may only use ids from searchDocuments results — search "${field.target}" first (got: ${unknown.join(', ')}).`);
|
|
117
|
+
}
|
|
118
|
+
if (!field.multiple && ids.length > 1) {
|
|
119
|
+
return issue('invalid-field-value', `Field "${field.name}" holds a single reference — pass one id.`);
|
|
120
|
+
}
|
|
121
|
+
const refs = ids.map((id) => referenceNode({ collection: field.target, id }));
|
|
122
|
+
return { value: field.multiple ? refs : refs[0] };
|
|
123
|
+
}
|
|
124
|
+
case 'asset': {
|
|
125
|
+
const ids = Array.isArray(raw) ? raw : [raw];
|
|
126
|
+
if (!ids.every(isNonEmptyString)) {
|
|
127
|
+
return issue('invalid-field-value', `Asset field "${field.name}" takes asset id string(s) from listAssets results.`);
|
|
128
|
+
}
|
|
129
|
+
// EC-381 — the asset id leash (same stance as relations, but LOUD): only
|
|
130
|
+
// searched library candidate ids or ids already on the document are usable;
|
|
131
|
+
// the assistant never invents or uploads an asset.
|
|
132
|
+
const allowed = new Set([
|
|
133
|
+
...(input.assetCandidates?.[field.name] ?? []).map((c) => c.id),
|
|
134
|
+
...currentAssetIds(document, field.name),
|
|
135
|
+
]);
|
|
136
|
+
const unknown = ids.filter((id) => !allowed.has(id));
|
|
137
|
+
if (unknown.length > 0) {
|
|
138
|
+
return issue('unknown-asset', `Asset field "${field.name}" may only use ids from listAssets results — search the media library first (got: ${unknown.join(', ')}).`);
|
|
139
|
+
}
|
|
140
|
+
if (!field.multiple && ids.length > 1) {
|
|
141
|
+
return issue('invalid-field-value', `Field "${field.name}" holds a single asset — pass one id.`);
|
|
142
|
+
}
|
|
143
|
+
const nodes = ids.map((id) => assetNode(id));
|
|
144
|
+
return { value: field.multiple ? nodes : nodes[0] };
|
|
145
|
+
}
|
|
146
|
+
case 'richText': {
|
|
147
|
+
// EC-379: a string value is MARKDOWN, converted through the engine's strict
|
|
148
|
+
// subset converter; out-of-subset constructs fail LOUD (the model retries
|
|
149
|
+
// in-conversation). A `{ version, doc }` envelope object stays accepted for
|
|
150
|
+
// robustness (getDocument may have shown one) — see rich-text-input.ts.
|
|
151
|
+
const converted = richTextValueFromModelInput(raw);
|
|
152
|
+
if (!converted.ok) {
|
|
153
|
+
return issue('invalid-field-value', `Field "${field.name}": ${converted.problems.join(' ')}`);
|
|
154
|
+
}
|
|
155
|
+
return { value: converted.value };
|
|
156
|
+
}
|
|
157
|
+
default:
|
|
158
|
+
// text / number / boolean / date / select / object / blocks: as-is
|
|
159
|
+
// (the merged document's validateDocument run is the shape leash).
|
|
160
|
+
return { value: raw };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Change one component prop of one root block (the `BlockChange`/`PropChange` address). */
|
|
164
|
+
function applyBlockPropChange(doc, field, change, locale, issue) {
|
|
165
|
+
if (field.type !== 'blocks') {
|
|
166
|
+
return issue('invalid-field-value', `"${change.field}" is not a blocks field — omit "blockId"/"prop" (set the field value directly).`);
|
|
167
|
+
}
|
|
168
|
+
if (!isNonEmptyString(change.blockId)) {
|
|
169
|
+
return issue('invalid-field-value', `Changing a block on "${change.field}" needs "blockId" — block ids come from getDocument.`);
|
|
170
|
+
}
|
|
171
|
+
if (!isNonEmptyString(change.prop)) {
|
|
172
|
+
return issue('invalid-field-value', `Changing block "${change.blockId}" needs "prop" — the component property to change.`);
|
|
173
|
+
}
|
|
174
|
+
const bucket = locale !== undefined ? doc.localized?.[locale] : doc.values;
|
|
175
|
+
const original = bucket?.[change.field];
|
|
176
|
+
const blocks = blockRootsOf(original);
|
|
177
|
+
const index = blocks.findIndex((block) => block.id === change.blockId);
|
|
178
|
+
if (index < 0) {
|
|
179
|
+
return issue('invalid-field-value', `Block "${change.blockId}" was not found on "${change.field}" — read the document first; block ids come from getDocument.`);
|
|
180
|
+
}
|
|
181
|
+
const block = blocks[index];
|
|
182
|
+
const props = { ...(block.props ?? {}) };
|
|
183
|
+
if (change.value === undefined || change.value === null)
|
|
184
|
+
delete props[change.prop];
|
|
185
|
+
else
|
|
186
|
+
props[change.prop] = staticProp(change.value);
|
|
187
|
+
const nextBlock = { ...block, props };
|
|
188
|
+
const nextBlocks = blocks.map((b, i) => (i === index ? nextBlock : b));
|
|
189
|
+
// Preserve the stored shape: an array for a `multiple` composition, a bare node otherwise.
|
|
190
|
+
const value = Array.isArray(original) ? nextBlocks : nextBlocks[0];
|
|
191
|
+
return { doc: writeFieldValue(doc, change.field, locale, value) };
|
|
192
|
+
}
|
|
193
|
+
/** Apply ONE change to a (copy of the) document, or report the targeting issue. */
|
|
194
|
+
function applyOneChange(input, doc, change) {
|
|
195
|
+
const { collection } = input;
|
|
196
|
+
const issue = (code, message) => ({
|
|
197
|
+
issue: cmsIssue({
|
|
198
|
+
code,
|
|
199
|
+
message,
|
|
200
|
+
path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field],
|
|
201
|
+
collectionId: collection.id,
|
|
202
|
+
documentId: doc.id,
|
|
203
|
+
meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
const field = collection.fields.find((f) => f.name === change.field);
|
|
207
|
+
if (!field) {
|
|
208
|
+
return issue('invalid-field-value', `Unknown field "${change.field}" on collection "${collection.id}" — use a field name from the schema.`);
|
|
209
|
+
}
|
|
210
|
+
if (field.ai?.exclude) {
|
|
211
|
+
return issue('invalid-field-value', `Field "${change.field}" must not be edited by the assistant.`);
|
|
212
|
+
}
|
|
213
|
+
const localized = isFieldLocalized(collection, change.field);
|
|
214
|
+
if (!localized && change.locale !== undefined) {
|
|
215
|
+
return issue('unknown-locale', `Field "${change.field}" is not localized — omit "locale".`);
|
|
216
|
+
}
|
|
217
|
+
if (change.locale !== undefined &&
|
|
218
|
+
input.localeConfig &&
|
|
219
|
+
!input.localeConfig.locales.includes(change.locale)) {
|
|
220
|
+
return issue('unknown-locale', `Unknown locale "${change.locale}" — this project has: ${input.localeConfig.locales.join(', ')}.`);
|
|
221
|
+
}
|
|
222
|
+
const locale = localized ? change.locale ?? input.locale : undefined;
|
|
223
|
+
if (localized && locale === undefined) {
|
|
224
|
+
return issue('missing-localized-value', `Field "${change.field}" is localized — provide "locale".`);
|
|
225
|
+
}
|
|
226
|
+
if (change.blockId !== undefined || change.prop !== undefined) {
|
|
227
|
+
return applyBlockPropChange(doc, field, change, locale, issue);
|
|
228
|
+
}
|
|
229
|
+
const mapped = mapUpdateValue(field, change, input.document, input, issue);
|
|
230
|
+
if ('issue' in mapped)
|
|
231
|
+
return mapped;
|
|
232
|
+
return { doc: writeFieldValue(doc, change.field, locale, mapped.value) };
|
|
233
|
+
}
|
|
234
|
+
/** Identity for "is this error pre-existing?" — code + path + message. */
|
|
235
|
+
function issueKey(issue) {
|
|
236
|
+
return `${issue.code}|${issue.path.join('.')}|${issue.message}`;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Build the reviewable update proposal: apply every change over a copy of the
|
|
240
|
+
* document (loud targeting), validate the merged result with the injected
|
|
241
|
+
* leashes (only errors the change INTRODUCES block), and report the semantic
|
|
242
|
+
* diff. `no-change` when the changes resolve to the current values.
|
|
243
|
+
*/
|
|
244
|
+
export function buildUpdateProposal(input) {
|
|
245
|
+
const { registry, collection, document } = input;
|
|
246
|
+
let next = structuredClone(document);
|
|
247
|
+
const targetingIssues = [];
|
|
248
|
+
for (const change of input.changes) {
|
|
249
|
+
const outcome = applyOneChange(input, next, change);
|
|
250
|
+
if ('issue' in outcome)
|
|
251
|
+
targetingIssues.push(outcome.issue);
|
|
252
|
+
else
|
|
253
|
+
next = outcome.doc;
|
|
254
|
+
}
|
|
255
|
+
if (targetingIssues.length > 0)
|
|
256
|
+
return { ok: false, issues: targetingIssues };
|
|
257
|
+
const validateOptions = {
|
|
258
|
+
...(input.store !== undefined ? { store: input.store } : {}),
|
|
259
|
+
...(input.localeConfig !== undefined ? { localeConfig: input.localeConfig } : {}),
|
|
260
|
+
...(input.validateComposition !== undefined
|
|
261
|
+
? { validateComposition: input.validateComposition }
|
|
262
|
+
: {}),
|
|
263
|
+
...(input.validateRichTextEmbeds !== undefined
|
|
264
|
+
? { validateRichTextEmbeds: input.validateRichTextEmbeds }
|
|
265
|
+
: {}),
|
|
266
|
+
};
|
|
267
|
+
const preExisting = new Set(validateDocument(registry, document, validateOptions)
|
|
268
|
+
.filter((issue) => issue.severity === 'error')
|
|
269
|
+
.map(issueKey));
|
|
270
|
+
const issues = validateDocument(registry, next, validateOptions);
|
|
271
|
+
const introduced = issues.filter((issue) => issue.severity === 'error' && !preExisting.has(issueKey(issue)));
|
|
272
|
+
if (introduced.length > 0)
|
|
273
|
+
return { ok: false, issues: introduced };
|
|
274
|
+
const diff = diffDocuments(collection, document, next);
|
|
275
|
+
if (diff.changes.length === 0)
|
|
276
|
+
return { ok: true, kind: 'no-change' };
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
kind: 'proposal',
|
|
280
|
+
patch: {
|
|
281
|
+
collection: collection.id,
|
|
282
|
+
id: document.id,
|
|
283
|
+
values: next.values,
|
|
284
|
+
...(next.localized && Object.keys(next.localized).length > 0
|
|
285
|
+
? { localized: next.localized }
|
|
286
|
+
: {}),
|
|
287
|
+
},
|
|
288
|
+
before: {
|
|
289
|
+
values: document.values,
|
|
290
|
+
...(document.localized ? { localized: document.localized } : {}),
|
|
291
|
+
},
|
|
292
|
+
changedFields: diff.changes.map((change) => change.locale !== undefined ? `${change.field} (${change.locale})` : change.field),
|
|
293
|
+
issues: issues.filter((issue) => issue.severity !== 'error'),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=update-fields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-fields.js","sourceRoot":"","sources":["../../../src/lib/ai/update-fields.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `buildUpdateProposal` — the EC-378 update-proposal builder (the edit-flow\n * counterpart of `applyContentWithShape`, minus the generation step).\n *\n * The MODEL authors the field changes directly in the `updateFields` tool input\n * (it has the conversation + `getDocument` output as context); this module turns\n * those changes into a reviewable proposal with the SAME engine discipline the\n * create flow has:\n *\n * - **merge semantics**: changes apply per field over the EXISTING document —\n * never a whole-document rewrite; the patch is the merged document, the diff\n * (`diffDocuments`) is what the editor reviews (changed fields only).\n * - **targeting is addressed with the engine's existing conventions** — field\n * name + optional locale bucket (the diff / `RestoreScope` shape) and, inside\n * a `blocks` field, `blockId` + `prop` (the `BlockChange`/`PropChange` shape,\n * root blocks by stable node id). No new path grammar.\n * - **leashes hold, loudly**: `ai.exclude` fields are untouchable; relation AND\n * asset ids (EC-381) must come from searched candidates (`searchDocuments` /\n * `listAssets`) or already be on the document; every targeting failure is an\n * explicit issue — never a silent drop (unlike the create shape, which makes bad\n * ids structurally unrepresentable, an update change is imperative — so it FAILS\n * instead of being filtered).\n * - **validation parity**: the merged document runs through `validateDocument`\n * with the studio's injected leashes. Only errors the change INTRODUCES block\n * the proposal — pre-existing brokenness on the document is not the\n * assistant's doing and must not force a whole-document repair (the editor's\n * own save path tolerates it the same way; errors gate publish, not drafts).\n *\n * Locale-awareness: a localized field's change lands in the change's `locale`,\n * defaulting to the editor's working locale (`input.locale`). Non-localized\n * fields reject a `locale` loudly.\n *\n * Pure and deterministic — no provider, no I/O; the executor (`tools.ts`) wires\n * it to `getDocument` + the candidate store and hands the result to the review\n * sink.\n */\nimport {\n assetNode,\n cmsIssue,\n isFieldLocalized,\n readAssetId,\n readDocumentRef,\n referenceNode,\n validateDocument,\n} from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n FieldDef,\n FieldValues,\n ValidateDocumentOptions,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport { blockRootsOf, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { diffDocuments } from '@elytracms/core/content'\nimport { richTextValueFromModelInput } from './rich-text-input'\nimport type { AssetCandidates, ContentPatch, RelationCandidates } from './types'\n\n/**\n * One field change of an `updateFields` proposal, addressed with the engine's\n * existing conventions (diff / restore-scope): `field` (+ `locale` bucket for a\n * localized field), and for a `blocks` field optionally `blockId` + `prop` to\n * change one component prop of one root block. `value` is the new value\n * (`null`/omitted clears the field or prop).\n */\nexport interface UpdateFieldChange {\n field: string\n /** Locale bucket for a LOCALIZED field (defaults to the editor's working locale). */\n locale?: string\n /** For a `blocks` field: the root block's stable node id (from `getDocument`). */\n blockId?: string\n /** With `blockId`: the component prop to change. */\n prop?: string\n /** The new value; `null` / omitted clears the field or prop. */\n value?: unknown\n}\n\nexport interface BuildUpdateProposalInput {\n registry: CollectionRegistry\n collection: CollectionDef\n /** The CURRENT document — the proposal's \"before\" side. Never mutated. */\n document: CmsDocument\n changes: readonly UpdateFieldChange[]\n /** The editor's working locale — the default bucket for localized fields. */\n locale?: string\n localeConfig?: LocaleConfig\n /** Relation candidates (searched ids) keyed by field name — the id leash. */\n candidates?: RelationCandidates\n /** Asset candidates (searched library ids) keyed by field name — the asset id leash (EC-381). */\n assetCandidates?: AssetCandidates\n /** Injected studio leashes — same discipline as the create engine. */\n validateRichTextEmbeds?: ValidateDocumentOptions['validateRichTextEmbeds']\n validateComposition?: ValidateDocumentOptions['validateComposition']\n store?: ValidateDocumentOptions['store']\n}\n\n/** The `before` snapshot the review diff renders against. */\nexport interface UpdateBeforeSnapshot {\n values: FieldValues\n localized?: Record<string, FieldValues>\n}\n\nexport type BuildUpdateProposalResult =\n | {\n ok: true\n kind: 'proposal'\n /** The MERGED document as a patch (full values; the diff shows changed fields only). */\n patch: ContentPatch\n before: UpdateBeforeSnapshot\n /** Human-readable descriptors of the changed fields (e.g. `title (de)`). */\n changedFields: string[]\n /** Non-error issues on the accepted result (warnings). */\n issues: CmsValidationIssue[]\n }\n | {\n /** Every change resolved to the current values — nothing to propose. */\n ok: true\n kind: 'no-change'\n }\n | { ok: false; issues: CmsValidationIssue[] }\n\ntype ChangeIssue = { issue: CmsValidationIssue }\ntype ChangeOutcome = { doc: CmsDocument } | ChangeIssue\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0\n}\n\n/**\n * The present/absent-key field write — the same semantics as the editor's\n * single-sourced `setFieldValue` (`lib/locale/fallback`): a resolved `locale`\n * writes the `localized[locale]` bucket, clearing DELETES the key. Local here\n * (not imported) because this module takes the locale as an already-resolved\n * argument per change; the semantics are pinned by tests on both sides.\n */\nfunction writeFieldValue(\n doc: CmsDocument,\n fieldName: string,\n locale: string | undefined,\n value: unknown,\n): CmsDocument {\n if (locale !== undefined) {\n const perLocale = { ...(doc.localized?.[locale] ?? {}) }\n if (value === undefined) delete perLocale[fieldName]\n else perLocale[fieldName] = value\n return { ...doc, localized: { ...doc.localized, [locale]: perLocale } }\n }\n const values = { ...doc.values }\n if (value === undefined) delete values[fieldName]\n else values[fieldName] = value\n return { ...doc, values }\n}\n\n/** Relation ids CURRENTLY on the document for a field (any bucket) — re-usable without a search. */\nfunction currentRelationIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const ref = readDocumentRef(entry)\n if (ref) ids.add(ref.id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/** Asset ids CURRENTLY on the document for a field (any bucket) — re-usable without a search (EC-381). */\nfunction currentAssetIds(doc: CmsDocument, fieldName: string): Set<string> {\n const ids = new Set<string>()\n const collect = (value: unknown) => {\n for (const entry of Array.isArray(value) ? value : [value]) {\n const id = readAssetId(entry)\n if (id) ids.add(id)\n }\n }\n collect(doc.values[fieldName])\n for (const bucket of Object.values(doc.localized ?? {})) collect(bucket[fieldName])\n return ids\n}\n\n/** Map a change's raw value into the stored value shape (envelopes; loud leash). */\nfunction mapUpdateValue(\n field: FieldDef,\n change: UpdateFieldChange,\n document: CmsDocument,\n input: BuildUpdateProposalInput,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): { value: unknown } | ChangeIssue {\n const raw = change.value\n if (raw === undefined || raw === null) return { value: undefined } // clear\n switch (field.type) {\n case 'relation': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Relation \"${field.name}\" takes document id string(s) from searchDocuments results.`,\n )\n }\n // The id leash (same stance as the create enum, but LOUD): only searched\n // candidate ids or ids already on the document are usable.\n const allowed = new Set([\n ...(input.candidates?.[field.name] ?? []).map((c) => c.id),\n ...currentRelationIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-relation-target',\n `Relation \"${field.name}\" may only use ids from searchDocuments results — search \"${field.target}\" first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single reference — pass one id.`)\n }\n const refs = ids.map((id) => referenceNode({ collection: field.target, id }))\n return { value: field.multiple ? refs : refs[0] }\n }\n case 'asset': {\n const ids = Array.isArray(raw) ? raw : [raw]\n if (!ids.every(isNonEmptyString)) {\n return issue(\n 'invalid-field-value',\n `Asset field \"${field.name}\" takes asset id string(s) from listAssets results.`,\n )\n }\n // EC-381 — the asset id leash (same stance as relations, but LOUD): only\n // searched library candidate ids or ids already on the document are usable;\n // the assistant never invents or uploads an asset.\n const allowed = new Set([\n ...(input.assetCandidates?.[field.name] ?? []).map((c) => c.id),\n ...currentAssetIds(document, field.name),\n ])\n const unknown = ids.filter((id) => !allowed.has(id))\n if (unknown.length > 0) {\n return issue(\n 'unknown-asset',\n `Asset field \"${field.name}\" may only use ids from listAssets results — search the media library first (got: ${unknown.join(', ')}).`,\n )\n }\n if (!field.multiple && ids.length > 1) {\n return issue('invalid-field-value', `Field \"${field.name}\" holds a single asset — pass one id.`)\n }\n const nodes = ids.map((id) => assetNode(id))\n return { value: field.multiple ? nodes : nodes[0] }\n }\n case 'richText': {\n // EC-379: a string value is MARKDOWN, converted through the engine's strict\n // subset converter; out-of-subset constructs fail LOUD (the model retries\n // in-conversation). A `{ version, doc }` envelope object stays accepted for\n // robustness (getDocument may have shown one) — see rich-text-input.ts.\n const converted = richTextValueFromModelInput(raw)\n if (!converted.ok) {\n return issue('invalid-field-value', `Field \"${field.name}\": ${converted.problems.join(' ')}`)\n }\n return { value: converted.value }\n }\n default:\n // text / number / boolean / date / select / object / blocks: as-is\n // (the merged document's validateDocument run is the shape leash).\n return { value: raw }\n }\n}\n\n/** Change one component prop of one root block (the `BlockChange`/`PropChange` address). */\nfunction applyBlockPropChange(\n doc: CmsDocument,\n field: FieldDef,\n change: UpdateFieldChange,\n locale: string | undefined,\n issue: (code: Parameters<typeof cmsIssue>[0]['code'], message: string) => ChangeIssue,\n): ChangeOutcome {\n if (field.type !== 'blocks') {\n return issue(\n 'invalid-field-value',\n `\"${change.field}\" is not a blocks field — omit \"blockId\"/\"prop\" (set the field value directly).`,\n )\n }\n if (!isNonEmptyString(change.blockId)) {\n return issue(\n 'invalid-field-value',\n `Changing a block on \"${change.field}\" needs \"blockId\" — block ids come from getDocument.`,\n )\n }\n if (!isNonEmptyString(change.prop)) {\n return issue(\n 'invalid-field-value',\n `Changing block \"${change.blockId}\" needs \"prop\" — the component property to change.`,\n )\n }\n const bucket = locale !== undefined ? doc.localized?.[locale] : doc.values\n const original = bucket?.[change.field]\n const blocks = blockRootsOf(original)\n const index = blocks.findIndex((block) => block.id === change.blockId)\n if (index < 0) {\n return issue(\n 'invalid-field-value',\n `Block \"${change.blockId}\" was not found on \"${change.field}\" — read the document first; block ids come from getDocument.`,\n )\n }\n const block = blocks[index]!\n const props = { ...(block.props ?? {}) }\n if (change.value === undefined || change.value === null) delete props[change.prop]\n else props[change.prop] = staticProp(change.value as JsonValue)\n const nextBlock: ComponentNode = { ...block, props }\n const nextBlocks = blocks.map((b, i) => (i === index ? nextBlock : b))\n // Preserve the stored shape: an array for a `multiple` composition, a bare node otherwise.\n const value = Array.isArray(original) ? nextBlocks : nextBlocks[0]\n return { doc: writeFieldValue(doc, change.field, locale, value) }\n}\n\n/** Apply ONE change to a (copy of the) document, or report the targeting issue. */\nfunction applyOneChange(\n input: BuildUpdateProposalInput,\n doc: CmsDocument,\n change: UpdateFieldChange,\n): ChangeOutcome {\n const { collection } = input\n const issue = (\n code: Parameters<typeof cmsIssue>[0]['code'],\n message: string,\n ): ChangeIssue => ({\n issue: cmsIssue({\n code,\n message,\n path: ['documents', `${collection.id}:${doc.id}`, 'values', change.field],\n collectionId: collection.id,\n documentId: doc.id,\n meta: { field: change.field, ...(change.locale !== undefined ? { locale: change.locale } : {}) },\n }),\n })\n\n const field = collection.fields.find((f) => f.name === change.field)\n if (!field) {\n return issue(\n 'invalid-field-value',\n `Unknown field \"${change.field}\" on collection \"${collection.id}\" — use a field name from the schema.`,\n )\n }\n if (field.ai?.exclude) {\n return issue('invalid-field-value', `Field \"${change.field}\" must not be edited by the assistant.`)\n }\n\n const localized = isFieldLocalized(collection, change.field)\n if (!localized && change.locale !== undefined) {\n return issue('unknown-locale', `Field \"${change.field}\" is not localized — omit \"locale\".`)\n }\n if (\n change.locale !== undefined &&\n input.localeConfig &&\n !input.localeConfig.locales.includes(change.locale)\n ) {\n return issue(\n 'unknown-locale',\n `Unknown locale \"${change.locale}\" — this project has: ${input.localeConfig.locales.join(', ')}.`,\n )\n }\n const locale = localized ? change.locale ?? input.locale : undefined\n if (localized && locale === undefined) {\n return issue('missing-localized-value', `Field \"${change.field}\" is localized — provide \"locale\".`)\n }\n\n if (change.blockId !== undefined || change.prop !== undefined) {\n return applyBlockPropChange(doc, field, change, locale, issue)\n }\n\n const mapped = mapUpdateValue(field, change, input.document, input, issue)\n if ('issue' in mapped) return mapped\n return { doc: writeFieldValue(doc, change.field, locale, mapped.value) }\n}\n\n/** Identity for \"is this error pre-existing?\" — code + path + message. */\nfunction issueKey(issue: CmsValidationIssue): string {\n return `${issue.code}|${issue.path.join('.')}|${issue.message}`\n}\n\n/**\n * Build the reviewable update proposal: apply every change over a copy of the\n * document (loud targeting), validate the merged result with the injected\n * leashes (only errors the change INTRODUCES block), and report the semantic\n * diff. `no-change` when the changes resolve to the current values.\n */\nexport function buildUpdateProposal(input: BuildUpdateProposalInput): BuildUpdateProposalResult {\n const { registry, collection, document } = input\n\n let next: CmsDocument = structuredClone(document)\n const targetingIssues: CmsValidationIssue[] = []\n for (const change of input.changes) {\n const outcome = applyOneChange(input, next, change)\n if ('issue' in outcome) targetingIssues.push(outcome.issue)\n else next = outcome.doc\n }\n if (targetingIssues.length > 0) return { ok: false, issues: targetingIssues }\n\n const validateOptions: ValidateDocumentOptions = {\n ...(input.store !== undefined ? { store: input.store } : {}),\n ...(input.localeConfig !== undefined ? { localeConfig: input.localeConfig } : {}),\n ...(input.validateComposition !== undefined\n ? { validateComposition: input.validateComposition }\n : {}),\n ...(input.validateRichTextEmbeds !== undefined\n ? { validateRichTextEmbeds: input.validateRichTextEmbeds }\n : {}),\n }\n const preExisting = new Set(\n validateDocument(registry, document, validateOptions)\n .filter((issue) => issue.severity === 'error')\n .map(issueKey),\n )\n const issues = validateDocument(registry, next, validateOptions)\n const introduced = issues.filter(\n (issue) => issue.severity === 'error' && !preExisting.has(issueKey(issue)),\n )\n if (introduced.length > 0) return { ok: false, issues: introduced }\n\n const diff = diffDocuments(collection, document, next)\n if (diff.changes.length === 0) return { ok: true, kind: 'no-change' }\n\n return {\n ok: true,\n kind: 'proposal',\n patch: {\n collection: collection.id,\n id: document.id,\n values: next.values,\n ...(next.localized && Object.keys(next.localized).length > 0\n ? { localized: next.localized }\n : {}),\n },\n before: {\n values: document.values,\n ...(document.localized ? { localized: document.localized } : {}),\n },\n changedFields: diff.changes.map((change) =>\n change.locale !== undefined ? `${change.field} (${change.locale})` : change.field,\n ),\n issues: issues.filter((issue) => issue.severity !== 'error'),\n }\n}\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CmsDocument } from '@elytracms/core/cms-core';
|
|
2
|
+
/**
|
|
3
|
+
* The current-document seam (EC-378) — a framework-free module store, like the
|
|
4
|
+
* AI panel's open state (`lib/ai-panel`). The document editor (`EditorBody`)
|
|
5
|
+
* publishes WHICH document is open; the AI chat panel — a disjoint React
|
|
6
|
+
* subtree — reads it so "this document" / "that element" resolve without
|
|
7
|
+
* interrogating the user: the coordinates go into the model's context
|
|
8
|
+
* (advisory) AND into the tool executor's `currentDocument` thunk (the
|
|
9
|
+
* deterministic fallback for `getDocument` / `updateFields`).
|
|
10
|
+
*
|
|
11
|
+
* `adoptDraft` is the narrow write-back half of the seam: after the panel
|
|
12
|
+
* applies an UPDATE proposal to the open document, it must replace the
|
|
13
|
+
* editor's draft buffer (the editor clones its document once at mount —
|
|
14
|
+
* without adoption, its autosave would write the stale buffer back and
|
|
15
|
+
* silently revert the applied change). It is exactly the editor's own
|
|
16
|
+
* `replaceDraft` (the same channel the translate dialog uses via `onApplied`).
|
|
17
|
+
*
|
|
18
|
+
* This module imports NO AI runtime — publishing the tap from the always-
|
|
19
|
+
* mounted editor never pulls the AI chunk (the heavy panel stays behind its
|
|
20
|
+
* lazy boundary).
|
|
21
|
+
*/
|
|
22
|
+
export interface AiCurrentDocument {
|
|
23
|
+
/** Collection id — the coordinate tools target. */
|
|
24
|
+
collection: string;
|
|
25
|
+
/** Document id — the coordinate tools target. */
|
|
26
|
+
id: string;
|
|
27
|
+
/** Resolved display title (advisory — for the model's context block). */
|
|
28
|
+
title: string;
|
|
29
|
+
/** Replace the open editor's draft buffer with an externally applied update. */
|
|
30
|
+
adoptDraft: (next: CmsDocument) => void;
|
|
31
|
+
}
|
|
32
|
+
/** The document currently open in the editor, or null. */
|
|
33
|
+
export declare function getAiCurrentDocument(): AiCurrentDocument | null;
|
|
34
|
+
/** Publish (or clear with `null`) the open document. */
|
|
35
|
+
export declare function setAiCurrentDocument(next: AiCurrentDocument | null): void;
|
|
36
|
+
/** Clear the store ONLY if it still holds the given document (unmount race guard). */
|
|
37
|
+
export declare function clearAiCurrentDocument(ref: {
|
|
38
|
+
collection: string;
|
|
39
|
+
id: string;
|
|
40
|
+
}): void;
|
|
41
|
+
/** React binding — a surface that wants to REACT to the open document. */
|
|
42
|
+
export declare function useAiCurrentDocument(): AiCurrentDocument | null;
|
|
43
|
+
/**
|
|
44
|
+
* The editor-side tap: publish the open document while mounted, clear on
|
|
45
|
+
* unmount. `adoptDraft` is kept fresh through a ref so the published value only
|
|
46
|
+
* changes when the coordinates/title do (no churn per keystroke elsewhere).
|
|
47
|
+
*/
|
|
48
|
+
export declare function useAiCurrentDocumentTap(input: {
|
|
49
|
+
collection: string;
|
|
50
|
+
id: string;
|
|
51
|
+
title: string;
|
|
52
|
+
adoptDraft: (next: CmsDocument) => void;
|
|
53
|
+
}): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect, useRef, useSyncExternalStore } from 'react';
|
|
2
|
+
let current = null;
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
function emit() {
|
|
5
|
+
for (const listener of listeners)
|
|
6
|
+
listener();
|
|
7
|
+
}
|
|
8
|
+
/** The document currently open in the editor, or null. */
|
|
9
|
+
export function getAiCurrentDocument() {
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
12
|
+
/** Publish (or clear with `null`) the open document. */
|
|
13
|
+
export function setAiCurrentDocument(next) {
|
|
14
|
+
if (next === current)
|
|
15
|
+
return;
|
|
16
|
+
current = next;
|
|
17
|
+
emit();
|
|
18
|
+
}
|
|
19
|
+
/** Clear the store ONLY if it still holds the given document (unmount race guard). */
|
|
20
|
+
export function clearAiCurrentDocument(ref) {
|
|
21
|
+
if (current && current.collection === ref.collection && current.id === ref.id) {
|
|
22
|
+
setAiCurrentDocument(null);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function subscribe(listener) {
|
|
26
|
+
listeners.add(listener);
|
|
27
|
+
return () => {
|
|
28
|
+
listeners.delete(listener);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** React binding — a surface that wants to REACT to the open document. */
|
|
32
|
+
export function useAiCurrentDocument() {
|
|
33
|
+
return useSyncExternalStore(subscribe, getAiCurrentDocument, getAiCurrentDocument);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The editor-side tap: publish the open document while mounted, clear on
|
|
37
|
+
* unmount. `adoptDraft` is kept fresh through a ref so the published value only
|
|
38
|
+
* changes when the coordinates/title do (no churn per keystroke elsewhere).
|
|
39
|
+
*/
|
|
40
|
+
export function useAiCurrentDocumentTap(input) {
|
|
41
|
+
const adoptRef = useRef(input.adoptDraft);
|
|
42
|
+
adoptRef.current = input.adoptDraft;
|
|
43
|
+
const { collection, id, title } = input;
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setAiCurrentDocument({
|
|
46
|
+
collection,
|
|
47
|
+
id,
|
|
48
|
+
title,
|
|
49
|
+
adoptDraft: (next) => adoptRef.current(next),
|
|
50
|
+
});
|
|
51
|
+
return () => clearAiCurrentDocument({ collection, id });
|
|
52
|
+
}, [collection, id, title]);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=ai-current-document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-current-document.js","sourceRoot":"","sources":["../../src/lib/ai-current-document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAkC/D,IAAI,OAAO,GAA6B,IAAI,CAAA;AAC5C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,SAAS,IAAI;IACX,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAA;AAC9C,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,oBAAoB;IAClC,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,oBAAoB,CAAC,IAA8B;IACjE,IAAI,IAAI,KAAK,OAAO;QAAE,OAAM;IAC5B,OAAO,GAAG,IAAI,CAAA;IACd,IAAI,EAAE,CAAA;AACR,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,sBAAsB,CAAC,GAAuC;IAC5E,IAAI,OAAO,IAAI,OAAO,CAAC,UAAU,KAAK,GAAG,CAAC,UAAU,IAAI,OAAO,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,EAAE,CAAC;QAC9E,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAC5B,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,QAAoB;IACrC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB;IAClC,OAAO,oBAAoB,CAAC,SAAS,EAAE,oBAAoB,EAAE,oBAAoB,CAAC,CAAA;AACpF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAKvC;IACC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;IACzC,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC,UAAU,CAAA;IACnC,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,KAAK,CAAA;IACvC,SAAS,CAAC,GAAG,EAAE;QACb,oBAAoB,CAAC;YACnB,UAAU;YACV,EAAE;YACF,KAAK;YACL,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC;SAC7C,CAAC,CAAA;QACF,OAAO,GAAG,EAAE,CAAC,sBAAsB,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAA;IACzD,CAAC,EAAE,CAAC,UAAU,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,CAAA;AAC7B,CAAC","sourcesContent":["import { useEffect, useRef, useSyncExternalStore } from 'react'\nimport type { CmsDocument } from '@elytracms/core/cms-core'\n\n/**\n * The current-document seam (EC-378) — a framework-free module store, like the\n * AI panel's open state (`lib/ai-panel`). The document editor (`EditorBody`)\n * publishes WHICH document is open; the AI chat panel — a disjoint React\n * subtree — reads it so \"this document\" / \"that element\" resolve without\n * interrogating the user: the coordinates go into the model's context\n * (advisory) AND into the tool executor's `currentDocument` thunk (the\n * deterministic fallback for `getDocument` / `updateFields`).\n *\n * `adoptDraft` is the narrow write-back half of the seam: after the panel\n * applies an UPDATE proposal to the open document, it must replace the\n * editor's draft buffer (the editor clones its document once at mount —\n * without adoption, its autosave would write the stale buffer back and\n * silently revert the applied change). It is exactly the editor's own\n * `replaceDraft` (the same channel the translate dialog uses via `onApplied`).\n *\n * This module imports NO AI runtime — publishing the tap from the always-\n * mounted editor never pulls the AI chunk (the heavy panel stays behind its\n * lazy boundary).\n */\nexport interface AiCurrentDocument {\n /** Collection id — the coordinate tools target. */\n collection: string\n /** Document id — the coordinate tools target. */\n id: string\n /** Resolved display title (advisory — for the model's context block). */\n title: string\n /** Replace the open editor's draft buffer with an externally applied update. */\n adoptDraft: (next: CmsDocument) => void\n}\n\nlet current: AiCurrentDocument | null = null\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\n/** The document currently open in the editor, or null. */\nexport function getAiCurrentDocument(): AiCurrentDocument | null {\n return current\n}\n\n/** Publish (or clear with `null`) the open document. */\nexport function setAiCurrentDocument(next: AiCurrentDocument | null): void {\n if (next === current) return\n current = next\n emit()\n}\n\n/** Clear the store ONLY if it still holds the given document (unmount race guard). */\nexport function clearAiCurrentDocument(ref: { collection: string; id: string }): void {\n if (current && current.collection === ref.collection && current.id === ref.id) {\n setAiCurrentDocument(null)\n }\n}\n\nfunction subscribe(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/** React binding — a surface that wants to REACT to the open document. */\nexport function useAiCurrentDocument(): AiCurrentDocument | null {\n return useSyncExternalStore(subscribe, getAiCurrentDocument, getAiCurrentDocument)\n}\n\n/**\n * The editor-side tap: publish the open document while mounted, clear on\n * unmount. `adoptDraft` is kept fresh through a ref so the published value only\n * changes when the coordinates/title do (no churn per keystroke elsewhere).\n */\nexport function useAiCurrentDocumentTap(input: {\n collection: string\n id: string\n title: string\n adoptDraft: (next: CmsDocument) => void\n}): void {\n const adoptRef = useRef(input.adoptDraft)\n adoptRef.current = input.adoptDraft\n const { collection, id, title } = input\n useEffect(() => {\n setAiCurrentDocument({\n collection,\n id,\n title,\n adoptDraft: (next) => adoptRef.current(next),\n })\n return () => clearAiCurrentDocument({ collection, id })\n }, [collection, id, title])\n}\n"]}
|
|
@@ -24,13 +24,13 @@ export declare const filterClauseSchema: z.ZodObject<{
|
|
|
24
24
|
empty: "empty";
|
|
25
25
|
contains: "contains";
|
|
26
26
|
in: "in";
|
|
27
|
-
endsWith: "endsWith";
|
|
28
|
-
startsWith: "startsWith";
|
|
29
27
|
eq: "eq";
|
|
30
28
|
gt: "gt";
|
|
31
29
|
gte: "gte";
|
|
32
30
|
lt: "lt";
|
|
33
31
|
lte: "lte";
|
|
32
|
+
endsWith: "endsWith";
|
|
33
|
+
startsWith: "startsWith";
|
|
34
34
|
}>;
|
|
35
35
|
value: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>;
|
|
36
36
|
}, z.core.$strip>;
|
|
@@ -50,13 +50,13 @@ export declare const tableViewStateSchema: z.ZodObject<{
|
|
|
50
50
|
empty: "empty";
|
|
51
51
|
contains: "contains";
|
|
52
52
|
in: "in";
|
|
53
|
-
endsWith: "endsWith";
|
|
54
|
-
startsWith: "startsWith";
|
|
55
53
|
eq: "eq";
|
|
56
54
|
gt: "gt";
|
|
57
55
|
gte: "gte";
|
|
58
56
|
lt: "lt";
|
|
59
57
|
lte: "lte";
|
|
58
|
+
endsWith: "endsWith";
|
|
59
|
+
startsWith: "startsWith";
|
|
60
60
|
}>;
|
|
61
61
|
value: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>;
|
|
62
62
|
}, z.core.$strip>>;
|
|
@@ -206,13 +206,13 @@ export declare const configViewSchema: z.ZodObject<{
|
|
|
206
206
|
empty: "empty";
|
|
207
207
|
contains: "contains";
|
|
208
208
|
in: "in";
|
|
209
|
-
endsWith: "endsWith";
|
|
210
|
-
startsWith: "startsWith";
|
|
211
209
|
eq: "eq";
|
|
212
210
|
gt: "gt";
|
|
213
211
|
gte: "gte";
|
|
214
212
|
lt: "lt";
|
|
215
213
|
lte: "lte";
|
|
214
|
+
endsWith: "endsWith";
|
|
215
|
+
startsWith: "startsWith";
|
|
216
216
|
}>;
|
|
217
217
|
value: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>;
|
|
218
218
|
}, z.core.$strip>>>;
|
|
@@ -11,14 +11,17 @@ import type { ListingQuery } from '@elytracms/core/runtime-renderer';
|
|
|
11
11
|
* client (closed AD-3 query vocabulary, published perspective, EC-018 locale
|
|
12
12
|
* fallback, depth-1 population, resolved assets) over a **content session**:
|
|
13
13
|
*
|
|
14
|
-
* - Playground mode (and tests): the deterministic EC-145 sample fixtures —
|
|
15
|
-
* byte-identical to the pre-hydration behavior, no backend.
|
|
16
14
|
* - Convex mode: the project-session gate (`components/studio/project-session`)
|
|
17
15
|
* configures the session with the project's HYDRATED schema + documents, so
|
|
18
16
|
* "Preview with" lists and section previews show the live content — never
|
|
19
17
|
* fixture titles (EC-177 gap 1).
|
|
18
|
+
* - Playground mode (EC-361): the SAME hydration loader runs over the seeded
|
|
19
|
+
* in-memory adapter (through operations — AD-17), so canvas listing/relation
|
|
20
|
+
* population resolves from the in-memory session documents with identical
|
|
21
|
+
* semantics. With no session configured (tests, boot instants, no persisted
|
|
22
|
+
* content) the engine serves nothing.
|
|
20
23
|
*
|
|
21
|
-
* Nothing here invents query semantics; both
|
|
24
|
+
* Nothing here invents query semantics; both modes go through the same
|
|
22
25
|
* canonical client.
|
|
23
26
|
*/
|
|
24
27
|
/** The schema + documents the preview engine serves (one project session). */
|
|
@@ -55,7 +58,9 @@ export interface ContentSessionState {
|
|
|
55
58
|
* Default (no-session) collection schema: EMPTY. The studio ships no demo content
|
|
56
59
|
* (Oli, 2026-06-19) — with no backend/content session configured, there is nothing
|
|
57
60
|
* to preview, and the studio says so rather than serving fixtures. A real project's
|
|
58
|
-
* collections + documents arrive via {@link configureContentSession}
|
|
61
|
+
* collections + documents arrive via {@link configureContentSession} in BOTH modes
|
|
62
|
+
* (EC-361): Convex hydration, or the playground's seeded in-memory adapter through
|
|
63
|
+
* the same loader.
|
|
59
64
|
*/
|
|
60
65
|
export declare const playgroundCollections: readonly CollectionDef[];
|
|
61
66
|
/**
|
|
@@ -68,9 +73,9 @@ export declare const playgroundCollections: readonly CollectionDef[];
|
|
|
68
73
|
*/
|
|
69
74
|
export type CanvasPerspective = 'draft' | 'published';
|
|
70
75
|
/**
|
|
71
|
-
* Point the preview engine at a project's hydrated content
|
|
72
|
-
*
|
|
73
|
-
*
|
|
76
|
+
* Point the preview engine at a project's hydrated content — Convex hydration
|
|
77
|
+
* or the playground's in-memory persisted state (EC-361), both through the same
|
|
78
|
+
* `loadHydratedCms` loader — or back at the empty no-session default (`null`).
|
|
74
79
|
*/
|
|
75
80
|
export declare function configureContentSession(next: ContentSessionState | null): void;
|
|
76
81
|
/** Whether a hydrated content session is configured (Convex mode). */
|
|
@@ -5,7 +5,9 @@ import { getStudioLocale } from '../locale/locale-store.js';
|
|
|
5
5
|
* Default (no-session) collection schema: EMPTY. The studio ships no demo content
|
|
6
6
|
* (Oli, 2026-06-19) — with no backend/content session configured, there is nothing
|
|
7
7
|
* to preview, and the studio says so rather than serving fixtures. A real project's
|
|
8
|
-
* collections + documents arrive via {@link configureContentSession}
|
|
8
|
+
* collections + documents arrive via {@link configureContentSession} in BOTH modes
|
|
9
|
+
* (EC-361): Convex hydration, or the playground's seeded in-memory adapter through
|
|
10
|
+
* the same loader.
|
|
9
11
|
*/
|
|
10
12
|
export const playgroundCollections = [];
|
|
11
13
|
let contentSession = null;
|
|
@@ -13,9 +15,9 @@ let lookup = createLookup(playgroundCollections, [], []);
|
|
|
13
15
|
// Keyed by `${locale}:${perspective}` (EC-300) — one deterministic client per pair.
|
|
14
16
|
const clients = new Map();
|
|
15
17
|
/**
|
|
16
|
-
* Point the preview engine at a project's hydrated content
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* Point the preview engine at a project's hydrated content — Convex hydration
|
|
19
|
+
* or the playground's in-memory persisted state (EC-361), both through the same
|
|
20
|
+
* `loadHydratedCms` loader — or back at the empty no-session default (`null`).
|
|
19
21
|
*/
|
|
20
22
|
export function configureContentSession(next) {
|
|
21
23
|
contentSession = next;
|