@aglyn/plugins-ai 1.0.0-beta.254 → 1.0.0-beta.256

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.
Files changed (71) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/assist-signals-page.component.js +5 -6
  3. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  4. package/src/lib/components/staff-org-ai-card.component.d.ts +0 -10
  5. package/src/lib/components/staff-org-ai-card.component.js +10 -79
  6. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  7. package/src/lib/components/staff-org-ai-conversations.component.js +8 -41
  8. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
  9. package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
  10. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  11. package/src/lib/jobs/ai-job-page-language.d.ts +14 -1
  12. package/src/lib/jobs/ai-job-page-language.js +18 -3
  13. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  14. package/src/lib/jobs/ai-job-page-step.js +17 -3
  15. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  16. package/src/lib/jobs/ai-job-plan-step.js +4 -0
  17. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  18. package/src/lib/jobs/ai-layout-stock-photos.d.ts +30 -0
  19. package/src/lib/jobs/ai-layout-stock-photos.js +76 -8
  20. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  21. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.d.ts +95 -0
  22. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.js +137 -0
  23. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.js.map +1 -0
  24. package/src/lib/layout-language/ai-layout-labels.d.ts +2 -0
  25. package/src/lib/layout-language/ai-layout-labels.js +12 -6
  26. package/src/lib/layout-language/ai-layout-labels.js.map +1 -1
  27. package/src/lib/layout-language/ai-layout-pictures.d.ts +12 -3
  28. package/src/lib/layout-language/ai-layout-pictures.js +6 -3
  29. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  30. package/src/lib/model/ai-products-bulk.d.ts +48 -0
  31. package/src/lib/model/ai-products-bulk.js +56 -0
  32. package/src/lib/model/ai-products-bulk.js.map +1 -0
  33. package/src/lib/model/ai-products.d.ts +2 -4
  34. package/src/lib/model/ai-products.js +4 -9
  35. package/src/lib/model/ai-products.js.map +1 -1
  36. package/src/lib/providers/anthropic.js +26 -5
  37. package/src/lib/providers/anthropic.js.map +1 -1
  38. package/src/lib/providers/contract.d.ts +10 -0
  39. package/src/lib/providers/contract.js.map +1 -1
  40. package/src/lib/runtime/ai-doctrine-validators.d.ts +20 -0
  41. package/src/lib/runtime/ai-doctrine-validators.js +50 -0
  42. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  43. package/src/lib/runtime/ai-doctrine.d.ts +11 -0
  44. package/src/lib/runtime/ai-doctrine.js +9 -4
  45. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  46. package/src/lib/runtime/ai-runtime.d.ts +6 -0
  47. package/src/lib/runtime/ai-runtime.js +10 -0
  48. package/src/lib/runtime/ai-runtime.js.map +1 -1
  49. package/src/lib/server/ai-admin-conversations.d.ts +2 -0
  50. package/src/lib/server/ai-admin-conversations.js +40 -7
  51. package/src/lib/server/ai-admin-conversations.js.map +1 -1
  52. package/src/lib/server/ai-admin-org.js +28 -2
  53. package/src/lib/server/ai-admin-org.js.map +1 -1
  54. package/src/lib/server/ai-admin-orgs-spend.js +19 -1
  55. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
  56. package/src/lib/server/ai-admin-signals.js +11 -3
  57. package/src/lib/server/ai-admin-signals.js.map +1 -1
  58. package/src/lib/server/ai-admin-user.js +11 -1
  59. package/src/lib/server/ai-admin-user.js.map +1 -1
  60. package/src/lib/usage/ai-usage-wire.d.ts +5 -2
  61. package/src/lib/usage/ai-usage-wire.js +5 -2
  62. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  63. package/src/lib/usage/assist-signals-view.d.ts +123 -0
  64. package/src/lib/usage/assist-signals-view.js +151 -0
  65. package/src/lib/usage/assist-signals-view.js.map +1 -0
  66. package/src/lib/usage/staff-org-ai-conversations-view.d.ts +51 -0
  67. package/src/lib/usage/staff-org-ai-conversations-view.js +127 -0
  68. package/src/lib/usage/staff-org-ai-conversations-view.js.map +1 -0
  69. package/src/lib/usage/staff-org-ai-view.d.ts +181 -0
  70. package/src/lib/usage/staff-org-ai-view.js +263 -0
  71. package/src/lib/usage/staff-org-ai-view.js.map +1 -0
