@aglyn/plugins-ai 1.0.0-beta.269 → 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 +8 -6
- package/src/lib/components/ai-credits-card.component.js +39 -22
- 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-links.d.ts +6 -0
- package/src/lib/components/ai-job-links.js +9 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-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-products-hub-card.component.d.ts +6 -0
- package/src/lib/components/ai-products-hub-card.component.js +21 -6
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +3 -2
- package/src/lib/components/ai-site-build-page.component.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-store-finish-card.component.d.ts +14 -3
- package/src/lib/components/ai-store-finish-card.component.js +67 -9
- package/src/lib/components/ai-store-finish-card.component.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 +14 -3
- 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.d.ts +3 -0
- package/src/lib/jobs/ai-job-build-step.js +13 -5
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.d.ts +24 -1
- package/src/lib/jobs/ai-job-form-step.js +93 -7
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +35 -11
- package/src/lib/jobs/ai-job-page-language.js +68 -2
- 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 +40 -17
- 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-owner-photos.d.ts +57 -0
- package/src/lib/jobs/ai-layout-owner-photos.js +110 -0
- package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -0
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +62 -4
- package/src/lib/jobs/ai-layout-stock-photos.js +523 -72
- 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-agl3942-recordings.d.ts +29 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3942-recordings.js +291 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3942-recordings.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/native-nav.d.ts +26 -0
- package/src/lib/native-nav.js +58 -0
- package/src/lib/native-nav.js.map +1 -0
- 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
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { AI_SITE_DETAIL_KEYS, AI_SITE_NO_DETAILS, aiSiteDetailProblem } from "./ai-site-details.js";
|
|
18
|
+
import { aiSiteKind, aiSiteKindFor } from "./ai-site-kinds.js";
|
|
19
|
+
import { AI_SITE_INSPIRATION_INPUT, aiSiteInspirationInput, aiSiteInspirationLimit, aiSiteInspirationRefusal, aiSiteInspirationUrl } from "./ai-site-inspiration.js";
|
|
17
20
|
import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from "./ai-site-job.js";
|
|
18
21
|
/**
|
|
19
22
|
* The examples the start offers, in the order they are shown. The platform's
|
|
@@ -71,15 +74,49 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
|
|
|
71
74
|
* unless it is told not to, and the Inbox because it is what every submission
|
|
72
75
|
* does anyway — filing a lead as well is the addition, and an addition is the
|
|
73
76
|
* thing a person should have to choose.
|
|
74
|
-
*/ export const AI_SITE_START_ANSWERS = {
|
|
77
|
+
*/ export const AI_SITE_START_ANSWERS = _extends({
|
|
75
78
|
siteType: '',
|
|
76
79
|
audience: '',
|
|
77
80
|
example: null,
|
|
78
81
|
kind: null,
|
|
79
82
|
pages: 5,
|
|
80
83
|
submissions: 'inbox',
|
|
81
|
-
welcomeEmail: true
|
|
82
|
-
|
|
84
|
+
welcomeEmail: true,
|
|
85
|
+
city: '',
|
|
86
|
+
brand: ''
|
|
87
|
+
}, AI_SITE_NO_DETAILS);
|
|
88
|
+
/** Every extra, in the order the review lists them. */ export const AI_SITE_START_EXTRA_KEYS = [
|
|
89
|
+
'phone',
|
|
90
|
+
'email',
|
|
91
|
+
'tagline',
|
|
92
|
+
'founded',
|
|
93
|
+
'serviceArea',
|
|
94
|
+
'priceRange',
|
|
95
|
+
'languages',
|
|
96
|
+
'brand',
|
|
97
|
+
'street',
|
|
98
|
+
'city',
|
|
99
|
+
'region',
|
|
100
|
+
'postalCode',
|
|
101
|
+
'country',
|
|
102
|
+
'hours',
|
|
103
|
+
'social'
|
|
104
|
+
];
|
|
105
|
+
/** What is wrong with one extra, worded for beside its field; `null` when it may be used. */ export function aiSiteStartExtraProblem(key, value) {
|
|
106
|
+
if (key === 'city' || key === 'brand') {
|
|
107
|
+
return value.trim().length > AI_SITE_INPUT_MAX_CHARS ? {
|
|
108
|
+
code: 'too_long',
|
|
109
|
+
message: `Keep this under ${AI_SITE_INPUT_MAX_CHARS} characters.`
|
|
110
|
+
} : null;
|
|
111
|
+
}
|
|
112
|
+
return aiSiteDetailProblem(key, value);
|
|
113
|
+
}
|
|
114
|
+
/** The extras a person gave, in the review's order: what "More details" was used for. */ export function aiSiteStartExtrasGiven(answers) {
|
|
115
|
+
return AI_SITE_START_EXTRA_KEYS.filter((key)=>{
|
|
116
|
+
var _answers_key;
|
|
117
|
+
return ((_answers_key = answers[key]) != null ? _answers_key : '').trim() !== '';
|
|
118
|
+
});
|
|
119
|
+
}
|
|
83
120
|
/** The kind of site the answers pick, else the one what the site is and who it is for suggest. */ export function aiSiteStartKind(answers) {
|
|
84
121
|
var _aiSiteKind, _answers_audience;
|
|
85
122
|
return (_aiSiteKind = aiSiteKind(answers.kind)) != null ? _aiSiteKind : aiSiteKindFor(answers.siteType, (_answers_audience = answers.audience) != null ? _answers_audience : '');
|
|
@@ -113,7 +150,23 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
|
|
|
113
150
|
if (!AI_SITE_SUBMISSIONS.includes(answers.submissions)) {
|
|
114
151
|
return 'Say where the contact form’s submissions go.';
|
|
115
152
|
}
|
|
116
|
-
|
|
153
|
+
for (const key of AI_SITE_START_EXTRA_KEYS){
|
|
154
|
+
var _answers_key;
|
|
155
|
+
const problem = aiSiteStartExtraProblem(key, (_answers_key = answers[key]) != null ? _answers_key : '');
|
|
156
|
+
if (problem) return problem.message;
|
|
157
|
+
}
|
|
158
|
+
return aiSiteStartInspirationRefusal(answers, options);
|
|
159
|
+
}
|
|
160
|
+
/** The sites they typed, without the empty fields. */ export function aiSiteStartInspirationTyped(answers) {
|
|
161
|
+
var _answers_inspiration;
|
|
162
|
+
return ((_answers_inspiration = answers.inspiration) != null ? _answers_inspiration : []).map((entry)=>entry.trim()).filter(Boolean);
|
|
163
|
+
}
|
|
164
|
+
/** Why the sites they like cannot go forward, as the door would refuse them; `null` when they can. */ export function aiSiteStartInspirationRefusal(answers, options = {}) {
|
|
165
|
+
const typed = aiSiteStartInspirationTyped(answers);
|
|
166
|
+
if (typed.some((entry)=>!aiSiteInspirationUrl(entry))) {
|
|
167
|
+
return 'Give each site you like as its https address, like https://example.com.';
|
|
168
|
+
}
|
|
169
|
+
return aiSiteInspirationRefusal(typed, options.freeTaste);
|
|
117
170
|
}
|
|
118
171
|
/**
|
|
119
172
|
* The answers as the brief the `site` job is created with: the plan step reads
|
|
@@ -157,21 +210,35 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
|
|
|
157
210
|
* reached the model with no name had the model invent one for its header and
|
|
158
211
|
* footer.
|
|
159
212
|
*/ export function aiSiteStartInputs(answers) {
|
|
160
|
-
var _ref;
|
|
213
|
+
var _ref, _answers_city, _answers_brand;
|
|
161
214
|
const example = aiSiteStartExample(answers);
|
|
162
215
|
const kind = aiSiteStartKind(answers);
|
|
163
|
-
|
|
216
|
+
// The details are sent only where given (AGL-3979): the plan step lists
|
|
217
|
+
// every scalar input on a line of its own, and a start without them is
|
|
218
|
+
// asked exactly what it was asked before.
|
|
219
|
+
const details = {};
|
|
220
|
+
for (const key of AI_SITE_DETAIL_KEYS){
|
|
221
|
+
var _answers_key;
|
|
222
|
+
const value = ((_answers_key = answers[key]) != null ? _answers_key : '').trim();
|
|
223
|
+
if (value) details[key] = value;
|
|
224
|
+
}
|
|
225
|
+
// The sites they like travel only when there are some (AGL-3975), so a
|
|
226
|
+
// start without them asks for exactly what it always did.
|
|
227
|
+
const inspiration = aiSiteInspirationInput(aiSiteStartInspirationTyped(answers));
|
|
228
|
+
return _extends({
|
|
164
229
|
businessType: answers.siteType.trim(),
|
|
165
230
|
audience: answers.audience.trim(),
|
|
166
231
|
starter: (_ref = example == null ? void 0 : example.id) != null ? _ref : kind.starter,
|
|
167
232
|
siteKind: kind.id,
|
|
168
233
|
pages: answers.pages,
|
|
169
234
|
businessName: '',
|
|
170
|
-
city: '',
|
|
171
|
-
brand: '',
|
|
235
|
+
city: ((_answers_city = answers.city) != null ? _answers_city : '').trim(),
|
|
236
|
+
brand: ((_answers_brand = answers.brand) != null ? _answers_brand : '').trim(),
|
|
172
237
|
submissions: answers.submissions,
|
|
173
238
|
welcomeEmail: answers.welcomeEmail
|
|
174
|
-
}
|
|
239
|
+
}, details, inspiration ? {
|
|
240
|
+
[AI_SITE_INSPIRATION_INPUT]: inspiration
|
|
241
|
+
} : {});
|
|
175
242
|
}
|
|
176
243
|
/** The site inputs a guided start's answers become, by name: what a summary carries back (AGL-3596). */ export const AI_SITE_START_INPUT_KEYS = [
|
|
177
244
|
'businessType',
|
|
@@ -183,7 +250,9 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
|
|
|
183
250
|
'city',
|
|
184
251
|
'brand',
|
|
185
252
|
'submissions',
|
|
186
|
-
'welcomeEmail'
|
|
253
|
+
'welcomeEmail',
|
|
254
|
+
...AI_SITE_DETAIL_KEYS,
|
|
255
|
+
AI_SITE_INSPIRATION_INPUT
|
|
187
256
|
];
|
|
188
257
|
/** A site job's scalar inputs, and nothing else its inputs carry; `null` when it has none. */ export function aiSiteStartInputsOf(inputs) {
|
|
189
258
|
if (!inputs) return null;
|
|
@@ -210,15 +279,99 @@ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from ".
|
|
|
210
279
|
const pages = typeof (inputs == null ? void 0 : inputs['pages']) === 'number' ? Math.round(inputs['pages']) : AI_SITE_START_ANSWERS.pages;
|
|
211
280
|
const starter = text('starter');
|
|
212
281
|
const submissions = text('submissions');
|
|
213
|
-
return {
|
|
282
|
+
return _extends({
|
|
214
283
|
siteType,
|
|
215
284
|
audience: text('audience'),
|
|
216
285
|
example: AI_SITE_START_EXAMPLES.some((entry)=>entry.id === starter) ? starter : null,
|
|
217
286
|
kind: (_ref = (_aiSiteKind = aiSiteKind(text('siteKind'))) == null ? void 0 : _aiSiteKind.id) != null ? _ref : null,
|
|
218
287
|
pages: Math.min(band.max, Math.max(band.min, pages)),
|
|
219
288
|
submissions: AI_SITE_SUBMISSIONS.includes(submissions) ? submissions : AI_SITE_START_ANSWERS.submissions,
|
|
220
|
-
welcomeEmail: options.freeTaste ? false : (inputs == null ? void 0 : inputs['welcomeEmail']) !== false
|
|
289
|
+
welcomeEmail: options.freeTaste ? false : (inputs == null ? void 0 : inputs['welcomeEmail']) !== false,
|
|
290
|
+
city: text('city'),
|
|
291
|
+
brand: text('brand')
|
|
292
|
+
}, Object.fromEntries(AI_SITE_DETAIL_KEYS.map((key)=>[
|
|
293
|
+
key,
|
|
294
|
+
text(key)
|
|
295
|
+
])), text(AI_SITE_INSPIRATION_INPUT) ? {
|
|
296
|
+
inspiration: text(AI_SITE_INSPIRATION_INPUT).split(/\s+/).filter(Boolean).slice(0, aiSiteInspirationLimit(options.freeTaste))
|
|
297
|
+
} : {});
|
|
298
|
+
}
|
|
299
|
+
// ── A build staff run for a client (AGL-3958) ───────────────────────────────
|
|
300
|
+
/** Shortens an answer to what a scalar input admits, on a word where it can. */ function bounded(value, max) {
|
|
301
|
+
const trimmed = value.trim().replace(/\s+/g, ' ');
|
|
302
|
+
if (trimmed.length <= max) return trimmed;
|
|
303
|
+
const cut = trimmed.slice(0, max);
|
|
304
|
+
const space = cut.lastIndexOf(' ');
|
|
305
|
+
return (space > max / 2 ? cut.slice(0, space) : cut).trim();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* The questions' answers for a site staff are building for a client: what
|
|
309
|
+
* the business does is the kind of site, and every other answer keeps its
|
|
310
|
+
* default, which the review shows to change. The rest of the intake is not an
|
|
311
|
+
* answer to these questions; it reaches the job through
|
|
312
|
+
* {@link aiSiteStartClientInputs} and {@link aiSiteStartClientBrief}.
|
|
313
|
+
*/ export function aiSiteStartAnswersFromClientBuild(intake) {
|
|
314
|
+
return _extends({}, AI_SITE_START_ANSWERS, {
|
|
315
|
+
siteType: bounded(intake.whatTheyDo, AI_SITE_INPUT_MAX_CHARS)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The per-site inputs the intake fills, which the guided start's own
|
|
320
|
+
* questions leave empty: the business's name for the header and footer, and
|
|
321
|
+
* the towns it serves as its place.
|
|
322
|
+
*/ export function aiSiteStartClientInputs(intake) {
|
|
323
|
+
return {
|
|
324
|
+
businessName: bounded(intake.businessName, AI_SITE_INPUT_MAX_CHARS),
|
|
325
|
+
city: bounded(intake.towns, AI_SITE_INPUT_MAX_CHARS)
|
|
221
326
|
};
|
|
222
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* The intake as sentences added to the brief: how to reach the business and
|
|
330
|
+
* when it is open, which the site should say where a visitor looks for them,
|
|
331
|
+
* and the owner's own words, quoted, which the copy should sound like. Empty
|
|
332
|
+
* answers say nothing.
|
|
333
|
+
*/ export function aiSiteStartClientBrief(intake) {
|
|
334
|
+
const name = intake.businessName.trim();
|
|
335
|
+
const towns = intake.towns.trim();
|
|
336
|
+
const phone = intake.phone.trim();
|
|
337
|
+
const email = intake.email.trim();
|
|
338
|
+
const hours = intake.hours.trim();
|
|
339
|
+
const words = intake.ownerWords.trim().replace(/\s+/g, ' ');
|
|
340
|
+
const sentences = [];
|
|
341
|
+
if (name) sentences.push(`The business is called ${name}.`);
|
|
342
|
+
if (towns) sentences.push(`It serves ${towns}.`);
|
|
343
|
+
const contact = [
|
|
344
|
+
phone ? `call ${phone}` : '',
|
|
345
|
+
email ? `email ${email}` : ''
|
|
346
|
+
].filter(Boolean);
|
|
347
|
+
if (contact.length) {
|
|
348
|
+
sentences.push(`Visitors reach it to ${contact.join(' or ')}; say so wherever the site tells them how to get in touch.`);
|
|
349
|
+
}
|
|
350
|
+
if (hours) sentences.push(`Its hours are ${hours}.`);
|
|
351
|
+
if (words) sentences.push(`In the owner's own words: "${words}" Write the copy so it sounds like them.`);
|
|
352
|
+
return sentences.join(' ');
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* What the review says the client build adds, so staff see the intake is
|
|
356
|
+
* used without it being asked again: "Also from the client's details:
|
|
357
|
+
* Harbor Plumbing, serving Austin, its phone, email and hours, and the
|
|
358
|
+
* owner's own words."
|
|
359
|
+
*/ export function aiSiteStartClientNote(intake) {
|
|
360
|
+
const name = intake.businessName.trim();
|
|
361
|
+
const towns = intake.towns.trim();
|
|
362
|
+
const reach = [
|
|
363
|
+
intake.phone.trim() ? 'phone' : '',
|
|
364
|
+
intake.email.trim() ? 'email' : '',
|
|
365
|
+
intake.hours.trim() ? 'hours' : ''
|
|
366
|
+
].filter(Boolean);
|
|
367
|
+
const parts = [
|
|
368
|
+
name && towns ? `${name}, serving ${towns}` : name || (towns ? `serving ${towns}` : ''),
|
|
369
|
+
reach.length ? `its ${reach.length > 1 ? `${reach.slice(0, -1).join(', ')} and ${reach[reach.length - 1]}` : reach[0]}` : '',
|
|
370
|
+
intake.ownerWords.trim() ? "the owner's own words" : ''
|
|
371
|
+
].filter(Boolean);
|
|
372
|
+
if (!parts.length) return 'Built from the client’s details.';
|
|
373
|
+
const list = parts.length > 1 ? `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}` : parts[0];
|
|
374
|
+
return `Also from the client’s details: ${list}.`;
|
|
375
|
+
}
|
|
223
376
|
|
|
224
377
|
//# sourceMappingURL=ai-site-start.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start.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 { aiSiteKind, aiSiteKindFor, type AiSiteKind } from './ai-site-kinds'\nimport {\n AI_SITE_INPUT_MAX_CHARS,\n AI_SITE_SUBMISSIONS,\n aiSitePagesBand,\n type AiSiteJobInputs,\n type AiSiteSubmissions,\n} from './ai-site-job'\n\n/**\n * The guided start (AGL-2918): the questions a person answers on a site they\n * have just made, and the site job those answers become.\n *\n * A scaffold already exists — the `site` job plans four to eight pages, a\n * layout, a contact form, a palette and a welcome email, and builds every one\n * of them as a draft. What it had no way in through was a brief: somebody had\n * to know what to write, on a site where nothing has happened yet. So this\n * module is the QUESTIONS, and nothing else: what is asked, what an answer may\n * be, and how a set of answers becomes the brief and the scalar inputs the\n * `site` door already takes.\n *\n * ── Answering is never the way out ───────────────────────────────────────\n *\n * There is no answer here that means \"not this\". The way past the questions\n * is the `hostFirstRun` zone's own `startBlank`, which is drawn beside them\n * from the first one and creates nothing at all — see the zone in\n * `CONSOLE_WIDGET_SLOTS`. Putting the exit inside the question set would make\n * leaving a thing you answer your way to, and it is not.\n *\n * ── Every answer but the first is optional ───────────────────────────────\n *\n * Only what a site is FOR has no default worth guessing. The audience narrows\n * the copy, the example steers the shape, the page count and the welcome\n * email are defaults a person may change — so a person who types six words\n * and confirms gets a planned site, and the rest of the questions are there\n * for the person who wants to answer them.\n *\n * Pure: shapes, vocabularies and two string builders. No React, no network,\n * and no import of the starter catalog — {@link AI_SITE_START_EXAMPLES} names\n * the starters rather than reading them, so the console does not carry a\n * catalog of node maps to draw five cards. `ai-site-start.spec.ts` holds the\n * two lists to each other.\n */\n\n/** A worked example a person picks to steer what the site should look like. */\nexport interface AiSiteStartExample {\n /** A `STARTER_TEMPLATES` id, which is a persisted identifier. */\n id: string\n /** The starter as its card names it. */\n label: string\n /** The starter's category, which is how the examples group. */\n category: string\n /** What the example is, in one line a person reads before picking it. */\n blurb: string\n}\n\n/**\n * The examples the start offers, in the order they are shown. The platform's\n * own starter sites: a person who has no words for what they want has seen\n * these five shapes before, and picking one says more than a sentence would.\n */\nexport const AI_SITE_START_EXAMPLES: readonly AiSiteStartExample[] = [\n {\n id: 'business',\n label: 'Business',\n category: 'Business',\n blurb: 'A home page led by what you do, an about page and a contact page',\n },\n {\n id: 'landing',\n label: 'Landing Page',\n category: 'Marketing',\n blurb: 'One page that leads to one action, with the reasons to take it',\n },\n {\n id: 'portfolio',\n label: 'Portfolio',\n category: 'Personal',\n blurb: 'An introduction, a grid of work and a way to get in touch',\n },\n {\n id: 'physical-shop',\n label: 'Shop (physical products)',\n category: 'Commerce',\n blurb: 'Featured products, a shop to browse, and a cart',\n },\n {\n id: 'digital-shop',\n label: 'Shop (digital products)',\n category: 'Commerce',\n blurb: 'Downloads up front, a shop to browse, and a cart',\n },\n]\n\n/**\n * Kinds of site offered beside the first question's box, so answering it can\n * be a tap. They fill the box rather than replacing it: what somebody is\n * building is theirs to say, and a list of eight is a list somebody is not on.\n */\nexport const AI_SITE_START_TYPES: readonly string[] = [\n 'a local service business',\n 'a restaurant or cafe',\n 'a professional practice',\n 'a trades business',\n 'a shop',\n 'a nonprofit',\n 'a personal portfolio',\n 'an event',\n]\n\n/** What a person answered; the shape the card holds and the brief is built from. */\nexport interface AiSiteStartAnswers {\n /** What kind of site this is. The one answer with no default. */\n siteType: string\n /** Who the site is for; empty when they did not say. */\n audience: string\n /** The example they liked, by {@link AiSiteStartExample.id}; `null` for none. */\n example: string | null\n /**\n * The kind of site they picked (AGL-3660), by its `AI_SITE_KINDS` id;\n * `null` until they pick one, when the kind their first answer suggests is\n * the one used ({@link aiSiteStartKind}).\n */\n kind: string | null\n /** How many pages to plan. */\n pages: number\n /** Where the contact form's submissions go. */\n submissions: AiSiteSubmissions\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n}\n\n/**\n * What the questions start on. Five pages because it is the middle of the\n * band the scaffold builds, a welcome email because the scaffold drafts one\n * unless it is told not to, and the Inbox because it is what every submission\n * does anyway — filing a lead as well is the addition, and an addition is the\n * thing a person should have to choose.\n */\nexport const AI_SITE_START_ANSWERS: AiSiteStartAnswers = {\n siteType: '',\n audience: '',\n example: null,\n kind: null,\n pages: 5,\n submissions: 'inbox',\n welcomeEmail: true,\n}\n\n/** The kind of site the answers pick, else the one what the site is and who it is for suggest. */\nexport function aiSiteStartKind(\n answers: Pick<AiSiteStartAnswers, 'kind' | 'siteType'> & Partial<Pick<AiSiteStartAnswers, 'audience'>>,\n): AiSiteKind {\n return aiSiteKind(answers.kind) ?? aiSiteKindFor(answers.siteType, answers.audience ?? '')\n}\n\n/** The example an answer names, or `null` when it names none this list knows. */\nexport function aiSiteStartExample(\n answers: AiSiteStartAnswers,\n): AiSiteStartExample | null {\n if (!answers.example) return null\n return AI_SITE_START_EXAMPLES.find((entry) => entry.id === answers.example) ?? null\n}\n\n/**\n * Why these answers cannot start a site, in a sentence a person reads; `null`\n * when they can. The door's own parser is what actually admits the job — this\n * is what the card says before it asks for one.\n */\nexport function aiSiteStartRefusal(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): string | null {\n // The workspace's own band (AGL-3594): one or two pages on the Free taste.\n const band = aiSitePagesBand(options.freeTaste)\n const siteType = answers.siteType.trim()\n if (!siteType) return 'Say what kind of site this is.'\n if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep what kind of site this is under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {\n return `A site is planned with ${band.min} to ${band.max} pages.`\n }\n if (answers.example !== null && !aiSiteStartExample(answers)) {\n return 'Pick one of the examples, or none of them.'\n }\n if (!(AI_SITE_SUBMISSIONS as readonly string[]).includes(answers.submissions)) {\n return 'Say where the contact form’s submissions go.'\n }\n return null\n}\n\n/**\n * The answers as the brief the `site` job is created with: the plan step reads\n * it as the person's own words, so it says what they said and asks for nothing\n * they did not.\n */\nexport function aiSiteStartBrief(answers: AiSiteStartAnswers): string {\n const siteType = answers.siteType.trim()\n const audience = answers.audience.trim()\n const example = aiSiteStartExample(answers)\n const sentences = [\n `A ${answers.pages}-page website for ${siteType}.`,\n ]\n if (audience) sentences.push(`It is for ${audience}.`)\n if (example) {\n sentences.push(\n `Follow the shape of the ${example.label} starter: ${example.blurb.toLowerCase()}.`,\n )\n }\n // The kind they picked, or the one their answer suggests (AGL-3660).\n sentences.push(`Make it a ${aiSiteStartKind(answers).label.toLowerCase()} site.`)\n // The form on the page a visitor opens to get in touch, never every page's\n // last band (AGL-3899): \"on the page that asks to be contacted\" read as\n // the home's closing band on 16 of the 22 newest production starts.\n sentences.push(\n answers.pages === 1\n ? 'Plan the page around what a visitor comes to do, with a contact form on it.'\n : 'Plan a page for each thing a visitor comes to do, with the contact form on the page a visitor opens to get in touch.',\n )\n if (audience) {\n // Said of the FORM as well as of the copy: who fills a form in is what\n // decides which fields it asks for, and a brief that named the audience\n // once, in a sentence about the pages, left the form to guess.\n sentences.push(`Ask the contact form for what ${audience} would be asked for.`)\n }\n return sentences.join(' ')\n}\n\n/**\n * The answers as the `site` job's scalar inputs. Read by the door's\n * {@link parseAiSiteJobInputs}, and put in front of the model one per line by\n * the plan step, which is how the audience and the example reach the plan\n * without a prompt of their own.\n *\n * The per-site variables an agency batch fills — the business name, the city,\n * the brand — are left empty here: the questions never ask them. The name is\n * the site's own, and the create door fills it from the site\n * (`aiJobAdmittedInputs`, AGL-3596) when the job is started — a job that\n * reached the model with no name had the model invent one for its header and\n * footer.\n */\nexport function aiSiteStartInputs(\n answers: AiSiteStartAnswers,\n): Omit<AiSiteJobInputs, 'batchId'> {\n const example = aiSiteStartExample(answers)\n const kind = aiSiteStartKind(answers)\n return {\n businessType: answers.siteType.trim(),\n audience: answers.audience.trim(),\n starter: example?.id ?? kind.starter,\n siteKind: kind.id,\n pages: answers.pages,\n businessName: '',\n city: '',\n brand: '',\n submissions: answers.submissions,\n welcomeEmail: answers.welcomeEmail,\n }\n}\n\n/** The site inputs a guided start's answers become, by name: what a summary carries back (AGL-3596). */\nexport const AI_SITE_START_INPUT_KEYS = [\n 'businessType',\n 'audience',\n 'starter',\n 'siteKind',\n 'pages',\n 'businessName',\n 'city',\n 'brand',\n 'submissions',\n 'welcomeEmail',\n] as const\n\n/** A site job's scalar inputs, and nothing else its inputs carry; `null` when it has none. */\nexport function aiSiteStartInputsOf(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): Record<string, unknown> | null {\n if (!inputs) return null\n const kept: Record<string, unknown> = {}\n for (const key of AI_SITE_START_INPUT_KEYS) {\n const value = inputs[key]\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') kept[key] = value\n }\n return Object.keys(kept).length ? kept : null\n}\n\n/**\n * A failed start's answers, back as the questions hold them (AGL-3596), so\n * Try again reopens the guided start filled in for the person to adjust:\n * the inverse of {@link aiSiteStartInputs}. Read defensively — an answer the\n * questions no longer offer goes back to its default — and held to the\n * workspace's page band. `null` when the inputs say no kind of site at all.\n */\nexport function aiSiteStartAnswersFromInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n options: { freeTaste?: boolean } = {},\n): AiSiteStartAnswers | null {\n const text = (key: string) => (typeof inputs?.[key] === 'string' ? (inputs[key] as string).trim() : '')\n const siteType = text('businessType')\n if (!siteType) return null\n const band = aiSitePagesBand(options.freeTaste)\n const pages = typeof inputs?.['pages'] === 'number' ? Math.round(inputs['pages'] as number) : AI_SITE_START_ANSWERS.pages\n const starter = text('starter')\n const submissions = text('submissions')\n return {\n siteType,\n audience: text('audience'),\n example: AI_SITE_START_EXAMPLES.some((entry) => entry.id === starter) ? starter : null,\n kind: aiSiteKind(text('siteKind'))?.id ?? null,\n pages: Math.min(band.max, Math.max(band.min, pages)),\n submissions: (AI_SITE_SUBMISSIONS as readonly string[]).includes(submissions)\n ? (submissions as AiSiteSubmissions)\n : AI_SITE_START_ANSWERS.submissions,\n welcomeEmail: options.freeTaste ? false : inputs?.['welcomeEmail'] !== false,\n }\n}\n"],"names":["aiSiteKind","aiSiteKindFor","AI_SITE_INPUT_MAX_CHARS","AI_SITE_SUBMISSIONS","aiSitePagesBand","AI_SITE_START_EXAMPLES","id","label","category","blurb","AI_SITE_START_TYPES","AI_SITE_START_ANSWERS","siteType","audience","example","kind","pages","submissions","welcomeEmail","aiSiteStartKind","answers","aiSiteStartExample","find","entry","aiSiteStartRefusal","options","band","freeTaste","trim","length","Number","isInteger","min","max","includes","aiSiteStartBrief","sentences","push","toLowerCase","join","aiSiteStartInputs","businessType","starter","siteKind","businessName","city","brand","AI_SITE_START_INPUT_KEYS","aiSiteStartInputsOf","inputs","kept","key","value","Object","keys","aiSiteStartAnswersFromInputs","text","Math","round","some"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,EAAEC,aAAa,QAAyB,qBAAiB;AAC5E,SACEC,uBAAuB,EACvBC,mBAAmB,EACnBC,eAAe,QAGV,mBAAe;AAiDtB;;;;CAIC,GACD,OAAO,MAAMC,yBAAwD;IACnE;QACEC,IAAI;QACJC,OAAO;QACPC,UAAU;QACVC,OAAO;IACT;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,UAAU;QACVC,OAAO;IACT;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,UAAU;QACVC,OAAO;IACT;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,UAAU;QACVC,OAAO;IACT;IACA;QACEH,IAAI;QACJC,OAAO;QACPC,UAAU;QACVC,OAAO;IACT;CACD,CAAA;AAED;;;;CAIC,GACD,OAAO,MAAMC,sBAAyC;IACpD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAwBD;;;;;;CAMC,GACD,OAAO,MAAMC,wBAA4C;IACvDC,UAAU;IACVC,UAAU;IACVC,SAAS;IACTC,MAAM;IACNC,OAAO;IACPC,aAAa;IACbC,cAAc;AAChB,EAAC;AAED,gGAAgG,GAChG,OAAO,SAASC,gBACdC,OAAsG;QAE/FpB,aAA4DoB;IAAnE,QAAOpB,cAAAA,WAAWoB,QAAQL,IAAI,aAAvBf,cAA4BC,cAAcmB,QAAQR,QAAQ,GAAEQ,oBAAAA,QAAQP,QAAQ,YAAhBO,oBAAoB;AACzF;AAEA,+EAA+E,GAC/E,OAAO,SAASC,mBACdD,OAA2B;QAGpBf;IADP,IAAI,CAACe,QAAQN,OAAO,EAAE,OAAO;IAC7B,QAAOT,+BAAAA,uBAAuBiB,IAAI,CAAC,CAACC,QAAUA,MAAMjB,EAAE,KAAKc,QAAQN,OAAO,aAAnET,+BAAwE;AACjF;AAEA;;;;CAIC,GACD,OAAO,SAASmB,mBACdJ,OAA2B,EAC3BK,UAAmC,CAAC,CAAC;IAErC,2EAA2E;IAC3E,MAAMC,OAAOtB,gBAAgBqB,QAAQE,SAAS;IAC9C,MAAMf,WAAWQ,QAAQR,QAAQ,CAACgB,IAAI;IACtC,IAAI,CAAChB,UAAU,OAAO;IACtB,IAAIA,SAASiB,MAAM,GAAG3B,yBAAyB;QAC7C,OAAO,CAAC,qCAAqC,EAAEA,wBAAwB,YAAY,CAAC;IACtF;IACA,IAAIkB,QAAQP,QAAQ,CAACe,IAAI,GAAGC,MAAM,GAAG3B,yBAAyB;QAC5D,OAAO,CAAC,+BAA+B,EAAEA,wBAAwB,YAAY,CAAC;IAChF;IACA,IAAI,CAAC4B,OAAOC,SAAS,CAACX,QAAQJ,KAAK,KAAKI,QAAQJ,KAAK,GAAGU,KAAKM,GAAG,IAAIZ,QAAQJ,KAAK,GAAGU,KAAKO,GAAG,EAAE;QAC5F,OAAO,CAAC,uBAAuB,EAAEP,KAAKM,GAAG,CAAC,IAAI,EAAEN,KAAKO,GAAG,CAAC,OAAO,CAAC;IACnE;IACA,IAAIb,QAAQN,OAAO,KAAK,QAAQ,CAACO,mBAAmBD,UAAU;QAC5D,OAAO;IACT;IACA,IAAI,CAAC,AAACjB,oBAA0C+B,QAAQ,CAACd,QAAQH,WAAW,GAAG;QAC7E,OAAO;IACT;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASkB,iBAAiBf,OAA2B;IAC1D,MAAMR,WAAWQ,QAAQR,QAAQ,CAACgB,IAAI;IACtC,MAAMf,WAAWO,QAAQP,QAAQ,CAACe,IAAI;IACtC,MAAMd,UAAUO,mBAAmBD;IACnC,MAAMgB,YAAY;QAChB,CAAC,EAAE,EAAEhB,QAAQJ,KAAK,CAAC,kBAAkB,EAAEJ,SAAS,CAAC,CAAC;KACnD;IACD,IAAIC,UAAUuB,UAAUC,IAAI,CAAC,CAAC,UAAU,EAAExB,SAAS,CAAC,CAAC;IACrD,IAAIC,SAAS;QACXsB,UAAUC,IAAI,CACZ,CAAC,wBAAwB,EAAEvB,QAAQP,KAAK,CAAC,UAAU,EAAEO,QAAQL,KAAK,CAAC6B,WAAW,GAAG,CAAC,CAAC;IAEvF;IACA,qEAAqE;IACrEF,UAAUC,IAAI,CAAC,CAAC,UAAU,EAAElB,gBAAgBC,SAASb,KAAK,CAAC+B,WAAW,GAAG,MAAM,CAAC;IAChF,2EAA2E;IAC3E,wEAAwE;IACxE,oEAAoE;IACpEF,UAAUC,IAAI,CACZjB,QAAQJ,KAAK,KAAK,IACd,gFACA;IAEN,IAAIH,UAAU;QACZ,uEAAuE;QACvE,wEAAwE;QACxE,+DAA+D;QAC/DuB,UAAUC,IAAI,CAAC,CAAC,8BAA8B,EAAExB,SAAS,oBAAoB,CAAC;IAChF;IACA,OAAOuB,UAAUG,IAAI,CAAC;AACxB;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,kBACdpB,OAA2B;;IAE3B,MAAMN,UAAUO,mBAAmBD;IACnC,MAAML,OAAOI,gBAAgBC;IAC7B,OAAO;QACLqB,cAAcrB,QAAQR,QAAQ,CAACgB,IAAI;QACnCf,UAAUO,QAAQP,QAAQ,CAACe,IAAI;QAC/Bc,OAAO,UAAE5B,2BAAAA,QAASR,EAAE,mBAAIS,KAAK2B,OAAO;QACpCC,UAAU5B,KAAKT,EAAE;QACjBU,OAAOI,QAAQJ,KAAK;QACpB4B,cAAc;QACdC,MAAM;QACNC,OAAO;QACP7B,aAAaG,QAAQH,WAAW;QAChCC,cAAcE,QAAQF,YAAY;IACpC;AACF;AAEA,sGAAsG,GACtG,OAAO,MAAM6B,2BAA2B;IACtC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAEV,4FAA4F,GAC5F,OAAO,SAASC,oBACdC,MAA4D;IAE5D,IAAI,CAACA,QAAQ,OAAO;IACpB,MAAMC,OAAgC,CAAC;IACvC,KAAK,MAAMC,OAAOJ,yBAA0B;QAC1C,MAAMK,QAAQH,MAAM,CAACE,IAAI;QACzB,IAAI,OAAOC,UAAU,YAAY,OAAOA,UAAU,YAAY,OAAOA,UAAU,WAAWF,IAAI,CAACC,IAAI,GAAGC;IACxG;IACA,OAAOC,OAAOC,IAAI,CAACJ,MAAMrB,MAAM,GAAGqB,OAAO;AAC3C;AAEA;;;;;;CAMC,GACD,OAAO,SAASK,6BACdN,MAA4D,EAC5DxB,UAAmC,CAAC,CAAC;;QAa7BzB;IAXR,MAAMwD,OAAO,CAACL,MAAiB,QAAOF,0BAAAA,MAAQ,CAACE,IAAI,MAAK,WAAW,AAACF,MAAM,CAACE,IAAI,CAAYvB,IAAI,KAAK;IACpG,MAAMhB,WAAW4C,KAAK;IACtB,IAAI,CAAC5C,UAAU,OAAO;IACtB,MAAMc,OAAOtB,gBAAgBqB,QAAQE,SAAS;IAC9C,MAAMX,QAAQ,QAAOiC,0BAAAA,MAAQ,CAAC,QAAQ,MAAK,WAAWQ,KAAKC,KAAK,CAACT,MAAM,CAAC,QAAQ,IAActC,sBAAsBK,KAAK;IACzH,MAAM0B,UAAUc,KAAK;IACrB,MAAMvC,cAAcuC,KAAK;IACzB,OAAO;QACL5C;QACAC,UAAU2C,KAAK;QACf1C,SAAST,uBAAuBsD,IAAI,CAAC,CAACpC,QAAUA,MAAMjB,EAAE,KAAKoC,WAAWA,UAAU;QAClF3B,IAAI,WAAEf,cAAAA,WAAWwD,KAAK,iCAAhBxD,YAA8BM,EAAE,mBAAI;QAC1CU,OAAOyC,KAAKzB,GAAG,CAACN,KAAKO,GAAG,EAAEwB,KAAKxB,GAAG,CAACP,KAAKM,GAAG,EAAEhB;QAC7CC,aAAa,AAACd,oBAA0C+B,QAAQ,CAACjB,eAC5DA,cACDN,sBAAsBM,WAAW;QACrCC,cAAcO,QAAQE,SAAS,GAAG,QAAQsB,CAAAA,0BAAAA,MAAQ,CAAC,eAAe,MAAK;IACzE;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start.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 type { ClientBuildIntake } from '@aglyn/aglyn/app-utils/client-build'\nimport {\n AI_SITE_DETAIL_KEYS,\n AI_SITE_NO_DETAILS,\n aiSiteDetailProblem,\n type AiSiteDetailKey,\n type AiSiteDetails,\n} from './ai-site-details'\nimport { aiSiteKind, aiSiteKindFor, type AiSiteKind } from './ai-site-kinds'\nimport {\n AI_SITE_INSPIRATION_INPUT,\n aiSiteInspirationInput,\n aiSiteInspirationLimit,\n aiSiteInspirationRefusal,\n aiSiteInspirationUrl,\n} from './ai-site-inspiration'\nimport {\n AI_SITE_INPUT_MAX_CHARS,\n AI_SITE_SUBMISSIONS,\n aiSitePagesBand,\n type AiSiteJobInputs,\n type AiSiteSubmissions,\n} from './ai-site-job'\n\n/**\n * The guided start (AGL-2918): the questions a person answers on a site they\n * have just made, and the site job those answers become.\n *\n * A scaffold already exists — the `site` job plans four to eight pages, a\n * layout, a contact form, a palette and a welcome email, and builds every one\n * of them as a draft. What it had no way in through was a brief: somebody had\n * to know what to write, on a site where nothing has happened yet. So this\n * module is the QUESTIONS, and nothing else: what is asked, what an answer may\n * be, and how a set of answers becomes the brief and the scalar inputs the\n * `site` door already takes.\n *\n * ── Answering is never the way out ───────────────────────────────────────\n *\n * There is no answer here that means \"not this\". The way past the questions\n * is the `hostFirstRun` zone's own `startBlank`, which is drawn beside them\n * from the first one and creates nothing at all — see the zone in\n * `CONSOLE_WIDGET_SLOTS`. Putting the exit inside the question set would make\n * leaving a thing you answer your way to, and it is not.\n *\n * ── Every answer but the first is optional ───────────────────────────────\n *\n * Only what a site is FOR has no default worth guessing. The audience narrows\n * the copy, the example steers the shape, the page count and the welcome\n * email are defaults a person may change — so a person who types six words\n * and confirms gets a planned site, and the rest of the questions are there\n * for the person who wants to answer them.\n *\n * Pure: shapes, vocabularies and two string builders. No React, no network,\n * and no import of the starter catalog — {@link AI_SITE_START_EXAMPLES} names\n * the starters rather than reading them, so the console does not carry a\n * catalog of node maps to draw five cards. `ai-site-start.spec.ts` holds the\n * two lists to each other.\n */\n\n/** A worked example a person picks to steer what the site should look like. */\nexport interface AiSiteStartExample {\n /** A `STARTER_TEMPLATES` id, which is a persisted identifier. */\n id: string\n /** The starter as its card names it. */\n label: string\n /** The starter's category, which is how the examples group. */\n category: string\n /** What the example is, in one line a person reads before picking it. */\n blurb: string\n}\n\n/**\n * The examples the start offers, in the order they are shown. The platform's\n * own starter sites: a person who has no words for what they want has seen\n * these five shapes before, and picking one says more than a sentence would.\n */\nexport const AI_SITE_START_EXAMPLES: readonly AiSiteStartExample[] = [\n {\n id: 'business',\n label: 'Business',\n category: 'Business',\n blurb: 'A home page led by what you do, an about page and a contact page',\n },\n {\n id: 'landing',\n label: 'Landing Page',\n category: 'Marketing',\n blurb: 'One page that leads to one action, with the reasons to take it',\n },\n {\n id: 'portfolio',\n label: 'Portfolio',\n category: 'Personal',\n blurb: 'An introduction, a grid of work and a way to get in touch',\n },\n {\n id: 'physical-shop',\n label: 'Shop (physical products)',\n category: 'Commerce',\n blurb: 'Featured products, a shop to browse, and a cart',\n },\n {\n id: 'digital-shop',\n label: 'Shop (digital products)',\n category: 'Commerce',\n blurb: 'Downloads up front, a shop to browse, and a cart',\n },\n]\n\n/**\n * Kinds of site offered beside the first question's box, so answering it can\n * be a tap. They fill the box rather than replacing it: what somebody is\n * building is theirs to say, and a list of eight is a list somebody is not on.\n */\nexport const AI_SITE_START_TYPES: readonly string[] = [\n 'a local service business',\n 'a restaurant or cafe',\n 'a professional practice',\n 'a trades business',\n 'a shop',\n 'a nonprofit',\n 'a personal portfolio',\n 'an event',\n]\n\n/**\n * What a person answered; the shape the card holds and the brief is built from.\n *\n * Beside the answers each step asks, the business details its \"More details\"\n * offers (AGL-3979, `AiSiteDetails`): every one optional and `''` until given,\n * the phone number and email on the Details step itself.\n */\nexport interface AiSiteStartAnswers extends AiSiteDetails {\n /** What kind of site this is. The one answer with no default. */\n siteType: string\n /** Who the site is for; empty when they did not say. */\n audience: string\n /** The example they liked, by {@link AiSiteStartExample.id}; `null` for none. */\n example: string | null\n /**\n * The kind of site they picked (AGL-3660), by its `AI_SITE_KINDS` id;\n * `null` until they pick one, when the kind their first answer suggests is\n * the one used ({@link aiSiteStartKind}).\n */\n kind: string | null\n /** How many pages to plan. */\n pages: number\n /** Where the contact form's submissions go. */\n submissions: AiSiteSubmissions\n /** Whether the scaffold also drafts a welcome email. */\n welcomeEmail: boolean\n /** The city or town the business is in (AGL-3979); empty when they did not say. */\n city: string\n /** The brand color, as a six-digit hex (AGL-3979); empty for the style's own. */\n brand: string\n /**\n * The sites they like (AGL-3975), as typed, one a field; absent or empty\n * for none. Asked only while `release_ai_site_inspiration` is on.\n */\n inspiration?: string[]\n}\n\n/**\n * What the questions start on. Five pages because it is the middle of the\n * band the scaffold builds, a welcome email because the scaffold drafts one\n * unless it is told not to, and the Inbox because it is what every submission\n * does anyway — filing a lead as well is the addition, and an addition is the\n * thing a person should have to choose.\n */\nexport const AI_SITE_START_ANSWERS: AiSiteStartAnswers = {\n siteType: '',\n audience: '',\n example: null,\n kind: null,\n pages: 5,\n submissions: 'inbox',\n welcomeEmail: true,\n city: '',\n brand: '',\n ...AI_SITE_NO_DETAILS,\n}\n\n/** The answers a person may give beyond the ones each step asks, by key (AGL-3979). */\nexport type AiSiteStartExtraKey = AiSiteDetailKey | 'city' | 'brand'\n\n/** Every extra, in the order the review lists them. */\nexport const AI_SITE_START_EXTRA_KEYS: readonly AiSiteStartExtraKey[] = [\n 'phone',\n 'email',\n 'tagline',\n 'founded',\n 'serviceArea',\n 'priceRange',\n 'languages',\n 'brand',\n 'street',\n 'city',\n 'region',\n 'postalCode',\n 'country',\n 'hours',\n 'social',\n]\n\n/** What is wrong with one extra, worded for beside its field; `null` when it may be used. */\nexport function aiSiteStartExtraProblem(\n key: AiSiteStartExtraKey,\n value: string,\n): { code: 'too_long' | 'invalid'; message: string } | null {\n if (key === 'city' || key === 'brand') {\n return value.trim().length > AI_SITE_INPUT_MAX_CHARS\n ? { code: 'too_long', message: `Keep this under ${AI_SITE_INPUT_MAX_CHARS} characters.` }\n : null\n }\n return aiSiteDetailProblem(key, value)\n}\n\n/** The extras a person gave, in the review's order: what \"More details\" was used for. */\nexport function aiSiteStartExtrasGiven(answers: AiSiteStartAnswers): AiSiteStartExtraKey[] {\n return AI_SITE_START_EXTRA_KEYS.filter((key) => (answers[key] ?? '').trim() !== '')\n}\n\n/** The kind of site the answers pick, else the one what the site is and who it is for suggest. */\nexport function aiSiteStartKind(\n answers: Pick<AiSiteStartAnswers, 'kind' | 'siteType'> & Partial<Pick<AiSiteStartAnswers, 'audience'>>,\n): AiSiteKind {\n return aiSiteKind(answers.kind) ?? aiSiteKindFor(answers.siteType, answers.audience ?? '')\n}\n\n/** The example an answer names, or `null` when it names none this list knows. */\nexport function aiSiteStartExample(\n answers: AiSiteStartAnswers,\n): AiSiteStartExample | null {\n if (!answers.example) return null\n return AI_SITE_START_EXAMPLES.find((entry) => entry.id === answers.example) ?? null\n}\n\n/**\n * Why these answers cannot start a site, in a sentence a person reads; `null`\n * when they can. The door's own parser is what actually admits the job — this\n * is what the card says before it asks for one.\n */\nexport function aiSiteStartRefusal(\n answers: AiSiteStartAnswers,\n options: { freeTaste?: boolean } = {},\n): string | null {\n // The workspace's own band (AGL-3594): one or two pages on the Free taste.\n const band = aiSitePagesBand(options.freeTaste)\n const siteType = answers.siteType.trim()\n if (!siteType) return 'Say what kind of site this is.'\n if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep what kind of site this is under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {\n return `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`\n }\n if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {\n return `A site is planned with ${band.min} to ${band.max} pages.`\n }\n if (answers.example !== null && !aiSiteStartExample(answers)) {\n return 'Pick one of the examples, or none of them.'\n }\n if (!(AI_SITE_SUBMISSIONS as readonly string[]).includes(answers.submissions)) {\n return 'Say where the contact form’s submissions go.'\n }\n for (const key of AI_SITE_START_EXTRA_KEYS) {\n const problem = aiSiteStartExtraProblem(key, answers[key] ?? '')\n if (problem) return problem.message\n }\n return aiSiteStartInspirationRefusal(answers, options)\n}\n\n/** The sites they typed, without the empty fields. */\nexport function aiSiteStartInspirationTyped(answers: Pick<AiSiteStartAnswers, 'inspiration'>): string[] {\n return (answers.inspiration ?? []).map((entry) => entry.trim()).filter(Boolean)\n}\n\n/** Why the sites they like cannot go forward, as the door would refuse them; `null` when they can. */\nexport function aiSiteStartInspirationRefusal(\n answers: Pick<AiSiteStartAnswers, 'inspiration'>,\n options: { freeTaste?: boolean } = {},\n): string | null {\n const typed = aiSiteStartInspirationTyped(answers)\n if (typed.some((entry) => !aiSiteInspirationUrl(entry))) {\n return 'Give each site you like as its https address, like https://example.com.'\n }\n return aiSiteInspirationRefusal(typed, options.freeTaste)\n}\n\n/**\n * The answers as the brief the `site` job is created with: the plan step reads\n * it as the person's own words, so it says what they said and asks for nothing\n * they did not.\n */\nexport function aiSiteStartBrief(answers: AiSiteStartAnswers): string {\n const siteType = answers.siteType.trim()\n const audience = answers.audience.trim()\n const example = aiSiteStartExample(answers)\n const sentences = [\n `A ${answers.pages}-page website for ${siteType}.`,\n ]\n if (audience) sentences.push(`It is for ${audience}.`)\n if (example) {\n sentences.push(\n `Follow the shape of the ${example.label} starter: ${example.blurb.toLowerCase()}.`,\n )\n }\n // The kind they picked, or the one their answer suggests (AGL-3660).\n sentences.push(`Make it a ${aiSiteStartKind(answers).label.toLowerCase()} site.`)\n // The form on the page a visitor opens to get in touch, never every page's\n // last band (AGL-3899): \"on the page that asks to be contacted\" read as\n // the home's closing band on 16 of the 22 newest production starts.\n sentences.push(\n answers.pages === 1\n ? 'Plan the page around what a visitor comes to do, with a contact form on it.'\n : 'Plan a page for each thing a visitor comes to do, with the contact form on the page a visitor opens to get in touch.',\n )\n if (audience) {\n // Said of the FORM as well as of the copy: who fills a form in is what\n // decides which fields it asks for, and a brief that named the audience\n // once, in a sentence about the pages, left the form to guess.\n sentences.push(`Ask the contact form for what ${audience} would be asked for.`)\n }\n return sentences.join(' ')\n}\n\n/**\n * The answers as the `site` job's scalar inputs. Read by the door's\n * {@link parseAiSiteJobInputs}, and put in front of the model one per line by\n * the plan step, which is how the audience and the example reach the plan\n * without a prompt of their own.\n *\n * The per-site variables an agency batch fills — the business name, the city,\n * the brand — are left empty here: the questions never ask them. The name is\n * the site's own, and the create door fills it from the site\n * (`aiJobAdmittedInputs`, AGL-3596) when the job is started — a job that\n * reached the model with no name had the model invent one for its header and\n * footer.\n */\nexport function aiSiteStartInputs(\n answers: AiSiteStartAnswers,\n): Omit<AiSiteJobInputs, 'batchId' | 'inspiration' | AiSiteDetailKey> & Partial<AiSiteDetails> & { inspiration?: string } {\n const example = aiSiteStartExample(answers)\n const kind = aiSiteStartKind(answers)\n // The details are sent only where given (AGL-3979): the plan step lists\n // every scalar input on a line of its own, and a start without them is\n // asked exactly what it was asked before.\n const details: Partial<AiSiteDetails> = {}\n for (const key of AI_SITE_DETAIL_KEYS) {\n const value = (answers[key] ?? '').trim()\n if (value) details[key] = value\n }\n // The sites they like travel only when there are some (AGL-3975), so a\n // start without them asks for exactly what it always did.\n const inspiration = aiSiteInspirationInput(aiSiteStartInspirationTyped(answers))\n return {\n businessType: answers.siteType.trim(),\n audience: answers.audience.trim(),\n starter: example?.id ?? kind.starter,\n siteKind: kind.id,\n pages: answers.pages,\n businessName: '',\n city: (answers.city ?? '').trim(),\n brand: (answers.brand ?? '').trim(),\n submissions: answers.submissions,\n welcomeEmail: answers.welcomeEmail,\n ...details,\n ...(inspiration ? { [AI_SITE_INSPIRATION_INPUT]: inspiration } : {}),\n }\n}\n\n/** The site inputs a guided start's answers become, by name: what a summary carries back (AGL-3596). */\nexport const AI_SITE_START_INPUT_KEYS = [\n 'businessType',\n 'audience',\n 'starter',\n 'siteKind',\n 'pages',\n 'businessName',\n 'city',\n 'brand',\n 'submissions',\n 'welcomeEmail',\n ...AI_SITE_DETAIL_KEYS,\n AI_SITE_INSPIRATION_INPUT,\n] as const\n\n/** A site job's scalar inputs, and nothing else its inputs carry; `null` when it has none. */\nexport function aiSiteStartInputsOf(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): Record<string, unknown> | null {\n if (!inputs) return null\n const kept: Record<string, unknown> = {}\n for (const key of AI_SITE_START_INPUT_KEYS) {\n const value = inputs[key]\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') kept[key] = value\n }\n return Object.keys(kept).length ? kept : null\n}\n\n/**\n * A failed start's answers, back as the questions hold them (AGL-3596), so\n * Try again reopens the guided start filled in for the person to adjust:\n * the inverse of {@link aiSiteStartInputs}. Read defensively — an answer the\n * questions no longer offer goes back to its default — and held to the\n * workspace's page band. `null` when the inputs say no kind of site at all.\n */\nexport function aiSiteStartAnswersFromInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n options: { freeTaste?: boolean } = {},\n): AiSiteStartAnswers | null {\n const text = (key: string) => (typeof inputs?.[key] === 'string' ? (inputs[key] as string).trim() : '')\n const siteType = text('businessType')\n if (!siteType) return null\n const band = aiSitePagesBand(options.freeTaste)\n const pages = typeof inputs?.['pages'] === 'number' ? Math.round(inputs['pages'] as number) : AI_SITE_START_ANSWERS.pages\n const starter = text('starter')\n const submissions = text('submissions')\n return {\n siteType,\n audience: text('audience'),\n example: AI_SITE_START_EXAMPLES.some((entry) => entry.id === starter) ? starter : null,\n kind: aiSiteKind(text('siteKind'))?.id ?? null,\n pages: Math.min(band.max, Math.max(band.min, pages)),\n submissions: (AI_SITE_SUBMISSIONS as readonly string[]).includes(submissions)\n ? (submissions as AiSiteSubmissions)\n : AI_SITE_START_ANSWERS.submissions,\n welcomeEmail: options.freeTaste ? false : inputs?.['welcomeEmail'] !== false,\n city: text('city'),\n brand: text('brand'),\n ...(Object.fromEntries(AI_SITE_DETAIL_KEYS.map((key) => [key, text(key)])) as unknown as AiSiteDetails),\n // The sites they liked come back as typed, within the workspace's limit (AGL-3975).\n ...(text(AI_SITE_INSPIRATION_INPUT)\n ? { inspiration: text(AI_SITE_INSPIRATION_INPUT).split(/\\s+/).filter(Boolean).slice(0, aiSiteInspirationLimit(options.freeTaste)) }\n : {}),\n }\n}\n\n// ── A build staff run for a client (AGL-3958) ───────────────────────────────\n\n/** Shortens an answer to what a scalar input admits, on a word where it can. */\nfunction bounded(value: string, max: number): string {\n const trimmed = value.trim().replace(/\\s+/g, ' ')\n if (trimmed.length <= max) return trimmed\n const cut = trimmed.slice(0, max)\n const space = cut.lastIndexOf(' ')\n return (space > max / 2 ? cut.slice(0, space) : cut).trim()\n}\n\n/**\n * The questions' answers for a site staff are building for a client: what\n * the business does is the kind of site, and every other answer keeps its\n * default, which the review shows to change. The rest of the intake is not an\n * answer to these questions; it reaches the job through\n * {@link aiSiteStartClientInputs} and {@link aiSiteStartClientBrief}.\n */\nexport function aiSiteStartAnswersFromClientBuild(intake: ClientBuildIntake): AiSiteStartAnswers {\n return {\n ...AI_SITE_START_ANSWERS,\n siteType: bounded(intake.whatTheyDo, AI_SITE_INPUT_MAX_CHARS),\n }\n}\n\n/**\n * The per-site inputs the intake fills, which the guided start's own\n * questions leave empty: the business's name for the header and footer, and\n * the towns it serves as its place.\n */\nexport function aiSiteStartClientInputs(\n intake: ClientBuildIntake,\n): Pick<AiSiteJobInputs, 'businessName' | 'city'> {\n return {\n businessName: bounded(intake.businessName, AI_SITE_INPUT_MAX_CHARS),\n city: bounded(intake.towns, AI_SITE_INPUT_MAX_CHARS),\n }\n}\n\n/**\n * The intake as sentences added to the brief: how to reach the business and\n * when it is open, which the site should say where a visitor looks for them,\n * and the owner's own words, quoted, which the copy should sound like. Empty\n * answers say nothing.\n */\nexport function aiSiteStartClientBrief(intake: ClientBuildIntake): string {\n const name = intake.businessName.trim()\n const towns = intake.towns.trim()\n const phone = intake.phone.trim()\n const email = intake.email.trim()\n const hours = intake.hours.trim()\n const words = intake.ownerWords.trim().replace(/\\s+/g, ' ')\n const sentences: string[] = []\n if (name) sentences.push(`The business is called ${name}.`)\n if (towns) sentences.push(`It serves ${towns}.`)\n const contact = [phone ? `call ${phone}` : '', email ? `email ${email}` : ''].filter(Boolean)\n if (contact.length) {\n sentences.push(`Visitors reach it to ${contact.join(' or ')}; say so wherever the site tells them how to get in touch.`)\n }\n if (hours) sentences.push(`Its hours are ${hours}.`)\n if (words) sentences.push(`In the owner's own words: \"${words}\" Write the copy so it sounds like them.`)\n return sentences.join(' ')\n}\n\n/**\n * What the review says the client build adds, so staff see the intake is\n * used without it being asked again: \"Also from the client's details:\n * Harbor Plumbing, serving Austin, its phone, email and hours, and the\n * owner's own words.\"\n */\nexport function aiSiteStartClientNote(intake: ClientBuildIntake): string {\n const name = intake.businessName.trim()\n const towns = intake.towns.trim()\n const reach = [\n intake.phone.trim() ? 'phone' : '',\n intake.email.trim() ? 'email' : '',\n intake.hours.trim() ? 'hours' : '',\n ].filter(Boolean)\n const parts = [\n name && towns ? `${name}, serving ${towns}` : name || (towns ? `serving ${towns}` : ''),\n reach.length ? `its ${reach.length > 1 ? `${reach.slice(0, -1).join(', ')} and ${reach[reach.length - 1]}` : reach[0]}` : '',\n intake.ownerWords.trim() ? \"the owner's own words\" : '',\n ].filter(Boolean)\n if (!parts.length) return 'Built from the client’s details.'\n const list = parts.length > 1 ? `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}` : parts[0]\n return `Also from the client’s details: ${list}.`\n}\n"],"names":["AI_SITE_DETAIL_KEYS","AI_SITE_NO_DETAILS","aiSiteDetailProblem","aiSiteKind","aiSiteKindFor","AI_SITE_INSPIRATION_INPUT","aiSiteInspirationInput","aiSiteInspirationLimit","aiSiteInspirationRefusal","aiSiteInspirationUrl","AI_SITE_INPUT_MAX_CHARS","AI_SITE_SUBMISSIONS","aiSitePagesBand","AI_SITE_START_EXAMPLES","id","label","category","blurb","AI_SITE_START_TYPES","AI_SITE_START_ANSWERS","siteType","audience","example","kind","pages","submissions","welcomeEmail","city","brand","AI_SITE_START_EXTRA_KEYS","aiSiteStartExtraProblem","key","value","trim","length","code","message","aiSiteStartExtrasGiven","answers","filter","aiSiteStartKind","aiSiteStartExample","find","entry","aiSiteStartRefusal","options","band","freeTaste","Number","isInteger","min","max","includes","problem","aiSiteStartInspirationRefusal","aiSiteStartInspirationTyped","inspiration","map","Boolean","typed","some","aiSiteStartBrief","sentences","push","toLowerCase","join","aiSiteStartInputs","details","businessType","starter","siteKind","businessName","AI_SITE_START_INPUT_KEYS","aiSiteStartInputsOf","inputs","kept","Object","keys","aiSiteStartAnswersFromInputs","text","Math","round","fromEntries","split","slice","bounded","trimmed","replace","cut","space","lastIndexOf","aiSiteStartAnswersFromClientBuild","intake","whatTheyDo","aiSiteStartClientInputs","towns","aiSiteStartClientBrief","name","phone","email","hours","words","ownerWords","contact","aiSiteStartClientNote","reach","parts","list"],"mappings":"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"}
|
|
@@ -78,8 +78,11 @@
|
|
|
78
78
|
* The sentence that says where the feature comes from, and who can add it.
|
|
79
79
|
* `feature` is what the entry is called: "Create with AI" unless it is an ask.
|
|
80
80
|
*/ export function aiUpsellOffer(canManageBilling, feature = AI_UPSELL_CREATE_LABEL) {
|
|
81
|
+
// Since AGL-3965 every paid plan's own credits pay for generation, so this
|
|
82
|
+
// door is reached only where it is switched off for the workspace and an
|
|
83
|
+
// Aglyn AI tier (which switches it on with its credits) is for sale.
|
|
81
84
|
const addon = aiAddonName();
|
|
82
|
-
return canManageBilling ? `${feature} comes with
|
|
85
|
+
return canManageBilling ? `${feature} is switched off for this workspace. It comes with an ${addon} ` + 'tier, which you can choose in Billing.' : `${feature} is switched off for this workspace. It comes with an ${addon} ` + 'tier. Ask a workspace owner or admin to choose one in Billing.';
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
//# sourceMappingURL=ai-upsell-copy.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-upsell-copy.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 { aiAddonName } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AiBriefKind } from './ai-brief-copy'\n\n/**\n * What a \"Create with AI\" or \"Ask AI\" entry makes: the brief kinds, an\n * automation, products, a function or variable, an overlay, and an answer\n * about the figures on a page.\n */\nexport type AiUpsellKind =\n | AiBriefKind\n | 'workflow'\n | 'product'\n | 'logic'\n | 'overlay'\n | 'funnel'\n | 'insight'\n\n/** What one entry's dialog says. */\nexport interface AiUpsellCopy {\n title: string\n does: string\n /** The entry's own label, which the button reads and the offer names; \"Create with AI\" when absent. */\n label?: string\n}\n\n/** The label every \"Create with AI\" entry reads. */\nexport const AI_UPSELL_CREATE_LABEL = 'Create with AI'\n\n/** What the dialog says one entry would do here. */\nexport const AI_UPSELL_COPY: Readonly<Record<AiUpsellKind, AiUpsellCopy>> = {\n page: {\n title: 'Create a page with AI',\n does:\n 'Describe the page you need and AI plans it from your brief, using your theme and ' +\n 'what the site already has. You confirm the plan, and the page is built as an ' +\n 'unpublished draft.',\n },\n template: {\n title: 'Create a page template with AI',\n does:\n 'Describe what each page should show and AI plans the template, filling what ' +\n 'changes from page to page from each record. You confirm the plan, and the template ' +\n 'is built as a draft in your library.',\n },\n layout: {\n title: 'Create a layout with AI',\n does:\n 'Describe what the layout should hold and AI plans it, linking your pages and placing ' +\n 'your navigation or menu component when the site has one. You confirm the plan, and ' +\n 'the layout is built as a draft no page uses until you assign it.',\n },\n form: {\n title: 'Create a form with AI',\n does:\n 'Describe what the form is for and AI plans its fields, its marketing consent and ' +\n 'where each submission goes. You confirm the plan, and the form is built as a draft.',\n },\n component: {\n title: 'Create a reusable component with AI',\n does:\n 'Describe what the component should show and AI plans it, making what each page can ' +\n 'change into its properties. You confirm the plan, and the component is built as a ' +\n 'draft.',\n },\n workflow: {\n title: 'Create an automation with AI',\n does:\n 'Describe what should happen, and when, and AI drafts the automation from the ' +\n 'triggers and steps your plan includes. It is saved switched off for you to review.',\n },\n email: {\n title: 'Create an email with AI',\n does:\n 'Describe the email you need and AI plans a design from the email blocks, with three ' +\n 'subject lines and three preheaders to choose between. You confirm the plan, and the ' +\n 'design is built as a draft that is sent to nobody.',\n },\n campaign: {\n title: 'Create a campaign with AI',\n does:\n 'Describe what the campaign is for and AI writes its email design and a draft campaign ' +\n 'that would send it. Nothing is sent or scheduled, and the campaign is aimed at nobody ' +\n 'until you pick its lists.',\n },\n product: {\n title: 'Create products with AI',\n does:\n 'Describe what the store sells and AI proposes up to twelve products. You review them ' +\n 'before any is created, and each is created as a draft at a suggested price that is not on ' +\n 'your storefront until you review it and activate it.',\n },\n logic: {\n title: 'Create a function or variable with AI',\n does:\n 'Describe what it should work out or hold and AI drafts the function or variable. It ' +\n 'opens unsaved in the editor for you to check and save.',\n },\n overlay: {\n title: 'Create an overlay with AI',\n does:\n 'Describe the announcement bar or popup you want and AI writes its copy within the ' +\n 'overlay’s limits. It is saved switched off, for you to check and turn on.',\n },\n funnel: {\n title: 'Create a funnel with AI',\n does:\n 'Describe the journey you want to measure and AI proposes its steps from the site’s ' +\n 'real pages, forms and products. It opens in the funnel editor for you to check and save.',\n },\n insight: {\n title: 'Ask AI about these numbers',\n label: 'Ask a question',\n does:\n 'Ask a question about the figures on this page and AI answers from them, every answer ' +\n 'traced to the figures it is built from. It reads; it changes nothing.',\n },\n}\n\n/** The label an entry's button reads. */\nexport function aiUpsellLabel(kind: AiUpsellKind): string {\n return AI_UPSELL_COPY[kind].label ?? AI_UPSELL_CREATE_LABEL\n}\n\n/**\n * The sentence that says where the feature comes from, and who can add it.\n * `feature` is what the entry is called: \"Create with AI\" unless it is an ask.\n */\nexport function aiUpsellOffer(canManageBilling: boolean, feature = AI_UPSELL_CREATE_LABEL): string {\n const addon = aiAddonName()\n return canManageBilling\n ? `${feature} comes with
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-upsell-copy.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 { aiAddonName } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AiBriefKind } from './ai-brief-copy'\n\n/**\n * What a \"Create with AI\" or \"Ask AI\" entry makes: the brief kinds, an\n * automation, products, a function or variable, an overlay, and an answer\n * about the figures on a page.\n */\nexport type AiUpsellKind =\n | AiBriefKind\n | 'workflow'\n | 'product'\n | 'logic'\n | 'overlay'\n | 'funnel'\n | 'insight'\n\n/** What one entry's dialog says. */\nexport interface AiUpsellCopy {\n title: string\n does: string\n /** The entry's own label, which the button reads and the offer names; \"Create with AI\" when absent. */\n label?: string\n}\n\n/** The label every \"Create with AI\" entry reads. */\nexport const AI_UPSELL_CREATE_LABEL = 'Create with AI'\n\n/** What the dialog says one entry would do here. */\nexport const AI_UPSELL_COPY: Readonly<Record<AiUpsellKind, AiUpsellCopy>> = {\n page: {\n title: 'Create a page with AI',\n does:\n 'Describe the page you need and AI plans it from your brief, using your theme and ' +\n 'what the site already has. You confirm the plan, and the page is built as an ' +\n 'unpublished draft.',\n },\n template: {\n title: 'Create a page template with AI',\n does:\n 'Describe what each page should show and AI plans the template, filling what ' +\n 'changes from page to page from each record. You confirm the plan, and the template ' +\n 'is built as a draft in your library.',\n },\n layout: {\n title: 'Create a layout with AI',\n does:\n 'Describe what the layout should hold and AI plans it, linking your pages and placing ' +\n 'your navigation or menu component when the site has one. You confirm the plan, and ' +\n 'the layout is built as a draft no page uses until you assign it.',\n },\n form: {\n title: 'Create a form with AI',\n does:\n 'Describe what the form is for and AI plans its fields, its marketing consent and ' +\n 'where each submission goes. You confirm the plan, and the form is built as a draft.',\n },\n component: {\n title: 'Create a reusable component with AI',\n does:\n 'Describe what the component should show and AI plans it, making what each page can ' +\n 'change into its properties. You confirm the plan, and the component is built as a ' +\n 'draft.',\n },\n workflow: {\n title: 'Create an automation with AI',\n does:\n 'Describe what should happen, and when, and AI drafts the automation from the ' +\n 'triggers and steps your plan includes. It is saved switched off for you to review.',\n },\n email: {\n title: 'Create an email with AI',\n does:\n 'Describe the email you need and AI plans a design from the email blocks, with three ' +\n 'subject lines and three preheaders to choose between. You confirm the plan, and the ' +\n 'design is built as a draft that is sent to nobody.',\n },\n campaign: {\n title: 'Create a campaign with AI',\n does:\n 'Describe what the campaign is for and AI writes its email design and a draft campaign ' +\n 'that would send it. Nothing is sent or scheduled, and the campaign is aimed at nobody ' +\n 'until you pick its lists.',\n },\n product: {\n title: 'Create products with AI',\n does:\n 'Describe what the store sells and AI proposes up to twelve products. You review them ' +\n 'before any is created, and each is created as a draft at a suggested price that is not on ' +\n 'your storefront until you review it and activate it.',\n },\n logic: {\n title: 'Create a function or variable with AI',\n does:\n 'Describe what it should work out or hold and AI drafts the function or variable. It ' +\n 'opens unsaved in the editor for you to check and save.',\n },\n overlay: {\n title: 'Create an overlay with AI',\n does:\n 'Describe the announcement bar or popup you want and AI writes its copy within the ' +\n 'overlay’s limits. It is saved switched off, for you to check and turn on.',\n },\n funnel: {\n title: 'Create a funnel with AI',\n does:\n 'Describe the journey you want to measure and AI proposes its steps from the site’s ' +\n 'real pages, forms and products. It opens in the funnel editor for you to check and save.',\n },\n insight: {\n title: 'Ask AI about these numbers',\n label: 'Ask a question',\n does:\n 'Ask a question about the figures on this page and AI answers from them, every answer ' +\n 'traced to the figures it is built from. It reads; it changes nothing.',\n },\n}\n\n/** The label an entry's button reads. */\nexport function aiUpsellLabel(kind: AiUpsellKind): string {\n return AI_UPSELL_COPY[kind].label ?? AI_UPSELL_CREATE_LABEL\n}\n\n/**\n * The sentence that says where the feature comes from, and who can add it.\n * `feature` is what the entry is called: \"Create with AI\" unless it is an ask.\n */\nexport function aiUpsellOffer(canManageBilling: boolean, feature = AI_UPSELL_CREATE_LABEL): string {\n // Since AGL-3965 every paid plan's own credits pay for generation, so this\n // door is reached only where it is switched off for the workspace and an\n // Aglyn AI tier (which switches it on with its credits) is for sale.\n const addon = aiAddonName()\n return canManageBilling\n ? `${feature} is switched off for this workspace. It comes with an ${addon} ` +\n 'tier, which you can choose in Billing.'\n : `${feature} is switched off for this workspace. It comes with an ${addon} ` +\n 'tier. Ask a workspace owner or admin to choose one in Billing.'\n}\n"],"names":["aiAddonName","AI_UPSELL_CREATE_LABEL","AI_UPSELL_COPY","page","title","does","template","layout","form","component","workflow","email","campaign","product","logic","overlay","funnel","insight","label","aiUpsellLabel","kind","aiUpsellOffer","canManageBilling","feature","addon"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,WAAW,QAAQ,2CAA0C;AAyBtE,kDAAkD,GAClD,OAAO,MAAMC,yBAAyB,iBAAgB;AAEtD,kDAAkD,GAClD,OAAO,MAAMC,iBAA+D;IAC1EC,MAAM;QACJC,OAAO;QACPC,MACE,sFACA,kFACA;IACJ;IACAC,UAAU;QACRF,OAAO;QACPC,MACE,iFACA,wFACA;IACJ;IACAE,QAAQ;QACNH,OAAO;QACPC,MACE,0FACA,wFACA;IACJ;IACAG,MAAM;QACJJ,OAAO;QACPC,MACE,sFACA;IACJ;IACAI,WAAW;QACTL,OAAO;QACPC,MACE,wFACA,uFACA;IACJ;IACAK,UAAU;QACRN,OAAO;QACPC,MACE,kFACA;IACJ;IACAM,OAAO;QACLP,OAAO;QACPC,MACE,yFACA,yFACA;IACJ;IACAO,UAAU;QACRR,OAAO;QACPC,MACE,2FACA,2FACA;IACJ;IACAQ,SAAS;QACPT,OAAO;QACPC,MACE,0FACA,+FACA;IACJ;IACAS,OAAO;QACLV,OAAO;QACPC,MACE,yFACA;IACJ;IACAU,SAAS;QACPX,OAAO;QACPC,MACE,uFACA;IACJ;IACAW,QAAQ;QACNZ,OAAO;QACPC,MACE,wFACA;IACJ;IACAY,SAAS;QACPb,OAAO;QACPc,OAAO;QACPb,MACE,0FACA;IACJ;AACF,EAAC;AAED,uCAAuC,GACvC,OAAO,SAASc,cAAcC,IAAkB;QACvClB;IAAP,QAAOA,6BAAAA,cAAc,CAACkB,KAAK,CAACF,KAAK,YAA1BhB,6BAA8BD;AACvC;AAEA;;;CAGC,GACD,OAAO,SAASoB,cAAcC,gBAAyB,EAAEC,UAAUtB,sBAAsB;IACvF,2EAA2E;IAC3E,yEAAyE;IACzE,qEAAqE;IACrE,MAAMuB,QAAQxB;IACd,OAAOsB,mBACH,GAAGC,QAAQ,sDAAsD,EAAEC,MAAM,CAAC,CAAC,GACzE,2CACF,GAAGD,QAAQ,sDAAsD,EAAEC,MAAM,CAAC,CAAC,GACzE;AACR"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type NativeNavContribution } from '@aglyn/aglyn/app-utils/native-nav';
|
|
18
|
+
/**
|
|
19
|
+
* AI jobs and AI credits in the native apps' navigation (AGL-3943): the rows this plugin
|
|
20
|
+
* adds to the manifest the shells draw, placed into the core's lists by
|
|
21
|
+
* `order` (`assembleNativeNav`, `generate:native-contracts`). Labels are the
|
|
22
|
+
* console's words, held by `apps/console/specs/native-nav-manifest.spec.ts`.
|
|
23
|
+
*
|
|
24
|
+
* Imports only the core's plain nav module, so the native contracts read it.
|
|
25
|
+
*/
|
|
26
|
+
export declare const NATIVE_NAV_ROWS: readonly NativeNavContribution[];
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
*/ import { nativeNavIcon, nativeNavRef } from "@aglyn/aglyn/app-utils/native-nav";
|
|
17
|
+
/**
|
|
18
|
+
* AI jobs and AI credits in the native apps' navigation (AGL-3943): the rows this plugin
|
|
19
|
+
* adds to the manifest the shells draw, placed into the core's lists by
|
|
20
|
+
* `order` (`assembleNativeNav`, `generate:native-contracts`). Labels are the
|
|
21
|
+
* console's words, held by `apps/console/specs/native-nav-manifest.spec.ts`.
|
|
22
|
+
*
|
|
23
|
+
* Imports only the core's plain nav module, so the native contracts read it.
|
|
24
|
+
*/ export const NATIVE_NAV_ROWS = [
|
|
25
|
+
{
|
|
26
|
+
list: 'site',
|
|
27
|
+
group: 'more',
|
|
28
|
+
row: {
|
|
29
|
+
id: 'aiJobs',
|
|
30
|
+
label: 'AI jobs',
|
|
31
|
+
icon: nativeNavIcon('sparkles', 'auto_awesome'),
|
|
32
|
+
screen: 'ai.jobs',
|
|
33
|
+
pluginId: 'ai',
|
|
34
|
+
order: 50,
|
|
35
|
+
// The console lists a site's AI jobs at `/ai-jobs` with no tab: its
|
|
36
|
+
// top bar's AI jobs indicator is the way in.
|
|
37
|
+
console: nativeNavRef('consoleWidget', 'consoleTopBar/ai-jobs-indicator')
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
list: 'workspaceSettings',
|
|
42
|
+
group: 'billing',
|
|
43
|
+
row: {
|
|
44
|
+
id: 'aiCredits',
|
|
45
|
+
label: 'AI credits',
|
|
46
|
+
icon: nativeNavIcon('sparkles', 'auto_awesome'),
|
|
47
|
+
screen: 'ai.credits',
|
|
48
|
+
pluginId: 'ai',
|
|
49
|
+
order: 30,
|
|
50
|
+
keywords: [
|
|
51
|
+
'billing'
|
|
52
|
+
],
|
|
53
|
+
console: nativeNavRef('consoleWidget', 'orgBillingUsage/ai-credits-meter')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=native-nav.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/native-nav.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n nativeNavIcon,\n nativeNavRef,\n type NativeNavContribution,\n} from '@aglyn/aglyn/app-utils/native-nav'\n\n/**\n * AI jobs and AI credits in the native apps' navigation (AGL-3943): the rows this plugin\n * adds to the manifest the shells draw, placed into the core's lists by\n * `order` (`assembleNativeNav`, `generate:native-contracts`). Labels are the\n * console's words, held by `apps/console/specs/native-nav-manifest.spec.ts`.\n *\n * Imports only the core's plain nav module, so the native contracts read it.\n */\n\nexport const NATIVE_NAV_ROWS: readonly NativeNavContribution[] = [\n {\n list: 'site',\n group: 'more',\n row: {\n id: 'aiJobs',\n label: 'AI jobs',\n icon: nativeNavIcon('sparkles', 'auto_awesome'),\n screen: 'ai.jobs',\n pluginId: 'ai',\n order: 50,\n // The console lists a site's AI jobs at `/ai-jobs` with no tab: its\n // top bar's AI jobs indicator is the way in.\n console: nativeNavRef('consoleWidget', 'consoleTopBar/ai-jobs-indicator'),\n },\n },\n {\n list: 'workspaceSettings',\n group: 'billing',\n row: {\n id: 'aiCredits',\n label: 'AI credits',\n icon: nativeNavIcon('sparkles', 'auto_awesome'),\n screen: 'ai.credits',\n pluginId: 'ai',\n order: 30,\n keywords: ['billing'],\n console: nativeNavRef('consoleWidget', 'orgBillingUsage/ai-credits-meter'),\n },\n },\n]\n"],"names":["nativeNavIcon","nativeNavRef","NATIVE_NAV_ROWS","list","group","row","id","label","icon","screen","pluginId","order","console","keywords"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,aAAa,EACbC,YAAY,QAEP,oCAAmC;AAE1C;;;;;;;CAOC,GAED,OAAO,MAAMC,kBAAoD;IAC/D;QACEC,MAAM;QACNC,OAAO;QACPC,KAAK;YACHC,IAAI;YACJC,OAAO;YACPC,MAAMR,cAAc,YAAY;YAChCS,QAAQ;YACRC,UAAU;YACVC,OAAO;YACP,oEAAoE;YACpE,6CAA6C;YAC7CC,SAASX,aAAa,iBAAiB;QACzC;IACF;IACA;QACEE,MAAM;QACNC,OAAO;QACPC,KAAK;YACHC,IAAI;YACJC,OAAO;YACPC,MAAMR,cAAc,YAAY;YAChCS,QAAQ;YACRC,UAAU;YACVC,OAAO;YACPE,UAAU;gBAAC;aAAU;YACrBD,SAASX,aAAa,iBAAiB;QACzC;IACF;CACD,CAAA"}
|
|
@@ -61,9 +61,10 @@ declare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {
|
|
|
61
61
|
* `tier-margin-floor.spec.ts` is the model, and it pins both the rule and
|
|
62
62
|
* the resulting figures.
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
* ONE POOL FOR EVERYTHING (AGL-3965): this band pays for every Aglyn AI
|
|
65
|
+
* action, generation included, on every plan. An Aglyn AI tier adds its
|
|
66
|
+
* credits (`AI_TIERS[tier].credits`) to it in `resolveOrgEntitlements`
|
|
67
|
+
* — one pool, widened, rather than a second meter.
|
|
67
68
|
*/
|
|
68
69
|
assistCreditsPerMonth?: number;
|
|
69
70
|
}
|
|
@@ -124,18 +124,13 @@
|
|
|
124
124
|
// plan bands at zero any more" was true of the number and false of the
|
|
125
125
|
// capability.
|
|
126
126
|
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// instead would hand the FIRST paid rung a door Pro through Agency have to
|
|
131
|
-
// buy.
|
|
127
|
+
// Since AGL-3965 it opens `aiGenerative` too, as every paid rung does: the
|
|
128
|
+
// included band pays for generation and assist alike (one pool), and an
|
|
129
|
+
// Aglyn AI tier adds credits on top rather than opening a door.
|
|
132
130
|
//
|
|
133
131
|
// No cost moves: `tier-margin-floor.spec.ts` already prices this tier's
|
|
134
132
|
// assist term at 100% burn of the 750 credits, 75¢/mo. A door does not
|
|
135
133
|
// widen a band; it only makes spend that was already modeled reachable.
|
|
136
|
-
//
|
|
137
|
-
// The AI add-on adds `AI_ADDON_CREDITS_PER_MONTH.starter` on top and
|
|
138
|
-
// switches `aiGenerative` on as well.
|
|
139
134
|
starter: 750,
|
|
140
135
|
// $2.75 of provider spend, against the $8.35 the tier's other seven cost
|
|
141
136
|
// terms leave out of $56. See the key's docblock for why the remainder and
|