@aglyn/plugins-ai 1.0.0-beta.244 → 1.0.0-beta.247
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/activity/ai-activity-actions.js +7 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-job-links.js +21 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +3 -1
- package/src/lib/components/ai-product-zones.js.map +1 -1
- package/src/lib/components/ai-products-hub-card.component.js +1 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.js +17 -9
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +98 -23
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/declarations.server.js +12 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +216 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +3 -1
- package/src/lib/jobs/ai-job-build-step.js +124 -18
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
- package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
- package/src/lib/jobs/ai-job-form-dataset.d.ts +8 -5
- package/src/lib/jobs/ai-job-form-dataset.js +28 -10
- package/src/lib/jobs/ai-job-form-dataset.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.d.ts +20 -4
- package/src/lib/jobs/ai-job-form-step.js +98 -28
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +6 -2
- package/src/lib/jobs/ai-job-layout-language.js +29 -5
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +4 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-media-step.d.ts +239 -0
- package/src/lib/jobs/ai-job-media-step.js +610 -0
- package/src/lib/jobs/ai-job-media-step.js.map +1 -0
- package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
- package/src/lib/jobs/ai-job-navigation-step.js +439 -0
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +4 -1
- package/src/lib/jobs/ai-job-page-language.js +30 -5
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +5 -4
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +3 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +14 -3
- package/src/lib/jobs/ai-job-plan-step.js +87 -20
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
- package/src/lib/jobs/ai-job-products-step.js +2 -2
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +268 -0
- package/src/lib/jobs/ai-job-settings-step.js +690 -0
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
- package/src/lib/jobs/ai-job-site-content.d.ts +22 -8
- package/src/lib/jobs/ai-job-site-content.js +33 -15
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +3 -1
- package/src/lib/jobs/ai-job-site-datasets.js +13 -5
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-events.d.ts +148 -0
- package/src/lib/jobs/ai-job-site-events.js +313 -0
- package/src/lib/jobs/ai-job-site-events.js.map +1 -0
- package/src/lib/jobs/ai-job-site-step.d.ts +12 -4
- package/src/lib/jobs/ai-job-site-step.js +132 -26
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +184 -0
- package/src/lib/jobs/ai-job-tracking-step.js +501 -0
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -0
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +21 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +41 -4
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +62 -4
- package/src/lib/jobs/ai-site-publish.js +202 -56
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/ai-stock-photo-picker.d.ts +24 -0
- package/src/lib/jobs/ai-stock-photo-picker.js +108 -0
- package/src/lib/jobs/ai-stock-photo-picker.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.d.ts +37 -0
- package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.js +47 -0
- package/src/lib/jobs/fixtures/ai-jazz-events-build-plan-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.d.ts +21 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.js +128 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3760-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +16 -7
- package/src/lib/layout-language/ai-layout-compiler.js +62 -42
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
- package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +24 -5
- package/src/lib/layout-language/ai-layout-frame.js +93 -20
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +3 -1
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-labels.d.ts +70 -0
- package/src/lib/layout-language/ai-layout-labels.js +162 -0
- package/src/lib/layout-language/ai-layout-labels.js.map +1 -0
- package/src/lib/layout-language/ai-layout-links.js +5 -2
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +49 -1
- package/src/lib/layout-language/ai-layout-listings.js +66 -5
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-tree.js +17 -0
- package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
- package/src/lib/model/ai-build-event-page.d.ts +70 -0
- package/src/lib/model/ai-build-event-page.js +145 -0
- package/src/lib/model/ai-build-event-page.js.map +1 -0
- package/src/lib/model/ai-build-job.d.ts +44 -0
- package/src/lib/model/ai-build-job.js +141 -13
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +39 -3
- package/src/lib/model/ai-build-plan.js +59 -5
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +52 -0
- package/src/lib/model/ai-credit-estimate.js +51 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.js +6 -1
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +18 -2
- package/src/lib/model/ai-jobs.types.js +6 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-layout-menus.d.ts +104 -0
- package/src/lib/model/ai-layout-menus.js +518 -0
- package/src/lib/model/ai-layout-menus.js.map +1 -0
- package/src/lib/model/ai-plan-capabilities.d.ts +8 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-products.d.ts +31 -1
- package/src/lib/model/ai-products.js +34 -1
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +17 -0
- package/src/lib/model/ai-site-inventory.js +5 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +12 -2
- package/src/lib/model/ai-site-job.js +39 -12
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.js +1 -1
- package/src/lib/model/ai-site-store-pages.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +78 -3
- package/src/lib/runtime/ai-doctrine-validators.js +219 -31
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.d.ts +6 -1
- package/src/lib/runtime/ai-doctrine.js +16 -7
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.d.ts +1 -1
- package/src/lib/runtime/ai-eval-live.js +1 -1
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +3 -1
- package/src/lib/runtime/ai-palette.generated.js +499 -11
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +5 -3
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +4 -2
- package/src/lib/runtime/ai-products-generation.js +12 -6
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/ai-template-examples.js +8 -2
- package/src/lib/runtime/ai-template-examples.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +12 -0
- package/src/lib/runtime/site-inventory.js +29 -1
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server.js +20 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.js +37 -3
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
- package/src/lib/tools/ai-products-tool.d.ts +3 -1
- package/src/lib/tools/ai-products-tool.js +15 -5
- package/src/lib/tools/ai-products-tool.js.map +1 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { DUPLICABLE_RESOURCE_NOUNS } from "@aglyn/aglyn/app-utils/duplicate-resource";
|
|
18
|
+
import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
|
|
19
|
+
import { duplicateResource, recipeFor } from "@aglyn/tenant-data-admin/server/duplicate-resource";
|
|
20
|
+
import { getLockdownVerdict } from "@aglyn/tenant-data-admin/server/lockdown";
|
|
21
|
+
import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
|
|
22
|
+
import { isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
23
|
+
import { parseAiDuplicateResourceInput } from "../tools/ai-duplicate-resource-tool.js";
|
|
24
|
+
import { registerAiJobAdmission } from "./ai-job-admission.js";
|
|
25
|
+
import { aiJobEditModel } from "./ai-job-edit-step.js";
|
|
26
|
+
import { aiLimitReview, aiUnspentOutcome } from "./ai-job-generation.js";
|
|
27
|
+
import { registerAiJobStep } from "./ai-jobs.js";
|
|
28
|
+
/**
|
|
29
|
+
* The `duplicate` step (AGL-3740): copying something the site already has —
|
|
30
|
+
* a page, a component, a layout, a template, a form, an email design or a
|
|
31
|
+
* workflow — as a first-class build operation, so "make another page like
|
|
32
|
+
* Services" copies instead of regenerating.
|
|
33
|
+
*
|
|
34
|
+
* NO MODEL. The copy is core's `duplicateResource`, the very function the
|
|
35
|
+
* console's Duplicate… row menus and `/api/hosts/resources` (`action:
|
|
36
|
+
* 'duplicate'`) call, and the one the `duplicate_resource` tool wraps: the
|
|
37
|
+
* same fields carried, the same uniqueness rules, the same plan counters met
|
|
38
|
+
* inside the transaction, the same `*.duplicated` activity row. So it costs
|
|
39
|
+
* no credits, and it can copy nothing the console could not.
|
|
40
|
+
*
|
|
41
|
+
* DRAFTS ONLY, by construction of the copy: a page copy has no address, a
|
|
42
|
+
* workflow copy no trigger, and nothing places a component, layout or form
|
|
43
|
+
* copy until a person does.
|
|
44
|
+
*
|
|
45
|
+
* ── Who may ──────────────────────────────────────────────────────────────
|
|
46
|
+
*
|
|
47
|
+
* The console door's rule: a member with a write role on the site (admin,
|
|
48
|
+
* editor or author), while no lockdown freezes the site, its org or the
|
|
49
|
+
* platform. `ai.generate` is the jobs door's own check. Asked at the door
|
|
50
|
+
* and again before the copy.
|
|
51
|
+
*
|
|
52
|
+
* ── Once per job ─────────────────────────────────────────────────────────
|
|
53
|
+
*
|
|
54
|
+
* The job's id is the copy's attempt key, so a pass asked again answers with
|
|
55
|
+
* the copy the first made rather than making a second.
|
|
56
|
+
*/ export const AI_DUPLICATE_KIND = 'duplicate';
|
|
57
|
+
/**
|
|
58
|
+
* ASSUMED: the least time the step needs — the site, the org, the source's
|
|
59
|
+
* collection when it is named by name, and the one transaction that copies.
|
|
60
|
+
* No model, so it fits an inline door.
|
|
61
|
+
*/ export const AI_JOB_DUPLICATE_STEP_MINIMUM_MS = 5000;
|
|
62
|
+
export const AI_DUPLICATE_NO_SITE_COPY = 'Open the site whose content should be copied.';
|
|
63
|
+
export const AI_DUPLICATE_ROLE_COPY = 'Copying on this site needs the admin, editor or author role on it.';
|
|
64
|
+
export const AI_DUPLICATE_LOCKED_COPY = 'This site is not accepting changes right now.';
|
|
65
|
+
export const AI_DUPLICATE_UNREADABLE_COPY = 'The request did not name something on this site to copy.';
|
|
66
|
+
export const AI_DUPLICATE_NOT_YET_COPY = 'Only something the site already has can be copied, not something this build is making.';
|
|
67
|
+
/** The output resource each duplicable kind is opened as. */ export const AI_DUPLICATE_OUTPUT_RESOURCES = {
|
|
68
|
+
screen: 'screen',
|
|
69
|
+
component: 'reusableComponent',
|
|
70
|
+
layout: 'layout',
|
|
71
|
+
template: 'template',
|
|
72
|
+
form: 'form',
|
|
73
|
+
emailDesign: 'emailScreen',
|
|
74
|
+
workflow: 'workflow'
|
|
75
|
+
};
|
|
76
|
+
/** The copy's attempt key: one copy per job, however often it is asked. */ export function aiDuplicateAttemptKey(job) {
|
|
77
|
+
return `ai-job:${job.$id}`.slice(0, 200);
|
|
78
|
+
}
|
|
79
|
+
/** What the plan's item asks, read; a refusal in words when it is unreadable. */ export function aiDuplicateRequestOf(inputs) {
|
|
80
|
+
var _parseAiDuplicateResourceInput;
|
|
81
|
+
const source = typeof (inputs == null ? void 0 : inputs['source']) === 'string' ? inputs['source'].trim() : '';
|
|
82
|
+
if (isAiPlanNewRef(source)) return AI_DUPLICATE_NOT_YET_COPY;
|
|
83
|
+
const name = typeof (inputs == null ? void 0 : inputs['name']) === 'string' ? inputs['name'] : null;
|
|
84
|
+
return (_parseAiDuplicateResourceInput = parseAiDuplicateResourceInput({
|
|
85
|
+
kind: inputs == null ? void 0 : inputs['kind'],
|
|
86
|
+
sourceId: source,
|
|
87
|
+
name
|
|
88
|
+
})) != null ? _parseAiDuplicateResourceInput : AI_DUPLICATE_UNREADABLE_COPY;
|
|
89
|
+
}
|
|
90
|
+
/** The rows of a kind's collection on a site. */ export async function readAiDuplicateSourceRows(firestore, hostId, kind) {
|
|
91
|
+
const recipe = recipeFor(kind);
|
|
92
|
+
if (!recipe) return [];
|
|
93
|
+
const snapshot = await firestore.collection('hosts').doc(hostId).collection(recipe.collection).select(recipe.nameField, 'kind', 'deletedAt').get();
|
|
94
|
+
return snapshot.docs.map((doc)=>{
|
|
95
|
+
var _doc_get;
|
|
96
|
+
return {
|
|
97
|
+
id: doc.id,
|
|
98
|
+
name: String((_doc_get = doc.get(recipe.nameField)) != null ? _doc_get : ''),
|
|
99
|
+
kind: doc.get('kind'),
|
|
100
|
+
deleted: doc.get('deletedAt') != null
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Whether a row of a shared collection is of this kind: pages and email designs share `screens`. */ function rowIsKind(kind, row) {
|
|
105
|
+
if (kind === 'emailDesign') return row.kind === 'email';
|
|
106
|
+
if (kind === 'screen') return row.kind !== 'email';
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The source's id: the row whose id it names, else the one row whose name it
|
|
111
|
+
* is (case aside) — so "copy the Welcome email" works where the inventory
|
|
112
|
+
* lists no email designs. Two rows of one name, or none, are refused in words.
|
|
113
|
+
*/ export function aiDuplicateSourceId(kind, source, rows) {
|
|
114
|
+
const noun = DUPLICABLE_RESOURCE_NOUNS[kind];
|
|
115
|
+
const live = rows.filter((row)=>!row.deleted && rowIsKind(kind, row));
|
|
116
|
+
if (live.some((row)=>row.id === source)) return {
|
|
117
|
+
id: source
|
|
118
|
+
};
|
|
119
|
+
const wanted = source.trim().toLowerCase();
|
|
120
|
+
const named = live.filter((row)=>row.name.trim().toLowerCase() === wanted);
|
|
121
|
+
if (named.length === 1) return {
|
|
122
|
+
id: named[0].id
|
|
123
|
+
};
|
|
124
|
+
if (named.length > 1) return {
|
|
125
|
+
error: `This site has more than one ${noun} called “${source}”; name the one to copy.`
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
error: `This site has no ${noun} “${source}” to copy.`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Why this member may not copy on this site now, or `null`. */ export async function aiDuplicateAccessRefusal(input, deps = {}) {
|
|
132
|
+
var _input_host_get, _deps_lockdown;
|
|
133
|
+
if (!input.host.exists) return 'Unknown site';
|
|
134
|
+
const roles = (_input_host_get = input.host.get('memberRoles')) != null ? _input_host_get : {};
|
|
135
|
+
if (!hostRoleCanWrite(roles[input.uid])) return AI_DUPLICATE_ROLE_COPY;
|
|
136
|
+
const locked = await ((_deps_lockdown = deps.lockdown) != null ? _deps_lockdown : getLockdownVerdict)({
|
|
137
|
+
staff: false,
|
|
138
|
+
uid: input.uid,
|
|
139
|
+
org: input.org,
|
|
140
|
+
host: input.host.data(),
|
|
141
|
+
intent: 'write'
|
|
142
|
+
});
|
|
143
|
+
return locked ? AI_DUPLICATE_LOCKED_COPY : null;
|
|
144
|
+
}
|
|
145
|
+
/** The copy's output note: what it is and what it waits for. */ export function aiDuplicateOutputNote(kind) {
|
|
146
|
+
const noun = DUPLICABLE_RESOURCE_NOUNS[kind];
|
|
147
|
+
return `A copy of the ${noun}, saved as a draft: nothing on the live site uses it until you do.`;
|
|
148
|
+
}
|
|
149
|
+
/** The request read and its source found, or a refusal in words. */ async function duplicatePlanOf(firestore, job, readRows) {
|
|
150
|
+
if (!job.hostId) return AI_DUPLICATE_NO_SITE_COPY;
|
|
151
|
+
const request = aiDuplicateRequestOf(job.inputs);
|
|
152
|
+
if (typeof request === 'string') return request;
|
|
153
|
+
const found = aiDuplicateSourceId(request.kind, request.sourceId, await readRows(firestore, job.hostId, request.kind));
|
|
154
|
+
if ('error' in found) return found.error;
|
|
155
|
+
return _extends({}, request, {
|
|
156
|
+
id: found.id
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export function createAiJobDuplicateStep(deps = {}) {
|
|
160
|
+
var _deps_duplicate, _deps_readRows;
|
|
161
|
+
const duplicate = (_deps_duplicate = deps.duplicate) != null ? _deps_duplicate : duplicateResource;
|
|
162
|
+
const readRows = (_deps_readRows = deps.readRows) != null ? _deps_readRows : readAiDuplicateSourceRows;
|
|
163
|
+
return async ({ job, firestore, org, modelFor })=>{
|
|
164
|
+
var _ref, _data;
|
|
165
|
+
// No model is asked: the outcome records the edit step's at no cost.
|
|
166
|
+
const model = (_ref = modelFor == null ? void 0 : modelFor('job.edit')) != null ? _ref : aiJobEditModel();
|
|
167
|
+
const unspent = (failure)=>_extends({}, aiUnspentOutcome(model), {
|
|
168
|
+
failure
|
|
169
|
+
});
|
|
170
|
+
const hostId = job.hostId;
|
|
171
|
+
if (!hostId) return unspent(AI_DUPLICATE_NO_SITE_COPY);
|
|
172
|
+
const plan = await duplicatePlanOf(firestore, job, readRows);
|
|
173
|
+
if (typeof plan === 'string') return unspent(plan);
|
|
174
|
+
const orgData = org != null ? org : (_data = (await firestore.collection('orgs').doc(job.orgId).get()).data()) != null ? _data : null;
|
|
175
|
+
const host = await firestore.collection('hosts').doc(hostId).get();
|
|
176
|
+
const refused = await aiDuplicateAccessRefusal({
|
|
177
|
+
host,
|
|
178
|
+
uid: job.createdBy,
|
|
179
|
+
org: orgData
|
|
180
|
+
}, {
|
|
181
|
+
lockdown: deps.lockdown
|
|
182
|
+
});
|
|
183
|
+
if (refused) return aiUnspentOutcome(model, {
|
|
184
|
+
review: aiLimitReview(refused)
|
|
185
|
+
});
|
|
186
|
+
const result = await duplicate(plan.kind, {
|
|
187
|
+
orgId: job.orgId,
|
|
188
|
+
hostId,
|
|
189
|
+
sourceId: plan.id,
|
|
190
|
+
name: plan.name,
|
|
191
|
+
uid: job.createdBy,
|
|
192
|
+
email: null,
|
|
193
|
+
org: orgData,
|
|
194
|
+
attemptKey: aiDuplicateAttemptKey(job)
|
|
195
|
+
});
|
|
196
|
+
if (result.ok === false) {
|
|
197
|
+
// A plan counter or a feature the plan lacks is a limit to relay, not a fault.
|
|
198
|
+
return result.status === 403 ? aiUnspentOutcome(model, {
|
|
199
|
+
review: aiLimitReview(result.error)
|
|
200
|
+
}) : unspent(result.error);
|
|
201
|
+
}
|
|
202
|
+
const output = {
|
|
203
|
+
resource: AI_DUPLICATE_OUTPUT_RESOURCES[plan.kind],
|
|
204
|
+
id: result.id,
|
|
205
|
+
versionId: result.versionId,
|
|
206
|
+
hostId,
|
|
207
|
+
hostSubdomain: typeof host.get('subdomain') === 'string' && host.get('subdomain') || null,
|
|
208
|
+
label: result.name,
|
|
209
|
+
note: aiDuplicateOutputNote(plan.kind)
|
|
210
|
+
};
|
|
211
|
+
return aiUnspentOutcome(model, {
|
|
212
|
+
outputs: [
|
|
213
|
+
output
|
|
214
|
+
]
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export const runAiJobDuplicateStep = createAiJobDuplicateStep();
|
|
219
|
+
/**
|
|
220
|
+
* A copy is admitted on a site of the job's own org, for a member who may
|
|
221
|
+
* write its content now, of a source the site holds. Refused in those words
|
|
222
|
+
* before anything exists; what the plan's counters allow is the copy's own
|
|
223
|
+
* transaction to say.
|
|
224
|
+
*/ export function createAiDuplicateJobAdmission(deps = {}) {
|
|
225
|
+
var _deps_ownerOf, _deps_readRows;
|
|
226
|
+
const ownerOf = (_deps_ownerOf = deps.ownerOf) != null ? _deps_ownerOf : resolveOrgIdForHost;
|
|
227
|
+
const readRows = (_deps_readRows = deps.readRows) != null ? _deps_readRows : readAiDuplicateSourceRows;
|
|
228
|
+
return async (context)=>{
|
|
229
|
+
var _context_uid;
|
|
230
|
+
if (!context.hostId) return {
|
|
231
|
+
status: 400,
|
|
232
|
+
error: AI_DUPLICATE_NO_SITE_COPY
|
|
233
|
+
};
|
|
234
|
+
const owner = await ownerOf(context.hostId);
|
|
235
|
+
if (!owner || owner !== context.orgId) return {
|
|
236
|
+
status: 404,
|
|
237
|
+
error: 'Unknown site'
|
|
238
|
+
};
|
|
239
|
+
const host = await context.firestore.collection('hosts').doc(context.hostId).get();
|
|
240
|
+
const refused = await aiDuplicateAccessRefusal({
|
|
241
|
+
host,
|
|
242
|
+
uid: (_context_uid = context.uid) != null ? _context_uid : '',
|
|
243
|
+
org: context.org
|
|
244
|
+
}, {
|
|
245
|
+
lockdown: deps.lockdown
|
|
246
|
+
});
|
|
247
|
+
if (refused) return {
|
|
248
|
+
status: 403,
|
|
249
|
+
error: refused
|
|
250
|
+
};
|
|
251
|
+
const plan = await duplicatePlanOf(context.firestore, {
|
|
252
|
+
hostId: context.hostId,
|
|
253
|
+
inputs: context.inputs
|
|
254
|
+
}, readRows);
|
|
255
|
+
return typeof plan === 'string' ? {
|
|
256
|
+
status: 400,
|
|
257
|
+
error: plan
|
|
258
|
+
} : null;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
export const aiDuplicateJobAdmission = createAiDuplicateJobAdmission();
|
|
262
|
+
/** Registers the duplicate step and its door (AGL-3740). */ export function registerAiDuplicateJob() {
|
|
263
|
+
registerAiJobStep(AI_DUPLICATE_KIND, runAiJobDuplicateStep, {
|
|
264
|
+
minimumMs: AI_JOB_DUPLICATE_STEP_MINIMUM_MS
|
|
265
|
+
});
|
|
266
|
+
registerAiJobAdmission(AI_DUPLICATE_KIND, aiDuplicateJobAdmission);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
//# sourceMappingURL=ai-job-duplicate-step.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-duplicate-step.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 DUPLICABLE_RESOURCE_NOUNS,\n type DuplicableHostResourceKind,\n} from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { duplicateResource, recipeFor } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type { Firestore } from 'firebase-admin/firestore'\nimport { isAiPlanNewRef } from '../model/ai-build-plan'\nimport type { AiJob, AiJobOutput, AiJobOutputResource } from '../model/ai-jobs.types'\nimport { parseAiDuplicateResourceInput, type AiDuplicateResourceInput } from '../tools/ai-duplicate-resource-tool'\nimport { registerAiJobAdmission, type AiJobAdmission, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { aiJobEditModel } from './ai-job-edit-step'\nimport { aiLimitReview, aiUnspentOutcome } from './ai-job-generation'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The `duplicate` step (AGL-3740): copying something the site already has —\n * a page, a component, a layout, a template, a form, an email design or a\n * workflow — as a first-class build operation, so \"make another page like\n * Services\" copies instead of regenerating.\n *\n * NO MODEL. The copy is core's `duplicateResource`, the very function the\n * console's Duplicate… row menus and `/api/hosts/resources` (`action:\n * 'duplicate'`) call, and the one the `duplicate_resource` tool wraps: the\n * same fields carried, the same uniqueness rules, the same plan counters met\n * inside the transaction, the same `*.duplicated` activity row. So it costs\n * no credits, and it can copy nothing the console could not.\n *\n * DRAFTS ONLY, by construction of the copy: a page copy has no address, a\n * workflow copy no trigger, and nothing places a component, layout or form\n * copy until a person does.\n *\n * ── Who may ──────────────────────────────────────────────────────────────\n *\n * The console door's rule: a member with a write role on the site (admin,\n * editor or author), while no lockdown freezes the site, its org or the\n * platform. `ai.generate` is the jobs door's own check. Asked at the door\n * and again before the copy.\n *\n * ── Once per job ─────────────────────────────────────────────────────────\n *\n * The job's id is the copy's attempt key, so a pass asked again answers with\n * the copy the first made rather than making a second.\n */\n\nexport const AI_DUPLICATE_KIND = 'duplicate' as const\n\n/**\n * ASSUMED: the least time the step needs — the site, the org, the source's\n * collection when it is named by name, and the one transaction that copies.\n * No model, so it fits an inline door.\n */\nexport const AI_JOB_DUPLICATE_STEP_MINIMUM_MS = 5_000\n\nexport const AI_DUPLICATE_NO_SITE_COPY = 'Open the site whose content should be copied.'\nexport const AI_DUPLICATE_ROLE_COPY = 'Copying on this site needs the admin, editor or author role on it.'\nexport const AI_DUPLICATE_LOCKED_COPY = 'This site is not accepting changes right now.'\nexport const AI_DUPLICATE_UNREADABLE_COPY = 'The request did not name something on this site to copy.'\nexport const AI_DUPLICATE_NOT_YET_COPY = 'Only something the site already has can be copied, not something this build is making.'\n\n/** The output resource each duplicable kind is opened as. */\nexport const AI_DUPLICATE_OUTPUT_RESOURCES: Readonly<Record<DuplicableHostResourceKind, AiJobOutputResource>> = {\n screen: 'screen',\n component: 'reusableComponent',\n layout: 'layout',\n template: 'template',\n form: 'form',\n emailDesign: 'emailScreen',\n workflow: 'workflow',\n}\n\n/** The copy's attempt key: one copy per job, however often it is asked. */\nexport function aiDuplicateAttemptKey(job: Pick<AiJob, '$id'>): string {\n return `ai-job:${job.$id}`.slice(0, 200)\n}\n\n/** What the plan's item asks, read; a refusal in words when it is unreadable. */\nexport function aiDuplicateRequestOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiDuplicateResourceInput | string {\n const source = typeof inputs?.['source'] === 'string' ? inputs['source'].trim() : ''\n if (isAiPlanNewRef(source)) return AI_DUPLICATE_NOT_YET_COPY\n const name = typeof inputs?.['name'] === 'string' ? inputs['name'] : null\n return parseAiDuplicateResourceInput({ kind: inputs?.['kind'], sourceId: source, name }) ?? AI_DUPLICATE_UNREADABLE_COPY\n}\n\n/** One row of a kind's collection, as a source is looked up by. */\nexport interface AiDuplicateSourceRow {\n id: string\n name: string\n kind: unknown\n deleted: boolean\n}\n\n/** The rows of a kind's collection on a site. */\nexport async function readAiDuplicateSourceRows(\n firestore: Firestore,\n hostId: string,\n kind: DuplicableHostResourceKind,\n): Promise<AiDuplicateSourceRow[]> {\n const recipe = recipeFor(kind)\n if (!recipe) return []\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection(recipe.collection)\n .select(recipe.nameField, 'kind', 'deletedAt')\n .get()\n return snapshot.docs.map((doc) => ({\n id: doc.id,\n name: String(doc.get(recipe.nameField) ?? ''),\n kind: doc.get('kind'),\n deleted: doc.get('deletedAt') != null,\n }))\n}\n\n/** Whether a row of a shared collection is of this kind: pages and email designs share `screens`. */\nfunction rowIsKind(kind: DuplicableHostResourceKind, row: AiDuplicateSourceRow): boolean {\n if (kind === 'emailDesign') return row.kind === 'email'\n if (kind === 'screen') return row.kind !== 'email'\n return true\n}\n\n/**\n * The source's id: the row whose id it names, else the one row whose name it\n * is (case aside) — so \"copy the Welcome email\" works where the inventory\n * lists no email designs. Two rows of one name, or none, are refused in words.\n */\nexport function aiDuplicateSourceId(\n kind: DuplicableHostResourceKind,\n source: string,\n rows: readonly AiDuplicateSourceRow[],\n): { id: string } | { error: string } {\n const noun = DUPLICABLE_RESOURCE_NOUNS[kind]\n const live = rows.filter((row) => !row.deleted && rowIsKind(kind, row))\n if (live.some((row) => row.id === source)) return { id: source }\n const wanted = source.trim().toLowerCase()\n const named = live.filter((row) => row.name.trim().toLowerCase() === wanted)\n if (named.length === 1) return { id: named[0].id }\n if (named.length > 1) return { error: `This site has more than one ${noun} called “${source}”; name the one to copy.` }\n return { error: `This site has no ${noun} “${source}” to copy.` }\n}\n\n/** Why this member may not copy on this site now, or `null`. */\nexport async function aiDuplicateAccessRefusal(\n input: { host: FirebaseFirestore.DocumentSnapshot; uid: string; org: object | null },\n deps: { lockdown?: typeof getLockdownVerdict } = {},\n): Promise<string | null> {\n if (!input.host.exists) return 'Unknown site'\n const roles = (input.host.get('memberRoles') ?? {}) as Record<string, unknown>\n if (!hostRoleCanWrite(roles[input.uid])) return AI_DUPLICATE_ROLE_COPY\n const locked = await (deps.lockdown ?? getLockdownVerdict)({\n staff: false,\n uid: input.uid,\n org: input.org as never,\n host: input.host.data() as never,\n intent: 'write',\n })\n return locked ? AI_DUPLICATE_LOCKED_COPY : null\n}\n\n/** The copy's output note: what it is and what it waits for. */\nexport function aiDuplicateOutputNote(kind: DuplicableHostResourceKind): string {\n const noun = DUPLICABLE_RESOURCE_NOUNS[kind]\n return `A copy of the ${noun}, saved as a draft: nothing on the live site uses it until you do.`\n}\n\nexport interface AiJobDuplicateStepDeps {\n duplicate?: typeof duplicateResource\n readRows?: typeof readAiDuplicateSourceRows\n lockdown?: typeof getLockdownVerdict\n}\n\n/** The request read and its source found, or a refusal in words. */\nasync function duplicatePlanOf(\n firestore: Firestore,\n job: Pick<AiJob, 'hostId' | 'inputs'>,\n readRows: typeof readAiDuplicateSourceRows,\n): Promise<(AiDuplicateResourceInput & { id: string }) | string> {\n if (!job.hostId) return AI_DUPLICATE_NO_SITE_COPY\n const request = aiDuplicateRequestOf(job.inputs)\n if (typeof request === 'string') return request\n const found = aiDuplicateSourceId(request.kind, request.sourceId, await readRows(firestore, job.hostId, request.kind))\n if ('error' in found) return found.error\n return { ...request, id: found.id }\n}\n\nexport function createAiJobDuplicateStep(deps: AiJobDuplicateStepDeps = {}): AiJobStepRunner {\n const duplicate = deps.duplicate ?? duplicateResource\n const readRows = deps.readRows ?? readAiDuplicateSourceRows\n return async ({ job, firestore, org, modelFor }): Promise<AiJobStepOutcome> => {\n // No model is asked: the outcome records the edit step's at no cost.\n const model = modelFor?.('job.edit') ?? aiJobEditModel()\n const unspent = (failure: string): AiJobStepOutcome => ({ ...aiUnspentOutcome(model), failure })\n const hostId = job.hostId\n if (!hostId) return unspent(AI_DUPLICATE_NO_SITE_COPY)\n const plan = await duplicatePlanOf(firestore, job, readRows)\n if (typeof plan === 'string') return unspent(plan)\n const orgData = (org ?? ((await firestore.collection('orgs').doc(job.orgId).get()).data() ?? null)) as Record<string, unknown> | null\n const host = await firestore.collection('hosts').doc(hostId).get()\n const refused = await aiDuplicateAccessRefusal({ host, uid: job.createdBy, org: orgData }, { lockdown: deps.lockdown })\n if (refused) return aiUnspentOutcome(model, { review: aiLimitReview(refused) })\n const result = await duplicate(plan.kind, {\n orgId: job.orgId,\n hostId,\n sourceId: plan.id,\n name: plan.name,\n uid: job.createdBy,\n email: null,\n org: orgData,\n attemptKey: aiDuplicateAttemptKey(job),\n })\n if (result.ok === false) {\n // A plan counter or a feature the plan lacks is a limit to relay, not a fault.\n return result.status === 403 ? aiUnspentOutcome(model, { review: aiLimitReview(result.error) }) : unspent(result.error)\n }\n const output: AiJobOutput = {\n resource: AI_DUPLICATE_OUTPUT_RESOURCES[plan.kind],\n id: result.id,\n versionId: result.versionId,\n hostId,\n hostSubdomain: (typeof host.get('subdomain') === 'string' && host.get('subdomain')) || null,\n label: result.name,\n note: aiDuplicateOutputNote(plan.kind),\n }\n return aiUnspentOutcome(model, { outputs: [output] })\n }\n}\n\nexport const runAiJobDuplicateStep = createAiJobDuplicateStep()\n\n// ── Admission ─────────────────────────────────────────────────────────────\n\nexport interface AiDuplicateJobAdmissionDeps {\n ownerOf?: typeof resolveOrgIdForHost\n readRows?: typeof readAiDuplicateSourceRows\n lockdown?: typeof getLockdownVerdict\n}\n\n/**\n * A copy is admitted on a site of the job's own org, for a member who may\n * write its content now, of a source the site holds. Refused in those words\n * before anything exists; what the plan's counters allow is the copy's own\n * transaction to say.\n */\nexport function createAiDuplicateJobAdmission(deps: AiDuplicateJobAdmissionDeps = {}): AiJobAdmission {\n const ownerOf = deps.ownerOf ?? resolveOrgIdForHost\n const readRows = deps.readRows ?? readAiDuplicateSourceRows\n return async (context): Promise<AiJobAdmissionRefusal | null> => {\n if (!context.hostId) return { status: 400, error: AI_DUPLICATE_NO_SITE_COPY }\n const owner = await ownerOf(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n const host = await context.firestore.collection('hosts').doc(context.hostId).get()\n const refused = await aiDuplicateAccessRefusal({ host, uid: context.uid ?? '', org: context.org }, { lockdown: deps.lockdown })\n if (refused) return { status: 403, error: refused }\n const plan = await duplicatePlanOf(context.firestore as Firestore, { hostId: context.hostId, inputs: context.inputs as Record<string, unknown> }, readRows)\n return typeof plan === 'string' ? { status: 400, error: plan } : null\n }\n}\n\nexport const aiDuplicateJobAdmission = createAiDuplicateJobAdmission()\n\n/** Registers the duplicate step and its door (AGL-3740). */\nexport function registerAiDuplicateJob(): void {\n registerAiJobStep(AI_DUPLICATE_KIND, runAiJobDuplicateStep, { minimumMs: AI_JOB_DUPLICATE_STEP_MINIMUM_MS })\n registerAiJobAdmission(AI_DUPLICATE_KIND, aiDuplicateJobAdmission)\n}\n"],"names":["DUPLICABLE_RESOURCE_NOUNS","hostRoleCanWrite","duplicateResource","recipeFor","getLockdownVerdict","resolveOrgIdForHost","isAiPlanNewRef","parseAiDuplicateResourceInput","registerAiJobAdmission","aiJobEditModel","aiLimitReview","aiUnspentOutcome","registerAiJobStep","AI_DUPLICATE_KIND","AI_JOB_DUPLICATE_STEP_MINIMUM_MS","AI_DUPLICATE_NO_SITE_COPY","AI_DUPLICATE_ROLE_COPY","AI_DUPLICATE_LOCKED_COPY","AI_DUPLICATE_UNREADABLE_COPY","AI_DUPLICATE_NOT_YET_COPY","AI_DUPLICATE_OUTPUT_RESOURCES","screen","component","layout","template","form","emailDesign","workflow","aiDuplicateAttemptKey","job","$id","slice","aiDuplicateRequestOf","inputs","source","trim","name","kind","sourceId","readAiDuplicateSourceRows","firestore","hostId","recipe","snapshot","collection","doc","select","nameField","get","docs","map","id","String","deleted","rowIsKind","row","aiDuplicateSourceId","rows","noun","live","filter","some","wanted","toLowerCase","named","length","error","aiDuplicateAccessRefusal","input","deps","host","exists","roles","uid","locked","lockdown","staff","org","data","intent","aiDuplicateOutputNote","duplicatePlanOf","readRows","request","found","createAiJobDuplicateStep","duplicate","modelFor","model","unspent","failure","plan","orgData","orgId","refused","createdBy","review","result","email","attemptKey","ok","status","output","resource","versionId","hostSubdomain","label","note","outputs","runAiJobDuplicateStep","createAiDuplicateJobAdmission","ownerOf","context","owner","aiDuplicateJobAdmission","registerAiDuplicateJob","minimumMs"],"mappings":"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"}
|
|
@@ -28,7 +28,7 @@ import type { AiSiteInventory } from '../model/ai-site-inventory';
|
|
|
28
28
|
* first — a site start's dataset unit, a build's dataset item, or one the
|
|
29
29
|
* site already has — and the form step binds the form it generated the way a
|
|
30
30
|
* person binds one on the Form element: the form node's `datasetId`, and
|
|
31
|
-
* each
|
|
31
|
+
* each answer element's `datasetFieldId` where the dataset has a field of the
|
|
32
32
|
* same name and a type the submitted value can be stored as. A field the
|
|
33
33
|
* dataset lacks is left unbound, as the Form element's mapping leaves one:
|
|
34
34
|
* its value stays in the Inbox copy, and a form never adds a field to a
|
|
@@ -71,10 +71,13 @@ export interface AiFormDatasetBinding {
|
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
73
|
* Binds a form design to a dataset, in place: the form node's `datasetId`,
|
|
74
|
-
* and `datasetFieldId` on each
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
74
|
+
* and `datasetFieldId` on each answer element (an Input, Select, Radio Group,
|
|
75
|
+
* Checkbox or Rating) the dataset has a field for, by the name it submits
|
|
76
|
+
* under or its label — the Form Label or Choice Label beside it — each
|
|
77
|
+
* dataset field taken once. Checkboxes sharing a name are one answer and are
|
|
78
|
+
* bound together. The platform's consent field is never bound. A binding the
|
|
79
|
+
* design already carried is replaced, so what the model drew never chooses
|
|
80
|
+
* where a record lands.
|
|
78
81
|
*/
|
|
79
82
|
export declare function aiBindFormToDataset(nodes: NodesMap, formNodeId: string, dataset: AiFormDataset): AiFormDatasetBinding;
|
|
80
83
|
export interface AiFormDatasetReadDeps {
|
|
@@ -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 { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
|
-
import { collectFormFieldNodeIds, FORM_COMPONENT_ID, isMarketingConsentFieldName } from "@aglyn/aglyn/app-utils/forms";
|
|
18
|
+
import { collectFormFieldNodeIds, FORM_COMPONENT_ID, formFieldDeclsFromNodes, formInputName, formInputType, isMarketingConsentFieldName } from "@aglyn/aglyn/app-utils/forms";
|
|
19
19
|
import { pluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
20
20
|
import { AI_DATASET_DRAFT_RESOURCE } from "./ai-build-unit-outcome.js";
|
|
21
21
|
/**
|
|
@@ -29,7 +29,7 @@ import { AI_DATASET_DRAFT_RESOURCE } from "./ai-build-unit-outcome.js";
|
|
|
29
29
|
* first — a site start's dataset unit, a build's dataset item, or one the
|
|
30
30
|
* site already has — and the form step binds the form it generated the way a
|
|
31
31
|
* person binds one on the Form element: the form node's `datasetId`, and
|
|
32
|
-
* each
|
|
32
|
+
* each answer element's `datasetFieldId` where the dataset has a field of the
|
|
33
33
|
* same name and a type the submitted value can be stored as. A field the
|
|
34
34
|
* dataset lacks is left unbound, as the Form element's mapping leaves one:
|
|
35
35
|
* its value stays in the Inbox copy, and a form never adds a field to a
|
|
@@ -64,10 +64,13 @@ const NUMBER_TYPES = new Set([
|
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Binds a form design to a dataset, in place: the form node's `datasetId`,
|
|
67
|
-
* and `datasetFieldId` on each
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* and `datasetFieldId` on each answer element (an Input, Select, Radio Group,
|
|
68
|
+
* Checkbox or Rating) the dataset has a field for, by the name it submits
|
|
69
|
+
* under or its label — the Form Label or Choice Label beside it — each
|
|
70
|
+
* dataset field taken once. Checkboxes sharing a name are one answer and are
|
|
71
|
+
* bound together. The platform's consent field is never bound. A binding the
|
|
72
|
+
* design already carried is replaced, so what the model drew never chooses
|
|
73
|
+
* where a record lands.
|
|
71
74
|
*/ export function aiBindFormToDataset(nodes, formNodeId, dataset) {
|
|
72
75
|
var _form_props;
|
|
73
76
|
const result = {
|
|
@@ -80,26 +83,41 @@ const NUMBER_TYPES = new Set([
|
|
|
80
83
|
delete props['datasetName'];
|
|
81
84
|
props['datasetId'] = dataset.id;
|
|
82
85
|
form.props = props;
|
|
86
|
+
const labels = new Map(formFieldDeclsFromNodes(nodes, formNodeId).map((decl)=>{
|
|
87
|
+
var _decl_label;
|
|
88
|
+
return [
|
|
89
|
+
decl.fieldName,
|
|
90
|
+
(_decl_label = decl.label) != null ? _decl_label : ''
|
|
91
|
+
];
|
|
92
|
+
}));
|
|
83
93
|
const taken = new Set();
|
|
94
|
+
/** The dataset field each answer's name was bound to, or `null` where none was. */ const byName = new Map();
|
|
84
95
|
for (const id of collectFormFieldNodeIds(nodes, formNodeId)){
|
|
85
|
-
var _field_props,
|
|
96
|
+
var _field_props, _labels_get, _matching;
|
|
86
97
|
const field = nodes[id];
|
|
87
98
|
if (!field) continue;
|
|
88
99
|
const fieldProps = _extends({}, (_field_props = field.props) != null ? _field_props : {});
|
|
89
100
|
delete fieldProps['datasetFieldId'];
|
|
90
101
|
field.props = fieldProps;
|
|
91
|
-
const fieldName =
|
|
102
|
+
const fieldName = formInputName(field);
|
|
92
103
|
if (!fieldName || isMarketingConsentFieldName(fieldName)) continue;
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
if (byName.has(fieldName)) {
|
|
105
|
+
const already = byName.get(fieldName);
|
|
106
|
+
if (already) fieldProps['datasetFieldId'] = already;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const label = (_labels_get = labels.get(fieldName)) != null ? _labels_get : '';
|
|
110
|
+
const fieldType = formInputType(field);
|
|
95
111
|
// Its name first, then its label: `fullName` is "Full name" before "Your name" is.
|
|
96
112
|
const matching = (key)=>key ? dataset.fields.find((one)=>!taken.has(one.id) && aiFormFieldKey(one.name) === key && aiFormFieldFitsDataset(fieldType, one.type)) : undefined;
|
|
97
113
|
const match = (_matching = matching(aiFormFieldKey(fieldName))) != null ? _matching : matching(aiFormFieldKey(label));
|
|
98
114
|
if (!match) {
|
|
115
|
+
byName.set(fieldName, null);
|
|
99
116
|
result.unbound.push(label || fieldName);
|
|
100
117
|
continue;
|
|
101
118
|
}
|
|
102
119
|
taken.add(match.id);
|
|
120
|
+
byName.set(fieldName, match.id);
|
|
103
121
|
fieldProps['datasetFieldId'] = match.id;
|
|
104
122
|
result.bound.push(fieldName);
|
|
105
123
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-form-dataset.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n isMarketingConsentFieldName,\n} from '@aglyn/aglyn/app-utils/forms'\nimport { pluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_DATASET_DRAFT_RESOURCE } from './ai-build-unit-outcome'\n\n/**\n * A FORM THAT WRITES TO A DATASET (AGL-3616; Zach, 2026-10-10: Aglyn AI's\n * forms can write to datasets — a volunteer sign-up into a Volunteers\n * dataset, an RSVP into an event's attendees, a catering inquiry into\n * Inquiries).\n *\n * The plan says which dataset a form writes to (`writesTo` on its creation);\n * it is never inferred from what a section places. The dataset is built\n * first — a site start's dataset unit, a build's dataset item, or one the\n * site already has — and the form step binds the form it generated the way a\n * person binds one on the Form element: the form node's `datasetId`, and\n * each Form Field's `datasetFieldId` where the dataset has a field of the\n * same name and a type the submitted value can be stored as. A field the\n * dataset lacks is left unbound, as the Form element's mapping leaves one:\n * its value stays in the Inbox copy, and a form never adds a field to a\n * dataset.\n *\n * The gates are the platform's. The binding is made only where the plan\n * includes the data store (`dataStore`), the data plugin's writer is loaded\n * here and reads the dataset, and the dataset is one this job made or one the\n * site's inventory shows. Records, their allowance and storage are held at\n * each submission by the data plugin's own record target. Anywhere else the\n * form is the form it always was, its submissions in the Inbox.\n */\n\n/** The unit input a dataset this job made for the form rides in, by id. */\nexport const AI_FORM_DATASET_MADE_INPUT = 'formDatasetMade'\n\n/** One field of the dataset a form writes to, as its writer reports it: storage type words. */\nexport interface AiFormDatasetField {\n id: string\n name: string\n type: string\n}\n\n/** The dataset a form writes to, with the fields a submission may fill. */\nexport interface AiFormDataset {\n id: string\n name: string\n fields: AiFormDatasetField[]\n}\n\n/** A field name as two spellings of it compare: `fullName`, `full_name` and \"Full name\" are one. */\nexport function aiFormFieldKey(name: string): string {\n return name\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '')\n}\n\nconst NUMBER_TYPES = new Set(['float', 'int32', 'int64', 'number', 'integer'])\n\n/**\n * Whether a submitted value of this form field type can be stored in a\n * dataset field of this storage type. Every value a form submits is text, so\n * a text field takes any of them and a number field takes a rating; any\n * other pairing is left unbound rather than refused at every submission.\n */\nexport function aiFormFieldFitsDataset(fieldType: string, datasetType: string): boolean {\n if (datasetType === 'text') return true\n if (NUMBER_TYPES.has(datasetType)) return fieldType === 'rating'\n return false\n}\n\n/** What a binding came to: the fields bound, and the fields the dataset has no place for. */\nexport interface AiFormDatasetBinding {\n bound: string[]\n unbound: string[]\n}\n\n/**\n * Binds a form design to a dataset, in place: the form node's `datasetId`,\n * and `datasetFieldId` on each Form Field the dataset has a field for, by its\n * `fieldName` or its label, each dataset field taken once. The platform's\n * consent field is never bound. A binding the design already carried is\n * replaced, so what the model drew never chooses where a record lands.\n */\nexport function aiBindFormToDataset(\n nodes: NodesMap,\n formNodeId: string,\n dataset: AiFormDataset,\n): AiFormDatasetBinding {\n const result: AiFormDatasetBinding = { bound: [], unbound: [] }\n const form = nodes[formNodeId]\n if (form?.componentId !== FORM_COMPONENT_ID) return result\n const props = { ...((form.props ?? {}) as Record<string, unknown>) }\n delete props['datasetName']\n props['datasetId'] = dataset.id\n form.props = props as never\n const taken = new Set<string>()\n for (const id of collectFormFieldNodeIds(nodes as never, formNodeId)) {\n const field = nodes[id]\n if (!field) continue\n const fieldProps = { ...((field.props ?? {}) as Record<string, unknown>) }\n delete fieldProps['datasetFieldId']\n field.props = fieldProps as never\n const fieldName = String(fieldProps['fieldName'] ?? '').trim()\n if (!fieldName || isMarketingConsentFieldName(fieldName)) continue\n const label = String(fieldProps['label'] ?? '').trim()\n const fieldType = String(fieldProps['fieldType'] ?? 'text')\n // Its name first, then its label: `fullName` is \"Full name\" before \"Your name\" is.\n const matching = (key: string) =>\n key\n ? dataset.fields.find((one) => !taken.has(one.id) && aiFormFieldKey(one.name) === key && aiFormFieldFitsDataset(fieldType, one.type))\n : undefined\n const match = matching(aiFormFieldKey(fieldName)) ?? matching(aiFormFieldKey(label))\n if (!match) {\n result.unbound.push(label || fieldName)\n continue\n }\n taken.add(match.id)\n fieldProps['datasetFieldId'] = match.id\n result.bound.push(fieldName)\n }\n return result\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nexport interface AiFormDatasetReadDeps {\n writerFor?: typeof pluginResourceDraftWriter\n}\n\n/**\n * The dataset a form may write to, or `null` where it may not: the plan\n * includes no data store, the data plugin's writer is not loaded here, the\n * dataset is neither one this job made nor one the site's inventory shows,\n * or it has no field a submission could fill.\n */\nexport async function aiReadFormDataset(\n input: {\n hostId: string\n id: string | null | undefined\n org: object | null\n inventory: Pick<AiSiteInventory, 'datasets'> | null\n /** The dataset this job made for the form, by id. */\n madeId?: unknown\n },\n deps: AiFormDatasetReadDeps = {},\n): Promise<AiFormDataset | null> {\n const id = str(input.id)\n if (!id) return null\n if (!checkEntitlement(input.org as never, 'dataStore')) return null\n const known = id === str(input.madeId) || (input.inventory?.datasets ?? []).some((row) => row.id === id)\n if (!known) return null\n const keeper = (deps.writerFor ?? pluginResourceDraftWriter)(AI_DATASET_DRAFT_RESOURCE)\n if (!keeper) return null\n const record = await keeper.writer.read({ hostId: input.hostId, id }).catch((error: unknown) => {\n console.error('ai form: the dataset it writes to could not be read', { hostId: input.hostId, id, error })\n return null\n })\n if (!record) return null\n const facts = (record.facts ?? {}) as { fields?: unknown; addressField?: unknown }\n const address = str(facts.addressField)\n const fields = (Array.isArray(facts.fields) ? (facts.fields as unknown[]) : [])\n .map((raw) => (raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}))\n .map((raw) => ({ id: str(raw['id']), name: str(raw['name']), type: str(raw['type']) || 'text' }))\n .filter((field) => field.id && field.name && field.id !== address)\n if (!fields.length) return null\n return { id, name: str(record.name) || 'dataset', fields }\n}\n\n/** What the form's generation is told about the dataset it writes to. */\nexport function aiFormDatasetPromptLine(dataset: AiFormDataset): string {\n const fields = dataset.fields.map((field) => `${field.name} (${NUMBER_TYPES.has(field.type) ? 'a number' : 'text'})`)\n return `Each submission is also saved as a record of the dataset “${dataset.name}”, whose fields are: ${fields.join(', ')}. Draw a field for each one a visitor can answer, labeled with the field's name.`\n}\n\n/** What the form's row says about where its submissions are also saved. */\nexport function aiFormDatasetNote(dataset: AiFormDataset, binding: AiFormDatasetBinding): string {\n const kept = `Each submission is also saved as a record of the “${dataset.name}” dataset, in Data.`\n if (!binding.unbound.length) return kept\n const one = binding.unbound.length === 1\n return `${kept} The dataset has no field for ${binding.unbound.map((name) => `“${name}”`).join(', ')}, so ${\n one ? 'that answer stays' : 'those answers stay'\n } in the Inbox only.`\n}\n"],"names":["checkEntitlement","collectFormFieldNodeIds","FORM_COMPONENT_ID","isMarketingConsentFieldName","pluginResourceDraftWriter","AI_DATASET_DRAFT_RESOURCE","AI_FORM_DATASET_MADE_INPUT","aiFormFieldKey","name","replace","toLowerCase","NUMBER_TYPES","Set","aiFormFieldFitsDataset","fieldType","datasetType","has","aiBindFormToDataset","nodes","formNodeId","dataset","form","result","bound","unbound","componentId","props","id","taken","field","fieldProps","matching","fieldName","String","trim","label","key","fields","find","one","type","undefined","match","push","add","str","value","aiReadFormDataset","input","deps","record","org","known","madeId","inventory","datasets","some","row","keeper","writerFor","writer","read","hostId","catch","error","console","facts","address","addressField","Array","isArray","map","raw","filter","length","aiFormDatasetPromptLine","join","aiFormDatasetNote","binding","kept"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-form-dataset.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 { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n formFieldDeclsFromNodes,\n formInputName,\n formInputType,\n isMarketingConsentFieldName,\n} from '@aglyn/aglyn/app-utils/forms'\nimport { pluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_DATASET_DRAFT_RESOURCE } from './ai-build-unit-outcome'\n\n/**\n * A FORM THAT WRITES TO A DATASET (AGL-3616; Zach, 2026-10-10: Aglyn AI's\n * forms can write to datasets — a volunteer sign-up into a Volunteers\n * dataset, an RSVP into an event's attendees, a catering inquiry into\n * Inquiries).\n *\n * The plan says which dataset a form writes to (`writesTo` on its creation);\n * it is never inferred from what a section places. The dataset is built\n * first — a site start's dataset unit, a build's dataset item, or one the\n * site already has — and the form step binds the form it generated the way a\n * person binds one on the Form element: the form node's `datasetId`, and\n * each answer element's `datasetFieldId` where the dataset has a field of the\n * same name and a type the submitted value can be stored as. A field the\n * dataset lacks is left unbound, as the Form element's mapping leaves one:\n * its value stays in the Inbox copy, and a form never adds a field to a\n * dataset.\n *\n * The gates are the platform's. The binding is made only where the plan\n * includes the data store (`dataStore`), the data plugin's writer is loaded\n * here and reads the dataset, and the dataset is one this job made or one the\n * site's inventory shows. Records, their allowance and storage are held at\n * each submission by the data plugin's own record target. Anywhere else the\n * form is the form it always was, its submissions in the Inbox.\n */\n\n/** The unit input a dataset this job made for the form rides in, by id. */\nexport const AI_FORM_DATASET_MADE_INPUT = 'formDatasetMade'\n\n/** One field of the dataset a form writes to, as its writer reports it: storage type words. */\nexport interface AiFormDatasetField {\n id: string\n name: string\n type: string\n}\n\n/** The dataset a form writes to, with the fields a submission may fill. */\nexport interface AiFormDataset {\n id: string\n name: string\n fields: AiFormDatasetField[]\n}\n\n/** A field name as two spellings of it compare: `fullName`, `full_name` and \"Full name\" are one. */\nexport function aiFormFieldKey(name: string): string {\n return name\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '')\n}\n\nconst NUMBER_TYPES = new Set(['float', 'int32', 'int64', 'number', 'integer'])\n\n/**\n * Whether a submitted value of this form field type can be stored in a\n * dataset field of this storage type. Every value a form submits is text, so\n * a text field takes any of them and a number field takes a rating; any\n * other pairing is left unbound rather than refused at every submission.\n */\nexport function aiFormFieldFitsDataset(fieldType: string, datasetType: string): boolean {\n if (datasetType === 'text') return true\n if (NUMBER_TYPES.has(datasetType)) return fieldType === 'rating'\n return false\n}\n\n/** What a binding came to: the fields bound, and the fields the dataset has no place for. */\nexport interface AiFormDatasetBinding {\n bound: string[]\n unbound: string[]\n}\n\n/**\n * Binds a form design to a dataset, in place: the form node's `datasetId`,\n * and `datasetFieldId` on each answer element (an Input, Select, Radio Group,\n * Checkbox or Rating) the dataset has a field for, by the name it submits\n * under or its label — the Form Label or Choice Label beside it — each\n * dataset field taken once. Checkboxes sharing a name are one answer and are\n * bound together. The platform's consent field is never bound. A binding the\n * design already carried is replaced, so what the model drew never chooses\n * where a record lands.\n */\nexport function aiBindFormToDataset(\n nodes: NodesMap,\n formNodeId: string,\n dataset: AiFormDataset,\n): AiFormDatasetBinding {\n const result: AiFormDatasetBinding = { bound: [], unbound: [] }\n const form = nodes[formNodeId]\n if (form?.componentId !== FORM_COMPONENT_ID) return result\n const props = { ...((form.props ?? {}) as Record<string, unknown>) }\n delete props['datasetName']\n props['datasetId'] = dataset.id\n form.props = props as never\n const labels = new Map(\n formFieldDeclsFromNodes(nodes as never, formNodeId).map((decl) => [decl.fieldName, decl.label ?? ''] as const),\n )\n const taken = new Set<string>()\n /** The dataset field each answer's name was bound to, or `null` where none was. */\n const byName = new Map<string, string | null>()\n for (const id of collectFormFieldNodeIds(nodes as never, formNodeId)) {\n const field = nodes[id]\n if (!field) continue\n const fieldProps = { ...((field.props ?? {}) as Record<string, unknown>) }\n delete fieldProps['datasetFieldId']\n field.props = fieldProps as never\n const fieldName = formInputName(field as never)\n if (!fieldName || isMarketingConsentFieldName(fieldName)) continue\n if (byName.has(fieldName)) {\n const already = byName.get(fieldName)\n if (already) fieldProps['datasetFieldId'] = already\n continue\n }\n const label = labels.get(fieldName) ?? ''\n const fieldType = formInputType(field as never)\n // Its name first, then its label: `fullName` is \"Full name\" before \"Your name\" is.\n const matching = (key: string) =>\n key\n ? dataset.fields.find((one) => !taken.has(one.id) && aiFormFieldKey(one.name) === key && aiFormFieldFitsDataset(fieldType, one.type))\n : undefined\n const match = matching(aiFormFieldKey(fieldName)) ?? matching(aiFormFieldKey(label))\n if (!match) {\n byName.set(fieldName, null)\n result.unbound.push(label || fieldName)\n continue\n }\n taken.add(match.id)\n byName.set(fieldName, match.id)\n fieldProps['datasetFieldId'] = match.id\n result.bound.push(fieldName)\n }\n return result\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nexport interface AiFormDatasetReadDeps {\n writerFor?: typeof pluginResourceDraftWriter\n}\n\n/**\n * The dataset a form may write to, or `null` where it may not: the plan\n * includes no data store, the data plugin's writer is not loaded here, the\n * dataset is neither one this job made nor one the site's inventory shows,\n * or it has no field a submission could fill.\n */\nexport async function aiReadFormDataset(\n input: {\n hostId: string\n id: string | null | undefined\n org: object | null\n inventory: Pick<AiSiteInventory, 'datasets'> | null\n /** The dataset this job made for the form, by id. */\n madeId?: unknown\n },\n deps: AiFormDatasetReadDeps = {},\n): Promise<AiFormDataset | null> {\n const id = str(input.id)\n if (!id) return null\n if (!checkEntitlement(input.org as never, 'dataStore')) return null\n const known = id === str(input.madeId) || (input.inventory?.datasets ?? []).some((row) => row.id === id)\n if (!known) return null\n const keeper = (deps.writerFor ?? pluginResourceDraftWriter)(AI_DATASET_DRAFT_RESOURCE)\n if (!keeper) return null\n const record = await keeper.writer.read({ hostId: input.hostId, id }).catch((error: unknown) => {\n console.error('ai form: the dataset it writes to could not be read', { hostId: input.hostId, id, error })\n return null\n })\n if (!record) return null\n const facts = (record.facts ?? {}) as { fields?: unknown; addressField?: unknown }\n const address = str(facts.addressField)\n const fields = (Array.isArray(facts.fields) ? (facts.fields as unknown[]) : [])\n .map((raw) => (raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}))\n .map((raw) => ({ id: str(raw['id']), name: str(raw['name']), type: str(raw['type']) || 'text' }))\n .filter((field) => field.id && field.name && field.id !== address)\n if (!fields.length) return null\n return { id, name: str(record.name) || 'dataset', fields }\n}\n\n/** What the form's generation is told about the dataset it writes to. */\nexport function aiFormDatasetPromptLine(dataset: AiFormDataset): string {\n const fields = dataset.fields.map((field) => `${field.name} (${NUMBER_TYPES.has(field.type) ? 'a number' : 'text'})`)\n return `Each submission is also saved as a record of the dataset “${dataset.name}”, whose fields are: ${fields.join(', ')}. Draw a field for each one a visitor can answer, labeled with the field's name.`\n}\n\n/** What the form's row says about where its submissions are also saved. */\nexport function aiFormDatasetNote(dataset: AiFormDataset, binding: AiFormDatasetBinding): string {\n const kept = `Each submission is also saved as a record of the “${dataset.name}” dataset, in Data.`\n if (!binding.unbound.length) return kept\n const one = binding.unbound.length === 1\n return `${kept} The dataset has no field for ${binding.unbound.map((name) => `“${name}”`).join(', ')}, so ${\n one ? 'that answer stays' : 'those answers stay'\n } in the Inbox only.`\n}\n"],"names":["checkEntitlement","collectFormFieldNodeIds","FORM_COMPONENT_ID","formFieldDeclsFromNodes","formInputName","formInputType","isMarketingConsentFieldName","pluginResourceDraftWriter","AI_DATASET_DRAFT_RESOURCE","AI_FORM_DATASET_MADE_INPUT","aiFormFieldKey","name","replace","toLowerCase","NUMBER_TYPES","Set","aiFormFieldFitsDataset","fieldType","datasetType","has","aiBindFormToDataset","nodes","formNodeId","dataset","form","result","bound","unbound","componentId","props","id","labels","Map","map","decl","fieldName","label","taken","byName","field","matching","fieldProps","already","get","key","fields","find","one","type","undefined","match","set","push","add","str","value","trim","aiReadFormDataset","input","deps","record","org","known","madeId","inventory","datasets","some","row","keeper","writerFor","writer","read","hostId","catch","error","console","facts","address","addressField","Array","isArray","raw","filter","length","aiFormDatasetPromptLine","join","aiFormDatasetNote","binding","kept"],"mappings":"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"}
|
|
@@ -40,9 +40,9 @@ import type { AiJobStepRunner } from './ai-job-text-step';
|
|
|
40
40
|
*
|
|
41
41
|
* - `fields` is `formFieldDeclsFromNodes` over the design;
|
|
42
42
|
* - a form that can yield an email address gets the platform's marketing
|
|
43
|
-
* consent field (`
|
|
44
|
-
*
|
|
45
|
-
* `consentFieldName`;
|
|
43
|
+
* consent field (`aiFormConsentNodes`: a Choice Label holding one Checkbox,
|
|
44
|
+
* worded as the Forms editor preset's option) in place of any consent-like
|
|
45
|
+
* field the model drew, named as its `consentFieldName`;
|
|
46
46
|
* - `routing` is `{ lead: true }` when the answer proposes a lead, and
|
|
47
47
|
* nothing otherwise. The stored routing has no place for an email list,
|
|
48
48
|
* so a list proposal leaves the form on the Inbox and says, in the output's
|
|
@@ -91,8 +91,24 @@ export declare const AI_FORM_ROUTING_KINDS: readonly ["inbox", "list", "lead"];
|
|
|
91
91
|
export type AiFormRoutingKind = (typeof AI_FORM_ROUTING_KINDS)[number];
|
|
92
92
|
/** The most things a note says the brief asked for and a form cannot collect. */
|
|
93
93
|
export declare const AI_FORM_CANNOT_COLLECT_MAX = 3;
|
|
94
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* The node id the platform's consent field takes in a generated design: the
|
|
96
|
+
* Choice Label that holds its Checkbox.
|
|
97
|
+
*/
|
|
95
98
|
export declare const AI_FORM_CONSENT_NODE_ID = "marketingConsentField";
|
|
99
|
+
/** The node id of the consent field's Checkbox, inside {@link AI_FORM_CONSENT_NODE_ID}. */
|
|
100
|
+
export declare const AI_FORM_CONSENT_CHECKBOX_NODE_ID = "marketingConsentCheckbox";
|
|
101
|
+
/** The name the consent Checkbox submits under, and the form's `consentFieldName`. */
|
|
102
|
+
export declare const AI_FORM_CONSENT_FIELD_NAME: string;
|
|
103
|
+
/** What a ticked consent Checkbox posts: an affirmative value `isConsentCheckboxTicked` reads as the tick. */
|
|
104
|
+
export declare const AI_FORM_CONSENT_VALUE = "Yes";
|
|
105
|
+
/**
|
|
106
|
+
* The platform's marketing consent field as MUI's separate elements: a Choice
|
|
107
|
+
* Label whose words say what the person agrees to — the Forms editor preset's
|
|
108
|
+
* own option text — holding one unticked, unrequired Checkbox named
|
|
109
|
+
* `marketingConsent`. Ids are fixed, so a re-asked design gets the same nodes.
|
|
110
|
+
*/
|
|
111
|
+
export declare function aiFormConsentNodes(formNodeId: string): NodesMap[string][];
|
|
96
112
|
/** The form step's own instructions, cached after the doctrine. */
|
|
97
113
|
export declare const AI_JOB_FORM_INSTRUCTIONS: readonly AiSystemBlock[];
|
|
98
114
|
/**
|