@@ -0,0 +1,95 @@
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
+ /**
18
+ * RECORDED, NOT GOLDEN: the `submit_build_plan` input the real model gave a
19
+ * Free guided start for "a family dental practice" (Maple Street Dental) in
20
+ * the live site-plan eval of 2026-10-15 (claude-haiku-5-5, replay entry
21
+ * 4177de69…, on a site as provisioning leaves it), copied verbatim. Every page
22
+ * is put in the site's layout `laySite`, and every section that places no
23
+ * form names `laySite` again in its `uses`: rule 2's
24
+ * `plan-layout-in-section` refused both attempts, and the job stopped at
25
+ * needs_input with its credits refunded.
26
+ */
27
+ export declare const AI_DENTAL_SITE_PLAN_ANSWER: {
28
+ readonly reuse: readonly [{
29
+ readonly kind: "layout";
30
+ readonly id: "laySite";
31
+ readonly purpose: "shared site header, navigation and footer for both pages";
32
+ }];
33
+ readonly create: readonly [{
34
+ readonly kind: "form";
35
+ readonly name: "bookingForm";
36
+ readonly why: "The site has no saved form, and the brief needs a booking request form that routes submissions to the practice inbox.";
37
+ readonly duplicateOf: any;
38
+ readonly fields: readonly ["parentName:text", "childName:text", "childAge:number", "phone:phone", "email:email", "preferredDay:text", "visitReason:select", "consent:checkbox"];
39
+ }];
40
+ readonly screens: readonly [{
41
+ readonly title: "Home";
42
+ readonly slug: "/";
43
+ readonly layout: "laySite";
44
+ readonly template: any;
45
+ readonly duplicateOf: "scrStarter";
46
+ readonly nav: true;
47
+ readonly seoTitle: "Maple Street Dental | Family Dentist for Kids and Parents";
48
+ readonly seoDescription: "Gentle family dental care at Maple Street Dental. Check-ups, cleanings and sealants for children and parents. Book a visit online.";
49
+ readonly sections: readonly [{
50
+ readonly name: "hero with headline, short intro and book-a-visit button";
51
+ readonly uses: readonly ["laySite"];
52
+ readonly items: 0;
53
+ }, {
54
+ readonly name: "services list: check-ups, cleanings, sealants, emergency visits";
55
+ readonly uses: readonly ["laySite"];
56
+ readonly items: 4;
57
+ }, {
58
+ readonly name: "why us: a kind team, a calm office and flexible family scheduling";
59
+ readonly uses: readonly ["laySite"];
60
+ readonly items: 3;
61
+ }, {
62
+ readonly name: "parent testimonials";
63
+ readonly uses: readonly ["laySite"];
64
+ readonly items: 3;
65
+ }, {
66
+ readonly name: "closing call to action to book a visit with the booking form";
67
+ readonly uses: readonly ["new:bookingForm"];
68
+ readonly items: 0;
69
+ }];
70
+ }, {
71
+ readonly title: "Book a Visit";
72
+ readonly slug: "/book-a-visit";
73
+ readonly layout: "laySite";
74
+ readonly template: any;
75
+ readonly duplicateOf: any;
76
+ readonly nav: true;
77
+ readonly seoTitle: "Book a Family Dental Visit | Maple Street Dental";
78
+ readonly seoDescription: "Request a check-up or cleaning for your child at Maple Street Dental. Share your preferred day and we will confirm your appointment.";
79
+ readonly sections: readonly [{
80
+ readonly name: "page intro: how booking works and office hours [hours to confirm]";
81
+ readonly uses: readonly ["laySite"];
82
+ readonly items: 0;
83
+ }, {
84
+ readonly name: "booking request form";
85
+ readonly uses: readonly ["new:bookingForm"];
86
+ readonly items: 0;
87
+ }, {
88
+ readonly name: "contact details: address, phone and parking [to confirm]";
89
+ readonly uses: readonly ["laySite"];
90
+ readonly items: 0;
91
+ }];
92
+ }];
93
+ readonly language: "en";
94
+ readonly photoWords: any;
95
+ };
@@ -0,0 +1,137 @@
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
+ * RECORDED, NOT GOLDEN: the `submit_build_plan` input the real model gave a
18
+ * Free guided start for "a family dental practice" (Maple Street Dental) in
19
+ * the live site-plan eval of 2026-10-15 (claude-haiku-5-5, replay entry
20
+ * 4177de69…, on a site as provisioning leaves it), copied verbatim. Every page
21
+ * is put in the site's layout `laySite`, and every section that places no
22
+ * form names `laySite` again in its `uses`: rule 2's
23
+ * `plan-layout-in-section` refused both attempts, and the job stopped at
24
+ * needs_input with its credits refunded.
25
+ */ export const AI_DENTAL_SITE_PLAN_ANSWER = {
26
+ reuse: [
27
+ {
28
+ kind: 'layout',
29
+ id: 'laySite',
30
+ purpose: 'shared site header, navigation and footer for both pages'
31
+ }
32
+ ],
33
+ create: [
34
+ {
35
+ kind: 'form',
36
+ name: 'bookingForm',
37
+ why: 'The site has no saved form, and the brief needs a booking request form that routes submissions to the practice inbox.',
38
+ duplicateOf: null,
39
+ fields: [
40
+ 'parentName:text',
41
+ 'childName:text',
42
+ 'childAge:number',
43
+ 'phone:phone',
44
+ 'email:email',
45
+ 'preferredDay:text',
46
+ 'visitReason:select',
47
+ 'consent:checkbox'
48
+ ]
49
+ }
50
+ ],
51
+ screens: [
52
+ {
53
+ title: 'Home',
54
+ slug: '/',
55
+ layout: 'laySite',
56
+ template: null,
57
+ duplicateOf: 'scrStarter',
58
+ nav: true,
59
+ seoTitle: 'Maple Street Dental | Family Dentist for Kids and Parents',
60
+ seoDescription: 'Gentle family dental care at Maple Street Dental. Check-ups, cleanings and sealants for children and parents. Book a visit online.',
61
+ sections: [
62
+ {
63
+ name: 'hero with headline, short intro and book-a-visit button',
64
+ uses: [
65
+ 'laySite'
66
+ ],
67
+ items: 0
68
+ },
69
+ {
70
+ name: 'services list: check-ups, cleanings, sealants, emergency visits',
71
+ uses: [
72
+ 'laySite'
73
+ ],
74
+ items: 4
75
+ },
76
+ {
77
+ name: 'why us: a kind team, a calm office and flexible family scheduling',
78
+ uses: [
79
+ 'laySite'
80
+ ],
81
+ items: 3
82
+ },
83
+ {
84
+ name: 'parent testimonials',
85
+ uses: [
86
+ 'laySite'
87
+ ],
88
+ items: 3
89
+ },
90
+ {
91
+ name: 'closing call to action to book a visit with the booking form',
92
+ uses: [
93
+ 'new:bookingForm'
94
+ ],
95
+ items: 0
96
+ }
97
+ ]
98
+ },
99
+ {
100
+ title: 'Book a Visit',
101
+ slug: '/book-a-visit',
102
+ layout: 'laySite',
103
+ template: null,
104
+ duplicateOf: null,
105
+ nav: true,
106
+ seoTitle: 'Book a Family Dental Visit | Maple Street Dental',
107
+ seoDescription: 'Request a check-up or cleaning for your child at Maple Street Dental. Share your preferred day and we will confirm your appointment.',
108
+ sections: [
109
+ {
110
+ name: 'page intro: how booking works and office hours [hours to confirm]',
111
+ uses: [
112
+ 'laySite'
113
+ ],
114
+ items: 0
115
+ },
116
+ {
117
+ name: 'booking request form',
118
+ uses: [
119
+ 'new:bookingForm'
120
+ ],
121
+ items: 0
122
+ },
123
+ {
124
+ name: 'contact details: address, phone and parking [to confirm]',
125
+ uses: [
126
+ 'laySite'
127
+ ],
128
+ items: 0
129
+ }
130
+ ]
131
+ }
132
+ ],
133
+ language: 'en',
134
+ photoWords: null
135
+ };
136
+
137
+ //# sourceMappingURL=ai-dental-site-plan-recording.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/ai/src/lib/jobs/fixtures/ai-dental-site-plan-recording.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\n/**\n * RECORDED, NOT GOLDEN: the `submit_build_plan` input the real model gave a\n * Free guided start for \"a family dental practice\" (Maple Street Dental) in\n * the live site-plan eval of 2026-10-15 (claude-haiku-5-5, replay entry\n * 4177de69…, on a site as provisioning leaves it), copied verbatim. Every page\n * is put in the site's layout `laySite`, and every section that places no\n * form names `laySite` again in its `uses`: rule 2's\n * `plan-layout-in-section` refused both attempts, and the job stopped at\n * needs_input with its credits refunded.\n */\nexport const AI_DENTAL_SITE_PLAN_ANSWER = {\n reuse: [\n {\n kind: 'layout',\n id: 'laySite',\n purpose: 'shared site header, navigation and footer for both pages',\n },\n ],\n create: [\n {\n kind: 'form',\n name: 'bookingForm',\n why: 'The site has no saved form, and the brief needs a booking request form that routes submissions to the practice inbox.',\n duplicateOf: null,\n fields: [\n 'parentName:text',\n 'childName:text',\n 'childAge:number',\n 'phone:phone',\n 'email:email',\n 'preferredDay:text',\n 'visitReason:select',\n 'consent:checkbox',\n ],\n },\n ],\n screens: [\n {\n title: 'Home',\n slug: '/',\n layout: 'laySite',\n template: null,\n duplicateOf: 'scrStarter',\n nav: true,\n seoTitle: 'Maple Street Dental | Family Dentist for Kids and Parents',\n seoDescription:\n 'Gentle family dental care at Maple Street Dental. Check-ups, cleanings and sealants for children and parents. Book a visit online.',\n sections: [\n {\n name: 'hero with headline, short intro and book-a-visit button',\n uses: ['laySite'],\n items: 0,\n },\n {\n name: 'services list: check-ups, cleanings, sealants, emergency visits',\n uses: ['laySite'],\n items: 4,\n },\n {\n name: 'why us: a kind team, a calm office and flexible family scheduling',\n uses: ['laySite'],\n items: 3,\n },\n {\n name: 'parent testimonials',\n uses: ['laySite'],\n items: 3,\n },\n {\n name: 'closing call to action to book a visit with the booking form',\n uses: ['new:bookingForm'],\n items: 0,\n },\n ],\n },\n {\n title: 'Book a Visit',\n slug: '/book-a-visit',\n layout: 'laySite',\n template: null,\n duplicateOf: null,\n nav: true,\n seoTitle: 'Book a Family Dental Visit | Maple Street Dental',\n seoDescription:\n 'Request a check-up or cleaning for your child at Maple Street Dental. Share your preferred day and we will confirm your appointment.',\n sections: [\n {\n name: 'page intro: how booking works and office hours [hours to confirm]',\n uses: ['laySite'],\n items: 0,\n },\n {\n name: 'booking request form',\n uses: ['new:bookingForm'],\n items: 0,\n },\n {\n name: 'contact details: address, phone and parking [to confirm]',\n uses: ['laySite'],\n items: 0,\n },\n ],\n },\n ],\n language: 'en',\n photoWords: null,\n} as const\n"],"names":["AI_DENTAL_SITE_PLAN_ANSWER","reuse","kind","id","purpose","create","name","why","duplicateOf","fields","screens","title","slug","layout","template","nav","seoTitle","seoDescription","sections","uses","items","language","photoWords"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GACD,OAAO,MAAMA,6BAA6B;IACxCC,OAAO;QACL;YACEC,MAAM;YACNC,IAAI;YACJC,SAAS;QACX;KACD;IACDC,QAAQ;QACN;YACEH,MAAM;YACNI,MAAM;YACNC,KAAK;YACLC,aAAa;YACbC,QAAQ;gBACN;gBACA;gBACA;gBACA;gBACA;gBACA;gBACA;gBACA;aACD;QACH;KACD;IACDC,SAAS;QACP;YACEC,OAAO;YACPC,MAAM;YACNC,QAAQ;YACRC,UAAU;YACVN,aAAa;YACbO,KAAK;YACLC,UAAU;YACVC,gBACE;YACFC,UAAU;gBACR;oBACEZ,MAAM;oBACNa,MAAM;wBAAC;qBAAU;oBACjBC,OAAO;gBACT;gBACA;oBACEd,MAAM;oBACNa,MAAM;wBAAC;qBAAU;oBACjBC,OAAO;gBACT;gBACA;oBACEd,MAAM;oBACNa,MAAM;wBAAC;qBAAU;oBACjBC,OAAO;gBACT;gBACA;oBACEd,MAAM;oBACNa,MAAM;wBAAC;qBAAU;oBACjBC,OAAO;gBACT;gBACA;oBACEd,MAAM;oBACNa,MAAM;wBAAC;qBAAkB;oBACzBC,OAAO;gBACT;aACD;QACH;QACA;YACET,OAAO;YACPC,MAAM;YACNC,QAAQ;YACRC,UAAU;YACVN,aAAa;YACbO,KAAK;YACLC,UAAU;YACVC,gBACE;YACFC,UAAU;gBACR;oBACEZ,MAAM;oBACNa,MAAM;wBAAC;qBAAU;oBACjBC,OAAO;gBACT;gBACA;oBACEd,MAAM;oBACNa,MAAM;wBAAC;qBAAkB;oBACzBC,OAAO;gBACT;gBACA;oBACEd,MAAM;oBACNa,MAAM;wBAAC;qBAAU;oBACjBC,OAAO;gBACT;aACD;QACH;KACD;IACDC,UAAU;IACVC,YAAY;AACd,EAAU"}
@@ -60,6 +60,8 @@ export declare const AI_LAYOUT_LABELS_EN: {
60
60
  readonly storeEmptyTextAlone: "Our first pieces are being finished now. Check back soon.";
61
61
  /** The way to get in touch under that empty grid, where the site has a page for it. */
62
62
  readonly getInTouch: "Get in touch";
63
+ /** A stock photo's alt text where the library's own words cannot describe it in the site's language. */
64
+ readonly illustrativePhoto: "Illustrative photo";
63
65
  };
