@aglyn/plugins-ai 1.0.0-beta.250 → 1.0.0-beta.252

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 (94) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-brief-dialog.component.js +15 -1
  3. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  4. package/src/lib/components/ai-job-links.js +1 -1
  5. package/src/lib/components/ai-job-links.js.map +1 -1
  6. package/src/lib/components/ai-job-plan.component.js +20 -2
  7. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  8. package/src/lib/components/ai-jobs-store.d.ts +11 -0
  9. package/src/lib/components/ai-jobs-store.js +22 -0
  10. package/src/lib/components/ai-jobs-store.js.map +1 -1
  11. package/src/lib/jobs/ai-job-crm-step.js +7 -4
  12. package/src/lib/jobs/ai-job-crm-step.js.map +1 -1
  13. package/src/lib/jobs/ai-job-drafts.d.ts +24 -0
  14. package/src/lib/jobs/ai-job-drafts.js +46 -0
  15. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  16. package/src/lib/jobs/ai-job-experiment-step.js +6 -1
  17. package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
  18. package/src/lib/jobs/ai-job-form-reuse.d.ts +78 -0
  19. package/src/lib/jobs/ai-job-form-reuse.js +136 -0
  20. package/src/lib/jobs/ai-job-form-reuse.js.map +1 -0
  21. package/src/lib/jobs/ai-job-form-step.d.ts +17 -0
  22. package/src/lib/jobs/ai-job-form-step.js +63 -11
  23. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  24. package/src/lib/jobs/ai-job-insight-step.js +6 -1
  25. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-plan-step.d.ts +19 -0
  27. package/src/lib/jobs/ai-job-plan-step.js +39 -1
  28. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  29. package/src/lib/jobs/ai-job-seo-step.js +5 -3
  30. package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
  31. package/src/lib/jobs/ai-job-site-content.d.ts +7 -5
  32. package/src/lib/jobs/ai-job-site-content.js +7 -5
  33. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  34. package/src/lib/jobs/ai-job-text-step.js +6 -3
  35. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  36. package/src/lib/jobs/ai-layout-stock-photos.d.ts +11 -7
  37. package/src/lib/jobs/ai-layout-stock-photos.js +25 -10
  38. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  39. package/src/lib/jobs/ai-stock-business-nouns.d.ts +62 -0
  40. package/src/lib/jobs/ai-stock-business-nouns.js +264 -0
  41. package/src/lib/jobs/ai-stock-business-nouns.js.map +1 -0
  42. package/src/lib/model/ai-build-plan.d.ts +7 -3
  43. package/src/lib/model/ai-build-plan.js +11 -1
  44. package/src/lib/model/ai-build-plan.js.map +1 -1
  45. package/src/lib/model/ai-credit-estimate.d.ts +32 -18
  46. package/src/lib/model/ai-credit-estimate.js +36 -18
  47. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  48. package/src/lib/model/ai-job-activity.d.ts +30 -0
  49. package/src/lib/model/ai-job-activity.js +35 -0
  50. package/src/lib/model/ai-job-activity.js.map +1 -1
  51. package/src/lib/model/ai-media-credits.d.ts +5 -3
  52. package/src/lib/model/ai-media-credits.js +7 -5
  53. package/src/lib/model/ai-media-credits.js.map +1 -1
  54. package/src/lib/model/ai-site-job.js +6 -3
  55. package/src/lib/model/ai-site-job.js.map +1 -1
  56. package/src/lib/model/ai-site-store-pages.d.ts +5 -0
  57. package/src/lib/model/ai-site-store-pages.js +2 -0
  58. package/src/lib/model/ai-site-store-pages.js.map +1 -1
  59. package/src/lib/plugin-config.js +2 -2
  60. package/src/lib/plugin-config.js.map +1 -1
  61. package/src/lib/providers/anthropic-crm-wording-superseded.d.ts +29 -0
  62. package/src/lib/providers/anthropic-crm-wording-superseded.js +31 -0
  63. package/src/lib/providers/anthropic-crm-wording-superseded.js.map +1 -0
  64. package/src/lib/providers/catalog.d.ts +24 -17
  65. package/src/lib/providers/catalog.js +63 -30
  66. package/src/lib/providers/catalog.js.map +1 -1
  67. package/src/lib/providers/model-choice.d.ts +1 -1
  68. package/src/lib/providers/model-choice.js +39 -30
  69. package/src/lib/providers/model-choice.js.map +1 -1
  70. package/src/lib/providers/routing.d.ts +20 -1
  71. package/src/lib/providers/routing.js +47 -8
  72. package/src/lib/providers/routing.js.map +1 -1
  73. package/src/lib/runtime/ai-dev-env.d.ts +37 -1
  74. package/src/lib/runtime/ai-dev-env.js +59 -6
  75. package/src/lib/runtime/ai-dev-env.js.map +1 -1
  76. package/src/lib/runtime/ai-doctrine-validators.js +3 -1
  77. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  78. package/src/lib/runtime/ai-doctrine.js +8 -4
  79. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  80. package/src/lib/runtime/ai-eval-live.d.ts +8 -0
  81. package/src/lib/runtime/ai-eval-live.js +34 -8
  82. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  83. package/src/lib/runtime/ai-palette.generated.js +4 -0
  84. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  85. package/src/lib/runtime/ai-runtime.js +17 -2
  86. package/src/lib/runtime/ai-runtime.js.map +1 -1
  87. package/src/lib/runtime/seo-fields.js +4 -3
  88. package/src/lib/runtime/seo-fields.js.map +1 -1
  89. package/src/lib/server/ai-assist.js +3 -2
  90. package/src/lib/server/ai-assist.js.map +1 -1
  91. package/src/lib/server/ai-media-svg.js +4 -0
  92. package/src/lib/server/ai-media-svg.js.map +1 -1
  93. package/src/lib/usage/ai-allotment-alerts.js +2 -5
  94. package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
