@aglyn/plugins-ai 1.0.0-beta.293 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-job-links.d.ts +17 -3
- package/src/lib/components/ai-job-links.js +32 -5
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +9 -0
- package/src/lib/components/ai-job-requests.js +34 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +28 -2
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/assist-edit-canvas.d.ts +12 -6
- package/src/lib/components/assist-edit-canvas.js +86 -36
- package/src/lib/components/assist-edit-canvas.js.map +1 -1
- package/src/lib/components/assist-edit-card.component.js +9 -8
- package/src/lib/components/assist-edit-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.d.ts +7 -1
- package/src/lib/components/assist-jobs-drawer.component.js +20 -6
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-page-change-card.component.d.ts +44 -0
- package/src/lib/components/assist-page-change-card.component.js +166 -0
- package/src/lib/components/assist-page-change-card.component.js.map +1 -0
- package/src/lib/components/assist-panel.component.d.ts +105 -1
- package/src/lib/components/assist-panel.component.js +242 -36
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/jobs/ai-job-edit-step.d.ts +82 -16
- package/src/lib/jobs/ai-job-edit-step.js +209 -62
- package/src/lib/jobs/ai-job-edit-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.js +4 -2
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/model/ai-edit-job.d.ts +33 -16
- package/src/lib/model/ai-edit-job.js +50 -34
- package/src/lib/model/ai-edit-job.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +6 -11
- package/src/lib/model/ai-job-activity.js +8 -15
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-job-notice.js +3 -1
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +6 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +6 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/assist-edit.d.ts +19 -1
- package/src/lib/model/assist-edit.js +7 -0
- package/src/lib/model/assist-edit.js.map +1 -1
- package/src/lib/model/assist-page-change.d.ts +100 -0
- package/src/lib/model/assist-page-change.js +107 -0
- package/src/lib/model/assist-page-change.js.map +1 -0
- package/src/lib/runtime/ai-eval.js +3 -1
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +5 -0
- package/src/lib/runtime/ai-node-tree.js +18 -3
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +8 -1
- package/src/lib/runtime/site-inventory.js +18 -0
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/assist-build.js +2 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.js +56 -3
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit.d.ts +61 -4
- package/src/lib/server/assist-edit.js +248 -38
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/server/assist-page-change.d.ts +62 -0
- package/src/lib/server/assist-page-change.js +140 -0
- package/src/lib/server/assist-page-change.js.map +1 -0
|
@@ -17,22 +17,26 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
18
|
import { SCREEN_KIND_TEMPLATE } from "@aglyn/aglyn/app-utils/screen-route";
|
|
19
19
|
import { screenSeoStageKey, SCREEN_SEO_TEXT_FIELDS } from "@aglyn/aglyn/app-utils/screen-seo-fields";
|
|
20
|
+
import { hasConcurrentWrite, versionStamp } from "@aglyn/aglyn/app-utils/guarded-version-save";
|
|
20
21
|
import { decodeStoredNodes, encodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
|
|
22
|
+
import { coeditMirrorRoomPath, VERSION_DRAFT_COLLECTION, VERSION_DRAFT_DOC_ID } from "@aglyn/aglyn/app-utils/version-draft";
|
|
21
23
|
import { CanvasManager } from "@aglyn/aglyn/canvas-manager/canvas-manager";
|
|
22
24
|
import { FEATURE_FLAG } from "@aglyn/aglyn/foundation/constants/shared";
|
|
25
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
23
26
|
import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
|
|
24
27
|
import { applyAssistEdit } from "../components/assist-edit-canvas.js";
|
|
25
|
-
import { AI_EDIT_COMPONENT_LEFT_OUT, AI_EDIT_EMAIL_COPY, AI_EDIT_GONE_COPY,
|
|
28
|
+
import { AI_EDIT_COMPONENT_LEFT_OUT, AI_EDIT_EMAIL_COPY, AI_EDIT_GONE_COPY, AI_EDIT_NO_NODES_COPY, AI_EDIT_NO_SITE_COPY, AI_EDIT_NOTHING_COPY, AI_EDIT_REFUSED_COPY, AI_EDIT_SEO_LEFT_OUT, AI_EDIT_STALE_COPY, AI_EDIT_VERSION_GONE_COPY, aiEditJobOutline, aiEditOutputNote, aiEditVersionName, parseAiEditJobInputs } from "../model/ai-edit-job.js";
|
|
26
29
|
import { ASSIST_EDIT_TOOL_NAME } from "../model/assist-edit.js";
|
|
27
30
|
import { AI_STEP_TIERS } from "../providers/catalog.js";
|
|
28
31
|
import { AI_ROUTING_TABLE, aiModelForStep } from "../providers/routing.js";
|
|
29
32
|
import { runValidatedGeneration, validateStreamedGeneration } from "../runtime/ai-doctrine.js";
|
|
30
33
|
import { AI_PALETTE } from "../runtime/ai-palette.generated.js";
|
|
31
|
-
import {
|
|
34
|
+
import { readAiSiteDatasets } from "../runtime/site-inventory.js";
|
|
35
|
+
import { assistEditTool, checkAssistEditAnswer, editCanvasBlock, editDatasetsBlock, editSelectionBlock, parseAssistEditContext } from "../server/assist-edit.js";
|
|
32
36
|
import { registerAiJobAdmission } from "./ai-job-admission.js";
|
|
33
37
|
import { aiJobStepBudget } from "./ai-job-budget.js";
|
|
34
38
|
import { aiOriginJobId } from "./ai-job-draft-ids.js";
|
|
35
|
-
import { aiGenerationSpent,
|
|
39
|
+
import { aiGenerationSpent, aiUnspentOutcome } from "./ai-job-generation.js";
|
|
36
40
|
import { AI_JOB_BRIEF_MAX_CHARS } from "./ai-job-text-step.js";
|
|
37
41
|
import { registerAiJobStep } from "./ai-jobs.js";
|
|
38
42
|
/**
|
|
@@ -49,13 +53,19 @@ import { registerAiJobStep } from "./ai-jobs.js";
|
|
|
49
53
|
*
|
|
50
54
|
* ── Where the change lands, decided before anything is spent ────────────
|
|
51
55
|
*
|
|
52
|
-
* Never on a version a visitor sees.
|
|
53
|
-
*
|
|
54
|
-
* the
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Never on a version a visitor sees. By default, on every plan, in the
|
|
57
|
+
* WORKING DRAFT of the version it started from (AGL-4116): the one document
|
|
58
|
+
* the Besigner saves to as an author works and offers with Open draft, which
|
|
59
|
+
* publishing moves into the version. The change is built on the draft the
|
|
60
|
+
* author already has, when the Besigner would still offer it, so their work
|
|
61
|
+
* in progress is never thrown away; otherwise on the version's own tree.
|
|
62
|
+
*
|
|
63
|
+
* Only when the request asks for a new version, and the plan keeps version
|
|
64
|
+
* history, a NEW version beside the one it started from — a copy of that
|
|
65
|
+
* version's stored fields with the changed nodes — which becomes the page's
|
|
66
|
+
* current version only when the page is not published; a published page's
|
|
67
|
+
* current version, and a layout's, never move. Asked for without version
|
|
68
|
+
* history, the change goes into the draft and the output says why.
|
|
59
69
|
*
|
|
60
70
|
* ── What a job cannot do that the editor can ────────────────────────────
|
|
61
71
|
*
|
|
@@ -69,8 +79,8 @@ import { registerAiJobStep } from "./ai-jobs.js";
|
|
|
69
79
|
*
|
|
70
80
|
* The new version is named by the job's id — a build's unit is named by its
|
|
71
81
|
* plan item's id — so a run asked again finds what it wrote and spends
|
|
72
|
-
* nothing. A
|
|
73
|
-
*
|
|
82
|
+
* nothing. A draft the job writes is stamped with the job's id for the same
|
|
83
|
+
* reason.
|
|
74
84
|
*/ /** The longest the step may take reading the page and writing it. */ export const AI_EDIT_READS_MS = 2000;
|
|
75
85
|
/** The step's time: its answer and its re-ask at the `job.edit` ceiling, with its own reads. */ export const AI_JOB_EDIT_STEP_BUDGET = aiJobStepBudget({
|
|
76
86
|
tier: AI_STEP_TIERS['job.edit'],
|
|
@@ -89,8 +99,8 @@ export function aiJobEditModel() {
|
|
|
89
99
|
*/ export const AI_JOB_EDIT_RULES = {
|
|
90
100
|
text: [
|
|
91
101
|
'You change a page or a layout the person already has, as a job that runs on its own. No editor is open, nothing is selected, and nobody can answer a question before you finish.',
|
|
92
|
-
`- Make exactly the change the request asks for, and keep everything else as it is: other elements, their text, their styles and their order. Never rewrite, restyle, move or remove what the request does not ask about.`,
|
|
93
|
-
'- Find the elements the request means in the
|
|
102
|
+
`- Make exactly the change the request asks for, and keep everything else as it is: other elements, their text, their styles and their order. Never rewrite, restyle, move or remove what the request does not ask about, beyond what making that change needs.`,
|
|
103
|
+
'- Find the elements the request means in the outline of the whole document the request carries, by what they say, what they are and where they sit on the page. Name only ids that outline lists.',
|
|
94
104
|
`- If the outline does not show an element the request is about, change nothing for that part: never guess at an element.`,
|
|
95
105
|
`- Call ${ASSIST_EDIT_TOOL_NAME} exactly once, with every operation. Its summary is one short line saying what changed, in the language of the request; it names the version the change is saved in.`,
|
|
96
106
|
"- Fill in a page's search title or description only when the request asks for it.",
|
|
@@ -107,13 +117,43 @@ export function aiJobEditModel() {
|
|
|
107
117
|
}
|
|
108
118
|
];
|
|
109
119
|
}
|
|
110
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* What the change is asked from: the document's outline, the site's datasets
|
|
122
|
+
* an element may show the records of (AGL-3616), then the request.
|
|
123
|
+
*/ export function aiJobEditPrompt(input) {
|
|
124
|
+
var _input_datasets;
|
|
125
|
+
const datasets = editDatasetsBlock((_input_datasets = input.datasets) != null ? _input_datasets : []);
|
|
111
126
|
return [
|
|
112
|
-
editSelectionBlock(input.context
|
|
127
|
+
editSelectionBlock(input.context, {
|
|
128
|
+
job: true
|
|
129
|
+
}),
|
|
130
|
+
...datasets ? [
|
|
131
|
+
'',
|
|
132
|
+
datasets
|
|
133
|
+
] : [],
|
|
113
134
|
'',
|
|
114
135
|
`Request: ${input.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`
|
|
115
136
|
].join('\n');
|
|
116
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The datasets an edit to this document knows (AGL-3616): every one the site
|
|
140
|
+
* holds, and the one whose record page the document is, if it is one.
|
|
141
|
+
*/ export function aiEditDatasetScope(datasets, read) {
|
|
142
|
+
var _ref;
|
|
143
|
+
const record = read.kind === 'screen' ? datasets.find((dataset)=>dataset.recordScreenId === read.id) : undefined;
|
|
144
|
+
return {
|
|
145
|
+
datasets: datasets.map(({ id, name, fields, fieldIds, imageField })=>_extends({
|
|
146
|
+
id,
|
|
147
|
+
name,
|
|
148
|
+
fields
|
|
149
|
+
}, fieldIds ? {
|
|
150
|
+
fieldIds
|
|
151
|
+
} : {}, imageField ? {
|
|
152
|
+
imageField
|
|
153
|
+
} : {})),
|
|
154
|
+
recordDatasetId: (_ref = record == null ? void 0 : record.id) != null ? _ref : null
|
|
155
|
+
};
|
|
156
|
+
}
|
|
117
157
|
/** The edit as the doctrine's loop runs it: the rung's tool, held to the rung's check. */ export function aiJobEditGeneration(request) {
|
|
118
158
|
const row = AI_ROUTING_TABLE['job.edit'];
|
|
119
159
|
const kind = request.target.kind;
|
|
@@ -136,10 +176,14 @@ export function aiJobEditModel() {
|
|
|
136
176
|
} : {}, request.signal ? {
|
|
137
177
|
signal: request.signal
|
|
138
178
|
} : {}, {
|
|
139
|
-
check: (answer)=>checkAssistEditAnswer(answer, {
|
|
179
|
+
check: (answer)=>checkAssistEditAnswer(answer, _extends({
|
|
140
180
|
context: request.context,
|
|
141
181
|
target: request.target
|
|
142
|
-
}
|
|
182
|
+
}, request.datasets ? {
|
|
183
|
+
datasets: request.datasets
|
|
184
|
+
} : {}, request.recordDatasetId ? {
|
|
185
|
+
recordDatasetId: request.recordDatasetId
|
|
186
|
+
} : {}))
|
|
143
187
|
});
|
|
144
188
|
}
|
|
145
189
|
// ── The document, headless ────────────────────────────────────────────────
|
|
@@ -214,6 +258,25 @@ function pendingScheduleVersion(doc) {
|
|
|
214
258
|
if (!schedule || schedule['status'] !== 'pending' || schedule['action'] === 'unpublish') return null;
|
|
215
259
|
return text(schedule['versionId']) || null;
|
|
216
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* A version's working draft as the Besigner would offer it: a tree, taken
|
|
263
|
+
* against the version as it is stored now. A draft the version has been
|
|
264
|
+
* saved past is not offered there (it would undo that save), so a change is
|
|
265
|
+
* not built on it here either. `aiEditJobId` names the job that wrote it.
|
|
266
|
+
*/ function usableDraft(draft, version) {
|
|
267
|
+
if (!draft.exists) return null;
|
|
268
|
+
const nodes = decodeStoredNodes(draft.get('nodes'));
|
|
269
|
+
if (!nodes || !Object.keys(nodes).length) return null;
|
|
270
|
+
const baseStamp = typeof draft.get('baseStamp') === 'string' ? draft.get('baseStamp') : null;
|
|
271
|
+
if (hasConcurrentWrite(baseStamp, versionStamp(version.get('updatedAt')))) return null;
|
|
272
|
+
return {
|
|
273
|
+
nodes,
|
|
274
|
+
aiEditJobId: text(draft.get('aiEditJobId')) || null
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/** A version's working draft document: the Besigner's, at the core's one address. */ function draftRef(versions, versionId) {
|
|
278
|
+
return versions.doc(versionId).collection(VERSION_DRAFT_COLLECTION).doc(VERSION_DRAFT_DOC_ID);
|
|
279
|
+
}
|
|
217
280
|
/**
|
|
218
281
|
* The document an edit job changes, on the job's own site: the page or
|
|
219
282
|
* layout named, not deleted, not an email design, and the version it starts
|
|
@@ -265,6 +328,7 @@ function pendingScheduleVersion(doc) {
|
|
|
265
328
|
return _extends({}, base, {
|
|
266
329
|
sourceVersionId: sourceVersionId != null ? sourceVersionId : input.jobId,
|
|
267
330
|
nodes: {},
|
|
331
|
+
draft: null,
|
|
268
332
|
written: {
|
|
269
333
|
versionId: input.jobId,
|
|
270
334
|
placement
|
|
@@ -275,7 +339,10 @@ function pendingScheduleVersion(doc) {
|
|
|
275
339
|
status: 404,
|
|
276
340
|
error: AI_EDIT_VERSION_GONE_COPY
|
|
277
341
|
};
|
|
278
|
-
const source = await
|
|
342
|
+
const [source, draftDoc] = await Promise.all([
|
|
343
|
+
versions.doc(sourceVersionId).get(),
|
|
344
|
+
draftRef(versions, sourceVersionId).get()
|
|
345
|
+
]);
|
|
279
346
|
if (!source.exists) return {
|
|
280
347
|
status: 404,
|
|
281
348
|
error: AI_EDIT_VERSION_GONE_COPY
|
|
@@ -284,12 +351,25 @@ function pendingScheduleVersion(doc) {
|
|
|
284
351
|
return _extends({}, base, {
|
|
285
352
|
sourceVersionId,
|
|
286
353
|
nodes: {},
|
|
354
|
+
draft: null,
|
|
287
355
|
written: {
|
|
288
356
|
versionId: sourceVersionId,
|
|
289
357
|
placement: 'in-place'
|
|
290
358
|
}
|
|
291
359
|
});
|
|
292
360
|
}
|
|
361
|
+
const draft = usableDraft(draftDoc, source);
|
|
362
|
+
if (input.jobId && (draft == null ? void 0 : draft.aiEditJobId) === input.jobId) {
|
|
363
|
+
return _extends({}, base, {
|
|
364
|
+
sourceVersionId,
|
|
365
|
+
nodes: {},
|
|
366
|
+
draft: null,
|
|
367
|
+
written: {
|
|
368
|
+
versionId: sourceVersionId,
|
|
369
|
+
placement: 'draft'
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
293
373
|
const nodes = decodeStoredNodes(source.get('nodes'));
|
|
294
374
|
if (!nodes || !Object.keys(nodes).length) return {
|
|
295
375
|
status: 400,
|
|
@@ -298,6 +378,9 @@ function pendingScheduleVersion(doc) {
|
|
|
298
378
|
return _extends({}, base, {
|
|
299
379
|
sourceVersionId,
|
|
300
380
|
nodes,
|
|
381
|
+
draft: draft ? {
|
|
382
|
+
nodes: draft.nodes
|
|
383
|
+
} : null,
|
|
301
384
|
written: null
|
|
302
385
|
});
|
|
303
386
|
}
|
|
@@ -314,6 +397,16 @@ function pendingScheduleVersion(doc) {
|
|
|
314
397
|
const seen = input.sourceScheduled || input.sourceIsPointer && input.served;
|
|
315
398
|
return seen ? 'refused' : 'in-place';
|
|
316
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Where an edit job's change lands (AGL-4116): the version's working draft,
|
|
402
|
+
* unless the request asked for a new version and the plan keeps version
|
|
403
|
+
* history — then where {@link aiEditPlacement} puts a new one. Never refused:
|
|
404
|
+
* a draft is on every plan, and no visitor sees it.
|
|
405
|
+
*/ export function aiEditJobPlacement(input) {
|
|
406
|
+
if (!input.newVersion || !input.versioning) return 'draft';
|
|
407
|
+
const placed = aiEditPlacement(input);
|
|
408
|
+
return placed === 'refused' ? 'draft' : placed;
|
|
409
|
+
}
|
|
317
410
|
/**
|
|
318
411
|
* The change written, in one transaction that reads the document as it is
|
|
319
412
|
* stored NOW — a member may have saved, published or changed plan's history
|
|
@@ -326,12 +419,15 @@ function pendingScheduleVersion(doc) {
|
|
|
326
419
|
const versions = docRef.collection('versions');
|
|
327
420
|
const sourceRef = versions.doc(input.sourceVersionId);
|
|
328
421
|
const newRef = versions.doc(input.jobVersionId);
|
|
422
|
+
const sourceDraftRef = draftRef(versions, input.sourceVersionId);
|
|
329
423
|
return firestore.runTransaction(async (tx)=>{
|
|
330
|
-
|
|
424
|
+
var _ref;
|
|
425
|
+
const [host, doc, source, already, draftDoc] = await Promise.all([
|
|
331
426
|
tx.get(hostRef),
|
|
332
427
|
tx.get(docRef),
|
|
333
428
|
tx.get(sourceRef),
|
|
334
|
-
tx.get(newRef)
|
|
429
|
+
tx.get(newRef),
|
|
430
|
+
tx.get(sourceDraftRef)
|
|
335
431
|
]);
|
|
336
432
|
const pointer = text(doc.get('versionId')) || null;
|
|
337
433
|
if (already.exists) {
|
|
@@ -351,22 +447,30 @@ function pendingScheduleVersion(doc) {
|
|
|
351
447
|
error: AI_EDIT_VERSION_GONE_COPY
|
|
352
448
|
};
|
|
353
449
|
const served = input.kind === 'layout' || screenServed(doc, host);
|
|
354
|
-
const placement =
|
|
450
|
+
const placement = aiEditJobPlacement({
|
|
451
|
+
newVersion: input.newVersion,
|
|
355
452
|
versioning: input.versioning,
|
|
356
453
|
served,
|
|
357
454
|
sourceIsPointer: pointer === input.sourceVersionId,
|
|
358
455
|
sourceScheduled: pendingScheduleVersion(doc) === input.sourceVersionId
|
|
359
456
|
});
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
457
|
+
// Read again here, as the version is: an author who saved their draft,
|
|
458
|
+
// or published it, since the read is what the change is built on now.
|
|
459
|
+
const draft = placement === 'draft' ? usableDraft(draftDoc, source) : null;
|
|
460
|
+
if ((draft == null ? void 0 : draft.aiEditJobId) === input.jobVersionId) {
|
|
461
|
+
return {
|
|
462
|
+
ok: true,
|
|
463
|
+
versionId: input.sourceVersionId,
|
|
464
|
+
placement,
|
|
465
|
+
seoWritten: false
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const nodes = (_ref = draft == null ? void 0 : draft.nodes) != null ? _ref : decodeStoredNodes(source.get('nodes'));
|
|
365
469
|
if (!nodes) return {
|
|
366
470
|
ok: false,
|
|
367
471
|
error: AI_EDIT_NO_NODES_COPY
|
|
368
472
|
};
|
|
369
|
-
const versionId = placement === '
|
|
473
|
+
const versionId = placement === 'draft' ? input.sourceVersionId : input.jobVersionId;
|
|
370
474
|
const seo = {};
|
|
371
475
|
const writeSeo = input.writeSeo && input.kind === 'screen' && !served;
|
|
372
476
|
const proposal = _extends({}, input.proposal, {
|
|
@@ -387,9 +491,14 @@ function pendingScheduleVersion(doc) {
|
|
|
387
491
|
};
|
|
388
492
|
const stored = Buffer.from(packed);
|
|
389
493
|
const docPatch = {};
|
|
390
|
-
if (placement === '
|
|
391
|
-
|
|
494
|
+
if (placement === 'draft') {
|
|
495
|
+
// The whole working tree, as the Besigner writes a draft, taken against
|
|
496
|
+
// the version as stored so the Besigner offers it with Open draft.
|
|
497
|
+
tx.set(sourceDraftRef, {
|
|
392
498
|
nodes: stored,
|
|
499
|
+
baseStamp: versionStamp(source.get('updatedAt')),
|
|
500
|
+
updatedByUid: input.uid,
|
|
501
|
+
updatedByEmail: null,
|
|
393
502
|
updatedAt: input.now,
|
|
394
503
|
aiEditJobId: input.jobVersionId
|
|
395
504
|
});
|
|
@@ -423,20 +532,45 @@ function pendingScheduleVersion(doc) {
|
|
|
423
532
|
};
|
|
424
533
|
});
|
|
425
534
|
}
|
|
426
|
-
/**
|
|
427
|
-
|
|
535
|
+
/**
|
|
536
|
+
* Clears the co-edit mirror of the version whose draft a job just wrote
|
|
537
|
+
* (AGL-4116), as the Besigner's own Save draft does.
|
|
538
|
+
*
|
|
539
|
+
* The mirror holds unsaved canvas work and replays everything newer than the
|
|
540
|
+
* version's last save to whoever opens it. A draft write does not move the
|
|
541
|
+
* version's save, so without this the canvas of someone who had opened the
|
|
542
|
+
* older draft replays over the change the job just saved, and a room with
|
|
543
|
+
* replayed work offers no draft at all: the newer draft can only be reached by
|
|
544
|
+
* a Discard that deletes it. Best effort: a mirror that cannot be cleared
|
|
545
|
+
* leaves the draft saved, and the job reports what it wrote.
|
|
546
|
+
*/ export async function clearAiEditMirror(input) {
|
|
547
|
+
await firebaseAdmin.database().ref(coeditMirrorRoomPath({
|
|
548
|
+
orgId: input.orgId,
|
|
549
|
+
hostId: input.hostId,
|
|
550
|
+
docType: input.kind,
|
|
551
|
+
docId: input.id,
|
|
552
|
+
versionId: input.versionId
|
|
553
|
+
})).remove();
|
|
554
|
+
}
|
|
555
|
+
/** What an edit wrote, as the job's output: a link into the Besigner on the version written. */ export function aiEditOutput(read, hostId, written, leftOut, options = {}) {
|
|
556
|
+
return _extends({
|
|
428
557
|
resource: read.kind,
|
|
429
558
|
id: read.id,
|
|
430
559
|
versionId: written.versionId,
|
|
431
560
|
hostId,
|
|
432
561
|
hostSubdomain: read.hostSubdomain,
|
|
433
|
-
label: read.name
|
|
434
|
-
|
|
562
|
+
label: read.name
|
|
563
|
+
}, written.placement === 'draft' ? {
|
|
564
|
+
inDraft: true
|
|
565
|
+
} : {}, {
|
|
566
|
+
note: aiEditOutputNote(_extends({
|
|
435
567
|
kind: read.kind,
|
|
436
568
|
placement: written.placement,
|
|
437
569
|
leftOut
|
|
438
|
-
}
|
|
439
|
-
|
|
570
|
+
}, options.versionsUnavailable ? {
|
|
571
|
+
versionsUnavailable: true
|
|
572
|
+
} : {}))
|
|
573
|
+
});
|
|
440
574
|
}
|
|
441
575
|
/** The new version's id: the job's own, which a build's unit takes from its plan item. */ export function aiEditVersionId(job) {
|
|
442
576
|
return job.$id.slice(0, 64);
|
|
@@ -460,9 +594,11 @@ function pendingScheduleVersion(doc) {
|
|
|
460
594
|
};
|
|
461
595
|
}
|
|
462
596
|
export function createAiJobEditStep(deps = {}) {
|
|
463
|
-
var _deps_readTarget, _deps_writeEdit;
|
|
597
|
+
var _deps_readTarget, _deps_writeEdit, _deps_readDatasets, _deps_clearMirror;
|
|
464
598
|
const readTarget = (_deps_readTarget = deps.readTarget) != null ? _deps_readTarget : readAiEditTarget;
|
|
465
599
|
const writeEdit = (_deps_writeEdit = deps.writeEdit) != null ? _deps_writeEdit : writeAiEdit;
|
|
600
|
+
const readDatasets = (_deps_readDatasets = deps.readDatasets) != null ? _deps_readDatasets : readAiSiteDatasets;
|
|
601
|
+
const clearMirror = (_deps_clearMirror = deps.clearMirror) != null ? _deps_clearMirror : clearAiEditMirror;
|
|
466
602
|
return async ({ job, now, signal, firestore, org, modelFor })=>{
|
|
467
603
|
var _ref, _data;
|
|
468
604
|
const model = (_ref = modelFor == null ? void 0 : modelFor('job.edit')) != null ? _ref : aiJobEditModel();
|
|
@@ -487,30 +623,34 @@ export function createAiJobEditStep(deps = {}) {
|
|
|
487
623
|
});
|
|
488
624
|
const orgData = org != null ? org : (_data = (await firestore.collection('orgs').doc(job.orgId).get()).data()) != null ? _data : null;
|
|
489
625
|
const versioning = checkEntitlement(orgData, 'versioning');
|
|
490
|
-
const placement =
|
|
626
|
+
const placement = aiEditJobPlacement({
|
|
627
|
+
newVersion: inputs.newVersion,
|
|
491
628
|
versioning,
|
|
492
629
|
served: read.served,
|
|
493
630
|
sourceIsPointer: read.sourceVersionId === read.pointerVersionId,
|
|
494
631
|
sourceScheduled: read.scheduledVersionId === read.sourceVersionId
|
|
495
632
|
});
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
633
|
+
// A change to the draft is made on the draft the author already has.
|
|
634
|
+
const outline = aiEditJobOutline(placement === 'draft' && read.draft ? read.draft.nodes : read.nodes);
|
|
635
|
+
const context = outline ? parseAssistEditContext(outline, {
|
|
636
|
+
whole: true
|
|
637
|
+
}) : null;
|
|
501
638
|
if (!context) return unspent(AI_EDIT_NO_NODES_COPY);
|
|
502
639
|
const target = {
|
|
503
640
|
kind: read.kind,
|
|
504
641
|
documentId: read.id,
|
|
505
|
-
versionId: placement === '
|
|
642
|
+
versionId: placement === 'draft' ? read.sourceVersionId : aiEditVersionId(job),
|
|
506
643
|
hostId
|
|
507
644
|
};
|
|
645
|
+
// The site's datasets, so a request to show records binds a repeat to a
|
|
646
|
+
// real one instead of writing tokens nothing fills (AGL-3616).
|
|
647
|
+
const datasets = aiEditDatasetScope(await readDatasets(job.orgId, hostId), read);
|
|
508
648
|
const generation = await runValidatedGeneration('edit', aiJobEditGeneration(_extends({
|
|
509
649
|
brief: job.brief,
|
|
510
650
|
context,
|
|
511
651
|
target,
|
|
512
652
|
model
|
|
513
|
-
}, signal ? {
|
|
653
|
+
}, datasets, signal ? {
|
|
514
654
|
signal
|
|
515
655
|
} : {})));
|
|
516
656
|
const spent = _extends({}, aiGenerationSpent(generation), generation.effort ? {
|
|
@@ -527,10 +667,10 @@ export function createAiJobEditStep(deps = {}) {
|
|
|
527
667
|
if (!proposal && refused) {
|
|
528
668
|
const salvaged = validateStreamedGeneration('edit', {
|
|
529
669
|
answer: refused,
|
|
530
|
-
check: (answer)=>checkAssistEditAnswer(answer, {
|
|
670
|
+
check: (answer)=>checkAssistEditAnswer(answer, _extends({
|
|
531
671
|
context,
|
|
532
672
|
target
|
|
533
|
-
})
|
|
673
|
+
}, datasets))
|
|
534
674
|
});
|
|
535
675
|
proposal = salvaged.status === 'ok' ? salvaged.value : null;
|
|
536
676
|
}
|
|
@@ -557,6 +697,7 @@ export function createAiJobEditStep(deps = {}) {
|
|
|
557
697
|
sourceVersionId: read.sourceVersionId,
|
|
558
698
|
jobVersionId: aiEditVersionId(job),
|
|
559
699
|
versioning,
|
|
700
|
+
newVersion: inputs.newVersion,
|
|
560
701
|
proposal: _extends({}, proposal, {
|
|
561
702
|
ops: kept.ops
|
|
562
703
|
}),
|
|
@@ -569,18 +710,33 @@ export function createAiJobEditStep(deps = {}) {
|
|
|
569
710
|
if (write.ok === false) return _extends({}, spent, {
|
|
570
711
|
failure: write.error
|
|
571
712
|
});
|
|
713
|
+
if (write.placement === 'draft') {
|
|
714
|
+
await clearMirror({
|
|
715
|
+
orgId: job.orgId,
|
|
716
|
+
hostId,
|
|
717
|
+
kind: read.kind,
|
|
718
|
+
id: read.id,
|
|
719
|
+
versionId: write.versionId
|
|
720
|
+
}).catch((error)=>console.warn('[ai edit] the co-edit mirror could not be cleared', {
|
|
721
|
+
jobId: job.$id,
|
|
722
|
+
error
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
const versionsUnavailable = inputs.newVersion && !versioning;
|
|
572
726
|
return _extends({}, spent, {
|
|
573
727
|
outputs: [
|
|
574
|
-
aiEditOutput(read, hostId, write, leftOut
|
|
728
|
+
aiEditOutput(read, hostId, write, leftOut, {
|
|
729
|
+
versionsUnavailable
|
|
730
|
+
})
|
|
575
731
|
]
|
|
576
732
|
});
|
|
577
733
|
};
|
|
578
734
|
}
|
|
579
735
|
export const runAiJobEditStep = createAiJobEditStep();
|
|
580
736
|
/**
|
|
581
|
-
* An edit job is admitted for a page or layout of a site of its own org
|
|
582
|
-
*
|
|
583
|
-
*
|
|
737
|
+
* An edit job is admitted for a page or layout of a site of its own org that
|
|
738
|
+
* has a version to start from. Every plan has somewhere to put the change
|
|
739
|
+
* that no visitor sees — the version's draft — so no plan is refused.
|
|
584
740
|
*/ export function createAiEditJobAdmission(deps = {}) {
|
|
585
741
|
var _deps_readTarget, _deps_ownerOf;
|
|
586
742
|
const readTarget = (_deps_readTarget = deps.readTarget) != null ? _deps_readTarget : readAiEditTarget;
|
|
@@ -607,17 +763,8 @@ export const runAiJobEditStep = createAiJobEditStep();
|
|
|
607
763
|
inputs,
|
|
608
764
|
jobId: null
|
|
609
765
|
});
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
versioning: checkEntitlement(context.org, 'versioning'),
|
|
613
|
-
served: read.served,
|
|
614
|
-
sourceIsPointer: read.sourceVersionId === read.pointerVersionId,
|
|
615
|
-
sourceScheduled: read.scheduledVersionId === read.sourceVersionId
|
|
616
|
-
});
|
|
617
|
-
return placement === 'refused' ? {
|
|
618
|
-
status: 403,
|
|
619
|
-
error: AI_EDIT_NEEDS_VERSIONING_COPY
|
|
620
|
-
} : null;
|
|
766
|
+
// Every plan has somewhere to put the change: the page's draft (AGL-4116).
|
|
767
|
+
return 'error' in read ? read : null;
|
|
621
768
|
};
|
|
622
769
|
}
|
|
623
770
|
export const aiEditJobAdmission = createAiEditJobAdmission();
|