64
66
  export type AiLayoutLabel = keyof typeof AI_LAYOUT_LABELS_EN;
65
67
  /** A language code's two-letter language, lowercase: `es` of `es-MX`; empty for none. */
@@ -40,7 +40,8 @@
40
40
  /** A store's Product grid while the store lists nothing yet. */ storeEmptyTitle: 'New pieces are on the way',
41
41
  storeEmptyText: 'Our first pieces are being finished now. Get in touch and we will let you know the moment they land.',
42
42
  storeEmptyTextAlone: 'Our first pieces are being finished now. Check back soon.',
43
- /** The way to get in touch under that empty grid, where the site has a page for it. */ getInTouch: 'Get in touch'
43
+ /** The way to get in touch under that empty grid, where the site has a page for it. */ getInTouch: 'Get in touch',
44
+ /** A stock photo's alt text where the library's own words cannot describe it in the site's language. */ illustrativePhoto: 'Illustrative photo'
44
45
  };
45
46
  /** The same labels in each language listed, by its two-letter code. */ const LABELS = {
46
47
  es: {
@@ -61,7 +62,8 @@
61
62
  storeEmptyTitle: 'Pronto llegarán nuevas piezas',
62
63
  storeEmptyText: 'Estamos terminando nuestras primeras piezas. Escríbenos y te avisaremos en cuanto lleguen.',
63
64
  storeEmptyTextAlone: 'Estamos terminando nuestras primeras piezas. Vuelve pronto.',
64
- getInTouch: 'Escríbenos'
65
+ getInTouch: 'Escríbenos',
66
+ illustrativePhoto: 'Foto ilustrativa'
65
67
  },
66
68
  fr: {
67
69
  seeAll: 'Tout voir',
@@ -81,7 +83,8 @@
81
83
  storeEmptyTitle: 'De nouvelles pièces arrivent bientôt',
82
84
  storeEmptyText: 'Nous terminons nos premières pièces. Écrivez-nous et nous vous préviendrons dès leur arrivée.',
83
85
  storeEmptyTextAlone: 'Nous terminons nos premières pièces. Revenez bientôt.',
84
- getInTouch: 'Écrivez-nous'
86
+ getInTouch: 'Écrivez-nous',
87
+ illustrativePhoto: 'Photo d’illustration'
85
88
  },
86
89
  pt: {
87
90
  seeAll: 'Ver tudo',
@@ -101,7 +104,8 @@
101
104
  storeEmptyTitle: 'Novas peças estão a caminho',
102
105
  storeEmptyText: 'Estamos finalizando nossas primeiras peças. Fale conosco e avisaremos assim que chegarem.',
103
106
  storeEmptyTextAlone: 'Estamos finalizando nossas primeiras peças. Volte em breve.',
104
- getInTouch: 'Fale conosco'
107
+ getInTouch: 'Fale conosco',
108
+ illustrativePhoto: 'Imagem ilustrativa'
105
109
  },
106
110
  de: {
107
111
  seeAll: 'Alle ansehen',
@@ -121,7 +125,8 @@
121
125
  storeEmptyTitle: 'Neue Stücke sind unterwegs',
122
126
  storeEmptyText: 'Unsere ersten Stücke werden gerade fertig. Schreiben Sie uns, und wir melden uns, sobald sie da sind.',
123
127
  storeEmptyTextAlone: 'Unsere ersten Stücke werden gerade fertig. Schauen Sie bald wieder vorbei.',
124
- getInTouch: 'Kontakt aufnehmen'
128
+ getInTouch: 'Kontakt aufnehmen',
129
+ illustrativePhoto: 'Symbolbild'
125
130
  },
126
131
  it: {
127
132
  seeAll: 'Vedi tutto',
@@ -141,7 +146,8 @@
141
146
  storeEmptyTitle: 'Nuovi pezzi in arrivo',
142
147
  storeEmptyText: 'Stiamo finendo i nostri primi pezzi. Scrivici e ti avviseremo appena arrivano.',
143
148
  storeEmptyTextAlone: 'Stiamo finendo i nostri primi pezzi. Torna presto.',
144
- getInTouch: 'Scrivici'
149
+ getInTouch: 'Scrivici',
150
+ illustrativePhoto: 'Immagine illustrativa'
145
151
  }
146
152
  };
