@aglyn/plugins-ai 1.0.0-beta.226 → 1.0.0-beta.228
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/README.md +3 -2
- package/package.json +17 -17
- package/src/lib/billing/ai-month-meter.js +5 -3
- package/src/lib/billing/ai-month-meter.js.map +1 -1
- package/src/lib/billing/ai-overage-ledger.js +3 -1
- package/src/lib/billing/ai-overage-ledger.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
- package/src/lib/components/ai-brief-dialog.component.js +36 -18
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +24 -4
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-button.component.js +6 -1
- package/src/lib/components/ai-describe-button.component.js.map +1 -1
- package/src/lib/components/ai-describe-page.component.d.ts +1 -1
- package/src/lib/components/ai-describe-page.component.js +1 -1
- package/src/lib/components/ai-describe-page.component.js.map +1 -1
- package/src/lib/components/ai-insight-dialog.component.js +24 -2
- package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-events.d.ts +20 -0
- package/src/lib/components/ai-job-events.js +78 -0
- package/src/lib/components/ai-job-events.js.map +1 -0
- package/src/lib/components/ai-job-follow.component.d.ts +49 -0
- package/src/lib/components/ai-job-follow.component.js +210 -0
- package/src/lib/components/ai-job-follow.component.js.map +1 -0
- package/src/lib/components/ai-job-links.d.ts +42 -0
- package/src/lib/components/ai-job-links.js +140 -0
- package/src/lib/components/ai-job-links.js.map +1 -0
- package/src/lib/components/ai-job-plan.component.d.ts +13 -6
- package/src/lib/components/ai-job-plan.component.js +29 -3
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +24 -0
- package/src/lib/components/ai-job-requests.js +76 -0
- package/src/lib/components/ai-job-requests.js.map +1 -0
- package/src/lib/components/ai-job-site.d.ts +22 -0
- package/src/lib/components/ai-job-site.js +73 -0
- package/src/lib/components/ai-job-site.js.map +1 -0
- package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
- package/src/lib/components/ai-jobs-indicator.component.js +80 -0
- package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
- package/src/lib/components/ai-jobs-list-page.component.d.ts +23 -0
- package/src/lib/components/ai-jobs-list-page.component.js +303 -0
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -0
- package/src/lib/components/ai-jobs-page.component.d.ts +7 -0
- package/src/lib/components/ai-jobs-page.component.js +30 -0
- package/src/lib/components/ai-jobs-page.component.js.map +1 -0
- package/src/lib/components/ai-jobs-store.d.ts +81 -0
- package/src/lib/components/ai-jobs-store.js +218 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -0
- package/src/lib/components/ai-page-brief-dialog.component.d.ts +4 -4
- package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.d.ts +51 -0
- package/src/lib/components/ai-site-build-page.component.js +495 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.d.ts +9 -1
- package/src/lib/components/ai-site-start-card.component.js +533 -132
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
- package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
- package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
- package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
- package/src/lib/components/assist-jobs-drawer.component.js +152 -172
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +69 -8
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
- package/src/lib/components/staff-ai-credit-return.component.js +346 -0
- package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
- package/src/lib/components/staff-org-ai-card.component.js +364 -279
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
- package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
- package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.js +5 -18
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
- package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
- package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
- package/src/lib/jobs/ai-job-draft-ids.d.ts +6 -0
- package/src/lib/jobs/ai-job-draft-ids.js +9 -0
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +7 -0
- package/src/lib/jobs/ai-job-drafts.js +16 -7
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +2 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +6 -5
- package/src/lib/jobs/ai-job-page-sections.js +3 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +19 -1
- package/src/lib/jobs/ai-job-page-step.js +50 -8
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
- package/src/lib/jobs/ai-job-plan-step.js +167 -18
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +28 -0
- package/src/lib/jobs/ai-job-site-step.js +130 -21
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +14 -1
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
- package/src/lib/jobs/ai-jobs-notify.js +51 -0
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
- package/src/lib/jobs/ai-jobs.d.ts +39 -4
- package/src/lib/jobs/ai-jobs.js +205 -34
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +98 -0
- package/src/lib/jobs/ai-site-publish.js +372 -0
- package/src/lib/jobs/ai-site-publish.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-page-briefs.d.ts +7 -6
- package/src/lib/jobs/fixtures/ai-page-briefs.js +23 -56
- package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +112 -0
- package/src/lib/model/ai-job-activity.js +144 -0
- package/src/lib/model/ai-job-activity.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
- package/src/lib/model/ai-job-failure-copy.js +78 -0
- package/src/lib/model/ai-job-failure-copy.js.map +1 -0
- package/src/lib/model/ai-job-notice.d.ts +63 -0
- package/src/lib/model/ai-job-notice.js +129 -0
- package/src/lib/model/ai-job-notice.js.map +1 -0
- package/src/lib/model/ai-jobs.types.d.ts +53 -0
- package/src/lib/model/ai-jobs.types.js +1 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +32 -10
- package/src/lib/model/ai-plan-capabilities.js +15 -2
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +61 -0
- package/src/lib/model/ai-site-build-progress.js +136 -0
- package/src/lib/model/ai-site-build-progress.js.map +1 -0
- package/src/lib/model/ai-site-inventory.d.ts +9 -0
- package/src/lib/model/ai-site-inventory.js +7 -2
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +93 -2
- package/src/lib/model/ai-site-job.js +88 -7
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +19 -0
- package/src/lib/model/ai-site-start-seo.js +91 -3
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +3 -1
- package/src/lib/model/ai-site-start.js +6 -4
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +3 -3
- package/src/lib/plan-entitlements.js +3 -3
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +38 -2
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
- package/src/lib/runtime/ai-abuse-guards.js +10 -7
- package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +16 -12
- package/src/lib/runtime/ai-doctrine-validators.js +50 -40
- 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-eval-live.d.ts +1 -1
- package/src/lib/runtime/ai-eval-live.js +1 -1
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-gate.d.ts +3 -2
- package/src/lib/runtime/ai-gate.js +3 -2
- package/src/lib/runtime/ai-gate.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +16 -1
- package/src/lib/runtime/site-inventory.js +43 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-admin-credits.d.ts +21 -0
- package/src/lib/server/ai-admin-credits.js +213 -0
- package/src/lib/server/ai-admin-credits.js.map +1 -0
- package/src/lib/server/ai-admin-org.js +12 -5
- package/src/lib/server/ai-admin-org.js.map +1 -1
- package/src/lib/server/ai-admin-signals.js +3 -1
- package/src/lib/server/ai-admin-signals.js.map +1 -1
- package/src/lib/server/ai-admin-user.js +12 -1
- package/src/lib/server/ai-admin-user.js.map +1 -1
- package/src/lib/server/ai-allotments.js +3 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-jobs-live-sites.d.ts +26 -0
- package/src/lib/server/ai-jobs-live-sites.js +58 -0
- package/src/lib/server/ai-jobs-live-sites.js.map +1 -0
- package/src/lib/server/ai-jobs-route.js +42 -5
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-chat.js +3 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/billing-credits.js +4 -3
- package/src/lib/server/billing-credits.js.map +1 -1
- package/src/lib/server/billing-overage.js +3 -2
- package/src/lib/server/billing-overage.js.map +1 -1
- package/src/lib/server.js +11 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
- package/src/lib/usage/assist-credit-returns-write.js +150 -0
- package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
- package/src/lib/usage/assist-credit-returns.d.ts +68 -0
- package/src/lib/usage/assist-credit-returns.js +79 -0
- package/src/lib/usage/assist-credit-returns.js.map +1 -0
- package/src/lib/usage/assist-free-taste.js +6 -3
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage/assist-job-refund.d.ts +100 -0
- package/src/lib/usage/assist-job-refund.js +109 -0
- package/src/lib/usage/assist-job-refund.js.map +1 -0
- package/src/lib/usage/assist-usage.js +5 -2
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/staff-org-ai.d.ts +31 -1
- package/src/lib/usage/staff-org-ai.js +19 -2
- package/src/lib/usage/staff-org-ai.js.map +1 -1
|
@@ -63,12 +63,93 @@ import { aiSiteWords } from "./ai-site-job.js";
|
|
|
63
63
|
/** The kind an `seo` output carries when it is a guided start's site listing. */ export const AI_SITE_SEO_PROPOSAL_KIND = 'site-listing';
|
|
64
64
|
/** The id the scaffold reports its one site listing under. */ export const AI_SITE_SEO_OUTPUT_ID = 'site:listing';
|
|
65
65
|
/** What the proposal says about itself wherever it is shown. */ export const AI_SITE_SEO_NOTE = 'Written from what you said the site is and who it is for, not from its pages — read it before you save it.';
|
|
66
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Words a phrase never ends on (AGL-3596): a title cut after "for" or "that"
|
|
68
|
+
* reads as broken mid-sentence, which a search result shows to everybody.
|
|
69
|
+
*/ const DANGLING = new Set([
|
|
70
|
+
'a',
|
|
71
|
+
'an',
|
|
72
|
+
'and',
|
|
73
|
+
'as',
|
|
74
|
+
'at',
|
|
75
|
+
'by',
|
|
76
|
+
'for',
|
|
77
|
+
'from',
|
|
78
|
+
'in',
|
|
79
|
+
'into',
|
|
80
|
+
'of',
|
|
81
|
+
'on',
|
|
82
|
+
'or',
|
|
83
|
+
'so',
|
|
84
|
+
'that',
|
|
85
|
+
'the',
|
|
86
|
+
'to',
|
|
87
|
+
'which',
|
|
88
|
+
'who',
|
|
89
|
+
'with',
|
|
90
|
+
'where',
|
|
91
|
+
'whose',
|
|
92
|
+
'while',
|
|
93
|
+
'your',
|
|
94
|
+
'our',
|
|
95
|
+
'—',
|
|
96
|
+
'-',
|
|
97
|
+
'–',
|
|
98
|
+
'&'
|
|
99
|
+
]);
|
|
100
|
+
/** Without the words a phrase never ends on, or the punctuation before them. */ function withoutDangling(text) {
|
|
101
|
+
const words = text.split(' ');
|
|
102
|
+
while(words.length > 1 && DANGLING.has(words[words.length - 1].toLowerCase()))words.pop();
|
|
103
|
+
return words.join(' ').replace(/[\s,;:—–-]+$/, '').trim();
|
|
104
|
+
}
|
|
105
|
+
/** Held to a length, cut at a word rather than mid-word, and never on a dangling word. */ function clip(value, max) {
|
|
67
106
|
const text = value.replace(/\s+/g, ' ').trim();
|
|
68
107
|
if (text.length <= max) return text;
|
|
69
108
|
const cut = text.slice(0, max + 1);
|
|
70
109
|
const space = cut.lastIndexOf(' ');
|
|
71
|
-
return (space > max / 2 ? cut.slice(0, space) : text.slice(0, max)).trim();
|
|
110
|
+
return withoutDangling((space > max / 2 ? cut.slice(0, space) : text.slice(0, max)).trim());
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Where the first clause of an answer ends: "a dog groomer in Austin THAT
|
|
114
|
+
* takes bookings for…" is a subject and then a sentence about it. A title
|
|
115
|
+
* keeps the subject.
|
|
116
|
+
*/ const CLAUSE_BOUNDARY = /\s+(?:that|which|who|whose|where|offering|providing|specializing|serving|helping|so|because|with|for|to|by|and|but)\b|[,;:(—–]|\s-\s/i;
|
|
117
|
+
/**
|
|
118
|
+
* A short search title (AGL-3596): the business's name, what it is and where,
|
|
119
|
+
* held to the title cap and never cut mid-phrase. The answer to "what kind of
|
|
120
|
+
* site" is often a whole sentence, and cutting that sentence at sixty
|
|
121
|
+
* characters published "…that takes bookings for" as a site's title.
|
|
122
|
+
*
|
|
123
|
+
* The longest of these that fits wins: the whole answer; its first clause
|
|
124
|
+
* (the subject before "that", "for", a comma…); that clause without its
|
|
125
|
+
* place. The city the person gave is added where the words do not already
|
|
126
|
+
* say it, and the name leads where there is one. Only when even the shortest
|
|
127
|
+
* does not fit is it cut, at a word, never on a dangling one.
|
|
128
|
+
*/ export function aiSiteSeoTitle(input) {
|
|
129
|
+
var _input_name, _input_city;
|
|
130
|
+
const max = AI_SITE_SEO_LIMITS.title;
|
|
131
|
+
const name = ((_input_name = input.name) != null ? _input_name : '').replace(/\s+/g, ' ').trim();
|
|
132
|
+
const subject = withoutDangling(input.subject.replace(/\s+/g, ' ').trim());
|
|
133
|
+
const city = ((_input_city = input.city) != null ? _input_city : '').replace(/\s+/g, ' ').trim();
|
|
134
|
+
const placed = (what)=>city && !what.toLowerCase().includes(city.toLowerCase()) ? `${what} in ${city}` : what;
|
|
135
|
+
const boundary = subject.search(CLAUSE_BOUNDARY);
|
|
136
|
+
const clause = withoutDangling(boundary > 0 ? subject.slice(0, boundary) : subject);
|
|
137
|
+
const bare = withoutDangling(clause.replace(/\s+in\s+[^]*$/i, '')) || clause;
|
|
138
|
+
const whats = [
|
|
139
|
+
...new Set([
|
|
140
|
+
placed(subject),
|
|
141
|
+
subject,
|
|
142
|
+
placed(clause),
|
|
143
|
+
clause,
|
|
144
|
+
bare
|
|
145
|
+
])
|
|
146
|
+
];
|
|
147
|
+
const candidates = name ? [
|
|
148
|
+
...whats.map((what)=>`${name} — ${what}`),
|
|
149
|
+
name
|
|
150
|
+
] : whats;
|
|
151
|
+
const fits = candidates.find((candidate)=>candidate.length <= max);
|
|
152
|
+
return fits != null ? fits : clip(candidates[candidates.length - 1], max);
|
|
72
153
|
}
|
|
73
154
|
/**
|
|
74
155
|
* The leading article dropped and the first letter raised: "a neighborhood
|
|
@@ -98,7 +179,11 @@ import { aiSiteWords } from "./ai-site-job.js";
|
|
|
98
179
|
if (!subject) return null;
|
|
99
180
|
const name = ((_input_siteName = input.siteName) != null ? _input_siteName : '').replace(/\s+/g, ' ').trim();
|
|
100
181
|
const audience = ((_input_audience = input.audience) != null ? _input_audience : '').replace(/\s+/g, ' ').trim();
|
|
101
|
-
const title =
|
|
182
|
+
const title = aiSiteSeoTitle({
|
|
183
|
+
name,
|
|
184
|
+
subject,
|
|
185
|
+
city: input.city
|
|
186
|
+
});
|
|
102
187
|
const described = name ? `${name} is ${(_input_about1 = input.about) == null ? void 0 : _input_about1.trim()}` : subject;
|
|
103
188
|
const description = clip(sentence(audience ? `${described}, for ${audience}` : described), AI_SITE_SEO_LIMITS.description);
|
|
104
189
|
return {
|
|
@@ -115,8 +200,11 @@ import { aiSiteWords } from "./ai-site-job.js";
|
|
|
115
200
|
/** The proposal a job's inputs imply, for the step that reports it. */ export function aiSiteSeoProposalForInputs(inputs) {
|
|
116
201
|
const words = aiSiteWords(inputs);
|
|
117
202
|
const businessName = inputs == null ? void 0 : inputs['businessName'];
|
|
203
|
+
const city = inputs == null ? void 0 : inputs['city'];
|
|
118
204
|
return aiSiteSeoProposal(_extends({}, words, typeof businessName === 'string' ? {
|
|
119
205
|
siteName: businessName
|
|
206
|
+
} : {}, typeof city === 'string' ? {
|
|
207
|
+
city
|
|
120
208
|
} : {}));
|
|
121
209
|
}
|
|
122
210
|
/** Whether a value is a record the reader may look inside. */ function isRecord(value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start-seo.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 { SCREEN_SEO_TEXT_GUIDANCE } from '@aglyn/aglyn/app-utils/screen-seo-fields'\nimport type { AiJobOutput } from './ai-jobs.types'\nimport { aiSiteWords, type AiSiteWords } from './ai-site-job'\n\n/**\n * The site's own search listing, from the answers (AGL-2918).\n *\n * A site has two SEO title-and-description pairs. A page's is the page's, and\n * the scaffold's page step writes one for each page it builds. The SITE's is\n * the fallback every page with none of its own publishes, and it is the one\n * nothing was filling: a newly created site carries whatever its creation\n * seeded, which is its subdomain or its display name, and a scaffolded site\n * would publish pages listed under that.\n *\n * ── Derived, not generated ───────────────────────────────────────────────\n *\n * The two values here are arithmetic on what the person typed, not a model's\n * writing, and that is deliberate on three counts. The answers are already\n * the sentence the fallback wants — \"what kind of site\" IS what the site is,\n * and \"who it is for\" IS who it is for. It costs nothing, on a job that is\n * already tens of model calls. And it is available on the scaffold's first\n * pass, so the proposal is waiting while the pages are still building rather\n * than arriving after them.\n *\n * ── A proposal, never a write ────────────────────────────────────────────\n *\n * Nothing here writes the site's settings. The values travel as a job output\n * and land in the site SEO form as unsaved edits through the `hostSeo` zone's\n * own `proposeDraft`; the form's Update is what stores them, and a person who\n * never opens that form is left with exactly what they had.\n *\n * Pure: strings in, strings out, no Firestore and no React.\n */\n\n/**\n * The site SEO form's field names, which are what a proposal is keyed by.\n *\n * The same two strings the host settings SEO section names its Title and\n * Description inputs. They are stated here rather than imported because the\n * form is the console's and this is a plugin: the zone's `proposeDraft`\n * ignores a key the form does not edit, so a name that drifted would stage\n * nothing rather than stage something wrong.\n */\nexport const AI_SITE_SEO_FORM_FIELDS = {\n title: 'seo.title',\n description: 'seo.description',\n} as const\n\nexport type AiSiteSeoFormField =\n (typeof AI_SITE_SEO_FORM_FIELDS)[keyof typeof AI_SITE_SEO_FORM_FIELDS]\n\n/**\n * What each value is held to: the lengths the SEO editors guide a listing to,\n * which are the same two the host settings form counts down from.\n */\nexport const AI_SITE_SEO_LIMITS = SCREEN_SEO_TEXT_GUIDANCE\n\n/** The kind an `seo` output carries when it is a guided start's site listing. */\nexport const AI_SITE_SEO_PROPOSAL_KIND = 'site-listing'\n\n/** The id the scaffold reports its one site listing under. */\nexport const AI_SITE_SEO_OUTPUT_ID = 'site:listing'\n\nexport interface AiSiteSeoProposal {\n kind: typeof AI_SITE_SEO_PROPOSAL_KIND\n /** Values for the site SEO form, by field name. */\n values: Record<AiSiteSeoFormField, string>\n /** Customer-safe lines about where the words came from. */\n notes: string[]\n}\n\n/** What the proposal says about itself wherever it is shown. */\nexport const AI_SITE_SEO_NOTE =\n 'Written from what you said the site is and who it is for, not from its pages — read it before you save it.'\n\n/** Held to a length, cut at a word rather than mid-word. */\nfunction clip(value: string, max: number): string {\n const text = value.replace(/\\s+/g, ' ').trim()\n if (text.length <= max) return text\n const cut = text.slice(0, max + 1)\n const space = cut.lastIndexOf(' ')\n return (space > max / 2 ? cut.slice(0, space) : text.slice(0, max)).trim()\n}\n\n/**\n * The leading article dropped and the first letter raised: \"a neighborhood\n * dog groomer\" is how a person answers the question and is not how a title\n * starts. Anything the person already capitalized is left alone, so a name\n * typed as the site type survives.\n */\nfunction asSubject(about: string): string {\n // `\\b` rather than `\\s+`, so an answer that is ONLY an article leaves\n // nothing behind and the caller proposes nothing: a one-letter title is a\n // worse thing to stage into a required field than no proposal at all.\n const text = about.replace(/\\s+/g, ' ').trim().replace(/^(?:an?|the)\\b\\s*/i, '')\n if (!text) return ''\n return text[0].toUpperCase() + text.slice(1)\n}\n\n/** A sentence closed with one period, and never two. */\nfunction sentence(text: string): string {\n const trimmed = text.trim()\n return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`\n}\n\n/**\n * What the listing is built from: either answer may be missing, because the\n * guided start requires only the first of them.\n */\nexport interface AiSiteSeoInput extends Partial<AiSiteWords> {\n /** The site's own name where the job carries one; empty for a site named on its pages. */\n siteName?: string\n}\n\n/**\n * The site's fallback listing from the answers, or `null` when the answers\n * describe no site at all — a job with no `businessType` has nothing to\n * propose, and an empty proposal is worse than none.\n */\nexport function aiSiteSeoProposal(input: AiSiteSeoInput): AiSiteSeoProposal | null {\n const subject = asSubject(input.about ?? '')\n if (!subject) return null\n const name = (input.siteName ?? '').replace(/\\s+/g, ' ').trim()\n const audience = (input.audience ?? '').replace(/\\s+/g, ' ').trim()\n const title = clip(name ? `${name} — ${subject}` : subject, AI_SITE_SEO_LIMITS.title)\n const described = name ? `${name} is ${input.about?.trim()}` : subject\n const description = clip(\n sentence(audience ? `${described}, for ${audience}` : described),\n AI_SITE_SEO_LIMITS.description,\n )\n return {\n kind: AI_SITE_SEO_PROPOSAL_KIND,\n values: {\n [AI_SITE_SEO_FORM_FIELDS.title]: title,\n [AI_SITE_SEO_FORM_FIELDS.description]: description,\n },\n notes: [AI_SITE_SEO_NOTE],\n }\n}\n\n/** The proposal a job's inputs imply, for the step that reports it. */\nexport function aiSiteSeoProposalForInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSeoProposal | null {\n const words = aiSiteWords(inputs)\n const businessName = inputs?.['businessName']\n return aiSiteSeoProposal({\n ...words,\n ...(typeof businessName === 'string' ? { siteName: businessName } : {}),\n })\n}\n\n/** Whether a value is a record the reader may look inside. */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * The site listing a job's outputs carry, or `null` when they carry none.\n *\n * Read defensively: an output is whatever a past release wrote, so a proposal\n * whose values are not two strings is treated as no proposal rather than\n * staged into somebody's live SEO form.\n */\nexport function aiSiteSeoProposalOf(\n outputs: readonly Pick<AiJobOutput, 'resource' | 'proposal'>[] | null | undefined,\n): AiSiteSeoProposal | null {\n for (const output of outputs ?? []) {\n if (output.resource !== 'seo') continue\n const proposal = output.proposal\n if (!isRecord(proposal) || proposal['kind'] !== AI_SITE_SEO_PROPOSAL_KIND) continue\n const values = isRecord(proposal['values']) ? proposal['values'] : {}\n const title = values[AI_SITE_SEO_FORM_FIELDS.title]\n const description = values[AI_SITE_SEO_FORM_FIELDS.description]\n if (typeof title !== 'string' || typeof description !== 'string') continue\n if (!title.trim() || !description.trim()) continue\n const notes = Array.isArray(proposal['notes'])\n ? proposal['notes'].filter((note): note is string => typeof note === 'string')\n : []\n return {\n kind: AI_SITE_SEO_PROPOSAL_KIND,\n values: {\n [AI_SITE_SEO_FORM_FIELDS.title]: title,\n [AI_SITE_SEO_FORM_FIELDS.description]: description,\n },\n notes,\n }\n }\n return null\n}\n"],"names":["SCREEN_SEO_TEXT_GUIDANCE","aiSiteWords","AI_SITE_SEO_FORM_FIELDS","title","description","AI_SITE_SEO_LIMITS","AI_SITE_SEO_PROPOSAL_KIND","AI_SITE_SEO_OUTPUT_ID","AI_SITE_SEO_NOTE","clip","value","max","text","replace","trim","length","cut","slice","space","lastIndexOf","asSubject","about","toUpperCase","sentence","trimmed","test","aiSiteSeoProposal","input","subject","name","siteName","audience","described","kind","values","notes","aiSiteSeoProposalForInputs","inputs","words","businessName","isRecord","Array","isArray","aiSiteSeoProposalOf","outputs","output","resource","proposal","filter","note"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-start-seo.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 { SCREEN_SEO_TEXT_GUIDANCE } from '@aglyn/aglyn/app-utils/screen-seo-fields'\nimport type { AiJobOutput } from './ai-jobs.types'\nimport { aiSiteWords, type AiSiteWords } from './ai-site-job'\n\n/**\n * The site's own search listing, from the answers (AGL-2918).\n *\n * A site has two SEO title-and-description pairs. A page's is the page's, and\n * the scaffold's page step writes one for each page it builds. The SITE's is\n * the fallback every page with none of its own publishes, and it is the one\n * nothing was filling: a newly created site carries whatever its creation\n * seeded, which is its subdomain or its display name, and a scaffolded site\n * would publish pages listed under that.\n *\n * ── Derived, not generated ───────────────────────────────────────────────\n *\n * The two values here are arithmetic on what the person typed, not a model's\n * writing, and that is deliberate on three counts. The answers are already\n * the sentence the fallback wants — \"what kind of site\" IS what the site is,\n * and \"who it is for\" IS who it is for. It costs nothing, on a job that is\n * already tens of model calls. And it is available on the scaffold's first\n * pass, so the proposal is waiting while the pages are still building rather\n * than arriving after them.\n *\n * ── A proposal, never a write ────────────────────────────────────────────\n *\n * Nothing here writes the site's settings. The values travel as a job output\n * and land in the site SEO form as unsaved edits through the `hostSeo` zone's\n * own `proposeDraft`; the form's Update is what stores them, and a person who\n * never opens that form is left with exactly what they had.\n *\n * Pure: strings in, strings out, no Firestore and no React.\n */\n\n/**\n * The site SEO form's field names, which are what a proposal is keyed by.\n *\n * The same two strings the host settings SEO section names its Title and\n * Description inputs. They are stated here rather than imported because the\n * form is the console's and this is a plugin: the zone's `proposeDraft`\n * ignores a key the form does not edit, so a name that drifted would stage\n * nothing rather than stage something wrong.\n */\nexport const AI_SITE_SEO_FORM_FIELDS = {\n title: 'seo.title',\n description: 'seo.description',\n} as const\n\nexport type AiSiteSeoFormField =\n (typeof AI_SITE_SEO_FORM_FIELDS)[keyof typeof AI_SITE_SEO_FORM_FIELDS]\n\n/**\n * What each value is held to: the lengths the SEO editors guide a listing to,\n * which are the same two the host settings form counts down from.\n */\nexport const AI_SITE_SEO_LIMITS = SCREEN_SEO_TEXT_GUIDANCE\n\n/** The kind an `seo` output carries when it is a guided start's site listing. */\nexport const AI_SITE_SEO_PROPOSAL_KIND = 'site-listing'\n\n/** The id the scaffold reports its one site listing under. */\nexport const AI_SITE_SEO_OUTPUT_ID = 'site:listing'\n\nexport interface AiSiteSeoProposal {\n kind: typeof AI_SITE_SEO_PROPOSAL_KIND\n /** Values for the site SEO form, by field name. */\n values: Record<AiSiteSeoFormField, string>\n /** Customer-safe lines about where the words came from. */\n notes: string[]\n}\n\n/** What the proposal says about itself wherever it is shown. */\nexport const AI_SITE_SEO_NOTE =\n 'Written from what you said the site is and who it is for, not from its pages — read it before you save it.'\n\n/**\n * Words a phrase never ends on (AGL-3596): a title cut after \"for\" or \"that\"\n * reads as broken mid-sentence, which a search result shows to everybody.\n */\nconst DANGLING = new Set([\n 'a', 'an', 'and', 'as', 'at', 'by', 'for', 'from', 'in', 'into', 'of', 'on', 'or', 'so',\n 'that', 'the', 'to', 'which', 'who', 'with', 'where', 'whose', 'while', 'your', 'our', '—', '-', '–', '&',\n])\n\n/** Without the words a phrase never ends on, or the punctuation before them. */\nfunction withoutDangling(text: string): string {\n const words = text.split(' ')\n while (words.length > 1 && DANGLING.has(words[words.length - 1].toLowerCase())) words.pop()\n return words.join(' ').replace(/[\\s,;:—–-]+$/, '').trim()\n}\n\n/** Held to a length, cut at a word rather than mid-word, and never on a dangling word. */\nfunction clip(value: string, max: number): string {\n const text = value.replace(/\\s+/g, ' ').trim()\n if (text.length <= max) return text\n const cut = text.slice(0, max + 1)\n const space = cut.lastIndexOf(' ')\n return withoutDangling((space > max / 2 ? cut.slice(0, space) : text.slice(0, max)).trim())\n}\n\n/**\n * Where the first clause of an answer ends: \"a dog groomer in Austin THAT\n * takes bookings for…\" is a subject and then a sentence about it. A title\n * keeps the subject.\n */\nconst CLAUSE_BOUNDARY =\n /\\s+(?:that|which|who|whose|where|offering|providing|specializing|serving|helping|so|because|with|for|to|by|and|but)\\b|[,;:(—–]|\\s-\\s/i\n\n/**\n * A short search title (AGL-3596): the business's name, what it is and where,\n * held to the title cap and never cut mid-phrase. The answer to \"what kind of\n * site\" is often a whole sentence, and cutting that sentence at sixty\n * characters published \"…that takes bookings for\" as a site's title.\n *\n * The longest of these that fits wins: the whole answer; its first clause\n * (the subject before \"that\", \"for\", a comma…); that clause without its\n * place. The city the person gave is added where the words do not already\n * say it, and the name leads where there is one. Only when even the shortest\n * does not fit is it cut, at a word, never on a dangling one.\n */\nexport function aiSiteSeoTitle(input: { name?: string; subject: string; city?: string }): string {\n const max = AI_SITE_SEO_LIMITS.title\n const name = (input.name ?? '').replace(/\\s+/g, ' ').trim()\n const subject = withoutDangling(input.subject.replace(/\\s+/g, ' ').trim())\n const city = (input.city ?? '').replace(/\\s+/g, ' ').trim()\n const placed = (what: string) =>\n city && !what.toLowerCase().includes(city.toLowerCase()) ? `${what} in ${city}` : what\n const boundary = subject.search(CLAUSE_BOUNDARY)\n const clause = withoutDangling(boundary > 0 ? subject.slice(0, boundary) : subject)\n const bare = withoutDangling(clause.replace(/\\s+in\\s+[^]*$/i, '')) || clause\n const whats = [...new Set([placed(subject), subject, placed(clause), clause, bare])]\n const candidates = name ? [...whats.map((what) => `${name} — ${what}`), name] : whats\n const fits = candidates.find((candidate) => candidate.length <= max)\n return fits ?? clip(candidates[candidates.length - 1], max)\n}\n\n/**\n * The leading article dropped and the first letter raised: \"a neighborhood\n * dog groomer\" is how a person answers the question and is not how a title\n * starts. Anything the person already capitalized is left alone, so a name\n * typed as the site type survives.\n */\nfunction asSubject(about: string): string {\n // `\\b` rather than `\\s+`, so an answer that is ONLY an article leaves\n // nothing behind and the caller proposes nothing: a one-letter title is a\n // worse thing to stage into a required field than no proposal at all.\n const text = about.replace(/\\s+/g, ' ').trim().replace(/^(?:an?|the)\\b\\s*/i, '')\n if (!text) return ''\n return text[0].toUpperCase() + text.slice(1)\n}\n\n/** A sentence closed with one period, and never two. */\nfunction sentence(text: string): string {\n const trimmed = text.trim()\n return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`\n}\n\n/**\n * What the listing is built from: either answer may be missing, because the\n * guided start requires only the first of them.\n */\nexport interface AiSiteSeoInput extends Partial<AiSiteWords> {\n /** The site's own name where the job carries one; empty for a site named on its pages. */\n siteName?: string\n /** The city the person gave, where they gave one. */\n city?: string\n}\n\n/**\n * The site's fallback listing from the answers, or `null` when the answers\n * describe no site at all — a job with no `businessType` has nothing to\n * propose, and an empty proposal is worse than none.\n */\nexport function aiSiteSeoProposal(input: AiSiteSeoInput): AiSiteSeoProposal | null {\n const subject = asSubject(input.about ?? '')\n if (!subject) return null\n const name = (input.siteName ?? '').replace(/\\s+/g, ' ').trim()\n const audience = (input.audience ?? '').replace(/\\s+/g, ' ').trim()\n const title = aiSiteSeoTitle({ name, subject, city: input.city })\n const described = name ? `${name} is ${input.about?.trim()}` : subject\n const description = clip(\n sentence(audience ? `${described}, for ${audience}` : described),\n AI_SITE_SEO_LIMITS.description,\n )\n return {\n kind: AI_SITE_SEO_PROPOSAL_KIND,\n values: {\n [AI_SITE_SEO_FORM_FIELDS.title]: title,\n [AI_SITE_SEO_FORM_FIELDS.description]: description,\n },\n notes: [AI_SITE_SEO_NOTE],\n }\n}\n\n/** The proposal a job's inputs imply, for the step that reports it. */\nexport function aiSiteSeoProposalForInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiSiteSeoProposal | null {\n const words = aiSiteWords(inputs)\n const businessName = inputs?.['businessName']\n const city = inputs?.['city']\n return aiSiteSeoProposal({\n ...words,\n ...(typeof businessName === 'string' ? { siteName: businessName } : {}),\n ...(typeof city === 'string' ? { city } : {}),\n })\n}\n\n/** Whether a value is a record the reader may look inside. */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * The site listing a job's outputs carry, or `null` when they carry none.\n *\n * Read defensively: an output is whatever a past release wrote, so a proposal\n * whose values are not two strings is treated as no proposal rather than\n * staged into somebody's live SEO form.\n */\nexport function aiSiteSeoProposalOf(\n outputs: readonly Pick<AiJobOutput, 'resource' | 'proposal'>[] | null | undefined,\n): AiSiteSeoProposal | null {\n for (const output of outputs ?? []) {\n if (output.resource !== 'seo') continue\n const proposal = output.proposal\n if (!isRecord(proposal) || proposal['kind'] !== AI_SITE_SEO_PROPOSAL_KIND) continue\n const values = isRecord(proposal['values']) ? proposal['values'] : {}\n const title = values[AI_SITE_SEO_FORM_FIELDS.title]\n const description = values[AI_SITE_SEO_FORM_FIELDS.description]\n if (typeof title !== 'string' || typeof description !== 'string') continue\n if (!title.trim() || !description.trim()) continue\n const notes = Array.isArray(proposal['notes'])\n ? proposal['notes'].filter((note): note is string => typeof note === 'string')\n : []\n return {\n kind: AI_SITE_SEO_PROPOSAL_KIND,\n values: {\n [AI_SITE_SEO_FORM_FIELDS.title]: title,\n [AI_SITE_SEO_FORM_FIELDS.description]: description,\n },\n notes,\n }\n }\n return null\n}\n"],"names":["SCREEN_SEO_TEXT_GUIDANCE","aiSiteWords","AI_SITE_SEO_FORM_FIELDS","title","description","AI_SITE_SEO_LIMITS","AI_SITE_SEO_PROPOSAL_KIND","AI_SITE_SEO_OUTPUT_ID","AI_SITE_SEO_NOTE","DANGLING","Set","withoutDangling","text","words","split","length","has","toLowerCase","pop","join","replace","trim","clip","value","max","cut","slice","space","lastIndexOf","CLAUSE_BOUNDARY","aiSiteSeoTitle","input","name","subject","city","placed","what","includes","boundary","search","clause","bare","whats","candidates","map","fits","find","candidate","asSubject","about","toUpperCase","sentence","trimmed","test","aiSiteSeoProposal","siteName","audience","described","kind","values","notes","aiSiteSeoProposalForInputs","inputs","businessName","isRecord","Array","isArray","aiSiteSeoProposalOf","outputs","output","resource","proposal","filter","note"],"mappings":"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"}
|
|
@@ -102,7 +102,9 @@ export declare function aiSiteStartExample(answers: AiSiteStartAnswers): AiSiteS
|
|
|
102
102
|
* when they can. The door's own parser is what actually admits the job — this
|
|
103
103
|
* is what the card says before it asks for one.
|
|
104
104
|
*/
|
|
105
|
-
export declare function aiSiteStartRefusal(answers: AiSiteStartAnswers
|
|
105
|
+
export declare function aiSiteStartRefusal(answers: AiSiteStartAnswers, options?: {
|
|
106
|
+
freeTaste?: boolean;
|
|
107
|
+
}): string | null;
|
|
106
108
|
/**
|
|
107
109
|
* The answers as the brief the `site` job is created with: the plan step reads
|
|
108
110
|
* it as the person's own words, so it says what they said and asks for nothing
|
|
@@ -13,7 +13,7 @@
|
|
|
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 { AI_SITE_INPUT_MAX_CHARS,
|
|
16
|
+
*/ import { AI_SITE_INPUT_MAX_CHARS, AI_SITE_SUBMISSIONS, aiSitePagesBand } from "./ai-site-job.js";
|
|
17
17
|
/**
|
|
18
18
|
* The examples the start offers, in the order they are shown. The platform's
|
|
19
19
|
* own starter sites: a person who has no words for what they want has seen
|
|
@@ -87,7 +87,9 @@
|
|
|
87
87
|
* Why these answers cannot start a site, in a sentence a person reads; `null`
|
|
88
88
|
* when they can. The door's own parser is what actually admits the job — this
|
|
89
89
|
* is what the card says before it asks for one.
|
|
90
|
-
*/ export function aiSiteStartRefusal(answers) {
|
|
90
|
+
*/ export function aiSiteStartRefusal(answers, options = {}) {
|
|
91
|
+
// The workspace's own band (AGL-3594): one or two pages on the Free taste.
|
|
92
|
+
const band = aiSitePagesBand(options.freeTaste);
|
|
91
93
|
const siteType = answers.siteType.trim();
|
|
92
94
|
if (!siteType) return 'Say what kind of site this is.';
|
|
93
95
|
if (siteType.length > AI_SITE_INPUT_MAX_CHARS) {
|
|
@@ -96,8 +98,8 @@
|
|
|
96
98
|
if (answers.audience.trim().length > AI_SITE_INPUT_MAX_CHARS) {
|
|
97
99
|
return `Keep who the site is for under ${AI_SITE_INPUT_MAX_CHARS} characters.`;
|
|
98
100
|
}
|
|
99
|
-
if (!Number.isInteger(answers.pages) || answers.pages <
|
|
100
|
-
return `A site is planned with ${
|
|
101
|
+
if (!Number.isInteger(answers.pages) || answers.pages < band.min || answers.pages > band.max) {
|
|
102
|
+
return `A site is planned with ${band.min} to ${band.max} pages.`;
|
|
101
103
|
}
|
|
102
104
|
if (answers.example !== null && !aiSiteStartExample(answers)) {
|
|
103
105
|
return 'Pick one of the examples, or none of them.';
|
|
@@ -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 {\n AI_SITE_INPUT_MAX_CHARS,\n AI_SITE_PAGES,\n AI_SITE_SUBMISSIONS,\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 /** 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 pages: 5,\n submissions: 'inbox',\n welcomeEmail: true,\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(answers: AiSiteStartAnswers): string | null {\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 (\n !Number.isInteger(answers.pages) ||\n answers.pages < AI_SITE_PAGES.min ||\n answers.pages > AI_SITE_PAGES.max\n ) {\n return `A site is planned with ${AI_SITE_PAGES.min} to ${AI_SITE_PAGES.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 sentences.push(\n 'Plan a page for each thing a visitor comes to do, and a contact form on the page that asks to be contacted.',\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: this is one person's own site, and its name is\n * on the site already.\n */\nexport function aiSiteStartInputs(\n answers: AiSiteStartAnswers,\n): Omit<AiSiteJobInputs, 'batchId'> {\n const example = aiSiteStartExample(answers)\n return {\n businessType: answers.siteType.trim(),\n audience: answers.audience.trim(),\n starter: example?.id ?? '',\n pages: answers.pages,\n businessName: '',\n city: '',\n brand: '',\n submissions: answers.submissions,\n welcomeEmail: answers.welcomeEmail,\n }\n}\n"],"names":["AI_SITE_INPUT_MAX_CHARS","AI_SITE_PAGES","AI_SITE_SUBMISSIONS","AI_SITE_START_EXAMPLES","id","label","category","blurb","AI_SITE_START_TYPES","AI_SITE_START_ANSWERS","siteType","audience","example","pages","submissions","welcomeEmail","aiSiteStartExample","answers","find","entry","aiSiteStartRefusal","trim","length","Number","isInteger","min","max","includes","aiSiteStartBrief","sentences","push","toLowerCase","join","aiSiteStartInputs","businessType","starter","businessName","city","brand"],"mappings":"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"}
|
|
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 {\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 /** 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 pages: 5,\n submissions: 'inbox',\n welcomeEmail: true,\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 sentences.push(\n 'Plan a page for each thing a visitor comes to do, and a contact form on the page that asks to be contacted.',\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: this is one person's own site, and its name is\n * on the site already.\n */\nexport function aiSiteStartInputs(\n answers: AiSiteStartAnswers,\n): Omit<AiSiteJobInputs, 'batchId'> {\n const example = aiSiteStartExample(answers)\n return {\n businessType: answers.siteType.trim(),\n audience: answers.audience.trim(),\n starter: example?.id ?? '',\n pages: answers.pages,\n businessName: '',\n city: '',\n brand: '',\n submissions: answers.submissions,\n welcomeEmail: answers.welcomeEmail,\n }\n}\n"],"names":["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","pages","submissions","welcomeEmail","aiSiteStartExample","answers","find","entry","aiSiteStartRefusal","options","band","freeTaste","trim","length","Number","isInteger","min","max","includes","aiSiteStartBrief","sentences","push","toLowerCase","join","aiSiteStartInputs","businessType","starter","businessName","city","brand"],"mappings":"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"}
|
|
@@ -131,9 +131,9 @@ export declare const ENTERPRISE_ASSIST_CREDITS_PER_MONTH = 116000;
|
|
|
131
131
|
* - it is metered per ACCOUNT as well as per workspace, so the three free
|
|
132
132
|
* workspaces an account may hold (AGL-2265) share one 300-credit
|
|
133
133
|
* allowance rather than tripling it (`reserveAssistMessage`);
|
|
134
|
-
* -
|
|
135
|
-
*
|
|
136
|
-
*
|
|
134
|
+
* - every free request is bounded per IP, per uid and per account per
|
|
135
|
+
* day (a minimum account age, `AI_FREE_MIN_ACCOUNT_AGE_HOURS`, is
|
|
136
|
+
* available and off by default — AGL-3591);
|
|
137
137
|
* - a platform-wide daily ceiling on free spend pauses the taste for
|
|
138
138
|
* everyone until the UTC day rolls, paid workspaces untouched.
|
|
139
139
|
*
|
|
@@ -78,9 +78,9 @@
|
|
|
78
78
|
* - it is metered per ACCOUNT as well as per workspace, so the three free
|
|
79
79
|
* workspaces an account may hold (AGL-2265) share one 300-credit
|
|
80
80
|
* allowance rather than tripling it (`reserveAssistMessage`);
|
|
81
|
-
* -
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* - every free request is bounded per IP, per uid and per account per
|
|
82
|
+
* day (a minimum account age, `AI_FREE_MIN_ACCOUNT_AGE_HOURS`, is
|
|
83
|
+
* available and off by default — AGL-3591);
|
|
84
84
|
* - a platform-wide daily ceiling on free spend pauses the taste for
|
|
85
85
|
* everyone until the UTC day rolls, paid workspaces untouched.
|
|
86
86
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/plan-entitlements.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Types only: the manifest generator loads this module on its own to compile\n// the plan figures, before the catalog file core reads them from exists in\n// its new shape, so nothing here may reach a core module at runtime.\nimport type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements'\n\n/**\n * THE AI PLUGIN'S PLAN BAND (AGL-3080): how many AI credits each plan\n * includes a month. Compiled into core's `PLAN_ENTITLEMENTS` by the manifest\n * generator (`register.planEntitlements`), so `/pricing`, the plan comparison\n * and every gate read the same figure they always have; this is where it is\n * written, beside the reasoning that sized it.\n */\ndeclare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {\n interface PluginEntitlementQuotas {\n /**\n * AI credits per calendar month — the band assist spends against.\n *\n * A CREDIT IS A UNIT OF MODEL COST, not a message. One assist action can\n * cost two orders of magnitude more than another: a question is a few\n * thousand tokens, while generating a screen carries the node tree, the\n * component catalog and the theme tokens in, structured markup out, and\n * iterates. A message allowance would price those the same, so one\n * workspace's ten screen builds would outspend another's thousand\n * questions and both would read as \"within allowance\".\n *\n * The unit is defined by `ASSIST_CREDIT_COST_USD` and the conversion\n * lives in `assist-credits.ts`. Only that module turns credits into\n * dollars; everything a customer sees counts credits, because the dollar\n * figure behind them is our provider bill and not a price.\n *\n * ## HOW THE BAND IS SIZED, and why it is not a share of the price\n *\n * A credit is a dollar of provider spend at `ASSIST_CREDIT_COST_USD`, so\n * the band IS a liability figure: the plan's whole assist give is\n * `assistCreditsPerMonth / 1000` dollars. Credits past it are not a give\n * — since AGL-2653 they are SOLD at `extraAssistCreditsUsdPer1k`, unless\n * the org's `assistOverage.hardCap` asks `reserveAssistMessage` to refuse\n * at the band instead. Sizing it as a share of the subscription price\n * says nothing about whether the tier can afford it, because the price is\n * also carrying storage, bandwidth, form submissions, dataset storage,\n * API requests, contacts and email.\n *\n * So each band is sized against what those SEVEN other terms leave.\n * Every paid band takes between a quarter and a third of that remainder,\n * which keeps the tier's worst case positive and keeps the ladder's\n * shape: the margin each tier holds at 100% of every band stays in\n * proportion to what it held before assist was sold at all.\n * `tier-margin-floor.spec.ts` is the model, and it pins both the rule and\n * the resulting figures.\n *\n * The Aglyn AI add-on (AGL-2896) adds `AI_ADDON_CREDITS_PER_MONTH[plan]`\n * to this band in `resolveOrgEntitlements` — one pool, widened, rather\n * than a second meter.\n */\n assistCreditsPerMonth?: number\n }\n}\n\n/**\n * AI credits an Enterprise agreement includes per month, before the contract\n * says otherwise.\n *\n * ## Never `UNLIMITED`, and the reason is not stylistic\n *\n * `UNLIMITED` is `Number.POSITIVE_INFINITY`, `JSON.stringify(Infinity)` is\n * `null`, and `Number(null)` is `0`. An unbounded assist band therefore\n * serialises to a band of ZERO through every route that does not also send\n * the explicit flag `restoreQuotaLimit` rebuilds from — which would hand the\n * only customers with a signed contract the one budget that refuses\n * everything. A finite number crosses the wire as itself.\n *\n * The band is also a real liability rather than a capacity we already own.\n * Storage and page views are infrastructure metered into the deal; assist is\n * a per-token charge from a third party, and generative building spends it in\n * units two orders of magnitude apart.\n *\n * ## Why this number\n *\n * Enterprise carries no list price, so the band cannot be sized the way every\n * self-serve tier's is — against what that tier's other cost terms leave out\n * of a known price. It is anchored to the top of the ladder instead: 116,000\n * credits is twice Agency's band, the rule every Enterprise fallback follows\n * since the 2026-09-07 pricing decision (see the `enterprise` row of\n * `PLAN_ENTITLEMENTS`). In cost that is $116 of provider spend a month, or\n * under 9% of any deal priced at or above the self-serve top. A deal below\n * that is sold as Agency, not written as an Enterprise agreement.\n *\n * ## It is a DEFAULT, and a contract raises it\n *\n * `resolveOrgEntitlements` applies a per-org\n * `entitlements.assistCreditsPerMonth` override ahead of this, so a deal that\n * buys more assist buys it on that org without moving the figure every other\n * agreement is measured against — the same mechanism the other contracted\n * bands use.\n */\nexport const ENTERPRISE_ASSIST_CREDITS_PER_MONTH = 116_000\n\n/**\n * The Free AI taste (AGL-2925): the credits a Free workspace draws on each\n * month, behind a hard wall.\n *\n * ## Why it exists\n *\n * So the AI landing pages can say \"generate your first page free\" and mean\n * it. Three hundred credits is one or two generated sections or a handful of\n * copy rewrites — enough to see what the assistant does, not enough to build\n * a site on. The band is a WALL: `PLAN_PRICING.free\n * .extraAssistCreditsUsdPer1k` stays `null`, so `assistBandRefuses` answers\n * true and the reservation refuses at 100% with no overage, no switch and no\n * invoice. Nothing about a Free workspace can produce a charge.\n *\n * ## What it costs, and why that is the number\n *\n * A credit is `ASSIST_CREDIT_COST_USD` of provider spend, so the band costs\n * at most **$0.30 per Free workspace per month** — asserted by\n * `apps/console/specs/tier-margin-floor.spec.ts`. It is the one AI band with\n * no invoice behind it, which is why the same issue closes every multiplier\n * a script could use before the flag flips:\n *\n * - it is metered per ACCOUNT as well as per workspace, so the three free\n * workspaces an account may hold (AGL-2265) share one 300-credit\n * allowance rather than tripling it (`reserveAssistMessage`);\n * - a fresh account waits `AI_FREE_MIN_ACCOUNT_AGE_HOURS` before it can\n * spend, and every free request is bounded per IP, per uid and per\n * account per day;\n * - a platform-wide daily ceiling on free spend pauses the taste for\n * everyone until the UTC day rolls, paid workspaces untouched.\n *\n * The account allowance reads THIS constant rather than the workspace's\n * resolved band, because a staff override that widens one workspace's band\n * is a decision about that workspace, not about how much one person may draw\n * across all of theirs.\n */\nexport const FREE_AI_TASTE_CREDITS_PER_MONTH = 300\n\n/** The AI credits each plan includes a month — see the key's own docblock. */\nexport const AI_CREDITS_BY_PLAN: Readonly<Record<OrgPlan, number>> = {\n // The Free AI taste (AGL-2925): a real band, and a wall. See the constant\n // for what it buys, what it costs and the precautions that keep one person\n // from drawing it three times over.\n free: FREE_AI_TASTE_CREDITS_PER_MONTH,\n // The first paid rung of the assist ladder (AGL-3203). Starter banded at 0\n // until 2026-09-20, which put a PAYING workspace below the Free taste's 300\n // on the one axis a visitor compares straight down the column — the\n // `/pricing` row read `300 / mo` for Free and `—` for Starter. A paid plan\n // that includes less of something than the free plan is not a packaging\n // subtlety; it is the comparison table arguing against the upgrade.\n //\n // 750 is deliberately small: 2.5x the taste, enough that the assistant is a\n // thing the tier HAS rather than a thing it is shown, and $0.75 of provider\n // spend a month at `ASSIST_CREDIT_COST_USD` against a $16 annual price — a\n // twentieth of the $8.81 the tier's other metered bands already cost, and\n // the widest margin on the ladder absorbs it with room left\n // (`tier-margin-floor.spec.ts` carries the arithmetic).\n //\n // It is NOT a wall, unlike Free's: `PLAN_PRICING.starter\n // .extraAssistCreditsUsdPer1k` is $3.00, so past 750 the tier meters and\n // bills like every paid plan above it. The two fields move together by rule\n // — a positive band with no rate beside it is usage past a bound that is\n // silently free, which `plan-entitlements.spec.ts` forbids.\n //\n // `features.aiAssist` is TRUE, and AGL-3203 shipping it false was the\n // defect AGL-3207 closes. The reasoning then was \"the band is credits, not\n // the guided rung, exactly as on Free\" — but on Free the band IS spendable,\n // through `aiGenerative`, which is the door the taste exists for. Starter\n // carried neither flag, so it was the only row on the ladder sold a band\n // with nothing to spend it through: 750 credits reachable only by buying\n // the $9 add-on that was also the only thing that made them usable. \"No\n // plan bands at zero any more\" was true of the number and false of the\n // capability.\n //\n // It opens `aiAssist` rather than `aiGenerative` because that is the shape\n // of every paid rung above it — the included band funds the guided\n // assistant and generation is the add-on. Opening the generative door here\n // instead would hand the FIRST paid rung a door Pro through Agency have to\n // buy.\n //\n // No cost moves: `tier-margin-floor.spec.ts` already prices this tier's\n // assist term at 100% burn of the 750 credits, 75¢/mo. A door does not\n // widen a band; it only makes spend that was already modeled reachable.\n //\n // The AI add-on adds `AI_ADDON_CREDITS_PER_MONTH.starter` on top and\n // switches `aiGenerative` on as well.\n starter: 750,\n // $2.75 of provider spend, against the $8.35 the tier's other seven cost\n // terms leave out of $56. See the key's docblock for why the remainder and\n // not the price is what sizes this.\n pro: 2_750,\n // $7.50, against the $22.69 the other seven terms leave out of $139.\n business: 7_500,\n // $10.00, against the $30.62 the other seven terms leave out of $249.\n scale: 10_000,\n // $13.00, against the $39.74 the other seven terms leave out of $399 — the\n // thinnest remainder on the ladder, and the tier that binds the whole\n // assist ladder's share of it.\n advanced: 13_000,\n // $58.00, against the $176.71 the other seven terms leave out of $1,299.\n agency: 58_000,\n enterprise: ENTERPRISE_ASSIST_CREDITS_PER_MONTH,\n}\n\n/** The AI plugin's plan figures, for the manifest generator. */\nexport function aiPlanEntitlements(): PluginPlanEntitlementsDeclaration {\n return {\n quotas: [\n {\n key: 'assistCreditsPerMonth',\n label: 'AI credits / mo',\n byPlan: AI_CREDITS_BY_PLAN,\n },\n ],\n }\n}\n"],"names":["ENTERPRISE_ASSIST_CREDITS_PER_MONTH","FREE_AI_TASTE_CREDITS_PER_MONTH","AI_CREDITS_BY_PLAN","free","starter","pro","business","scale","advanced","agency","enterprise","aiPlanEntitlements","quotas","key","label","byPlan"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,2EAA2E;AAC3E,qEAAqE;AAyDrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GACD,OAAO,MAAMA,sCAAsC,OAAO;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GACD,OAAO,MAAMC,kCAAkC,IAAG;AAElD,4EAA4E,GAC5E,OAAO,MAAMC,qBAAwD;IACnE,0EAA0E;IAC1E,2EAA2E;IAC3E,oCAAoC;IACpCC,MAAMF;IACN,2EAA2E;IAC3E,4EAA4E;IAC5E,oEAAoE;IACpE,2EAA2E;IAC3E,wEAAwE;IACxE,oEAAoE;IACpE,EAAE;IACF,4EAA4E;IAC5E,4EAA4E;IAC5E,2EAA2E;IAC3E,0EAA0E;IAC1E,4DAA4D;IAC5D,wDAAwD;IACxD,EAAE;IACF,yDAAyD;IACzD,yEAAyE;IACzE,4EAA4E;IAC5E,yEAAyE;IACzE,4DAA4D;IAC5D,EAAE;IACF,sEAAsE;IACtE,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,wEAAwE;IACxE,uEAAuE;IACvE,cAAc;IACd,EAAE;IACF,2EAA2E;IAC3E,mEAAmE;IACnE,2EAA2E;IAC3E,2EAA2E;IAC3E,OAAO;IACP,EAAE;IACF,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxE,EAAE;IACF,qEAAqE;IACrE,sCAAsC;IACtCG,SAAS;IACT,yEAAyE;IACzE,2EAA2E;IAC3E,oCAAoC;IACpCC,KAAK;IACL,qEAAqE;IACrEC,UAAU;IACV,sEAAsE;IACtEC,OAAO;IACP,2EAA2E;IAC3E,sEAAsE;IACtE,+BAA+B;IAC/BC,UAAU;IACV,yEAAyE;IACzEC,QAAQ;IACRC,YAAYV;AACd,EAAC;AAED,8DAA8D,GAC9D,OAAO,SAASW;IACd,OAAO;QACLC,QAAQ;YACN;gBACEC,KAAK;gBACLC,OAAO;gBACPC,QAAQb;YACV;SACD;IACH;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/plan-entitlements.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Types only: the manifest generator loads this module on its own to compile\n// the plan figures, before the catalog file core reads them from exists in\n// its new shape, so nothing here may reach a core module at runtime.\nimport type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements'\n\n/**\n * THE AI PLUGIN'S PLAN BAND (AGL-3080): how many AI credits each plan\n * includes a month. Compiled into core's `PLAN_ENTITLEMENTS` by the manifest\n * generator (`register.planEntitlements`), so `/pricing`, the plan comparison\n * and every gate read the same figure they always have; this is where it is\n * written, beside the reasoning that sized it.\n */\ndeclare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {\n interface PluginEntitlementQuotas {\n /**\n * AI credits per calendar month — the band assist spends against.\n *\n * A CREDIT IS A UNIT OF MODEL COST, not a message. One assist action can\n * cost two orders of magnitude more than another: a question is a few\n * thousand tokens, while generating a screen carries the node tree, the\n * component catalog and the theme tokens in, structured markup out, and\n * iterates. A message allowance would price those the same, so one\n * workspace's ten screen builds would outspend another's thousand\n * questions and both would read as \"within allowance\".\n *\n * The unit is defined by `ASSIST_CREDIT_COST_USD` and the conversion\n * lives in `assist-credits.ts`. Only that module turns credits into\n * dollars; everything a customer sees counts credits, because the dollar\n * figure behind them is our provider bill and not a price.\n *\n * ## HOW THE BAND IS SIZED, and why it is not a share of the price\n *\n * A credit is a dollar of provider spend at `ASSIST_CREDIT_COST_USD`, so\n * the band IS a liability figure: the plan's whole assist give is\n * `assistCreditsPerMonth / 1000` dollars. Credits past it are not a give\n * — since AGL-2653 they are SOLD at `extraAssistCreditsUsdPer1k`, unless\n * the org's `assistOverage.hardCap` asks `reserveAssistMessage` to refuse\n * at the band instead. Sizing it as a share of the subscription price\n * says nothing about whether the tier can afford it, because the price is\n * also carrying storage, bandwidth, form submissions, dataset storage,\n * API requests, contacts and email.\n *\n * So each band is sized against what those SEVEN other terms leave.\n * Every paid band takes between a quarter and a third of that remainder,\n * which keeps the tier's worst case positive and keeps the ladder's\n * shape: the margin each tier holds at 100% of every band stays in\n * proportion to what it held before assist was sold at all.\n * `tier-margin-floor.spec.ts` is the model, and it pins both the rule and\n * the resulting figures.\n *\n * The Aglyn AI add-on (AGL-2896) adds `AI_ADDON_CREDITS_PER_MONTH[plan]`\n * to this band in `resolveOrgEntitlements` — one pool, widened, rather\n * than a second meter.\n */\n assistCreditsPerMonth?: number\n }\n}\n\n/**\n * AI credits an Enterprise agreement includes per month, before the contract\n * says otherwise.\n *\n * ## Never `UNLIMITED`, and the reason is not stylistic\n *\n * `UNLIMITED` is `Number.POSITIVE_INFINITY`, `JSON.stringify(Infinity)` is\n * `null`, and `Number(null)` is `0`. An unbounded assist band therefore\n * serialises to a band of ZERO through every route that does not also send\n * the explicit flag `restoreQuotaLimit` rebuilds from — which would hand the\n * only customers with a signed contract the one budget that refuses\n * everything. A finite number crosses the wire as itself.\n *\n * The band is also a real liability rather than a capacity we already own.\n * Storage and page views are infrastructure metered into the deal; assist is\n * a per-token charge from a third party, and generative building spends it in\n * units two orders of magnitude apart.\n *\n * ## Why this number\n *\n * Enterprise carries no list price, so the band cannot be sized the way every\n * self-serve tier's is — against what that tier's other cost terms leave out\n * of a known price. It is anchored to the top of the ladder instead: 116,000\n * credits is twice Agency's band, the rule every Enterprise fallback follows\n * since the 2026-09-07 pricing decision (see the `enterprise` row of\n * `PLAN_ENTITLEMENTS`). In cost that is $116 of provider spend a month, or\n * under 9% of any deal priced at or above the self-serve top. A deal below\n * that is sold as Agency, not written as an Enterprise agreement.\n *\n * ## It is a DEFAULT, and a contract raises it\n *\n * `resolveOrgEntitlements` applies a per-org\n * `entitlements.assistCreditsPerMonth` override ahead of this, so a deal that\n * buys more assist buys it on that org without moving the figure every other\n * agreement is measured against — the same mechanism the other contracted\n * bands use.\n */\nexport const ENTERPRISE_ASSIST_CREDITS_PER_MONTH = 116_000\n\n/**\n * The Free AI taste (AGL-2925): the credits a Free workspace draws on each\n * month, behind a hard wall.\n *\n * ## Why it exists\n *\n * So the AI landing pages can say \"generate your first page free\" and mean\n * it. Three hundred credits is one or two generated sections or a handful of\n * copy rewrites — enough to see what the assistant does, not enough to build\n * a site on. The band is a WALL: `PLAN_PRICING.free\n * .extraAssistCreditsUsdPer1k` stays `null`, so `assistBandRefuses` answers\n * true and the reservation refuses at 100% with no overage, no switch and no\n * invoice. Nothing about a Free workspace can produce a charge.\n *\n * ## What it costs, and why that is the number\n *\n * A credit is `ASSIST_CREDIT_COST_USD` of provider spend, so the band costs\n * at most **$0.30 per Free workspace per month** — asserted by\n * `apps/console/specs/tier-margin-floor.spec.ts`. It is the one AI band with\n * no invoice behind it, which is why the same issue closes every multiplier\n * a script could use before the flag flips:\n *\n * - it is metered per ACCOUNT as well as per workspace, so the three free\n * workspaces an account may hold (AGL-2265) share one 300-credit\n * allowance rather than tripling it (`reserveAssistMessage`);\n * - every free request is bounded per IP, per uid and per account per\n * day (a minimum account age, `AI_FREE_MIN_ACCOUNT_AGE_HOURS`, is\n * available and off by default — AGL-3591);\n * - a platform-wide daily ceiling on free spend pauses the taste for\n * everyone until the UTC day rolls, paid workspaces untouched.\n *\n * The account allowance reads THIS constant rather than the workspace's\n * resolved band, because a staff override that widens one workspace's band\n * is a decision about that workspace, not about how much one person may draw\n * across all of theirs.\n */\nexport const FREE_AI_TASTE_CREDITS_PER_MONTH = 300\n\n/** The AI credits each plan includes a month — see the key's own docblock. */\nexport const AI_CREDITS_BY_PLAN: Readonly<Record<OrgPlan, number>> = {\n // The Free AI taste (AGL-2925): a real band, and a wall. See the constant\n // for what it buys, what it costs and the precautions that keep one person\n // from drawing it three times over.\n free: FREE_AI_TASTE_CREDITS_PER_MONTH,\n // The first paid rung of the assist ladder (AGL-3203). Starter banded at 0\n // until 2026-09-20, which put a PAYING workspace below the Free taste's 300\n // on the one axis a visitor compares straight down the column — the\n // `/pricing` row read `300 / mo` for Free and `—` for Starter. A paid plan\n // that includes less of something than the free plan is not a packaging\n // subtlety; it is the comparison table arguing against the upgrade.\n //\n // 750 is deliberately small: 2.5x the taste, enough that the assistant is a\n // thing the tier HAS rather than a thing it is shown, and $0.75 of provider\n // spend a month at `ASSIST_CREDIT_COST_USD` against a $16 annual price — a\n // twentieth of the $8.81 the tier's other metered bands already cost, and\n // the widest margin on the ladder absorbs it with room left\n // (`tier-margin-floor.spec.ts` carries the arithmetic).\n //\n // It is NOT a wall, unlike Free's: `PLAN_PRICING.starter\n // .extraAssistCreditsUsdPer1k` is $3.00, so past 750 the tier meters and\n // bills like every paid plan above it. The two fields move together by rule\n // — a positive band with no rate beside it is usage past a bound that is\n // silently free, which `plan-entitlements.spec.ts` forbids.\n //\n // `features.aiAssist` is TRUE, and AGL-3203 shipping it false was the\n // defect AGL-3207 closes. The reasoning then was \"the band is credits, not\n // the guided rung, exactly as on Free\" — but on Free the band IS spendable,\n // through `aiGenerative`, which is the door the taste exists for. Starter\n // carried neither flag, so it was the only row on the ladder sold a band\n // with nothing to spend it through: 750 credits reachable only by buying\n // the $9 add-on that was also the only thing that made them usable. \"No\n // plan bands at zero any more\" was true of the number and false of the\n // capability.\n //\n // It opens `aiAssist` rather than `aiGenerative` because that is the shape\n // of every paid rung above it — the included band funds the guided\n // assistant and generation is the add-on. Opening the generative door here\n // instead would hand the FIRST paid rung a door Pro through Agency have to\n // buy.\n //\n // No cost moves: `tier-margin-floor.spec.ts` already prices this tier's\n // assist term at 100% burn of the 750 credits, 75¢/mo. A door does not\n // widen a band; it only makes spend that was already modeled reachable.\n //\n // The AI add-on adds `AI_ADDON_CREDITS_PER_MONTH.starter` on top and\n // switches `aiGenerative` on as well.\n starter: 750,\n // $2.75 of provider spend, against the $8.35 the tier's other seven cost\n // terms leave out of $56. See the key's docblock for why the remainder and\n // not the price is what sizes this.\n pro: 2_750,\n // $7.50, against the $22.69 the other seven terms leave out of $139.\n business: 7_500,\n // $10.00, against the $30.62 the other seven terms leave out of $249.\n scale: 10_000,\n // $13.00, against the $39.74 the other seven terms leave out of $399 — the\n // thinnest remainder on the ladder, and the tier that binds the whole\n // assist ladder's share of it.\n advanced: 13_000,\n // $58.00, against the $176.71 the other seven terms leave out of $1,299.\n agency: 58_000,\n enterprise: ENTERPRISE_ASSIST_CREDITS_PER_MONTH,\n}\n\n/** The AI plugin's plan figures, for the manifest generator. */\nexport function aiPlanEntitlements(): PluginPlanEntitlementsDeclaration {\n return {\n quotas: [\n {\n key: 'assistCreditsPerMonth',\n label: 'AI credits / mo',\n byPlan: AI_CREDITS_BY_PLAN,\n },\n ],\n }\n}\n"],"names":["ENTERPRISE_ASSIST_CREDITS_PER_MONTH","FREE_AI_TASTE_CREDITS_PER_MONTH","AI_CREDITS_BY_PLAN","free","starter","pro","business","scale","advanced","agency","enterprise","aiPlanEntitlements","quotas","key","label","byPlan"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,2EAA2E;AAC3E,qEAAqE;AAyDrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GACD,OAAO,MAAMA,sCAAsC,OAAO;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCC,GACD,OAAO,MAAMC,kCAAkC,IAAG;AAElD,4EAA4E,GAC5E,OAAO,MAAMC,qBAAwD;IACnE,0EAA0E;IAC1E,2EAA2E;IAC3E,oCAAoC;IACpCC,MAAMF;IACN,2EAA2E;IAC3E,4EAA4E;IAC5E,oEAAoE;IACpE,2EAA2E;IAC3E,wEAAwE;IACxE,oEAAoE;IACpE,EAAE;IACF,4EAA4E;IAC5E,4EAA4E;IAC5E,2EAA2E;IAC3E,0EAA0E;IAC1E,4DAA4D;IAC5D,wDAAwD;IACxD,EAAE;IACF,yDAAyD;IACzD,yEAAyE;IACzE,4EAA4E;IAC5E,yEAAyE;IACzE,4DAA4D;IAC5D,EAAE;IACF,sEAAsE;IACtE,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,wEAAwE;IACxE,uEAAuE;IACvE,cAAc;IACd,EAAE;IACF,2EAA2E;IAC3E,mEAAmE;IACnE,2EAA2E;IAC3E,2EAA2E;IAC3E,OAAO;IACP,EAAE;IACF,wEAAwE;IACxE,uEAAuE;IACvE,wEAAwE;IACxE,EAAE;IACF,qEAAqE;IACrE,sCAAsC;IACtCG,SAAS;IACT,yEAAyE;IACzE,2EAA2E;IAC3E,oCAAoC;IACpCC,KAAK;IACL,qEAAqE;IACrEC,UAAU;IACV,sEAAsE;IACtEC,OAAO;IACP,2EAA2E;IAC3E,sEAAsE;IACtE,+BAA+B;IAC/BC,UAAU;IACV,yEAAyE;IACzEC,QAAQ;IACRC,YAAYV;AACd,EAAC;AAED,8DAA8D,GAC9D,OAAO,SAASW;IACd,OAAO;QACLC,QAAQ;YACN;gBACEC,KAAK;gBACLC,OAAO;gBACPC,QAAQb;YACV;SACD;IACH;AACF"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -19,6 +19,7 @@ import { AiSaveAsComponent } from "./components/ai-save-as-component.component.j
|
|
|
19
19
|
import { AiGenerateSectionControl, AiRewriteControl } from "./components/besigner-ai-controls.component.js";
|
|
20
20
|
import { AiAssistProviderOnHost, AssistPanelOnHost } from "./components/ai-permissions-on-host.component.js";
|
|
21
21
|
import AiCreditsCard from "./components/ai-credits-card.component.js";
|
|
22
|
+
import AiJobsTopBarIndicator from "./components/ai-jobs-indicator.component.js";
|
|
22
23
|
import { AiCollaboratorCreditsCell, AiCollaboratorCreditsHeader, AiMemberCreditsCell, AiMemberCreditsHeader } from "./components/ai-credits-columns.component.js";
|
|
23
24
|
import { AssistSignalsPage } from "./components/assist-signals-page.component.js";
|
|
24
25
|
import BillingAssistOverageCard from "./components/billing-assist-overage-card.component.js";
|
|
@@ -48,6 +49,7 @@ import { AiExplainAutomation, AiExplainRunFailure } from "./components/ai-explai
|
|
|
48
49
|
import AiProductCopyCard from "./components/ai-product-copy-card.component.js";
|
|
49
50
|
import AiProductImportOption from "./components/ai-product-import-option.component.js";
|
|
50
51
|
import AiProductsHubCard from "./components/ai-products-hub-card.component.js";
|
|
52
|
+
import AiJobsPage from "./components/ai-jobs-page.component.js";
|
|
51
53
|
import { AI_PLUGIN_ID } from "./constants.js";
|
|
52
54
|
import { registerAiDeclarations } from "./declarations.js";
|
|
53
55
|
/**
|
|
@@ -87,6 +89,28 @@ import { registerAiDeclarations } from "./declarations.js";
|
|
|
87
89
|
Component: AssistSignalsPage
|
|
88
90
|
}
|
|
89
91
|
],
|
|
92
|
+
// A site's AI jobs (AGL-3596), an address under the site and no tab:
|
|
93
|
+
// `/ai-jobs` lists them, and `/ai-jobs/{jobId}` is one job's page —
|
|
94
|
+
// "Building your site" (AGL-3594), where the guided start lands a person
|
|
95
|
+
// and where a site job's notification opens. The `href` is a literal,
|
|
96
|
+
// `AI_SITE_BUILD_HREF`'s value, because the tab-title manifest is read
|
|
97
|
+
// from this source (`ai-jobs-page.spec.tsx` holds the two equal).
|
|
98
|
+
navItems: [
|
|
99
|
+
{
|
|
100
|
+
label: 'AI jobs',
|
|
101
|
+
href: '/ai-jobs',
|
|
102
|
+
recordTitle: 'Building your site',
|
|
103
|
+
header: {
|
|
104
|
+
title: 'AI jobs',
|
|
105
|
+
docsTopic: 'howAglynAiBuilds',
|
|
106
|
+
docsAnchor: '#finding-your-ai-jobs'
|
|
107
|
+
},
|
|
108
|
+
unlisted: true,
|
|
109
|
+
ownsSubtree: true,
|
|
110
|
+
permission: 'ai.generate',
|
|
111
|
+
Component: AiJobsPage
|
|
112
|
+
}
|
|
113
|
+
],
|
|
90
114
|
widgets: [
|
|
91
115
|
{
|
|
92
116
|
slot: 'consoleDock',
|
|
@@ -94,6 +118,18 @@ import { registerAiDeclarations } from "./declarations.js";
|
|
|
94
118
|
title: 'Assistant',
|
|
95
119
|
Component: AssistPanelOnHost
|
|
96
120
|
},
|
|
121
|
+
// The AI jobs indicator (AGL-3593), beside the notifications bell on
|
|
122
|
+
// every page: what the workspace's jobs are doing, and the way to them.
|
|
123
|
+
// Gated as the other generative widgets are; it asks the shell for the
|
|
124
|
+
// release flags of the jobs and of the panel it opens.
|
|
125
|
+
{
|
|
126
|
+
slot: 'consoleTopBar',
|
|
127
|
+
widgetId: 'ai-jobs-indicator',
|
|
128
|
+
title: 'AI jobs',
|
|
129
|
+
featureFlag: 'aiGenerative',
|
|
130
|
+
permission: 'ai.generate',
|
|
131
|
+
Component: AiJobsTopBarIndicator
|
|
132
|
+
},
|
|
97
133
|
{
|
|
98
134
|
slot: 'orgBillingUsage',
|
|
99
135
|
widgetId: 'ai-credits-meter',
|
|
@@ -314,7 +350,7 @@ import { registerAiDeclarations } from "./declarations.js";
|
|
|
314
350
|
permission: 'ai.generate',
|
|
315
351
|
Component: AiSeoAuditCard
|
|
316
352
|
},
|
|
317
|
-
// A page from a brief (AGL-2907): "
|
|
353
|
+
// A page from a brief (AGL-2907): "Create with AI" beside Templates and
|
|
318
354
|
// Create New Screen. Gated as the other generative widgets are, and it
|
|
319
355
|
// asks the jobs route about the release flag before it shows anything.
|
|
320
356
|
{
|
|
@@ -365,7 +401,7 @@ import { registerAiDeclarations } from "./declarations.js";
|
|
|
365
401
|
Component: AiDescribeComponentButton
|
|
366
402
|
},
|
|
367
403
|
// Automations by AI (AGL-2919), in the zones the workflows plugin hosts
|
|
368
|
-
// on its Automation page: "
|
|
404
|
+
// on its Automation page: "Create with AI" beside Add action and Recipes,
|
|
369
405
|
// "Explain it" in the editor of a saved automation, and "Why did this
|
|
370
406
|
// fail?" on a failed run. Gated as the other generative widgets are;
|
|
371
407
|
// each asks the jobs route about the release flag before it shows
|