@aglyn/plugins-ai 1.0.0-beta.266 → 1.0.0-beta.269
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 +18 -18
- package/src/lib/components/ai-job-site.d.ts +8 -1
- package/src/lib/components/ai-job-site.js +7 -0
- package/src/lib/components/ai-job-site.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +37 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +25 -6
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +8 -2
- package/src/lib/components/ai-site-start-steps.component.js +55 -26
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-start-tracking.d.ts +11 -3
- package/src/lib/components/ai-site-start-tracking.js +28 -9
- package/src/lib/components/ai-site-start-tracking.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +24 -2
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/jobs/ai-empty-site-sweep.d.ts +154 -0
- package/src/lib/jobs/ai-empty-site-sweep.js +346 -0
- package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +7 -1
- package/src/lib/jobs/ai-job-page-language.js +16 -3
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-site-look.d.ts +2 -1
- package/src/lib/jobs/ai-job-site-look.js +6 -2
- package/src/lib/jobs/ai-job-site-look.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +11 -6
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-beat.d.ts +11 -3
- package/src/lib/jobs/ai-jobs-beat.js +23 -6
- package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +46 -7
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +43 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +735 -22
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.d.ts +35 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.js +379 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.d.ts +35 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.js +294 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-mexicano-recordings.d.ts +31 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-mexicano-recordings.js +325 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-mexicano-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-design.js +5 -0
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-motion.d.ts +60 -7
- package/src/lib/layout-language/ai-layout-motion.js +231 -4
- package/src/lib/layout-language/ai-layout-motion.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +8 -0
- package/src/lib/layout-language/ai-layout-pictures.js +68 -4
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +20 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +10 -1
- package/src/lib/model/ai-site-look.js +14 -3
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-start-handoff.d.ts +49 -0
- package/src/lib/model/ai-site-start-handoff.js +88 -0
- package/src/lib/model/ai-site-start-handoff.js.map +1 -0
|
@@ -0,0 +1,346 @@
|
|
|
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 { erasePluginSite } from "@aglyn/aglyn/plugin-manager/plugin-site-erase";
|
|
18
|
+
/**
|
|
19
|
+
* THE EMPTY SITE A GUIDED START LEFT IS REMOVED (AGL-3936).
|
|
20
|
+
*
|
|
21
|
+
* Since AGL-3859 a site is created only when its start is admitted: the
|
|
22
|
+
* starter, Skip, or "Plan my site". A site start cancelled or failed before
|
|
23
|
+
* it built anything leaves that site with no page, public, answering "This
|
|
24
|
+
* site is coming soon". The build page offers Start again, Try again and Use
|
|
25
|
+
* the starter, and a person who leaves without choosing used to leave the
|
|
26
|
+
* placeholder behind for good (kc-x-carder 2026-10-15, cancelled 13 s in;
|
|
27
|
+
* pflomo 2026-10-17, 35 s in).
|
|
28
|
+
*
|
|
29
|
+
* So when a site job settles `canceled` or `failed` with nothing built, a due
|
|
30
|
+
* marker is written, `aiEmptySiteSweeps/{hostId}`, an hour out. The AI jobs
|
|
31
|
+
* beat reads the markers that are due, a few a beat, and checks every
|
|
32
|
+
* condition again at that moment; only a site that is still truly empty is
|
|
33
|
+
* deleted, through `core.site-erase` — the console's own delete path, so the
|
|
34
|
+
* subdomain, the routing index, the org's site count, the members' rows and
|
|
35
|
+
* the cache all go as they do for a site its admin deletes. Anything a person
|
|
36
|
+
* did in the hour keeps the site and drops the marker. The starter is never
|
|
37
|
+
* written for them: the starter is written only when a person chooses it.
|
|
38
|
+
*
|
|
39
|
+
* The marker is server-only (the rules deny every client), holds ids and
|
|
40
|
+
* instants and nothing a person wrote, and is deleted once it is decided.
|
|
41
|
+
*/ /** The due markers, top level, one per site, keyed by the site's id. */ export const AI_EMPTY_SITE_SWEEPS_COLLECTION = 'aiEmptySiteSweeps';
|
|
42
|
+
/** How long a person has to come back and choose before the empty site goes. */ export const AI_EMPTY_SITE_GRACE_MS = 60 * 60000;
|
|
43
|
+
/** Markers one beat decides; the rest wait for the next minute's beat. */ export const AI_EMPTY_SITE_SWEEP_MAX = 3;
|
|
44
|
+
/** Wall clock one beat gives the removals before it runs any job's step. */ export const AI_EMPTY_SITE_SWEEP_BUDGET_MS = 20000;
|
|
45
|
+
/** A removal the eraser could not make is tried again this much later. */ export const AI_EMPTY_SITE_RETRY_MS = 15 * 60000;
|
|
46
|
+
/** Removals tried this many times without an answer are given up and logged. */ export const AI_EMPTY_SITE_MAX_ATTEMPTS = 5;
|
|
47
|
+
/** The workspace feed's words for it; the feed names the site beside them. */ export const AI_EMPTY_SITE_ACTIVITY = 'Removed an empty site';
|
|
48
|
+
/** Why, on the staff audit row. */ export const AI_EMPTY_SITE_REASON = 'ai-start-empty';
|
|
49
|
+
/** The org's jobs, as `AI_JOBS_COLLECTION` in `ai-jobs.ts` names them (that module imports this one). */ const JOBS_COLLECTION = 'aiJobs';
|
|
50
|
+
/**
|
|
51
|
+
* What a site holds that a person, or a start, put there. One document in
|
|
52
|
+
* any of them keeps the site: pages and emails (`screens`), layouts, forms,
|
|
53
|
+
* media, components, templates, content collections, redirects and plugin
|
|
54
|
+
* settings. `activity` and the visitors' `analytics` beacon are not on the
|
|
55
|
+
* list, and are read on their own terms below.
|
|
56
|
+
*/ export const AI_EMPTY_SITE_CONTENT_COLLECTIONS = [
|
|
57
|
+
'screens',
|
|
58
|
+
'layouts',
|
|
59
|
+
'forms',
|
|
60
|
+
'media',
|
|
61
|
+
'components',
|
|
62
|
+
'templates',
|
|
63
|
+
'collections',
|
|
64
|
+
'redirects',
|
|
65
|
+
'pluginSettings'
|
|
66
|
+
];
|
|
67
|
+
/** The site's own feed entries that are not a person editing it. */ const UNEDITED_ACTIVITY = new Set([
|
|
68
|
+
'Created the site'
|
|
69
|
+
]);
|
|
70
|
+
/** More feed entries than this and the site was worked on. */ const ACTIVITY_SCAN = 20;
|
|
71
|
+
function toMillis(value) {
|
|
72
|
+
if (value === null || value === undefined) return null;
|
|
73
|
+
if (value instanceof Date) return value.getTime();
|
|
74
|
+
if (typeof value === 'number') return value;
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
const parsed = Date.parse(value);
|
|
77
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
78
|
+
}
|
|
79
|
+
return typeof value.toMillis === 'function' ? value.toMillis() : null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether a job leaves the empty site this sweep is for: a site start that
|
|
83
|
+
* ended `canceled` or `failed` with nothing built — no output but the
|
|
84
|
+
* site's listing proposal, which rides out with the first unit and builds
|
|
85
|
+
* nothing (the rule `aiSiteStarterFallbackOffered` offers the starter by).
|
|
86
|
+
*/ export function aiJobLeavesEmptySite(job) {
|
|
87
|
+
var _job_outputs;
|
|
88
|
+
if (job.kind !== 'site' || !job.hostId || job.siteRemoved) return false;
|
|
89
|
+
if (job.status !== 'canceled' && job.status !== 'failed') return false;
|
|
90
|
+
return ((_job_outputs = job.outputs) != null ? _job_outputs : []).every((output)=>output.resource === 'seo');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Writes the due marker for a job that just ended, when it leaves an empty
|
|
94
|
+
* site. Never throws: a marker that could not be written leaves the site as
|
|
95
|
+
* it was before this existed, and must not fail the write it follows.
|
|
96
|
+
*/ export async function scheduleAiEmptySiteSweep(firestore, job, now = new Date()) {
|
|
97
|
+
if (!aiJobLeavesEmptySite(job) || !job.hostId) return false;
|
|
98
|
+
const marker = {
|
|
99
|
+
hostId: job.hostId,
|
|
100
|
+
orgId: job.orgId,
|
|
101
|
+
jobId: job.$id,
|
|
102
|
+
status: job.status,
|
|
103
|
+
settledAt: now,
|
|
104
|
+
dueAt: new Date(now.getTime() + AI_EMPTY_SITE_GRACE_MS),
|
|
105
|
+
attempts: 0
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
await firestore.collection(AI_EMPTY_SITE_SWEEPS_COLLECTION).doc(job.hostId).set(marker);
|
|
109
|
+
return true;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn('ai empty site: the removal could not be scheduled', {
|
|
112
|
+
orgId: job.orgId,
|
|
113
|
+
jobId: job.$id,
|
|
114
|
+
hostId: job.hostId,
|
|
115
|
+
error: error instanceof Error ? error.message : String(error)
|
|
116
|
+
});
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Drops a site's marker: a job started, resumed or retried on the site, so
|
|
122
|
+
* it is not empty for long. Never throws; the beat's check at due time says
|
|
123
|
+
* the same thing if this write is lost.
|
|
124
|
+
*/ export async function clearAiEmptySiteSweep(firestore, hostId) {
|
|
125
|
+
if (!hostId) return;
|
|
126
|
+
try {
|
|
127
|
+
await firestore.collection(AI_EMPTY_SITE_SWEEPS_COLLECTION).doc(hostId).delete();
|
|
128
|
+
} catch (unused) {
|
|
129
|
+
// A marker left behind is decided at its due time, against the site as it is then.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Every condition, read again as the site is now (AGL-3936). The site is
|
|
134
|
+
* removed only when all of them hold:
|
|
135
|
+
*
|
|
136
|
+
* - it exists, in the marker's workspace, with no custom domain, no sending
|
|
137
|
+
* domain, no page on its routing map, and no staff hold;
|
|
138
|
+
* - the job is still the one that ended, still `canceled` or `failed`, and
|
|
139
|
+
* still built nothing — not resumed, retried or finished since;
|
|
140
|
+
* - no other AI job names the site;
|
|
141
|
+
* - nothing in {@link AI_EMPTY_SITE_CONTENT_COLLECTIONS}: no page, email,
|
|
142
|
+
* layout, form or media file;
|
|
143
|
+
* - the site's own feed holds nothing but "Created the site" and the AI
|
|
144
|
+
* job's own `ai.job.*` entries: nobody edited it.
|
|
145
|
+
*/ export async function aiEmptySiteVerdict(firestore, marker) {
|
|
146
|
+
const hostRef = firestore.collection('hosts').doc(marker.hostId);
|
|
147
|
+
const host = await hostRef.get();
|
|
148
|
+
if (!host.exists) return {
|
|
149
|
+
kind: 'keep',
|
|
150
|
+
reason: 'site-gone'
|
|
151
|
+
};
|
|
152
|
+
if (host.get('orgId') !== marker.orgId) return {
|
|
153
|
+
kind: 'keep',
|
|
154
|
+
reason: 'site-moved'
|
|
155
|
+
};
|
|
156
|
+
const text = (field)=>{
|
|
157
|
+
var _host_get;
|
|
158
|
+
return String((_host_get = host.get(field)) != null ? _host_get : '').trim();
|
|
159
|
+
};
|
|
160
|
+
if (text('cname') || host.get('hasCustomDomain') === true) return {
|
|
161
|
+
kind: 'keep',
|
|
162
|
+
reason: 'custom-domain'
|
|
163
|
+
};
|
|
164
|
+
if (text('sendingLabel')) return {
|
|
165
|
+
kind: 'keep',
|
|
166
|
+
reason: 'sending-domain'
|
|
167
|
+
};
|
|
168
|
+
if (host.get('suspended') === true || host.get('suspendedAt')) return {
|
|
169
|
+
kind: 'keep',
|
|
170
|
+
reason: 'staff-hold'
|
|
171
|
+
};
|
|
172
|
+
const routes = host.get('screens');
|
|
173
|
+
if (routes && typeof routes === 'object' && Object.keys(routes).length > 0) {
|
|
174
|
+
return {
|
|
175
|
+
kind: 'keep',
|
|
176
|
+
reason: 'routed-pages'
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const jobSnapshot = await firestore.collection('orgs').doc(marker.orgId).collection(JOBS_COLLECTION).doc(marker.jobId).get();
|
|
180
|
+
if (!jobSnapshot.exists) return {
|
|
181
|
+
kind: 'keep',
|
|
182
|
+
reason: 'job-gone'
|
|
183
|
+
};
|
|
184
|
+
const job = _extends({}, jobSnapshot.data(), {
|
|
185
|
+
$id: jobSnapshot.id
|
|
186
|
+
});
|
|
187
|
+
if (job.hostId !== marker.hostId || !aiJobLeavesEmptySite(job)) return {
|
|
188
|
+
kind: 'keep',
|
|
189
|
+
reason: 'job-moved-on'
|
|
190
|
+
};
|
|
191
|
+
const settledMs = toMillis(job.updatedAt);
|
|
192
|
+
const markedMs = marker.dueAt.getTime() - AI_EMPTY_SITE_GRACE_MS;
|
|
193
|
+
if (settledMs !== null && settledMs > markedMs + 1000) {
|
|
194
|
+
return {
|
|
195
|
+
kind: 'later',
|
|
196
|
+
dueAt: new Date(settledMs + AI_EMPTY_SITE_GRACE_MS)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const [others, ...contents] = await Promise.all([
|
|
200
|
+
firestore.collection('orgs').doc(marker.orgId).collection(JOBS_COLLECTION).where('hostId', '==', marker.hostId).limit(2).get(),
|
|
201
|
+
...AI_EMPTY_SITE_CONTENT_COLLECTIONS.map((name)=>hostRef.collection(name).limit(1).get())
|
|
202
|
+
]);
|
|
203
|
+
if (others.docs.some((doc)=>doc.id !== marker.jobId)) return {
|
|
204
|
+
kind: 'keep',
|
|
205
|
+
reason: 'another-job'
|
|
206
|
+
};
|
|
207
|
+
const held = AI_EMPTY_SITE_CONTENT_COLLECTIONS.find((_, index)=>!contents[index].empty);
|
|
208
|
+
if (held) return {
|
|
209
|
+
kind: 'keep',
|
|
210
|
+
reason: `has-${held}`
|
|
211
|
+
};
|
|
212
|
+
const activity = await hostRef.collection('activity').limit(ACTIVITY_SCAN + 1).get();
|
|
213
|
+
if (activity.size > ACTIVITY_SCAN) return {
|
|
214
|
+
kind: 'keep',
|
|
215
|
+
reason: 'edited'
|
|
216
|
+
};
|
|
217
|
+
const edited = activity.docs.some((doc)=>{
|
|
218
|
+
var _doc_get;
|
|
219
|
+
const action = String((_doc_get = doc.get('action')) != null ? _doc_get : '');
|
|
220
|
+
return !UNEDITED_ACTIVITY.has(action) && !action.startsWith('ai.job.');
|
|
221
|
+
});
|
|
222
|
+
if (edited) return {
|
|
223
|
+
kind: 'keep',
|
|
224
|
+
reason: 'edited'
|
|
225
|
+
};
|
|
226
|
+
const name = host.get('displayName');
|
|
227
|
+
return {
|
|
228
|
+
kind: 'empty',
|
|
229
|
+
name: typeof name === 'string' && name ? name : null
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* One beat's removals (AGL-3936): the markers due now, oldest first, a few
|
|
234
|
+
* at a time and inside a few seconds of wall clock, each decided against the
|
|
235
|
+
* site as it is at that moment. The marker goes once it is decided; one the
|
|
236
|
+
* eraser could not answer is tried again later, and given up after
|
|
237
|
+
* {@link AI_EMPTY_SITE_MAX_ATTEMPTS}.
|
|
238
|
+
*/ export async function sweepAiEmptySites(options) {
|
|
239
|
+
var _options_now, _options_budgetMs, _options_erase, _options_max;
|
|
240
|
+
const now = (_options_now = options.now) != null ? _options_now : Date.now;
|
|
241
|
+
const startedAt = now();
|
|
242
|
+
const budgetMs = (_options_budgetMs = options.budgetMs) != null ? _options_budgetMs : AI_EMPTY_SITE_SWEEP_BUDGET_MS;
|
|
243
|
+
const erase = (_options_erase = options.erase) != null ? _options_erase : erasePluginSite;
|
|
244
|
+
const markers = options.firestore.collection(AI_EMPTY_SITE_SWEEPS_COLLECTION);
|
|
245
|
+
const due = await markers.where('dueAt', '<=', new Date(startedAt)).orderBy('dueAt', 'asc').limit((_options_max = options.max) != null ? _options_max : AI_EMPTY_SITE_SWEEP_MAX).get();
|
|
246
|
+
const result = {
|
|
247
|
+
due: due.size,
|
|
248
|
+
removed: 0,
|
|
249
|
+
kept: 0,
|
|
250
|
+
later: 0
|
|
251
|
+
};
|
|
252
|
+
for (const doc of due.docs){
|
|
253
|
+
var _stored_hostId, _stored_orgId, _stored_jobId, _toMillis, _stored_attempts;
|
|
254
|
+
if (now() - startedAt >= budgetMs) break;
|
|
255
|
+
const stored = doc.data();
|
|
256
|
+
const marker = {
|
|
257
|
+
hostId: String((_stored_hostId = stored.hostId) != null ? _stored_hostId : doc.id),
|
|
258
|
+
orgId: String((_stored_orgId = stored.orgId) != null ? _stored_orgId : ''),
|
|
259
|
+
jobId: String((_stored_jobId = stored.jobId) != null ? _stored_jobId : ''),
|
|
260
|
+
dueAt: new Date((_toMillis = toMillis(stored.dueAt)) != null ? _toMillis : startedAt),
|
|
261
|
+
attempts: Number((_stored_attempts = stored.attempts) != null ? _stored_attempts : 0)
|
|
262
|
+
};
|
|
263
|
+
try {
|
|
264
|
+
if (!marker.orgId || !marker.jobId) {
|
|
265
|
+
await doc.ref.delete();
|
|
266
|
+
result.kept += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const verdict = await aiEmptySiteVerdict(options.firestore, marker);
|
|
270
|
+
if (verdict.kind === 'keep') {
|
|
271
|
+
await doc.ref.delete();
|
|
272
|
+
result.kept += 1;
|
|
273
|
+
console.info('ai empty site kept', _extends({}, marker, {
|
|
274
|
+
reason: verdict.reason
|
|
275
|
+
}));
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (verdict.kind === 'later') {
|
|
279
|
+
await doc.ref.set({
|
|
280
|
+
dueAt: verdict.dueAt
|
|
281
|
+
}, {
|
|
282
|
+
merge: true
|
|
283
|
+
});
|
|
284
|
+
result.later += 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const erased = await erase({
|
|
288
|
+
hostId: marker.hostId,
|
|
289
|
+
orgId: marker.orgId,
|
|
290
|
+
activity: AI_EMPTY_SITE_ACTIVITY,
|
|
291
|
+
reason: AI_EMPTY_SITE_REASON
|
|
292
|
+
});
|
|
293
|
+
if (erased.erased) {
|
|
294
|
+
// Said on the job only once the site is gone, so its pages can say so.
|
|
295
|
+
await options.firestore.collection('orgs').doc(marker.orgId).collection(JOBS_COLLECTION).doc(marker.jobId).set({
|
|
296
|
+
siteRemoved: {
|
|
297
|
+
at: new Date(now()),
|
|
298
|
+
name: verdict.name
|
|
299
|
+
}
|
|
300
|
+
}, {
|
|
301
|
+
merge: true
|
|
302
|
+
});
|
|
303
|
+
await doc.ref.delete();
|
|
304
|
+
result.removed += 1;
|
|
305
|
+
console.info('ai empty site removed', marker);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const refused = 'reason' in erased ? erased.reason : 'unavailable';
|
|
309
|
+
if (refused !== 'unavailable') {
|
|
310
|
+
await doc.ref.delete();
|
|
311
|
+
result.kept += 1;
|
|
312
|
+
console.info('ai empty site kept', _extends({}, marker, {
|
|
313
|
+
reason: refused
|
|
314
|
+
}));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
await retryLater(doc.ref, marker, now());
|
|
318
|
+
result.later += 1;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error('ai empty site sweep failed', _extends({}, marker, {
|
|
321
|
+
error
|
|
322
|
+
}));
|
|
323
|
+
await retryLater(doc.ref, marker, now()).catch(()=>undefined);
|
|
324
|
+
result.later += 1;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
/** Moves a marker the eraser could not answer later, or gives it up. */ async function retryLater(ref, marker, nowMs) {
|
|
330
|
+
const attempts = marker.attempts + 1;
|
|
331
|
+
if (attempts >= AI_EMPTY_SITE_MAX_ATTEMPTS) {
|
|
332
|
+
console.error('ai empty site: removal given up', _extends({}, marker, {
|
|
333
|
+
attempts
|
|
334
|
+
}));
|
|
335
|
+
await ref.delete();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
await ref.set({
|
|
339
|
+
attempts,
|
|
340
|
+
dueAt: new Date(nowMs + AI_EMPTY_SITE_RETRY_MS)
|
|
341
|
+
}, {
|
|
342
|
+
merge: true
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
//# sourceMappingURL=ai-empty-site-sweep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-empty-site-sweep.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 erasePluginSite,\n type PluginSiteEraseRequest,\n type PluginSiteEraseResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-site-erase'\nimport type { AiJob } from '../model/ai-jobs.types'\n\n/**\n * THE EMPTY SITE A GUIDED START LEFT IS REMOVED (AGL-3936).\n *\n * Since AGL-3859 a site is created only when its start is admitted: the\n * starter, Skip, or \"Plan my site\". A site start cancelled or failed before\n * it built anything leaves that site with no page, public, answering \"This\n * site is coming soon\". The build page offers Start again, Try again and Use\n * the starter, and a person who leaves without choosing used to leave the\n * placeholder behind for good (kc-x-carder 2026-10-15, cancelled 13 s in;\n * pflomo 2026-10-17, 35 s in).\n *\n * So when a site job settles `canceled` or `failed` with nothing built, a due\n * marker is written, `aiEmptySiteSweeps/{hostId}`, an hour out. The AI jobs\n * beat reads the markers that are due, a few a beat, and checks every\n * condition again at that moment; only a site that is still truly empty is\n * deleted, through `core.site-erase` — the console's own delete path, so the\n * subdomain, the routing index, the org's site count, the members' rows and\n * the cache all go as they do for a site its admin deletes. Anything a person\n * did in the hour keeps the site and drops the marker. The starter is never\n * written for them: the starter is written only when a person chooses it.\n *\n * The marker is server-only (the rules deny every client), holds ids and\n * instants and nothing a person wrote, and is deleted once it is decided.\n */\n\n/** The due markers, top level, one per site, keyed by the site's id. */\nexport const AI_EMPTY_SITE_SWEEPS_COLLECTION = 'aiEmptySiteSweeps'\n\n/** How long a person has to come back and choose before the empty site goes. */\nexport const AI_EMPTY_SITE_GRACE_MS = 60 * 60_000\n\n/** Markers one beat decides; the rest wait for the next minute's beat. */\nexport const AI_EMPTY_SITE_SWEEP_MAX = 3\n\n/** Wall clock one beat gives the removals before it runs any job's step. */\nexport const AI_EMPTY_SITE_SWEEP_BUDGET_MS = 20_000\n\n/** A removal the eraser could not make is tried again this much later. */\nexport const AI_EMPTY_SITE_RETRY_MS = 15 * 60_000\n\n/** Removals tried this many times without an answer are given up and logged. */\nexport const AI_EMPTY_SITE_MAX_ATTEMPTS = 5\n\n/** The workspace feed's words for it; the feed names the site beside them. */\nexport const AI_EMPTY_SITE_ACTIVITY = 'Removed an empty site'\n\n/** Why, on the staff audit row. */\nexport const AI_EMPTY_SITE_REASON = 'ai-start-empty'\n\n/** The org's jobs, as `AI_JOBS_COLLECTION` in `ai-jobs.ts` names them (that module imports this one). */\nconst JOBS_COLLECTION = 'aiJobs'\n\n/**\n * What a site holds that a person, or a start, put there. One document in\n * any of them keeps the site: pages and emails (`screens`), layouts, forms,\n * media, components, templates, content collections, redirects and plugin\n * settings. `activity` and the visitors' `analytics` beacon are not on the\n * list, and are read on their own terms below.\n */\nexport const AI_EMPTY_SITE_CONTENT_COLLECTIONS = [\n 'screens',\n 'layouts',\n 'forms',\n 'media',\n 'components',\n 'templates',\n 'collections',\n 'redirects',\n 'pluginSettings',\n] as const\n\n/** The site's own feed entries that are not a person editing it. */\nconst UNEDITED_ACTIVITY = new Set(['Created the site'])\n\n/** More feed entries than this and the site was worked on. */\nconst ACTIVITY_SCAN = 20\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Instant = Date | { toMillis(): number } | number | string | null | undefined\n\nfunction toMillis(value: Instant): number | null {\n if (value === null || value === undefined) return null\n if (value instanceof Date) return value.getTime()\n if (typeof value === 'number') return value\n if (typeof value === 'string') {\n const parsed = Date.parse(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return typeof value.toMillis === 'function' ? value.toMillis() : null\n}\n\n/** The due marker, as stored. */\nexport interface AiEmptySiteSweep {\n hostId: string\n orgId: string\n jobId: string\n /** What the job ended as. */\n status: 'canceled' | 'failed'\n settledAt: Date\n dueAt: Date\n attempts: number\n}\n\n/**\n * Whether a job leaves the empty site this sweep is for: a site start that\n * ended `canceled` or `failed` with nothing built — no output but the\n * site's listing proposal, which rides out with the first unit and builds\n * nothing (the rule `aiSiteStarterFallbackOffered` offers the starter by).\n */\nexport function aiJobLeavesEmptySite(\n job: Pick<AiJob, 'kind' | 'status' | 'hostId' | 'outputs' | 'siteRemoved'>,\n): boolean {\n if (job.kind !== 'site' || !job.hostId || job.siteRemoved) return false\n if (job.status !== 'canceled' && job.status !== 'failed') return false\n return (job.outputs ?? []).every((output) => output.resource === 'seo')\n}\n\n/**\n * Writes the due marker for a job that just ended, when it leaves an empty\n * site. Never throws: a marker that could not be written leaves the site as\n * it was before this existed, and must not fail the write it follows.\n */\nexport async function scheduleAiEmptySiteSweep(\n firestore: Firestore,\n job: Pick<AiJob, '$id' | 'orgId' | 'kind' | 'status' | 'hostId' | 'outputs' | 'siteRemoved'>,\n now = new Date(),\n): Promise<boolean> {\n if (!aiJobLeavesEmptySite(job) || !job.hostId) return false\n const marker: AiEmptySiteSweep = {\n hostId: job.hostId,\n orgId: job.orgId,\n jobId: job.$id,\n status: job.status as AiEmptySiteSweep['status'],\n settledAt: now,\n dueAt: new Date(now.getTime() + AI_EMPTY_SITE_GRACE_MS),\n attempts: 0,\n }\n try {\n await firestore.collection(AI_EMPTY_SITE_SWEEPS_COLLECTION).doc(job.hostId).set(marker)\n return true\n } catch (error) {\n console.warn('ai empty site: the removal could not be scheduled', {\n orgId: job.orgId,\n jobId: job.$id,\n hostId: job.hostId,\n error: error instanceof Error ? error.message : String(error),\n })\n return false\n }\n}\n\n/**\n * Drops a site's marker: a job started, resumed or retried on the site, so\n * it is not empty for long. Never throws; the beat's check at due time says\n * the same thing if this write is lost.\n */\nexport async function clearAiEmptySiteSweep(firestore: Firestore, hostId: string | null | undefined): Promise<void> {\n if (!hostId) return\n try {\n await firestore.collection(AI_EMPTY_SITE_SWEEPS_COLLECTION).doc(hostId).delete()\n } catch {\n // A marker left behind is decided at its due time, against the site as it is then.\n }\n}\n\n/** What the check at due time found. */\nexport type AiEmptySiteVerdict =\n | { kind: 'empty'; name: string | null }\n /** The job settled again after the marker was written: wait a full grace from then. */\n | { kind: 'later'; dueAt: Date }\n /** The site is kept; the marker is dropped. */\n | { kind: 'keep'; reason: string }\n\n/**\n * Every condition, read again as the site is now (AGL-3936). The site is\n * removed only when all of them hold:\n *\n * - it exists, in the marker's workspace, with no custom domain, no sending\n * domain, no page on its routing map, and no staff hold;\n * - the job is still the one that ended, still `canceled` or `failed`, and\n * still built nothing — not resumed, retried or finished since;\n * - no other AI job names the site;\n * - nothing in {@link AI_EMPTY_SITE_CONTENT_COLLECTIONS}: no page, email,\n * layout, form or media file;\n * - the site's own feed holds nothing but \"Created the site\" and the AI\n * job's own `ai.job.*` entries: nobody edited it.\n */\nexport async function aiEmptySiteVerdict(\n firestore: Firestore,\n marker: Pick<AiEmptySiteSweep, 'hostId' | 'orgId' | 'jobId' | 'dueAt'>,\n): Promise<AiEmptySiteVerdict> {\n const hostRef = firestore.collection('hosts').doc(marker.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { kind: 'keep', reason: 'site-gone' }\n if (host.get('orgId') !== marker.orgId) return { kind: 'keep', reason: 'site-moved' }\n const text = (field: string) => String(host.get(field) ?? '').trim()\n if (text('cname') || host.get('hasCustomDomain') === true) return { kind: 'keep', reason: 'custom-domain' }\n if (text('sendingLabel')) return { kind: 'keep', reason: 'sending-domain' }\n if (host.get('suspended') === true || host.get('suspendedAt')) return { kind: 'keep', reason: 'staff-hold' }\n const routes = host.get('screens')\n if (routes && typeof routes === 'object' && Object.keys(routes as object).length > 0) {\n return { kind: 'keep', reason: 'routed-pages' }\n }\n\n const jobSnapshot = await firestore\n .collection('orgs')\n .doc(marker.orgId)\n .collection(JOBS_COLLECTION)\n .doc(marker.jobId)\n .get()\n if (!jobSnapshot.exists) return { kind: 'keep', reason: 'job-gone' }\n const job = { ...(jobSnapshot.data() as Omit<AiJob, '$id'>), $id: jobSnapshot.id } as AiJob\n if (job.hostId !== marker.hostId || !aiJobLeavesEmptySite(job)) return { kind: 'keep', reason: 'job-moved-on' }\n const settledMs = toMillis(job.updatedAt as Instant)\n const markedMs = marker.dueAt.getTime() - AI_EMPTY_SITE_GRACE_MS\n if (settledMs !== null && settledMs > markedMs + 1_000) {\n return { kind: 'later', dueAt: new Date(settledMs + AI_EMPTY_SITE_GRACE_MS) }\n }\n\n const [others, ...contents] = await Promise.all([\n firestore\n .collection('orgs')\n .doc(marker.orgId)\n .collection(JOBS_COLLECTION)\n .where('hostId', '==', marker.hostId)\n .limit(2)\n .get(),\n ...AI_EMPTY_SITE_CONTENT_COLLECTIONS.map((name) => hostRef.collection(name).limit(1).get()),\n ])\n if (others.docs.some((doc) => doc.id !== marker.jobId)) return { kind: 'keep', reason: 'another-job' }\n const held = AI_EMPTY_SITE_CONTENT_COLLECTIONS.find((_, index) => !contents[index].empty)\n if (held) return { kind: 'keep', reason: `has-${held}` }\n\n const activity = await hostRef.collection('activity').limit(ACTIVITY_SCAN + 1).get()\n if (activity.size > ACTIVITY_SCAN) return { kind: 'keep', reason: 'edited' }\n const edited = activity.docs.some((doc) => {\n const action = String(doc.get('action') ?? '')\n return !UNEDITED_ACTIVITY.has(action) && !action.startsWith('ai.job.')\n })\n if (edited) return { kind: 'keep', reason: 'edited' }\n\n const name = host.get('displayName')\n return { kind: 'empty', name: typeof name === 'string' && name ? name : null }\n}\n\nexport interface AiEmptySiteSweepResult {\n /** Markers that were due. */\n due: number\n /** Sites removed. */\n removed: number\n /** Markers dropped because the site was kept, or was already gone. */\n kept: number\n /** Markers moved later: the job settled again, or the eraser did not answer. */\n later: number\n}\n\nexport interface SweepAiEmptySitesOptions {\n firestore: Firestore\n now?: () => number\n max?: number\n budgetMs?: number\n /** The site's removal; `core.site-erase` unless a spec replaces it. */\n erase?: (request: PluginSiteEraseRequest) => Promise<PluginSiteEraseResult>\n}\n\n/**\n * One beat's removals (AGL-3936): the markers due now, oldest first, a few\n * at a time and inside a few seconds of wall clock, each decided against the\n * site as it is at that moment. The marker goes once it is decided; one the\n * eraser could not answer is tried again later, and given up after\n * {@link AI_EMPTY_SITE_MAX_ATTEMPTS}.\n */\nexport async function sweepAiEmptySites(options: SweepAiEmptySitesOptions): Promise<AiEmptySiteSweepResult> {\n const now = options.now ?? Date.now\n const startedAt = now()\n const budgetMs = options.budgetMs ?? AI_EMPTY_SITE_SWEEP_BUDGET_MS\n const erase = options.erase ?? erasePluginSite\n const markers = options.firestore.collection(AI_EMPTY_SITE_SWEEPS_COLLECTION)\n const due = await markers\n .where('dueAt', '<=', new Date(startedAt))\n .orderBy('dueAt', 'asc')\n .limit(options.max ?? AI_EMPTY_SITE_SWEEP_MAX)\n .get()\n const result: AiEmptySiteSweepResult = { due: due.size, removed: 0, kept: 0, later: 0 }\n\n for (const doc of due.docs) {\n if (now() - startedAt >= budgetMs) break\n const stored = doc.data() as Partial<AiEmptySiteSweep> & { dueAt?: Instant; settledAt?: Instant }\n const marker = {\n hostId: String(stored.hostId ?? doc.id),\n orgId: String(stored.orgId ?? ''),\n jobId: String(stored.jobId ?? ''),\n dueAt: new Date(toMillis(stored.dueAt) ?? startedAt),\n attempts: Number(stored.attempts ?? 0),\n }\n try {\n if (!marker.orgId || !marker.jobId) {\n await doc.ref.delete()\n result.kept += 1\n continue\n }\n const verdict = await aiEmptySiteVerdict(options.firestore, marker)\n if (verdict.kind === 'keep') {\n await doc.ref.delete()\n result.kept += 1\n console.info('ai empty site kept', { ...marker, reason: verdict.reason })\n continue\n }\n if (verdict.kind === 'later') {\n await doc.ref.set({ dueAt: verdict.dueAt }, { merge: true })\n result.later += 1\n continue\n }\n const erased = await erase({\n hostId: marker.hostId,\n orgId: marker.orgId,\n activity: AI_EMPTY_SITE_ACTIVITY,\n reason: AI_EMPTY_SITE_REASON,\n })\n if (erased.erased) {\n // Said on the job only once the site is gone, so its pages can say so.\n await options.firestore\n .collection('orgs')\n .doc(marker.orgId)\n .collection(JOBS_COLLECTION)\n .doc(marker.jobId)\n .set({ siteRemoved: { at: new Date(now()), name: verdict.name } }, { merge: true })\n await doc.ref.delete()\n result.removed += 1\n console.info('ai empty site removed', marker)\n continue\n }\n const refused = 'reason' in erased ? erased.reason : 'unavailable'\n if (refused !== 'unavailable') {\n await doc.ref.delete()\n result.kept += 1\n console.info('ai empty site kept', { ...marker, reason: refused })\n continue\n }\n await retryLater(doc.ref, marker, now())\n result.later += 1\n } catch (error) {\n console.error('ai empty site sweep failed', { ...marker, error })\n await retryLater(doc.ref, marker, now()).catch(() => undefined)\n result.later += 1\n }\n }\n return result\n}\n\n/** Moves a marker the eraser could not answer later, or gives it up. */\nasync function retryLater(\n ref: FirebaseFirestore.DocumentReference,\n marker: { hostId: string; orgId: string; jobId: string; attempts: number },\n nowMs: number,\n): Promise<void> {\n const attempts = marker.attempts + 1\n if (attempts >= AI_EMPTY_SITE_MAX_ATTEMPTS) {\n console.error('ai empty site: removal given up', { ...marker, attempts })\n await ref.delete()\n return\n }\n await ref.set({ attempts, dueAt: new Date(nowMs + AI_EMPTY_SITE_RETRY_MS) }, { merge: true })\n}\n"],"names":["erasePluginSite","AI_EMPTY_SITE_SWEEPS_COLLECTION","AI_EMPTY_SITE_GRACE_MS","AI_EMPTY_SITE_SWEEP_MAX","AI_EMPTY_SITE_SWEEP_BUDGET_MS","AI_EMPTY_SITE_RETRY_MS","AI_EMPTY_SITE_MAX_ATTEMPTS","AI_EMPTY_SITE_ACTIVITY","AI_EMPTY_SITE_REASON","JOBS_COLLECTION","AI_EMPTY_SITE_CONTENT_COLLECTIONS","UNEDITED_ACTIVITY","Set","ACTIVITY_SCAN","toMillis","value","undefined","Date","getTime","parsed","parse","Number","isFinite","aiJobLeavesEmptySite","job","kind","hostId","siteRemoved","status","outputs","every","output","resource","scheduleAiEmptySiteSweep","firestore","now","marker","orgId","jobId","$id","settledAt","dueAt","attempts","collection","doc","set","error","console","warn","Error","message","String","clearAiEmptySiteSweep","delete","aiEmptySiteVerdict","hostRef","host","get","exists","reason","text","field","trim","routes","Object","keys","length","jobSnapshot","data","id","settledMs","updatedAt","markedMs","others","contents","Promise","all","where","limit","map","name","docs","some","held","find","_","index","empty","activity","size","edited","action","has","startsWith","sweepAiEmptySites","options","startedAt","budgetMs","erase","markers","due","orderBy","max","result","removed","kept","later","stored","ref","verdict","info","merge","erased","at","refused","retryLater","catch","nowMs"],"mappings":"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"}
|
|
@@ -18,6 +18,7 @@ import type { NodesMap } from '@aglyn/aglyn/types/nodes';
|
|
|
18
18
|
import { type AiBuildPlanScreen } from '../model/ai-build-plan';
|
|
19
19
|
import type { AiJob, AiJobPlan } from '../model/ai-jobs.types';
|
|
20
20
|
import { type AiSiteInventory } from '../model/ai-site-inventory';
|
|
21
|
+
import { type AiSiteMotionStyle } from '../layout-language/ai-layout-motion';
|
|
21
22
|
import { type AiLayoutListing } from '../layout-language/ai-layout-listings';
|
|
22
23
|
import { type AiLayoutSection, type AiLayoutSettlement } from '../layout-language/ai-layout-language';
|
|
23
24
|
import type { AiLayoutTargets } from '../layout-language/ai-layout-links';
|
|
@@ -192,11 +193,15 @@ export declare function aiLayoutDesignOf(job: Pick<AiJob, '$id' | 'inputs'>, slu
|
|
|
192
193
|
/**
|
|
193
194
|
* The site a page's entrance motion is drawn for (AGL-3838): its design where
|
|
194
195
|
* the job names a kind of site, else the job's own seed, which moves in the
|
|
195
|
-
* standard mood — so every page of one site moves alike.
|
|
196
|
+
* standard mood — so every page of one site moves alike. The motion style its
|
|
197
|
+
* look chose rides along (AGL-3941); a job whose look chose none — one from
|
|
198
|
+
* before the look did, a replay of one, a job with no look — moves in its
|
|
199
|
+
* kind's mood.
|
|
196
200
|
*/
|
|
197
201
|
export declare function aiLayoutMotionSiteOf(job: Pick<AiJob, '$id' | 'inputs'>, slug: string): {
|
|
198
202
|
kind?: string;
|
|
199
203
|
seed: number;
|
|
204
|
+
motion?: AiSiteMotionStyle;
|
|
200
205
|
};
|
|
201
206
|
/** A validated language page, stored as the page keeps it. */
|
|
202
207
|
export interface AiLayoutPageBuilt {
|
|
@@ -229,6 +234,7 @@ export interface AiLayoutPageCheckInput {
|
|
|
229
234
|
motion?: {
|
|
230
235
|
kind?: string;
|
|
231
236
|
seed: number;
|
|
237
|
+
motion?: AiSiteMotionStyle;
|
|
232
238
|
};
|
|
233
239
|
/** Whether copy may promise what is coming (a coming-soon site, AGL-3887); absent, it may not. */
|
|
234
240
|
promisesAllowed?: boolean;
|
|
@@ -18,7 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
19
19
|
import { aiHomeScreenIds } from "../model/ai-site-inventory.js";
|
|
20
20
|
import { AI_LAYOUT_POST_ADDRESS_TOKENS, aiCompileLayoutPage } from "../layout-language/ai-layout-compiler.js";
|
|
21
|
-
import { aiWithSiteMotion } from "../layout-language/ai-layout-motion.js";
|
|
21
|
+
import { aiSiteMotionStyle, aiWithSiteMotion } from "../layout-language/ai-layout-motion.js";
|
|
22
22
|
import { aiLayoutListingAt, aiLayoutListingsOf, aiLayoutRecordHref } from "../layout-language/ai-layout-listings.js";
|
|
23
23
|
import { AI_LAYOUT_LANGUAGE_TEXT, AI_LAYOUT_PAGE_TOOL, aiReadLayoutPage } from "../layout-language/ai-layout-language.js";
|
|
24
24
|
import { aiLayoutCopyCheck, aiLayoutWithoutSections } from "../layout-language/ai-layout-gaps.js";
|
|
@@ -382,12 +382,25 @@ export const AI_JOB_PAGE_LANGUAGE_BUDGET = aiJobStepBudget({
|
|
|
382
382
|
/**
|
|
383
383
|
* The site a page's entrance motion is drawn for (AGL-3838): its design where
|
|
384
384
|
* the job names a kind of site, else the job's own seed, which moves in the
|
|
385
|
-
* standard mood — so every page of one site moves alike.
|
|
385
|
+
* standard mood — so every page of one site moves alike. The motion style its
|
|
386
|
+
* look chose rides along (AGL-3941); a job whose look chose none — one from
|
|
387
|
+
* before the look did, a replay of one, a job with no look — moves in its
|
|
388
|
+
* kind's mood.
|
|
386
389
|
*/ export function aiLayoutMotionSiteOf(job, slug) {
|
|
387
390
|
var _aiLayoutDesignOf;
|
|
388
|
-
|
|
391
|
+
var _job_inputs;
|
|
392
|
+
const style = (_job_inputs = job.inputs) == null ? void 0 : _job_inputs['siteStyle'];
|
|
393
|
+
const motion = aiSiteMotionStyle(style == null ? void 0 : style['motion']);
|
|
394
|
+
const { kind, seed } = (_aiLayoutDesignOf = aiLayoutDesignOf(job, slug)) != null ? _aiLayoutDesignOf : {
|
|
389
395
|
seed: aiSiteSeed(aiOriginJobId(job))
|
|
390
396
|
};
|
|
397
|
+
return _extends({}, kind ? {
|
|
398
|
+
kind
|
|
399
|
+
} : {}, {
|
|
400
|
+
seed
|
|
401
|
+
}, motion ? {
|
|
402
|
+
motion
|
|
403
|
+
} : {});
|
|
391
404
|
}
|
|
392
405
|
/**
|
|
393
406
|
* The follow-up turn for the sections a page's answer left out (AGL-3660):
|