147
153
  /** A language code's two-letter language, lowercase: `es` of `es-MX`; empty for none. */ export function aiLayoutLanguageOf(language) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-labels.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\n/**\n * THE WORDS THE PLATFORM WRITES ON A PAGE ITSELF, IN THE SITE'S LANGUAGE (AGL-3760).\n *\n * The model writes a page's copy in the language its brief is in; a few\n * labels are the compiler's own — the button under a featured list that opens\n * the whole of it, a post card's link, a store's fixed lines. A Spanish\n * guided start (beta.246) showed \"See all\" under \"Lo que horneamos\". Each\n * label is English, then the same words in the languages below; a site in a\n * language not listed keeps the English.\n */\n\n/** The labels the compiler and the frame place, in English. */\nexport const AI_LAYOUT_LABELS_EN = {\n /** The button under a featured band of the site's records that opens the whole list. */\n seeAll: 'See all',\n /** The same, for the blog's posts. */\n allPosts: 'All posts',\n /** The same, for the site's music. */\n allMusic: 'All music',\n /** The same, for the site's events. */\n allEvents: 'All events',\n /** The button a storefront's hero, or a featured band of products, opens the shop with. */\n shopAll: 'Shop all',\n /** A post card's link to the post. */\n readPost: 'Read the post',\n /** A record card's link to the record's own page. */\n viewDetails: 'View details',\n /** A store's newsletter sign-up button. */\n subscribe: 'Subscribe',\n /** A store's reviews band, where neither the design nor the plan heads it. */\n reviewsHeading: 'What our customers say',\n /** A store's sign-up band, where neither the design nor the plan heads it. */\n signupHeading: 'Join our newsletter',\n /** The header's link to a store's account page. */\n account: 'Account',\n /** The footer's heading over a store's account and policy links. */\n customerCare: 'Customer care',\n /** A store's announcement bar, where its design gave it no line. */\n storeAnnouncement: 'Shop the full collection',\n /** The way into the shop beside the design's own announcement. */\n shopNow: 'Shop now',\n /** A store's Product grid while the store lists nothing yet. */\n storeEmptyTitle: 'New pieces are on the way',\n storeEmptyText: 'Our first pieces are being finished now. Get in touch and we will let you know the moment they land.',\n storeEmptyTextAlone: 'Our first pieces are being finished now. Check back soon.',\n /** The way to get in touch under that empty grid, where the site has a page for it. */\n getInTouch: 'Get in touch',\n} as const\n\nexport type AiLayoutLabel = keyof typeof AI_LAYOUT_LABELS_EN\n\ntype Labels = Record<AiLayoutLabel, string>\n\n/** The same labels in each language listed, by its two-letter code. */\nconst LABELS: Readonly<Record<string, Labels>> = {\n es: {\n seeAll: 'Ver todo',\n allPosts: 'Todas las entradas',\n allMusic: 'Toda la música',\n allEvents: 'Todos los eventos',\n shopAll: 'Ver la tienda',\n readPost: 'Leer la entrada',\n viewDetails: 'Ver detalles',\n subscribe: 'Suscribirme',\n reviewsHeading: 'Lo que dicen nuestros clientes',\n signupHeading: 'Suscríbete a nuestro boletín',\n account: 'Mi cuenta',\n customerCare: 'Atención al cliente',\n storeAnnouncement: 'Descubre toda la colección',\n shopNow: 'Comprar ahora',\n storeEmptyTitle: 'Pronto llegarán nuevas piezas',\n storeEmptyText: 'Estamos terminando nuestras primeras piezas. Escríbenos y te avisaremos en cuanto lleguen.',\n storeEmptyTextAlone: 'Estamos terminando nuestras primeras piezas. Vuelve pronto.',\n getInTouch: 'Escríbenos',\n },\n fr: {\n seeAll: 'Tout voir',\n allPosts: 'Tous les articles',\n allMusic: 'Toute la musique',\n allEvents: 'Tous les événements',\n shopAll: 'Voir la boutique',\n readPost: 'Lire l’article',\n viewDetails: 'Voir les détails',\n subscribe: 'S’abonner',\n reviewsHeading: 'Ce que disent nos clients',\n signupHeading: 'Abonnez-vous à notre newsletter',\n account: 'Mon compte',\n customerCare: 'Service client',\n storeAnnouncement: 'Découvrez toute la collection',\n shopNow: 'Acheter',\n storeEmptyTitle: 'De nouvelles pièces arrivent bientôt',\n storeEmptyText: 'Nous terminons nos premières pièces. Écrivez-nous et nous vous préviendrons dès leur arrivée.',\n storeEmptyTextAlone: 'Nous terminons nos premières pièces. Revenez bientôt.',\n getInTouch: 'Écrivez-nous',\n },\n pt: {\n seeAll: 'Ver tudo',\n allPosts: 'Todos os posts',\n allMusic: 'Todas as músicas',\n allEvents: 'Todos os eventos',\n shopAll: 'Ver a loja',\n readPost: 'Ler o post',\n viewDetails: 'Ver detalhes',\n subscribe: 'Inscrever-me',\n reviewsHeading: 'O que dizem nossos clientes',\n signupHeading: 'Assine nossa newsletter',\n account: 'Minha conta',\n customerCare: 'Atendimento ao cliente',\n storeAnnouncement: 'Conheça toda a coleção',\n shopNow: 'Comprar agora',\n storeEmptyTitle: 'Novas peças estão a caminho',\n storeEmptyText: 'Estamos finalizando nossas primeiras peças. Fale conosco e avisaremos assim que chegarem.',\n storeEmptyTextAlone: 'Estamos finalizando nossas primeiras peças. Volte em breve.',\n getInTouch: 'Fale conosco',\n },\n de: {\n seeAll: 'Alle ansehen',\n allPosts: 'Alle Beiträge',\n allMusic: 'Alle Musik',\n allEvents: 'Alle Veranstaltungen',\n shopAll: 'Zum Shop',\n readPost: 'Beitrag lesen',\n viewDetails: 'Details ansehen',\n subscribe: 'Abonnieren',\n reviewsHeading: 'Was unsere Kunden sagen',\n signupHeading: 'Abonnieren Sie unseren Newsletter',\n account: 'Mein Konto',\n customerCare: 'Kundenservice',\n storeAnnouncement: 'Entdecken Sie die ganze Kollektion',\n shopNow: 'Jetzt kaufen',\n storeEmptyTitle: 'Neue Stücke sind unterwegs',\n storeEmptyText: 'Unsere ersten Stücke werden gerade fertig. Schreiben Sie uns, und wir melden uns, sobald sie da sind.',\n storeEmptyTextAlone: 'Unsere ersten Stücke werden gerade fertig. Schauen Sie bald wieder vorbei.',\n getInTouch: 'Kontakt aufnehmen',\n },\n it: {\n seeAll: 'Vedi tutto',\n allPosts: 'Tutti gli articoli',\n allMusic: 'Tutta la musica',\n allEvents: 'Tutti gli eventi',\n shopAll: 'Vai al negozio',\n readPost: 'Leggi l’articolo',\n viewDetails: 'Vedi dettagli',\n subscribe: 'Iscriviti',\n reviewsHeading: 'Cosa dicono i nostri clienti',\n signupHeading: 'Iscriviti alla nostra newsletter',\n account: 'Il mio account',\n customerCare: 'Assistenza clienti',\n storeAnnouncement: 'Scopri tutta la collezione',\n shopNow: 'Acquista ora',\n storeEmptyTitle: 'Nuovi pezzi in arrivo',\n storeEmptyText: 'Stiamo finendo i nostri primi pezzi. Scrivici e ti avviseremo appena arrivano.',\n storeEmptyTextAlone: 'Stiamo finendo i nostri primi pezzi. Torna presto.',\n getInTouch: 'Scrivici',\n },\n}\n\n/** A language code's two-letter language, lowercase: `es` of `es-MX`; empty for none. */\nexport function aiLayoutLanguageOf(language: string | null | undefined): string {\n return typeof language === 'string' ? (/^([a-z]{2})(?:[-_]|$)/i.exec(language.trim())?.[1] ?? '').toLowerCase() : ''\n}\n\n/** Whether a site's language is one other than English: a code that names one. */\nexport function aiLayoutSpeaksOtherThanEnglish(language: string | null | undefined): boolean {\n const code = aiLayoutLanguageOf(language)\n return code !== '' && code !== 'en'\n}\n\n/** A label the platform places, in the site's language where it is listed, else in English. */\nexport function aiLayoutLabel(key: AiLayoutLabel, language?: string | null): string {\n return LABELS[aiLayoutLanguageOf(language)]?.[key] ?? AI_LAYOUT_LABELS_EN[key]\n}\n"],"names":["AI_LAYOUT_LABELS_EN","seeAll","allPosts","allMusic","allEvents","shopAll","readPost","viewDetails","subscribe","reviewsHeading","signupHeading","account","customerCare","storeAnnouncement","shopNow","storeEmptyTitle","storeEmptyText","storeEmptyTextAlone","getInTouch","LABELS","es","fr","pt","de","it","aiLayoutLanguageOf","language","exec","trim","toLowerCase","aiLayoutSpeaksOtherThanEnglish","code","aiLayoutLabel","key"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-labels.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\n/**\n * THE WORDS THE PLATFORM WRITES ON A PAGE ITSELF, IN THE SITE'S LANGUAGE (AGL-3760).\n *\n * The model writes a page's copy in the language its brief is in; a few\n * labels are the compiler's own — the button under a featured list that opens\n * the whole of it, a post card's link, a store's fixed lines. A Spanish\n * guided start (beta.246) showed \"See all\" under \"Lo que horneamos\". Each\n * label is English, then the same words in the languages below; a site in a\n * language not listed keeps the English.\n */\n\n/** The labels the compiler and the frame place, in English. */\nexport const AI_LAYOUT_LABELS_EN = {\n /** The button under a featured band of the site's records that opens the whole list. */\n seeAll: 'See all',\n /** The same, for the blog's posts. */\n allPosts: 'All posts',\n /** The same, for the site's music. */\n allMusic: 'All music',\n /** The same, for the site's events. */\n allEvents: 'All events',\n /** The button a storefront's hero, or a featured band of products, opens the shop with. */\n shopAll: 'Shop all',\n /** A post card's link to the post. */\n readPost: 'Read the post',\n /** A record card's link to the record's own page. */\n viewDetails: 'View details',\n /** A store's newsletter sign-up button. */\n subscribe: 'Subscribe',\n /** A store's reviews band, where neither the design nor the plan heads it. */\n reviewsHeading: 'What our customers say',\n /** A store's sign-up band, where neither the design nor the plan heads it. */\n signupHeading: 'Join our newsletter',\n /** The header's link to a store's account page. */\n account: 'Account',\n /** The footer's heading over a store's account and policy links. */\n customerCare: 'Customer care',\n /** A store's announcement bar, where its design gave it no line. */\n storeAnnouncement: 'Shop the full collection',\n /** The way into the shop beside the design's own announcement. */\n shopNow: 'Shop now',\n /** A store's Product grid while the store lists nothing yet. */\n storeEmptyTitle: 'New pieces are on the way',\n storeEmptyText: 'Our first pieces are being finished now. Get in touch and we will let you know the moment they land.',\n storeEmptyTextAlone: 'Our first pieces are being finished now. Check back soon.',\n /** The way to get in touch under that empty grid, where the site has a page for it. */\n getInTouch: 'Get in touch',\n /** A stock photo's alt text where the library's own words cannot describe it in the site's language. */\n illustrativePhoto: 'Illustrative photo',\n} as const\n\nexport type AiLayoutLabel = keyof typeof AI_LAYOUT_LABELS_EN\n\ntype Labels = Record<AiLayoutLabel, string>\n\n/** The same labels in each language listed, by its two-letter code. */\nconst LABELS: Readonly<Record<string, Labels>> = {\n es: {\n seeAll: 'Ver todo',\n allPosts: 'Todas las entradas',\n allMusic: 'Toda la música',\n allEvents: 'Todos los eventos',\n shopAll: 'Ver la tienda',\n readPost: 'Leer la entrada',\n viewDetails: 'Ver detalles',\n subscribe: 'Suscribirme',\n reviewsHeading: 'Lo que dicen nuestros clientes',\n signupHeading: 'Suscríbete a nuestro boletín',\n account: 'Mi cuenta',\n customerCare: 'Atención al cliente',\n storeAnnouncement: 'Descubre toda la colección',\n shopNow: 'Comprar ahora',\n storeEmptyTitle: 'Pronto llegarán nuevas piezas',\n storeEmptyText: 'Estamos terminando nuestras primeras piezas. Escríbenos y te avisaremos en cuanto lleguen.',\n storeEmptyTextAlone: 'Estamos terminando nuestras primeras piezas. Vuelve pronto.',\n getInTouch: 'Escríbenos',\n illustrativePhoto: 'Foto ilustrativa',\n },\n fr: {\n seeAll: 'Tout voir',\n allPosts: 'Tous les articles',\n allMusic: 'Toute la musique',\n allEvents: 'Tous les événements',\n shopAll: 'Voir la boutique',\n readPost: 'Lire l’article',\n viewDetails: 'Voir les détails',\n subscribe: 'S’abonner',\n reviewsHeading: 'Ce que disent nos clients',\n signupHeading: 'Abonnez-vous à notre newsletter',\n account: 'Mon compte',\n customerCare: 'Service client',\n storeAnnouncement: 'Découvrez toute la collection',\n shopNow: 'Acheter',\n storeEmptyTitle: 'De nouvelles pièces arrivent bientôt',\n storeEmptyText: 'Nous terminons nos premières pièces. Écrivez-nous et nous vous préviendrons dès leur arrivée.',\n storeEmptyTextAlone: 'Nous terminons nos premières pièces. Revenez bientôt.',\n getInTouch: 'Écrivez-nous',\n illustrativePhoto: 'Photo d’illustration',\n },\n pt: {\n seeAll: 'Ver tudo',\n allPosts: 'Todos os posts',\n allMusic: 'Todas as músicas',\n allEvents: 'Todos os eventos',\n shopAll: 'Ver a loja',\n readPost: 'Ler o post',\n viewDetails: 'Ver detalhes',\n subscribe: 'Inscrever-me',\n reviewsHeading: 'O que dizem nossos clientes',\n signupHeading: 'Assine nossa newsletter',\n account: 'Minha conta',\n customerCare: 'Atendimento ao cliente',\n storeAnnouncement: 'Conheça toda a coleção',\n shopNow: 'Comprar agora',\n storeEmptyTitle: 'Novas peças estão a caminho',\n storeEmptyText: 'Estamos finalizando nossas primeiras peças. Fale conosco e avisaremos assim que chegarem.',\n storeEmptyTextAlone: 'Estamos finalizando nossas primeiras peças. Volte em breve.',\n getInTouch: 'Fale conosco',\n illustrativePhoto: 'Imagem ilustrativa',\n },\n de: {\n seeAll: 'Alle ansehen',\n allPosts: 'Alle Beiträge',\n allMusic: 'Alle Musik',\n allEvents: 'Alle Veranstaltungen',\n shopAll: 'Zum Shop',\n readPost: 'Beitrag lesen',\n viewDetails: 'Details ansehen',\n subscribe: 'Abonnieren',\n reviewsHeading: 'Was unsere Kunden sagen',\n signupHeading: 'Abonnieren Sie unseren Newsletter',\n account: 'Mein Konto',\n customerCare: 'Kundenservice',\n storeAnnouncement: 'Entdecken Sie die ganze Kollektion',\n shopNow: 'Jetzt kaufen',\n storeEmptyTitle: 'Neue Stücke sind unterwegs',\n storeEmptyText: 'Unsere ersten Stücke werden gerade fertig. Schreiben Sie uns, und wir melden uns, sobald sie da sind.',\n storeEmptyTextAlone: 'Unsere ersten Stücke werden gerade fertig. Schauen Sie bald wieder vorbei.',\n getInTouch: 'Kontakt aufnehmen',\n illustrativePhoto: 'Symbolbild',\n },\n it: {\n seeAll: 'Vedi tutto',\n allPosts: 'Tutti gli articoli',\n allMusic: 'Tutta la musica',\n allEvents: 'Tutti gli eventi',\n shopAll: 'Vai al negozio',\n readPost: 'Leggi l’articolo',\n viewDetails: 'Vedi dettagli',\n subscribe: 'Iscriviti',\n reviewsHeading: 'Cosa dicono i nostri clienti',\n signupHeading: 'Iscriviti alla nostra newsletter',\n account: 'Il mio account',\n customerCare: 'Assistenza clienti',\n storeAnnouncement: 'Scopri tutta la collezione',\n shopNow: 'Acquista ora',\n storeEmptyTitle: 'Nuovi pezzi in arrivo',\n storeEmptyText: 'Stiamo finendo i nostri primi pezzi. Scrivici e ti avviseremo appena arrivano.',\n storeEmptyTextAlone: 'Stiamo finendo i nostri primi pezzi. Torna presto.',\n getInTouch: 'Scrivici',\n illustrativePhoto: 'Immagine illustrativa',\n },\n}\n\n/** A language code's two-letter language, lowercase: `es` of `es-MX`; empty for none. */\nexport function aiLayoutLanguageOf(language: string | null | undefined): string {\n return typeof language === 'string' ? (/^([a-z]{2})(?:[-_]|$)/i.exec(language.trim())?.[1] ?? '').toLowerCase() : ''\n}\n\n/** Whether a site's language is one other than English: a code that names one. */\nexport function aiLayoutSpeaksOtherThanEnglish(language: string | null | undefined): boolean {\n const code = aiLayoutLanguageOf(language)\n return code !== '' && code !== 'en'\n}\n\n/** A label the platform places, in the site's language where it is listed, else in English. */\nexport function aiLayoutLabel(key: AiLayoutLabel, language?: string | null): string {\n return LABELS[aiLayoutLanguageOf(language)]?.[key] ?? AI_LAYOUT_LABELS_EN[key]\n}\n"],"names":["AI_LAYOUT_LABELS_EN","seeAll","allPosts","allMusic","allEvents","shopAll","readPost","viewDetails","subscribe","reviewsHeading","signupHeading","account","customerCare","storeAnnouncement","shopNow","storeEmptyTitle","storeEmptyText","storeEmptyTextAlone","getInTouch","illustrativePhoto","LABELS","es","fr","pt","de","it","aiLayoutLanguageOf","language","exec","trim","toLowerCase","aiLayoutSpeaksOtherThanEnglish","code","aiLayoutLabel","key"],"mappings":"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"}
