@aglyn/plugins-ai 1.0.0-beta.293 → 1.0.0-beta.296
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 +15 -2
- package/src/lib/components/ai-job-links.js +24 -3
- 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-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 +224 -29
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/jobs/ai-job-edit-step.d.ts +62 -16
- package/src/lib/jobs/ai-job-edit-step.js +172 -61
- 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-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 +18 -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 +86 -0
- package/src/lib/model/assist-page-change.js +86 -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 +51 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit.d.ts +54 -2
- package/src/lib/server/assist-edit.js +219 -30
- 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
|
@@ -23,6 +23,8 @@ import type { AiJob, AiJobOutput } from '../model/ai-jobs.types';
|
|
|
23
23
|
import { type AssistEditCanvasContext, type AssistEditOp, type AssistEditProposal, type AssistEditTarget } from '../model/assist-edit';
|
|
24
24
|
import { type AiCustomGenerationInput } from '../runtime/ai-doctrine';
|
|
25
25
|
import type { AiSystemBlock } from '../runtime/ai-runtime';
|
|
26
|
+
import { readAiSiteDatasets } from '../runtime/site-inventory';
|
|
27
|
+
import { type AssistEditDataset, type AssistEditDatasetScope } from '../server/assist-edit';
|
|
26
28
|
import { type AiJobAdmission } from './ai-job-admission';
|
|
27
29
|
import { type AiJobStepRunner } from './ai-job-text-step';
|
|
28
30
|
/**
|
|
@@ -39,13 +41,19 @@ import { type AiJobStepRunner } from './ai-job-text-step';
|
|
|
39
41
|
*
|
|
40
42
|
* ── Where the change lands, decided before anything is spent ────────────
|
|
41
43
|
*
|
|
42
|
-
* Never on a version a visitor sees.
|
|
43
|
-
*
|
|
44
|
-
* the
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* Never on a version a visitor sees. By default, on every plan, in the
|
|
45
|
+
* WORKING DRAFT of the version it started from (AGL-4116): the one document
|
|
46
|
+
* the Besigner saves to as an author works and offers with Open draft, which
|
|
47
|
+
* publishing moves into the version. The change is built on the draft the
|
|
48
|
+
* author already has, when the Besigner would still offer it, so their work
|
|
49
|
+
* in progress is never thrown away; otherwise on the version's own tree.
|
|
50
|
+
*
|
|
51
|
+
* Only when the request asks for a new version, and the plan keeps version
|
|
52
|
+
* history, a NEW version beside the one it started from — a copy of that
|
|
53
|
+
* version's stored fields with the changed nodes — which becomes the page's
|
|
54
|
+
* current version only when the page is not published; a published page's
|
|
55
|
+
* current version, and a layout's, never move. Asked for without version
|
|
56
|
+
* history, the change goes into the draft and the output says why.
|
|
49
57
|
*
|
|
50
58
|
* ── What a job cannot do that the editor can ────────────────────────────
|
|
51
59
|
*
|
|
@@ -59,8 +67,8 @@ import { type AiJobStepRunner } from './ai-job-text-step';
|
|
|
59
67
|
*
|
|
60
68
|
* The new version is named by the job's id — a build's unit is named by its
|
|
61
69
|
* plan item's id — so a run asked again finds what it wrote and spends
|
|
62
|
-
* nothing. A
|
|
63
|
-
*
|
|
70
|
+
* nothing. A draft the job writes is stamped with the job's id for the same
|
|
71
|
+
* reason.
|
|
64
72
|
*/
|
|
65
73
|
/** The longest the step may take reading the page and writing it. */
|
|
66
74
|
export declare const AI_EDIT_READS_MS = 2000;
|
|
@@ -76,11 +84,22 @@ export declare function aiJobEditModel(): string;
|
|
|
76
84
|
export declare const AI_JOB_EDIT_RULES: AiSystemBlock;
|
|
77
85
|
/** The system blocks for one kind of document. */
|
|
78
86
|
export declare function aiJobEditInstructions(kind: AiEditTargetKind): AiSystemBlock[];
|
|
79
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* What the change is asked from: the document's outline, the site's datasets
|
|
89
|
+
* an element may show the records of (AGL-3616), then the request.
|
|
90
|
+
*/
|
|
80
91
|
export declare function aiJobEditPrompt(input: {
|
|
81
92
|
brief: string;
|
|
82
93
|
context: AssistEditCanvasContext;
|
|
94
|
+
datasets?: readonly AssistEditDataset[];
|
|
83
95
|
}): string;
|
|
96
|
+
/**
|
|
97
|
+
* The datasets an edit to this document knows (AGL-3616): every one the site
|
|
98
|
+
* holds, and the one whose record page the document is, if it is one.
|
|
99
|
+
*/
|
|
100
|
+
export declare function aiEditDatasetScope(datasets: ReadonlyArray<AssistEditDataset & {
|
|
101
|
+
recordScreenId?: string;
|
|
102
|
+
}>, read: Pick<AiEditTargetRead, 'kind' | 'id'>): Required<AssistEditDatasetScope>;
|
|
84
103
|
/** The edit as the doctrine's loop runs it: the rung's tool, held to the rung's check. */
|
|
85
104
|
export declare function aiJobEditGeneration(request: {
|
|
86
105
|
brief: string;
|
|
@@ -88,7 +107,7 @@ export declare function aiJobEditGeneration(request: {
|
|
|
88
107
|
target: AssistEditTarget;
|
|
89
108
|
model: string;
|
|
90
109
|
signal?: AbortSignal;
|
|
91
|
-
}): AiCustomGenerationInput<AssistEditProposal>;
|
|
110
|
+
} & AssistEditDatasetScope): AiCustomGenerationInput<AssistEditProposal>;
|
|
92
111
|
/** A canvas over a stored node map, with the editor's own guarded mutators and no editor. */
|
|
93
112
|
export declare function aiEditCanvas(nodes: NodesMap): CanvasManager;
|
|
94
113
|
/**
|
|
@@ -115,7 +134,16 @@ export interface AiEditTargetRead {
|
|
|
115
134
|
pointerVersionId: string | null;
|
|
116
135
|
/** The version the change starts from. */
|
|
117
136
|
sourceVersionId: string;
|
|
137
|
+
/** The version's own tree. */
|
|
118
138
|
nodes: NodesMap;
|
|
139
|
+
/**
|
|
140
|
+
* The version's working draft, when it has one the Besigner would still
|
|
141
|
+
* offer: an author's saved work in progress, which a change to the draft
|
|
142
|
+
* is built on. Null when there is none, or the version was saved since.
|
|
143
|
+
*/
|
|
144
|
+
draft: {
|
|
145
|
+
nodes: NodesMap;
|
|
146
|
+
} | null;
|
|
119
147
|
/** Whether visitors see the document's current version: a published page, a collection template, any layout. */
|
|
120
148
|
served: boolean;
|
|
121
149
|
/** The version a pending publish schedule will make current, if any. */
|
|
@@ -152,14 +180,29 @@ export declare function aiEditPlacement(input: {
|
|
|
152
180
|
sourceIsPointer: boolean;
|
|
153
181
|
sourceScheduled: boolean;
|
|
154
182
|
}): AiEditPlacement | 'refused';
|
|
183
|
+
/**
|
|
184
|
+
* Where an edit job's change lands (AGL-4116): the version's working draft,
|
|
185
|
+
* unless the request asked for a new version and the plan keeps version
|
|
186
|
+
* history — then where {@link aiEditPlacement} puts a new one. Never refused:
|
|
187
|
+
* a draft is on every plan, and no visitor sees it.
|
|
188
|
+
*/
|
|
189
|
+
export declare function aiEditJobPlacement(input: {
|
|
190
|
+
newVersion: boolean;
|
|
191
|
+
versioning: boolean;
|
|
192
|
+
served: boolean;
|
|
193
|
+
sourceIsPointer: boolean;
|
|
194
|
+
sourceScheduled: boolean;
|
|
195
|
+
}): AiEditPlacement;
|
|
155
196
|
export interface AiEditWriteInput {
|
|
156
197
|
hostId: string;
|
|
157
198
|
kind: AiEditTargetKind;
|
|
158
199
|
id: string;
|
|
159
200
|
sourceVersionId: string;
|
|
160
|
-
/** The new version's id, which is the job's; also the stamp a
|
|
201
|
+
/** The new version's id, which is the job's; also the stamp a draft it writes carries. */
|
|
161
202
|
jobVersionId: string;
|
|
162
203
|
versioning: boolean;
|
|
204
|
+
/** The request asked for a new version (see {@link aiEditJobPlacement}). */
|
|
205
|
+
newVersion: boolean;
|
|
163
206
|
proposal: AssistEditProposal;
|
|
164
207
|
/** Whether the page's search fields may be written: a page no visitor reaches. */
|
|
165
208
|
writeSeo: boolean;
|
|
@@ -189,12 +232,15 @@ export declare function writeAiEdit(firestore: Firestore, input: AiEditWriteInpu
|
|
|
189
232
|
export interface AiJobEditStepDeps {
|
|
190
233
|
readTarget?: typeof readAiEditTarget;
|
|
191
234
|
writeEdit?: typeof writeAiEdit;
|
|
235
|
+
readDatasets?: typeof readAiSiteDatasets;
|
|
192
236
|
}
|
|
193
237
|
/** What an edit wrote, as the job's output: a link into the Besigner on the version written. */
|
|
194
238
|
export declare function aiEditOutput(read: Pick<AiEditTargetRead, 'kind' | 'id' | 'name' | 'hostSubdomain'>, hostId: string, written: {
|
|
195
239
|
versionId: string;
|
|
196
240
|
placement: AiEditPlacement;
|
|
197
|
-
}, leftOut: readonly string[]
|
|
241
|
+
}, leftOut: readonly string[], options?: {
|
|
242
|
+
versionsUnavailable?: boolean;
|
|
243
|
+
}): AiJobOutput;
|
|
198
244
|
/** The new version's id: the job's own, which a build's unit takes from its plan item. */
|
|
199
245
|
export declare function aiEditVersionId(job: Pick<AiJob, '$id'>): string;
|
|
200
246
|
/**
|
|
@@ -214,9 +260,9 @@ export interface AiEditJobAdmissionDeps {
|
|
|
214
260
|
ownerOf?: typeof resolveOrgIdForHost;
|
|
215
261
|
}
|
|
216
262
|
/**
|
|
217
|
-
* An edit job is admitted for a page or layout of a site of its own org
|
|
218
|
-
*
|
|
219
|
-
*
|
|
263
|
+
* An edit job is admitted for a page or layout of a site of its own org that
|
|
264
|
+
* has a version to start from. Every plan has somewhere to put the change
|
|
265
|
+
* that no visitor sees — the version's draft — so no plan is refused.
|
|
220
266
|
*/
|
|
221
267
|
export declare function createAiEditJobAdmission(deps?: AiEditJobAdmissionDeps): AiJobAdmission;
|
|
222
268
|
export declare const aiEditJobAdmission: AiJobAdmission;
|
|
@@ -17,22 +17,25 @@ 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 { 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";
|
|
23
25
|
import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
|
|
24
26
|
import { applyAssistEdit } from "../components/assist-edit-canvas.js";
|
|
25
|
-
import { AI_EDIT_COMPONENT_LEFT_OUT, AI_EDIT_EMAIL_COPY, AI_EDIT_GONE_COPY,
|
|
27
|
+
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
28
|
import { ASSIST_EDIT_TOOL_NAME } from "../model/assist-edit.js";
|
|
27
29
|
import { AI_STEP_TIERS } from "../providers/catalog.js";
|
|
28
30
|
import { AI_ROUTING_TABLE, aiModelForStep } from "../providers/routing.js";
|
|
29
31
|
import { runValidatedGeneration, validateStreamedGeneration } from "../runtime/ai-doctrine.js";
|
|
30
32
|
import { AI_PALETTE } from "../runtime/ai-palette.generated.js";
|
|
31
|
-
import {
|
|
33
|
+
import { readAiSiteDatasets } from "../runtime/site-inventory.js";
|
|
34
|
+
import { assistEditTool, checkAssistEditAnswer, editCanvasBlock, editDatasetsBlock, editSelectionBlock, parseAssistEditContext } from "../server/assist-edit.js";
|
|
32
35
|
import { registerAiJobAdmission } from "./ai-job-admission.js";
|
|
33
36
|
import { aiJobStepBudget } from "./ai-job-budget.js";
|
|
34
37
|
import { aiOriginJobId } from "./ai-job-draft-ids.js";
|
|
35
|
-
import { aiGenerationSpent,
|
|
38
|
+
import { aiGenerationSpent, aiUnspentOutcome } from "./ai-job-generation.js";
|
|
36
39
|
import { AI_JOB_BRIEF_MAX_CHARS } from "./ai-job-text-step.js";
|
|
37
40
|
import { registerAiJobStep } from "./ai-jobs.js";
|
|
38
41
|
/**
|
|
@@ -49,13 +52,19 @@ import { registerAiJobStep } from "./ai-jobs.js";
|
|
|
49
52
|
*
|
|
50
53
|
* ── Where the change lands, decided before anything is spent ────────────
|
|
51
54
|
*
|
|
52
|
-
* Never on a version a visitor sees.
|
|
53
|
-
*
|
|
54
|
-
* the
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
55
|
+
* Never on a version a visitor sees. By default, on every plan, in the
|
|
56
|
+
* WORKING DRAFT of the version it started from (AGL-4116): the one document
|
|
57
|
+
* the Besigner saves to as an author works and offers with Open draft, which
|
|
58
|
+
* publishing moves into the version. The change is built on the draft the
|
|
59
|
+
* author already has, when the Besigner would still offer it, so their work
|
|
60
|
+
* in progress is never thrown away; otherwise on the version's own tree.
|
|
61
|
+
*
|
|
62
|
+
* Only when the request asks for a new version, and the plan keeps version
|
|
63
|
+
* history, a NEW version beside the one it started from — a copy of that
|
|
64
|
+
* version's stored fields with the changed nodes — which becomes the page's
|
|
65
|
+
* current version only when the page is not published; a published page's
|
|
66
|
+
* current version, and a layout's, never move. Asked for without version
|
|
67
|
+
* history, the change goes into the draft and the output says why.
|
|
59
68
|
*
|
|
60
69
|
* ── What a job cannot do that the editor can ────────────────────────────
|
|
61
70
|
*
|
|
@@ -69,8 +78,8 @@ import { registerAiJobStep } from "./ai-jobs.js";
|
|
|
69
78
|
*
|
|
70
79
|
* The new version is named by the job's id — a build's unit is named by its
|
|
71
80
|
* plan item's id — so a run asked again finds what it wrote and spends
|
|
72
|
-
* nothing. A
|
|
73
|
-
*
|
|
81
|
+
* nothing. A draft the job writes is stamped with the job's id for the same
|
|
82
|
+
* reason.
|
|
74
83
|
*/ /** The longest the step may take reading the page and writing it. */ export const AI_EDIT_READS_MS = 2000;
|
|
75
84
|
/** 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
85
|
tier: AI_STEP_TIERS['job.edit'],
|
|
@@ -89,8 +98,8 @@ export function aiJobEditModel() {
|
|
|
89
98
|
*/ export const AI_JOB_EDIT_RULES = {
|
|
90
99
|
text: [
|
|
91
100
|
'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
|
|
101
|
+
`- 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.`,
|
|
102
|
+
'- 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
103
|
`- If the outline does not show an element the request is about, change nothing for that part: never guess at an element.`,
|
|
95
104
|
`- 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
105
|
"- Fill in a page's search title or description only when the request asks for it.",
|
|
@@ -107,13 +116,41 @@ export function aiJobEditModel() {
|
|
|
107
116
|
}
|
|
108
117
|
];
|
|
109
118
|
}
|
|
110
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* What the change is asked from: the document's outline, the site's datasets
|
|
121
|
+
* an element may show the records of (AGL-3616), then the request.
|
|
122
|
+
*/ export function aiJobEditPrompt(input) {
|
|
123
|
+
var _input_datasets;
|
|
124
|
+
const datasets = editDatasetsBlock((_input_datasets = input.datasets) != null ? _input_datasets : []);
|
|
111
125
|
return [
|
|
112
126
|
editSelectionBlock(input.context),
|
|
127
|
+
...datasets ? [
|
|
128
|
+
'',
|
|
129
|
+
datasets
|
|
130
|
+
] : [],
|
|
113
131
|
'',
|
|
114
132
|
`Request: ${input.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`
|
|
115
133
|
].join('\n');
|
|
116
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The datasets an edit to this document knows (AGL-3616): every one the site
|
|
137
|
+
* holds, and the one whose record page the document is, if it is one.
|
|
138
|
+
*/ export function aiEditDatasetScope(datasets, read) {
|
|
139
|
+
var _ref;
|
|
140
|
+
const record = read.kind === 'screen' ? datasets.find((dataset)=>dataset.recordScreenId === read.id) : undefined;
|
|
141
|
+
return {
|
|
142
|
+
datasets: datasets.map(({ id, name, fields, fieldIds, imageField })=>_extends({
|
|
143
|
+
id,
|
|
144
|
+
name,
|
|
145
|
+
fields
|
|
146
|
+
}, fieldIds ? {
|
|
147
|
+
fieldIds
|
|
148
|
+
} : {}, imageField ? {
|
|
149
|
+
imageField
|
|
150
|
+
} : {})),
|
|
151
|
+
recordDatasetId: (_ref = record == null ? void 0 : record.id) != null ? _ref : null
|
|
152
|
+
};
|
|
153
|
+
}
|
|
117
154
|
/** The edit as the doctrine's loop runs it: the rung's tool, held to the rung's check. */ export function aiJobEditGeneration(request) {
|
|
118
155
|
const row = AI_ROUTING_TABLE['job.edit'];
|
|
119
156
|
const kind = request.target.kind;
|
|
@@ -136,10 +173,14 @@ export function aiJobEditModel() {
|
|
|
136
173
|
} : {}, request.signal ? {
|
|
137
174
|
signal: request.signal
|
|
138
175
|
} : {}, {
|
|
139
|
-
check: (answer)=>checkAssistEditAnswer(answer, {
|
|
176
|
+
check: (answer)=>checkAssistEditAnswer(answer, _extends({
|
|
140
177
|
context: request.context,
|
|
141
178
|
target: request.target
|
|
142
|
-
}
|
|
179
|
+
}, request.datasets ? {
|
|
180
|
+
datasets: request.datasets
|
|
181
|
+
} : {}, request.recordDatasetId ? {
|
|
182
|
+
recordDatasetId: request.recordDatasetId
|
|
183
|
+
} : {}))
|
|
143
184
|
});
|
|
144
185
|
}
|
|
145
186
|
// ── The document, headless ────────────────────────────────────────────────
|
|
@@ -214,6 +255,25 @@ function pendingScheduleVersion(doc) {
|
|
|
214
255
|
if (!schedule || schedule['status'] !== 'pending' || schedule['action'] === 'unpublish') return null;
|
|
215
256
|
return text(schedule['versionId']) || null;
|
|
216
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* A version's working draft as the Besigner would offer it: a tree, taken
|
|
260
|
+
* against the version as it is stored now. A draft the version has been
|
|
261
|
+
* saved past is not offered there (it would undo that save), so a change is
|
|
262
|
+
* not built on it here either. `aiEditJobId` names the job that wrote it.
|
|
263
|
+
*/ function usableDraft(draft, version) {
|
|
264
|
+
if (!draft.exists) return null;
|
|
265
|
+
const nodes = decodeStoredNodes(draft.get('nodes'));
|
|
266
|
+
if (!nodes || !Object.keys(nodes).length) return null;
|
|
267
|
+
const baseStamp = typeof draft.get('baseStamp') === 'string' ? draft.get('baseStamp') : null;
|
|
268
|
+
if (hasConcurrentWrite(baseStamp, versionStamp(version.get('updatedAt')))) return null;
|
|
269
|
+
return {
|
|
270
|
+
nodes,
|
|
271
|
+
aiEditJobId: text(draft.get('aiEditJobId')) || null
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** A version's working draft document: the Besigner's, at the core's one address. */ function draftRef(versions, versionId) {
|
|
275
|
+
return versions.doc(versionId).collection(VERSION_DRAFT_COLLECTION).doc(VERSION_DRAFT_DOC_ID);
|
|
276
|
+
}
|
|
217
277
|
/**
|
|
218
278
|
* The document an edit job changes, on the job's own site: the page or
|
|
219
279
|
* layout named, not deleted, not an email design, and the version it starts
|
|
@@ -265,6 +325,7 @@ function pendingScheduleVersion(doc) {
|
|
|
265
325
|
return _extends({}, base, {
|
|
266
326
|
sourceVersionId: sourceVersionId != null ? sourceVersionId : input.jobId,
|
|
267
327
|
nodes: {},
|
|
328
|
+
draft: null,
|
|
268
329
|
written: {
|
|
269
330
|
versionId: input.jobId,
|
|
270
331
|
placement
|
|
@@ -275,7 +336,10 @@ function pendingScheduleVersion(doc) {
|
|
|
275
336
|
status: 404,
|
|
276
337
|
error: AI_EDIT_VERSION_GONE_COPY
|
|
277
338
|
};
|
|
278
|
-
const source = await
|
|
339
|
+
const [source, draftDoc] = await Promise.all([
|
|
340
|
+
versions.doc(sourceVersionId).get(),
|
|
341
|
+
draftRef(versions, sourceVersionId).get()
|
|
342
|
+
]);
|
|
279
343
|
if (!source.exists) return {
|
|
280
344
|
status: 404,
|
|
281
345
|
error: AI_EDIT_VERSION_GONE_COPY
|
|
@@ -284,12 +348,25 @@ function pendingScheduleVersion(doc) {
|
|
|
284
348
|
return _extends({}, base, {
|
|
285
349
|
sourceVersionId,
|
|
286
350
|
nodes: {},
|
|
351
|
+
draft: null,
|
|
287
352
|
written: {
|
|
288
353
|
versionId: sourceVersionId,
|
|
289
354
|
placement: 'in-place'
|
|
290
355
|
}
|
|
291
356
|
});
|
|
292
357
|
}
|
|
358
|
+
const draft = usableDraft(draftDoc, source);
|
|
359
|
+
if (input.jobId && (draft == null ? void 0 : draft.aiEditJobId) === input.jobId) {
|
|
360
|
+
return _extends({}, base, {
|
|
361
|
+
sourceVersionId,
|
|
362
|
+
nodes: {},
|
|
363
|
+
draft: null,
|
|
364
|
+
written: {
|
|
365
|
+
versionId: sourceVersionId,
|
|
366
|
+
placement: 'draft'
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
293
370
|
const nodes = decodeStoredNodes(source.get('nodes'));
|
|
294
371
|
if (!nodes || !Object.keys(nodes).length) return {
|
|
295
372
|
status: 400,
|
|
@@ -298,6 +375,9 @@ function pendingScheduleVersion(doc) {
|
|
|
298
375
|
return _extends({}, base, {
|
|
299
376
|
sourceVersionId,
|
|
300
377
|
nodes,
|
|
378
|
+
draft: draft ? {
|
|
379
|
+
nodes: draft.nodes
|
|
380
|
+
} : null,
|
|
301
381
|
written: null
|
|
302
382
|
});
|
|
303
383
|
}
|
|
@@ -314,6 +394,16 @@ function pendingScheduleVersion(doc) {
|
|
|
314
394
|
const seen = input.sourceScheduled || input.sourceIsPointer && input.served;
|
|
315
395
|
return seen ? 'refused' : 'in-place';
|
|
316
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Where an edit job's change lands (AGL-4116): the version's working draft,
|
|
399
|
+
* unless the request asked for a new version and the plan keeps version
|
|
400
|
+
* history — then where {@link aiEditPlacement} puts a new one. Never refused:
|
|
401
|
+
* a draft is on every plan, and no visitor sees it.
|
|
402
|
+
*/ export function aiEditJobPlacement(input) {
|
|
403
|
+
if (!input.newVersion || !input.versioning) return 'draft';
|
|
404
|
+
const placed = aiEditPlacement(input);
|
|
405
|
+
return placed === 'refused' ? 'draft' : placed;
|
|
406
|
+
}
|
|
317
407
|
/**
|
|
318
408
|
* The change written, in one transaction that reads the document as it is
|
|
319
409
|
* stored NOW — a member may have saved, published or changed plan's history
|
|
@@ -326,12 +416,15 @@ function pendingScheduleVersion(doc) {
|
|
|
326
416
|
const versions = docRef.collection('versions');
|
|
327
417
|
const sourceRef = versions.doc(input.sourceVersionId);
|
|
328
418
|
const newRef = versions.doc(input.jobVersionId);
|
|
419
|
+
const sourceDraftRef = draftRef(versions, input.sourceVersionId);
|
|
329
420
|
return firestore.runTransaction(async (tx)=>{
|
|
330
|
-
|
|
421
|
+
var _ref;
|
|
422
|
+
const [host, doc, source, already, draftDoc] = await Promise.all([
|
|
331
423
|
tx.get(hostRef),
|
|
332
424
|
tx.get(docRef),
|
|
333
425
|
tx.get(sourceRef),
|
|
334
|
-
tx.get(newRef)
|
|
426
|
+
tx.get(newRef),
|
|
427
|
+
tx.get(sourceDraftRef)
|
|
335
428
|
]);
|
|
336
429
|
const pointer = text(doc.get('versionId')) || null;
|
|
337
430
|
if (already.exists) {
|
|
@@ -351,22 +444,30 @@ function pendingScheduleVersion(doc) {
|
|
|
351
444
|
error: AI_EDIT_VERSION_GONE_COPY
|
|
352
445
|
};
|
|
353
446
|
const served = input.kind === 'layout' || screenServed(doc, host);
|
|
354
|
-
const placement =
|
|
447
|
+
const placement = aiEditJobPlacement({
|
|
448
|
+
newVersion: input.newVersion,
|
|
355
449
|
versioning: input.versioning,
|
|
356
450
|
served,
|
|
357
451
|
sourceIsPointer: pointer === input.sourceVersionId,
|
|
358
452
|
sourceScheduled: pendingScheduleVersion(doc) === input.sourceVersionId
|
|
359
453
|
});
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
454
|
+
// Read again here, as the version is: an author who saved their draft,
|
|
455
|
+
// or published it, since the read is what the change is built on now.
|
|
456
|
+
const draft = placement === 'draft' ? usableDraft(draftDoc, source) : null;
|
|
457
|
+
if ((draft == null ? void 0 : draft.aiEditJobId) === input.jobVersionId) {
|
|
458
|
+
return {
|
|
459
|
+
ok: true,
|
|
460
|
+
versionId: input.sourceVersionId,
|
|
461
|
+
placement,
|
|
462
|
+
seoWritten: false
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const nodes = (_ref = draft == null ? void 0 : draft.nodes) != null ? _ref : decodeStoredNodes(source.get('nodes'));
|
|
365
466
|
if (!nodes) return {
|
|
366
467
|
ok: false,
|
|
367
468
|
error: AI_EDIT_NO_NODES_COPY
|
|
368
469
|
};
|
|
369
|
-
const versionId = placement === '
|
|
470
|
+
const versionId = placement === 'draft' ? input.sourceVersionId : input.jobVersionId;
|
|
370
471
|
const seo = {};
|
|
371
472
|
const writeSeo = input.writeSeo && input.kind === 'screen' && !served;
|
|
372
473
|
const proposal = _extends({}, input.proposal, {
|
|
@@ -387,9 +488,14 @@ function pendingScheduleVersion(doc) {
|
|
|
387
488
|
};
|
|
388
489
|
const stored = Buffer.from(packed);
|
|
389
490
|
const docPatch = {};
|
|
390
|
-
if (placement === '
|
|
391
|
-
|
|
491
|
+
if (placement === 'draft') {
|
|
492
|
+
// The whole working tree, as the Besigner writes a draft, taken against
|
|
493
|
+
// the version as stored so the Besigner offers it with Open draft.
|
|
494
|
+
tx.set(sourceDraftRef, {
|
|
392
495
|
nodes: stored,
|
|
496
|
+
baseStamp: versionStamp(source.get('updatedAt')),
|
|
497
|
+
updatedByUid: input.uid,
|
|
498
|
+
updatedByEmail: null,
|
|
393
499
|
updatedAt: input.now,
|
|
394
500
|
aiEditJobId: input.jobVersionId
|
|
395
501
|
});
|
|
@@ -423,20 +529,25 @@ function pendingScheduleVersion(doc) {
|
|
|
423
529
|
};
|
|
424
530
|
});
|
|
425
531
|
}
|
|
426
|
-
/** 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) {
|
|
427
|
-
return {
|
|
532
|
+
/** 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 = {}) {
|
|
533
|
+
return _extends({
|
|
428
534
|
resource: read.kind,
|
|
429
535
|
id: read.id,
|
|
430
536
|
versionId: written.versionId,
|
|
431
537
|
hostId,
|
|
432
538
|
hostSubdomain: read.hostSubdomain,
|
|
433
|
-
label: read.name
|
|
434
|
-
|
|
539
|
+
label: read.name
|
|
540
|
+
}, written.placement === 'draft' ? {
|
|
541
|
+
inDraft: true
|
|
542
|
+
} : {}, {
|
|
543
|
+
note: aiEditOutputNote(_extends({
|
|
435
544
|
kind: read.kind,
|
|
436
545
|
placement: written.placement,
|
|
437
546
|
leftOut
|
|
438
|
-
}
|
|
439
|
-
|
|
547
|
+
}, options.versionsUnavailable ? {
|
|
548
|
+
versionsUnavailable: true
|
|
549
|
+
} : {}))
|
|
550
|
+
});
|
|
440
551
|
}
|
|
441
552
|
/** The new version's id: the job's own, which a build's unit takes from its plan item. */ export function aiEditVersionId(job) {
|
|
442
553
|
return job.$id.slice(0, 64);
|
|
@@ -460,9 +571,10 @@ function pendingScheduleVersion(doc) {
|
|
|
460
571
|
};
|
|
461
572
|
}
|
|
462
573
|
export function createAiJobEditStep(deps = {}) {
|
|
463
|
-
var _deps_readTarget, _deps_writeEdit;
|
|
574
|
+
var _deps_readTarget, _deps_writeEdit, _deps_readDatasets;
|
|
464
575
|
const readTarget = (_deps_readTarget = deps.readTarget) != null ? _deps_readTarget : readAiEditTarget;
|
|
465
576
|
const writeEdit = (_deps_writeEdit = deps.writeEdit) != null ? _deps_writeEdit : writeAiEdit;
|
|
577
|
+
const readDatasets = (_deps_readDatasets = deps.readDatasets) != null ? _deps_readDatasets : readAiSiteDatasets;
|
|
466
578
|
return async ({ job, now, signal, firestore, org, modelFor })=>{
|
|
467
579
|
var _ref, _data;
|
|
468
580
|
const model = (_ref = modelFor == null ? void 0 : modelFor('job.edit')) != null ? _ref : aiJobEditModel();
|
|
@@ -487,30 +599,34 @@ export function createAiJobEditStep(deps = {}) {
|
|
|
487
599
|
});
|
|
488
600
|
const orgData = org != null ? org : (_data = (await firestore.collection('orgs').doc(job.orgId).get()).data()) != null ? _data : null;
|
|
489
601
|
const versioning = checkEntitlement(orgData, 'versioning');
|
|
490
|
-
const placement =
|
|
602
|
+
const placement = aiEditJobPlacement({
|
|
603
|
+
newVersion: inputs.newVersion,
|
|
491
604
|
versioning,
|
|
492
605
|
served: read.served,
|
|
493
606
|
sourceIsPointer: read.sourceVersionId === read.pointerVersionId,
|
|
494
607
|
sourceScheduled: read.scheduledVersionId === read.sourceVersionId
|
|
495
608
|
});
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
609
|
+
// A change to the draft is made on the draft the author already has.
|
|
610
|
+
const outline = aiEditJobOutline(placement === 'draft' && read.draft ? read.draft.nodes : read.nodes);
|
|
611
|
+
const context = outline ? parseAssistEditContext(outline, {
|
|
612
|
+
whole: true
|
|
613
|
+
}) : null;
|
|
501
614
|
if (!context) return unspent(AI_EDIT_NO_NODES_COPY);
|
|
502
615
|
const target = {
|
|
503
616
|
kind: read.kind,
|
|
504
617
|
documentId: read.id,
|
|
505
|
-
versionId: placement === '
|
|
618
|
+
versionId: placement === 'draft' ? read.sourceVersionId : aiEditVersionId(job),
|
|
506
619
|
hostId
|
|
507
620
|
};
|
|
621
|
+
// The site's datasets, so a request to show records binds a repeat to a
|
|
622
|
+
// real one instead of writing tokens nothing fills (AGL-3616).
|
|
623
|
+
const datasets = aiEditDatasetScope(await readDatasets(job.orgId, hostId), read);
|
|
508
624
|
const generation = await runValidatedGeneration('edit', aiJobEditGeneration(_extends({
|
|
509
625
|
brief: job.brief,
|
|
510
626
|
context,
|
|
511
627
|
target,
|
|
512
628
|
model
|
|
513
|
-
}, signal ? {
|
|
629
|
+
}, datasets, signal ? {
|
|
514
630
|
signal
|
|
515
631
|
} : {})));
|
|
516
632
|
const spent = _extends({}, aiGenerationSpent(generation), generation.effort ? {
|
|
@@ -527,10 +643,10 @@ export function createAiJobEditStep(deps = {}) {
|
|
|
527
643
|
if (!proposal && refused) {
|
|
528
644
|
const salvaged = validateStreamedGeneration('edit', {
|
|
529
645
|
answer: refused,
|
|
530
|
-
check: (answer)=>checkAssistEditAnswer(answer, {
|
|
646
|
+
check: (answer)=>checkAssistEditAnswer(answer, _extends({
|
|
531
647
|
context,
|
|
532
648
|
target
|
|
533
|
-
})
|
|
649
|
+
}, datasets))
|
|
534
650
|
});
|
|
535
651
|
proposal = salvaged.status === 'ok' ? salvaged.value : null;
|
|
536
652
|
}
|
|
@@ -557,6 +673,7 @@ export function createAiJobEditStep(deps = {}) {
|
|
|
557
673
|
sourceVersionId: read.sourceVersionId,
|
|
558
674
|
jobVersionId: aiEditVersionId(job),
|
|
559
675
|
versioning,
|
|
676
|
+
newVersion: inputs.newVersion,
|
|
560
677
|
proposal: _extends({}, proposal, {
|
|
561
678
|
ops: kept.ops
|
|
562
679
|
}),
|
|
@@ -569,18 +686,21 @@ export function createAiJobEditStep(deps = {}) {
|
|
|
569
686
|
if (write.ok === false) return _extends({}, spent, {
|
|
570
687
|
failure: write.error
|
|
571
688
|
});
|
|
689
|
+
const versionsUnavailable = inputs.newVersion && !versioning;
|
|
572
690
|
return _extends({}, spent, {
|
|
573
691
|
outputs: [
|
|
574
|
-
aiEditOutput(read, hostId, write, leftOut
|
|
692
|
+
aiEditOutput(read, hostId, write, leftOut, {
|
|
693
|
+
versionsUnavailable
|
|
694
|
+
})
|
|
575
695
|
]
|
|
576
696
|
});
|
|
577
697
|
};
|
|
578
698
|
}
|
|
579
699
|
export const runAiJobEditStep = createAiJobEditStep();
|
|
580
700
|
/**
|
|
581
|
-
* An edit job is admitted for a page or layout of a site of its own org
|
|
582
|
-
*
|
|
583
|
-
*
|
|
701
|
+
* An edit job is admitted for a page or layout of a site of its own org that
|
|
702
|
+
* has a version to start from. Every plan has somewhere to put the change
|
|
703
|
+
* that no visitor sees — the version's draft — so no plan is refused.
|
|
584
704
|
*/ export function createAiEditJobAdmission(deps = {}) {
|
|
585
705
|
var _deps_readTarget, _deps_ownerOf;
|
|
586
706
|
const readTarget = (_deps_readTarget = deps.readTarget) != null ? _deps_readTarget : readAiEditTarget;
|
|
@@ -607,17 +727,8 @@ export const runAiJobEditStep = createAiJobEditStep();
|
|
|
607
727
|
inputs,
|
|
608
728
|
jobId: null
|
|
609
729
|
});
|
|
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;
|
|
730
|
+
// Every plan has somewhere to put the change: the page's draft (AGL-4116).
|
|
731
|
+
return 'error' in read ? read : null;
|
|
621
732
|
};
|
|
622
733
|
}
|
|
623
734
|
export const aiEditJobAdmission = createAiEditJobAdmission();
|