@aglyn/plugins-ai 1.0.0-beta.240 → 1.0.0-beta.241
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 -0
- package/src/lib/components/ai-job-links.js +63 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +8 -1
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +7 -1
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-store-finish-card.component.d.ts +28 -0
- package/src/lib/components/ai-store-finish-card.component.js +116 -0
- package/src/lib/components/ai-store-finish-card.component.js.map +1 -0
- package/src/lib/components/assist-build-card.component.js +12 -1
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +45 -1
- package/src/lib/jobs/ai-job-build-step.js +283 -7
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
- package/src/lib/jobs/ai-job-drafts.js +4 -1
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
- package/src/lib/jobs/ai-job-layout-language.js +4 -2
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +3 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
- package/src/lib/jobs/ai-job-page-language.js +19 -7
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +11 -0
- package/src/lib/jobs/ai-job-page-sections.js +40 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-page-step.js +19 -8
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +12 -0
- package/src/lib/jobs/ai-job-plan-step.js +41 -7
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +76 -2
- package/src/lib/jobs/ai-job-site-datasets.js +206 -13
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +18 -2
- package/src/lib/jobs/ai-job-site-step.js +109 -22
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-store-pages.d.ts +75 -0
- package/src/lib/jobs/ai-job-site-store-pages.js +195 -0
- package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -0
- package/src/lib/jobs/ai-job-store-links-layout.d.ts +71 -0
- package/src/lib/jobs/ai-job-store-links-layout.js +95 -0
- package/src/lib/jobs/ai-job-store-links-layout.js.map +1 -0
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +69 -3
- package/src/lib/jobs/ai-layout-stock-photos.js +603 -42
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +18 -0
- package/src/lib/jobs/ai-site-publish.js +76 -15
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.d.ts +43 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js +61 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.js +25 -2
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +10 -0
- package/src/lib/layout-language/ai-layout-frame.js +58 -4
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-listings.js +3 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +2 -0
- package/src/lib/model/ai-build-job.js +23 -3
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +7 -0
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +3 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-build-store-pages.d.ts +133 -0
- package/src/lib/model/ai-build-store-pages.js +209 -0
- package/src/lib/model/ai-build-store-pages.js.map +1 -0
- package/src/lib/model/ai-layout-store-links.d.ts +64 -0
- package/src/lib/model/ai-layout-store-links.js +165 -0
- package/src/lib/model/ai-layout-store-links.js.map +1 -0
- package/src/lib/model/ai-site-inventory.d.ts +5 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.d.ts +192 -0
- package/src/lib/model/ai-site-store-pages.js +383 -0
- package/src/lib/model/ai-site-store-pages.js.map +1 -0
- package/src/lib/runtime/site-inventory.js +8 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.js +8 -4
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
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 { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
|
+
import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
|
|
19
|
+
import { ACCOUNTS_PLUGIN_ID } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
20
|
+
import { HostScreenVisibility } from "@aglyn/aglyn/foundation/definitions/platform.types";
|
|
21
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
22
|
+
import { aiStorePageNodes, aiStorePagesToWrite, AI_STORE_PAGES } from "../model/ai-site-store-pages.js";
|
|
23
|
+
import { aiModelForStep } from "../providers/routing.js";
|
|
24
|
+
import { aiOriginJobId } from "./ai-job-draft-ids.js";
|
|
25
|
+
import { writeAiDraft, writeAiDraftScreenSeo } from "./ai-job-drafts.js";
|
|
26
|
+
import { aiUnspentOutcome } from "./ai-job-generation.js";
|
|
27
|
+
import { aiPluginDraftAdmissionRefusal } from "./ai-job-plugin-drafts.js";
|
|
28
|
+
/**
|
|
29
|
+
* The store pages' unit of a site start (AGL-3676): the account, cart and
|
|
30
|
+
* policy pages a paid store gets beside its planned pages
|
|
31
|
+
* (`model/ai-site-store-pages.ts`), written by code in one pass that asks no
|
|
32
|
+
* model and costs no credits.
|
|
33
|
+
*
|
|
34
|
+
* Each page is a draft written through the same writer, under the same pages
|
|
35
|
+
* allowance, as every page the start builds (`writeAiDraft`), under an id
|
|
36
|
+
* derived from the unit's job, so a pass run again finds its own drafts. The
|
|
37
|
+
* guided start's publish then puts them live with the rest.
|
|
38
|
+
*
|
|
39
|
+
* The site's user accounts are turned on here too: the Customer account's
|
|
40
|
+
* sign-in, and the platform's /signin and /signup, answer only where the site
|
|
41
|
+
* serves accounts (`accounts` is off by default on a site that predates
|
|
42
|
+
* 2026-08; every new site is born with it). A site whose owner switched them
|
|
43
|
+
* off keeps them off: an explicit deny is theirs.
|
|
44
|
+
*/ /** The unit input a store pages unit carries what it writes in. */ export const AI_SITE_STORE_PAGES_INPUT = 'storePages';
|
|
45
|
+
/** The commerce plugin's name, in the refusal that asks for it. */ const STORE_LABEL = 'Commerce';
|
|
46
|
+
/** What a store pages row says where nothing could be written: our failure. */ export const AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY = 'Your account, cart and policy pages could not be added this time.';
|
|
47
|
+
/** The id of the store page a unit writes for `key`. */ export function aiSiteStorePageId(unitJobId, key) {
|
|
48
|
+
return `${unitJobId}-${key}`;
|
|
49
|
+
}
|
|
50
|
+
const PATH = /^\/[a-z0-9-]*$/;
|
|
51
|
+
function inputOf(job) {
|
|
52
|
+
var _job_inputs;
|
|
53
|
+
const raw = (_job_inputs = job.inputs) == null ? void 0 : _job_inputs[AI_SITE_STORE_PAGES_INPUT];
|
|
54
|
+
if (!raw || !Array.isArray(raw.pages) || !raw.facts) return null;
|
|
55
|
+
const keys = new Set(AI_STORE_PAGES.map((page)=>page.key));
|
|
56
|
+
const pages = raw.pages.filter((page)=>page && keys.has(page.key) && typeof page.slug === 'string');
|
|
57
|
+
const path = (value, fallback)=>typeof value === 'string' && PATH.test(value) ? value : fallback;
|
|
58
|
+
return {
|
|
59
|
+
pages,
|
|
60
|
+
facts: {
|
|
61
|
+
contactPath: raw.facts.contactPath ? path(raw.facts.contactPath, '') || null : null,
|
|
62
|
+
shopPath: path(raw.facts.shopPath, '/shop'),
|
|
63
|
+
shippingPath: path(raw.facts.shippingPath, '/shipping-returns')
|
|
64
|
+
},
|
|
65
|
+
layoutId: typeof raw.layoutId === 'string' && raw.layoutId ? raw.layoutId : null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether the store pages may be added for this member on this site, asked
|
|
70
|
+
* before their pass so a refusal spends nothing; `null` admits. The plan must
|
|
71
|
+
* include a store (Starter and up), the commerce plugin must run here, and the
|
|
72
|
+
* member must be able to edit the site's pages.
|
|
73
|
+
*/ export async function aiSiteStorePagesRefusal(context, deps = {}) {
|
|
74
|
+
var _context_org, _deps_admission, _context_org1, _host_get;
|
|
75
|
+
const { job, firestore } = context;
|
|
76
|
+
if (!job.hostId) return 'Open the site first.';
|
|
77
|
+
if (!checkEntitlement((_context_org = context.org) != null ? _context_org : null, 'commerce')) return 'Your plan does not include a store.';
|
|
78
|
+
const refused = await ((_deps_admission = deps.admission) != null ? _deps_admission : aiPluginDraftAdmissionRefusal)({
|
|
79
|
+
firestore,
|
|
80
|
+
orgId: job.orgId,
|
|
81
|
+
hostId: job.hostId,
|
|
82
|
+
inputs: {},
|
|
83
|
+
org: (_context_org1 = context.org) != null ? _context_org1 : null
|
|
84
|
+
}, {
|
|
85
|
+
kind: 'site',
|
|
86
|
+
drafts: [
|
|
87
|
+
{
|
|
88
|
+
resource: 'product',
|
|
89
|
+
label: STORE_LABEL
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
now: context.now
|
|
93
|
+
});
|
|
94
|
+
if (refused) return refused.error;
|
|
95
|
+
const host = await firestore.collection('hosts').doc(job.hostId).get();
|
|
96
|
+
return hostRoleCanWrite(((_host_get = host.get('memberRoles')) != null ? _host_get : {})[job.createdBy]) ? null : 'Editing requires the editor role';
|
|
97
|
+
}
|
|
98
|
+
/** The store pages' pass: every page written, user accounts on, one output a page. */ export function createAiSiteStorePagesRunner(deps = {}) {
|
|
99
|
+
var _deps_write, _deps_writeSeo;
|
|
100
|
+
const write = (_deps_write = deps.write) != null ? _deps_write : writeAiDraft;
|
|
101
|
+
const writeSeo = (_deps_writeSeo = deps.writeSeo) != null ? _deps_writeSeo : writeAiDraftScreenSeo;
|
|
102
|
+
return async (context)=>{
|
|
103
|
+
var _ref;
|
|
104
|
+
const { job, firestore, now } = context;
|
|
105
|
+
const model = (_ref = context.modelFor == null ? void 0 : context.modelFor.call(context, 'job.plan')) != null ? _ref : aiModelForStep('job.plan');
|
|
106
|
+
const input = inputOf(job);
|
|
107
|
+
const hostId = job.hostId;
|
|
108
|
+
if (!input || !hostId) return _extends({}, aiUnspentOutcome(model), {
|
|
109
|
+
failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY
|
|
110
|
+
});
|
|
111
|
+
const outputs = [];
|
|
112
|
+
let refusal = null;
|
|
113
|
+
for (const page of aiStorePagesToWrite(input.pages)){
|
|
114
|
+
var _context_org;
|
|
115
|
+
const definition = AI_STORE_PAGES.find((one)=>one.key === page.key);
|
|
116
|
+
if (!definition) continue;
|
|
117
|
+
const id = aiSiteStorePageId(job.$id, page.key);
|
|
118
|
+
const draft = await write(firestore, _extends({
|
|
119
|
+
kind: 'screen',
|
|
120
|
+
hostId,
|
|
121
|
+
id,
|
|
122
|
+
uid: job.createdBy,
|
|
123
|
+
org: (_context_org = context.org) != null ? _context_org : null,
|
|
124
|
+
name: definition.title,
|
|
125
|
+
slug: page.slug,
|
|
126
|
+
nodes: aiStorePageNodes(page.key, input.facts),
|
|
127
|
+
layoutId: input.layoutId
|
|
128
|
+
}, definition.noindex ? {
|
|
129
|
+
visibility: HostScreenVisibility.UNLISTED
|
|
130
|
+
} : {}, {
|
|
131
|
+
aiJobId: aiOriginJobId(job),
|
|
132
|
+
now
|
|
133
|
+
}));
|
|
134
|
+
if (draft.ok === false) {
|
|
135
|
+
// The pages allowance is full, or the site is gone: what was written stays.
|
|
136
|
+
refusal = draft.error;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
await writeSeo(firestore, {
|
|
140
|
+
hostId,
|
|
141
|
+
id,
|
|
142
|
+
seo: definition.seo,
|
|
143
|
+
now
|
|
144
|
+
});
|
|
145
|
+
outputs.push({
|
|
146
|
+
resource: 'screen',
|
|
147
|
+
id,
|
|
148
|
+
versionId: draft.versionId,
|
|
149
|
+
hostId,
|
|
150
|
+
hostSubdomain: draft.hostSubdomain,
|
|
151
|
+
label: draft.name,
|
|
152
|
+
proposal: {
|
|
153
|
+
storePage: page.key,
|
|
154
|
+
path: page.href
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (outputs.length) await turnOnSiteAccounts(firestore, hostId, now);
|
|
159
|
+
if (!outputs.length) {
|
|
160
|
+
return refusal ? aiUnspentOutcome(model, {
|
|
161
|
+
review: {
|
|
162
|
+
reason: 'limit',
|
|
163
|
+
message: refusal,
|
|
164
|
+
findings: []
|
|
165
|
+
}
|
|
166
|
+
}) : _extends({}, aiUnspentOutcome(model), {
|
|
167
|
+
failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return aiUnspentOutcome(model, {
|
|
171
|
+
outputs
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export const runAiSiteStorePagesUnit = createAiSiteStorePagesRunner();
|
|
176
|
+
/**
|
|
177
|
+
* Turns on the site's user accounts (`accounts`, default-off per site), so
|
|
178
|
+
* the account page's sign-in and the platform's /signin and /signup answer.
|
|
179
|
+
* An owner's explicit switch-off is left as it is.
|
|
180
|
+
*/ export async function turnOnSiteAccounts(firestore, hostId, now) {
|
|
181
|
+
var _host_get, _host_get1;
|
|
182
|
+
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
183
|
+
const host = await hostRef.get();
|
|
184
|
+
if (!host.exists) return false;
|
|
185
|
+
const disabled = (_host_get = host.get('disabledPlugins')) != null ? _host_get : [];
|
|
186
|
+
const enabled = (_host_get1 = host.get('enabledPlugins')) != null ? _host_get1 : [];
|
|
187
|
+
if (disabled.includes(ACCOUNTS_PLUGIN_ID) || enabled.includes(ACCOUNTS_PLUGIN_ID)) return false;
|
|
188
|
+
await hostRef.update({
|
|
189
|
+
enabledPlugins: FieldValue.arrayUnion(ACCOUNTS_PLUGIN_ID),
|
|
190
|
+
updatedAt: now
|
|
191
|
+
});
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
//# sourceMappingURL=ai-job-site-store-pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-store-pages.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 { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { ACCOUNTS_PLUGIN_ID } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport { HostScreenVisibility } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n aiStorePageNodes,\n aiStorePagesToWrite,\n AI_STORE_PAGES,\n type AiStorePageFacts,\n type AiStorePagePlan,\n} from '../model/ai-site-store-pages'\nimport { aiModelForStep } from '../providers/routing'\nimport { aiOriginJobId } from './ai-job-draft-ids'\nimport { writeAiDraft, writeAiDraftScreenSeo } from './ai-job-drafts'\nimport { aiUnspentOutcome } from './ai-job-generation'\nimport { aiPluginDraftAdmissionRefusal } from './ai-job-plugin-drafts'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\n\n/**\n * The store pages' unit of a site start (AGL-3676): the account, cart and\n * policy pages a paid store gets beside its planned pages\n * (`model/ai-site-store-pages.ts`), written by code in one pass that asks no\n * model and costs no credits.\n *\n * Each page is a draft written through the same writer, under the same pages\n * allowance, as every page the start builds (`writeAiDraft`), under an id\n * derived from the unit's job, so a pass run again finds its own drafts. The\n * guided start's publish then puts them live with the rest.\n *\n * The site's user accounts are turned on here too: the Customer account's\n * sign-in, and the platform's /signin and /signup, answer only where the site\n * serves accounts (`accounts` is off by default on a site that predates\n * 2026-08; every new site is born with it). A site whose owner switched them\n * off keeps them off: an explicit deny is theirs.\n */\n\n/** The unit input a store pages unit carries what it writes in. */\nexport const AI_SITE_STORE_PAGES_INPUT = 'storePages'\n\n/** What a store pages unit is told: the store pages the plan implies, and what their words link. */\nexport interface AiSiteStorePagesInput {\n pages: AiStorePagePlan[]\n facts: AiStorePageFacts\n /** The layout the start built, which the pages render inside; the site's otherwise. */\n layoutId: string | null\n}\n\n/** The commerce plugin's name, in the refusal that asks for it. */\nconst STORE_LABEL = 'Commerce'\n\n/** What a store pages row says where nothing could be written: our failure. */\nexport const AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY = 'Your account, cart and policy pages could not be added this time.'\n\n/** The id of the store page a unit writes for `key`. */\nexport function aiSiteStorePageId(unitJobId: string, key: string): string {\n return `${unitJobId}-${key}`\n}\n\nconst PATH = /^\\/[a-z0-9-]*$/\n\nfunction inputOf(job: Pick<AiJob, 'inputs'>): AiSiteStorePagesInput | null {\n const raw = job.inputs?.[AI_SITE_STORE_PAGES_INPUT] as Partial<AiSiteStorePagesInput> | undefined\n if (!raw || !Array.isArray(raw.pages) || !raw.facts) return null\n const keys = new Set(AI_STORE_PAGES.map((page) => page.key))\n const pages = raw.pages.filter((page) => page && keys.has(page.key) && typeof page.slug === 'string')\n const path = (value: unknown, fallback: string) => (typeof value === 'string' && PATH.test(value) ? value : fallback)\n return {\n pages,\n facts: {\n contactPath: raw.facts.contactPath ? path(raw.facts.contactPath, '') || null : null,\n shopPath: path(raw.facts.shopPath, '/shop'),\n shippingPath: path(raw.facts.shippingPath, '/shipping-returns'),\n },\n layoutId: typeof raw.layoutId === 'string' && raw.layoutId ? raw.layoutId : null,\n }\n}\n\n/**\n * Whether the store pages may be added for this member on this site, asked\n * before their pass so a refusal spends nothing; `null` admits. The plan must\n * include a store (Starter and up), the commerce plugin must run here, and the\n * member must be able to edit the site's pages.\n */\nexport async function aiSiteStorePagesRefusal(\n context: Pick<AiJobStepContext, 'firestore' | 'org' | 'now'> & { job: Pick<AiJob, 'orgId' | 'hostId' | 'createdBy'> },\n deps: { admission?: typeof aiPluginDraftAdmissionRefusal } = {},\n): Promise<string | null> {\n const { job, firestore } = context\n if (!job.hostId) return 'Open the site first.'\n if (!checkEntitlement((context.org ?? null) as never, 'commerce')) return 'Your plan does not include a store.'\n const refused = await (deps.admission ?? aiPluginDraftAdmissionRefusal)(\n { firestore, orgId: job.orgId, hostId: job.hostId, inputs: {}, org: context.org ?? null },\n { kind: 'site', drafts: [{ resource: 'product', label: STORE_LABEL }], now: context.now },\n )\n if (refused) return refused.error\n const host = await firestore.collection('hosts').doc(job.hostId).get()\n return hostRoleCanWrite((host.get('memberRoles') ?? {})[job.createdBy]) ? null : 'Editing requires the editor role'\n}\n\nexport interface AiSiteStorePagesRunnerDeps {\n write?: typeof writeAiDraft\n writeSeo?: typeof writeAiDraftScreenSeo\n}\n\n/** The store pages' pass: every page written, user accounts on, one output a page. */\nexport function createAiSiteStorePagesRunner(deps: AiSiteStorePagesRunnerDeps = {}): AiJobStepRunner {\n const write = deps.write ?? writeAiDraft\n const writeSeo = deps.writeSeo ?? writeAiDraftScreenSeo\n return async (context): Promise<AiJobStepOutcome> => {\n const { job, firestore, now } = context\n const model = context.modelFor?.('job.plan') ?? aiModelForStep('job.plan')\n const input = inputOf(job)\n const hostId = job.hostId\n if (!input || !hostId) return { ...aiUnspentOutcome(model), failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY }\n const outputs: AiJobOutput[] = []\n let refusal: string | null = null\n for (const page of aiStorePagesToWrite(input.pages)) {\n const definition = AI_STORE_PAGES.find((one) => one.key === page.key)\n if (!definition) continue\n const id = aiSiteStorePageId(job.$id, page.key)\n const draft = await write(firestore, {\n kind: 'screen',\n hostId,\n id,\n uid: job.createdBy,\n org: context.org ?? null,\n name: definition.title,\n slug: page.slug,\n nodes: aiStorePageNodes(page.key, input.facts) as unknown as NodesMap,\n layoutId: input.layoutId,\n // The account and the cart are a shopper's own pages: unlisted, noindex (AGL-3676).\n ...(definition.noindex ? { visibility: HostScreenVisibility.UNLISTED } : {}),\n aiJobId: aiOriginJobId(job),\n now,\n })\n if (draft.ok === false) {\n // The pages allowance is full, or the site is gone: what was written stays.\n refusal = draft.error\n break\n }\n await writeSeo(firestore, { hostId, id, seo: definition.seo, now })\n outputs.push({\n resource: 'screen',\n id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n proposal: { storePage: page.key, path: page.href },\n })\n }\n if (outputs.length) await turnOnSiteAccounts(firestore, hostId, now)\n if (!outputs.length) {\n return refusal\n ? aiUnspentOutcome(model, { review: { reason: 'limit', message: refusal, findings: [] } })\n : { ...aiUnspentOutcome(model), failure: AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY }\n }\n return aiUnspentOutcome(model, { outputs })\n }\n}\n\nexport const runAiSiteStorePagesUnit = createAiSiteStorePagesRunner()\n\n/**\n * Turns on the site's user accounts (`accounts`, default-off per site), so\n * the account page's sign-in and the platform's /signin and /signup answer.\n * An owner's explicit switch-off is left as it is.\n */\nexport async function turnOnSiteAccounts(firestore: FirebaseFirestore.Firestore, hostId: string, now: Date): Promise<boolean> {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return false\n const disabled = (host.get('disabledPlugins') ?? []) as unknown[]\n const enabled = (host.get('enabledPlugins') ?? []) as unknown[]\n if (disabled.includes(ACCOUNTS_PLUGIN_ID) || enabled.includes(ACCOUNTS_PLUGIN_ID)) return false\n await hostRef.update({ enabledPlugins: FieldValue.arrayUnion(ACCOUNTS_PLUGIN_ID), updatedAt: now })\n return true\n}\n"],"names":["checkEntitlement","hostRoleCanWrite","ACCOUNTS_PLUGIN_ID","HostScreenVisibility","FieldValue","aiStorePageNodes","aiStorePagesToWrite","AI_STORE_PAGES","aiModelForStep","aiOriginJobId","writeAiDraft","writeAiDraftScreenSeo","aiUnspentOutcome","aiPluginDraftAdmissionRefusal","AI_SITE_STORE_PAGES_INPUT","STORE_LABEL","AI_SITE_STORE_PAGES_NOT_WRITTEN_COPY","aiSiteStorePageId","unitJobId","key","PATH","inputOf","job","raw","inputs","Array","isArray","pages","facts","keys","Set","map","page","filter","has","slug","path","value","fallback","test","contactPath","shopPath","shippingPath","layoutId","aiSiteStorePagesRefusal","context","deps","host","firestore","hostId","org","refused","admission","orgId","kind","drafts","resource","label","now","error","collection","doc","get","createdBy","createAiSiteStorePagesRunner","write","writeSeo","model","modelFor","input","failure","outputs","refusal","definition","find","one","id","$id","draft","uid","name","title","nodes","noindex","visibility","UNLISTED","aiJobId","ok","seo","push","versionId","hostSubdomain","proposal","storePage","href","length","turnOnSiteAccounts","review","reason","message","findings","runAiSiteStorePagesUnit","hostRef","exists","disabled","enabled","includes","update","enabledPlugins","arrayUnion","updatedAt"],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { Firestore } from 'firebase-admin/firestore';
|
|
18
|
+
import { type AiStoreLayoutLinks } from '../model/ai-layout-store-links';
|
|
19
|
+
/**
|
|
20
|
+
* A store's links in the site's own layout, as a DRAFT (AGL-3676): a build
|
|
21
|
+
* that makes the site a store and builds no layout writes a new version of
|
|
22
|
+
* the layout its store pages render inside, with Account (and Cart, where
|
|
23
|
+
* the header has none) in the header and the account and policies in the
|
|
24
|
+
* footer (`aiLayoutWithStoreLinks`). The layout's current version — the one
|
|
25
|
+
* visitors see — is never touched: the new version waits beside it, and goes
|
|
26
|
+
* live only when the build publishes (`aiPublishGuidedSite`'s `storeLinks`),
|
|
27
|
+
* as every draft the build makes does.
|
|
28
|
+
*/
|
|
29
|
+
/** The field a draft version keeps the version it was made from in, so a publish knows whether it is still current. */
|
|
30
|
+
export declare const AI_STORE_LINKS_SOURCE_FIELD = "aiStoreLinksSourceVersionId";
|
|
31
|
+
/** What a store links draft version is called in the layout's versions. */
|
|
32
|
+
export declare const AI_STORE_LINKS_VERSION_NAME = "Account and policy links";
|
|
33
|
+
/** What writing a store's links into the site's layout came to. */
|
|
34
|
+
export type AiStoreLinksLayoutDraft = {
|
|
35
|
+
status: 'written';
|
|
36
|
+
layoutId: string;
|
|
37
|
+
versionId: string;
|
|
38
|
+
name: string;
|
|
39
|
+
}
|
|
40
|
+
/** Every link was already there. */
|
|
41
|
+
| {
|
|
42
|
+
status: 'present';
|
|
43
|
+
layoutId: string;
|
|
44
|
+
}
|
|
45
|
+
/** The layout has no header list or no footer list to add to: what it had room for, if anything, was written. */
|
|
46
|
+
| {
|
|
47
|
+
status: 'unrecognized';
|
|
48
|
+
layoutId: string;
|
|
49
|
+
versionId: string | null;
|
|
50
|
+
name: string;
|
|
51
|
+
}
|
|
52
|
+
/** The layout is gone, or has no current version to start from. */
|
|
53
|
+
| {
|
|
54
|
+
status: 'gone';
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Writes the draft version (idempotent: a version with this id already
|
|
58
|
+
* written is that draft). Reads the layout's current version and writes the
|
|
59
|
+
* new one beside it, a copy of it — its fields, everything the editor stored
|
|
60
|
+
* — with the links added.
|
|
61
|
+
*/
|
|
62
|
+
export declare function writeAiStoreLinksLayoutDraft(firestore: Firestore, input: {
|
|
63
|
+
hostId: string;
|
|
64
|
+
layoutId: string;
|
|
65
|
+
versionId: string;
|
|
66
|
+
links: AiStoreLayoutLinks;
|
|
67
|
+
screenPaths: Readonly<Record<string, string>>;
|
|
68
|
+
uid: string;
|
|
69
|
+
aiJobId: string;
|
|
70
|
+
now: Date;
|
|
71
|
+
}): Promise<AiStoreLinksLayoutDraft>;
|
|
@@ -0,0 +1,95 @@
|
|
|
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 { decodeStoredNodes, encodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
|
|
18
|
+
import { aiLayoutWithStoreLinks } from "../model/ai-layout-store-links.js";
|
|
19
|
+
/**
|
|
20
|
+
* A store's links in the site's own layout, as a DRAFT (AGL-3676): a build
|
|
21
|
+
* that makes the site a store and builds no layout writes a new version of
|
|
22
|
+
* the layout its store pages render inside, with Account (and Cart, where
|
|
23
|
+
* the header has none) in the header and the account and policies in the
|
|
24
|
+
* footer (`aiLayoutWithStoreLinks`). The layout's current version — the one
|
|
25
|
+
* visitors see — is never touched: the new version waits beside it, and goes
|
|
26
|
+
* live only when the build publishes (`aiPublishGuidedSite`'s `storeLinks`),
|
|
27
|
+
* as every draft the build makes does.
|
|
28
|
+
*/ /** The field a draft version keeps the version it was made from in, so a publish knows whether it is still current. */ export const AI_STORE_LINKS_SOURCE_FIELD = 'aiStoreLinksSourceVersionId';
|
|
29
|
+
/** What a store links draft version is called in the layout's versions. */ export const AI_STORE_LINKS_VERSION_NAME = 'Account and policy links';
|
|
30
|
+
/**
|
|
31
|
+
* Writes the draft version (idempotent: a version with this id already
|
|
32
|
+
* written is that draft). Reads the layout's current version and writes the
|
|
33
|
+
* new one beside it, a copy of it — its fields, everything the editor stored
|
|
34
|
+
* — with the links added.
|
|
35
|
+
*/ export async function writeAiStoreLinksLayoutDraft(firestore, input) {
|
|
36
|
+
var _layout_get;
|
|
37
|
+
const layoutRef = firestore.collection('hosts').doc(input.hostId).collection('layouts').doc(input.layoutId);
|
|
38
|
+
const layout = await layoutRef.get();
|
|
39
|
+
const currentVersionId = layout.get('versionId');
|
|
40
|
+
if (!layout.exists || layout.get('deletedAt') != null || typeof currentVersionId !== 'string' || !currentVersionId) {
|
|
41
|
+
return {
|
|
42
|
+
status: 'gone'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const name = String((_layout_get = layout.get('displayName')) != null ? _layout_get : '') || 'Layout';
|
|
46
|
+
const draftRef = layoutRef.collection('versions').doc(input.versionId);
|
|
47
|
+
const already = await draftRef.get();
|
|
48
|
+
if (already.exists) return {
|
|
49
|
+
status: 'written',
|
|
50
|
+
layoutId: input.layoutId,
|
|
51
|
+
versionId: input.versionId,
|
|
52
|
+
name
|
|
53
|
+
};
|
|
54
|
+
const current = await layoutRef.collection('versions').doc(currentVersionId).get();
|
|
55
|
+
const nodes = current.exists ? decodeStoredNodes(current.get('nodes')) : null;
|
|
56
|
+
if (!nodes) return {
|
|
57
|
+
status: 'gone'
|
|
58
|
+
};
|
|
59
|
+
const result = aiLayoutWithStoreLinks(nodes, input.links, input.screenPaths);
|
|
60
|
+
const recognized = result.header && result.footer;
|
|
61
|
+
const packed = result.nodes ? encodeStoredNodes(result.nodes) : null;
|
|
62
|
+
if (!packed) return recognized ? {
|
|
63
|
+
status: 'present',
|
|
64
|
+
layoutId: input.layoutId
|
|
65
|
+
} : {
|
|
66
|
+
status: 'unrecognized',
|
|
67
|
+
layoutId: input.layoutId,
|
|
68
|
+
versionId: null,
|
|
69
|
+
name
|
|
70
|
+
};
|
|
71
|
+
const copy = _extends({}, current.data());
|
|
72
|
+
delete copy['aiEditJobId'];
|
|
73
|
+
await draftRef.create(_extends({}, copy, {
|
|
74
|
+
nodes: Buffer.from(packed),
|
|
75
|
+
displayName: AI_STORE_LINKS_VERSION_NAME,
|
|
76
|
+
createdAt: input.now,
|
|
77
|
+
updatedAt: input.now,
|
|
78
|
+
createdBy: input.uid,
|
|
79
|
+
aiJobId: input.aiJobId,
|
|
80
|
+
[AI_STORE_LINKS_SOURCE_FIELD]: currentVersionId
|
|
81
|
+
}));
|
|
82
|
+
return recognized ? {
|
|
83
|
+
status: 'written',
|
|
84
|
+
layoutId: input.layoutId,
|
|
85
|
+
versionId: input.versionId,
|
|
86
|
+
name
|
|
87
|
+
} : {
|
|
88
|
+
status: 'unrecognized',
|
|
89
|
+
layoutId: input.layoutId,
|
|
90
|
+
versionId: input.versionId,
|
|
91
|
+
name
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=ai-job-store-links-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-store-links-layout.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 { decodeStoredNodes, encodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport type { Firestore } from 'firebase-admin/firestore'\nimport { aiLayoutWithStoreLinks, type AiStoreLayoutLinks } from '../model/ai-layout-store-links'\n\n/**\n * A store's links in the site's own layout, as a DRAFT (AGL-3676): a build\n * that makes the site a store and builds no layout writes a new version of\n * the layout its store pages render inside, with Account (and Cart, where\n * the header has none) in the header and the account and policies in the\n * footer (`aiLayoutWithStoreLinks`). The layout's current version — the one\n * visitors see — is never touched: the new version waits beside it, and goes\n * live only when the build publishes (`aiPublishGuidedSite`'s `storeLinks`),\n * as every draft the build makes does.\n */\n\n/** The field a draft version keeps the version it was made from in, so a publish knows whether it is still current. */\nexport const AI_STORE_LINKS_SOURCE_FIELD = 'aiStoreLinksSourceVersionId'\n\n/** What a store links draft version is called in the layout's versions. */\nexport const AI_STORE_LINKS_VERSION_NAME = 'Account and policy links'\n\n/** What writing a store's links into the site's layout came to. */\nexport type AiStoreLinksLayoutDraft =\n | { status: 'written'; layoutId: string; versionId: string; name: string }\n /** Every link was already there. */\n | { status: 'present'; layoutId: string }\n /** The layout has no header list or no footer list to add to: what it had room for, if anything, was written. */\n | { status: 'unrecognized'; layoutId: string; versionId: string | null; name: string }\n /** The layout is gone, or has no current version to start from. */\n | { status: 'gone' }\n\n/**\n * Writes the draft version (idempotent: a version with this id already\n * written is that draft). Reads the layout's current version and writes the\n * new one beside it, a copy of it — its fields, everything the editor stored\n * — with the links added.\n */\nexport async function writeAiStoreLinksLayoutDraft(\n firestore: Firestore,\n input: {\n hostId: string\n layoutId: string\n versionId: string\n links: AiStoreLayoutLinks\n screenPaths: Readonly<Record<string, string>>\n uid: string\n aiJobId: string\n now: Date\n },\n): Promise<AiStoreLinksLayoutDraft> {\n const layoutRef = firestore.collection('hosts').doc(input.hostId).collection('layouts').doc(input.layoutId)\n const layout = await layoutRef.get()\n const currentVersionId = layout.get('versionId')\n if (!layout.exists || layout.get('deletedAt') != null || typeof currentVersionId !== 'string' || !currentVersionId) {\n return { status: 'gone' }\n }\n const name = String(layout.get('displayName') ?? '') || 'Layout'\n const draftRef = layoutRef.collection('versions').doc(input.versionId)\n const already = await draftRef.get()\n if (already.exists) return { status: 'written', layoutId: input.layoutId, versionId: input.versionId, name }\n const current = await layoutRef.collection('versions').doc(currentVersionId).get()\n const nodes = current.exists ? decodeStoredNodes<Record<string, unknown>>(current.get('nodes')) : null\n if (!nodes) return { status: 'gone' }\n const result = aiLayoutWithStoreLinks(nodes, input.links, input.screenPaths)\n const recognized = result.header && result.footer\n const packed = result.nodes ? encodeStoredNodes(result.nodes as never) : null\n if (!packed) return recognized ? { status: 'present', layoutId: input.layoutId } : { status: 'unrecognized', layoutId: input.layoutId, versionId: null, name }\n const copy = { ...(current.data() as Record<string, unknown>) }\n delete copy['aiEditJobId']\n await draftRef.create({\n ...copy,\n nodes: Buffer.from(packed),\n displayName: AI_STORE_LINKS_VERSION_NAME,\n createdAt: input.now,\n updatedAt: input.now,\n createdBy: input.uid,\n aiJobId: input.aiJobId,\n [AI_STORE_LINKS_SOURCE_FIELD]: currentVersionId,\n })\n return recognized\n ? { status: 'written', layoutId: input.layoutId, versionId: input.versionId, name }\n : { status: 'unrecognized', layoutId: input.layoutId, versionId: input.versionId, name }\n}\n"],"names":["decodeStoredNodes","encodeStoredNodes","aiLayoutWithStoreLinks","AI_STORE_LINKS_SOURCE_FIELD","AI_STORE_LINKS_VERSION_NAME","writeAiStoreLinksLayoutDraft","firestore","input","layout","layoutRef","collection","doc","hostId","layoutId","get","currentVersionId","exists","status","name","String","draftRef","versionId","already","current","nodes","result","links","screenPaths","recognized","header","footer","packed","copy","data","create","Buffer","from","displayName","createdAt","now","updatedAt","createdBy","uid","aiJobId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,EAAEC,iBAAiB,QAAQ,sCAAqC;AAE1F,SAASC,sBAAsB,QAAiC,oCAAgC;AAEhG;;;;;;;;;CASC,GAED,qHAAqH,GACrH,OAAO,MAAMC,8BAA8B,8BAA6B;AAExE,yEAAyE,GACzE,OAAO,MAAMC,8BAA8B,2BAA0B;AAYrE;;;;;CAKC,GACD,OAAO,eAAeC,6BACpBC,SAAoB,EACpBC,KASC;QAQmBC;IANpB,MAAMC,YAAYH,UAAUI,UAAU,CAAC,SAASC,GAAG,CAACJ,MAAMK,MAAM,EAAEF,UAAU,CAAC,WAAWC,GAAG,CAACJ,MAAMM,QAAQ;IAC1G,MAAML,SAAS,MAAMC,UAAUK,GAAG;IAClC,MAAMC,mBAAmBP,OAAOM,GAAG,CAAC;IACpC,IAAI,CAACN,OAAOQ,MAAM,IAAIR,OAAOM,GAAG,CAAC,gBAAgB,QAAQ,OAAOC,qBAAqB,YAAY,CAACA,kBAAkB;QAClH,OAAO;YAAEE,QAAQ;QAAO;IAC1B;IACA,MAAMC,OAAOC,QAAOX,cAAAA,OAAOM,GAAG,CAAC,0BAAXN,cAA6B,OAAO;IACxD,MAAMY,WAAWX,UAAUC,UAAU,CAAC,YAAYC,GAAG,CAACJ,MAAMc,SAAS;IACrE,MAAMC,UAAU,MAAMF,SAASN,GAAG;IAClC,IAAIQ,QAAQN,MAAM,EAAE,OAAO;QAAEC,QAAQ;QAAWJ,UAAUN,MAAMM,QAAQ;QAAEQ,WAAWd,MAAMc,SAAS;QAAEH;IAAK;IAC3G,MAAMK,UAAU,MAAMd,UAAUC,UAAU,CAAC,YAAYC,GAAG,CAACI,kBAAkBD,GAAG;IAChF,MAAMU,QAAQD,QAAQP,MAAM,GAAGhB,kBAA2CuB,QAAQT,GAAG,CAAC,YAAY;IAClG,IAAI,CAACU,OAAO,OAAO;QAAEP,QAAQ;IAAO;IACpC,MAAMQ,SAASvB,uBAAuBsB,OAAOjB,MAAMmB,KAAK,EAAEnB,MAAMoB,WAAW;IAC3E,MAAMC,aAAaH,OAAOI,MAAM,IAAIJ,OAAOK,MAAM;IACjD,MAAMC,SAASN,OAAOD,KAAK,GAAGvB,kBAAkBwB,OAAOD,KAAK,IAAa;IACzE,IAAI,CAACO,QAAQ,OAAOH,aAAa;QAAEX,QAAQ;QAAWJ,UAAUN,MAAMM,QAAQ;IAAC,IAAI;QAAEI,QAAQ;QAAgBJ,UAAUN,MAAMM,QAAQ;QAAEQ,WAAW;QAAMH;IAAK;IAC7J,MAAMc,OAAO,aAAMT,QAAQU,IAAI;IAC/B,OAAOD,IAAI,CAAC,cAAc;IAC1B,MAAMZ,SAASc,MAAM,CAAC,aACjBF;QACHR,OAAOW,OAAOC,IAAI,CAACL;QACnBM,aAAajC;QACbkC,WAAW/B,MAAMgC,GAAG;QACpBC,WAAWjC,MAAMgC,GAAG;QACpBE,WAAWlC,MAAMmC,GAAG;QACpBC,SAASpC,MAAMoC,OAAO;QACtB,CAACxC,4BAA4B,EAAEY;;IAEjC,OAAOa,aACH;QAAEX,QAAQ;QAAWJ,UAAUN,MAAMM,QAAQ;QAAEQ,WAAWd,MAAMc,SAAS;QAAEH;IAAK,IAChF;QAAED,QAAQ;QAAgBJ,UAAUN,MAAMM,QAAQ;QAAEQ,WAAWd,MAAMc,SAAS;QAAEH;IAAK;AAC3F"}
|
|
@@ -71,6 +71,17 @@ import { type AiLayoutPictureSlot, type AiLayoutPictureSource } from '../layout-
|
|
|
71
71
|
* ranked: the object in the hit's first tags (what the photo is OF) over
|
|
72
72
|
* a mention in its last, the subject's other words, the domain. Among
|
|
73
73
|
* equals the job's seed picks, so two sites differ and one job repeats.
|
|
74
|
+
* What the beta.239 Ember & Oak start added: "hand-poured", "handmade"
|
|
75
|
+
* and "by hand" never make "hands" a subject, and a story of the craft
|
|
76
|
+
* asks for the maker at work ("candle making"); a hit carrying a topic
|
|
77
|
+
* the brief never named (a cross, a grave, a flag, a gun, wine, a child,
|
|
78
|
+
* a mannequin) never fills anything; a product, and a collection tile,
|
|
79
|
+
* take only a hit naming its head noun among the first tags, held to the
|
|
80
|
+
* shop's category; and an object whose word is not enough ("melt") is
|
|
81
|
+
* named and searched by its kin ("wax melt", "melt warmer"), never by the
|
|
82
|
+
* category alone. And from the Kiln & Clover portfolio: nudity never fills
|
|
83
|
+
* anything, for any business; a hit naming a rival craft (glass, for a
|
|
84
|
+
* potter) counts only when it names the craft's own words too.
|
|
74
85
|
* 4. **Never twice.** A photo is placed once per job: not twice on a page,
|
|
75
86
|
* and not on a page when another page (or another pass, in this process)
|
|
76
87
|
* of the same job already placed it. The caller hands in what the job's
|
|
@@ -103,6 +114,12 @@ export declare const AI_LAYOUT_STOCK_MAX_BYTES: number;
|
|
|
103
114
|
* written to, whose photos a page does not place again (AGL-3660).
|
|
104
115
|
*/
|
|
105
116
|
export declare const AI_STOCK_PHOTO_PAGES_INPUT = "stockPhotoPages";
|
|
117
|
+
/**
|
|
118
|
+
* The unit job input naming library photos (`media:` references) another
|
|
119
|
+
* part of the job already shows outside its pages — a dataset's records
|
|
120
|
+
* (AGL-3616) — which a page does not place again.
|
|
121
|
+
*/
|
|
122
|
+
export declare const AI_STOCK_PHOTO_AVOID_INPUT = "stockPhotoAvoid";
|
|
106
123
|
/**
|
|
107
124
|
* A word's singular, for matching only: "bowls" and "bowl", "vases" and
|
|
108
125
|
* "vase", "berries" and "berry", "dishes" and "dish" meet. Both sides of a
|
|
@@ -140,6 +157,15 @@ export interface AiStockSiteTerms {
|
|
|
140
157
|
domain: string[];
|
|
141
158
|
/** The searches for the site's world itself, broadest last. */
|
|
142
159
|
broad: string[];
|
|
160
|
+
/** The searches for its maker at work, for a story of the craft (AGL-3660); empty for a craft not listed. */
|
|
161
|
+
making: string[];
|
|
162
|
+
/** Every stem of the business type or brief: what lets a sensitive topic in ({@link SENSITIVE_TOPICS}). */
|
|
163
|
+
named: string[];
|
|
164
|
+
/** Rival crafts' words, each with the words that must then be named too ({@link CRAFT_KIN}). */
|
|
165
|
+
rivals: Array<{
|
|
166
|
+
words: string[];
|
|
167
|
+
core: string[];
|
|
168
|
+
}>;
|
|
143
169
|
}
|
|
144
170
|
/** The site's terms, read from its business type. */
|
|
145
171
|
export declare function aiStockSiteTerms(businessType: string): AiStockSiteTerms;
|
|
@@ -170,7 +196,21 @@ export interface AiStockSearch extends StockPhotoSearchRequest {
|
|
|
170
196
|
subject?: string;
|
|
171
197
|
/** A word every hit must name: a product's photo names the shop's category (AGL-3676). */
|
|
172
198
|
requires?: string;
|
|
199
|
+
/** The thing every hit must name, where it is not the subject's own: a product's head noun (AGL-3660). */
|
|
200
|
+
object?: string;
|
|
201
|
+
/** Phrases that name the object where its own word is not enough: "wax melt" ({@link OBJECT_KIN}). */
|
|
202
|
+
kin?: readonly string[];
|
|
203
|
+
/** Whether the object must be among the hit's first tags, what the photo is OF (AGL-3660). */
|
|
204
|
+
lead?: boolean;
|
|
173
205
|
}
|
|
206
|
+
/** A slot's part of a page, where it asks for more than its role: a collection tile or a story of the craft (AGL-3660). */
|
|
207
|
+
export type AiStockSlotPart = 'collection' | 'story';
|
|
208
|
+
/**
|
|
209
|
+
* A slot's part (AGL-3660): a tile of a collections band is product-like,
|
|
210
|
+
* held to the shop's category and its own object; a picture of a story of
|
|
211
|
+
* the craft, or of a thing made by hand, shows the maker at work.
|
|
212
|
+
*/
|
|
213
|
+
export declare function aiStockSlotPart(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt'>, sectionName: string): AiStockSlotPart | undefined;
|
|
174
214
|
/**
|
|
175
215
|
* A product's searches (AGL-3676), most specific first: each of its subjects
|
|
176
216
|
* — its name's noun phrase, then its photo's — with the shop's category
|
|
@@ -179,10 +219,17 @@ export interface AiStockSearch extends StockPhotoSearchRequest {
|
|
|
179
219
|
* hit naming the category, so no product is ever filled with a lifestyle shot
|
|
180
220
|
* of something else: the beta.237 Willow Wick start put a laptop and roses
|
|
181
221
|
* under "Candle Wick Trimmer" and an antique tea set under "Candle Gift Set".
|
|
222
|
+
*
|
|
223
|
+
* And every one must find a hit naming the product's own head noun among its
|
|
224
|
+
* first tags (AGL-3660): the beta.239 Ember & Oak start put a bed with a book,
|
|
225
|
+
* a mug and a camera (its candle a last tag) under "Hand-Poured Soy Candle".
|
|
226
|
+
* An object whose own word is not enough ({@link OBJECT_KIN}) is searched by
|
|
227
|
+
* its kin instead of the category alone, and named by them: a single taper
|
|
228
|
+
* candle filled "Wax Melt Gift Set".
|
|
182
229
|
*/
|
|
183
|
-
export declare function aiStockProductSearches(slot: Pick<AiLayoutPictureSlot, 'aspect' | 'product'>, craft: string): AiStockSearch[];
|
|
230
|
+
export declare function aiStockProductSearches(slot: Pick<AiLayoutPictureSlot, 'aspect' | 'product'>, craft: string, domain?: readonly string[]): AiStockSearch[];
|
|
184
231
|
/** The searches a slot tries, in order, most specific first, each distinct. */
|
|
185
|
-
export declare function aiStockSearchesFor(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect'>, terms: Pick<AiStockSiteTerms, 'business' | 'craft' | 'domain' | 'broad'
|
|
232
|
+
export declare function aiStockSearchesFor(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect'>, terms: Pick<AiStockSiteTerms, 'business' | 'craft' | 'domain' | 'broad'> & Partial<Pick<AiStockSiteTerms, 'making'>>, subject: string, part?: AiStockSlotPart): AiStockSearch[];
|
|
186
233
|
/** What a hit is judged by. */
|
|
187
234
|
export interface AiStockJudgement {
|
|
188
235
|
/** The words the picture is of; empty for a picture of the site's world. */
|
|
@@ -196,10 +243,29 @@ export interface AiStockJudgement {
|
|
|
196
243
|
strict?: boolean;
|
|
197
244
|
/** A word every hit must name: a product's photo names the shop's category (AGL-3676). */
|
|
198
245
|
requires?: string;
|
|
246
|
+
/** The thing a hit must name, where it is not the subject's own (AGL-3660). */
|
|
247
|
+
object?: string;
|
|
248
|
+
/** Phrases naming the object where its word is not enough; one of them is then required, and stands for the category. */
|
|
249
|
+
kin?: readonly string[];
|
|
250
|
+
/** Whether the object must be among the hit's first tags (AGL-3660). */
|
|
251
|
+
lead?: boolean;
|
|
252
|
+
/** The stems of the site's business type or brief, which let a sensitive topic in ({@link AiStockSiteTerms.named}). */
|
|
253
|
+
named?: readonly string[];
|
|
254
|
+
/** Rival crafts: a hit naming one is rejected unless it names the site's core words ({@link AiStockSiteTerms.rivals}). */
|
|
255
|
+
rivals?: ReadonlyArray<{
|
|
256
|
+
words: readonly string[];
|
|
257
|
+
core: readonly string[];
|
|
258
|
+
}>;
|
|
199
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Whether a hit carries a topic its site never named ({@link SENSITIVE_TOPICS}):
|
|
262
|
+
* a cross, a grave, a flag, a gun, a glass of wine, a child, a mannequin.
|
|
263
|
+
*/
|
|
264
|
+
export declare function aiStockSensitive(photo: Pick<StockPhoto, 'tags' | 'alt' | 'pageUrl'>, named?: readonly string[]): boolean;
|
|
200
265
|
/**
|
|
201
266
|
* How well a hit shows what a slot needs (AGL-3660), or 0 when it must not
|
|
202
|
-
* fill it: a mockup or a blank never; a
|
|
267
|
+
* fill it: a mockup or a blank never; a topic the site never named never
|
|
268
|
+
* ({@link aiStockSensitive}); a picture of a thing only when the hit
|
|
203
269
|
* names its object AND either the site's world or the subject's own
|
|
204
270
|
* two-word phrase ("bud vase", "wick trimmer"); anything else only when it
|
|
205
271
|
* names the site's world (when the site names one). Among the accepted, the
|