@@ -35,7 +35,10 @@ import type { NodesMap } from '@aglyn/aglyn/types/nodes';
35
35
  * CREDITS file beside them). They are the platform's own files, so a
36
36
  * visitor's browser asks no other host for them.
37
37
  *
38
- * The slot keeps the alt text the model wrote whichever source fills it.
38
+ * A slot keeps the alt text the model wrote unless the photo filling it
39
+ * brings its own words: the model wrote it before any photo was found, so a
40
+ * stock photo that is not what it says is described by what it is
41
+ * (`aiStockPhotoAlt`).
39
42
  */
40
43
  /** What a picture slot is for, read from where it sits and what it says. */
41
44
  export type AiLayoutPictureRole = 'hero' | 'about' | 'gallery';
@@ -68,6 +71,12 @@ export interface AiLayoutPicturePhoto {
68
71
  src: string;
69
72
  width: number;
70
73
  height: number;
74
+ /**
75
+ * What the photo shows, where the slot's own alt text does not say it: the
76
+ * slot takes it in place of the words the model wrote before the photo was
77
+ * found. Absent, the slot's words describe it.
78
+ */
79
+ alt?: string;
71
80
  }
72
81
  /** A slot's role: the first section's picture is the hero; people make an about picture. */
73
82
  export declare function aiLayoutPictureRole(input: {
@@ -133,8 +142,8 @@ export declare function aiLayoutStarterPhotos(slots: readonly AiLayoutPictureSlo
133
142
  * The page with each slot filled by its photo: the `image` given its source,
134
143
  * its intrinsic size and how it loads (the hero eagerly, as the starter home
135
144
  * page loads its own), and the placeholder icon taken out of the frame. The
136
- * alt text is kept as the model wrote it. A slot given no photo is left as
137
- * the compiler wrote it.
145
+ * alt text is kept as the model wrote it unless the photo brings its own
146
+ * words. A slot given no photo is left as the compiler wrote it.
138
147
  */
139
148
  export declare function aiLayoutPlacePictures(nodes: NodesMap, slots: readonly AiLayoutPictureSlot[], photos: ReadonlyArray<AiLayoutPicturePhoto | null>): NodesMap;
140
149
  /**
@@ -213,8 +213,8 @@ function rotate(list, by) {
213
213
  * The page with each slot filled by its photo: the `image` given its source,
214
214
  * its intrinsic size and how it loads (the hero eagerly, as the starter home
215
215
  * page loads its own), and the placeholder icon taken out of the frame. The
216
- * alt text is kept as the model wrote it. A slot given no photo is left as
217
- * the compiler wrote it.
216
+ * alt text is kept as the model wrote it unless the photo brings its own
217
+ * words. A slot given no photo is left as the compiler wrote it.
218
218
  */ export function aiLayoutPlacePictures(nodes, slots, photos) {
219
219
  const next = _extends({}, nodes);
220
220
  // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero
@@ -224,6 +224,7 @@ function rotate(list, by) {
224
224
  const lead = slots.findIndex((_, index)=>Boolean(photos[index] && next[slots[index].imageId]));
225
225
  slots.forEach((slot, index)=>{
226
226
  var _node_props;
227
+ var _photo_alt;
227
228
  const photo = photos[index];
228
229
  const node = next[slot.imageId];
229
230
  if (!photo || !node) return;
@@ -234,7 +235,9 @@ function rotate(list, by) {
234
235
  src: photo.src,
235
236
  intrinsicWidth: photo.width,
236
237
  intrinsicHeight: photo.height
237
- }, index === lead && slot.role === 'hero' ? {
238
+ }, ((_photo_alt = photo.alt) == null ? void 0 : _photo_alt.trim()) ? {
239
+ alt: photo.alt.trim()
240
+ } : {}, index === lead && slot.role === 'hero' ? {
238
241
  loading: 'eager'
239
242
  } : {})
240
243
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.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 { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Otherwise, or when the search finds nothing fitting, one of the STARTER\n * PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`, served at\n * `/_static/starter/` by the tenant and the console alike, credited in the\n * CREDITS file beside them). They are the platform's own files, so a\n * visitor's browser asks no other host for them.\n *\n * The slot keeps the alt text the model wrote whichever source fills it.\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660).\n const candidates = [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it. A slot given no photo is left as\n * the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero\n // of several photos loaded them all eagerly, and a live Portfolio page was\n // refused for it (2026-10-10). The slots are in document order; every later\n // picture leaves `loading` unset, which the renderer loads when reached.\n const lead = slots.findIndex((_, index) => Boolean(photos[index] && next[slots[index].imageId]))\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n const props = { ...((node['props'] as Record<string, unknown> | undefined) ?? {}) }\n delete props['loading']\n next[slot.imageId] = {\n ...node,\n props: {\n ...props,\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n ...(index === lead && slot.role === 'hero' ? { loading: 'eager' } : {}),\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering one photo or `null` per slot, in order.\n * It may throw; a slot it leaves unfilled takes a starter photo.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers, else from the starter photos. Never throws,\n * and never leaves a page worse than it came: a picture is decoration, and a\n * job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n const starters = aiLayoutStarterPhotos(slots, input.seed)\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => found[index] ?? starters[index]),\n )\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","role","names","AI_LAYOUT_STARTER_PHOTOS","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","Object","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","width","height","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","starters","_slot"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-pictures.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 { DEFAULT_SITE_IMAGES } from '@aglyn/aglyn/app-utils/default-site'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\n\n/**\n * THE PICTURES OF A LANGUAGE PAGE (AGL-3660).\n *\n * The compiler writes a picture slot as a frame holding an icon and an\n * `image` with alt text and no source (`ai-layout-compiler.ts`), and stays\n * pure: it never looks a photo up. This module is what runs after it, on the\n * stored page — it finds each empty slot, decides what kind of picture the\n * slot is, and fills it with a photo the site can serve from its own origin.\n *\n * Two sources fill a slot, in order:\n *\n * 1. A stock photo searched for by the slot's own description and copied into\n * the site's media library (`jobs/ai-layout-stock-photos.ts`), when the\n * deployment has a stock-photo key.\n * 2. Otherwise, or when the search finds nothing fitting, one of the STARTER\n * PHOTOS every new site is born with (`DEFAULT_SITE_IMAGES`, served at\n * `/_static/starter/` by the tenant and the console alike, credited in the\n * CREDITS file beside them). They are the platform's own files, so a\n * visitor's browser asks no other host for them.\n *\n * A slot keeps the alt text the model wrote unless the photo filling it\n * brings its own words: the model wrote it before any photo was found, so a\n * stock photo that is not what it says is described by what it is\n * (`aiStockPhotoAlt`).\n */\n\n/** What a picture slot is for, read from where it sits and what it says. */\nexport type AiLayoutPictureRole = 'hero' | 'about' | 'gallery'\n\n/** One empty picture slot of a stored page, in document order. */\nexport interface AiLayoutPictureSlot {\n /** The `image` node. */\n imageId: string\n /** The frame around it, which sets the slot's shape. */\n frameId: string | null\n /** The placeholder icon sharing the frame, taken out once a photo fills it. */\n iconId: string | null\n alt: string\n /** The frame's shape as width over height; 1 when it names none. */\n aspect: number\n /** The plan index of the section it sits in, or -1. */\n sectionIndex: number\n role: AiLayoutPictureRole\n /**\n * A product's photo (AGL-3676): what it is of, most specific first — its\n * name, then what its photo should show — searched for with the shop's\n * category and held to it, so a wick trimmer is never a laptop on a desk.\n */\n product?: { subjects: readonly string[] }\n}\n\n/** A photo a slot is filled with. */\nexport interface AiLayoutPicturePhoto {\n /** The site-served address: a starter path or the media library's own. */\n src: string\n width: number\n height: number\n /**\n * What the photo shows, where the slot's own alt text does not say it: the\n * slot takes it in place of the words the model wrote before the photo was\n * found. Absent, the slot's words describe it.\n */\n alt?: string\n}\n\n/** Words that say a picture shows people, or a section is about them. */\nconst PEOPLE =\n /\\b(about|team|story|owner|owners|founder|founders|staff|crew|people|person|portrait|meet|family|families|who we are|our (?:team|crew|people|story)|therapist|counsel(?:l)?or|coach|doctor|dentist|stylist|trainer|smil(?:e|es|ing)|headshot)\\b/i\n\n/**\n * An `aspectRatio` (`'16 / 9'`) as a number, or `null` where none is named.\n * A responsive one (`{ xs: '4 / 3', md: '4 / 5' }`, as a designed page's\n * pictures carry, AGL-3660) reads at the desktop breakpoint first: one photo\n * serves every width, and the wide screen is where its shape shows most.\n */\nfunction aspectOf(value: unknown): number | null {\n if (value && typeof value === 'object') {\n const byBreakpoint = value as Record<string, unknown>\n for (const breakpoint of ['md', 'lg', 'xl', 'sm', 'xs']) {\n const ratio = aspectOf(byBreakpoint[breakpoint])\n if (ratio !== null) return ratio\n }\n return null\n }\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*\\/\\s*(\\d+(?:\\.\\d+)?)\\s*$/.exec(String(value ?? ''))\n if (!match) return null\n const ratio = Number(match[1]) / Number(match[2])\n return Number.isFinite(ratio) && ratio > 0 ? ratio : null\n}\n\n/** The shape a full-bleed cover's photo is searched for: it fills a wide band. */\nconst COVER_ASPECT = 16 / 9\n\n/**\n * A slot's shape (AGL-3660): the image's own `aspectRatio` (a designed\n * picture card sets it on the image), else its frame's, else — for a frame\n * laid absolutely over its whole section, a designed photo cover — a wide\n * band's. 1 where nothing says.\n */\nfunction slotAspect(image: { sx?: Record<string, unknown> } | undefined, frame: { sx?: Record<string, unknown> } | undefined): number {\n const own = aspectOf(image?.sx?.['aspectRatio']) ?? aspectOf(frame?.sx?.['aspectRatio'])\n if (own !== null) return own\n const sx = frame?.sx\n if (sx?.['position'] === 'absolute' && sx['width'] === '100%' && sx['height'] === '100%') return COVER_ASPECT\n return 1\n}\n\n/** A slot's role: the first section's picture is the hero; people make an about picture. */\nexport function aiLayoutPictureRole(input: {\n sectionIndex: number\n sectionName: string\n alt: string\n}): AiLayoutPictureRole {\n if (input.sectionIndex === 0) return 'hero'\n if (PEOPLE.test(input.sectionName) || PEOPLE.test(input.alt)) return 'about'\n return 'gallery'\n}\n\n/**\n * Every empty picture slot of a stored page, walked from its root in\n * document order. A slot that already has a source — one the owner placed, or\n * an earlier pass filled — is not a slot.\n */\nexport function aiLayoutPictureSlots(\n nodes: NodesMap,\n rootId: string,\n sections: { ids: readonly string[]; names: readonly string[] },\n): AiLayoutPictureSlot[] {\n const map = nodes as unknown as Record<\n string,\n { componentId?: string; props?: Record<string, unknown>; sx?: Record<string, unknown>; nodes?: string[] }\n >\n const slots: AiLayoutPictureSlot[] = []\n const seen = new Set<string>()\n const visit = (id: string, parentId: string | null, sectionIndex: number) => {\n if (seen.has(id)) return\n seen.add(id)\n const node = map[id]\n if (!node) return\n const index = sections.ids.indexOf(id)\n const here = index >= 0 ? index : sectionIndex\n if (node.componentId === 'image') {\n const src = node.props?.['src']\n const alt = typeof node.props?.['alt'] === 'string' ? (node.props['alt'] as string).trim() : ''\n if ((typeof src !== 'string' || !src.trim()) && alt) {\n const frame = parentId ? map[parentId] : undefined\n const iconId =\n frame?.nodes?.find((child) => child !== id && map[child]?.componentId === 'icon') ?? null\n slots.push({\n imageId: id,\n frameId: frame ? parentId : null,\n iconId,\n alt,\n aspect: slotAspect(node, frame),\n sectionIndex: here,\n role: aiLayoutPictureRole({\n sectionIndex: here,\n sectionName: here >= 0 ? (sections.names[here] ?? '') : '',\n alt,\n }),\n })\n }\n }\n for (const child of node.nodes ?? []) visit(child, id, here)\n }\n visit(rootId, null, -1)\n return slots\n}\n\n/** The starter photos, by name. */\nexport const AI_LAYOUT_STARTER_PHOTOS = DEFAULT_SITE_IMAGES\n\ntype StarterName = keyof typeof AI_LAYOUT_STARTER_PHOTOS\n\n/**\n * The starter photos each role draws from, best first. A hero draws from the\n * wide photos that read as a page's opening, an about picture leads with the\n * owner, and the rest are the gallery's.\n */\nconst STARTER_POOLS: Readonly<Record<AiLayoutPictureRole, readonly StarterName[]>> = {\n hero: ['hero', 'desk', 'craft'],\n about: ['about', 'hero'],\n gallery: ['desk', 'plants', 'craft'],\n}\n\nconst ALL_STARTERS = Object.keys(AI_LAYOUT_STARTER_PHOTOS) as StarterName[]\n\n/** A small stable number from a seed, so the same job always picks the same photos. */\nexport function aiLayoutSeedNumber(seed: string): number {\n let hash = 2166136261\n for (let index = 0; index < seed.length; index += 1) {\n hash ^= seed.charCodeAt(index)\n hash = Math.imul(hash, 16777619)\n }\n return hash >>> 0\n}\n\nfunction rotate<T>(list: readonly T[], by: number): T[] {\n if (!list.length) return []\n const start = by % list.length\n return [...list.slice(start), ...list.slice(0, start)]\n}\n\n/**\n * A starter photo for each slot (AGL-3660). No photo repeats on a page while\n * an unused one remains, and every pool turns by the seed, so sites do not\n * all open with the same photo while one job always picks the same.\n *\n * The slots are served by what they are for, not where they sit: the hero\n * first, then the pictures of people (an about picture leads with the owner),\n * then the rest. The live Juniper Clay start (2026-10-08) had six slots and\n * five starters, and the owner's photo went to a stoneware card before the\n * About section that described the artist asked for it. Only a page with more\n * slots than starters repeats one: the photo placed longest ago, never the\n * one in a slot beside it. The owner's portrait goes only to a picture of\n * people; a gallery picture never takes it, even to avoid a repeat.\n */\nexport function aiLayoutStarterPhotos(\n slots: readonly AiLayoutPictureSlot[],\n seed: string,\n): AiLayoutPicturePhoto[] {\n const turn = aiLayoutSeedNumber(seed)\n const order: AiLayoutPictureRole[] = ['hero', 'about', 'gallery']\n const served = slots\n .map((slot, index) => ({ slot, index }))\n .sort((a, b) => order.indexOf(a.slot.role) - order.indexOf(b.slot.role) || a.index - b.index)\n const names: Array<StarterName | undefined> = slots.map(() => undefined)\n /** When each starter was last placed, by its turn in the serving order. */\n const placedAt = new Map<StarterName, number>()\n served.forEach(({ slot, index }, step) => {\n // An about picture leads with the owner on every site; the others turn.\n const pool = slot.role === 'about' ? [...STARTER_POOLS.about] : rotate(STARTER_POOLS[slot.role], turn)\n const rest = rotate(\n ALL_STARTERS.filter((name) => !pool.includes(name)),\n turn,\n )\n // The owner's portrait is a picture of a person, and only a picture of\n // people takes it: never a vase in a gallery (AGL-3660).\n const candidates = [...pool, ...rest].filter((name) => slot.role === 'about' || name !== 'about')\n const unused = candidates.find((candidate) => !placedAt.has(candidate))\n const beside = new Set([names[index - 1], names[index + 1]].filter(Boolean))\n const name =\n unused ??\n [...candidates]\n .sort((a, b) => (placedAt.get(a) ?? -1) - (placedAt.get(b) ?? -1))\n .find((candidate) => !beside.has(candidate)) ??\n candidates[0]\n names[index] = name\n placedAt.set(name, step)\n })\n return names.map((name) => {\n const photo = AI_LAYOUT_STARTER_PHOTOS[name as StarterName]\n return { src: photo.src, width: photo.width, height: photo.height }\n })\n}\n\n/**\n * The page with each slot filled by its photo: the `image` given its source,\n * its intrinsic size and how it loads (the hero eagerly, as the starter home\n * page loads its own), and the placeholder icon taken out of the frame. The\n * alt text is kept as the model wrote it unless the photo brings its own\n * words. A slot given no photo is left as the compiler wrote it.\n */\nexport function aiLayoutPlacePictures(\n nodes: NodesMap,\n slots: readonly AiLayoutPictureSlot[],\n photos: ReadonlyArray<AiLayoutPicturePhoto | null>,\n): NodesMap {\n const next = { ...(nodes as unknown as Record<string, Record<string, unknown>>) }\n // Only the page's first picture loads eagerly (rule 16, AGL-3660): a hero\n // of several photos loaded them all eagerly, and a live Portfolio page was\n // refused for it (2026-10-10). The slots are in document order; every later\n // picture leaves `loading` unset, which the renderer loads when reached.\n const lead = slots.findIndex((_, index) => Boolean(photos[index] && next[slots[index].imageId]))\n slots.forEach((slot, index) => {\n const photo = photos[index]\n const node = next[slot.imageId]\n if (!photo || !node) return\n const props = { ...((node['props'] as Record<string, unknown> | undefined) ?? {}) }\n delete props['loading']\n next[slot.imageId] = {\n ...node,\n props: {\n ...props,\n src: photo.src,\n intrinsicWidth: photo.width,\n intrinsicHeight: photo.height,\n ...(photo.alt?.trim() ? { alt: photo.alt.trim() } : {}),\n ...(index === lead && slot.role === 'hero' ? { loading: 'eager' } : {}),\n },\n }\n if (slot.iconId && slot.frameId && next[slot.frameId]) {\n const frame = next[slot.frameId]\n next[slot.frameId] = {\n ...frame,\n nodes: ((frame['nodes'] as string[] | undefined) ?? []).filter((child) => child !== slot.iconId),\n }\n delete next[slot.iconId]\n }\n })\n return next as unknown as NodesMap\n}\n\n/**\n * A source of photos found for the slots themselves — a stock search copied\n * into the site's library — answering one photo or `null` per slot, in order.\n * It may throw; a slot it leaves unfilled takes a starter photo.\n */\nexport type AiLayoutPictureSource = (\n slots: readonly AiLayoutPictureSlot[],\n) => Promise<ReadonlyArray<AiLayoutPicturePhoto | null>>\n\n/**\n * Fills a stored page's empty picture slots (AGL-3660): from `source` where\n * one is given and it answers, else from the starter photos. Never throws,\n * and never leaves a page worse than it came: a picture is decoration, and a\n * job never fails over one.\n */\nexport async function aiResolveLayoutPictures(\n nodes: NodesMap,\n input: {\n rootId: string\n sectionIds: readonly string[]\n sectionNames: readonly string[]\n /** A per-job word, so two sites do not open with the same photo. */\n seed: string\n source?: AiLayoutPictureSource | null\n },\n): Promise<NodesMap> {\n try {\n const slots = aiLayoutPictureSlots(nodes, input.rootId, {\n ids: input.sectionIds,\n names: input.sectionNames,\n })\n if (!slots.length) return nodes\n let found: ReadonlyArray<AiLayoutPicturePhoto | null> = []\n if (input.source) {\n try {\n found = await input.source(slots)\n } catch (error) {\n console.warn('ai layout pictures: the stock source failed; starter photos fill the page', { error })\n found = []\n }\n }\n const starters = aiLayoutStarterPhotos(slots, input.seed)\n return aiLayoutPlacePictures(\n nodes,\n slots,\n slots.map((_slot, index) => found[index] ?? starters[index]),\n )\n } catch (error) {\n console.error('ai layout pictures: the page keeps its empty picture slots', { error })\n return nodes\n }\n}\n"],"names":["DEFAULT_SITE_IMAGES","PEOPLE","aspectOf","value","byBreakpoint","breakpoint","ratio","match","exec","String","Number","isFinite","COVER_ASPECT","slotAspect","image","frame","own","sx","aiLayoutPictureRole","input","sectionIndex","test","sectionName","alt","aiLayoutPictureSlots","nodes","rootId","sections","map","slots","seen","Set","visit","id","parentId","node","has","add","index","ids","indexOf","here","componentId","src","props","trim","undefined","iconId","find","child","push","imageId","frameId","aspect","role","names","AI_LAYOUT_STARTER_PHOTOS","STARTER_POOLS","hero","about","gallery","ALL_STARTERS","Object","keys","aiLayoutSeedNumber","seed","hash","length","charCodeAt","Math","imul","rotate","list","by","start","slice","aiLayoutStarterPhotos","turn","order","served","slot","sort","a","b","placedAt","Map","forEach","step","unused","pool","rest","filter","name","includes","candidates","candidate","beside","Boolean","get","set","photo","width","height","aiLayoutPlacePictures","photos","next","lead","findIndex","_","intrinsicWidth","intrinsicHeight","loading","aiResolveLayoutPictures","sectionIds","sectionNames","found","source","error","console","warn","starters","_slot"],"mappings":"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"}
@@ -0,0 +1,48 @@
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
+ /**
18
+ * THE BULK COPY JOB AN IMPORT ASKS FOR (AGL-2916, AGL-3671) — the pure half of
19
+ * "write descriptions with AI as they land", with no import at all, so the
20
+ * native apps replay it (`aiProductsImportJob`) beside the console's products
21
+ * hub card, which starts the very same job.
22
+ *
23
+ * A product import that asked for its copy leaves one `products` job for the
24
+ * products it CREATED: the first `AI_PRODUCTS_BULK_MAX` of them, their ids
25
+ * read from the import job's results (`api/transfer/status`), one brief that
26
+ * names how many, and `inputs` that carry the ids.
27
+ */
28
+ /** The most products one bulk job writes the copy of. */
29
+ export declare const AI_PRODUCTS_BULK_MAX = 50;
30
+ /** The brief a bulk copy job carries, which the jobs list shows and a card recognizes it by. */
31
+ export declare const aiProductsBulkBrief: (count: number) => string;
32
+ /** A bulk job's `inputs`: the product ids that can be read back, once each, at most `AI_PRODUCTS_BULK_MAX`. */
33
+ export declare function aiProductsBulkInputs(productIds: readonly string[]): Record<string, string>;
34
+ /** One row of an import job's results. */
35
+ export interface ImportResultRow {
36
+ outcome?: string;
37
+ recordId?: string;
38
+ }
39
+ /** The products an applied import created, once each and in the order the results list them. */
40
+ export declare function createdProductIdsOf(rows: readonly ImportResultRow[] | null | undefined): string[];
41
+ /**
42
+ * The copy job a landed import starts — the brief and the `inputs` the jobs route takes — for the first
43
+ * `AI_PRODUCTS_BULK_MAX` products it created; `null` when it created none.
44
+ */
45
+ export declare function aiProductsImportJob(rows: readonly ImportResultRow[] | null | undefined): {
46
+ brief: string;
47
+ inputs: Record<string, string>;
48
+ } | null;