@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
|
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { CANVAS_ROOT_ELEMENT_ID } from "@aglyn/aglyn/foundation/constants/canvas";
|
|
18
|
-
import {
|
|
18
|
+
import { ASSIST_EDIT_WHOLE_MAX_NODES } from "./assist-edit.js";
|
|
19
19
|
/**
|
|
20
20
|
* An `edit` job (AGL-3616): a change to a page or a layout the site already
|
|
21
21
|
* has, asked for in words, made without the Besigner open.
|
|
@@ -25,13 +25,17 @@ import { ASSIST_EDIT_CONTEXT_MAX_NODES } from "./assist-edit.js";
|
|
|
25
25
|
* — against the stored document instead of an open canvas, and the change is
|
|
26
26
|
* saved where no visitor sees it:
|
|
27
27
|
*
|
|
28
|
-
* - on
|
|
29
|
-
* started from
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
28
|
+
* - by default, on every plan, into the WORKING DRAFT of the version it
|
|
29
|
+
* started from (AGL-4116) — the draft the Besigner saves to as an author
|
|
30
|
+
* works and offers with Open draft, so the author reviews the change and
|
|
31
|
+
* publishes it there exactly as they would their own. A draft the author
|
|
32
|
+
* already has is built on, never replaced;
|
|
33
|
+
* - when the request asks for a new version and the plan keeps version
|
|
34
|
+
* history, as a NEW version beside the one it started from, which becomes
|
|
35
|
+
* the page's current version only when the page is not published; a
|
|
36
|
+
* published page's and a layout's current version never move. Asked for
|
|
37
|
+
* on a plan without version history, the change goes into the draft and
|
|
38
|
+
* the output says why.
|
|
35
39
|
*
|
|
36
40
|
* Pure: the inputs, the sentences a person reads, and the outline the model
|
|
37
41
|
* is shown. The runner is `jobs/ai-job-edit-step.ts`.
|
|
@@ -57,7 +61,8 @@ export const AI_EDIT_VERSION_ID_COPY = 'inputs.versionId must name a version of
|
|
|
57
61
|
return {
|
|
58
62
|
target,
|
|
59
63
|
targetKind: kind ? kind : null,
|
|
60
|
-
versionId: typeof versionId === 'string' && versionId ? versionId : null
|
|
64
|
+
versionId: typeof versionId === 'string' && versionId ? versionId : null,
|
|
65
|
+
newVersion: (inputs == null ? void 0 : inputs['newVersion']) === true
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
68
|
// ── What a person reads ───────────────────────────────────────────────────
|
|
@@ -84,11 +89,16 @@ const NOUNS = {
|
|
|
84
89
|
screen: 'page',
|
|
85
90
|
layout: 'layout'
|
|
86
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* Why a change asked for as a new version went into the draft: the plan
|
|
94
|
+
* keeps no version history. The versions route's own words for the plan.
|
|
95
|
+
*/ export const AI_EDIT_NO_VERSIONS_DRAFT_COPY = 'A new version needs version history, which requires a Pro plan, so the change went into the draft instead.';
|
|
87
96
|
/** The output's note: where the change is, that nothing visitors see moved, and what was left out. */ export function aiEditOutputNote(input) {
|
|
88
97
|
const noun = NOUNS[input.kind];
|
|
89
|
-
const where = input.placement === 'new-current' ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.` : input.placement === 'new-beside' ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.` : `Saved on the version it started from, which no visitor sees: open it to review it.`;
|
|
98
|
+
const where = input.placement === 'draft' ? `Saved as this ${noun}’s draft. Nothing visitors see has changed: open it in the Besigner to review it, then publish it when it is ready.` : input.placement === 'new-current' ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.` : input.placement === 'new-beside' ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.` : `Saved on the version it started from, which no visitor sees: open it to review it.`;
|
|
99
|
+
const versions = input.versionsUnavailable ? ` ${AI_EDIT_NO_VERSIONS_DRAFT_COPY}` : '';
|
|
90
100
|
const leftOut = input.leftOut.length ? ` Left out: ${input.leftOut.join('; ')}.` : '';
|
|
91
|
-
return `${where}${leftOut}`.slice(0, 1000);
|
|
101
|
+
return `${where}${versions}${leftOut}`.slice(0, 1000);
|
|
92
102
|
}
|
|
93
103
|
/** What the output says of search fields a published page keeps on the page itself. */ export const AI_EDIT_SEO_LEFT_OUT = 'the search title and description, which a published page uses as soon as they are saved — change them in Page properties';
|
|
94
104
|
/** What the output says of a part a job cannot save on its own. */ export const AI_EDIT_COMPONENT_LEFT_OUT = 'saving part of the page as a reusable component, which is done in the editor';
|
|
@@ -96,48 +106,52 @@ const NOUNS = {
|
|
|
96
106
|
const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
97
107
|
/**
|
|
98
108
|
* The stored document as the outline the edit rung's protocol reads
|
|
99
|
-
* (`parseAssistEditContext` holds it to its limits): the
|
|
100
|
-
* every element
|
|
101
|
-
* chain the check walks is whole —
|
|
109
|
+
* (`parseAssistEditContext` with `whole` holds it to its limits): the
|
|
110
|
+
* document root, then every element in the order it reads on the page — each
|
|
111
|
+
* after its parent, so every ancestor chain the check walks is whole — with
|
|
112
|
+
* its settings and its styles, up to the whole-document cap.
|
|
102
113
|
*
|
|
103
114
|
* In the editor, an outline is the neighborhood of what the author selected.
|
|
104
|
-
* A job has no selection, so it is the
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
115
|
+
* A job has no selection and nobody to ask, so it is the WHOLE document: a
|
|
116
|
+
* request about a picture five levels down is about an element the model can
|
|
117
|
+
* see, and a restyle starts from the styles the element has. Each element
|
|
118
|
+
* keeps its real child count and the document its real size, so the prompt
|
|
119
|
+
* can say what a document past the cap leaves out. `null` without a root.
|
|
108
120
|
*/ export function aiEditJobOutline(nodes) {
|
|
109
121
|
const root = nodes[CANVAS_ROOT_ELEMENT_ID];
|
|
110
122
|
if (!isRecord(root)) return null;
|
|
111
123
|
const childrenOf = (node)=>Array.isArray(node['nodes']) ? node['nodes'].filter((id)=>typeof id === 'string' && isRecord(nodes[id])) : [];
|
|
112
|
-
const order = [
|
|
124
|
+
const order = [];
|
|
125
|
+
const seen = new Set([
|
|
126
|
+
CANVAS_ROOT_ELEMENT_ID
|
|
127
|
+
]);
|
|
128
|
+
// Depth first, children in their stored order: the page as it reads.
|
|
129
|
+
const stack = [
|
|
113
130
|
{
|
|
114
131
|
id: CANVAS_ROOT_ELEMENT_ID,
|
|
115
132
|
parentId: null,
|
|
116
133
|
index: 0
|
|
117
134
|
}
|
|
118
135
|
];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
childrenOf(nodes[id]).forEach((child, index)=>{
|
|
127
|
-
if (seen.has(child)) return;
|
|
136
|
+
while(stack.length){
|
|
137
|
+
const at = stack.pop();
|
|
138
|
+
order.push(at);
|
|
139
|
+
const children = childrenOf(nodes[at.id]);
|
|
140
|
+
for(let index = children.length - 1; index >= 0; index -= 1){
|
|
141
|
+
const child = children[index];
|
|
142
|
+
if (seen.has(child)) continue;
|
|
128
143
|
seen.add(child);
|
|
129
|
-
|
|
130
|
-
order.push({
|
|
144
|
+
stack.push({
|
|
131
145
|
id: child,
|
|
132
|
-
parentId: id,
|
|
146
|
+
parentId: at.id,
|
|
133
147
|
index
|
|
134
148
|
});
|
|
135
|
-
}
|
|
149
|
+
}
|
|
136
150
|
}
|
|
137
151
|
return {
|
|
138
152
|
selectedId: null,
|
|
139
|
-
total,
|
|
140
|
-
nodes: order.slice(0,
|
|
153
|
+
total: order.length,
|
|
154
|
+
nodes: order.slice(0, ASSIST_EDIT_WHOLE_MAX_NODES).map(({ id, parentId, index })=>{
|
|
141
155
|
const node = nodes[id];
|
|
142
156
|
return _extends({
|
|
143
157
|
id,
|
|
@@ -149,6 +163,8 @@ const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.
|
|
|
149
163
|
name: node['name']
|
|
150
164
|
} : {}, isRecord(node['props']) ? {
|
|
151
165
|
props: node['props']
|
|
166
|
+
} : {}, isRecord(node['sx']) ? {
|
|
167
|
+
sx: node['sx']
|
|
152
168
|
} : {});
|
|
153
169
|
})
|
|
154
170
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-edit-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport { ASSIST_EDIT_CONTEXT_MAX_NODES } from './assist-edit'\n\n/**\n * An `edit` job (AGL-3616): a change to a page or a layout the site already\n * has, asked for in words, made without the Besigner open.\n *\n * It speaks the Assist edit rung's own protocol — the same tool, the same\n * closed world of element ids, the same validators, the same canvas mutators\n * — against the stored document instead of an open canvas, and the change is\n * saved where no visitor sees it:\n *\n * - on a plan with version history, as a NEW version beside the one it\n * started from, which becomes the page's current version only when the\n * page is not published (no visitor reaches it); a published page's and a\n * layout's current version never move;\n * - on a plan without it, in place, and only on a version no visitor\n * reaches — an unpublished page's, or a version that is not the current\n * one. Anything else is refused, before anything is spent.\n *\n * Pure: the inputs, the sentences a person reads, and the outline the model\n * is shown. The runner is `jobs/ai-job-edit-step.ts`.\n */\n\n/** The documents an edit job changes: those that keep versions and render on a site. */\nexport const AI_EDIT_TARGET_KINDS = ['screen', 'layout'] as const\nexport type AiEditTargetKind = (typeof AI_EDIT_TARGET_KINDS)[number]\n\nexport interface AiEditJobInputs {\n /** The page's or layout's document id, on the job's site. */\n target: string\n /** Which collection the id is in; both are looked in, a page's first, when absent. */\n targetKind: AiEditTargetKind | null\n /** The version to start from; the document's current version when absent. */\n versionId: string | null\n}\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/\n\nexport const AI_EDIT_TARGET_REQUIRED_COPY = 'Name the page or layout to change.'\nexport const AI_EDIT_TARGET_KIND_COPY = 'inputs.targetKind must be screen or layout'\nexport const AI_EDIT_VERSION_ID_COPY = 'inputs.versionId must name a version of the page or layout'\n\n/** A job's inputs read, or the sentence that says what is wrong with them. */\nexport function parseAiEditJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiEditJobInputs | string {\n const target = inputs?.['target']\n if (typeof target !== 'string' || !DOCUMENT_ID.test(target)) return AI_EDIT_TARGET_REQUIRED_COPY\n const kind = inputs?.['targetKind']\n if (kind != null && kind !== '' && !(AI_EDIT_TARGET_KINDS as readonly unknown[]).includes(kind)) {\n return AI_EDIT_TARGET_KIND_COPY\n }\n const versionId = inputs?.['versionId']\n if (versionId != null && versionId !== '' && (typeof versionId !== 'string' || !DOCUMENT_ID.test(versionId))) {\n return AI_EDIT_VERSION_ID_COPY\n }\n return {\n target,\n targetKind: kind ? (kind as AiEditTargetKind) : null,\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n }\n}\n\n// ── What a person reads ───────────────────────────────────────────────────\n\nexport const AI_EDIT_NO_SITE_COPY = 'Open the site the change is for before starting the job'\nexport const AI_EDIT_GONE_COPY = 'The page or layout to change is not on the site.'\nexport const AI_EDIT_VERSION_GONE_COPY = 'The version to start from is no longer on the page or layout.'\nexport const AI_EDIT_EMAIL_COPY = 'That is an email design, not a page — change it from Emails → Templates.'\nexport const AI_EDIT_NO_NODES_COPY = 'That version has nothing on it to change.'\n\n/**\n * A change that would land on a version visitors see, on a plan without\n * version history: the versions route's own sentence, after what it means\n * here.\n */\nexport const AI_EDIT_NEEDS_VERSIONING_COPY =\n 'Visitors see that version, so the change has to go into a new version beside it. Version history requires a Pro plan — see Billing to upgrade.'\n\n/** The model answered and no change survived its checks, even after its re-ask. */\nexport const AI_EDIT_NOTHING_COPY =\n 'No change could be made from that request. Say which part of the page to change, the way it reads on the page, and ask again.'\n\n/** The stored document moved under the change between the read and the write. */\nexport const AI_EDIT_STALE_COPY =\n 'The page changed while this edit was being written, so nothing was saved. Ask again.'\n\n/** The canvas's own guards refused part of the change. */\nexport const AI_EDIT_REFUSED_COPY = 'The editor refused part of this change, so none of it was saved.'\n\n/** What a version an edit job makes is called, from the change's one-line summary. */\nexport const AI_EDIT_VERSION_PREFIX = 'AI edit'\n\n/** The longest label a version takes, as the versions route cuts one. */\nexport const AI_EDIT_VERSION_NAME_MAX_CHARS = 200\n\nexport function aiEditVersionName(summary: string): string {\n const said = String(summary ?? '').replace(/\\s+/g, ' ').trim()\n return (said ? `${AI_EDIT_VERSION_PREFIX}: ${said}` : AI_EDIT_VERSION_PREFIX).slice(0, AI_EDIT_VERSION_NAME_MAX_CHARS)\n}\n\n/** Where the change landed, which decides what the output's note says. */\nexport type AiEditPlacement =\n /** A new version, now the unpublished page's current one. */\n | 'new-current'\n /** A new version beside the one it started from, which is unchanged. */\n | 'new-beside'\n /** The version it started from, changed in place: one no visitor reaches. */\n | 'in-place'\n\nconst NOUNS: Readonly<Record<AiEditTargetKind, string>> = { screen: 'page', layout: 'layout' }\n\n/** The output's note: where the change is, that nothing visitors see moved, and what was left out. */\nexport function aiEditOutputNote(input: {\n kind: AiEditTargetKind\n placement: AiEditPlacement\n leftOut: readonly string[]\n}): string {\n const noun = NOUNS[input.kind]\n const where =\n input.placement === 'new-current'\n ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.`\n : input.placement === 'new-beside'\n ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.`\n : `Saved on the version it started from, which no visitor sees: open it to review it.`\n const leftOut = input.leftOut.length ? ` Left out: ${input.leftOut.join('; ')}.` : ''\n return `${where}${leftOut}`.slice(0, 1_000)\n}\n\n/** What the output says of search fields a published page keeps on the page itself. */\nexport const AI_EDIT_SEO_LEFT_OUT =\n 'the search title and description, which a published page uses as soon as they are saved — change them in Page properties'\n\n/** What the output says of a part a job cannot save on its own. */\nexport const AI_EDIT_COMPONENT_LEFT_OUT = 'saving part of the page as a reusable component, which is done in the editor'\n\n// ── The outline the model is shown ────────────────────────────────────────\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * The stored document as the outline the edit rung's protocol reads\n * (`parseAssistEditContext` holds it to its limits): the document root, then\n * every element level by level — each after its parent, so every ancestor\n * chain the check walks is whole — until the rung's cap.\n *\n * In the editor, an outline is the neighborhood of what the author selected.\n * A job has no selection, so it is the TOP of the document, read breadth\n * first: every section, and as deep inside each as the cap reaches. Each\n * element keeps its real child count and the document its real size, so the\n * prompt can say how much the outline leaves out. `null` without a root.\n */\nexport function aiEditJobOutline(nodes: Readonly<Record<string, unknown>>): {\n selectedId: null\n total: number\n nodes: Array<Record<string, unknown>>\n} | null {\n const root = nodes[CANVAS_ROOT_ELEMENT_ID]\n if (!isRecord(root)) return null\n const childrenOf = (node: Record<string, unknown>): string[] =>\n Array.isArray(node['nodes']) ? node['nodes'].filter((id): id is string => typeof id === 'string' && isRecord(nodes[id])) : []\n const order: Array<{ id: string; parentId: string | null; index: number }> = [\n { id: CANVAS_ROOT_ELEMENT_ID, parentId: null, index: 0 },\n ]\n const seen = new Set<string>([CANVAS_ROOT_ELEMENT_ID])\n // Every element reachable through child lists, for the document's size.\n let total = 1\n for (let at = 0; at < order.length; at += 1) {\n const { id } = order[at]\n childrenOf(nodes[id] as Record<string, unknown>).forEach((child, index) => {\n if (seen.has(child)) return\n seen.add(child)\n total += 1\n order.push({ id: child, parentId: id, index })\n })\n }\n return {\n selectedId: null,\n total,\n nodes: order.slice(0, ASSIST_EDIT_CONTEXT_MAX_NODES).map(({ id, parentId, index }) => {\n const node = nodes[id] as Record<string, unknown>\n return {\n id,\n componentId: node['componentId'],\n parentId,\n index,\n childCount: childrenOf(node).length,\n ...(typeof node['name'] === 'string' && node['name'] ? { name: node['name'] } : {}),\n ...(isRecord(node['props']) ? { props: node['props'] } : {}),\n }\n }),\n }\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","ASSIST_EDIT_CONTEXT_MAX_NODES","AI_EDIT_TARGET_KINDS","DOCUMENT_ID","AI_EDIT_TARGET_REQUIRED_COPY","AI_EDIT_TARGET_KIND_COPY","AI_EDIT_VERSION_ID_COPY","parseAiEditJobInputs","inputs","target","test","kind","includes","versionId","targetKind","AI_EDIT_NO_SITE_COPY","AI_EDIT_GONE_COPY","AI_EDIT_VERSION_GONE_COPY","AI_EDIT_EMAIL_COPY","AI_EDIT_NO_NODES_COPY","AI_EDIT_NEEDS_VERSIONING_COPY","AI_EDIT_NOTHING_COPY","AI_EDIT_STALE_COPY","AI_EDIT_REFUSED_COPY","AI_EDIT_VERSION_PREFIX","AI_EDIT_VERSION_NAME_MAX_CHARS","aiEditVersionName","summary","said","String","replace","trim","slice","NOUNS","screen","layout","aiEditOutputNote","input","noun","where","placement","leftOut","length","join","AI_EDIT_SEO_LEFT_OUT","AI_EDIT_COMPONENT_LEFT_OUT","isRecord","value","Array","isArray","aiEditJobOutline","nodes","root","childrenOf","node","filter","id","order","parentId","index","seen","Set","total","at","forEach","child","has","add","push","selectedId","map","componentId","childCount","name","props"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-edit-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { CANVAS_ROOT_ELEMENT_ID } from '@aglyn/aglyn/foundation/constants/canvas'\nimport { ASSIST_EDIT_WHOLE_MAX_NODES } from './assist-edit'\n\n/**\n * An `edit` job (AGL-3616): a change to a page or a layout the site already\n * has, asked for in words, made without the Besigner open.\n *\n * It speaks the Assist edit rung's own protocol — the same tool, the same\n * closed world of element ids, the same validators, the same canvas mutators\n * — against the stored document instead of an open canvas, and the change is\n * saved where no visitor sees it:\n *\n * - by default, on every plan, into the WORKING DRAFT of the version it\n * started from (AGL-4116) — the draft the Besigner saves to as an author\n * works and offers with Open draft, so the author reviews the change and\n * publishes it there exactly as they would their own. A draft the author\n * already has is built on, never replaced;\n * - when the request asks for a new version and the plan keeps version\n * history, as a NEW version beside the one it started from, which becomes\n * the page's current version only when the page is not published; a\n * published page's and a layout's current version never move. Asked for\n * on a plan without version history, the change goes into the draft and\n * the output says why.\n *\n * Pure: the inputs, the sentences a person reads, and the outline the model\n * is shown. The runner is `jobs/ai-job-edit-step.ts`.\n */\n\n/** The documents an edit job changes: those that keep versions and render on a site. */\nexport const AI_EDIT_TARGET_KINDS = ['screen', 'layout'] as const\nexport type AiEditTargetKind = (typeof AI_EDIT_TARGET_KINDS)[number]\n\nexport interface AiEditJobInputs {\n /** The page's or layout's document id, on the job's site. */\n target: string\n /** Which collection the id is in; both are looked in, a page's first, when absent. */\n targetKind: AiEditTargetKind | null\n /** The version to start from; the document's current version when absent. */\n versionId: string | null\n /** The request asked for a new version; otherwise the change goes into the draft. */\n newVersion: boolean\n}\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,64}$/\n\nexport const AI_EDIT_TARGET_REQUIRED_COPY = 'Name the page or layout to change.'\nexport const AI_EDIT_TARGET_KIND_COPY = 'inputs.targetKind must be screen or layout'\nexport const AI_EDIT_VERSION_ID_COPY = 'inputs.versionId must name a version of the page or layout'\n\n/** A job's inputs read, or the sentence that says what is wrong with them. */\nexport function parseAiEditJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiEditJobInputs | string {\n const target = inputs?.['target']\n if (typeof target !== 'string' || !DOCUMENT_ID.test(target)) return AI_EDIT_TARGET_REQUIRED_COPY\n const kind = inputs?.['targetKind']\n if (kind != null && kind !== '' && !(AI_EDIT_TARGET_KINDS as readonly unknown[]).includes(kind)) {\n return AI_EDIT_TARGET_KIND_COPY\n }\n const versionId = inputs?.['versionId']\n if (versionId != null && versionId !== '' && (typeof versionId !== 'string' || !DOCUMENT_ID.test(versionId))) {\n return AI_EDIT_VERSION_ID_COPY\n }\n return {\n target,\n targetKind: kind ? (kind as AiEditTargetKind) : null,\n versionId: typeof versionId === 'string' && versionId ? versionId : null,\n newVersion: inputs?.['newVersion'] === true,\n }\n}\n\n// ── What a person reads ───────────────────────────────────────────────────\n\nexport const AI_EDIT_NO_SITE_COPY = 'Open the site the change is for before starting the job'\nexport const AI_EDIT_GONE_COPY = 'The page or layout to change is not on the site.'\nexport const AI_EDIT_VERSION_GONE_COPY = 'The version to start from is no longer on the page or layout.'\nexport const AI_EDIT_EMAIL_COPY = 'That is an email design, not a page — change it from Emails → Templates.'\nexport const AI_EDIT_NO_NODES_COPY = 'That version has nothing on it to change.'\n\n/**\n * A change that would land on a version visitors see, on a plan without\n * version history: the versions route's own sentence, after what it means\n * here.\n */\nexport const AI_EDIT_NEEDS_VERSIONING_COPY =\n 'Visitors see that version, so the change has to go into a new version beside it. Version history requires a Pro plan — see Billing to upgrade.'\n\n/** The model answered and no change survived its checks, even after its re-ask. */\nexport const AI_EDIT_NOTHING_COPY =\n 'No change could be made from that request. Say which part of the page to change, the way it reads on the page, and ask again.'\n\n/** The stored document moved under the change between the read and the write. */\nexport const AI_EDIT_STALE_COPY =\n 'The page changed while this edit was being written, so nothing was saved. Ask again.'\n\n/** The canvas's own guards refused part of the change. */\nexport const AI_EDIT_REFUSED_COPY = 'The editor refused part of this change, so none of it was saved.'\n\n/** What a version an edit job makes is called, from the change's one-line summary. */\nexport const AI_EDIT_VERSION_PREFIX = 'AI edit'\n\n/** The longest label a version takes, as the versions route cuts one. */\nexport const AI_EDIT_VERSION_NAME_MAX_CHARS = 200\n\nexport function aiEditVersionName(summary: string): string {\n const said = String(summary ?? '').replace(/\\s+/g, ' ').trim()\n return (said ? `${AI_EDIT_VERSION_PREFIX}: ${said}` : AI_EDIT_VERSION_PREFIX).slice(0, AI_EDIT_VERSION_NAME_MAX_CHARS)\n}\n\n/** Where the change landed, which decides what the output's note says. */\nexport type AiEditPlacement =\n /** A new version, now the unpublished page's current one. */\n | 'new-current'\n /** A new version beside the one it started from, which is unchanged. */\n | 'new-beside'\n /** The version it started from, changed in place: one no visitor reaches. Written before AGL-4116. */\n | 'in-place'\n /** The working draft of the version it started from, which the Besigner offers with Open draft. */\n | 'draft'\n\nconst NOUNS: Readonly<Record<AiEditTargetKind, string>> = { screen: 'page', layout: 'layout' }\n\n/**\n * Why a change asked for as a new version went into the draft: the plan\n * keeps no version history. The versions route's own words for the plan.\n */\nexport const AI_EDIT_NO_VERSIONS_DRAFT_COPY =\n 'A new version needs version history, which requires a Pro plan, so the change went into the draft instead.'\n\n/** The output's note: where the change is, that nothing visitors see moved, and what was left out. */\nexport function aiEditOutputNote(input: {\n kind: AiEditTargetKind\n placement: AiEditPlacement\n leftOut: readonly string[]\n /** A new version was asked for on a plan without version history. */\n versionsUnavailable?: boolean\n}): string {\n const noun = NOUNS[input.kind]\n const where =\n input.placement === 'draft'\n ? `Saved as this ${noun}’s draft. Nothing visitors see has changed: open it in the Besigner to review it, then publish it when it is ready.`\n : input.placement === 'new-current'\n ? `Saved as a new version, now this ${noun}’s current one. The ${noun} is not published, so no visitor sees it until you publish it.`\n : input.placement === 'new-beside'\n ? `Saved as a new version beside the one it started from. Nothing visitors see has changed: open it to review it, and publish it when it is ready.`\n : `Saved on the version it started from, which no visitor sees: open it to review it.`\n const versions = input.versionsUnavailable ? ` ${AI_EDIT_NO_VERSIONS_DRAFT_COPY}` : ''\n const leftOut = input.leftOut.length ? ` Left out: ${input.leftOut.join('; ')}.` : ''\n return `${where}${versions}${leftOut}`.slice(0, 1_000)\n}\n\n/** What the output says of search fields a published page keeps on the page itself. */\nexport const AI_EDIT_SEO_LEFT_OUT =\n 'the search title and description, which a published page uses as soon as they are saved — change them in Page properties'\n\n/** What the output says of a part a job cannot save on its own. */\nexport const AI_EDIT_COMPONENT_LEFT_OUT = 'saving part of the page as a reusable component, which is done in the editor'\n\n// ── The outline the model is shown ────────────────────────────────────────\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * The stored document as the outline the edit rung's protocol reads\n * (`parseAssistEditContext` with `whole` holds it to its limits): the\n * document root, then every element in the order it reads on the page — each\n * after its parent, so every ancestor chain the check walks is whole — with\n * its settings and its styles, up to the whole-document cap.\n *\n * In the editor, an outline is the neighborhood of what the author selected.\n * A job has no selection and nobody to ask, so it is the WHOLE document: a\n * request about a picture five levels down is about an element the model can\n * see, and a restyle starts from the styles the element has. Each element\n * keeps its real child count and the document its real size, so the prompt\n * can say what a document past the cap leaves out. `null` without a root.\n */\nexport function aiEditJobOutline(nodes: Readonly<Record<string, unknown>>): {\n selectedId: null\n total: number\n nodes: Array<Record<string, unknown>>\n} | null {\n const root = nodes[CANVAS_ROOT_ELEMENT_ID]\n if (!isRecord(root)) return null\n const childrenOf = (node: Record<string, unknown>): string[] =>\n Array.isArray(node['nodes']) ? node['nodes'].filter((id): id is string => typeof id === 'string' && isRecord(nodes[id])) : []\n const order: Array<{ id: string; parentId: string | null; index: number }> = []\n const seen = new Set<string>([CANVAS_ROOT_ELEMENT_ID])\n // Depth first, children in their stored order: the page as it reads.\n const stack: Array<{ id: string; parentId: string | null; index: number }> = [\n { id: CANVAS_ROOT_ELEMENT_ID, parentId: null, index: 0 },\n ]\n while (stack.length) {\n const at = stack.pop() as (typeof stack)[number]\n order.push(at)\n const children = childrenOf(nodes[at.id] as Record<string, unknown>)\n for (let index = children.length - 1; index >= 0; index -= 1) {\n const child = children[index]\n if (seen.has(child)) continue\n seen.add(child)\n stack.push({ id: child, parentId: at.id, index })\n }\n }\n return {\n selectedId: null,\n total: order.length,\n nodes: order.slice(0, ASSIST_EDIT_WHOLE_MAX_NODES).map(({ id, parentId, index }) => {\n const node = nodes[id] as Record<string, unknown>\n return {\n id,\n componentId: node['componentId'],\n parentId,\n index,\n childCount: childrenOf(node).length,\n ...(typeof node['name'] === 'string' && node['name'] ? { name: node['name'] } : {}),\n ...(isRecord(node['props']) ? { props: node['props'] } : {}),\n ...(isRecord(node['sx']) ? { sx: node['sx'] } : {}),\n }\n }),\n }\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","ASSIST_EDIT_WHOLE_MAX_NODES","AI_EDIT_TARGET_KINDS","DOCUMENT_ID","AI_EDIT_TARGET_REQUIRED_COPY","AI_EDIT_TARGET_KIND_COPY","AI_EDIT_VERSION_ID_COPY","parseAiEditJobInputs","inputs","target","test","kind","includes","versionId","targetKind","newVersion","AI_EDIT_NO_SITE_COPY","AI_EDIT_GONE_COPY","AI_EDIT_VERSION_GONE_COPY","AI_EDIT_EMAIL_COPY","AI_EDIT_NO_NODES_COPY","AI_EDIT_NEEDS_VERSIONING_COPY","AI_EDIT_NOTHING_COPY","AI_EDIT_STALE_COPY","AI_EDIT_REFUSED_COPY","AI_EDIT_VERSION_PREFIX","AI_EDIT_VERSION_NAME_MAX_CHARS","aiEditVersionName","summary","said","String","replace","trim","slice","NOUNS","screen","layout","AI_EDIT_NO_VERSIONS_DRAFT_COPY","aiEditOutputNote","input","noun","where","placement","versions","versionsUnavailable","leftOut","length","join","AI_EDIT_SEO_LEFT_OUT","AI_EDIT_COMPONENT_LEFT_OUT","isRecord","value","Array","isArray","aiEditJobOutline","nodes","root","childrenOf","node","filter","id","order","seen","Set","stack","parentId","index","at","pop","push","children","child","has","add","selectedId","total","map","componentId","childCount","name","props","sx"],"mappings":"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"}
|
|
@@ -138,20 +138,16 @@ export declare function aiJobKindNoun(kind: AiJobKind): string;
|
|
|
138
138
|
* the site as it was, and race for what its plan holds one of.
|
|
139
139
|
*/
|
|
140
140
|
export declare const AI_PAGE_BUILDING_JOB_KINDS: readonly AiJobKind[];
|
|
141
|
-
/**
|
|
142
|
-
* How long a plan waiting for its confirmation still counts as a build on its
|
|
143
|
-
* way (AGL-3765): one the person is reading now, not one left unconfirmed
|
|
144
|
-
* days ago.
|
|
145
|
-
*/
|
|
146
|
-
export declare const AI_SITE_BUILD_PLAN_FRESH_MS: number;
|
|
147
141
|
/** What a door that starts a page says while another is being built on the site (AGL-3765). */
|
|
148
142
|
export declare const AI_SITE_BUILD_IN_FLIGHT_COPY = "A page is already being built. Wait for it, or start another anyway.";
|
|
149
143
|
/**
|
|
150
144
|
* Another job building pages on this site (AGL-3765): queued, planning or
|
|
151
|
-
* building
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
145
|
+
* building. `null` when there is none. A door warns with it and still starts
|
|
146
|
+
* the job: two builds can share a site, they just cannot both make what its
|
|
147
|
+
* plan holds only one of. A plan waiting for its confirmation is not one
|
|
148
|
+
* (AGL-4116): nothing is being built until it is confirmed, and saying so
|
|
149
|
+
* over an unconfirmed plan told a person a page was being built that they
|
|
150
|
+
* had never asked for.
|
|
155
151
|
*/
|
|
156
152
|
export declare function aiSiteBuildInFlight<T extends AiJobPhaseSource & {
|
|
157
153
|
id: string;
|
|
@@ -160,7 +156,6 @@ export declare function aiSiteBuildInFlight<T extends AiJobPhaseSource & {
|
|
|
160
156
|
}>(jobs: readonly T[], input: {
|
|
161
157
|
hostId: string | null | undefined;
|
|
162
158
|
exceptJobId?: string | null;
|
|
163
|
-
now?: number;
|
|
164
159
|
}): T | null;
|
|
165
160
|
/** The fields of a job {@link aiJobSettled} and {@link aiJobProblem} read. */
|
|
166
161
|
export interface AiJobSettleSource {
|
|
@@ -181,30 +181,23 @@ export function aiJobsActivity(jobs) {
|
|
|
181
181
|
'build',
|
|
182
182
|
'import'
|
|
183
183
|
];
|
|
184
|
-
/**
|
|
185
|
-
* How long a plan waiting for its confirmation still counts as a build on its
|
|
186
|
-
* way (AGL-3765): one the person is reading now, not one left unconfirmed
|
|
187
|
-
* days ago.
|
|
188
|
-
*/ export const AI_SITE_BUILD_PLAN_FRESH_MS = 15 * 60000;
|
|
189
184
|
/** What a door that starts a page says while another is being built on the site (AGL-3765). */ export const AI_SITE_BUILD_IN_FLIGHT_COPY = 'A page is already being built. Wait for it, or start another anyway.';
|
|
190
185
|
/**
|
|
191
186
|
* Another job building pages on this site (AGL-3765): queued, planning or
|
|
192
|
-
* building
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
187
|
+
* building. `null` when there is none. A door warns with it and still starts
|
|
188
|
+
* the job: two builds can share a site, they just cannot both make what its
|
|
189
|
+
* plan holds only one of. A plan waiting for its confirmation is not one
|
|
190
|
+
* (AGL-4116): nothing is being built until it is confirmed, and saying so
|
|
191
|
+
* over an unconfirmed plan told a person a page was being built that they
|
|
192
|
+
* had never asked for.
|
|
196
193
|
*/ export function aiSiteBuildInFlight(jobs, input) {
|
|
197
|
-
var
|
|
194
|
+
var _jobs_find;
|
|
198
195
|
if (!input.hostId) return null;
|
|
199
|
-
const now = (_input_now = input.now) != null ? _input_now : Date.now();
|
|
200
196
|
return (_jobs_find = jobs.find((job)=>{
|
|
201
197
|
if (job.id === input.exceptJobId || job.hostId !== input.hostId) return false;
|
|
202
198
|
if (!AI_PAGE_BUILDING_JOB_KINDS.includes(job.kind)) return false;
|
|
203
199
|
const phase = aiJobPhase(job);
|
|
204
|
-
|
|
205
|
-
if (phase !== 'plan-ready') return false;
|
|
206
|
-
const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN;
|
|
207
|
-
return Number.isFinite(at) && now - at < AI_SITE_BUILD_PLAN_FRESH_MS;
|
|
200
|
+
return phase === 'queued' || phase === 'planning' || phase === 'building';
|
|
208
201
|
})) != null ? _jobs_find : null;
|
|
209
202
|
}
|
|
210
203
|
/** Whether a job has stopped moving: finished, or waiting on something only a person can change. */ export function aiJobSettled(job) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-activity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_JOB_PLAN_STEP,\n AI_JOB_TERMINAL_STATUSES,\n type AiJobKind,\n type AiJobReviewReason,\n type AiJobStatus,\n type AiJobStepStatus,\n} from './ai-jobs.types'\n\n/**\n * WHAT A JOB IS DOING, IN THE WORDS EVERY SURFACE SHOWS (AGL-3593).\n *\n * A job's `status` is the machine's word: `needs_review` covers both a plan\n * waiting to be confirmed and an answer that broke a building rule twice, and\n * `queued`/`running` say nothing about whether the job is still planning or\n * already building. The console's surfaces — the AI jobs drawer's header and\n * rows, the top-bar indicator, the Assist launcher's badge, the dialog that\n * started the job, and the notification the machine raises — each tell a\n * person the same thing, so they read it from here and nowhere else. A drawer\n * that counted a refused plan as \"running\" while the chip said \"needs you\" is\n * the disagreement this exists to rule out.\n *\n * Pure, and over the fields a stored job and its wire form share, so the\n * server's notifier and the console read the same answer.\n */\n\n/** The fields a phase is read from: a stored job and its wire summary both carry them. */\nexport interface AiJobPhaseSource {\n status: AiJobStatus\n review?: { reason: AiJobReviewReason } | null\n steps: ReadonlyArray<{ name: string; status: AiJobStepStatus }>\n /** The wire summary's: the machine confirms this job's plan itself. */\n autoConfirm?: boolean\n /** When the job last moved; the wire summary's ISO string. */\n updatedAt?: unknown\n}\n\n/**\n * How long a self-confirming plan may sit parked before it reads as waiting\n * for a person after all: a confirmation that never came.\n */\nexport const AI_JOB_SELF_CONFIRM_GRACE_MS = 5 * 60_000\n\n/**\n * Whether a job is parked on a plan the machine is about to confirm itself\n * (AGL-3596). A guided site start records its plan step — parking the job on\n * the plan as every planned job does — and confirms it in the next write, so\n * a surface that reads in between sees `needs_review` for an instant. That\n * instant is the build going on, never a stop: read as one, it showed the\n * person a failed site that turned green again a moment later. The job's\n * `updatedAt` there is the clock its step was claimed at, so the grace covers\n * the plan step's own run as well as the confirmation. Past\n * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is\n * what it says.\n */\nexport function aiJobConfirmingOwnPlan(\n job: Pick<AiJobPhaseSource, 'status' | 'review' | 'autoConfirm' | 'updatedAt'>,\n now: number = Date.now(),\n): boolean {\n if (job.status !== 'needs_review' || job.review?.reason !== 'plan' || job.autoConfirm !== true) return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return !Number.isFinite(at) || now - at < AI_JOB_SELF_CONFIRM_GRACE_MS\n}\n\n/**\n * Where one job stands, as a person reads it:\n *\n * - `queued` — created, no step has started.\n * - `planning` — its plan step is the one running or next.\n * - `plan-ready` — its plan waits for the person to confirm it.\n * - `building` — a step after the plan (or a kind with no plan) is running or next.\n * - `attention` — it stopped for something only a person can change: an\n * answer that broke a building rule, a site at an allowance, the\n * workspace's credits or a switch.\n * - `done`, `failed`, `canceled` — settled.\n */\nexport type AiJobPhase =\n | 'queued'\n | 'planning'\n | 'plan-ready'\n | 'building'\n | 'attention'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport function aiJobPhase(job: AiJobPhaseSource): AiJobPhase {\n if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {\n return job.status\n }\n if (job.status === 'needs_review' && !aiJobConfirmingOwnPlan(job)) {\n return job.review?.reason === 'plan' ? 'plan-ready' : 'attention'\n }\n if (job.status === 'needs_input') return 'attention'\n // Moving: the first step that has not finished says which half it is in.\n const next = job.steps.find((step) => step.status === 'running' || step.status === 'pending')\n if (next?.name === AI_JOB_PLAN_STEP) return 'planning'\n if (job.status === 'queued' && job.steps.every((step) => step.status === 'pending')) {\n return 'queued'\n }\n return 'building'\n}\n\n/** Each phase in the words a status line and a chip use. */\nexport const AI_JOB_PHASE_LABELS: Readonly<Record<AiJobPhase, string>> = {\n queued: 'Queued',\n planning: 'Planning',\n 'plan-ready': 'Plan ready',\n building: 'Building',\n attention: 'Needs attention',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\n/**\n * What the top-bar indicator, the launcher badge and the drawer's header\n * count a job as, or `null` for a settled job, which none of them count.\n *\n * - `needs-you` — a plan waits to be confirmed: the one decision that, left\n * alone, leaves the person's site unbuilt.\n * - `attention` — stopped for something a person must change, which is not a\n * plan to confirm.\n * - `running` — queued, planning or building. Never a stopped job.\n */\nexport type AiJobActivityState = 'needs-you' | 'attention' | 'running'\n\n/** Loudest first: the order the indicator picks its one state by. */\nexport const AI_JOB_ACTIVITY_PRECEDENCE: readonly AiJobActivityState[] = [\n 'needs-you',\n 'attention',\n 'running',\n]\n\nexport function aiJobActivityState(job: AiJobPhaseSource): AiJobActivityState | null {\n const phase = aiJobPhase(job)\n if (phase === 'plan-ready') return 'needs-you'\n if (phase === 'attention') return 'attention'\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return 'running'\n return null\n}\n\n/**\n * The statuses a job the indicator counts can have: every one that is not in\n * `AI_JOB_TERMINAL_STATUSES`. The jobs route's `status=active` list asks\n * Firestore for exactly these, under the (status, createdAt) index.\n */\nexport const AI_JOB_ACTIVE_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n]\n\n/** How many of a list's jobs stand in each state. */\nexport type AiJobActivityCounts = Readonly<Record<AiJobActivityState, number>>\n\nexport function aiJobActivityCounts(jobs: readonly AiJobPhaseSource[]): AiJobActivityCounts {\n const counts: Record<AiJobActivityState, number> = { 'needs-you': 0, attention: 0, running: 0 }\n for (const job of jobs) {\n const state = aiJobActivityState(job)\n if (state) counts[state] += 1\n }\n return counts\n}\n\n/**\n * What the indicator and the launcher show for a list of jobs: the loudest\n * state any of them is in, how many are in it, and the job the indicator\n * opens — the oldest of them, which has waited longest. `null` when none is\n * moving or waiting, which is when both show nothing.\n */\nexport interface AiJobsActivity<T> {\n state: AiJobActivityState\n count: number\n /** The job a click targets. */\n job: T\n /** Planning rather than building, for the running chip's word. */\n planning: boolean\n}\n\nexport function aiJobsActivity<T extends AiJobPhaseSource>(\n jobs: readonly T[],\n): AiJobsActivity<T> | null {\n for (const state of AI_JOB_ACTIVITY_PRECEDENCE) {\n const matching = jobs.filter((job) => aiJobActivityState(job) === state)\n if (!matching.length) continue\n return {\n state,\n count: matching.length,\n // Lists arrive newest first.\n job: matching[matching.length - 1],\n planning:\n state === 'running' &&\n matching.every((job) => {\n const phase = aiJobPhase(job)\n return phase === 'planning' || phase === 'queued'\n }),\n }\n }\n return null\n}\n\n/** The chip's words: `AI · plan ready`, `AI · 2 need attention`, `AI · planning`. */\nexport function aiJobsActivityLabel(activity: AiJobsActivity<unknown>): string {\n const { state, count } = activity\n if (state === 'needs-you') {\n return count === 1 ? 'AI · plan ready' : `AI · ${count} plans ready`\n }\n if (state === 'attention') {\n return count === 1 ? 'AI · needs attention' : `AI · ${count} need attention`\n }\n const verb = activity.planning ? 'planning' : 'working'\n return count === 1 ? `AI · ${verb}` : `AI · ${count} ${verb}`\n}\n\n/**\n * The launcher's accessible name: what the button opens, and what waits in it.\n * `Aglyn Assist, 1 AI job needs you`.\n */\nexport function aiJobsLauncherLabel(\n productName: string,\n activity: AiJobsActivity<unknown> | null,\n): string {\n const base = `Open ${productName} Assist`\n if (!activity) return base\n const jobs = activity.count === 1 ? '1 AI job' : `${activity.count} AI jobs`\n const verb = activity.count === 1 ? 'needs' : 'need'\n if (activity.state === 'needs-you') return `${base}, ${jobs} ${verb} you`\n if (activity.state === 'attention') return `${base}, ${jobs} ${verb} attention`\n return `${base}, ${jobs} running`\n}\n\n/** The drawer header's chips, one per state that has a job, loudest first. */\nexport function aiJobsHeaderChips(\n counts: AiJobActivityCounts,\n): Array<{ state: AiJobActivityState; label: string }> {\n return AI_JOB_ACTIVITY_PRECEDENCE.filter((state) => counts[state] > 0).map((state) => ({\n state,\n label:\n state === 'needs-you'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} you`\n : state === 'attention'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} attention`\n : `${counts[state]} running`,\n }))\n}\n\n/** What a job makes, as a sentence's subject: `Your site`, `Your page`. */\nconst KIND_NOUNS: Partial<Record<AiJobKind, string>> = {\n site: 'site',\n page: 'page',\n template: 'page template',\n layout: 'layout',\n form: 'form',\n component: 'component',\n email: 'email',\n campaign: 'campaign',\n workflow: 'automation',\n logic: 'function',\n edit: 'change',\n theme: 'theme',\n products: 'products',\n experiment: 'experiment',\n insight: 'answer',\n build: 'build',\n navigation: 'menu',\n duplicate: 'copy',\n settings: 'settings',\n media: 'media library change',\n tracking: 'tracking',\n privacy: 'privacy change',\n import: 'site import',\n}\n\n/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */\nexport function aiJobKindNoun(kind: AiJobKind): string {\n return KIND_NOUNS[kind] ?? 'AI job'\n}\n\n/**\n * The job kinds that build a site's pages (AGL-3765): a page, a guided start,\n * a build from the Assist chat and a site import (AGL-3976). Two at once on one site each plan against\n * the site as it was, and race for what its plan holds one of.\n */\nexport const AI_PAGE_BUILDING_JOB_KINDS: readonly AiJobKind[] = ['page', 'site', 'build', 'import']\n\n/**\n * How long a plan waiting for its confirmation still counts as a build on its\n * way (AGL-3765): one the person is reading now, not one left unconfirmed\n * days ago.\n */\nexport const AI_SITE_BUILD_PLAN_FRESH_MS = 15 * 60_000\n\n/** What a door that starts a page says while another is being built on the site (AGL-3765). */\nexport const AI_SITE_BUILD_IN_FLIGHT_COPY = 'A page is already being built. Wait for it, or start another anyway.'\n\n/**\n * Another job building pages on this site (AGL-3765): queued, planning or\n * building, or a plan that waits for its confirmation and moved in the last\n * {@link AI_SITE_BUILD_PLAN_FRESH_MS}. `null` when there is none. A door warns\n * with it and still starts the job: two builds can share a site, they just\n * cannot both make what its plan holds only one of.\n */\nexport function aiSiteBuildInFlight<\n T extends AiJobPhaseSource & { id: string; kind: AiJobKind | string; hostId?: string | null },\n>(\n jobs: readonly T[],\n input: { hostId: string | null | undefined; exceptJobId?: string | null; now?: number },\n): T | null {\n if (!input.hostId) return null\n const now = input.now ?? Date.now()\n return (\n jobs.find((job) => {\n if (job.id === input.exceptJobId || job.hostId !== input.hostId) return false\n if (!AI_PAGE_BUILDING_JOB_KINDS.includes(job.kind as AiJobKind)) return false\n const phase = aiJobPhase(job)\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return true\n if (phase !== 'plan-ready') return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return Number.isFinite(at) && now - at < AI_SITE_BUILD_PLAN_FRESH_MS\n }) ?? null\n )\n}\n\n/** The fields of a job {@link aiJobSettled} and {@link aiJobProblem} read. */\nexport interface AiJobSettleSource {\n status: AiJobStatus\n error?: string | null\n review?: { message?: string | null } | null\n}\n\n/** Whether a job has stopped moving: finished, or waiting on something only a person can change. */\nexport function aiJobSettled(job: AiJobSettleSource | null): boolean {\n if (!job) return false\n return (\n AI_JOB_TERMINAL_STATUSES.includes(job.status) ||\n job.status === 'needs_input' ||\n job.status === 'needs_review'\n )\n}\n\n/**\n * Why a settled job has no result to show, in words a person can act on, or\n * `null` for a job that finished.\n */\nexport function aiJobProblem(job: AiJobSettleSource | null, failed: string): string | null {\n if (!job || !aiJobSettled(job) || job.status === 'done') return null\n if (job.status === 'canceled') return 'The job was canceled.'\n if (job.status === 'needs_review') return job.review?.message || job.error || failed\n return job.error || failed\n}\n\nexport type AiFollowTone = 'info' | 'warning' | 'success' | 'error'\n\n/**\n * What the follow card's status line says in each phase, and how loudly\n * (AGL-3593): the dialog that started a job, and the apps' follow view.\n */\nexport const AI_JOB_FOLLOW_PHASE_LINES: Readonly<Record<AiJobPhase, { severity: AiFollowTone; text: string }>> = {\n queued: { severity: 'info', text: 'Queued. It starts in a moment.' },\n planning: { severity: 'info', text: 'Planning. The plan appears here when it is ready.' },\n 'plan-ready': {\n severity: 'warning',\n text: 'Your plan is ready. Read it below and confirm it to build — nothing is built until you do.',\n },\n building: { severity: 'info', text: 'Building. Everything it builds is an unpublished draft.' },\n attention: { severity: 'warning', text: 'The job stopped and needs you.' },\n done: { severity: 'success', text: 'Done. Your drafts are ready to open.' },\n failed: { severity: 'error', text: 'The job stopped before it finished.' },\n canceled: { severity: 'info', text: 'The job was canceled.' },\n}\n"],"names":["AI_JOB_PLAN_STEP","AI_JOB_TERMINAL_STATUSES","AI_JOB_SELF_CONFIRM_GRACE_MS","aiJobConfirmingOwnPlan","job","now","Date","status","review","reason","autoConfirm","at","updatedAt","parse","NaN","Number","isFinite","aiJobPhase","next","steps","find","step","name","every","AI_JOB_PHASE_LABELS","queued","planning","building","attention","done","failed","canceled","AI_JOB_ACTIVITY_PRECEDENCE","aiJobActivityState","phase","AI_JOB_ACTIVE_STATUSES","aiJobActivityCounts","jobs","counts","running","state","aiJobsActivity","matching","filter","length","count","aiJobsActivityLabel","activity","verb","aiJobsLauncherLabel","productName","base","aiJobsHeaderChips","map","label","KIND_NOUNS","site","page","template","layout","form","component","email","campaign","workflow","logic","edit","theme","products","experiment","insight","build","navigation","duplicate","settings","media","tracking","privacy","import","aiJobKindNoun","kind","AI_PAGE_BUILDING_JOB_KINDS","AI_SITE_BUILD_PLAN_FRESH_MS","AI_SITE_BUILD_IN_FLIGHT_COPY","aiSiteBuildInFlight","input","hostId","id","exceptJobId","includes","aiJobSettled","aiJobProblem","message","error","AI_JOB_FOLLOW_PHASE_LINES","severity","text"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-activity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AI_JOB_PLAN_STEP,\n AI_JOB_TERMINAL_STATUSES,\n type AiJobKind,\n type AiJobReviewReason,\n type AiJobStatus,\n type AiJobStepStatus,\n} from './ai-jobs.types'\n\n/**\n * WHAT A JOB IS DOING, IN THE WORDS EVERY SURFACE SHOWS (AGL-3593).\n *\n * A job's `status` is the machine's word: `needs_review` covers both a plan\n * waiting to be confirmed and an answer that broke a building rule twice, and\n * `queued`/`running` say nothing about whether the job is still planning or\n * already building. The console's surfaces — the AI jobs drawer's header and\n * rows, the top-bar indicator, the Assist launcher's badge, the dialog that\n * started the job, and the notification the machine raises — each tell a\n * person the same thing, so they read it from here and nowhere else. A drawer\n * that counted a refused plan as \"running\" while the chip said \"needs you\" is\n * the disagreement this exists to rule out.\n *\n * Pure, and over the fields a stored job and its wire form share, so the\n * server's notifier and the console read the same answer.\n */\n\n/** The fields a phase is read from: a stored job and its wire summary both carry them. */\nexport interface AiJobPhaseSource {\n status: AiJobStatus\n review?: { reason: AiJobReviewReason } | null\n steps: ReadonlyArray<{ name: string; status: AiJobStepStatus }>\n /** The wire summary's: the machine confirms this job's plan itself. */\n autoConfirm?: boolean\n /** When the job last moved; the wire summary's ISO string. */\n updatedAt?: unknown\n}\n\n/**\n * How long a self-confirming plan may sit parked before it reads as waiting\n * for a person after all: a confirmation that never came.\n */\nexport const AI_JOB_SELF_CONFIRM_GRACE_MS = 5 * 60_000\n\n/**\n * Whether a job is parked on a plan the machine is about to confirm itself\n * (AGL-3596). A guided site start records its plan step — parking the job on\n * the plan as every planned job does — and confirms it in the next write, so\n * a surface that reads in between sees `needs_review` for an instant. That\n * instant is the build going on, never a stop: read as one, it showed the\n * person a failed site that turned green again a moment later. The job's\n * `updatedAt` there is the clock its step was claimed at, so the grace covers\n * the plan step's own run as well as the confirmation. Past\n * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is\n * what it says.\n */\nexport function aiJobConfirmingOwnPlan(\n job: Pick<AiJobPhaseSource, 'status' | 'review' | 'autoConfirm' | 'updatedAt'>,\n now: number = Date.now(),\n): boolean {\n if (job.status !== 'needs_review' || job.review?.reason !== 'plan' || job.autoConfirm !== true) return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return !Number.isFinite(at) || now - at < AI_JOB_SELF_CONFIRM_GRACE_MS\n}\n\n/**\n * Where one job stands, as a person reads it:\n *\n * - `queued` — created, no step has started.\n * - `planning` — its plan step is the one running or next.\n * - `plan-ready` — its plan waits for the person to confirm it.\n * - `building` — a step after the plan (or a kind with no plan) is running or next.\n * - `attention` — it stopped for something only a person can change: an\n * answer that broke a building rule, a site at an allowance, the\n * workspace's credits or a switch.\n * - `done`, `failed`, `canceled` — settled.\n */\nexport type AiJobPhase =\n | 'queued'\n | 'planning'\n | 'plan-ready'\n | 'building'\n | 'attention'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport function aiJobPhase(job: AiJobPhaseSource): AiJobPhase {\n if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {\n return job.status\n }\n if (job.status === 'needs_review' && !aiJobConfirmingOwnPlan(job)) {\n return job.review?.reason === 'plan' ? 'plan-ready' : 'attention'\n }\n if (job.status === 'needs_input') return 'attention'\n // Moving: the first step that has not finished says which half it is in.\n const next = job.steps.find((step) => step.status === 'running' || step.status === 'pending')\n if (next?.name === AI_JOB_PLAN_STEP) return 'planning'\n if (job.status === 'queued' && job.steps.every((step) => step.status === 'pending')) {\n return 'queued'\n }\n return 'building'\n}\n\n/** Each phase in the words a status line and a chip use. */\nexport const AI_JOB_PHASE_LABELS: Readonly<Record<AiJobPhase, string>> = {\n queued: 'Queued',\n planning: 'Planning',\n 'plan-ready': 'Plan ready',\n building: 'Building',\n attention: 'Needs attention',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\n/**\n * What the top-bar indicator, the launcher badge and the drawer's header\n * count a job as, or `null` for a settled job, which none of them count.\n *\n * - `needs-you` — a plan waits to be confirmed: the one decision that, left\n * alone, leaves the person's site unbuilt.\n * - `attention` — stopped for something a person must change, which is not a\n * plan to confirm.\n * - `running` — queued, planning or building. Never a stopped job.\n */\nexport type AiJobActivityState = 'needs-you' | 'attention' | 'running'\n\n/** Loudest first: the order the indicator picks its one state by. */\nexport const AI_JOB_ACTIVITY_PRECEDENCE: readonly AiJobActivityState[] = [\n 'needs-you',\n 'attention',\n 'running',\n]\n\nexport function aiJobActivityState(job: AiJobPhaseSource): AiJobActivityState | null {\n const phase = aiJobPhase(job)\n if (phase === 'plan-ready') return 'needs-you'\n if (phase === 'attention') return 'attention'\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return 'running'\n return null\n}\n\n/**\n * The statuses a job the indicator counts can have: every one that is not in\n * `AI_JOB_TERMINAL_STATUSES`. The jobs route's `status=active` list asks\n * Firestore for exactly these, under the (status, createdAt) index.\n */\nexport const AI_JOB_ACTIVE_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n]\n\n/** How many of a list's jobs stand in each state. */\nexport type AiJobActivityCounts = Readonly<Record<AiJobActivityState, number>>\n\nexport function aiJobActivityCounts(jobs: readonly AiJobPhaseSource[]): AiJobActivityCounts {\n const counts: Record<AiJobActivityState, number> = { 'needs-you': 0, attention: 0, running: 0 }\n for (const job of jobs) {\n const state = aiJobActivityState(job)\n if (state) counts[state] += 1\n }\n return counts\n}\n\n/**\n * What the indicator and the launcher show for a list of jobs: the loudest\n * state any of them is in, how many are in it, and the job the indicator\n * opens — the oldest of them, which has waited longest. `null` when none is\n * moving or waiting, which is when both show nothing.\n */\nexport interface AiJobsActivity<T> {\n state: AiJobActivityState\n count: number\n /** The job a click targets. */\n job: T\n /** Planning rather than building, for the running chip's word. */\n planning: boolean\n}\n\nexport function aiJobsActivity<T extends AiJobPhaseSource>(\n jobs: readonly T[],\n): AiJobsActivity<T> | null {\n for (const state of AI_JOB_ACTIVITY_PRECEDENCE) {\n const matching = jobs.filter((job) => aiJobActivityState(job) === state)\n if (!matching.length) continue\n return {\n state,\n count: matching.length,\n // Lists arrive newest first.\n job: matching[matching.length - 1],\n planning:\n state === 'running' &&\n matching.every((job) => {\n const phase = aiJobPhase(job)\n return phase === 'planning' || phase === 'queued'\n }),\n }\n }\n return null\n}\n\n/** The chip's words: `AI · plan ready`, `AI · 2 need attention`, `AI · planning`. */\nexport function aiJobsActivityLabel(activity: AiJobsActivity<unknown>): string {\n const { state, count } = activity\n if (state === 'needs-you') {\n return count === 1 ? 'AI · plan ready' : `AI · ${count} plans ready`\n }\n if (state === 'attention') {\n return count === 1 ? 'AI · needs attention' : `AI · ${count} need attention`\n }\n const verb = activity.planning ? 'planning' : 'working'\n return count === 1 ? `AI · ${verb}` : `AI · ${count} ${verb}`\n}\n\n/**\n * The launcher's accessible name: what the button opens, and what waits in it.\n * `Aglyn Assist, 1 AI job needs you`.\n */\nexport function aiJobsLauncherLabel(\n productName: string,\n activity: AiJobsActivity<unknown> | null,\n): string {\n const base = `Open ${productName} Assist`\n if (!activity) return base\n const jobs = activity.count === 1 ? '1 AI job' : `${activity.count} AI jobs`\n const verb = activity.count === 1 ? 'needs' : 'need'\n if (activity.state === 'needs-you') return `${base}, ${jobs} ${verb} you`\n if (activity.state === 'attention') return `${base}, ${jobs} ${verb} attention`\n return `${base}, ${jobs} running`\n}\n\n/** The drawer header's chips, one per state that has a job, loudest first. */\nexport function aiJobsHeaderChips(\n counts: AiJobActivityCounts,\n): Array<{ state: AiJobActivityState; label: string }> {\n return AI_JOB_ACTIVITY_PRECEDENCE.filter((state) => counts[state] > 0).map((state) => ({\n state,\n label:\n state === 'needs-you'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} you`\n : state === 'attention'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} attention`\n : `${counts[state]} running`,\n }))\n}\n\n/** What a job makes, as a sentence's subject: `Your site`, `Your page`. */\nconst KIND_NOUNS: Partial<Record<AiJobKind, string>> = {\n site: 'site',\n page: 'page',\n template: 'page template',\n layout: 'layout',\n form: 'form',\n component: 'component',\n email: 'email',\n campaign: 'campaign',\n workflow: 'automation',\n logic: 'function',\n edit: 'change',\n theme: 'theme',\n products: 'products',\n experiment: 'experiment',\n insight: 'answer',\n build: 'build',\n navigation: 'menu',\n duplicate: 'copy',\n settings: 'settings',\n media: 'media library change',\n tracking: 'tracking',\n privacy: 'privacy change',\n import: 'site import',\n}\n\n/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */\nexport function aiJobKindNoun(kind: AiJobKind): string {\n return KIND_NOUNS[kind] ?? 'AI job'\n}\n\n/**\n * The job kinds that build a site's pages (AGL-3765): a page, a guided start,\n * a build from the Assist chat and a site import (AGL-3976). Two at once on one site each plan against\n * the site as it was, and race for what its plan holds one of.\n */\nexport const AI_PAGE_BUILDING_JOB_KINDS: readonly AiJobKind[] = ['page', 'site', 'build', 'import']\n\n/** What a door that starts a page says while another is being built on the site (AGL-3765). */\nexport const AI_SITE_BUILD_IN_FLIGHT_COPY = 'A page is already being built. Wait for it, or start another anyway.'\n\n/**\n * Another job building pages on this site (AGL-3765): queued, planning or\n * building. `null` when there is none. A door warns with it and still starts\n * the job: two builds can share a site, they just cannot both make what its\n * plan holds only one of. A plan waiting for its confirmation is not one\n * (AGL-4116): nothing is being built until it is confirmed, and saying so\n * over an unconfirmed plan told a person a page was being built that they\n * had never asked for.\n */\nexport function aiSiteBuildInFlight<\n T extends AiJobPhaseSource & { id: string; kind: AiJobKind | string; hostId?: string | null },\n>(\n jobs: readonly T[],\n input: { hostId: string | null | undefined; exceptJobId?: string | null },\n): T | null {\n if (!input.hostId) return null\n return (\n jobs.find((job) => {\n if (job.id === input.exceptJobId || job.hostId !== input.hostId) return false\n if (!AI_PAGE_BUILDING_JOB_KINDS.includes(job.kind as AiJobKind)) return false\n const phase = aiJobPhase(job)\n return phase === 'queued' || phase === 'planning' || phase === 'building'\n }) ?? null\n )\n}\n\n/** The fields of a job {@link aiJobSettled} and {@link aiJobProblem} read. */\nexport interface AiJobSettleSource {\n status: AiJobStatus\n error?: string | null\n review?: { message?: string | null } | null\n}\n\n/** Whether a job has stopped moving: finished, or waiting on something only a person can change. */\nexport function aiJobSettled(job: AiJobSettleSource | null): boolean {\n if (!job) return false\n return (\n AI_JOB_TERMINAL_STATUSES.includes(job.status) ||\n job.status === 'needs_input' ||\n job.status === 'needs_review'\n )\n}\n\n/**\n * Why a settled job has no result to show, in words a person can act on, or\n * `null` for a job that finished.\n */\nexport function aiJobProblem(job: AiJobSettleSource | null, failed: string): string | null {\n if (!job || !aiJobSettled(job) || job.status === 'done') return null\n if (job.status === 'canceled') return 'The job was canceled.'\n if (job.status === 'needs_review') return job.review?.message || job.error || failed\n return job.error || failed\n}\n\nexport type AiFollowTone = 'info' | 'warning' | 'success' | 'error'\n\n/**\n * What the follow card's status line says in each phase, and how loudly\n * (AGL-3593): the dialog that started a job, and the apps' follow view.\n */\nexport const AI_JOB_FOLLOW_PHASE_LINES: Readonly<Record<AiJobPhase, { severity: AiFollowTone; text: string }>> = {\n queued: { severity: 'info', text: 'Queued. It starts in a moment.' },\n planning: { severity: 'info', text: 'Planning. The plan appears here when it is ready.' },\n 'plan-ready': {\n severity: 'warning',\n text: 'Your plan is ready. Read it below and confirm it to build — nothing is built until you do.',\n },\n building: { severity: 'info', text: 'Building. Everything it builds is an unpublished draft.' },\n attention: { severity: 'warning', text: 'The job stopped and needs you.' },\n done: { severity: 'success', text: 'Done. Your drafts are ready to open.' },\n failed: { severity: 'error', text: 'The job stopped before it finished.' },\n canceled: { severity: 'info', text: 'The job was canceled.' },\n}\n"],"names":["AI_JOB_PLAN_STEP","AI_JOB_TERMINAL_STATUSES","AI_JOB_SELF_CONFIRM_GRACE_MS","aiJobConfirmingOwnPlan","job","now","Date","status","review","reason","autoConfirm","at","updatedAt","parse","NaN","Number","isFinite","aiJobPhase","next","steps","find","step","name","every","AI_JOB_PHASE_LABELS","queued","planning","building","attention","done","failed","canceled","AI_JOB_ACTIVITY_PRECEDENCE","aiJobActivityState","phase","AI_JOB_ACTIVE_STATUSES","aiJobActivityCounts","jobs","counts","running","state","aiJobsActivity","matching","filter","length","count","aiJobsActivityLabel","activity","verb","aiJobsLauncherLabel","productName","base","aiJobsHeaderChips","map","label","KIND_NOUNS","site","page","template","layout","form","component","email","campaign","workflow","logic","edit","theme","products","experiment","insight","build","navigation","duplicate","settings","media","tracking","privacy","import","aiJobKindNoun","kind","AI_PAGE_BUILDING_JOB_KINDS","AI_SITE_BUILD_IN_FLIGHT_COPY","aiSiteBuildInFlight","input","hostId","id","exceptJobId","includes","aiJobSettled","aiJobProblem","message","error","AI_JOB_FOLLOW_PHASE_LINES","severity","text"],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { besignerOpenDraftHref } from "@aglyn/aglyn/app-utils/version-draft";
|
|
17
18
|
import { aiBuildOutcomeLine, aiSitePartialCopy } from "./ai-build-progress.js";
|
|
18
19
|
import { aiJobKindNoun } from "./ai-job-activity.js";
|
|
19
20
|
import { aiCustomerSafeCopy, aiJobRefundCopy } from "./ai-job-failure-copy.js";
|
|
@@ -74,7 +75,8 @@ import { aiCustomerSafeCopy, aiJobRefundCopy } from "./ai-job-failure-copy.js";
|
|
|
74
75
|
const versioned = own.find((output)=>AI_JOB_BESIGNER_SEGMENT[output.resource] && output.versionId);
|
|
75
76
|
if (own.length === 1 && versioned) {
|
|
76
77
|
const segment = AI_JOB_BESIGNER_SEGMENT[versioned.resource];
|
|
77
|
-
|
|
78
|
+
const besigner = `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`;
|
|
79
|
+
return versioned.inDraft ? besignerOpenDraftHref(besigner) : besigner;
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-notice.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynNotification } from '@aglyn/aglyn/app-utils/notifications'\nimport { aiBuildOutcomeLine, aiSitePartialCopy } from './ai-build-progress'\nimport { aiJobKindNoun } from './ai-job-activity'\nimport { aiCustomerSafeCopy, aiJobRefundCopy } from './ai-job-failure-copy'\nimport type { AiJobItemLedger, AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types'\n\n/** The besigner segment each versioned resource lives under. */\nexport const AI_JOB_BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>> = {\n screen: 'screens',\n reusableComponent: 'components',\n layout: 'layouts',\n template: 'templates',\n // An email design is a screen: it opens in the screen besigner, as the\n // Emails page's own Edit design does.\n emailScreen: 'screens',\n}\n\n/**\n * The query parameter a console address carries to open AI jobs on one job\n * (AGL-3593): the Assist panel reads it on arrival.\n */\nexport const AI_JOB_LINK_PARAM = 'aiJob'\n\n/** Where a site's \"Building your site\" page lives, under the site (AGL-3594). */\nexport const AI_SITE_BUILD_HREF = '/ai-jobs'\n\n/**\n * What a job the meter paused says comes next (AGL-3660), on its notice and\n * its page alike: nothing it built is lost, and it carries on from where it\n * stopped — by Resume once there are credits, or by itself when they return.\n */\nexport const AI_JOB_PAUSED_NEXT_COPY =\n 'Nothing is lost: what it built so far is saved. Once you have AI credits again — from Billing, ' +\n 'or when your monthly credits reset — choose Resume, or it carries on by itself within about an hour.'\n\n/** A paused job's heading, by what it builds. */\nexport function aiJobPausedTitle(kind: AiJobKind): string {\n const noun = aiJobKindNoun(kind)\n return noun === 'AI job' ? 'Your AI job is paused' : `Your ${noun} ${noun.endsWith('s') ? 'are' : 'is'} paused`\n}\n\n/** A change the person who started a job is told about. */\nexport type AiJobNoticeTransition = 'needs-review' | 'done' | 'failed' | 'paused'\n\n/** The fields of a stored job a notice is written from. */\nexport interface AiJobNoticeSource {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n review?: { reason: AiJobReviewReason; message?: string | null } | null\n outputs?: readonly AiJobOutput[] | null\n error?: string | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: Pick<AiJobSitePublish, 'published' | 'drafts'> | null\n /** What the job spent, and gave back for a failure on our side (AGL-3596). */\n creditsSpent?: number\n refundedCredits?: number\n /** A build's or a guided start's items (AGL-3616), so a finished one says what was not built. */\n items?: AiJobItemLedger[] | null\n /** What a job waiting in `needs_input` asks the person (AGL-3978). */\n input?: { message: string } | null\n}\n\n/** A stopped job's sentence with what became of its credits, from the job's recorded give-back (AGL-3596). */\nfunction withRefund(job: AiJobNoticeSource, text: string, status: 'failed' | 'needs_review'): string {\n const refund = aiJobRefundCopy({ status, refundedCredits: job.refundedCredits, creditsSpent: job.creditsSpent })\n return refund ? `${text} ${refund}` : text\n}\n\nexport type AiJobNotice = Pick<\n AglynNotification,\n 'type' | 'title' | 'body' | 'link' | 'orgId' | 'hostId' | 'level'\n>\n\n/**\n * Where a notice's link goes, in the stored form the console rewrites when it\n * is followed (`normalizeNotificationLink`): `/{hostId}/…` for the job's site,\n * `/org…` for a job of the workspace's.\n *\n * A job that is done leads to what it built (AGL-3593): several pages of the\n * job's site open that site's Pages list, one versioned output opens in the\n * editor on the version the job wrote. Everything else — a plan to confirm, a\n * job that stopped, a done job with nothing of its own to open — opens AI\n * jobs on the job.\n */\nexport function aiJobNoticeLink(job: AiJobNoticeSource, to: AiJobNoticeTransition): string {\n const base = job.hostId ? `/${job.hostId}` : '/org'\n // A site job's every notice opens its \"Building your site\" page (AGL-3594),\n // which leads with the next step whatever the job's state.\n // A site import's too (AGL-3976): its waves are reviewed and started there.\n if ((job.kind === 'site' || job.kind === 'import') && job.hostId) {\n return `${base}${AI_SITE_BUILD_HREF}/${encodeURIComponent(job.$id)}`\n }\n if (to === 'done' && job.hostId) {\n const own = (job.outputs ?? []).filter((output) => output.hostId === job.hostId)\n const screens = own.filter((output) => output.resource === 'screen')\n if (screens.length > 1) return `${base}/screens`\n const versioned = own.find(\n (output) => AI_JOB_BESIGNER_SEGMENT[output.resource] && output.versionId,\n )\n if (own.length === 1 && versioned) {\n const segment = AI_JOB_BESIGNER_SEGMENT[versioned.resource]\n return `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`\n }\n }\n return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`\n}\n\n/**\n * The in-app notification for one change of one job (AGL-3593), for the\n * person who created it. Customer-safe throughout: a stopped job's sentence is\n * the job's own `error`, which the machine writes as a fixed sentence.\n */\n/** The site a job worked on, as its notice names it: its name, and where it is live. */\nexport interface AiJobNoticeSite {\n name: string\n /** The site's live address, absolute; `null` when it has none yet. */\n url: string | null\n}\n\nexport function aiJobNotice(\n job: AiJobNoticeSource,\n to: AiJobNoticeTransition,\n site: AiJobNoticeSite | null = null,\n): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, to)\n if (to === 'needs-review') {\n // A plan that needs an ID only the person has first (AGL-3978).\n if (job.input?.message) {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : noun} needs one detail from you`,\n body: `${job.input.message} Nothing is built until you answer it and confirm the plan. It waits in AI jobs.`,\n link,\n ...scope,\n }\n }\n // A site import's wave is in, or its site is read (AGL-3976): a decision\n // waiting, said as one, never as a job that stopped.\n if (job.review?.reason === 'wave') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'info',\n title: 'Your site import is ready for you',\n body: job.review.message || 'Review the pages brought in, then start the next wave when you are ready.',\n link,\n ...scope,\n }\n }\n if (job.review?.reason === 'plan') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title:\n noun === 'AI job'\n ? 'Your plan is ready: confirm it to build'\n : `Your ${noun} plan is ready: confirm it to build`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs.',\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} needs you`,\n // The doctrine's own words never reach a customer (AGL-3596).\n body: withRefund(\n job,\n aiCustomerSafeCopy(job.review?.message || job.error || 'It stopped for your decision. It waits in AI jobs.', { page: job.kind === 'page' }),\n 'needs_review',\n ),\n link,\n ...scope,\n }\n }\n if (to === 'paused') {\n // Paused by the meter (AGL-3660): out of credits or at a cap. Nothing is\n // lost and the job is not over, so it is a warning that names the way on.\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: aiJobPausedTitle(job.kind),\n body: [job.error, AI_JOB_PAUSED_NEXT_COPY].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n if (to === 'done') {\n // A job that finished with part of it unbuilt (AGL-3596) says so in the\n // words its page uses, as a warning: \"done\" alone hid a page that failed.\n const items = job.items ?? []\n const unbuilt = items.some((row) => row.status === 'failed' || row.status === 'skipped')\n const level = unbuilt ? 'warning' : 'success'\n const partial =\n job.kind === 'site'\n ? aiSitePartialCopy({ items, status: 'done' })\n : unbuilt\n ? aiBuildOutcomeLine({ items, status: 'done' })\n : null\n // A guided site start publishes its pages when it finishes (AGL-3596):\n // with one live, the site is, and the build page lists any that stayed\n // drafts with the reason for each.\n const published = job.kind === 'site' ? (job.sitePublish?.published.length ?? 0) : 0\n if (published > 0) {\n const drafts = job.sitePublish?.drafts.length ?? 0\n const stayed = drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`\n // Named, with its address (AGL-3889): someone with several sites, or\n // reading the email days later, cannot tell which site went live from\n // \"Your site is live\", nor where to see it.\n const live = site ? `${site.name} is live` : 'Your site is live'\n const at = site?.url ? ` at ${site.url}` : ''\n return {\n type: 'content.aiJobDone',\n level,\n title: partial ? `${live}, but part of it wasn’t built` : live,\n body: partial\n ? `${partial} ${drafts > 0 ? `What was built is published${at}, except ${stayed}.` : `What was built is published${at}.`}`\n : drafts > 0\n ? `Your pages are published${at}, except ${stayed}. Open the build page to see why.`\n : at\n ? `Your pages are published${at}. Visit your site there, or open it in the console to edit your pages.`\n : 'Your pages are published. Open it to view your site or edit your pages.',\n link,\n ...scope,\n }\n }\n const pages = (job.outputs ?? []).filter((output) => output.resource === 'screen').length\n const title =\n job.kind === 'site' || pages > 1\n ? 'Your site’s draft pages are ready'\n : job.kind === 'page'\n ? 'Your draft page is ready'\n : noun === 'AI job'\n ? 'Your AI job finished'\n : `Your ${noun} is ready`\n return {\n type: 'content.aiJobDone',\n level,\n title,\n body:\n job.kind === 'insight' || job.kind === 'text'\n ? 'Open AI jobs to read it.'\n : [partial, 'Everything it built is an unpublished draft until you publish it.'].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobFailed',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} stopped`,\n body: withRefund(job, aiCustomerSafeCopy(job.error || 'It stopped before it finished.', { page: job.kind === 'page' }), 'failed'),\n link,\n ...scope,\n }\n}\n\n/**\n * The one reminder a plan that waits for its confirmation gets (AGL-4076),\n * a day after it was ready. The same kind of notice as the first one\n * (`content.aiJobNeedsYou`), so the same switch in the person's notification\n * settings turns both off, and it opens the same place: AI jobs on the job, or\n * a site start's build page. It says it is the only one, because it is.\n */\nexport function aiJobPlanReminderNotice(job: AiJobNoticeSource, site: AiJobNoticeSite | null = null): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, 'needs-review')\n const on = site ? ` for ${site.name}` : ''\n if (job.input?.message) {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun}${on} still needs one detail from you`,\n body: `${job.input.message} Nothing is built until you answer it and confirm the plan. It waits in AI jobs. This is the only reminder.`,\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: noun === 'AI job' ? `Your plan${on} is still waiting for you` : `Your ${noun} plan${on} is still waiting for you`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs. This is the only reminder.',\n link,\n ...scope,\n }\n}\n"],"names":["aiBuildOutcomeLine","aiSitePartialCopy","aiJobKindNoun","aiCustomerSafeCopy","aiJobRefundCopy","AI_JOB_BESIGNER_SEGMENT","screen","reusableComponent","layout","template","emailScreen","AI_JOB_LINK_PARAM","AI_SITE_BUILD_HREF","AI_JOB_PAUSED_NEXT_COPY","aiJobPausedTitle","kind","noun","endsWith","withRefund","job","text","status","refund","refundedCredits","creditsSpent","aiJobNoticeLink","to","base","hostId","encodeURIComponent","$id","own","outputs","filter","output","screens","resource","length","versioned","find","versionId","segment","id","aiJobNotice","site","scope","orgId","link","input","message","type","level","title","body","review","reason","error","page","Boolean","join","items","unbuilt","some","row","partial","published","sitePublish","drafts","stayed","live","name","at","url","pages","aiJobPlanReminderNotice","on"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-notice.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynNotification } from '@aglyn/aglyn/app-utils/notifications'\nimport { besignerOpenDraftHref } from '@aglyn/aglyn/app-utils/version-draft'\nimport { aiBuildOutcomeLine, aiSitePartialCopy } from './ai-build-progress'\nimport { aiJobKindNoun } from './ai-job-activity'\nimport { aiCustomerSafeCopy, aiJobRefundCopy } from './ai-job-failure-copy'\nimport type { AiJobItemLedger, AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types'\n\n/** The besigner segment each versioned resource lives under. */\nexport const AI_JOB_BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>> = {\n screen: 'screens',\n reusableComponent: 'components',\n layout: 'layouts',\n template: 'templates',\n // An email design is a screen: it opens in the screen besigner, as the\n // Emails page's own Edit design does.\n emailScreen: 'screens',\n}\n\n/**\n * The query parameter a console address carries to open AI jobs on one job\n * (AGL-3593): the Assist panel reads it on arrival.\n */\nexport const AI_JOB_LINK_PARAM = 'aiJob'\n\n/** Where a site's \"Building your site\" page lives, under the site (AGL-3594). */\nexport const AI_SITE_BUILD_HREF = '/ai-jobs'\n\n/**\n * What a job the meter paused says comes next (AGL-3660), on its notice and\n * its page alike: nothing it built is lost, and it carries on from where it\n * stopped — by Resume once there are credits, or by itself when they return.\n */\nexport const AI_JOB_PAUSED_NEXT_COPY =\n 'Nothing is lost: what it built so far is saved. Once you have AI credits again — from Billing, ' +\n 'or when your monthly credits reset — choose Resume, or it carries on by itself within about an hour.'\n\n/** A paused job's heading, by what it builds. */\nexport function aiJobPausedTitle(kind: AiJobKind): string {\n const noun = aiJobKindNoun(kind)\n return noun === 'AI job' ? 'Your AI job is paused' : `Your ${noun} ${noun.endsWith('s') ? 'are' : 'is'} paused`\n}\n\n/** A change the person who started a job is told about. */\nexport type AiJobNoticeTransition = 'needs-review' | 'done' | 'failed' | 'paused'\n\n/** The fields of a stored job a notice is written from. */\nexport interface AiJobNoticeSource {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n review?: { reason: AiJobReviewReason; message?: string | null } | null\n outputs?: readonly AiJobOutput[] | null\n error?: string | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: Pick<AiJobSitePublish, 'published' | 'drafts'> | null\n /** What the job spent, and gave back for a failure on our side (AGL-3596). */\n creditsSpent?: number\n refundedCredits?: number\n /** A build's or a guided start's items (AGL-3616), so a finished one says what was not built. */\n items?: AiJobItemLedger[] | null\n /** What a job waiting in `needs_input` asks the person (AGL-3978). */\n input?: { message: string } | null\n}\n\n/** A stopped job's sentence with what became of its credits, from the job's recorded give-back (AGL-3596). */\nfunction withRefund(job: AiJobNoticeSource, text: string, status: 'failed' | 'needs_review'): string {\n const refund = aiJobRefundCopy({ status, refundedCredits: job.refundedCredits, creditsSpent: job.creditsSpent })\n return refund ? `${text} ${refund}` : text\n}\n\nexport type AiJobNotice = Pick<\n AglynNotification,\n 'type' | 'title' | 'body' | 'link' | 'orgId' | 'hostId' | 'level'\n>\n\n/**\n * Where a notice's link goes, in the stored form the console rewrites when it\n * is followed (`normalizeNotificationLink`): `/{hostId}/…` for the job's site,\n * `/org…` for a job of the workspace's.\n *\n * A job that is done leads to what it built (AGL-3593): several pages of the\n * job's site open that site's Pages list, one versioned output opens in the\n * editor on the version the job wrote. Everything else — a plan to confirm, a\n * job that stopped, a done job with nothing of its own to open — opens AI\n * jobs on the job.\n */\nexport function aiJobNoticeLink(job: AiJobNoticeSource, to: AiJobNoticeTransition): string {\n const base = job.hostId ? `/${job.hostId}` : '/org'\n // A site job's every notice opens its \"Building your site\" page (AGL-3594),\n // which leads with the next step whatever the job's state.\n // A site import's too (AGL-3976): its waves are reviewed and started there.\n if ((job.kind === 'site' || job.kind === 'import') && job.hostId) {\n return `${base}${AI_SITE_BUILD_HREF}/${encodeURIComponent(job.$id)}`\n }\n if (to === 'done' && job.hostId) {\n const own = (job.outputs ?? []).filter((output) => output.hostId === job.hostId)\n const screens = own.filter((output) => output.resource === 'screen')\n if (screens.length > 1) return `${base}/screens`\n const versioned = own.find(\n (output) => AI_JOB_BESIGNER_SEGMENT[output.resource] && output.versionId,\n )\n if (own.length === 1 && versioned) {\n const segment = AI_JOB_BESIGNER_SEGMENT[versioned.resource]\n const besigner = `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`\n return versioned.inDraft ? besignerOpenDraftHref(besigner) : besigner\n }\n }\n return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`\n}\n\n/**\n * The in-app notification for one change of one job (AGL-3593), for the\n * person who created it. Customer-safe throughout: a stopped job's sentence is\n * the job's own `error`, which the machine writes as a fixed sentence.\n */\n/** The site a job worked on, as its notice names it: its name, and where it is live. */\nexport interface AiJobNoticeSite {\n name: string\n /** The site's live address, absolute; `null` when it has none yet. */\n url: string | null\n}\n\nexport function aiJobNotice(\n job: AiJobNoticeSource,\n to: AiJobNoticeTransition,\n site: AiJobNoticeSite | null = null,\n): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, to)\n if (to === 'needs-review') {\n // A plan that needs an ID only the person has first (AGL-3978).\n if (job.input?.message) {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : noun} needs one detail from you`,\n body: `${job.input.message} Nothing is built until you answer it and confirm the plan. It waits in AI jobs.`,\n link,\n ...scope,\n }\n }\n // A site import's wave is in, or its site is read (AGL-3976): a decision\n // waiting, said as one, never as a job that stopped.\n if (job.review?.reason === 'wave') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'info',\n title: 'Your site import is ready for you',\n body: job.review.message || 'Review the pages brought in, then start the next wave when you are ready.',\n link,\n ...scope,\n }\n }\n if (job.review?.reason === 'plan') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title:\n noun === 'AI job'\n ? 'Your plan is ready: confirm it to build'\n : `Your ${noun} plan is ready: confirm it to build`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs.',\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} needs you`,\n // The doctrine's own words never reach a customer (AGL-3596).\n body: withRefund(\n job,\n aiCustomerSafeCopy(job.review?.message || job.error || 'It stopped for your decision. It waits in AI jobs.', { page: job.kind === 'page' }),\n 'needs_review',\n ),\n link,\n ...scope,\n }\n }\n if (to === 'paused') {\n // Paused by the meter (AGL-3660): out of credits or at a cap. Nothing is\n // lost and the job is not over, so it is a warning that names the way on.\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: aiJobPausedTitle(job.kind),\n body: [job.error, AI_JOB_PAUSED_NEXT_COPY].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n if (to === 'done') {\n // A job that finished with part of it unbuilt (AGL-3596) says so in the\n // words its page uses, as a warning: \"done\" alone hid a page that failed.\n const items = job.items ?? []\n const unbuilt = items.some((row) => row.status === 'failed' || row.status === 'skipped')\n const level = unbuilt ? 'warning' : 'success'\n const partial =\n job.kind === 'site'\n ? aiSitePartialCopy({ items, status: 'done' })\n : unbuilt\n ? aiBuildOutcomeLine({ items, status: 'done' })\n : null\n // A guided site start publishes its pages when it finishes (AGL-3596):\n // with one live, the site is, and the build page lists any that stayed\n // drafts with the reason for each.\n const published = job.kind === 'site' ? (job.sitePublish?.published.length ?? 0) : 0\n if (published > 0) {\n const drafts = job.sitePublish?.drafts.length ?? 0\n const stayed = drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`\n // Named, with its address (AGL-3889): someone with several sites, or\n // reading the email days later, cannot tell which site went live from\n // \"Your site is live\", nor where to see it.\n const live = site ? `${site.name} is live` : 'Your site is live'\n const at = site?.url ? ` at ${site.url}` : ''\n return {\n type: 'content.aiJobDone',\n level,\n title: partial ? `${live}, but part of it wasn’t built` : live,\n body: partial\n ? `${partial} ${drafts > 0 ? `What was built is published${at}, except ${stayed}.` : `What was built is published${at}.`}`\n : drafts > 0\n ? `Your pages are published${at}, except ${stayed}. Open the build page to see why.`\n : at\n ? `Your pages are published${at}. Visit your site there, or open it in the console to edit your pages.`\n : 'Your pages are published. Open it to view your site or edit your pages.',\n link,\n ...scope,\n }\n }\n const pages = (job.outputs ?? []).filter((output) => output.resource === 'screen').length\n const title =\n job.kind === 'site' || pages > 1\n ? 'Your site’s draft pages are ready'\n : job.kind === 'page'\n ? 'Your draft page is ready'\n : noun === 'AI job'\n ? 'Your AI job finished'\n : `Your ${noun} is ready`\n return {\n type: 'content.aiJobDone',\n level,\n title,\n body:\n job.kind === 'insight' || job.kind === 'text'\n ? 'Open AI jobs to read it.'\n : [partial, 'Everything it built is an unpublished draft until you publish it.'].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobFailed',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} stopped`,\n body: withRefund(job, aiCustomerSafeCopy(job.error || 'It stopped before it finished.', { page: job.kind === 'page' }), 'failed'),\n link,\n ...scope,\n }\n}\n\n/**\n * The one reminder a plan that waits for its confirmation gets (AGL-4076),\n * a day after it was ready. The same kind of notice as the first one\n * (`content.aiJobNeedsYou`), so the same switch in the person's notification\n * settings turns both off, and it opens the same place: AI jobs on the job, or\n * a site start's build page. It says it is the only one, because it is.\n */\nexport function aiJobPlanReminderNotice(job: AiJobNoticeSource, site: AiJobNoticeSite | null = null): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, 'needs-review')\n const on = site ? ` for ${site.name}` : ''\n if (job.input?.message) {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun}${on} still needs one detail from you`,\n body: `${job.input.message} Nothing is built until you answer it and confirm the plan. It waits in AI jobs. This is the only reminder.`,\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: noun === 'AI job' ? `Your plan${on} is still waiting for you` : `Your ${noun} plan${on} is still waiting for you`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs. This is the only reminder.',\n link,\n ...scope,\n }\n}\n"],"names":["besignerOpenDraftHref","aiBuildOutcomeLine","aiSitePartialCopy","aiJobKindNoun","aiCustomerSafeCopy","aiJobRefundCopy","AI_JOB_BESIGNER_SEGMENT","screen","reusableComponent","layout","template","emailScreen","AI_JOB_LINK_PARAM","AI_SITE_BUILD_HREF","AI_JOB_PAUSED_NEXT_COPY","aiJobPausedTitle","kind","noun","endsWith","withRefund","job","text","status","refund","refundedCredits","creditsSpent","aiJobNoticeLink","to","base","hostId","encodeURIComponent","$id","own","outputs","filter","output","screens","resource","length","versioned","find","versionId","segment","besigner","id","inDraft","aiJobNotice","site","scope","orgId","link","input","message","type","level","title","body","review","reason","error","page","Boolean","join","items","unbuilt","some","row","partial","published","sitePublish","drafts","stayed","live","name","at","url","pages","aiJobPlanReminderNotice","on"],"mappings":"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"}
|
|
@@ -254,6 +254,12 @@ export interface AiJobOutput {
|
|
|
254
254
|
* its writer is registered on the resource-draft seam.
|
|
255
255
|
*/
|
|
256
256
|
draftResource?: string;
|
|
257
|
+
/**
|
|
258
|
+
* The change is in the WORKING DRAFT of `versionId` (AGL-4116), which the
|
|
259
|
+
* Besigner offers with Open draft, not in the version itself — so the
|
|
260
|
+
* version's preview does not show it, and only the Besigner is linked.
|
|
261
|
+
*/
|
|
262
|
+
inDraft?: true;
|
|
257
263
|
}
|
|
258
264
|
export interface AiJobLease {
|
|
259
265
|
/** The process that holds the step: a route request id or a beat id. */
|