@aglyn/plugins-ai 1.0.0-beta.270 → 1.0.0-beta.271
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +1 -1
- package/src/lib/activity/ai-activity-actions.js +1 -0
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/assets/site-styles/beauty.webp +0 -0
- package/src/lib/assets/site-styles/blog.webp +0 -0
- package/src/lib/assets/site-styles/business.webp +0 -0
- package/src/lib/assets/site-styles/coming-soon.webp +0 -0
- package/src/lib/assets/site-styles/education.webp +0 -0
- package/src/lib/assets/site-styles/events.webp +0 -0
- package/src/lib/assets/site-styles/fitness.webp +0 -0
- package/src/lib/assets/site-styles/landing.webp +0 -0
- package/src/lib/assets/site-styles/music.webp +0 -0
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/personal.webp +0 -0
- package/src/lib/assets/site-styles/photography.webp +0 -0
- package/src/lib/assets/site-styles/portfolio.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/realestate.webp +0 -0
- package/src/lib/assets/site-styles/restaurant.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/studio.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/assets/site-styles/wellness.webp +0 -0
- package/src/lib/assets/site-styles/yoga.webp +0 -0
- package/src/lib/components/ai-credits-card.component.d.ts +4 -3
- package/src/lib/components/ai-credits-card.component.js +15 -15
- package/src/lib/components/ai-credits-card.component.js.map +1 -1
- package/src/lib/components/ai-job-input-form.component.d.ts +36 -0
- package/src/lib/components/ai-job-input-form.component.js +100 -0
- package/src/lib/components/ai-job-input-form.component.js.map +1 -0
- package/src/lib/components/ai-job-requests.d.ts +4 -0
- package/src/lib/components/ai-job-requests.js +4 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +99 -31
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +43 -2
- package/src/lib/components/ai-site-start-steps.component.js +651 -15
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-start-tracking.d.ts +10 -0
- package/src/lib/components/ai-site-start-tracking.js +14 -0
- package/src/lib/components/ai-site-start-tracking.js.map +1 -1
- package/src/lib/components/ai-site-style-previews.js +26 -22
- package/src/lib/components/ai-site-style-previews.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.d.ts +7 -5
- package/src/lib/components/ai-upsell-dialog.component.js +8 -6
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/components/assist-build-card.component.js +11 -1
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +9 -1
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +8 -4
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-card.component.js +4 -7
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/declarations.d.ts +2 -2
- package/src/lib/declarations.js +9 -5
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +3 -1
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-business-profile-prefill.d.ts +1 -1
- package/src/lib/jobs/ai-business-profile-prefill.js +14 -3
- package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +5 -3
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.d.ts +15 -1
- package/src/lib/jobs/ai-job-form-step.js +75 -8
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +13 -0
- package/src/lib/jobs/ai-job-page-language.js +37 -1
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +17 -0
- package/src/lib/jobs/ai-job-page-step.js +37 -16
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +25 -2
- package/src/lib/jobs/ai-job-plan-step.js +103 -29
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.js +4 -3
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +27 -0
- package/src/lib/jobs/ai-job-site-datasets.js +140 -14
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-look.js +18 -5
- package/src/lib/jobs/ai-job-site-look.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +16 -1
- package/src/lib/jobs/ai-job-site-step.js +95 -22
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +8 -1
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +14 -3
- package/src/lib/jobs/ai-job-tracking-step.js +28 -5
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +8 -1
- package/src/lib/jobs/ai-jobs.js +63 -8
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +27 -2
- package/src/lib/jobs/ai-layout-stock-photos.js +190 -19
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-inspiration-signals.d.ts +98 -0
- package/src/lib/jobs/ai-site-inspiration-signals.js +585 -0
- package/src/lib/jobs/ai-site-inspiration-signals.js.map +1 -0
- package/src/lib/jobs/ai-site-inspiration.d.ts +157 -0
- package/src/lib/jobs/ai-site-inspiration.js +346 -0
- package/src/lib/jobs/ai-site-inspiration.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +26 -1
- package/src/lib/jobs/ai-site-publish.js +162 -13
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +16 -0
- package/src/lib/jobs/ai-theme-brand-inputs.js +18 -4
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.d.ts +97 -0
- package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.js +137 -0
- package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-inspiration-pages.d.ts +38 -0
- package/src/lib/jobs/fixtures/ai-site-inspiration-pages.js +103 -0
- package/src/lib/jobs/fixtures/ai-site-inspiration-pages.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.d.ts +25 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.js +174 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +1 -1
- package/src/lib/layout-language/ai-layout-compiler.js +438 -25
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-contact.d.ts +57 -0
- package/src/lib/layout-language/ai-layout-contact.js +137 -0
- package/src/lib/layout-language/ai-layout-contact.js.map +1 -0
- package/src/lib/layout-language/ai-layout-copy.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-copy.js +34 -2
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +65 -8
- package/src/lib/layout-language/ai-layout-design.js +411 -97
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.js +23 -0
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-labels.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-labels.js +12 -6
- package/src/lib/layout-language/ai-layout-labels.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +3 -2
- package/src/lib/layout-language/ai-layout-language.js +4 -3
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.d.ts +13 -0
- package/src/lib/layout-language/ai-layout-links.js +30 -0
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +49 -6
- package/src/lib/layout-language/ai-layout-pictures.js +145 -22
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +16 -1
- package/src/lib/model/ai-build-job.js +40 -0
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-job-input.d.ts +33 -0
- package/src/lib/model/ai-job-input.js +59 -0
- package/src/lib/model/ai-job-input.js.map +1 -0
- package/src/lib/model/ai-job-notice.d.ts +4 -0
- package/src/lib/model/ai-job-notice.js +11 -1
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +40 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-details.d.ts +122 -0
- package/src/lib/model/ai-site-details.js +300 -0
- package/src/lib/model/ai-site-details.js.map +1 -0
- package/src/lib/model/ai-site-entity.d.ts +181 -0
- package/src/lib/model/ai-site-entity.js +390 -0
- package/src/lib/model/ai-site-entity.js.map +1 -0
- package/src/lib/model/ai-site-inspiration.d.ts +72 -0
- package/src/lib/model/ai-site-inspiration.js +109 -0
- package/src/lib/model/ai-site-inspiration.js.map +1 -0
- package/src/lib/model/ai-site-job.d.ts +13 -1
- package/src/lib/model/ai-site-job.js +15 -2
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.js +109 -21
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +10 -0
- package/src/lib/model/ai-site-look.js +24 -0
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +19 -5
- package/src/lib/model/ai-site-start-seo.js +25 -7
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-start-steps.d.ts +9 -3
- package/src/lib/model/ai-site-start-steps.js +67 -5
- package/src/lib/model/ai-site-start-steps.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +68 -4
- package/src/lib/model/ai-site-start.js +166 -13
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/ai-upsell-copy.js +4 -1
- package/src/lib/model/ai-upsell-copy.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +4 -3
- package/src/lib/plan-entitlements.js +3 -8
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +8 -1
- package/src/lib/runtime/ai-dataset-generation.js +12 -2
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +26 -33
- package/src/lib/runtime/ai-doctrine-validators.js +54 -44
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +1 -1
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +7 -0
- package/src/lib/runtime/ai-node-tree.js +9 -1
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/server/ai-create-route.d.ts +1 -1
- package/src/lib/server/ai-create-route.js +3 -4
- package/src/lib/server/ai-create-route.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js +27 -3
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +7 -0
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/usage/assist-credits.d.ts +4 -4
- package/src/lib/usage/assist-credits.js +4 -4
- package/src/lib/usage/assist-credits.js.map +1 -1
- package/src/lib/usage/staff-org-ai-view.js +6 -3
- package/src/lib/usage/staff-org-ai-view.js.map +1 -1
- package/src/lib/usage/staff-org-ai.d.ts +4 -2
- package/src/lib/usage/staff-org-ai.js +7 -7
- package/src/lib/usage/staff-org-ai.js.map +1 -1
- package/src/lib/usage/staff-org-usage-ai.js +2 -2
- package/src/lib/usage/staff-org-usage-ai.js.map +1 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { defaultHomeDescription, defaultSiteSeo } from "@aglyn/aglyn/app-utils/default-site";
|
|
18
|
+
import { HostEntityType } from "@aglyn/aglyn/foundation/definitions/platform.types";
|
|
19
|
+
import { aiSiteAddressLine, aiSiteDetailsOf, aiSiteHoursSentence, aiSiteServiceAreas } from "./ai-site-details.js";
|
|
20
|
+
import { aiSiteKindOfInputs } from "./ai-site-kinds.js";
|
|
21
|
+
import { aiSiteSeoProposalForInputs, AI_SITE_SEO_FORM_FIELDS } from "./ai-site-start-seo.js";
|
|
22
|
+
/**
|
|
23
|
+
* Each `AI_SITE_KINDS` entry's `schema.org` type (`ai-site-entity.spec.ts`
|
|
24
|
+
* holds every kind to an entry). A kind with no business type publishes as an
|
|
25
|
+
* Organization — a blog, a band, an event, a course — unless the person gave
|
|
26
|
+
* a street address or opening hours, which make it a place a visitor goes:
|
|
27
|
+
* a LocalBusiness. Only types on the platform's `LOCAL_BUSINESS_TYPES` list
|
|
28
|
+
* are used, since only those publish.
|
|
29
|
+
*/ export const AI_SITE_KIND_SCHEMA = {
|
|
30
|
+
business: {
|
|
31
|
+
businessType: 'LocalBusiness',
|
|
32
|
+
refine: [
|
|
33
|
+
[
|
|
34
|
+
/\b(?:marketing|consult\w*|agency|freelanc\w*|staffing|bookkeep\w*|design(?:er)?s?)\b/i,
|
|
35
|
+
'ProfessionalService'
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
/\b(?:car wash|detailing|auto repair|mechanic|tire|tyre)\b/i,
|
|
39
|
+
'AutomotiveBusiness'
|
|
40
|
+
]
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
trades: {
|
|
44
|
+
businessType: 'HomeAndConstructionBusiness',
|
|
45
|
+
refine: [
|
|
46
|
+
[
|
|
47
|
+
/\bplumb\w*/i,
|
|
48
|
+
'Plumber'
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
/\belectric\w*/i,
|
|
52
|
+
'Electrician'
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
/\b(?:hvac|heating|air condition\w*|furnace)\b/i,
|
|
56
|
+
'HVACBusiness'
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
/\broof\w*/i,
|
|
60
|
+
'RoofingContractor'
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
/\bpaint\w*/i,
|
|
64
|
+
'HousePainter'
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
/\blocksmith\w*/i,
|
|
68
|
+
'Locksmith'
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
/\bmov(?:ing|ers?)\b/i,
|
|
72
|
+
'MovingCompany'
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
/\b(?:contractor|remodel\w*|renovat\w*|builders?|construction)\b/i,
|
|
76
|
+
'GeneralContractor'
|
|
77
|
+
]
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
professional: {
|
|
81
|
+
businessType: 'ProfessionalService',
|
|
82
|
+
refine: [
|
|
83
|
+
[
|
|
84
|
+
/\b(?:law|lawyers?|attorneys?|legal|solicitors?)\b/i,
|
|
85
|
+
'LegalService'
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
/\b(?:accounting|accountants?|bookkeep\w*|tax|cpa)\b/i,
|
|
89
|
+
'AccountingService'
|
|
90
|
+
]
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
wellness: {
|
|
94
|
+
businessType: 'HealthAndBeautyBusiness',
|
|
95
|
+
refine: [
|
|
96
|
+
[
|
|
97
|
+
/\b(?:dentist\w*|dental|orthodont\w*)\b/i,
|
|
98
|
+
'Dentist'
|
|
99
|
+
],
|
|
100
|
+
[
|
|
101
|
+
/\b(?:clinic|doctors?|physicians?|medical|therap\w*|chiropract\w*|physio\w*|counsel\w*)\b/i,
|
|
102
|
+
'MedicalBusiness'
|
|
103
|
+
]
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
restaurant: {
|
|
107
|
+
businessType: 'Restaurant',
|
|
108
|
+
refine: [
|
|
109
|
+
[
|
|
110
|
+
/\b(?:caf[eé]s?|coffee|bakery|bakeries|bars?|brewery|food truck|deli)\b/i,
|
|
111
|
+
'FoodEstablishment'
|
|
112
|
+
]
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
store: {
|
|
116
|
+
businessType: 'Store'
|
|
117
|
+
},
|
|
118
|
+
portfolio: {},
|
|
119
|
+
studio: {
|
|
120
|
+
businessType: 'ProfessionalService'
|
|
121
|
+
},
|
|
122
|
+
photography: {
|
|
123
|
+
businessType: 'ProfessionalService'
|
|
124
|
+
},
|
|
125
|
+
blog: {},
|
|
126
|
+
events: {},
|
|
127
|
+
fitness: {
|
|
128
|
+
businessType: 'SportsActivityLocation'
|
|
129
|
+
},
|
|
130
|
+
yoga: {
|
|
131
|
+
businessType: 'SportsActivityLocation'
|
|
132
|
+
},
|
|
133
|
+
beauty: {
|
|
134
|
+
businessType: 'HealthAndBeautyBusiness'
|
|
135
|
+
},
|
|
136
|
+
realestate: {
|
|
137
|
+
businessType: 'RealEstateAgent'
|
|
138
|
+
},
|
|
139
|
+
education: {
|
|
140
|
+
refine: [
|
|
141
|
+
[
|
|
142
|
+
/\b(?:day ?care|child ?care|preschool|nursery)\b/i,
|
|
143
|
+
'ChildCare'
|
|
144
|
+
]
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
nonprofit: {
|
|
148
|
+
refine: [
|
|
149
|
+
[
|
|
150
|
+
/\b(?:animal (?:shelter|rescue)|pet rescue)\b/i,
|
|
151
|
+
'AnimalShelter'
|
|
152
|
+
]
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
music: {},
|
|
156
|
+
personal: {
|
|
157
|
+
person: true
|
|
158
|
+
},
|
|
159
|
+
landing: {},
|
|
160
|
+
'coming-soon': {}
|
|
161
|
+
};
|
|
162
|
+
/** What a job's inputs say the site publishes as. */ export function aiSiteSchemaOf(inputs) {
|
|
163
|
+
var _ref;
|
|
164
|
+
var _entry_refine_find, _entry_refine;
|
|
165
|
+
const kind = aiSiteKindOfInputs(inputs);
|
|
166
|
+
const entry = kind && AI_SITE_KIND_SCHEMA[kind.id] || {};
|
|
167
|
+
if (entry.person) return {
|
|
168
|
+
person: true,
|
|
169
|
+
businessType: null
|
|
170
|
+
};
|
|
171
|
+
const words = [
|
|
172
|
+
inputs == null ? void 0 : inputs['businessType'],
|
|
173
|
+
inputs == null ? void 0 : inputs['audience']
|
|
174
|
+
].filter((value)=>typeof value === 'string').join(' ');
|
|
175
|
+
const refined = (_entry_refine = entry.refine) == null ? void 0 : (_entry_refine_find = _entry_refine.find(([pattern])=>pattern.test(words))) == null ? void 0 : _entry_refine_find[1];
|
|
176
|
+
const details = aiSiteDetailsOf(inputs);
|
|
177
|
+
const place = Boolean(details.street || details.hours);
|
|
178
|
+
return {
|
|
179
|
+
person: false,
|
|
180
|
+
businessType: (_ref = refined != null ? refined : entry.businessType) != null ? _ref : place ? 'LocalBusiness' : null
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const text = (value)=>typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
184
|
+
/** "a, b and c". */ function listed(parts) {
|
|
185
|
+
if (parts.length < 2) return parts.join('');
|
|
186
|
+
return `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The site's agent guidance from its answers (AGL-3979): when an agent should
|
|
190
|
+
* use the site — what the business is, then what the site answers — and,
|
|
191
|
+
* where the person gave a way to reach it, how to reach a person. Written in
|
|
192
|
+
* English, as the rest of `/llms.txt` is.
|
|
193
|
+
*/ export function aiSiteAgentGuidance(input) {
|
|
194
|
+
const who = input.name || 'this business';
|
|
195
|
+
const answers = [
|
|
196
|
+
`what ${who} offers`,
|
|
197
|
+
...input.areas.length ? [
|
|
198
|
+
`the areas it serves (${input.areas.slice(0, 6).join(', ')})`
|
|
199
|
+
] : [],
|
|
200
|
+
...input.hours ? [
|
|
201
|
+
'its opening hours'
|
|
202
|
+
] : [],
|
|
203
|
+
...input.prices ? [
|
|
204
|
+
'its prices'
|
|
205
|
+
] : [],
|
|
206
|
+
'how to get in touch'
|
|
207
|
+
];
|
|
208
|
+
// A site the answers say nothing about gets no guidance: the platform's own
|
|
209
|
+
// derived "When to use this site" says more than a sentence about nobody.
|
|
210
|
+
const said = Boolean(input.name || input.description);
|
|
211
|
+
const when = said ? [
|
|
212
|
+
input.description,
|
|
213
|
+
`Use this site for ${listed(answers)}.`,
|
|
214
|
+
input.languages ? `It serves customers in ${input.languages.replace(/[.\s]+$/, '')}.` : ''
|
|
215
|
+
].filter(Boolean).join(' ') : '';
|
|
216
|
+
const reach = [
|
|
217
|
+
input.phone ? `call ${input.phone}` : '',
|
|
218
|
+
input.email ? `email ${input.email}` : ''
|
|
219
|
+
].filter(Boolean);
|
|
220
|
+
const how = [
|
|
221
|
+
reach.length ? `To reach a person at ${who}, ${reach.join(' or ')}.` : '',
|
|
222
|
+
input.address ? `${input.name || 'The business'} is at ${input.address}.` : ''
|
|
223
|
+
].filter(Boolean).join(' ');
|
|
224
|
+
return {
|
|
225
|
+
whenToUse: when,
|
|
226
|
+
howToUse: how
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The Entity and AI-agent values a site job's inputs imply, in the language
|
|
231
|
+
* its plan writes the site in. `siteName` is the site's own name: it leads
|
|
232
|
+
* the description and the guidance where the inputs name no business, and is
|
|
233
|
+
* never written as the Entity's name, which falls back to it at render and so
|
|
234
|
+
* follows a rename.
|
|
235
|
+
*/ export function aiSiteEntityValues(inputs, options = {}) {
|
|
236
|
+
var _options_language, _options_siteName, _ref, _schema_businessType;
|
|
237
|
+
const details = aiSiteDetailsOf(inputs);
|
|
238
|
+
const schema = aiSiteSchemaOf(inputs);
|
|
239
|
+
const name = text(inputs == null ? void 0 : inputs['businessName']);
|
|
240
|
+
const city = text(inputs == null ? void 0 : inputs['city']);
|
|
241
|
+
const listing = aiSiteSeoProposalForInputs(inputs, (_options_language = options.language) != null ? _options_language : null, (_options_siteName = options.siteName) != null ? _options_siteName : null);
|
|
242
|
+
const description = (_ref = listing == null ? void 0 : listing.values[AI_SITE_SEO_FORM_FIELDS.description]) != null ? _ref : '';
|
|
243
|
+
const areas = aiSiteServiceAreas(details.serviceArea);
|
|
244
|
+
// A business that names no other area serves the city it is in.
|
|
245
|
+
const areaServed = schema.businessType ? areas.length ? areas : city ? [
|
|
246
|
+
city
|
|
247
|
+
] : [] : [];
|
|
248
|
+
const sameAs = details.social ? details.social.split('\n').filter(Boolean) : [];
|
|
249
|
+
const address = details.street ? aiSiteAddressLine(details, city) : '';
|
|
250
|
+
const guidance = aiSiteAgentGuidance({
|
|
251
|
+
name: name || text(options.siteName),
|
|
252
|
+
description,
|
|
253
|
+
areas,
|
|
254
|
+
hours: Boolean(aiSiteHoursSentence(details.hours)),
|
|
255
|
+
prices: Boolean(details.priceRange),
|
|
256
|
+
languages: details.languages,
|
|
257
|
+
phone: details.phone,
|
|
258
|
+
email: details.email,
|
|
259
|
+
address
|
|
260
|
+
});
|
|
261
|
+
return _extends({
|
|
262
|
+
person: schema.person,
|
|
263
|
+
name,
|
|
264
|
+
description,
|
|
265
|
+
email: details.email,
|
|
266
|
+
telephone: details.phone,
|
|
267
|
+
address: {
|
|
268
|
+
streetAddress: details.street,
|
|
269
|
+
// The city is where the business is only where it gave a street to be on.
|
|
270
|
+
addressLocality: details.street || details.postalCode ? city : '',
|
|
271
|
+
addressRegion: details.region,
|
|
272
|
+
postalCode: details.postalCode,
|
|
273
|
+
addressCountry: details.country
|
|
274
|
+
},
|
|
275
|
+
postalAddress: address,
|
|
276
|
+
businessType: (_schema_businessType = schema.businessType) != null ? _schema_businessType : '',
|
|
277
|
+
areaServed,
|
|
278
|
+
openingHours: details.hours,
|
|
279
|
+
priceRange: details.priceRange,
|
|
280
|
+
sameAs
|
|
281
|
+
}, guidance);
|
|
282
|
+
}
|
|
283
|
+
const isBlank = (value)=>value === undefined || value === null || typeof value === 'string' && !value.trim() || Array.isArray(value) && !value.some((entry)=>typeof entry === 'string' && entry.trim());
|
|
284
|
+
const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
285
|
+
/** The contact a start's phone or email answers, as the Setup form's **Contact is for** says it: its default. */ export const AI_SITE_CONTACT_TYPE = 'customer support';
|
|
286
|
+
/**
|
|
287
|
+
* The host updates that put a start's Entity and AI-agent values on its site,
|
|
288
|
+
* as field paths (`seo.entity.telephone`), for the publish's own batch: only
|
|
289
|
+
* fields that are empty on the site, so nothing the owner wrote is replaced.
|
|
290
|
+
* The address is written whole or not at all — a start's street beside the
|
|
291
|
+
* owner's own city would be an address of neither. `{}` when there is
|
|
292
|
+
* nothing to write.
|
|
293
|
+
*
|
|
294
|
+
* Beside the Entity, from what the site already has and the same answers:
|
|
295
|
+
* - the Entity logo is the site's own logo, where it has one and the Entity
|
|
296
|
+
* names none (a Person's Entity picture is their photo, never a logo);
|
|
297
|
+
* - **Contact is for** says what the phone or email this start wrote answers;
|
|
298
|
+
* - Setup → Details → Business details gets the email as the support email
|
|
299
|
+
* (which `{{host.supportEmail}}` and a form that cannot take a message name)
|
|
300
|
+
* and the street address as its one block of text.
|
|
301
|
+
*/ export function aiSiteEntityUpdates(seo, values, site = {}) {
|
|
302
|
+
const current = isRecord(seo) ? seo : {};
|
|
303
|
+
const entity = isRecord(current['entity']) ? current['entity'] : {};
|
|
304
|
+
const agent = isRecord(current['agent']) ? current['agent'] : {};
|
|
305
|
+
const business = isRecord(site.business) ? site.business : {};
|
|
306
|
+
const updates = {};
|
|
307
|
+
const fill = (path, now, next)=>{
|
|
308
|
+
if (!isBlank(next) && isBlank(now)) updates[path] = next;
|
|
309
|
+
};
|
|
310
|
+
const person = values.person && isBlank(entity['type']);
|
|
311
|
+
// As the Setup form's select stores it: the enum's value as text.
|
|
312
|
+
if (person) updates['seo.entity.type'] = String(HostEntityType.PERSON);
|
|
313
|
+
fill('seo.entity.name', entity['name'], values.name);
|
|
314
|
+
fill('seo.entity.description', entity['description'], values.description);
|
|
315
|
+
fill('seo.entity.email', entity['email'], values.email);
|
|
316
|
+
fill('seo.entity.telephone', entity['telephone'], values.telephone);
|
|
317
|
+
if (updates['seo.entity.email'] || updates['seo.entity.telephone']) {
|
|
318
|
+
fill('seo.entity.contactType', entity['contactType'], AI_SITE_CONTACT_TYPE);
|
|
319
|
+
}
|
|
320
|
+
const address = isRecord(entity['address']) ? entity['address'] : {};
|
|
321
|
+
if (Object.values(address).every(isBlank)) {
|
|
322
|
+
for (const [field, value] of Object.entries(values.address)){
|
|
323
|
+
if (value) updates[`seo.entity.address.${field}`] = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// A Person carries none of a business's fields (`siteEntityJsonLd`).
|
|
327
|
+
const isPerson = person || Number(entity['type']) === HostEntityType.PERSON;
|
|
328
|
+
if (!isPerson) {
|
|
329
|
+
fill('seo.entity.logo', entity['logo'], typeof site.logoUrl === 'string' ? site.logoUrl.trim() : '');
|
|
330
|
+
fill('seo.entity.businessType', entity['businessType'], values.businessType);
|
|
331
|
+
fill('seo.entity.areaServed', entity['areaServed'], values.areaServed);
|
|
332
|
+
fill('seo.entity.openingHours', entity['openingHours'], values.openingHours);
|
|
333
|
+
fill('seo.entity.priceRange', entity['priceRange'], values.priceRange);
|
|
334
|
+
}
|
|
335
|
+
fill('seo.entity.sameAs', entity['sameAs'], values.sameAs);
|
|
336
|
+
fill('seo.agent.whenToUse', agent['whenToUse'], values.whenToUse);
|
|
337
|
+
fill('seo.agent.howToUse', agent['howToUse'], values.howToUse);
|
|
338
|
+
fill('business.supportEmail', business['supportEmail'], values.email);
|
|
339
|
+
fill('business.address', business['address'], values.postalAddress);
|
|
340
|
+
return updates;
|
|
341
|
+
}
|
|
342
|
+
/** Trimmed, or `''` for anything that is not a string: as site creation compares names. */ const trimmed = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
343
|
+
/**
|
|
344
|
+
* Which half of a site's own listing is still the one its creation seeded
|
|
345
|
+
* (AGL-3979): `defaultSiteSeo(name)` — the site's name as its title and
|
|
346
|
+
* "Welcome to {name}. See what we do, browse our work and get in touch." as
|
|
347
|
+
* its description. Blank counts as seeded too: nobody chose it.
|
|
348
|
+
*
|
|
349
|
+
* The title is seeded when it is the site's name, or when it still pairs with
|
|
350
|
+
* a seeded description, as it does on a site renamed after it was made. The
|
|
351
|
+
* description is seeded when it is the welcome line for the site's name, for
|
|
352
|
+
* the title's, or for no name. Anything else is the owner's own words.
|
|
353
|
+
*/ export function aiSiteListingSeeded(seo, displayName) {
|
|
354
|
+
const current = isRecord(seo) ? seo : {};
|
|
355
|
+
const title = trimmed(current['title']);
|
|
356
|
+
const description = trimmed(current['description']);
|
|
357
|
+
const name = trimmed(displayName);
|
|
358
|
+
const welcomes = new Set([
|
|
359
|
+
name,
|
|
360
|
+
title,
|
|
361
|
+
''
|
|
362
|
+
].map((each)=>defaultHomeDescription(each)));
|
|
363
|
+
const seededDescription = !description || welcomes.has(description);
|
|
364
|
+
const seededTitle = !title || title === trimmed(defaultSiteSeo(name)['title']) || Boolean(description) && description === defaultHomeDescription(title);
|
|
365
|
+
return {
|
|
366
|
+
title: seededTitle,
|
|
367
|
+
description: seededDescription
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* The host updates that put a start's own listing on its site (AGL-3979), as
|
|
372
|
+
* field paths for the publish's batch: each half only where it is still the
|
|
373
|
+
* seeded default and differs from the listing, so the owner's own title or
|
|
374
|
+
* description is never written over, and a second publish writes nothing.
|
|
375
|
+
* `{}` with no listing to write.
|
|
376
|
+
*/ export function aiSiteListingUpdates(seo, displayName, listing) {
|
|
377
|
+
if (!listing) return {};
|
|
378
|
+
const current = isRecord(seo) ? seo : {};
|
|
379
|
+
const seeded = aiSiteListingSeeded(current, displayName);
|
|
380
|
+
const updates = {};
|
|
381
|
+
const title = trimmed(listing.values[AI_SITE_SEO_FORM_FIELDS.title]);
|
|
382
|
+
const description = trimmed(listing.values[AI_SITE_SEO_FORM_FIELDS.description]);
|
|
383
|
+
if (title && seeded.title && title !== trimmed(current['title'])) updates[AI_SITE_SEO_FORM_FIELDS.title] = title;
|
|
384
|
+
if (description && seeded.description && description !== trimmed(current['description'])) {
|
|
385
|
+
updates[AI_SITE_SEO_FORM_FIELDS.description] = description;
|
|
386
|
+
}
|
|
387
|
+
return updates;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
//# sourceMappingURL=ai-site-entity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-entity.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 { defaultHomeDescription, defaultSiteSeo } from '@aglyn/aglyn/app-utils/default-site'\nimport type { LocalBusinessType } from '@aglyn/aglyn/app-utils/local-business'\nimport { HostEntityType } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { aiSiteAddressLine, aiSiteDetailsOf, aiSiteHoursSentence, aiSiteServiceAreas } from './ai-site-details'\nimport { aiSiteKindOfInputs } from './ai-site-kinds'\nimport { aiSiteSeoProposalForInputs, AI_SITE_SEO_FORM_FIELDS, type AiSiteSeoProposal } from './ai-site-start-seo'\n\n/**\n * Who publishes a site a guided start built, as its structured data and its\n * `/llms.txt` say (AGL-3979).\n *\n * Every tenant page publishes a top-level Organization (or Person) node, and\n * `/llms.txt` carries the site's agent guidance, both read off the host's\n * Setup → SEO → Entity and AI agents fields (`siteEntityJsonLd`,\n * `buildLlmsTxt`). Before this, a guided start wrote none of those fields:\n * every AI-built site published a node holding its name and nothing else — no\n * contact point, no address, no business type — and the guidance was the\n * platform's own default. The answers the person gave are the facts those\n * fields ask for, so the publish writes them.\n *\n * ── Derived, never generated ─────────────────────────────────────────────\n *\n * Each value is the person's own words or arithmetic on them: the schema\n * type from the kind of site they picked (and, for a few kinds, the words\n * they used), the description from the same composition as the site's\n * search listing, the guidance from what they said and gave. No model is\n * asked, and nothing is invented: a field the answers say nothing about is\n * left alone.\n *\n * ── Never over the owner ─────────────────────────────────────────────────\n *\n * {@link aiSiteEntityUpdates} writes only fields that are empty, so a site\n * whose owner already filled its Entity keeps every word of it, and a second\n * publish (Try again) writes nothing it wrote before.\n *\n * ── The site's own listing ───────────────────────────────────────────────\n *\n * `/llms.txt` opens with the site's title and description, and every page\n * without its own title falls back to them. A new site is created with its\n * name and a \"Welcome to …\" line there, which nobody chose, so\n * {@link aiSiteListingUpdates} puts the start's listing over that seeded\n * default, and only over it: a title or description the owner changed keeps\n * their words, and the SEO section offers the start's beside them.\n *\n * Pure: no Firestore and no React.\n */\n\n/** How a kind of site publishes: as a person, or as a business of a `schema.org` LocalBusiness type. */\ninterface KindSchema {\n person?: true\n businessType?: LocalBusinessType\n /** A narrower type the person's words name, first match wins. */\n refine?: ReadonlyArray<readonly [RegExp, LocalBusinessType]>\n}\n\n/**\n * Each `AI_SITE_KINDS` entry's `schema.org` type (`ai-site-entity.spec.ts`\n * holds every kind to an entry). A kind with no business type publishes as an\n * Organization — a blog, a band, an event, a course — unless the person gave\n * a street address or opening hours, which make it a place a visitor goes:\n * a LocalBusiness. Only types on the platform's `LOCAL_BUSINESS_TYPES` list\n * are used, since only those publish.\n */\nexport const AI_SITE_KIND_SCHEMA: Readonly<Record<string, KindSchema>> = {\n business: {\n businessType: 'LocalBusiness',\n refine: [\n [/\\b(?:marketing|consult\\w*|agency|freelanc\\w*|staffing|bookkeep\\w*|design(?:er)?s?)\\b/i, 'ProfessionalService'],\n [/\\b(?:car wash|detailing|auto repair|mechanic|tire|tyre)\\b/i, 'AutomotiveBusiness'],\n ],\n },\n trades: {\n businessType: 'HomeAndConstructionBusiness',\n refine: [\n [/\\bplumb\\w*/i, 'Plumber'],\n [/\\belectric\\w*/i, 'Electrician'],\n [/\\b(?:hvac|heating|air condition\\w*|furnace)\\b/i, 'HVACBusiness'],\n [/\\broof\\w*/i, 'RoofingContractor'],\n [/\\bpaint\\w*/i, 'HousePainter'],\n [/\\blocksmith\\w*/i, 'Locksmith'],\n [/\\bmov(?:ing|ers?)\\b/i, 'MovingCompany'],\n [/\\b(?:contractor|remodel\\w*|renovat\\w*|builders?|construction)\\b/i, 'GeneralContractor'],\n ],\n },\n professional: {\n businessType: 'ProfessionalService',\n refine: [\n [/\\b(?:law|lawyers?|attorneys?|legal|solicitors?)\\b/i, 'LegalService'],\n [/\\b(?:accounting|accountants?|bookkeep\\w*|tax|cpa)\\b/i, 'AccountingService'],\n ],\n },\n wellness: {\n businessType: 'HealthAndBeautyBusiness',\n refine: [\n [/\\b(?:dentist\\w*|dental|orthodont\\w*)\\b/i, 'Dentist'],\n [/\\b(?:clinic|doctors?|physicians?|medical|therap\\w*|chiropract\\w*|physio\\w*|counsel\\w*)\\b/i, 'MedicalBusiness'],\n ],\n },\n restaurant: {\n businessType: 'Restaurant',\n refine: [[/\\b(?:caf[eé]s?|coffee|bakery|bakeries|bars?|brewery|food truck|deli)\\b/i, 'FoodEstablishment']],\n },\n store: { businessType: 'Store' },\n portfolio: {},\n studio: { businessType: 'ProfessionalService' },\n photography: { businessType: 'ProfessionalService' },\n blog: {},\n events: {},\n fitness: { businessType: 'SportsActivityLocation' },\n yoga: { businessType: 'SportsActivityLocation' },\n beauty: { businessType: 'HealthAndBeautyBusiness' },\n realestate: { businessType: 'RealEstateAgent' },\n education: { refine: [[/\\b(?:day ?care|child ?care|preschool|nursery)\\b/i, 'ChildCare']] },\n nonprofit: { refine: [[/\\b(?:animal (?:shelter|rescue)|pet rescue)\\b/i, 'AnimalShelter']] },\n music: {},\n personal: { person: true },\n landing: {},\n 'coming-soon': {},\n}\n\n/** The schema a site's answers publish under: a Person, or an Organization with a business type or none. */\nexport interface AiSiteSchema {\n person: boolean\n businessType: LocalBusinessType | null\n}\n\n/** What a job's inputs say the site publishes as. */\nexport function aiSiteSchemaOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiSiteSchema {\n const kind = aiSiteKindOfInputs(inputs)\n const entry = (kind && AI_SITE_KIND_SCHEMA[kind.id]) || {}\n if (entry.person) return { person: true, businessType: null }\n const words = [inputs?.['businessType'], inputs?.['audience']].filter((value) => typeof value === 'string').join(' ')\n const refined = entry.refine?.find(([pattern]) => pattern.test(words))?.[1]\n const details = aiSiteDetailsOf(inputs)\n const place = Boolean(details.street || details.hours)\n return { person: false, businessType: refined ?? entry.businessType ?? (place ? 'LocalBusiness' : null) }\n}\n\n/** The Entity and AI-agent values a job's answers give; a field they say nothing about is `''` or `[]`. */\nexport interface AiSiteEntityValues {\n person: boolean\n name: string\n description: string\n email: string\n telephone: string\n address: {\n streetAddress: string\n addressLocality: string\n addressRegion: string\n postalCode: string\n addressCountry: string\n }\n /** The street address as one line, as Setup → Details → Business details holds it; `''` without a street. */\n postalAddress: string\n businessType: string\n areaServed: string[]\n openingHours: string\n priceRange: string\n sameAs: string[]\n whenToUse: string\n howToUse: string\n}\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : '')\n\n/** \"a, b and c\". */\nfunction listed(parts: readonly string[]): string {\n if (parts.length < 2) return parts.join('')\n return `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n}\n\n/**\n * The site's agent guidance from its answers (AGL-3979): when an agent should\n * use the site — what the business is, then what the site answers — and,\n * where the person gave a way to reach it, how to reach a person. Written in\n * English, as the rest of `/llms.txt` is.\n */\nexport function aiSiteAgentGuidance(input: {\n name: string\n description: string\n areas: readonly string[]\n hours: boolean\n prices: boolean\n languages: string\n phone: string\n email: string\n address: string\n}): { whenToUse: string; howToUse: string } {\n const who = input.name || 'this business'\n const answers = [\n `what ${who} offers`,\n ...(input.areas.length ? [`the areas it serves (${input.areas.slice(0, 6).join(', ')})`] : []),\n ...(input.hours ? ['its opening hours'] : []),\n ...(input.prices ? ['its prices'] : []),\n 'how to get in touch',\n ]\n // A site the answers say nothing about gets no guidance: the platform's own\n // derived \"When to use this site\" says more than a sentence about nobody.\n const said = Boolean(input.name || input.description)\n const when = said\n ? [\n input.description,\n `Use this site for ${listed(answers)}.`,\n input.languages ? `It serves customers in ${input.languages.replace(/[.\\s]+$/, '')}.` : '',\n ]\n .filter(Boolean)\n .join(' ')\n : ''\n const reach = [input.phone ? `call ${input.phone}` : '', input.email ? `email ${input.email}` : ''].filter(Boolean)\n const how = [\n reach.length ? `To reach a person at ${who}, ${reach.join(' or ')}.` : '',\n input.address ? `${input.name || 'The business'} is at ${input.address}.` : '',\n ]\n .filter(Boolean)\n .join(' ')\n return { whenToUse: when, howToUse: how }\n}\n\n/**\n * The Entity and AI-agent values a site job's inputs imply, in the language\n * its plan writes the site in. `siteName` is the site's own name: it leads\n * the description and the guidance where the inputs name no business, and is\n * never written as the Entity's name, which falls back to it at render and so\n * follows a rename.\n */\nexport function aiSiteEntityValues(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n options: { language?: string | null; siteName?: string | null } = {},\n): AiSiteEntityValues {\n const details = aiSiteDetailsOf(inputs)\n const schema = aiSiteSchemaOf(inputs)\n const name = text(inputs?.['businessName'])\n const city = text(inputs?.['city'])\n const listing = aiSiteSeoProposalForInputs(inputs, options.language ?? null, options.siteName ?? null)\n const description = listing?.values[AI_SITE_SEO_FORM_FIELDS.description] ?? ''\n const areas = aiSiteServiceAreas(details.serviceArea)\n // A business that names no other area serves the city it is in.\n const areaServed = schema.businessType ? (areas.length ? areas : city ? [city] : []) : []\n const sameAs = details.social ? details.social.split('\\n').filter(Boolean) : []\n const address = details.street ? aiSiteAddressLine(details, city) : ''\n const guidance = aiSiteAgentGuidance({\n name: name || text(options.siteName),\n description,\n areas,\n hours: Boolean(aiSiteHoursSentence(details.hours)),\n prices: Boolean(details.priceRange),\n languages: details.languages,\n phone: details.phone,\n email: details.email,\n address,\n })\n return {\n person: schema.person,\n name,\n description,\n email: details.email,\n telephone: details.phone,\n address: {\n streetAddress: details.street,\n // The city is where the business is only where it gave a street to be on.\n addressLocality: details.street || details.postalCode ? city : '',\n addressRegion: details.region,\n postalCode: details.postalCode,\n addressCountry: details.country,\n },\n postalAddress: address,\n businessType: schema.businessType ?? '',\n areaServed,\n openingHours: details.hours,\n priceRange: details.priceRange,\n sameAs,\n ...guidance,\n }\n}\n\nconst isBlank = (value: unknown): boolean =>\n value === undefined ||\n value === null ||\n (typeof value === 'string' && !value.trim()) ||\n (Array.isArray(value) && !value.some((entry) => typeof entry === 'string' && entry.trim()))\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/** The contact a start's phone or email answers, as the Setup form's **Contact is for** says it: its default. */\nexport const AI_SITE_CONTACT_TYPE = 'customer support'\n\n/**\n * The host updates that put a start's Entity and AI-agent values on its site,\n * as field paths (`seo.entity.telephone`), for the publish's own batch: only\n * fields that are empty on the site, so nothing the owner wrote is replaced.\n * The address is written whole or not at all — a start's street beside the\n * owner's own city would be an address of neither. `{}` when there is\n * nothing to write.\n *\n * Beside the Entity, from what the site already has and the same answers:\n * - the Entity logo is the site's own logo, where it has one and the Entity\n * names none (a Person's Entity picture is their photo, never a logo);\n * - **Contact is for** says what the phone or email this start wrote answers;\n * - Setup → Details → Business details gets the email as the support email\n * (which `{{host.supportEmail}}` and a form that cannot take a message name)\n * and the street address as its one block of text.\n */\nexport function aiSiteEntityUpdates(\n seo: unknown,\n values: AiSiteEntityValues,\n site: { logoUrl?: unknown; business?: unknown } = {},\n): Record<string, unknown> {\n const current = isRecord(seo) ? seo : {}\n const entity = isRecord(current['entity']) ? current['entity'] : {}\n const agent = isRecord(current['agent']) ? current['agent'] : {}\n const business = isRecord(site.business) ? site.business : {}\n const updates: Record<string, unknown> = {}\n const fill = (path: string, now: unknown, next: string | string[]) => {\n if (!isBlank(next) && isBlank(now)) updates[path] = next\n }\n const person = values.person && isBlank(entity['type'])\n // As the Setup form's select stores it: the enum's value as text.\n if (person) updates['seo.entity.type'] = String(HostEntityType.PERSON)\n fill('seo.entity.name', entity['name'], values.name)\n fill('seo.entity.description', entity['description'], values.description)\n fill('seo.entity.email', entity['email'], values.email)\n fill('seo.entity.telephone', entity['telephone'], values.telephone)\n if (updates['seo.entity.email'] || updates['seo.entity.telephone']) {\n fill('seo.entity.contactType', entity['contactType'], AI_SITE_CONTACT_TYPE)\n }\n const address = isRecord(entity['address']) ? entity['address'] : {}\n if (Object.values(address).every(isBlank)) {\n for (const [field, value] of Object.entries(values.address)) {\n if (value) updates[`seo.entity.address.${field}`] = value\n }\n }\n // A Person carries none of a business's fields (`siteEntityJsonLd`).\n const isPerson = person || Number(entity['type']) === HostEntityType.PERSON\n if (!isPerson) {\n fill('seo.entity.logo', entity['logo'], typeof site.logoUrl === 'string' ? site.logoUrl.trim() : '')\n fill('seo.entity.businessType', entity['businessType'], values.businessType)\n fill('seo.entity.areaServed', entity['areaServed'], values.areaServed)\n fill('seo.entity.openingHours', entity['openingHours'], values.openingHours)\n fill('seo.entity.priceRange', entity['priceRange'], values.priceRange)\n }\n fill('seo.entity.sameAs', entity['sameAs'], values.sameAs)\n fill('seo.agent.whenToUse', agent['whenToUse'], values.whenToUse)\n fill('seo.agent.howToUse', agent['howToUse'], values.howToUse)\n fill('business.supportEmail', business['supportEmail'], values.email)\n fill('business.address', business['address'], values.postalAddress)\n return updates\n}\n\n/** Trimmed, or `''` for anything that is not a string: as site creation compares names. */\nconst trimmed = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/**\n * Which half of a site's own listing is still the one its creation seeded\n * (AGL-3979): `defaultSiteSeo(name)` — the site's name as its title and\n * \"Welcome to {name}. See what we do, browse our work and get in touch.\" as\n * its description. Blank counts as seeded too: nobody chose it.\n *\n * The title is seeded when it is the site's name, or when it still pairs with\n * a seeded description, as it does on a site renamed after it was made. The\n * description is seeded when it is the welcome line for the site's name, for\n * the title's, or for no name. Anything else is the owner's own words.\n */\nexport function aiSiteListingSeeded(seo: unknown, displayName: unknown): { title: boolean; description: boolean } {\n const current = isRecord(seo) ? seo : {}\n const title = trimmed(current['title'])\n const description = trimmed(current['description'])\n const name = trimmed(displayName)\n const welcomes = new Set([name, title, ''].map((each) => defaultHomeDescription(each)))\n const seededDescription = !description || welcomes.has(description)\n const seededTitle =\n !title ||\n title === trimmed(defaultSiteSeo(name)['title']) ||\n (Boolean(description) && description === defaultHomeDescription(title))\n return { title: seededTitle, description: seededDescription }\n}\n\n/**\n * The host updates that put a start's own listing on its site (AGL-3979), as\n * field paths for the publish's batch: each half only where it is still the\n * seeded default and differs from the listing, so the owner's own title or\n * description is never written over, and a second publish writes nothing.\n * `{}` with no listing to write.\n */\nexport function aiSiteListingUpdates(\n seo: unknown,\n displayName: unknown,\n listing: AiSiteSeoProposal | null,\n): Record<string, string> {\n if (!listing) return {}\n const current = isRecord(seo) ? seo : {}\n const seeded = aiSiteListingSeeded(current, displayName)\n const updates: Record<string, string> = {}\n const title = trimmed(listing.values[AI_SITE_SEO_FORM_FIELDS.title])\n const description = trimmed(listing.values[AI_SITE_SEO_FORM_FIELDS.description])\n if (title && seeded.title && title !== trimmed(current['title'])) updates[AI_SITE_SEO_FORM_FIELDS.title] = title\n if (description && seeded.description && description !== trimmed(current['description'])) {\n updates[AI_SITE_SEO_FORM_FIELDS.description] = description\n }\n return updates\n}\n"],"names":["defaultHomeDescription","defaultSiteSeo","HostEntityType","aiSiteAddressLine","aiSiteDetailsOf","aiSiteHoursSentence","aiSiteServiceAreas","aiSiteKindOfInputs","aiSiteSeoProposalForInputs","AI_SITE_SEO_FORM_FIELDS","AI_SITE_KIND_SCHEMA","business","businessType","refine","trades","professional","wellness","restaurant","store","portfolio","studio","photography","blog","events","fitness","yoga","beauty","realestate","education","nonprofit","music","personal","person","landing","aiSiteSchemaOf","inputs","refined","entry","kind","id","words","filter","value","join","find","pattern","test","details","place","Boolean","street","hours","text","replace","trim","listed","parts","length","slice","aiSiteAgentGuidance","input","who","name","answers","areas","prices","said","description","when","languages","reach","phone","email","how","address","whenToUse","howToUse","aiSiteEntityValues","options","schema","city","listing","language","siteName","values","serviceArea","areaServed","sameAs","social","split","guidance","priceRange","telephone","streetAddress","addressLocality","postalCode","addressRegion","region","addressCountry","country","postalAddress","openingHours","isBlank","undefined","Array","isArray","some","isRecord","AI_SITE_CONTACT_TYPE","aiSiteEntityUpdates","seo","site","current","entity","agent","updates","fill","path","now","next","String","PERSON","Object","every","field","entries","isPerson","Number","logoUrl","trimmed","aiSiteListingSeeded","displayName","title","welcomes","Set","map","each","seededDescription","has","seededTitle","aiSiteListingUpdates","seeded"],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
* Sites I like (AGL-3975, part of AGL-3973): the sites a person names in the
|
|
19
|
+
* guided start as inspiration, as the job carries them.
|
|
20
|
+
*
|
|
21
|
+
* Free names one site and a paid plan three. From each the site job takes
|
|
22
|
+
* SIGNALS only — how the sections run, the colors, the type's mood, the shape
|
|
23
|
+
* of buttons and cards, how the pages move and, on a paid plan, which
|
|
24
|
+
* features it offers — each read by code into the site job's own closed sets
|
|
25
|
+
* (`jobs/ai-site-inspiration-signals.ts`). Never a word, a picture or a logo
|
|
26
|
+
* of theirs: nothing a page says reaches a prompt.
|
|
27
|
+
*
|
|
28
|
+
* Behind `release_ai_site_inspiration`, off by default: with it off the
|
|
29
|
+
* guided start asks for no site and a job's inspiration is never read.
|
|
30
|
+
*
|
|
31
|
+
* Pure and console-safe: the input's name, the limits and how an answer is
|
|
32
|
+
* read. No network.
|
|
33
|
+
*/
|
|
34
|
+
/** The release flag the whole feature waits behind. */
|
|
35
|
+
export declare const AI_SITE_INSPIRATION_FLAG: "release_ai_site_inspiration";
|
|
36
|
+
/**
|
|
37
|
+
* The job input the sites travel in: one string, the addresses separated by
|
|
38
|
+
* spaces, because a job's inputs are scalars (`parseCreateAiJobBody`).
|
|
39
|
+
*/
|
|
40
|
+
export declare const AI_SITE_INSPIRATION_INPUT = "inspiration";
|
|
41
|
+
/** How many sites a plan may name: Free one, every paid plan three. */
|
|
42
|
+
export declare const AI_SITE_INSPIRATION_LIMITS: {
|
|
43
|
+
readonly free: 1;
|
|
44
|
+
readonly paid: 3;
|
|
45
|
+
};
|
|
46
|
+
/** The most any job may carry, whatever its plan. */
|
|
47
|
+
export declare const AI_SITE_INSPIRATION_MAX: 3;
|
|
48
|
+
/** The longest address one site may be given as. */
|
|
49
|
+
export declare const AI_SITE_INSPIRATION_URL_MAX_CHARS = 300;
|
|
50
|
+
/** What the guided start says a paid plan adds, beside a Free workspace's locked fields. */
|
|
51
|
+
export declare const AI_SITE_INSPIRATION_PAID_NOTE: string;
|
|
52
|
+
/** How many sites a workspace may name. */
|
|
53
|
+
export declare function aiSiteInspirationLimit(freeTaste: boolean | null | undefined): number;
|
|
54
|
+
/**
|
|
55
|
+
* One address as the job keeps it: https only, no credentials, the default
|
|
56
|
+
* port, no fragment. `null` for anything else. A bare host ("example.com")
|
|
57
|
+
* is read as its https address, the way a person types one.
|
|
58
|
+
*/
|
|
59
|
+
export declare function aiSiteInspirationUrl(value: unknown): string | null;
|
|
60
|
+
/**
|
|
61
|
+
* The sites a job's inputs name, or the sentence the door refuses them with.
|
|
62
|
+
* Absent or empty is none. Each must be an https address; more than
|
|
63
|
+
* {@link AI_SITE_INSPIRATION_MAX} is refused, and the same site twice counts
|
|
64
|
+
* once. The plan's own limit is the door's (`aiSiteInspirationRefusal`).
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseAiSiteInspiration(inputs: Readonly<Record<string, unknown>> | null | undefined): string[] | string;
|
|
67
|
+
/** Why a workspace may not name this many sites, in a sentence a member reads; `null` when it may. */
|
|
68
|
+
export declare function aiSiteInspirationRefusal(urls: readonly string[], freeTaste: boolean | null | undefined): string | null;
|
|
69
|
+
/** The sites as the job input carries them; empty for none, which the start leaves out of its inputs. */
|
|
70
|
+
export declare function aiSiteInspirationInput(urls: readonly string[]): string;
|
|
71
|
+
/** A site as the review names it: its host, without `www.`. */
|
|
72
|
+
export declare function aiSiteInspirationHost(url: string): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
* Sites I like (AGL-3975, part of AGL-3973): the sites a person names in the
|
|
18
|
+
* guided start as inspiration, as the job carries them.
|
|
19
|
+
*
|
|
20
|
+
* Free names one site and a paid plan three. From each the site job takes
|
|
21
|
+
* SIGNALS only — how the sections run, the colors, the type's mood, the shape
|
|
22
|
+
* of buttons and cards, how the pages move and, on a paid plan, which
|
|
23
|
+
* features it offers — each read by code into the site job's own closed sets
|
|
24
|
+
* (`jobs/ai-site-inspiration-signals.ts`). Never a word, a picture or a logo
|
|
25
|
+
* of theirs: nothing a page says reaches a prompt.
|
|
26
|
+
*
|
|
27
|
+
* Behind `release_ai_site_inspiration`, off by default: with it off the
|
|
28
|
+
* guided start asks for no site and a job's inspiration is never read.
|
|
29
|
+
*
|
|
30
|
+
* Pure and console-safe: the input's name, the limits and how an answer is
|
|
31
|
+
* read. No network.
|
|
32
|
+
*/ /** The release flag the whole feature waits behind. */ export const AI_SITE_INSPIRATION_FLAG = 'release_ai_site_inspiration';
|
|
33
|
+
/**
|
|
34
|
+
* The job input the sites travel in: one string, the addresses separated by
|
|
35
|
+
* spaces, because a job's inputs are scalars (`parseCreateAiJobBody`).
|
|
36
|
+
*/ export const AI_SITE_INSPIRATION_INPUT = 'inspiration';
|
|
37
|
+
/** How many sites a plan may name: Free one, every paid plan three. */ export const AI_SITE_INSPIRATION_LIMITS = {
|
|
38
|
+
free: 1,
|
|
39
|
+
paid: 3
|
|
40
|
+
};
|
|
41
|
+
/** The most any job may carry, whatever its plan. */ export const AI_SITE_INSPIRATION_MAX = AI_SITE_INSPIRATION_LIMITS.paid;
|
|
42
|
+
/** The longest address one site may be given as. */ export const AI_SITE_INSPIRATION_URL_MAX_CHARS = 300;
|
|
43
|
+
/** What the guided start says a paid plan adds, beside a Free workspace's locked fields. */ export const AI_SITE_INSPIRATION_PAID_NOTE = `Paid plans can name up to ${AI_SITE_INSPIRATION_LIMITS.paid} sites, and also match the features they offer, like booking, a store or a blog.`;
|
|
44
|
+
/** How many sites a workspace may name. */ export function aiSiteInspirationLimit(freeTaste) {
|
|
45
|
+
return freeTaste ? AI_SITE_INSPIRATION_LIMITS.free : AI_SITE_INSPIRATION_LIMITS.paid;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One address as the job keeps it: https only, no credentials, the default
|
|
49
|
+
* port, no fragment. `null` for anything else. A bare host ("example.com")
|
|
50
|
+
* is read as its https address, the way a person types one.
|
|
51
|
+
*/ export function aiSiteInspirationUrl(value) {
|
|
52
|
+
if (typeof value !== 'string') return null;
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
if (!trimmed || trimmed.length > AI_SITE_INSPIRATION_URL_MAX_CHARS) return null;
|
|
55
|
+
const written = /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
56
|
+
let url;
|
|
57
|
+
try {
|
|
58
|
+
url = new URL(written);
|
|
59
|
+
} catch (unused) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (url.protocol !== 'https:' || url.username || url.password) return null;
|
|
63
|
+
if (url.port && url.port !== '443') return null;
|
|
64
|
+
// A host with no dot is a typo or an internal name, never a public site.
|
|
65
|
+
if (!url.hostname.includes('.') || url.hostname.endsWith('.')) return null;
|
|
66
|
+
url.hash = '';
|
|
67
|
+
return url.toString();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The sites a job's inputs name, or the sentence the door refuses them with.
|
|
71
|
+
* Absent or empty is none. Each must be an https address; more than
|
|
72
|
+
* {@link AI_SITE_INSPIRATION_MAX} is refused, and the same site twice counts
|
|
73
|
+
* once. The plan's own limit is the door's (`aiSiteInspirationRefusal`).
|
|
74
|
+
*/ export function parseAiSiteInspiration(inputs) {
|
|
75
|
+
const raw = inputs == null ? void 0 : inputs[AI_SITE_INSPIRATION_INPUT];
|
|
76
|
+
if (raw === undefined || raw === null || raw === '') return [];
|
|
77
|
+
if (typeof raw !== 'string') {
|
|
78
|
+
return `${AI_SITE_INSPIRATION_INPUT} must be text: the addresses of the sites you like, separated by spaces`;
|
|
79
|
+
}
|
|
80
|
+
const urls = [];
|
|
81
|
+
for (const word of raw.split(/\s+/).filter(Boolean)){
|
|
82
|
+
const url = aiSiteInspirationUrl(word);
|
|
83
|
+
if (!url) return 'Give each site you like as its https address, like https://example.com.';
|
|
84
|
+
if (!urls.includes(url)) urls.push(url);
|
|
85
|
+
}
|
|
86
|
+
if (urls.length > AI_SITE_INSPIRATION_MAX) return `Name at most ${AI_SITE_INSPIRATION_MAX} sites you like.`;
|
|
87
|
+
return urls;
|
|
88
|
+
}
|
|
89
|
+
/** Why a workspace may not name this many sites, in a sentence a member reads; `null` when it may. */ export function aiSiteInspirationRefusal(urls, freeTaste) {
|
|
90
|
+
const limit = aiSiteInspirationLimit(freeTaste);
|
|
91
|
+
if (urls.length <= limit) return null;
|
|
92
|
+
return freeTaste ? `A Free workspace can name ${AI_SITE_INSPIRATION_LIMITS.free} site you like. ${AI_SITE_INSPIRATION_PAID_NOTE}` : `Name at most ${limit} sites you like.`;
|
|
93
|
+
}
|
|
94
|
+
/** The sites as the job input carries them; empty for none, which the start leaves out of its inputs. */ export function aiSiteInspirationInput(urls) {
|
|
95
|
+
const kept = [];
|
|
96
|
+
for (const url of urls.map(aiSiteInspirationUrl)){
|
|
97
|
+
if (url && !kept.includes(url)) kept.push(url);
|
|
98
|
+
}
|
|
99
|
+
return kept.slice(0, AI_SITE_INSPIRATION_MAX).join(' ');
|
|
100
|
+
}
|
|
101
|
+
/** A site as the review names it: its host, without `www.`. */ export function aiSiteInspirationHost(url) {
|
|
102
|
+
try {
|
|
103
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
104
|
+
} catch (unused) {
|
|
105
|
+
return url;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=ai-site-inspiration.js.map
|