@elytracms/studio 0.0.44 → 0.0.46
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/convex/cms.ts +60 -21
- package/convex/content.ts +6 -3
- package/convex/delivery.ts +8 -5
- package/convex/schema.ts +3 -0
- package/convex/triggers.ts +8 -1
- package/dist/components/ai/ai-chat-panel-lazy.d.ts +1 -0
- package/dist/components/ai/ai-chat-panel-lazy.js +21 -0
- package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -0
- package/dist/components/ai/ai-chat-panel.d.ts +10 -0
- package/dist/components/ai/ai-chat-panel.js +202 -0
- package/dist/components/ai/ai-chat-panel.js.map +1 -0
- package/dist/components/ai/ai-cms-panel.d.ts +9 -0
- package/dist/components/ai/ai-cms-panel.js +24 -0
- package/dist/components/ai/ai-cms-panel.js.map +1 -0
- package/dist/components/ai/ai-panel-toggle.d.ts +8 -0
- package/dist/components/ai/ai-panel-toggle.js +27 -0
- package/dist/components/ai/ai-panel-toggle.js.map +1 -0
- package/dist/components/ai/chat-ui.d.ts +36 -0
- package/dist/components/ai/chat-ui.js +68 -0
- package/dist/components/ai/chat-ui.js.map +1 -0
- package/dist/components/ai/proposal-card.d.ts +15 -0
- package/dist/components/ai/proposal-card.js +42 -0
- package/dist/components/ai/proposal-card.js.map +1 -0
- package/dist/components/app-views/default-modules.js +2 -1
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/cms/document-diff-view.d.ts +41 -0
- package/dist/components/cms/document-diff-view.js +299 -0
- package/dist/components/cms/document-diff-view.js.map +1 -0
- package/dist/components/cms/version-compare.d.ts +7 -13
- package/dist/components/cms/version-compare.js +24 -365
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +1 -1
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/studio-shell.js +2 -1
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +73 -0
- package/dist/lib/ai/chat-state.js +101 -0
- package/dist/lib/ai/chat-state.js.map +1 -0
- package/dist/lib/ai/create-flow.d.ts +116 -0
- package/dist/lib/ai/create-flow.js +194 -0
- package/dist/lib/ai/create-flow.js.map +1 -0
- package/dist/lib/ai/index.d.ts +4 -0
- package/dist/lib/ai/index.js +4 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/route-provider.d.ts +50 -0
- package/dist/lib/ai/route-provider.js +60 -0
- package/dist/lib/ai/route-provider.js.map +1 -0
- package/dist/lib/ai/route.d.ts +12 -0
- package/dist/lib/ai/route.js +42 -3
- package/dist/lib/ai/route.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +65 -0
- package/dist/lib/ai/tools.js +63 -0
- package/dist/lib/ai/tools.js.map +1 -0
- package/dist/lib/ai-panel.d.ts +20 -0
- package/dist/lib/ai-panel.js +48 -0
- package/dist/lib/ai-panel.js.map +1 -0
- package/dist/lib/i18n/messages.en.js +34 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +2 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/package.json +2 -2
package/convex/cms.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { v } from 'convex/values'
|
|
2
|
+
import { foldDocument, unfoldDocument } from '@elytracms/core/cms-core'
|
|
3
|
+
import type { CmsDocument } from '@elytracms/core/cms-core'
|
|
2
4
|
import { mutation, query } from './functions'
|
|
3
5
|
import { requireCapability } from './guard'
|
|
4
6
|
|
|
@@ -9,8 +11,29 @@ import { requireCapability } from './guard'
|
|
|
9
11
|
* table and no schema read/write function here. Collection-membership
|
|
10
12
|
* validation lives in `@elytracms/core/cms-core` `validateDocument` (the studio runs it
|
|
11
13
|
* pre-dispatch); this layer is a pure document store.
|
|
14
|
+
*
|
|
15
|
+
* ## EC-260 depth-budget codec (the storage boundary)
|
|
16
|
+
*
|
|
17
|
+
* Convex caps STORED documents at 16 nesting levels but allows 64 in function
|
|
18
|
+
* args/returns. Ordinary content (deep rich text, nested compositions) trips 16 at
|
|
19
|
+
* rest, so every write here FOLDS over-budget field values into self-describing
|
|
20
|
+
* leaf envelopes immediately before `ctx.db`, and every read UNFOLDS before the
|
|
21
|
+
* value leaves the function. Mutations receive the UNFOLDED value over the wire, so
|
|
22
|
+
* the AD-15 reference walker (in the write-chokepoint trigger, which unfolds first)
|
|
23
|
+
* always sees structured content. The wire never carries an envelope; only raw
|
|
24
|
+
* `ctx.db` reads / the dashboard see the folded storage form.
|
|
12
25
|
*/
|
|
13
26
|
|
|
27
|
+
/** Unfold the `document` blob of a stored row before it leaves the function. */
|
|
28
|
+
function withUnfoldedDocument<R extends { document: unknown }>(row: R): R {
|
|
29
|
+
return { ...row, document: unfoldDocument(row.document as CmsDocument) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Unfold the `snapshot` blob of a version row before it leaves the function. */
|
|
33
|
+
function withUnfoldedSnapshot<R extends { snapshot: unknown }>(row: R): R {
|
|
34
|
+
return { ...row, snapshot: unfoldDocument(row.snapshot as CmsDocument) }
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
export const upsertDocument = mutation({
|
|
15
38
|
args: {
|
|
16
39
|
collection: v.string(),
|
|
@@ -21,6 +44,10 @@ export const upsertDocument = mutation({
|
|
|
21
44
|
},
|
|
22
45
|
handler: async (ctx, args) => {
|
|
23
46
|
await requireCapability(ctx, 'content.write') // EC-150
|
|
47
|
+
// EC-260: fold for storage. `args.document` arrives UNFOLDED over the 64-level
|
|
48
|
+
// arg wire; the trigger unfolds before extracting references, so folding here
|
|
49
|
+
// (strictly before the `ctx.db` write) never hides a reference from the walker.
|
|
50
|
+
const stored = { ...args, document: foldDocument(args.document as CmsDocument) }
|
|
24
51
|
const existing = await ctx.db
|
|
25
52
|
.query('cmsDocuments')
|
|
26
53
|
.withIndex('by_doc', (q) => q.eq('collection', args.collection).eq('docId', args.docId))
|
|
@@ -28,17 +55,19 @@ export const upsertDocument = mutation({
|
|
|
28
55
|
if (existing) {
|
|
29
56
|
// EC-153 parity: updates preserve `createdAt`, exactly like in-memory.
|
|
30
57
|
// EC-286: the write-chokepoint trigger reindexes references in-transaction.
|
|
31
|
-
const patch = { ...
|
|
58
|
+
const patch = { ...stored, createdAt: existing.createdAt }
|
|
32
59
|
await ctx.db.patch(existing._id, patch)
|
|
33
60
|
// EC-322: return the merged row directly instead of re-reading the full
|
|
34
61
|
// (up-to-~20 KB) document body via `ctx.db.get`. `patch` only overwrites the
|
|
35
62
|
// supplied fields, so `{ ...existing, ...patch }` is byte-identical to the
|
|
36
63
|
// stored row (system fields + untouched `publishedVersion` carried from
|
|
37
64
|
// `existing`) — cutting one full-document read off the hottest write path.
|
|
38
|
-
|
|
65
|
+
// EC-260: unfold the folded body back to structured form for the return.
|
|
66
|
+
return withUnfoldedDocument({ ...existing, ...patch })
|
|
39
67
|
}
|
|
40
|
-
const id = await ctx.db.insert('cmsDocuments',
|
|
41
|
-
|
|
68
|
+
const id = await ctx.db.insert('cmsDocuments', stored)
|
|
69
|
+
const row = await ctx.db.get(id)
|
|
70
|
+
return row ? withUnfoldedDocument(row) : row
|
|
42
71
|
},
|
|
43
72
|
})
|
|
44
73
|
|
|
@@ -46,10 +75,11 @@ export const getDocument = query({
|
|
|
46
75
|
args: { collection: v.string(), docId: v.string() },
|
|
47
76
|
handler: async (ctx, args) => {
|
|
48
77
|
await requireCapability(ctx, 'content.read') // EC-150
|
|
49
|
-
|
|
78
|
+
const row = await ctx.db
|
|
50
79
|
.query('cmsDocuments')
|
|
51
80
|
.withIndex('by_doc', (q) => q.eq('collection', args.collection).eq('docId', args.docId))
|
|
52
81
|
.first()
|
|
82
|
+
return row ? withUnfoldedDocument(row) : row // EC-260: unfold before returning
|
|
53
83
|
},
|
|
54
84
|
})
|
|
55
85
|
|
|
@@ -57,14 +87,13 @@ export const listDocuments = query({
|
|
|
57
87
|
args: { collection: v.optional(v.string()) },
|
|
58
88
|
handler: async (ctx, args) => {
|
|
59
89
|
await requireCapability(ctx, 'content.read') // EC-150
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return ctx.db.query('cmsDocuments').collect()
|
|
90
|
+
const rows = args.collection
|
|
91
|
+
? await ctx.db
|
|
92
|
+
.query('cmsDocuments')
|
|
93
|
+
.withIndex('by_collection', (q) => q.eq('collection', args.collection as string))
|
|
94
|
+
.collect()
|
|
95
|
+
: await ctx.db.query('cmsDocuments').collect()
|
|
96
|
+
return rows.map(withUnfoldedDocument) // EC-260: unfold before returning
|
|
68
97
|
},
|
|
69
98
|
})
|
|
70
99
|
|
|
@@ -100,6 +129,7 @@ export const saveDocumentVersion = mutation({
|
|
|
100
129
|
docId: v.string(),
|
|
101
130
|
snapshot: v.any(),
|
|
102
131
|
label: v.optional(v.string()),
|
|
132
|
+
origin: v.optional(v.string()), // EC-199 provenance ('assistant' for AI-authored)
|
|
103
133
|
createdAt: v.string(),
|
|
104
134
|
},
|
|
105
135
|
handler: async (ctx, args) => {
|
|
@@ -113,8 +143,12 @@ export const saveDocumentVersion = mutation({
|
|
|
113
143
|
.order('desc')
|
|
114
144
|
.first()
|
|
115
145
|
const version = (latest?.version ?? 0) + 1
|
|
116
|
-
|
|
117
|
-
|
|
146
|
+
// EC-260: the snapshot embeds the CmsDocument at draft depth, so it folds at rest
|
|
147
|
+
// identically. `documentVersions` has no trigger — no walker to keep structured.
|
|
148
|
+
const stored = { ...args, snapshot: foldDocument(args.snapshot as CmsDocument), version }
|
|
149
|
+
const id = await ctx.db.insert('documentVersions', stored)
|
|
150
|
+
const row = await ctx.db.get(id)
|
|
151
|
+
return row ? withUnfoldedSnapshot(row) : row
|
|
118
152
|
},
|
|
119
153
|
})
|
|
120
154
|
|
|
@@ -127,7 +161,7 @@ export const listDocumentVersions = query({
|
|
|
127
161
|
.withIndex('by_doc', (q) => q.eq('collection', args.collection).eq('docId', args.docId))
|
|
128
162
|
.collect()
|
|
129
163
|
// Newest first (parity with the in-memory `listDocumentVersions`).
|
|
130
|
-
return rows.sort((a, b) => b.version - a.version)
|
|
164
|
+
return rows.sort((a, b) => b.version - a.version).map(withUnfoldedSnapshot) // EC-260
|
|
131
165
|
},
|
|
132
166
|
})
|
|
133
167
|
|
|
@@ -138,12 +172,13 @@ export const getDocumentVersion = query({
|
|
|
138
172
|
// EC-307: address the row directly via `by_doc_version` instead of collecting
|
|
139
173
|
// the whole — unbounded — per-document history and finding in JS. `version` is
|
|
140
174
|
// unique per (collection, docId), so `.unique()` both fetches and asserts that.
|
|
141
|
-
|
|
175
|
+
const row = await ctx.db
|
|
142
176
|
.query('documentVersions')
|
|
143
177
|
.withIndex('by_doc_version', (q) =>
|
|
144
178
|
q.eq('collection', args.collection).eq('docId', args.docId).eq('version', args.version),
|
|
145
179
|
)
|
|
146
180
|
.unique()
|
|
181
|
+
return row ? withUnfoldedSnapshot(row) : row // EC-260: unfold before returning
|
|
147
182
|
},
|
|
148
183
|
})
|
|
149
184
|
|
|
@@ -175,7 +210,8 @@ export const listPublishedDocuments = query({
|
|
|
175
210
|
q.eq('collection', doc.collection).eq('docId', doc.docId).eq('version', version),
|
|
176
211
|
)
|
|
177
212
|
.unique()
|
|
178
|
-
|
|
213
|
+
// EC-260: unfold the pinned snapshot before it leaves the function.
|
|
214
|
+
if (pinned) snapshots.push(unfoldDocument(pinned.snapshot as CmsDocument))
|
|
179
215
|
}
|
|
180
216
|
return snapshots
|
|
181
217
|
},
|
|
@@ -186,7 +222,8 @@ export const listAllDocumentVersions = query({
|
|
|
186
222
|
args: {},
|
|
187
223
|
handler: async (ctx) => {
|
|
188
224
|
await requireCapability(ctx, 'content.read') // EC-150
|
|
189
|
-
|
|
225
|
+
const rows = await ctx.db.query('documentVersions').collect()
|
|
226
|
+
return rows.map(withUnfoldedSnapshot) // EC-260: unfold before returning
|
|
190
227
|
},
|
|
191
228
|
})
|
|
192
229
|
|
|
@@ -208,7 +245,8 @@ export const publishDocumentVersion = mutation({
|
|
|
208
245
|
// write-chokepoint trigger sees — it reindexes the published reference rows
|
|
209
246
|
// and fires host revalidation (EC-146 instant publish). No manual calls here.
|
|
210
247
|
await ctx.db.patch(doc._id, { publishedVersion: args.version, updatedAt: args.updatedAt })
|
|
211
|
-
|
|
248
|
+
const row = await ctx.db.get(doc._id)
|
|
249
|
+
return row ? withUnfoldedDocument(row) : row // EC-260: unfold before returning
|
|
212
250
|
},
|
|
213
251
|
})
|
|
214
252
|
|
|
@@ -225,6 +263,7 @@ export const unpublishDocument = mutation({
|
|
|
225
263
|
// EC-286: clearing the pin is a change the write-chokepoint trigger sees — it
|
|
226
264
|
// drops the published index rows and revalidates the host. No manual calls.
|
|
227
265
|
await ctx.db.patch(doc._id, { publishedVersion: undefined, updatedAt: args.updatedAt })
|
|
228
|
-
|
|
266
|
+
const row = await ctx.db.get(doc._id)
|
|
267
|
+
return row ? withUnfoldedDocument(row) : row // EC-260: unfold before returning
|
|
229
268
|
},
|
|
230
269
|
})
|
package/convex/content.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { documentKey, documentSchema } from '@elytracms/core/cms-core'
|
|
1
|
+
import { documentKey, documentSchema, unfoldValue } from '@elytracms/core/cms-core'
|
|
2
2
|
import type {
|
|
3
3
|
CmsDocument,
|
|
4
4
|
CmsValidationIssue,
|
|
@@ -157,7 +157,9 @@ export async function createContent(
|
|
|
157
157
|
// a published read can serve the *pinned* version rather than the live draft.
|
|
158
158
|
const versionsByKey = new Map<string, Map<number, CmsDocument>>()
|
|
159
159
|
for (const row of await ctx.db.query('documentVersions').collect()) {
|
|
160
|
-
|
|
160
|
+
// EC-260: unfold the folded storage form before parsing — the kit resolves over
|
|
161
|
+
// STRUCTURED values, exactly like the Next-side snapshot (byte-equivalence).
|
|
162
|
+
const parsed = documentSchema.safeParse(unfoldValue(row.snapshot))
|
|
161
163
|
if (!parsed.success) continue
|
|
162
164
|
const key = `${row.collection}:${row.docId}`
|
|
163
165
|
let byVersion = versionsByKey.get(key)
|
|
@@ -176,7 +178,8 @@ export async function createContent(
|
|
|
176
178
|
// resolves exactly as before.
|
|
177
179
|
const parsedSources: { key: string; source: ContentDocumentSource }[] = []
|
|
178
180
|
for (const row of documentRows) {
|
|
179
|
-
|
|
181
|
+
// EC-260: unfold the folded storage form before parsing (see the versions loop).
|
|
182
|
+
const parsed = documentSchema.safeParse(unfoldValue(row.document))
|
|
180
183
|
if (!parsed.success) {
|
|
181
184
|
loadIssues.push({
|
|
182
185
|
code: 'invalid-stored-document',
|
package/convex/delivery.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { v } from 'convex/values'
|
|
2
|
+
import { unfoldValue } from '@elytracms/core/cms-core'
|
|
2
3
|
import { dataSourceSchema } from '@elytracms/core/data-binding'
|
|
3
4
|
import { query } from './functions'
|
|
4
5
|
import type { QueryCtx } from './functions'
|
|
@@ -134,6 +135,10 @@ export async function loadSnapshot(
|
|
|
134
135
|
const { projectId } = args
|
|
135
136
|
|
|
136
137
|
const documentRows = await ctx.db.query('cmsDocuments').collect()
|
|
138
|
+
// EC-260: the stored `document`/`snapshot` blobs are folded; unfold every one
|
|
139
|
+
// before it enters the delivery snapshot so the host receives STRUCTURED content
|
|
140
|
+
// (no client ever sees an envelope). `unfoldValue` is identity on legacy data.
|
|
141
|
+
const unfoldDraft = (row: { document: unknown }): unknown => unfoldValue(row.document)
|
|
137
142
|
// THE published-content invariant (the trust boundary): the public snapshot
|
|
138
143
|
// ships ONLY published content — never a draft. Under the EC-224 pointer model
|
|
139
144
|
// a published row is served as its *pinned version snapshot* (decoupled from
|
|
@@ -142,7 +147,7 @@ export async function loadSnapshot(
|
|
|
142
147
|
// fallback (the `state` field is gone).
|
|
143
148
|
let documents: unknown[]
|
|
144
149
|
if (perspective === 'draft') {
|
|
145
|
-
documents = documentRows.map(
|
|
150
|
+
documents = documentRows.map(unfoldDraft)
|
|
146
151
|
} else {
|
|
147
152
|
documents = []
|
|
148
153
|
for (const row of documentRows) {
|
|
@@ -160,7 +165,7 @@ export async function loadSnapshot(
|
|
|
160
165
|
q.eq('collection', row.collection).eq('docId', row.docId).eq('version', version),
|
|
161
166
|
)
|
|
162
167
|
.unique()
|
|
163
|
-
if (pinned) documents.push(pinned.snapshot
|
|
168
|
+
if (pinned) documents.push(unfoldValue(pinned.snapshot))
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
171
|
|
|
@@ -202,9 +207,7 @@ export async function loadSnapshot(
|
|
|
202
207
|
// extracted from every stored row (independent of publish state) — published
|
|
203
208
|
// bindings must always resolve their definitions, even when the defining
|
|
204
209
|
// `cms-data-sources` row is not itself "published".
|
|
205
|
-
const sources = dataSourcesFromSystemCollection(
|
|
206
|
-
documentRows.map((row) => row.document as unknown),
|
|
207
|
-
)
|
|
210
|
+
const sources = dataSourcesFromSystemCollection(documentRows.map(unfoldDraft))
|
|
208
211
|
|
|
209
212
|
// Editorial redirects (EC-303): only ENABLED entries serve — a disabled redirect
|
|
210
213
|
// is stored but never live, in either perspective. Projected to the routing
|
package/convex/schema.ts
CHANGED
|
@@ -120,6 +120,9 @@ export default defineSchema({
|
|
|
120
120
|
version: v.number(),
|
|
121
121
|
snapshot: v.any(),
|
|
122
122
|
label: v.optional(v.string()),
|
|
123
|
+
// EC-199 provenance: authoring origin ('assistant' for AI-authored content).
|
|
124
|
+
// Additive scalar, outside the folded snapshot — invisible when unset.
|
|
125
|
+
origin: v.optional(v.string()),
|
|
123
126
|
createdAt: v.string(),
|
|
124
127
|
})
|
|
125
128
|
.index('by_doc', ['collection', 'docId'])
|
package/convex/triggers.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unfoldDocument } from '@elytracms/core/cms-core'
|
|
1
2
|
import type { CmsDocument } from '@elytracms/core/cms-core'
|
|
2
3
|
import { Triggers } from 'convex-helpers/server/triggers'
|
|
3
4
|
import type { DataModel } from './functions'
|
|
@@ -42,11 +43,17 @@ triggers.register('cmsDocuments', async (ctx, change) => {
|
|
|
42
43
|
return
|
|
43
44
|
}
|
|
44
45
|
const doc = change.newDoc
|
|
46
|
+
// EC-260: the stored `document` is FOLDED (deep field values collapsed to leaf
|
|
47
|
+
// envelopes). Unfold it here so `extractReferences` walks the STRUCTURED value and
|
|
48
|
+
// still finds references buried inside a folded field — this is the write chokepoint
|
|
49
|
+
// (EC-286), so every writer's references are derived from structured content by
|
|
50
|
+
// construction, regardless of the folded storage form. `unfoldDocument` is identity
|
|
51
|
+
// on already-structural data, so a legacy unfolded row costs nothing.
|
|
45
52
|
await maintainDocumentReferences(
|
|
46
53
|
ctx,
|
|
47
54
|
doc.collection,
|
|
48
55
|
doc.docId,
|
|
49
|
-
doc.document as CmsDocument,
|
|
56
|
+
unfoldDocument(doc.document as CmsDocument),
|
|
50
57
|
doc.publishedVersion !== undefined,
|
|
51
58
|
)
|
|
52
59
|
const before = change.oldDoc?.publishedVersion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function AiChatPanel(): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, lazy } from 'react';
|
|
3
|
+
import { Text } from '@elytracms/studio/ui';
|
|
4
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
5
|
+
/**
|
|
6
|
+
* Lazy boundary for the AI chat panel (EC-199, EC-331 bundle discipline). The panel
|
|
7
|
+
* and everything it imports — the creation orchestrator, `applyContentWithShape`, the
|
|
8
|
+
* provider adapters (and the AI SDK behind the Anthropic one via dynamic import) — load
|
|
9
|
+
* ONLY when a configured studio actually opens the panel. An unconfigured studio never
|
|
10
|
+
* renders the toggle, never mounts this, and so pulls none of the AI chunk onto the CMS
|
|
11
|
+
* critical path (pinned by the `lib/ai` bundle test). Same pattern as
|
|
12
|
+
* `version-compare-lazy` / `rich-text/lazy`.
|
|
13
|
+
*/
|
|
14
|
+
const AiChatPanelImpl = lazy(() => import('./ai-chat-panel').then((module) => ({ default: module.AiChatPanel })));
|
|
15
|
+
function AiPanelFallback() {
|
|
16
|
+
return (_jsx("aside", { className: "flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card", children: _jsx("div", { className: "grid min-h-0 flex-1 place-items-center p-6", children: _jsx(Text, { size: "sm", tone: "muted", children: translate('ai.panel.loading') }) }) }));
|
|
17
|
+
}
|
|
18
|
+
export function AiChatPanel() {
|
|
19
|
+
return (_jsx(Suspense, { fallback: _jsx(AiPanelFallback, {}), children: _jsx(AiChatPanelImpl, {}) }));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=ai-chat-panel-lazy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-chat-panel-lazy.js","sourceRoot":"","sources":["../../../src/components/ai/ai-chat-panel-lazy.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;GAQG;AACH,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAChC,MAAM,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAC9E,CAAA;AAED,SAAS,eAAe;IACtB,OAAO,CACL,gBAAO,SAAS,EAAC,gFAAgF,YAC/F,cAAK,SAAS,EAAC,4CAA4C,YACzD,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,kBAAkB,CAAC,GACzB,GACH,GACA,CACT,CAAA;AACH,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,CACL,KAAC,QAAQ,IAAC,QAAQ,EAAE,KAAC,eAAe,KAAG,YACrC,KAAC,eAAe,KAAG,GACV,CACZ,CAAA;AACH,CAAC","sourcesContent":["import { Suspense, lazy } from 'react'\nimport { Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Lazy boundary for the AI chat panel (EC-199, EC-331 bundle discipline). The panel\n * and everything it imports — the creation orchestrator, `applyContentWithShape`, the\n * provider adapters (and the AI SDK behind the Anthropic one via dynamic import) — load\n * ONLY when a configured studio actually opens the panel. An unconfigured studio never\n * renders the toggle, never mounts this, and so pulls none of the AI chunk onto the CMS\n * critical path (pinned by the `lib/ai` bundle test). Same pattern as\n * `version-compare-lazy` / `rich-text/lazy`.\n */\nconst AiChatPanelImpl = lazy(() =>\n import('./ai-chat-panel').then((module) => ({ default: module.AiChatPanel })),\n)\n\nfunction AiPanelFallback() {\n return (\n <aside className=\"flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card\">\n <div className=\"grid min-h-0 flex-1 place-items-center p-6\">\n <Text size=\"sm\" tone=\"muted\">\n {translate('ai.panel.loading')}\n </Text>\n </div>\n </aside>\n )\n}\n\nexport function AiChatPanel() {\n return (\n <Suspense fallback={<AiPanelFallback />}>\n <AiChatPanelImpl />\n </Suspense>\n )\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI assistant chat panel (EC-199) — the right-hand collapsible surface in the CMS
|
|
3
|
+
* area. It owns the chat state (the pure `chatReducer`), resolves the EC-331 provider
|
|
4
|
+
* from `defineStudio({ ai })`, binds the narrow tools (search/list) to the operations
|
|
5
|
+
* layer with the user's session, runs the creation orchestrator, and applies a
|
|
6
|
+
* reviewed proposal as a DRAFT (operations upsert + a provenance-tagged version
|
|
7
|
+
* snapshot). NEVER a modal (house rule). The whole module sits behind a lazy boundary
|
|
8
|
+
* (`ai-chat-panel-lazy`), so an unconfigured studio pulls none of it — and no AI SDK.
|
|
9
|
+
*/
|
|
10
|
+
export declare function AiChatPanel(): import("react").JSX.Element;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useReducer, useState } from 'react';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { createCollectionRegistry, resolveDocumentTitle, } from '@elytracms/core/cms-core';
|
|
5
|
+
import { Button, Heading, Row, Spacer, Text } from '@elytracms/studio/ui';
|
|
6
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
|
|
8
|
+
import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
|
|
9
|
+
import { getStudioOperations } from '@elytracms/studio/lib/operations';
|
|
10
|
+
import { AiRouteError, chatReducer, initialChatState, resolveAiConfig, resolveKnowledge, runCreateFlow, } from '@elytracms/studio/lib/ai';
|
|
11
|
+
import { useCms } from '../cms/cms-store';
|
|
12
|
+
import { validateComposition, validateRichTextEmbeds } from '../cms/cms-composition-validators';
|
|
13
|
+
import { AssistantTurn, Conversation, ConversationEmpty, PromptInput, ToolStatus, UserTurn, } from './chat-ui';
|
|
14
|
+
import { ProposalCard } from './proposal-card';
|
|
15
|
+
import { setAiPanelOpen } from '@elytracms/studio/lib/ai-panel';
|
|
16
|
+
/**
|
|
17
|
+
* The AI assistant chat panel (EC-199) — the right-hand collapsible surface in the CMS
|
|
18
|
+
* area. It owns the chat state (the pure `chatReducer`), resolves the EC-331 provider
|
|
19
|
+
* from `defineStudio({ ai })`, binds the narrow tools (search/list) to the operations
|
|
20
|
+
* layer with the user's session, runs the creation orchestrator, and applies a
|
|
21
|
+
* reviewed proposal as a DRAFT (operations upsert + a provenance-tagged version
|
|
22
|
+
* snapshot). NEVER a modal (house rule). The whole module sits behind a lazy boundary
|
|
23
|
+
* (`ai-chat-panel-lazy`), so an unconfigured studio pulls none of it — and no AI SDK.
|
|
24
|
+
*/
|
|
25
|
+
export function AiChatPanel() {
|
|
26
|
+
const cms = useCms();
|
|
27
|
+
const navigate = useNavigate();
|
|
28
|
+
const [state, dispatch] = useReducer(chatReducer, initialChatState);
|
|
29
|
+
const [input, setInput] = useState('');
|
|
30
|
+
const projectId = cms.projectId;
|
|
31
|
+
const activeLocale = cms.activeLocale;
|
|
32
|
+
// Resolve the AI platform config once (presence + capability gate). The panel only
|
|
33
|
+
// mounts when configured, but a misconfigured provider still fails loud here.
|
|
34
|
+
const resolved = useMemo(() => resolveAiConfig(resolveDeploymentAi()), []);
|
|
35
|
+
const provider = resolved.resolved?.provider ?? null;
|
|
36
|
+
const knowledgeSources = resolved.resolved?.knowledge ?? [];
|
|
37
|
+
const maxRepairAttempts = resolved.resolved?.limits?.maxRepairAttempts;
|
|
38
|
+
const ready = provider !== null && resolved.ok && projectId !== undefined;
|
|
39
|
+
const registry = useMemo(() => createCollectionRegistry(cms.collections), [cms.collections]);
|
|
40
|
+
/** Merge a document's non-localized + active-locale values for title/prose reads. */
|
|
41
|
+
const localeValues = useCallback((doc) => ({
|
|
42
|
+
...doc.values,
|
|
43
|
+
...(doc.localized?.[activeLocale] ?? {}),
|
|
44
|
+
}), [activeLocale]);
|
|
45
|
+
/** Operations-backed tool readers — client-side, with the user's own session. */
|
|
46
|
+
const tools = useMemo(() => {
|
|
47
|
+
const matches = (query, ...fields) => !query || fields.some((f) => f?.toLowerCase().includes(query.toLowerCase()));
|
|
48
|
+
return {
|
|
49
|
+
async searchDocuments({ collection, query }) {
|
|
50
|
+
if (projectId === undefined)
|
|
51
|
+
return [];
|
|
52
|
+
const { operations } = await getStudioOperations();
|
|
53
|
+
const result = await operations.documents.list({ projectId, collection });
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
return [];
|
|
56
|
+
const target = cms.getCollection(collection);
|
|
57
|
+
return result.value
|
|
58
|
+
.map((rec) => ({
|
|
59
|
+
id: rec.document.id,
|
|
60
|
+
title: target
|
|
61
|
+
? resolveDocumentTitle(target, localeValues(rec.document), rec.document.id)
|
|
62
|
+
: rec.document.id,
|
|
63
|
+
}))
|
|
64
|
+
.filter((r) => matches(query, r.title, r.id));
|
|
65
|
+
},
|
|
66
|
+
async listAssets({ query }) {
|
|
67
|
+
if (projectId === undefined)
|
|
68
|
+
return [];
|
|
69
|
+
const { operations } = await getStudioOperations();
|
|
70
|
+
const result = await operations.assets.list({ projectId });
|
|
71
|
+
if (!result.ok)
|
|
72
|
+
return [];
|
|
73
|
+
return result.value
|
|
74
|
+
.map((asset) => ({ id: asset.id, title: asset.title ?? asset.filename }))
|
|
75
|
+
.filter((r) => matches(query, r.title, r.id));
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}, [projectId, cms, localeValues]);
|
|
79
|
+
/** A fresh, collision-free draft id for the target collection. */
|
|
80
|
+
const mintDocumentId = useCallback((collectionId) => {
|
|
81
|
+
const existing = new Set(cms.documentsOf(collectionId).map((d) => d.id));
|
|
82
|
+
let n = 1;
|
|
83
|
+
while (existing.has(`new-${collectionId}-${n}`))
|
|
84
|
+
n += 1;
|
|
85
|
+
return `new-${collectionId}-${n}`;
|
|
86
|
+
}, [cms]);
|
|
87
|
+
/** Read `fromCollection(...)` knowledge prose through the operations layer. */
|
|
88
|
+
const readCollection = useCallback(async (source) => {
|
|
89
|
+
if (projectId === undefined)
|
|
90
|
+
return [];
|
|
91
|
+
const { operations } = await getStudioOperations();
|
|
92
|
+
const result = await operations.documents.list({ projectId, collection: source.collectionId });
|
|
93
|
+
if (!result.ok)
|
|
94
|
+
return [];
|
|
95
|
+
const target = cms.getCollection(source.collectionId);
|
|
96
|
+
const field = source.field ?? target?.fields.find((f) => f.type === 'text' || f.type === 'richText')?.name;
|
|
97
|
+
return result.value
|
|
98
|
+
.map((rec) => {
|
|
99
|
+
const values = localeValues(rec.document);
|
|
100
|
+
const content = field ? String(values[field] ?? '') : '';
|
|
101
|
+
return { content };
|
|
102
|
+
})
|
|
103
|
+
.filter((item) => item.content.trim().length > 0);
|
|
104
|
+
}, [projectId, cms, localeValues]);
|
|
105
|
+
const runTurn = useCallback(async (text) => {
|
|
106
|
+
if (!provider)
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
const knowledge = await resolveKnowledge(knowledgeSources, { readCollection });
|
|
110
|
+
const result = await runCreateFlow({
|
|
111
|
+
provider,
|
|
112
|
+
tools,
|
|
113
|
+
registry,
|
|
114
|
+
collections: cms.collections,
|
|
115
|
+
instruction: text,
|
|
116
|
+
knowledge,
|
|
117
|
+
locale: activeLocale,
|
|
118
|
+
mintDocumentId,
|
|
119
|
+
validateRichTextEmbeds,
|
|
120
|
+
validateComposition,
|
|
121
|
+
...(maxRepairAttempts !== undefined ? { maxRepairAttempts } : {}),
|
|
122
|
+
onStep: (step) => dispatch({ type: 'step', step }),
|
|
123
|
+
});
|
|
124
|
+
dispatch({ type: 'result', result });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const message = error instanceof AiRouteError
|
|
128
|
+
? translate('ai.error.route')
|
|
129
|
+
: translate('ai.error.generic');
|
|
130
|
+
dispatch({ type: 'fail', message });
|
|
131
|
+
}
|
|
132
|
+
}, [
|
|
133
|
+
provider,
|
|
134
|
+
knowledgeSources,
|
|
135
|
+
readCollection,
|
|
136
|
+
tools,
|
|
137
|
+
registry,
|
|
138
|
+
cms.collections,
|
|
139
|
+
activeLocale,
|
|
140
|
+
mintDocumentId,
|
|
141
|
+
maxRepairAttempts,
|
|
142
|
+
]);
|
|
143
|
+
const submit = useCallback(() => {
|
|
144
|
+
const text = input.trim();
|
|
145
|
+
if (text.length === 0 || state.status === 'busy' || !ready)
|
|
146
|
+
return;
|
|
147
|
+
setInput('');
|
|
148
|
+
dispatch({ type: 'submit', text });
|
|
149
|
+
void runTurn(text);
|
|
150
|
+
}, [input, state.status, ready, runTurn]);
|
|
151
|
+
const applyProposal = useCallback(async (proposal) => {
|
|
152
|
+
if (projectId === undefined)
|
|
153
|
+
return;
|
|
154
|
+
const doc = {
|
|
155
|
+
collection: proposal.collection,
|
|
156
|
+
id: proposal.documentId,
|
|
157
|
+
values: proposal.patch.values,
|
|
158
|
+
...(proposal.patch.localized ? { localized: proposal.patch.localized } : {}),
|
|
159
|
+
};
|
|
160
|
+
const { operations } = await getStudioOperations();
|
|
161
|
+
const upserted = await operations.documents.upsert({ projectId, document: doc });
|
|
162
|
+
if (!upserted.ok) {
|
|
163
|
+
dispatch({ type: 'fail', message: translate('ai.error.applyFailed') });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Provenance: record an AI-tagged version snapshot (a restore point + the
|
|
167
|
+
// 'assistant' origin surfaced in the versions pane). Best-effort — the draft
|
|
168
|
+
// already landed above; a snapshot failure must not lose the applied content.
|
|
169
|
+
try {
|
|
170
|
+
await operations.documents.snapshot({
|
|
171
|
+
projectId,
|
|
172
|
+
ref: { collection: proposal.collection, id: proposal.documentId },
|
|
173
|
+
label: translate('cms.version.aiAuthoredTitle'),
|
|
174
|
+
origin: 'assistant',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// ignore — provenance is additive
|
|
179
|
+
}
|
|
180
|
+
// Reflect in the session so the new draft shows in the list + is navigable.
|
|
181
|
+
cms.saveDocument(doc);
|
|
182
|
+
dispatch({ type: 'apply', proposalId: proposal.id });
|
|
183
|
+
}, [projectId, cms]);
|
|
184
|
+
const openProposal = useCallback((proposal) => {
|
|
185
|
+
if (projectId === undefined)
|
|
186
|
+
return;
|
|
187
|
+
void navigate({
|
|
188
|
+
to: STUDIO_PROJECT_ROUTE,
|
|
189
|
+
params: { projectId, _splat: studioPath.cmsDocument(proposal.collection, proposal.documentId) },
|
|
190
|
+
});
|
|
191
|
+
}, [navigate, projectId]);
|
|
192
|
+
return (_jsxs("aside", { className: "flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card", children: [_jsxs(Row, { gap: "2", align: "center", className: "h-12 shrink-0 border-b border-border px-3", children: [_jsx(Heading, { level: 2, size: "h4", children: translate('ai.panel.title') }), _jsx(Spacer, {}), _jsx(Button, { size: "icon-sm", variant: "ghost", onClick: () => setAiPanelOpen(false), title: translate('ai.panel.close'), "aria-label": translate('ai.panel.close'), children: _jsx(X, {}) })] }), _jsxs(Conversation, { atBottomKey: state.messages, children: [state.messages.length === 0 ? _jsx(ConversationEmpty, {}) : null, state.messages.map((message) => {
|
|
193
|
+
if (message.role === 'user')
|
|
194
|
+
return _jsx(UserTurn, { text: message.text }, message.id);
|
|
195
|
+
if (message.kind === 'text')
|
|
196
|
+
return (_jsx(AssistantTurn, { children: _jsx(Text, { size: "sm", className: "whitespace-pre-wrap break-words", children: message.text }) }, message.id));
|
|
197
|
+
if (message.kind === 'activity')
|
|
198
|
+
return (_jsx(AssistantTurn, { children: _jsx(ToolStatus, { steps: message.steps, active: state.status === 'busy' && state.activityId === message.id }) }, message.id));
|
|
199
|
+
return (_jsx(ProposalCard, { proposal: message.proposal, onApply: () => void applyProposal(message.proposal), onDiscard: () => dispatch({ type: 'reject', proposalId: message.proposal.id }), onOpen: () => openProposal(message.proposal) }, message.id));
|
|
200
|
+
})] }), ready ? (_jsx(PromptInput, { value: input, onChange: setInput, onSubmit: submit, disabled: state.status === 'busy' })) : (_jsx("div", { className: "border-t border-border p-3", children: _jsx(Text, { size: "xs", tone: "muted", children: translate('ai.panel.unavailable') }) }))] }));
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=ai-chat-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-chat-panel.js","sourceRoot":"","sources":["../../../src/components/ai/ai-chat-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useReducer, useState } from 'react'\nimport { X } from 'lucide-react'\nimport {\n createCollectionRegistry,\n resolveDocumentTitle,\n} from '@elytracms/core/cms-core'\nimport type { CmsDocument, FieldValues } from '@elytracms/core/cms-core'\nimport { Button, Heading, Row, Spacer, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport { resolveDeploymentAi } from '@elytracms/studio/lib/workspace'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport {\n AiRouteError,\n chatReducer,\n initialChatState,\n resolveAiConfig,\n resolveKnowledge,\n runCreateFlow,\n} from '@elytracms/studio/lib/ai'\nimport type {\n CreateFlowStep,\n CreateFlowTools,\n KnowledgeCollectionReader,\n ProposalReview,\n} from '@elytracms/studio/lib/ai'\nimport { useCms } from '../cms/cms-store'\nimport { validateComposition, validateRichTextEmbeds } from '../cms/cms-composition-validators'\nimport {\n AssistantTurn,\n Conversation,\n ConversationEmpty,\n PromptInput,\n ToolStatus,\n UserTurn,\n} from './chat-ui'\nimport { ProposalCard } from './proposal-card'\nimport { setAiPanelOpen } from '@elytracms/studio/lib/ai-panel'\n\n/**\n * The AI assistant chat panel (EC-199) — the right-hand collapsible surface in the CMS\n * area. It owns the chat state (the pure `chatReducer`), resolves the EC-331 provider\n * from `defineStudio({ ai })`, binds the narrow tools (search/list) to the operations\n * layer with the user's session, runs the creation orchestrator, and applies a\n * reviewed proposal as a DRAFT (operations upsert + a provenance-tagged version\n * snapshot). NEVER a modal (house rule). The whole module sits behind a lazy boundary\n * (`ai-chat-panel-lazy`), so an unconfigured studio pulls none of it — and no AI SDK.\n */\nexport function AiChatPanel() {\n const cms = useCms()\n const navigate = useNavigate()\n const [state, dispatch] = useReducer(chatReducer, initialChatState)\n const [input, setInput] = useState('')\n\n const projectId = cms.projectId\n const activeLocale = cms.activeLocale\n\n // Resolve the AI platform config once (presence + capability gate). The panel only\n // mounts when configured, but a misconfigured provider still fails loud here.\n const resolved = useMemo(() => resolveAiConfig(resolveDeploymentAi()), [])\n const provider = resolved.resolved?.provider ?? null\n const knowledgeSources = resolved.resolved?.knowledge ?? []\n const maxRepairAttempts = resolved.resolved?.limits?.maxRepairAttempts\n const ready = provider !== null && resolved.ok && projectId !== undefined\n\n const registry = useMemo(() => createCollectionRegistry(cms.collections), [cms.collections])\n\n /** Merge a document's non-localized + active-locale values for title/prose reads. */\n const localeValues = useCallback(\n (doc: CmsDocument): FieldValues => ({\n ...doc.values,\n ...(doc.localized?.[activeLocale] ?? {}),\n }),\n [activeLocale],\n )\n\n /** Operations-backed tool readers — client-side, with the user's own session. */\n const tools = useMemo<CreateFlowTools>(() => {\n const matches = (query: string | undefined, ...fields: (string | undefined)[]) =>\n !query || fields.some((f) => f?.toLowerCase().includes(query.toLowerCase()))\n return {\n async searchDocuments({ collection, query }) {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.documents.list({ projectId, collection })\n if (!result.ok) return []\n const target = cms.getCollection(collection)\n return result.value\n .map((rec) => ({\n id: rec.document.id,\n title: target\n ? resolveDocumentTitle(target, localeValues(rec.document), rec.document.id)\n : rec.document.id,\n }))\n .filter((r) => matches(query, r.title, r.id))\n },\n async listAssets({ query }) {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.assets.list({ projectId })\n if (!result.ok) return []\n return result.value\n .map((asset) => ({ id: asset.id, title: asset.title ?? asset.filename }))\n .filter((r) => matches(query, r.title, r.id))\n },\n }\n }, [projectId, cms, localeValues])\n\n /** A fresh, collision-free draft id for the target collection. */\n const mintDocumentId = useCallback(\n (collectionId: string): string => {\n const existing = new Set(cms.documentsOf(collectionId).map((d) => d.id))\n let n = 1\n while (existing.has(`new-${collectionId}-${n}`)) n += 1\n return `new-${collectionId}-${n}`\n },\n [cms],\n )\n\n /** Read `fromCollection(...)` knowledge prose through the operations layer. */\n const readCollection = useCallback<KnowledgeCollectionReader>(\n async (source) => {\n if (projectId === undefined) return []\n const { operations } = await getStudioOperations()\n const result = await operations.documents.list({ projectId, collection: source.collectionId })\n if (!result.ok) return []\n const target = cms.getCollection(source.collectionId)\n const field =\n source.field ?? target?.fields.find((f) => f.type === 'text' || f.type === 'richText')?.name\n return result.value\n .map((rec) => {\n const values = localeValues(rec.document)\n const content = field ? String(values[field] ?? '') : ''\n return { content }\n })\n .filter((item) => item.content.trim().length > 0)\n },\n [projectId, cms, localeValues],\n )\n\n const runTurn = useCallback(\n async (text: string) => {\n if (!provider) return\n try {\n const knowledge = await resolveKnowledge(knowledgeSources, { readCollection })\n const result = await runCreateFlow({\n provider,\n tools,\n registry,\n collections: cms.collections,\n instruction: text,\n knowledge,\n locale: activeLocale,\n mintDocumentId,\n validateRichTextEmbeds,\n validateComposition,\n ...(maxRepairAttempts !== undefined ? { maxRepairAttempts } : {}),\n onStep: (step: CreateFlowStep) => dispatch({ type: 'step', step }),\n })\n dispatch({ type: 'result', result })\n } catch (error) {\n const message =\n error instanceof AiRouteError\n ? translate('ai.error.route')\n : translate('ai.error.generic')\n dispatch({ type: 'fail', message })\n }\n },\n [\n provider,\n knowledgeSources,\n readCollection,\n tools,\n registry,\n cms.collections,\n activeLocale,\n mintDocumentId,\n maxRepairAttempts,\n ],\n )\n\n const submit = useCallback(() => {\n const text = input.trim()\n if (text.length === 0 || state.status === 'busy' || !ready) return\n setInput('')\n dispatch({ type: 'submit', text })\n void runTurn(text)\n }, [input, state.status, ready, runTurn])\n\n const applyProposal = useCallback(\n async (proposal: ProposalReview) => {\n if (projectId === undefined) return\n const doc: CmsDocument = {\n collection: proposal.collection,\n id: proposal.documentId,\n values: proposal.patch.values,\n ...(proposal.patch.localized ? { localized: proposal.patch.localized } : {}),\n }\n const { operations } = await getStudioOperations()\n const upserted = await operations.documents.upsert({ projectId, document: doc })\n if (!upserted.ok) {\n dispatch({ type: 'fail', message: translate('ai.error.applyFailed') })\n return\n }\n // Provenance: record an AI-tagged version snapshot (a restore point + the\n // 'assistant' origin surfaced in the versions pane). Best-effort — the draft\n // already landed above; a snapshot failure must not lose the applied content.\n try {\n await operations.documents.snapshot({\n projectId,\n ref: { collection: proposal.collection, id: proposal.documentId },\n label: translate('cms.version.aiAuthoredTitle'),\n origin: 'assistant',\n })\n } catch {\n // ignore — provenance is additive\n }\n // Reflect in the session so the new draft shows in the list + is navigable.\n cms.saveDocument(doc)\n dispatch({ type: 'apply', proposalId: proposal.id })\n },\n [projectId, cms],\n )\n\n const openProposal = useCallback(\n (proposal: ProposalReview) => {\n if (projectId === undefined) return\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: studioPath.cmsDocument(proposal.collection, proposal.documentId) },\n })\n },\n [navigate, projectId],\n )\n\n return (\n <aside className=\"flex h-full min-h-0 w-[360px] shrink-0 flex-col border-l border-border bg-card\">\n <Row gap=\"2\" align=\"center\" className=\"h-12 shrink-0 border-b border-border px-3\">\n <Heading level={2} size=\"h4\">\n {translate('ai.panel.title')}\n </Heading>\n <Spacer />\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n onClick={() => setAiPanelOpen(false)}\n title={translate('ai.panel.close')}\n aria-label={translate('ai.panel.close')}\n >\n <X />\n </Button>\n </Row>\n\n <Conversation atBottomKey={state.messages}>\n {state.messages.length === 0 ? <ConversationEmpty /> : null}\n {state.messages.map((message) => {\n if (message.role === 'user') return <UserTurn key={message.id} text={message.text} />\n if (message.kind === 'text')\n return (\n <AssistantTurn key={message.id}>\n <Text size=\"sm\" className=\"whitespace-pre-wrap break-words\">\n {message.text}\n </Text>\n </AssistantTurn>\n )\n if (message.kind === 'activity')\n return (\n <AssistantTurn key={message.id}>\n <ToolStatus\n steps={message.steps}\n active={state.status === 'busy' && state.activityId === message.id}\n />\n </AssistantTurn>\n )\n return (\n <ProposalCard\n key={message.id}\n proposal={message.proposal}\n onApply={() => void applyProposal(message.proposal)}\n onDiscard={() => dispatch({ type: 'reject', proposalId: message.proposal.id })}\n onOpen={() => openProposal(message.proposal)}\n />\n )\n })}\n </Conversation>\n\n {ready ? (\n <PromptInput value={input} onChange={setInput} onSubmit={submit} disabled={state.status === 'busy'} />\n ) : (\n <div className=\"border-t border-border p-3\">\n <Text size=\"xs\" tone=\"muted\">\n {translate('ai.panel.unavailable')}\n </Text>\n </div>\n )}\n </aside>\n )\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CMS-area mount of the AI assistant (EC-199): a right-hand sibling of the CMS
|
|
3
|
+
* content, shown on BOTH the collection list and the document editor. Gated twice —
|
|
4
|
+
* the studio must be `ai`-configured (EC-331 presence-enable) AND the panel open — so
|
|
5
|
+
* the lazy `AiChatPanel` chunk is requested only when a configured studio actually
|
|
6
|
+
* opens it. Sits inside `CmsProvider`, so the panel reads the live CMS store
|
|
7
|
+
* (collections, active locale) for its tools and applies drafts through it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function AiCmsPanel(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { resolveDeploymentAi } from '@elytracms/studio/lib/workspace';
|
|
4
|
+
import { useAiPanelOpen } from '@elytracms/studio/lib/ai-panel';
|
|
5
|
+
import { AiChatPanel } from './ai-chat-panel-lazy';
|
|
6
|
+
/**
|
|
7
|
+
* The CMS-area mount of the AI assistant (EC-199): a right-hand sibling of the CMS
|
|
8
|
+
* content, shown on BOTH the collection list and the document editor. Gated twice —
|
|
9
|
+
* the studio must be `ai`-configured (EC-331 presence-enable) AND the panel open — so
|
|
10
|
+
* the lazy `AiChatPanel` chunk is requested only when a configured studio actually
|
|
11
|
+
* opens it. Sits inside `CmsProvider`, so the panel reads the live CMS store
|
|
12
|
+
* (collections, active locale) for its tools and applies drafts through it.
|
|
13
|
+
*/
|
|
14
|
+
export function AiCmsPanel() {
|
|
15
|
+
const open = useAiPanelOpen();
|
|
16
|
+
const configured = useMemo(() => {
|
|
17
|
+
const ai = resolveDeploymentAi();
|
|
18
|
+
return !!ai && ai.enabled !== false;
|
|
19
|
+
}, []);
|
|
20
|
+
if (!configured || !open)
|
|
21
|
+
return null;
|
|
22
|
+
return _jsx(AiChatPanel, {});
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ai-cms-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-cms-panel.js","sourceRoot":"","sources":["../../../src/components/ai/ai-cms-panel.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAElD;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,cAAc,EAAE,CAAA;IAC7B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE;QAC9B,MAAM,EAAE,GAAG,mBAAmB,EAAE,CAAA;QAChC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,OAAO,KAAK,KAAK,CAAA;IACrC,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACrC,OAAO,KAAC,WAAW,KAAG,CAAA;AACxB,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport { resolveDeploymentAi } from '@elytracms/studio/lib/workspace'\nimport { useAiPanelOpen } from '@elytracms/studio/lib/ai-panel'\nimport { AiChatPanel } from './ai-chat-panel-lazy'\n\n/**\n * The CMS-area mount of the AI assistant (EC-199): a right-hand sibling of the CMS\n * content, shown on BOTH the collection list and the document editor. Gated twice —\n * the studio must be `ai`-configured (EC-331 presence-enable) AND the panel open — so\n * the lazy `AiChatPanel` chunk is requested only when a configured studio actually\n * opens it. Sits inside `CmsProvider`, so the panel reads the live CMS store\n * (collections, active locale) for its tools and applies drafts through it.\n */\nexport function AiCmsPanel() {\n const open = useAiPanelOpen()\n const configured = useMemo(() => {\n const ai = resolveDeploymentAi()\n return !!ai && ai.enabled !== false\n }, [])\n if (!configured || !open) return null\n return <AiChatPanel />\n}\n"]}
|