@aglyn/plugins-ai 1.0.0-beta.233 → 1.0.0-beta.235
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +25 -0
- package/src/lib/activity/ai-activity-actions.js +85 -4
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.js +46 -2
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-campaign-create.component.js +1 -1
- package/src/lib/components/ai-campaign-create.component.js.map +1 -1
- package/src/lib/components/ai-credits-card.component.js +3 -3
- package/src/lib/components/ai-credits-card.component.js.map +1 -1
- package/src/lib/components/ai-crm-email-draft.component.js +1 -1
- package/src/lib/components/ai-crm-email-draft.component.js.map +1 -1
- package/src/lib/components/ai-crm-import-mapping.component.js +1 -1
- package/src/lib/components/ai-crm-import-mapping.component.js.map +1 -1
- package/src/lib/components/ai-crm-record-card.component.js +1 -1
- package/src/lib/components/ai-crm-record-card.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +4 -2
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-org-automation.component.js +1 -1
- package/src/lib/components/ai-describe-org-automation.component.js.map +1 -1
- package/src/lib/components/ai-experiment-cards.component.js +2 -2
- package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
- package/src/lib/components/ai-explain-automation.component.js +1 -1
- package/src/lib/components/ai-explain-automation.component.js.map +1 -1
- package/src/lib/components/ai-insight-card.component.js +3 -1
- package/src/lib/components/ai-insight-card.component.js.map +1 -1
- package/src/lib/components/ai-job-links.d.ts +6 -0
- package/src/lib/components/ai-job-links.js +9 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-jobs-list-page.component.d.ts +6 -1
- package/src/lib/components/ai-jobs-list-page.component.js +15 -2
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
- package/src/lib/components/ai-logic.component.js +5 -3
- package/src/lib/components/ai-logic.component.js.map +1 -1
- package/src/lib/components/ai-media-create.component.js +88 -16
- package/src/lib/components/ai-media-create.component.js.map +1 -1
- package/src/lib/components/ai-model-selector.component.js +8 -0
- package/src/lib/components/ai-model-selector.component.js.map +1 -1
- package/src/lib/components/ai-overlay-cards.component.js +2 -2
- package/src/lib/components/ai-overlay-cards.component.js.map +1 -1
- package/src/lib/components/ai-product-copy-card.component.js +2 -1
- package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
- package/src/lib/components/ai-products-hub-card.component.js +2 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-revise-automation.component.js +1 -1
- package/src/lib/components/ai-revise-automation.component.js.map +1 -1
- package/src/lib/components/ai-seo-fields-card.component.js +7 -3
- package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
- package/src/lib/components/ai-site-batch-card.component.js +7 -4
- package/src/lib/components/ai-site-batch-card.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +48 -6
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-seo-start-card.component.js +7 -4
- package/src/lib/components/ai-site-seo-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +152 -36
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-theme-proposal-card.component.js +1 -0
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js +17 -3
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/components/ai-usage-strip.component.js +13 -2
- package/src/lib/components/ai-usage-strip.component.js.map +1 -1
- package/src/lib/components/assist-build-card.component.js +20 -3
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +7 -1
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/billing-ai-allotments.component.js +2 -2
- package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
- package/src/lib/components/billing-ai-top-users.component.js +1 -0
- package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
- package/src/lib/components/billing-assist-overage-card.component.js +2 -2
- package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
- package/src/lib/components/host-ai-allotments.component.js +2 -2
- package/src/lib/components/host-ai-allotments.component.js.map +1 -1
- package/src/lib/components/member-ai-allotment-card.component.js +2 -2
- package/src/lib/components/member-ai-allotment-card.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-card.component.js +1 -0
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-conversations.component.d.ts +9 -3
- package/src/lib/components/staff-org-ai-conversations.component.js +174 -83
- package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
- package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
- package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
- package/src/lib/jobs/ai-job-admission.d.ts +2 -1
- package/src/lib/jobs/ai-job-admission.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +39 -0
- package/src/lib/jobs/ai-job-email-step.js +116 -5
- package/src/lib/jobs/ai-job-email-step.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 +6 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-site-pages.d.ts +14 -0
- package/src/lib/jobs/ai-job-layout-site-pages.js +43 -11
- package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +3 -2
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +26 -1
- package/src/lib/jobs/ai-job-page-language.js +75 -13
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +3 -0
- package/src/lib/jobs/ai-job-page-step.js +32 -7
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +34 -0
- package/src/lib/jobs/ai-job-plan-step.js +106 -14
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +81 -13
- package/src/lib/jobs/ai-job-site-content.js +210 -43
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +19 -0
- package/src/lib/jobs/ai-job-site-step.js +152 -51
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +8 -4
- package/src/lib/jobs/ai-jobs.js +19 -2
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +98 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +248 -0
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +11 -0
- package/src/lib/jobs/ai-site-publish.js +31 -1
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.d.ts +151 -0
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js +183 -0
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js +192 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js +222 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +37 -0
- package/src/lib/layout-language/ai-layout-compiler.js +1003 -19
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +114 -0
- package/src/lib/layout-language/ai-layout-design.js +209 -0
- package/src/lib/layout-language/ai-layout-design.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-frame.js +33 -8
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +18 -2
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-language.js +15 -1
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.d.ts +36 -0
- package/src/lib/layout-language/ai-layout-links.js +26 -2
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +125 -0
- package/src/lib/layout-language/ai-layout-listings.js +187 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-pictures.d.ts +11 -5
- package/src/lib/layout-language/ai-layout-pictures.js +80 -17
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +9 -1
- package/src/lib/model/ai-job-notice.js +23 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +12 -2
- package/src/lib/model/ai-site-build-progress.js +104 -15
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +236 -24
- package/src/lib/model/ai-site-job.js +269 -22
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +30 -8
- package/src/lib/model/ai-site-kinds.js +735 -116
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +21 -0
- package/src/lib/model/ai-site-look.js +63 -14
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +30 -0
- package/src/lib/model/ai-site-start-seo.js +132 -11
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +2 -2
- package/src/lib/model/ai-site-start.js +3 -3
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/plugin.js +15 -3
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -1
- package/src/lib/runtime/ai-doctrine-validators.js +227 -4
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +5 -3
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +31 -5
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-admin-conversations.d.ts +9 -0
- package/src/lib/server/ai-admin-conversations.js +151 -9
- package/src/lib/server/ai-admin-conversations.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +15 -3
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-edit.js +5 -2
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +54 -0
- package/src/lib/usage/free-ai-credits-left.js +62 -0
- package/src/lib/usage/free-ai-credits-left.js.map +1 -0
- package/src/lib/usage/staff-org-ai-conversations.d.ts +23 -0
- package/src/lib/usage/staff-org-ai-conversations.js +45 -0
- package/src/lib/usage/staff-org-ai-conversations.js.map +1 -1
|
@@ -0,0 +1,98 @@
|
|
|
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 PluginMediaIngest } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest';
|
|
18
|
+
import { type StockPhoto, type StockPhotoOrientation, type StockPhotoProvider, type StockPhotoSearchRequest } from '@aglyn/aglyn/plugin-manager/stock-photo-provider';
|
|
19
|
+
import { type AiLayoutPictureSlot, type AiLayoutPictureSource } from '../layout-language/ai-layout-pictures';
|
|
20
|
+
/**
|
|
21
|
+
* STOCK PHOTOS FOR A LANGUAGE PAGE'S PICTURE SLOTS (AGL-3660).
|
|
22
|
+
*
|
|
23
|
+
* The source `aiResolveLayoutPictures` asks before it falls back to the
|
|
24
|
+
* starter photos. It names no library: it asks core for the deployment's
|
|
25
|
+
* stock photo provider (`core.stock-photos`) and for the media library's
|
|
26
|
+
* server door (`core.media-ingest`), and builds no source at all when either
|
|
27
|
+
* is missing, so a deployment without a key behaves exactly as before.
|
|
28
|
+
*
|
|
29
|
+
* For each slot, in document order:
|
|
30
|
+
*
|
|
31
|
+
* 1. **Search.** Plain words: the kind of business (the head of the site's
|
|
32
|
+
* business type, "yoga studio" from "a family-owned yoga studio in
|
|
33
|
+
* Austin") for the hero; the picture's own subject (the content words of
|
|
34
|
+
* the alt text the model wrote, else its section's name) with the business
|
|
35
|
+
* for the rest, and an about picture asks for people. The orientation and
|
|
36
|
+
* a minimum size come from the slot's frame. The provider caches every
|
|
37
|
+
* search for a day and keeps its own rate limit.
|
|
38
|
+
* 2. **Pick.** Among the first hits not already on the page, by the job's
|
|
39
|
+
* seed and the slot, so two sites built from the same words rarely open
|
|
40
|
+
* with the same photo while one job always picks the same.
|
|
41
|
+
* 3. **Keep.** A photo the site's library already holds (by its source key)
|
|
42
|
+
* is reused; otherwise its bytes are downloaded from the library and
|
|
43
|
+
* stored in the site's own media library as the member who started the
|
|
44
|
+
* job, credited to its photographer. The page names the site's asset,
|
|
45
|
+
* never the library's address.
|
|
46
|
+
*
|
|
47
|
+
* A slot that finds nothing, any error, a refusal from the library (its
|
|
48
|
+
* storage band, say) or the time running out leaves the slot to a starter
|
|
49
|
+
* photo. Nothing here throws, and none of it is an AI model call: photos cost
|
|
50
|
+
* no AI credits.
|
|
51
|
+
*/
|
|
52
|
+
/** Wall clock the whole page's photos may take, inside the step's own signal. */
|
|
53
|
+
export declare const AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS = 20000;
|
|
54
|
+
/** Searches one page may ask, cached ones included. */
|
|
55
|
+
export declare const AI_LAYOUT_STOCK_SEARCHES_PER_PAGE = 8;
|
|
56
|
+
/** The hits a pick is made among. */
|
|
57
|
+
export declare const AI_LAYOUT_STOCK_PICK_WINDOW = 8;
|
|
58
|
+
/** The largest photo copied: inside one direct upload. */
|
|
59
|
+
export declare const AI_LAYOUT_STOCK_MAX_BYTES: number;
|
|
60
|
+
/**
|
|
61
|
+
* The kind of business in a few words: the first clause of the business
|
|
62
|
+
* type, before a place or an audience, its last three content words (the
|
|
63
|
+
* head of an English noun phrase is at its end).
|
|
64
|
+
*/
|
|
65
|
+
export declare function aiStockBusinessWords(businessType: string): string;
|
|
66
|
+
/** A picture's subject in a few words: its alt text's first content words, else its section's name. */
|
|
67
|
+
export declare function aiStockSubjectWords(alt: string, sectionName: string): string;
|
|
68
|
+
/** The frame's orientation: a wide frame wants a landscape photo, a tall one a portrait. */
|
|
69
|
+
export declare function aiStockOrientation(aspect: number): StockPhotoOrientation;
|
|
70
|
+
/** The searches a slot tries, in order, most specific first, each distinct. */
|
|
71
|
+
export declare function aiStockSearchesFor(slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect'>, words: {
|
|
72
|
+
business: string;
|
|
73
|
+
subject: string;
|
|
74
|
+
}): StockPhotoSearchRequest[];
|
|
75
|
+
/** The pick among a search's hits: not on the page yet, among the first few, by the seed. */
|
|
76
|
+
export declare function aiStockPick(photos: readonly StockPhoto[], used: ReadonlySet<string>, seed: string): StockPhoto | null;
|
|
77
|
+
export interface AiLayoutStockPhotoSourceInput {
|
|
78
|
+
hostId: string;
|
|
79
|
+
/** The member the photos are stored as: the job's creator. */
|
|
80
|
+
uid: string;
|
|
81
|
+
/** The job's seed, as the starter photos turn by. */
|
|
82
|
+
seed: string;
|
|
83
|
+
/** The site's business type, else the job's brief. */
|
|
84
|
+
business: string;
|
|
85
|
+
/** The page's section names, by plan index. */
|
|
86
|
+
sectionNames: readonly string[];
|
|
87
|
+
signal?: AbortSignal;
|
|
88
|
+
}
|
|
89
|
+
export interface AiLayoutStockPhotoDeps {
|
|
90
|
+
provider?: () => StockPhotoProvider | null;
|
|
91
|
+
ingest?: () => PluginMediaIngest | null;
|
|
92
|
+
budgetMs?: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The source for one page, or `null` when this deployment has no stock
|
|
96
|
+
* photo library or no media door, which leaves every slot to the starters.
|
|
97
|
+
*/
|
|
98
|
+
export declare function aiLayoutStockPhotoSource(input: AiLayoutStockPhotoSourceInput, deps?: AiLayoutStockPhotoDeps): AiLayoutPictureSource | null;
|
|
@@ -0,0 +1,248 @@
|
|
|
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 { pluginMediaIngest } from "@aglyn/aglyn/plugin-manager/plugin-media-ingest";
|
|
18
|
+
import { STOCK_PHOTO_QUERY_MAX_CHARS, stockPhotoProvider, stockPhotoSourceKey } from "@aglyn/aglyn/plugin-manager/stock-photo-provider";
|
|
19
|
+
import { aiLayoutSeedNumber } from "../layout-language/ai-layout-pictures.js";
|
|
20
|
+
/**
|
|
21
|
+
* STOCK PHOTOS FOR A LANGUAGE PAGE'S PICTURE SLOTS (AGL-3660).
|
|
22
|
+
*
|
|
23
|
+
* The source `aiResolveLayoutPictures` asks before it falls back to the
|
|
24
|
+
* starter photos. It names no library: it asks core for the deployment's
|
|
25
|
+
* stock photo provider (`core.stock-photos`) and for the media library's
|
|
26
|
+
* server door (`core.media-ingest`), and builds no source at all when either
|
|
27
|
+
* is missing, so a deployment without a key behaves exactly as before.
|
|
28
|
+
*
|
|
29
|
+
* For each slot, in document order:
|
|
30
|
+
*
|
|
31
|
+
* 1. **Search.** Plain words: the kind of business (the head of the site's
|
|
32
|
+
* business type, "yoga studio" from "a family-owned yoga studio in
|
|
33
|
+
* Austin") for the hero; the picture's own subject (the content words of
|
|
34
|
+
* the alt text the model wrote, else its section's name) with the business
|
|
35
|
+
* for the rest, and an about picture asks for people. The orientation and
|
|
36
|
+
* a minimum size come from the slot's frame. The provider caches every
|
|
37
|
+
* search for a day and keeps its own rate limit.
|
|
38
|
+
* 2. **Pick.** Among the first hits not already on the page, by the job's
|
|
39
|
+
* seed and the slot, so two sites built from the same words rarely open
|
|
40
|
+
* with the same photo while one job always picks the same.
|
|
41
|
+
* 3. **Keep.** A photo the site's library already holds (by its source key)
|
|
42
|
+
* is reused; otherwise its bytes are downloaded from the library and
|
|
43
|
+
* stored in the site's own media library as the member who started the
|
|
44
|
+
* job, credited to its photographer. The page names the site's asset,
|
|
45
|
+
* never the library's address.
|
|
46
|
+
*
|
|
47
|
+
* A slot that finds nothing, any error, a refusal from the library (its
|
|
48
|
+
* storage band, say) or the time running out leaves the slot to a starter
|
|
49
|
+
* photo. Nothing here throws, and none of it is an AI model call: photos cost
|
|
50
|
+
* no AI credits.
|
|
51
|
+
*/ /** Wall clock the whole page's photos may take, inside the step's own signal. */ export const AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS = 20000;
|
|
52
|
+
/** Searches one page may ask, cached ones included. */ export const AI_LAYOUT_STOCK_SEARCHES_PER_PAGE = 8;
|
|
53
|
+
/** The hits a pick is made among. */ export const AI_LAYOUT_STOCK_PICK_WINDOW = 8;
|
|
54
|
+
/** The largest photo copied: inside one direct upload. */ export const AI_LAYOUT_STOCK_MAX_BYTES = 3 * 1024 * 1024;
|
|
55
|
+
/** Words that carry no subject. */ const STOPWORDS = new Set(('a an the and or of in on at to for with from by into onto over under near our your their my his her its ' + 'this that these those is are be being was were who whom which where while as about than then very just ' + 'small local best new top quality premium family-owned family owned independent professional ' + 'business company shop services service site website page photo picture image showing shows show ' + 'someone people person one two three some many several').split(' '));
|
|
56
|
+
/** Lowercase words, letters only, without stopwords. */ function contentWords(text) {
|
|
57
|
+
return text.toLowerCase().replace(/https?:\/\/\S+|\S+@\S+|\+?\d[\d\s().-]{5,}\d/g, ' ').replace(/[^a-zÀ-ɏ\s-]/g, ' ').split(/\s+/).map((word)=>word.replace(/^-+|-+$/g, '')).filter((word)=>word.length > 1 && !STOPWORDS.has(word));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The kind of business in a few words: the first clause of the business
|
|
61
|
+
* type, before a place or an audience, its last three content words (the
|
|
62
|
+
* head of an English noun phrase is at its end).
|
|
63
|
+
*/ export function aiStockBusinessWords(businessType) {
|
|
64
|
+
const clause = businessType.toLowerCase().split(/[,;:.()]|\s[-–—]\s|\b(?:in|for|near|serving|based|located|that|which|who|with|from|since|and|&)\b/)[0];
|
|
65
|
+
const words = contentWords(clause != null ? clause : '');
|
|
66
|
+
return words.slice(-3).join(' ');
|
|
67
|
+
}
|
|
68
|
+
/** A picture's subject in a few words: its alt text's first content words, else its section's name. */ export function aiStockSubjectWords(alt, sectionName) {
|
|
69
|
+
const words = contentWords(alt);
|
|
70
|
+
if (words.length) return words.slice(0, 3).join(' ');
|
|
71
|
+
return contentWords(sectionName).slice(0, 2).join(' ');
|
|
72
|
+
}
|
|
73
|
+
const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUERY_MAX_CHARS).trim();
|
|
74
|
+
/** The frame's orientation: a wide frame wants a landscape photo, a tall one a portrait. */ export function aiStockOrientation(aspect) {
|
|
75
|
+
if (aspect > 1.15) return 'horizontal';
|
|
76
|
+
if (aspect < 0.87) return 'vertical';
|
|
77
|
+
return 'any';
|
|
78
|
+
}
|
|
79
|
+
/** The searches a slot tries, in order, most specific first, each distinct. */ export function aiStockSearchesFor(slot, words) {
|
|
80
|
+
const orientation = aiStockOrientation(slot.aspect);
|
|
81
|
+
const size = slot.role === 'hero' ? {
|
|
82
|
+
minWidth: 1600
|
|
83
|
+
} : orientation === 'vertical' ? {
|
|
84
|
+
minHeight: 900
|
|
85
|
+
} : {
|
|
86
|
+
minWidth: 900
|
|
87
|
+
};
|
|
88
|
+
const queries = slot.role === 'hero' ? [
|
|
89
|
+
words.business,
|
|
90
|
+
`${words.business} ${words.subject}`
|
|
91
|
+
] : slot.role === 'about' ? [
|
|
92
|
+
`${words.business} ${words.subject}`,
|
|
93
|
+
words.business
|
|
94
|
+
] : [
|
|
95
|
+
`${words.business} ${words.subject}`,
|
|
96
|
+
words.subject,
|
|
97
|
+
words.business
|
|
98
|
+
];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const searches = [];
|
|
101
|
+
for (const raw of queries){
|
|
102
|
+
const query = clip(raw);
|
|
103
|
+
if (!query || seen.has(query)) continue;
|
|
104
|
+
seen.add(query);
|
|
105
|
+
searches.push(_extends({
|
|
106
|
+
query,
|
|
107
|
+
orientation
|
|
108
|
+
}, size, slot.role === 'about' ? {
|
|
109
|
+
people: true
|
|
110
|
+
} : {}));
|
|
111
|
+
}
|
|
112
|
+
return searches;
|
|
113
|
+
}
|
|
114
|
+
/** The pick among a search's hits: not on the page yet, among the first few, by the seed. */ export function aiStockPick(photos, used, seed) {
|
|
115
|
+
var _fresh_;
|
|
116
|
+
const fresh = photos.filter((photo)=>!used.has(stockPhotoSourceKey(photo))).slice(0, AI_LAYOUT_STOCK_PICK_WINDOW);
|
|
117
|
+
if (!fresh.length) return null;
|
|
118
|
+
return (_fresh_ = fresh[aiLayoutSeedNumber(seed) % fresh.length]) != null ? _fresh_ : null;
|
|
119
|
+
}
|
|
120
|
+
const EXTENSIONS = {
|
|
121
|
+
'image/jpeg': 'jpg',
|
|
122
|
+
'image/png': 'png',
|
|
123
|
+
'image/webp': 'webp'
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* The source for one page, or `null` when this deployment has no stock
|
|
127
|
+
* photo library or no media door, which leaves every slot to the starters.
|
|
128
|
+
*/ export function aiLayoutStockPhotoSource(input, deps = {}) {
|
|
129
|
+
var _deps_provider, _deps_ingest;
|
|
130
|
+
const provider = ((_deps_provider = deps.provider) != null ? _deps_provider : stockPhotoProvider)();
|
|
131
|
+
const ingest = ((_deps_ingest = deps.ingest) != null ? _deps_ingest : pluginMediaIngest)();
|
|
132
|
+
if (!provider || !ingest || !input.hostId || !input.uid) return null;
|
|
133
|
+
const business = aiStockBusinessWords(input.business);
|
|
134
|
+
return async (slots)=>{
|
|
135
|
+
var _deps_budgetMs;
|
|
136
|
+
const timeout = AbortSignal.timeout((_deps_budgetMs = deps.budgetMs) != null ? _deps_budgetMs : AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS);
|
|
137
|
+
const signal = input.signal ? AbortSignal.any([
|
|
138
|
+
input.signal,
|
|
139
|
+
timeout
|
|
140
|
+
]) : timeout;
|
|
141
|
+
const used = new Set();
|
|
142
|
+
let searches = 0;
|
|
143
|
+
let storing = true;
|
|
144
|
+
const photos = [];
|
|
145
|
+
for (const [index, slot] of slots.entries()){
|
|
146
|
+
var _input_sectionNames_slot_sectionIndex;
|
|
147
|
+
if (signal.aborted) {
|
|
148
|
+
photos.push(null);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const subject = aiStockSubjectWords(slot.alt, (_input_sectionNames_slot_sectionIndex = input.sectionNames[slot.sectionIndex]) != null ? _input_sectionNames_slot_sectionIndex : '');
|
|
152
|
+
let placed = null;
|
|
153
|
+
for (const request of aiStockSearchesFor(slot, {
|
|
154
|
+
business,
|
|
155
|
+
subject
|
|
156
|
+
})){
|
|
157
|
+
if (placed || signal.aborted || searches >= AI_LAYOUT_STOCK_SEARCHES_PER_PAGE) break;
|
|
158
|
+
searches += 1;
|
|
159
|
+
let found;
|
|
160
|
+
try {
|
|
161
|
+
var _ref;
|
|
162
|
+
var _this;
|
|
163
|
+
found = (_ref = (_this = await provider.search(request, {
|
|
164
|
+
signal
|
|
165
|
+
})) == null ? void 0 : _this.photos) != null ? _ref : [];
|
|
166
|
+
} catch (unused) {
|
|
167
|
+
found = [];
|
|
168
|
+
}
|
|
169
|
+
const photo = aiStockPick(found, used, `${input.seed}:${index}:${request.query}`);
|
|
170
|
+
if (!photo) continue;
|
|
171
|
+
const sourceKey = stockPhotoSourceKey(photo);
|
|
172
|
+
used.add(sourceKey);
|
|
173
|
+
try {
|
|
174
|
+
var _EXTENSIONS_bytes_contentType;
|
|
175
|
+
const kept = await ingest.findStockPhoto({
|
|
176
|
+
hostId: input.hostId,
|
|
177
|
+
sourceKey
|
|
178
|
+
});
|
|
179
|
+
if (kept) {
|
|
180
|
+
var _kept_width, _kept_height;
|
|
181
|
+
placed = {
|
|
182
|
+
src: kept.src,
|
|
183
|
+
width: (_kept_width = kept.width) != null ? _kept_width : photo.width,
|
|
184
|
+
height: (_kept_height = kept.height) != null ? _kept_height : photo.height
|
|
185
|
+
};
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
// A refusal to store (the storage band, a lockdown) holds for the
|
|
189
|
+
// whole page, so the rest of it reuses what it can and copies nothing.
|
|
190
|
+
if (!storing) continue;
|
|
191
|
+
const bytes = await provider.download(photo, {
|
|
192
|
+
maxBytes: AI_LAYOUT_STOCK_MAX_BYTES,
|
|
193
|
+
signal
|
|
194
|
+
});
|
|
195
|
+
if (!bytes || signal.aborted) continue;
|
|
196
|
+
const credit = provider.credit(photo);
|
|
197
|
+
const stored = await ingest.ingest({
|
|
198
|
+
hostId: input.hostId,
|
|
199
|
+
uid: input.uid,
|
|
200
|
+
fileName: `${photo.provider}-${photo.id}.${(_EXTENSIONS_bytes_contentType = EXTENSIONS[bytes.contentType]) != null ? _EXTENSIONS_bytes_contentType : 'jpg'}`,
|
|
201
|
+
contentType: bytes.contentType,
|
|
202
|
+
bytes: bytes.bytes,
|
|
203
|
+
alt: slot.alt,
|
|
204
|
+
description: credit.text,
|
|
205
|
+
stockPhoto: _extends({
|
|
206
|
+
key: sourceKey,
|
|
207
|
+
provider: photo.provider,
|
|
208
|
+
providerLabel: credit.providerLabel,
|
|
209
|
+
id: photo.id,
|
|
210
|
+
pageUrl: photo.pageUrl,
|
|
211
|
+
photographer: photo.photographer
|
|
212
|
+
}, photo.photographerUrl ? {
|
|
213
|
+
photographerUrl: photo.photographerUrl
|
|
214
|
+
} : {}, {
|
|
215
|
+
license: credit.license,
|
|
216
|
+
licenseUrl: credit.licenseUrl,
|
|
217
|
+
attributionRequired: credit.attributionRequired,
|
|
218
|
+
query: request.query
|
|
219
|
+
})
|
|
220
|
+
});
|
|
221
|
+
if (stored.ok) {
|
|
222
|
+
var _stored_width, _stored_height;
|
|
223
|
+
placed = {
|
|
224
|
+
src: stored.src,
|
|
225
|
+
width: (_stored_width = stored.width) != null ? _stored_width : photo.width,
|
|
226
|
+
height: (_stored_height = stored.height) != null ? _stored_height : photo.height
|
|
227
|
+
};
|
|
228
|
+
} else {
|
|
229
|
+
storing = false;
|
|
230
|
+
const refusal = stored;
|
|
231
|
+
console.warn('ai layout pictures: the media library refused a stock photo', {
|
|
232
|
+
status: refusal.status,
|
|
233
|
+
reason: refusal.reason
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.warn('ai layout pictures: a stock photo could not be kept', {
|
|
238
|
+
error: String(error)
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
photos.push(placed);
|
|
243
|
+
}
|
|
244
|
+
return photos;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
//# sourceMappingURL=ai-layout-stock-photos.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-layout-stock-photos.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 pluginMediaIngest,\n type PluginMediaIngest,\n type PluginMediaIngestResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport {\n STOCK_PHOTO_QUERY_MAX_CHARS,\n stockPhotoProvider,\n stockPhotoSourceKey,\n type StockPhoto,\n type StockPhotoOrientation,\n type StockPhotoProvider,\n type StockPhotoSearchRequest,\n} from '@aglyn/aglyn/plugin-manager/stock-photo-provider'\nimport {\n aiLayoutSeedNumber,\n type AiLayoutPicturePhoto,\n type AiLayoutPictureSlot,\n type AiLayoutPictureSource,\n} from '../layout-language/ai-layout-pictures'\n\n/**\n * STOCK PHOTOS FOR A LANGUAGE PAGE'S PICTURE SLOTS (AGL-3660).\n *\n * The source `aiResolveLayoutPictures` asks before it falls back to the\n * starter photos. It names no library: it asks core for the deployment's\n * stock photo provider (`core.stock-photos`) and for the media library's\n * server door (`core.media-ingest`), and builds no source at all when either\n * is missing, so a deployment without a key behaves exactly as before.\n *\n * For each slot, in document order:\n *\n * 1. **Search.** Plain words: the kind of business (the head of the site's\n * business type, \"yoga studio\" from \"a family-owned yoga studio in\n * Austin\") for the hero; the picture's own subject (the content words of\n * the alt text the model wrote, else its section's name) with the business\n * for the rest, and an about picture asks for people. The orientation and\n * a minimum size come from the slot's frame. The provider caches every\n * search for a day and keeps its own rate limit.\n * 2. **Pick.** Among the first hits not already on the page, by the job's\n * seed and the slot, so two sites built from the same words rarely open\n * with the same photo while one job always picks the same.\n * 3. **Keep.** A photo the site's library already holds (by its source key)\n * is reused; otherwise its bytes are downloaded from the library and\n * stored in the site's own media library as the member who started the\n * job, credited to its photographer. The page names the site's asset,\n * never the library's address.\n *\n * A slot that finds nothing, any error, a refusal from the library (its\n * storage band, say) or the time running out leaves the slot to a starter\n * photo. Nothing here throws, and none of it is an AI model call: photos cost\n * no AI credits.\n */\n\n/** Wall clock the whole page's photos may take, inside the step's own signal. */\nexport const AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS = 20_000\n\n/** Searches one page may ask, cached ones included. */\nexport const AI_LAYOUT_STOCK_SEARCHES_PER_PAGE = 8\n\n/** The hits a pick is made among. */\nexport const AI_LAYOUT_STOCK_PICK_WINDOW = 8\n\n/** The largest photo copied: inside one direct upload. */\nexport const AI_LAYOUT_STOCK_MAX_BYTES = 3 * 1024 * 1024\n\n/** Words that carry no subject. */\nconst STOPWORDS = new Set(\n (\n 'a an the and or of in on at to for with from by into onto over under near our your their my his her its ' +\n 'this that these those is are be being was were who whom which where while as about than then very just ' +\n 'small local best new top quality premium family-owned family owned independent professional ' +\n 'business company shop services service site website page photo picture image showing shows show ' +\n 'someone people person one two three some many several'\n ).split(' '),\n)\n\n/** Lowercase words, letters only, without stopwords. */\nfunction contentWords(text: string): string[] {\n return text\n .toLowerCase()\n .replace(/https?:\\/\\/\\S+|\\S+@\\S+|\\+?\\d[\\d\\s().-]{5,}\\d/g, ' ')\n .replace(/[^a-zÀ-ɏ\\s-]/g, ' ')\n .split(/\\s+/)\n .map((word) => word.replace(/^-+|-+$/g, ''))\n .filter((word) => word.length > 1 && !STOPWORDS.has(word))\n}\n\n/**\n * The kind of business in a few words: the first clause of the business\n * type, before a place or an audience, its last three content words (the\n * head of an English noun phrase is at its end).\n */\nexport function aiStockBusinessWords(businessType: string): string {\n const clause = businessType\n .toLowerCase()\n .split(/[,;:.()]|\\s[-–—]\\s|\\b(?:in|for|near|serving|based|located|that|which|who|with|from|since|and|&)\\b/)[0]\n const words = contentWords(clause ?? '')\n return words.slice(-3).join(' ')\n}\n\n/** A picture's subject in a few words: its alt text's first content words, else its section's name. */\nexport function aiStockSubjectWords(alt: string, sectionName: string): string {\n const words = contentWords(alt)\n if (words.length) return words.slice(0, 3).join(' ')\n return contentWords(sectionName).slice(0, 2).join(' ')\n}\n\nconst clip = (query: string) => query.replace(/\\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUERY_MAX_CHARS).trim()\n\n/** The frame's orientation: a wide frame wants a landscape photo, a tall one a portrait. */\nexport function aiStockOrientation(aspect: number): StockPhotoOrientation {\n if (aspect > 1.15) return 'horizontal'\n if (aspect < 0.87) return 'vertical'\n return 'any'\n}\n\n/** The searches a slot tries, in order, most specific first, each distinct. */\nexport function aiStockSearchesFor(\n slot: Pick<AiLayoutPictureSlot, 'role' | 'alt' | 'aspect'>,\n words: { business: string; subject: string },\n): StockPhotoSearchRequest[] {\n const orientation = aiStockOrientation(slot.aspect)\n const size =\n slot.role === 'hero'\n ? { minWidth: 1600 }\n : orientation === 'vertical'\n ? { minHeight: 900 }\n : { minWidth: 900 }\n const queries =\n slot.role === 'hero'\n ? [words.business, `${words.business} ${words.subject}`]\n : slot.role === 'about'\n ? [`${words.business} ${words.subject}`, words.business]\n : [`${words.business} ${words.subject}`, words.subject, words.business]\n const seen = new Set<string>()\n const searches: StockPhotoSearchRequest[] = []\n for (const raw of queries) {\n const query = clip(raw)\n if (!query || seen.has(query)) continue\n seen.add(query)\n searches.push({\n query,\n orientation,\n ...size,\n ...(slot.role === 'about' ? { people: true } : {}),\n })\n }\n return searches\n}\n\n/** The pick among a search's hits: not on the page yet, among the first few, by the seed. */\nexport function aiStockPick(\n photos: readonly StockPhoto[],\n used: ReadonlySet<string>,\n seed: string,\n): StockPhoto | null {\n const fresh = photos.filter((photo) => !used.has(stockPhotoSourceKey(photo))).slice(0, AI_LAYOUT_STOCK_PICK_WINDOW)\n if (!fresh.length) return null\n return fresh[aiLayoutSeedNumber(seed) % fresh.length] ?? null\n}\n\nconst EXTENSIONS: Record<string, string> = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' }\n\nexport interface AiLayoutStockPhotoSourceInput {\n hostId: string\n /** The member the photos are stored as: the job's creator. */\n uid: string\n /** The job's seed, as the starter photos turn by. */\n seed: string\n /** The site's business type, else the job's brief. */\n business: string\n /** The page's section names, by plan index. */\n sectionNames: readonly string[]\n signal?: AbortSignal\n}\n\nexport interface AiLayoutStockPhotoDeps {\n provider?: () => StockPhotoProvider | null\n ingest?: () => PluginMediaIngest | null\n budgetMs?: number\n}\n\n/**\n * The source for one page, or `null` when this deployment has no stock\n * photo library or no media door, which leaves every slot to the starters.\n */\nexport function aiLayoutStockPhotoSource(\n input: AiLayoutStockPhotoSourceInput,\n deps: AiLayoutStockPhotoDeps = {},\n): AiLayoutPictureSource | null {\n const provider = (deps.provider ?? stockPhotoProvider)()\n const ingest = (deps.ingest ?? pluginMediaIngest)()\n if (!provider || !ingest || !input.hostId || !input.uid) return null\n const business = aiStockBusinessWords(input.business)\n return async (slots) => {\n const timeout = AbortSignal.timeout(deps.budgetMs ?? AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS)\n const signal = input.signal ? AbortSignal.any([input.signal, timeout]) : timeout\n const used = new Set<string>()\n let searches = 0\n let storing = true\n const photos: Array<AiLayoutPicturePhoto | null> = []\n for (const [index, slot] of slots.entries()) {\n if (signal.aborted) {\n photos.push(null)\n continue\n }\n const subject = aiStockSubjectWords(slot.alt, input.sectionNames[slot.sectionIndex] ?? '')\n let placed: AiLayoutPicturePhoto | null = null\n for (const request of aiStockSearchesFor(slot, { business, subject })) {\n if (placed || signal.aborted || searches >= AI_LAYOUT_STOCK_SEARCHES_PER_PAGE) break\n searches += 1\n let found: StockPhoto[]\n try {\n found = (await provider.search(request, { signal }))?.photos ?? []\n } catch {\n found = []\n }\n const photo = aiStockPick(found, used, `${input.seed}:${index}:${request.query}`)\n if (!photo) continue\n const sourceKey = stockPhotoSourceKey(photo)\n used.add(sourceKey)\n try {\n const kept = await ingest.findStockPhoto({ hostId: input.hostId, sourceKey })\n if (kept) {\n placed = { src: kept.src, width: kept.width ?? photo.width, height: kept.height ?? photo.height }\n break\n }\n // A refusal to store (the storage band, a lockdown) holds for the\n // whole page, so the rest of it reuses what it can and copies nothing.\n if (!storing) continue\n const bytes = await provider.download(photo, { maxBytes: AI_LAYOUT_STOCK_MAX_BYTES, signal })\n if (!bytes || signal.aborted) continue\n const credit = provider.credit(photo)\n const stored = await ingest.ingest({\n hostId: input.hostId,\n uid: input.uid,\n fileName: `${photo.provider}-${photo.id}.${EXTENSIONS[bytes.contentType] ?? 'jpg'}`,\n contentType: bytes.contentType,\n bytes: bytes.bytes,\n alt: slot.alt,\n description: credit.text,\n stockPhoto: {\n key: sourceKey,\n provider: photo.provider,\n providerLabel: credit.providerLabel,\n id: photo.id,\n pageUrl: photo.pageUrl,\n photographer: photo.photographer,\n ...(photo.photographerUrl ? { photographerUrl: photo.photographerUrl } : {}),\n license: credit.license,\n licenseUrl: credit.licenseUrl,\n attributionRequired: credit.attributionRequired,\n query: request.query,\n },\n })\n if (stored.ok) {\n placed = { src: stored.src, width: stored.width ?? photo.width, height: stored.height ?? photo.height }\n } else {\n storing = false\n const refusal = stored as Extract<PluginMediaIngestResult, { ok: false }>\n console.warn('ai layout pictures: the media library refused a stock photo', {\n status: refusal.status,\n reason: refusal.reason,\n })\n }\n } catch (error) {\n console.warn('ai layout pictures: a stock photo could not be kept', { error: String(error) })\n }\n }\n photos.push(placed)\n }\n return photos\n }\n}\n"],"names":["pluginMediaIngest","STOCK_PHOTO_QUERY_MAX_CHARS","stockPhotoProvider","stockPhotoSourceKey","aiLayoutSeedNumber","AI_LAYOUT_STOCK_PHOTOS_BUDGET_MS","AI_LAYOUT_STOCK_SEARCHES_PER_PAGE","AI_LAYOUT_STOCK_PICK_WINDOW","AI_LAYOUT_STOCK_MAX_BYTES","STOPWORDS","Set","split","contentWords","text","toLowerCase","replace","map","word","filter","length","has","aiStockBusinessWords","businessType","clause","words","slice","join","aiStockSubjectWords","alt","sectionName","clip","query","trim","aiStockOrientation","aspect","aiStockSearchesFor","slot","orientation","size","role","minWidth","minHeight","queries","business","subject","seen","searches","raw","add","push","people","aiStockPick","photos","used","seed","fresh","photo","EXTENSIONS","aiLayoutStockPhotoSource","input","deps","provider","ingest","hostId","uid","slots","timeout","AbortSignal","budgetMs","signal","any","storing","index","entries","aborted","sectionNames","sectionIndex","placed","request","found","search","sourceKey","kept","findStockPhoto","src","width","height","bytes","download","maxBytes","credit","stored","fileName","id","contentType","description","stockPhoto","key","providerLabel","pageUrl","photographer","photographerUrl","license","licenseUrl","attributionRequired","ok","refusal","console","warn","status","reason","error","String"],"mappings":"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"}
|
|
@@ -80,6 +80,13 @@ interface NavEntry {
|
|
|
80
80
|
screenId: string;
|
|
81
81
|
label: string;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The blog paths a start's layout links and must not publish (AGL-3660): its
|
|
85
|
+
* header, phone menu and footer link the blog by path while the posts part is
|
|
86
|
+
* still owed, so a start whose posts part then failed or was skipped drops
|
|
87
|
+
* every address the blog could have taken. None while the blog was written.
|
|
88
|
+
*/
|
|
89
|
+
export declare function aiSiteUnwrittenBlogHrefs(blogUnwritten: boolean | undefined): string[];
|
|
83
90
|
/**
|
|
84
91
|
* The layout's nodes with its links to the retired starter home pointed at
|
|
85
92
|
* the new home, and a link for each proposed entry the header does not
|
|
@@ -91,6 +98,8 @@ export declare function aiLayoutWithNavigation(nodes: NodesMap, input: {
|
|
|
91
98
|
entries: readonly NavEntry[];
|
|
92
99
|
retiredHomeId?: string | null;
|
|
93
100
|
homeId?: string | null;
|
|
101
|
+
/** Paths whose links come out: a blog the start linked and never wrote (AGL-3660). */
|
|
102
|
+
droppedHrefs?: readonly string[];
|
|
94
103
|
}): NodesMap | null;
|
|
95
104
|
/**
|
|
96
105
|
* Publishes the pages a guided site start built. Never throws for a page; a
|
|
@@ -101,5 +110,7 @@ export declare function aiPublishGuidedSite(firestore: Firestore, input: {
|
|
|
101
110
|
job: Pick<AiJob, '$id' | 'orgId' | 'hostId'>;
|
|
102
111
|
outputs: readonly AiJobOutput[];
|
|
103
112
|
now: Date;
|
|
113
|
+
/** The start owed its blog's posts and wrote none (AGL-3660): its links to the blog come out. */
|
|
114
|
+
blogUnwritten?: boolean;
|
|
104
115
|
}, deps?: AiSitePublishDeps): Promise<AiJobSitePublish>;
|
|
105
116
|
export {};
|
|
@@ -22,6 +22,7 @@ import { decodeStoredNodes, encodeStoredNodes } from "@aglyn/aglyn/app-utils/sto
|
|
|
22
22
|
import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
|
|
23
23
|
import { sendGa4SitePublished } from "@aglyn/tenant-data-admin/server/ga4-measurement-protocol";
|
|
24
24
|
import { FieldValue } from "firebase-admin/firestore";
|
|
25
|
+
import { AI_SITE_BLOG_SLUGS } from "../model/ai-site-job.js";
|
|
25
26
|
/**
|
|
26
27
|
* Whether a build puts its pages live (AGL-3616): only where its request
|
|
27
28
|
* asked to (`publish`, set by Assist's proposal) AND the member ticked the
|
|
@@ -67,6 +68,14 @@ function navEntriesOf(outputs) {
|
|
|
67
68
|
}
|
|
68
69
|
return entries;
|
|
69
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The blog paths a start's layout links and must not publish (AGL-3660): its
|
|
73
|
+
* header, phone menu and footer link the blog by path while the posts part is
|
|
74
|
+
* still owed, so a start whose posts part then failed or was skipped drops
|
|
75
|
+
* every address the blog could have taken. None while the blog was written.
|
|
76
|
+
*/ export function aiSiteUnwrittenBlogHrefs(blogUnwritten) {
|
|
77
|
+
return blogUnwritten ? AI_SITE_BLOG_SLUGS.map((slug)=>`/${slug}`) : [];
|
|
78
|
+
}
|
|
70
79
|
/**
|
|
71
80
|
* The layout's nodes with its links to the retired starter home pointed at
|
|
72
81
|
* the new home, and a link for each proposed entry the header does not
|
|
@@ -74,6 +83,7 @@ function navEntriesOf(outputs) {
|
|
|
74
83
|
* `null` when nothing changes, or when the layout has no toolbar to hold an
|
|
75
84
|
* entry and no link to repoint.
|
|
76
85
|
*/ export function aiLayoutWithNavigation(nodes, input) {
|
|
86
|
+
var _input_droppedHrefs;
|
|
77
87
|
const map = {};
|
|
78
88
|
for (const [id, node] of Object.entries(nodes)){
|
|
79
89
|
map[id] = _extends({}, node, node.props ? {
|
|
@@ -85,6 +95,25 @@ function navEntriesOf(outputs) {
|
|
|
85
95
|
} : {});
|
|
86
96
|
}
|
|
87
97
|
let changed = false;
|
|
98
|
+
// The header, the phone menu and the footer link a blog by its path before
|
|
99
|
+
// its posts are written (AGL-3660); a start whose posts were not written
|
|
100
|
+
// publishes none of those links to a page that does not exist.
|
|
101
|
+
const dropped = new Set((_input_droppedHrefs = input.droppedHrefs) != null ? _input_droppedHrefs : []);
|
|
102
|
+
if (dropped.size) {
|
|
103
|
+
const gone = new Set(Object.entries(map).filter(([, node])=>{
|
|
104
|
+
var _ref;
|
|
105
|
+
var _node_props;
|
|
106
|
+
return node.componentId === 'muiScreenLink' && dropped.has(String((_ref = (_node_props = node.props) == null ? void 0 : _node_props['href']) != null ? _ref : ''));
|
|
107
|
+
}).map(([id])=>id));
|
|
108
|
+
if (gone.size) {
|
|
109
|
+
for (const id of gone)delete map[id];
|
|
110
|
+
for (const node of Object.values(map)){
|
|
111
|
+
var _node_nodes;
|
|
112
|
+
if ((_node_nodes = node.nodes) == null ? void 0 : _node_nodes.some((child)=>gone.has(child))) node.nodes = node.nodes.filter((child)=>!gone.has(child));
|
|
113
|
+
}
|
|
114
|
+
changed = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
88
117
|
const links = Object.values(map).filter((node)=>node.componentId === 'muiScreenLink');
|
|
89
118
|
if (input.retiredHomeId && input.homeId) {
|
|
90
119
|
for (const link of links){
|
|
@@ -223,6 +252,7 @@ function navEntriesOf(outputs) {
|
|
|
223
252
|
hostId,
|
|
224
253
|
pageIds: accepted.map((page)=>page.id),
|
|
225
254
|
entries: navEntriesOf(pages).filter((entry)=>entries[entry.screenId]),
|
|
255
|
+
droppedHrefs: aiSiteUnwrittenBlogHrefs(input.blogUnwritten),
|
|
226
256
|
retiredHomeId: releases,
|
|
227
257
|
homeId: (_ref = home == null ? void 0 : home.id) != null ? _ref : null,
|
|
228
258
|
jobId: job.$id,
|
|
@@ -337,7 +367,7 @@ function navEntriesOf(outputs) {
|
|
|
337
367
|
* their navigation entries and the home link moved off a retired starter;
|
|
338
368
|
* `null` when there is nothing to change or no one layout to change.
|
|
339
369
|
*/ async function aiNavigationLayoutWrite(firestore, input) {
|
|
340
|
-
if (!input.entries.length && !input.retiredHomeId) return null;
|
|
370
|
+
if (!input.entries.length && !input.retiredHomeId && !input.droppedHrefs.length) return null;
|
|
341
371
|
const hostRef = firestore.collection('hosts').doc(input.hostId);
|
|
342
372
|
const layoutIds = new Set();
|
|
343
373
|
for (const id of input.pageIds){
|