@@ -0,0 +1,62 @@
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 ENGLISH NAME OF A BUSINESS TYPE WRITTEN IN ANOTHER LANGUAGE (AGL-3779).
19
+ *
20
+ * The stock photo libraries are tagged in English, so a site in another
21
+ * language is searched by an English subject (`aiStockSourceTerms`). The
22
+ * business type the person typed is the surest word for what the site is
23
+ * about, once it is English: "Una panadería familiar … con pan dulce" read
24
+ * as an English noun phrase searched "con pan dulce" and placed the Río
25
+ * Dulce, the kind of site picked ("restaurant cafe") placed a coffee mug as a
26
+ * bakery's hero (jobs ih6kVvCGP1 and iGmqrEgyW4, beta.250), and a live plan's
27
+ * photo words for a florist, "wedding bouquets event flowers", led with
28
+ * "wedding". So a business this table names leads the search, ahead of the
29
+ * plan's photo words, which decide only for a business it does not name.
30
+ *
31
+ * This table names the common kinds of local business in Spanish, French,
32
+ * Portuguese, German and Italian, each with the English words a library is
33
+ * searched by. It is deliberately small: a kind belongs here when people
34
+ * start sites for it and its name is not already English. Words English
35
+ * shares ("café", "restaurant", "boutique", "pizzeria") are left out, because
36
+ * the guided start's own brief is English around the person's words ("Make
37
+ * it a restaurant & café site") and must never decide the subject; such a
38
+ * site falls through to the kind it picked, as before.
39
+ *
40
+ * A word matches whole, accents and case aside: "panaderia" and "Panadería"
41
+ * meet, so a name two languages spell alike ("farmacia", "fotógrafo") is
42
+ * listed once; "pan" never matches inside "panadería", and a German compound
43
+ * ("Familienbäckerei") matches only if it is listed itself. The first listed
44
+ * name the text says wins, the longer of two that start at the same word —
45
+ * "peluquería canina" is a dog groomer, "peluquería" a hair salon — so the
46
+ * head noun a Romance-language business type opens with decides.
47
+ */
48
+ /** A kind of business: the English words its photos are searched by, and its names in other languages. */
49
+ interface AiStockBusinessNoun {
50
+ /** What a stock photo library is searched by: "bakery". */
51
+ english: string;
52
+ /** Its names in es, fr, pt, de and it, as people write them. */
53
+ names: readonly string[];
54
+ }
55
+ export declare const AI_STOCK_BUSINESS_NOUNS: readonly AiStockBusinessNoun[];
56
+ /**
57
+ * The English words for the kind of business a text in another language
58
+ * names ({@link AI_STOCK_BUSINESS_NOUNS}): "bakery" for "Una panadería
59
+ * familiar de San Antonio con pan dulce". Empty when it names none listed.
60
+ */
61
+ export declare function aiStockBusinessNoun(text: string): string;
62
+ export {};
@@ -0,0 +1,264 @@
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
+ * THE ENGLISH NAME OF A BUSINESS TYPE WRITTEN IN ANOTHER LANGUAGE (AGL-3779).
18
+ *
19
+ * The stock photo libraries are tagged in English, so a site in another
20
+ * language is searched by an English subject (`aiStockSourceTerms`). The
21
+ * business type the person typed is the surest word for what the site is
22
+ * about, once it is English: "Una panadería familiar … con pan dulce" read
23
+ * as an English noun phrase searched "con pan dulce" and placed the Río
24
+ * Dulce, the kind of site picked ("restaurant cafe") placed a coffee mug as a
25
+ * bakery's hero (jobs ih6kVvCGP1 and iGmqrEgyW4, beta.250), and a live plan's
26
+ * photo words for a florist, "wedding bouquets event flowers", led with
27
+ * "wedding". So a business this table names leads the search, ahead of the
28
+ * plan's photo words, which decide only for a business it does not name.
29
+ *
30
+ * This table names the common kinds of local business in Spanish, French,
31
+ * Portuguese, German and Italian, each with the English words a library is
32
+ * searched by. It is deliberately small: a kind belongs here when people
33
+ * start sites for it and its name is not already English. Words English
34
+ * shares ("café", "restaurant", "boutique", "pizzeria") are left out, because
35
+ * the guided start's own brief is English around the person's words ("Make
36
+ * it a restaurant & café site") and must never decide the subject; such a
37
+ * site falls through to the kind it picked, as before.
38
+ *
39
+ * A word matches whole, accents and case aside: "panaderia" and "Panadería"
40
+ * meet, so a name two languages spell alike ("farmacia", "fotógrafo") is
41
+ * listed once; "pan" never matches inside "panadería", and a German compound
42
+ * ("Familienbäckerei") matches only if it is listed itself. The first listed
43
+ * name the text says wins, the longer of two that start at the same word —
44
+ * "peluquería canina" is a dog groomer, "peluquería" a hair salon — so the
45
+ * head noun a Romance-language business type opens with decides.
46
+ */ /** A kind of business: the English words its photos are searched by, and its names in other languages. */ export const AI_STOCK_BUSINESS_NOUNS = [
47
+ {
48
+ english: 'bakery',
49
+ names: [
50
+ 'panadería',
51
+ 'boulangerie',
52
+ 'padaria',
53
+ 'Bäckerei',
54
+ 'panetteria',
55
+ 'panificio'
56
+ ]
57
+ },
58
+ {
59
+ english: 'pastry shop',
60
+ names: [
61
+ 'pastelería',
62
+ 'repostería',
63
+ 'pâtisserie',
64
+ 'confeitaria',
65
+ 'Konditorei',
66
+ 'pasticceria'
67
+ ]
68
+ },
69
+ {
70
+ english: 'tacos',
71
+ names: [
72
+ 'taquería'
73
+ ]
74
+ },
75
+ {
76
+ english: 'cafe',
77
+ names: [
78
+ 'cafetería',
79
+ 'caffetteria'
80
+ ]
81
+ },
82
+ {
83
+ english: 'restaurant',
84
+ names: [
85
+ 'restaurante',
86
+ 'ristorante',
87
+ 'Gaststätte'
88
+ ]
89
+ },
90
+ // A library knows the trattoria by its own name, a narrower search than "restaurant".
91
+ {
92
+ english: 'trattoria',
93
+ names: [
94
+ 'trattoria',
95
+ 'osteria'
96
+ ]
97
+ },
98
+ {
99
+ english: 'butcher shop',
100
+ names: [
101
+ 'carnicería',
102
+ 'boucherie',
103
+ 'açougue',
104
+ 'Metzgerei',
105
+ 'macelleria'
106
+ ]
107
+ },
108
+ {
109
+ english: 'florist',
110
+ names: [
111
+ 'floristería',
112
+ 'florería',
113
+ 'fleuriste',
114
+ 'floricultura',
115
+ 'Blumenladen',
116
+ 'fioraio',
117
+ 'fioreria'
118
+ ]
119
+ },
120
+ {
121
+ english: 'hair salon',
122
+ names: [
123
+ 'peluquería',
124
+ 'salon de coiffure',
125
+ 'coiffeur',
126
+ 'coiffeuse',
127
+ 'cabeleireiro',
128
+ 'Friseur',
129
+ 'Friseursalon',
130
+ 'parrucchiere'
131
+ ]
132
+ },
133
+ {
134
+ english: 'dog grooming',
135
+ names: [
136
+ 'peluquería canina',
137
+ 'toilettage',
138
+ 'banho e tosa',
139
+ 'Hundesalon',
140
+ 'toelettatura'
141
+ ]
142
+ },
143
+ {
144
+ english: 'beauty salon',
145
+ names: [
146
+ 'salón de belleza',
147
+ 'institut de beauté',
148
+ 'salão de beleza',
149
+ 'Kosmetikstudio',
150
+ 'centro estetico'
151
+ ]
152
+ },
153
+ {
154
+ english: 'barber shop',
155
+ names: [
156
+ 'barbería',
157
+ 'barbearia',
158
+ 'Barbier',
159
+ 'barbiere'
160
+ ]
161
+ },
162
+ {
163
+ english: 'auto repair',
164
+ names: [
165
+ 'taller mecánico',
166
+ 'oficina mecânica',
167
+ 'Autowerkstatt',
168
+ 'Kfz-Werkstatt',
169
+ 'officina meccanica',
170
+ 'autofficina'
171
+ ]
172
+ },
173
+ {
174
+ english: 'dentist',
175
+ names: [
176
+ 'dentista',
177
+ 'clínica dental',
178
+ 'dentiste',
179
+ 'cabinet dentaire',
180
+ 'Zahnarzt',
181
+ 'Zahnarztpraxis',
182
+ 'studio dentistico'
183
+ ]
184
+ },
185
+ {
186
+ english: 'veterinary clinic',
187
+ names: [
188
+ 'veterinaria',
189
+ 'veterinario',
190
+ 'vétérinaire',
191
+ 'Tierarzt',
192
+ 'Tierarztpraxis'
193
+ ]
194
+ },
195
+ {
196
+ english: 'pharmacy',
197
+ names: [
198
+ 'farmacia',
199
+ 'pharmacie',
200
+ 'Apotheke'
201
+ ]
202
+ },
203
+ {
204
+ english: 'bookstore',
205
+ names: [
206
+ 'librería',
207
+ 'librairie',
208
+ 'livraria',
209
+ 'Buchhandlung'
210
+ ]
211
+ },
212
+ {
213
+ english: 'photographer',
214
+ names: [
215
+ 'fotógrafo',
216
+ 'fotógrafa',
217
+ 'photographe',
218
+ 'Fotograf',
219
+ 'Fotografin'
220
+ ]
221
+ },
222
+ {
223
+ english: 'plumber',
224
+ names: [
225
+ 'fontanero',
226
+ 'plomero',
227
+ 'plombier',
228
+ 'encanador',
229
+ 'Klempner',
230
+ 'idraulico'
231
+ ]
232
+ },
233
+ {
234
+ english: 'cleaning service',
235
+ names: [
236
+ 'servicio de limpieza',
237
+ 'nettoyage',
238
+ 'limpeza',
239
+ 'Reinigung',
240
+ 'impresa di pulizie'
241
+ ]
242
+ }
243
+ ];
244
+ /** A text's words, lowercase, without accents: "Panadería" and "panaderia" are one word. */ function foldedWords(text) {
245
+ return text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/ß/g, 'ss').split(/[^a-z]+/).filter(Boolean);
246
+ }
247
+ /** Every listed name as its words, the longest first, so the longer of two at one word wins. */ const NAMES = AI_STOCK_BUSINESS_NOUNS.flatMap(({ english, names })=>names.map((name)=>({
248
+ words: foldedWords(name),
249
+ english
250
+ }))).sort((a, b)=>b.words.length - a.words.length);
251
+ /**
252
+ * The English words for the kind of business a text in another language
253
+ * names ({@link AI_STOCK_BUSINESS_NOUNS}): "bakery" for "Una panadería
254
+ * familiar de San Antonio con pan dulce". Empty when it names none listed.
255
+ */ export function aiStockBusinessNoun(text) {
256
+ const words = foldedWords(text);
257
+ for(let at = 0; at < words.length; at += 1){
258
+ const found = NAMES.find((name)=>name.words.every((word, index)=>words[at + index] === word));
259
+ if (found) return found.english;
260
+ }
261
+ return '';
262
+ }
263
+
264
+ //# sourceMappingURL=ai-stock-business-nouns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-stock-business-nouns.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 ENGLISH NAME OF A BUSINESS TYPE WRITTEN IN ANOTHER LANGUAGE (AGL-3779).\n *\n * The stock photo libraries are tagged in English, so a site in another\n * language is searched by an English subject (`aiStockSourceTerms`). The\n * business type the person typed is the surest word for what the site is\n * about, once it is English: \"Una panadería familiar … con pan dulce\" read\n * as an English noun phrase searched \"con pan dulce\" and placed the Río\n * Dulce, the kind of site picked (\"restaurant cafe\") placed a coffee mug as a\n * bakery's hero (jobs ih6kVvCGP1 and iGmqrEgyW4, beta.250), and a live plan's\n * photo words for a florist, \"wedding bouquets event flowers\", led with\n * \"wedding\". So a business this table names leads the search, ahead of the\n * plan's photo words, which decide only for a business it does not name.\n *\n * This table names the common kinds of local business in Spanish, French,\n * Portuguese, German and Italian, each with the English words a library is\n * searched by. It is deliberately small: a kind belongs here when people\n * start sites for it and its name is not already English. Words English\n * shares (\"café\", \"restaurant\", \"boutique\", \"pizzeria\") are left out, because\n * the guided start's own brief is English around the person's words (\"Make\n * it a restaurant & café site\") and must never decide the subject; such a\n * site falls through to the kind it picked, as before.\n *\n * A word matches whole, accents and case aside: \"panaderia\" and \"Panadería\"\n * meet, so a name two languages spell alike (\"farmacia\", \"fotógrafo\") is\n * listed once; \"pan\" never matches inside \"panadería\", and a German compound\n * (\"Familienbäckerei\") matches only if it is listed itself. The first listed\n * name the text says wins, the longer of two that start at the same word —\n * \"peluquería canina\" is a dog groomer, \"peluquería\" a hair salon — so the\n * head noun a Romance-language business type opens with decides.\n */\n\n/** A kind of business: the English words its photos are searched by, and its names in other languages. */\ninterface AiStockBusinessNoun {\n /** What a stock photo library is searched by: \"bakery\". */\n english: string\n /** Its names in es, fr, pt, de and it, as people write them. */\n names: readonly string[]\n}\n\nexport const AI_STOCK_BUSINESS_NOUNS: readonly AiStockBusinessNoun[] = [\n { english: 'bakery', names: ['panadería', 'boulangerie', 'padaria', 'Bäckerei', 'panetteria', 'panificio'] },\n { english: 'pastry shop', names: ['pastelería', 'repostería', 'pâtisserie', 'confeitaria', 'Konditorei', 'pasticceria'] },\n { english: 'tacos', names: ['taquería'] },\n { english: 'cafe', names: ['cafetería', 'caffetteria'] },\n { english: 'restaurant', names: ['restaurante', 'ristorante', 'Gaststätte'] },\n // A library knows the trattoria by its own name, a narrower search than \"restaurant\".\n { english: 'trattoria', names: ['trattoria', 'osteria'] },\n { english: 'butcher shop', names: ['carnicería', 'boucherie', 'açougue', 'Metzgerei', 'macelleria'] },\n { english: 'florist', names: ['floristería', 'florería', 'fleuriste', 'floricultura', 'Blumenladen', 'fioraio', 'fioreria'] },\n {\n english: 'hair salon',\n names: ['peluquería', 'salon de coiffure', 'coiffeur', 'coiffeuse', 'cabeleireiro', 'Friseur', 'Friseursalon', 'parrucchiere'],\n },\n { english: 'dog grooming', names: ['peluquería canina', 'toilettage', 'banho e tosa', 'Hundesalon', 'toelettatura'] },\n { english: 'beauty salon', names: ['salón de belleza', 'institut de beauté', 'salão de beleza', 'Kosmetikstudio', 'centro estetico'] },\n { english: 'barber shop', names: ['barbería', 'barbearia', 'Barbier', 'barbiere'] },\n { english: 'auto repair', names: ['taller mecánico', 'oficina mecânica', 'Autowerkstatt', 'Kfz-Werkstatt', 'officina meccanica', 'autofficina'] },\n { english: 'dentist', names: ['dentista', 'clínica dental', 'dentiste', 'cabinet dentaire', 'Zahnarzt', 'Zahnarztpraxis', 'studio dentistico'] },\n { english: 'veterinary clinic', names: ['veterinaria', 'veterinario', 'vétérinaire', 'Tierarzt', 'Tierarztpraxis'] },\n { english: 'pharmacy', names: ['farmacia', 'pharmacie', 'Apotheke'] },\n { english: 'bookstore', names: ['librería', 'librairie', 'livraria', 'Buchhandlung'] },\n { english: 'photographer', names: ['fotógrafo', 'fotógrafa', 'photographe', 'Fotograf', 'Fotografin'] },\n { english: 'plumber', names: ['fontanero', 'plomero', 'plombier', 'encanador', 'Klempner', 'idraulico'] },\n { english: 'cleaning service', names: ['servicio de limpieza', 'nettoyage', 'limpeza', 'Reinigung', 'impresa di pulizie'] },\n]\n\n/** A text's words, lowercase, without accents: \"Panadería\" and \"panaderia\" are one word. */\nfunction foldedWords(text: string): string[] {\n return text\n .normalize('NFD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/ß/g, 'ss')\n .split(/[^a-z]+/)\n .filter(Boolean)\n}\n\n/** Every listed name as its words, the longest first, so the longer of two at one word wins. */\nconst NAMES: ReadonlyArray<{ words: string[]; english: string }> = AI_STOCK_BUSINESS_NOUNS.flatMap(({ english, names }) =>\n names.map((name) => ({ words: foldedWords(name), english })),\n).sort((a, b) => b.words.length - a.words.length)\n\n/**\n * The English words for the kind of business a text in another language\n * names ({@link AI_STOCK_BUSINESS_NOUNS}): \"bakery\" for \"Una panadería\n * familiar de San Antonio con pan dulce\". Empty when it names none listed.\n */\nexport function aiStockBusinessNoun(text: string): string {\n const words = foldedWords(text)\n for (let at = 0; at < words.length; at += 1) {\n const found = NAMES.find((name) => name.words.every((word, index) => words[at + index] === word))\n if (found) return found.english\n }\n return ''\n}\n"],"names":["AI_STOCK_BUSINESS_NOUNS","english","names","foldedWords","text","normalize","replace","toLowerCase","split","filter","Boolean","NAMES","flatMap","map","name","words","sort","a","b","length","aiStockBusinessNoun","at","found","find","every","word","index"],"mappings":"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"}
@@ -228,9 +228,13 @@ export interface AiBuildPlan {
228
228
  * is in (AGL-3760): two to four words a stock photo library is searched by,
229
229
  * the business's subject first — "bakery bread pastries" for "una panadería
230
230
  * … con pan dulce, conchas". The libraries are tagged in English, so a site
231
- * in another language searches by these rather than its own words. Absent
232
- * on every other kind's plan, on one that said null, and on one whose words
233
- * were not plain English letters.
231
+ * in another language searches by these rather than its own words — unless
232
+ * its business type names a business the curated table knows in English
233
+ * (`aiStockBusinessNoun`), which leads — and its plan is asked for them
234
+ * again once when it gave none (AGL-3779). Absent on every other kind's
235
+ * plan, on one that said null, on one whose words were not plain English
236
+ * letters, and on a plan in another language whose re-ask still gave none,
237
+ * whose photos then fall back to its kind of site.
234
238
  */
235
239
  photoWords?: string;
236
240
  }
@@ -451,7 +451,17 @@ const LANGUAGE_SCHEMA = nullableString("The language the brief is written in, wh
451
451
  const words = text.split(/[\s,&]+/).map((word)=>word.replace(/^[-']+|[-']+$/g, '')).filter((word)=>word.length > 1);
452
452
  return words.length ? words.slice(0, AI_PLAN_PHOTO_WORDS_MAX).join(' ') : null;
453
453
  }
454
- const PHOTO_WORDS_SCHEMA = nullableString("For a brief not in English: 2-4 English words for its stock photos, subject first, e.g. bakery bread pastries; no place. Else null.");
454
+ /**
455
+ * Required whenever the plan's language is not English (AGL-3779), the field
456
+ * beside it. Asked "for a brief not in English", two Spanish bakeries' plans
457
+ * said `es` and gave none (jobs ih6kVvCGP1, iGmqrEgyW4): a guided start's
458
+ * brief is English sentences around the person's own words. A strict schema
459
+ * has no conditional keyword, so the condition is said here and held by the
460
+ * plan step (`aiSitePhotoWordsCheck`), which re-asks once. The kind of
461
+ * business comes first because the first word is the subject every photo is
462
+ * searched by (`aiStockPhotoWordsSubject`): asked for the most specific word
463
+ * first, a florist's live plan wrote "wedding bouquets event flowers".
464
+ */ const PHOTO_WORDS_SCHEMA = nullableString('Required when language is not en: 2-4 English words, the kind of business first, then what it sells, e.g. florist wedding bouquets; never a place or the business name. Null when language is en.');
455
465
  /**
456
466
  * Any plan tool with the site's language (AGL-3750) and its photo words in
457
467
  * English (AGL-3760), offered only to a `site` job, whose guided start sets