@aglyn/plugins-ai 1.0.0-beta.261 → 1.0.0-beta.262
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-brief-dialog.component.d.ts +21 -58
- package/src/lib/components/ai-brief-dialog.component.js +8 -132
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-follow.component.js +2 -35
- package/src/lib/components/ai-job-follow.component.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.d.ts +2 -8
- package/src/lib/components/ai-job-plan.component.js +13 -74
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-template-collection-field.component.d.ts +2 -12
- package/src/lib/components/ai-template-collection-field.component.js +13 -10
- package/src/lib/components/ai-template-collection-field.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.d.ts +3 -25
- package/src/lib/components/ai-upsell-dialog.component.js +9 -66
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.d.ts +3 -8
- package/src/lib/components/use-ai-job-run.js +4 -15
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/insights/ai-figure-readers.js +15 -5
- package/src/lib/insights/ai-figure-readers.js.map +1 -1
- package/src/lib/insights/ai-figure-summaries.d.ts +27 -0
- package/src/lib/insights/ai-figure-summaries.js +79 -0
- package/src/lib/insights/ai-figure-summaries.js.map +1 -0
- package/src/lib/insights/ai-insight-readers.d.ts +2 -1
- package/src/lib/insights/ai-insight-readers.js +11 -26
- package/src/lib/insights/ai-insight-readers.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.js +11 -1
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/model/ai-brief-copy.d.ts +85 -0
- package/src/lib/model/ai-brief-copy.js +152 -0
- package/src/lib/model/ai-brief-copy.js.map +1 -0
- package/src/lib/model/ai-create-view.d.ts +200 -0
- package/src/lib/model/ai-create-view.js +263 -0
- package/src/lib/model/ai-create-view.js.map +1 -0
- package/src/lib/model/ai-insight.d.ts +1 -1
- package/src/lib/model/ai-insight.js +4 -39
- package/src/lib/model/ai-insight.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +24 -0
- package/src/lib/model/ai-job-activity.js +52 -1
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-job-plan-lines.d.ts +46 -0
- package/src/lib/model/ai-job-plan-lines.js +70 -0
- package/src/lib/model/ai-job-plan-lines.js.map +1 -0
- package/src/lib/model/ai-template-collections.d.ts +28 -0
- package/src/lib/model/ai-template-collections.js +28 -0
- package/src/lib/model/ai-template-collections.js.map +1 -0
- package/src/lib/model/ai-upsell-copy.d.ts +41 -0
- package/src/lib/model/ai-upsell-copy.js +85 -0
- package/src/lib/model/ai-upsell-copy.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.js +16 -2
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-eval-memory-firestore.js +2 -0
- package/src/lib/runtime/ai-eval-memory-firestore.js.map +1 -1
- package/src/lib/server/ai-create-route.d.ts +32 -0
- package/src/lib/server/ai-create-route.js +295 -0
- package/src/lib/server/ai-create-route.js.map +1 -0
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { pluginRecordHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
18
|
+
import { AI_JOB_BESIGNER_SEGMENT } from "./ai-job-notice.js";
|
|
19
|
+
import { AI_BRIEF_COPY, AI_BRIEF_MAX_CHARS, AI_TEMPLATE_SUBJECT_LABELS } from "./ai-brief-copy.js";
|
|
20
|
+
import { aiJobCreditRange } from "./ai-build-job.js";
|
|
21
|
+
import { aiCreditRangeText } from "./ai-credit-estimate.js";
|
|
22
|
+
import { AI_JOB_FOLLOW_PHASE_LINES, AI_JOB_PHASE_LABELS, AI_SITE_BUILD_IN_FLIGHT_COPY, aiJobConfirmingOwnPlan, aiJobPhase, aiJobProblem } from "./ai-job-activity.js";
|
|
23
|
+
import { aiJobPlanHeading, aiJobPlanLines } from "./ai-job-plan-lines.js";
|
|
24
|
+
import { AI_JOB_TERMINAL_STATUSES } from "./ai-jobs.types.js";
|
|
25
|
+
import { AI_PAGE_TYPES } from "./ai-page-job.js";
|
|
26
|
+
import { aiFreeCreditsResetLabel } from "./ai-site-job.js";
|
|
27
|
+
import { AI_TEMPLATE_SUBJECTS } from "./ai-template-subjects.js";
|
|
28
|
+
import { AI_UPSELL_COPY, AI_UPSELL_CREATE_LABEL, aiUpsellOffer } from "./ai-upsell-copy.js";
|
|
29
|
+
/**
|
|
30
|
+
* "CREATE WITH AI" AS THE NATIVE APPS DRAW IT (AGL-3854): the door a list's
|
|
31
|
+
* Create with AI button reads, and the follow view of the job it starts.
|
|
32
|
+
*
|
|
33
|
+
* The console decides each entry from gates its shell already resolved — the
|
|
34
|
+
* plan and the add-on it could buy, `ai.generate`, the release flag and the
|
|
35
|
+
* site's AI switch — and draws the brief and the job from the plugin's own
|
|
36
|
+
* components. An app has neither the shell nor the components, so the AI
|
|
37
|
+
* plugin's `/api/ai/create` door answers both as data: whether the entry is
|
|
38
|
+
* drawn and as what, every word the brief dialog says, and the job as the
|
|
39
|
+
* dialog's follow card reads it, in sentences built here from the same models
|
|
40
|
+
* the console's components read. Nothing here decides what a request may do:
|
|
41
|
+
* the jobs route's own ladder still decides each start, confirm and cancel.
|
|
42
|
+
*/ /** The lists the apps offer Create with AI on, beside their own create action. */ export const AI_CREATE_KINDS = [
|
|
43
|
+
'page',
|
|
44
|
+
'template',
|
|
45
|
+
'layout',
|
|
46
|
+
'form',
|
|
47
|
+
'component'
|
|
48
|
+
];
|
|
49
|
+
export function isAiCreateKind(value) {
|
|
50
|
+
return typeof value === 'string' && AI_CREATE_KINDS.includes(value);
|
|
51
|
+
}
|
|
52
|
+
const n = (value)=>Math.max(0, Math.round(value)).toLocaleString('en-US');
|
|
53
|
+
/** The credits line above the brief, or `null` where the workspace has no band to read. */ export function aiCreateCreditsView(facts) {
|
|
54
|
+
if (facts.kind === 'unlimited') return {
|
|
55
|
+
text: 'AI credits are not capped on this workspace.',
|
|
56
|
+
exhausted: false
|
|
57
|
+
};
|
|
58
|
+
if (facts.kind === 'free') {
|
|
59
|
+
const until = aiFreeCreditsResetLabel(facts.left.resetsOn);
|
|
60
|
+
return facts.left.left > 0 ? {
|
|
61
|
+
text: `You have ${n(facts.left.left)} of your ${n(facts.left.total)} free AI credits left this month, until ${until}.`,
|
|
62
|
+
exhausted: false
|
|
63
|
+
} : {
|
|
64
|
+
text: `You have used your free AI credits for this month. They come back on ${until}.`,
|
|
65
|
+
exhausted: true
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (facts.kind === 'band') {
|
|
69
|
+
if (facts.remaining > 0) {
|
|
70
|
+
return {
|
|
71
|
+
text: `${n(facts.remaining)} of ${n(facts.limit)} AI credits left this month.`,
|
|
72
|
+
exhausted: false
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return facts.stopsAtBand ? {
|
|
76
|
+
text: 'This workspace used its AI credits for the month.',
|
|
77
|
+
exhausted: true
|
|
78
|
+
} : {
|
|
79
|
+
text: `This workspace used its ${n(facts.limit)} AI credits for the month; more are billed as overage.`,
|
|
80
|
+
exhausted: false
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/** The chips each kind offers beside its brief, as the console's dialog draws them. */ export function aiCreateChoices(kind) {
|
|
86
|
+
if (kind === 'page') {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
key: 'pageType',
|
|
90
|
+
label: 'Page type (optional)',
|
|
91
|
+
required: false,
|
|
92
|
+
options: AI_PAGE_TYPES.map((type)=>({
|
|
93
|
+
id: type.id,
|
|
94
|
+
label: type.label
|
|
95
|
+
}))
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
if (kind === 'template') {
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
key: 'subject',
|
|
103
|
+
label: 'One page for each (required)',
|
|
104
|
+
required: true,
|
|
105
|
+
options: AI_TEMPLATE_SUBJECTS.map((subject)=>({
|
|
106
|
+
id: subject,
|
|
107
|
+
label: AI_TEMPLATE_SUBJECT_LABELS[subject]
|
|
108
|
+
}))
|
|
109
|
+
}
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
const HIDDEN = (kind)=>({
|
|
115
|
+
kind,
|
|
116
|
+
door: 'hidden',
|
|
117
|
+
label: AI_UPSELL_CREATE_LABEL,
|
|
118
|
+
copy: null,
|
|
119
|
+
briefMaxChars: AI_BRIEF_MAX_CHARS,
|
|
120
|
+
choices: [],
|
|
121
|
+
collections: null,
|
|
122
|
+
credits: null,
|
|
123
|
+
building: null,
|
|
124
|
+
upsell: null
|
|
125
|
+
});
|
|
126
|
+
/** The door one list's Create with AI reads. */ export function aiCreateDoorView(input) {
|
|
127
|
+
const { kind } = input;
|
|
128
|
+
if (!input.shown) return HIDDEN(kind);
|
|
129
|
+
if (!input.entitled) {
|
|
130
|
+
if (!input.purchasable) return HIDDEN(kind);
|
|
131
|
+
const copy = AI_UPSELL_COPY[kind];
|
|
132
|
+
return _extends({}, HIDDEN(kind), {
|
|
133
|
+
door: 'upsell',
|
|
134
|
+
upsell: {
|
|
135
|
+
title: copy.title,
|
|
136
|
+
does: copy.does,
|
|
137
|
+
offer: aiUpsellOffer(input.canManageBilling),
|
|
138
|
+
canManageBilling: input.canManageBilling,
|
|
139
|
+
action: input.canManageBilling ? `Add ${input.addonName}` : null,
|
|
140
|
+
dismiss: input.canManageBilling ? 'Not now' : 'Close'
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const collections = kind === 'template' && input.collections ? {
|
|
145
|
+
when: 'entry',
|
|
146
|
+
label: 'Collection',
|
|
147
|
+
options: input.collections.options,
|
|
148
|
+
helper: input.collections.status === 'error' ? 'This site’s collections could not be loaded. Close this and try again.' : input.collections.options.length ? 'Each page shows one entry of this collection.' : 'This site has no content collections yet. Add one in Content first.'
|
|
149
|
+
} : null;
|
|
150
|
+
return _extends({}, HIDDEN(kind), {
|
|
151
|
+
door: 'create',
|
|
152
|
+
copy: AI_BRIEF_COPY[kind],
|
|
153
|
+
choices: aiCreateChoices(kind),
|
|
154
|
+
collections,
|
|
155
|
+
credits: aiCreateCreditsView(input.credits),
|
|
156
|
+
building: kind === 'page' && input.buildInFlight ? AI_SITE_BUILD_IN_FLIGHT_COPY : null
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// ── The job it started ──────────────────────────────────────────────────
|
|
160
|
+
export { AI_JOB_FOLLOW_PHASE_LINES };
|
|
161
|
+
/**
|
|
162
|
+
* Where one output opens in an app, as the console addresses the apps' link
|
|
163
|
+
* resolver answers natively: a page opens its page (the console's preview
|
|
164
|
+
* address, which the apps' Pages claims), a component, layout or template its
|
|
165
|
+
* details, and anything else the page its owner publishes through the
|
|
166
|
+
* record-route seam (a form's own page). `besigner` is the version the job
|
|
167
|
+
* wrote, opened in the Besigner. `null` where the output has no page of its own,
|
|
168
|
+
* names no site, or its owner is not loaded.
|
|
169
|
+
*/ export function aiJobOutputAppLinks(output, orgSlug) {
|
|
170
|
+
const host = output.hostSubdomain;
|
|
171
|
+
if (!host || !orgSlug) return null;
|
|
172
|
+
const site = `/${orgSlug}/hosts/${host}`;
|
|
173
|
+
const id = encodeURIComponent(output.id);
|
|
174
|
+
const segment = AI_JOB_BESIGNER_SEGMENT[output.resource];
|
|
175
|
+
if (segment) {
|
|
176
|
+
const version = output.versionId ? encodeURIComponent(output.versionId) : null;
|
|
177
|
+
const besigner = version ? `${site}/${segment}/${id}/versions/${version}/besigner` : null;
|
|
178
|
+
if (output.resource === 'screen' || output.resource === 'emailScreen') {
|
|
179
|
+
return {
|
|
180
|
+
detail: version ? `${site}/screens/${id}/versions/${version}/view` : `${site}/screens`,
|
|
181
|
+
besigner
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
detail: `${site}/${segment}/${id}`,
|
|
186
|
+
besigner
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// A new form has no version to design yet: it opens on its own page.
|
|
190
|
+
const record = pluginRecordHref(output.resource, {
|
|
191
|
+
orgSlug,
|
|
192
|
+
host
|
|
193
|
+
}, output.id);
|
|
194
|
+
return record ? {
|
|
195
|
+
detail: record,
|
|
196
|
+
besigner: null
|
|
197
|
+
} : null;
|
|
198
|
+
}
|
|
199
|
+
export function aiJobFollowOpen(job, orgSlug) {
|
|
200
|
+
if (job.status !== 'done' || !orgSlug) return null;
|
|
201
|
+
const screens = job.outputs.filter((output)=>output.resource === 'screen' && output.hostSubdomain);
|
|
202
|
+
if (screens.length > 1) {
|
|
203
|
+
return {
|
|
204
|
+
label: 'Open your draft pages',
|
|
205
|
+
detail: `/${orgSlug}/hosts/${screens[0].hostSubdomain}/screens`,
|
|
206
|
+
besigner: null
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
for (const output of job.outputs){
|
|
210
|
+
const links = aiJobOutputAppLinks(output, orgSlug);
|
|
211
|
+
if (links) {
|
|
212
|
+
return _extends({
|
|
213
|
+
label: output.resource === 'screen' ? 'Open the draft page' : `Open ${output.label}`
|
|
214
|
+
}, links);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
/** The follow card's view of one job, for the apps (the console's `AiJobFollow` and `AiJobPlan`). */ export function aiJobFollowView(job, options = {}) {
|
|
220
|
+
var _ref, _options_orgSlug;
|
|
221
|
+
const phase = aiJobPhase(job);
|
|
222
|
+
const line = AI_JOB_FOLLOW_PHASE_LINES[phase];
|
|
223
|
+
const { plan, review } = job;
|
|
224
|
+
const waiting = job.status === 'needs_review' && review !== null && !aiJobConfirmingOwnPlan(job, options.now);
|
|
225
|
+
const range = plan && waiting && review.reason === 'plan' ? aiJobCreditRange(job.kind, plan) : null;
|
|
226
|
+
const freeCredits = (_ref = review == null ? void 0 : review.freeCredits) != null ? _ref : null;
|
|
227
|
+
const estimate = range && range.ceiling > 0 ? `Estimated cost: ${aiCreditRangeText(range)}. What it costs is what its steps spend.` + (freeCredits ? ` You have ${freeCredits.left.toLocaleString('en-US')} of your free AI credits left this month, until ${aiFreeCreditsResetLabel(freeCredits.resetsOn)}.` : '') : null;
|
|
228
|
+
const stepsDone = job.steps.filter((step)=>step.status === 'done').length;
|
|
229
|
+
const problem = phase === 'attention' || phase === 'failed' ? aiJobProblem(job, line.text) : null;
|
|
230
|
+
return {
|
|
231
|
+
id: job.id,
|
|
232
|
+
kind: job.kind,
|
|
233
|
+
status: job.status,
|
|
234
|
+
hostId: job.hostId,
|
|
235
|
+
phase,
|
|
236
|
+
phaseLabel: AI_JOB_PHASE_LABELS[phase],
|
|
237
|
+
line: {
|
|
238
|
+
tone: line.severity,
|
|
239
|
+
text: problem != null ? problem : line.text
|
|
240
|
+
},
|
|
241
|
+
moving: phase === 'queued' || phase === 'planning' || phase === 'building',
|
|
242
|
+
settled: AI_JOB_TERMINAL_STATUSES.includes(job.status),
|
|
243
|
+
steps: job.steps.length > 1 ? `${stepsDone} of ${job.steps.length} steps` : null,
|
|
244
|
+
plan: plan ? {
|
|
245
|
+
heading: aiJobPlanHeading(plan),
|
|
246
|
+
lines: aiJobPlanLines(plan).map((entry)=>entry.text)
|
|
247
|
+
} : null,
|
|
248
|
+
findings: (review == null ? void 0 : review.reason) === 'doctrine' && review.findings.length > 0 && (options.staff || !review.detail) ? review.findings.map((finding)=>finding.message) : [],
|
|
249
|
+
detail: options.staff && (review == null ? void 0 : review.detail) ? review.detail : null,
|
|
250
|
+
estimate,
|
|
251
|
+
retryRefusal: waiting && review.retryRefusal && review.reason !== 'plan' ? review.retryRefusal : null,
|
|
252
|
+
confirm: waiting ? {
|
|
253
|
+
label: review.reason === 'plan' ? 'Confirm plan' : 'Try again',
|
|
254
|
+
enabled: review.reason === 'plan' || !review.retryRefusal
|
|
255
|
+
} : null,
|
|
256
|
+
building: waiting && review.reason === 'plan' && options.buildInFlight ? AI_SITE_BUILD_IN_FLIGHT_COPY : null,
|
|
257
|
+
problem,
|
|
258
|
+
spent: job.creditsSpent > 0 ? `Used ${n(job.creditsSpent)} ${job.creditsSpent === 1 ? 'credit' : 'credits'}${phase === 'done' || AI_JOB_TERMINAL_STATUSES.includes(job.status) ? '' : ' so far'}.` : null,
|
|
259
|
+
open: aiJobFollowOpen(job, (_options_orgSlug = options.orgSlug) != null ? _options_orgSlug : '')
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
//# sourceMappingURL=ai-create-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-create-view.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\nimport { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AI_JOB_BESIGNER_SEGMENT } from './ai-job-notice'\nimport {\n AI_BRIEF_COPY,\n AI_BRIEF_MAX_CHARS,\n AI_TEMPLATE_SUBJECT_LABELS,\n type AiBriefCopy,\n} from './ai-brief-copy'\nimport { aiJobCreditRange } from './ai-build-job'\nimport { aiCreditRangeText } from './ai-credit-estimate'\nimport {\n AI_JOB_FOLLOW_PHASE_LINES,\n AI_JOB_PHASE_LABELS,\n AI_SITE_BUILD_IN_FLIGHT_COPY,\n aiJobConfirmingOwnPlan,\n aiJobPhase,\n aiJobProblem,\n type AiFollowTone,\n type AiJobPhase,\n} from './ai-job-activity'\nimport { aiJobPlanHeading, aiJobPlanLines } from './ai-job-plan-lines'\nimport { AI_JOB_TERMINAL_STATUSES, type AiJobOutput, type AiJobSummary } from './ai-jobs.types'\nimport { AI_PAGE_TYPES } from './ai-page-job'\nimport { aiFreeCreditsResetLabel, type AiFreeCreditsLeft } from './ai-site-job'\nimport { AI_TEMPLATE_SUBJECTS } from './ai-template-subjects'\nimport type { AiTemplateCollectionOption } from './ai-template-collections'\nimport { AI_UPSELL_COPY, AI_UPSELL_CREATE_LABEL, aiUpsellOffer } from './ai-upsell-copy'\n\n/**\n * \"CREATE WITH AI\" AS THE NATIVE APPS DRAW IT (AGL-3854): the door a list's\n * Create with AI button reads, and the follow view of the job it starts.\n *\n * The console decides each entry from gates its shell already resolved — the\n * plan and the add-on it could buy, `ai.generate`, the release flag and the\n * site's AI switch — and draws the brief and the job from the plugin's own\n * components. An app has neither the shell nor the components, so the AI\n * plugin's `/api/ai/create` door answers both as data: whether the entry is\n * drawn and as what, every word the brief dialog says, and the job as the\n * dialog's follow card reads it, in sentences built here from the same models\n * the console's components read. Nothing here decides what a request may do:\n * the jobs route's own ladder still decides each start, confirm and cancel.\n */\n\n/** The lists the apps offer Create with AI on, beside their own create action. */\nexport const AI_CREATE_KINDS = ['page', 'template', 'layout', 'form', 'component'] as const\n\nexport type AiCreateKind = (typeof AI_CREATE_KINDS)[number]\n\nexport function isAiCreateKind(value: unknown): value is AiCreateKind {\n return typeof value === 'string' && (AI_CREATE_KINDS as readonly string[]).includes(value)\n}\n\n/** One row of chips beside the brief, and the input the pick is sent as. */\nexport interface AiCreateChoiceView {\n /** The `inputs` key the pick is sent under. */\n key: 'pageType' | 'subject'\n label: string\n /** A required pick keeps the start disabled until one is made; an optional one clears when pressed again. */\n required: boolean\n options: Array<{ id: string; label: string }>\n}\n\n/** A template's collection field, shown once \"Collection entry\" is picked. */\nexport interface AiCreateCollectionsView {\n /** The subject the field belongs to. */\n when: 'entry'\n label: string\n options: AiTemplateCollectionOption[]\n helper: string\n}\n\n/** What is left of the month's AI credits, said before a brief is sent. */\nexport interface AiCreateCreditsView {\n text: string\n /** Nothing is left and the workspace stops at its band, so a start is refused until credits return. */\n exhausted: boolean\n}\n\n/** The add-on's offer, on a plan that could buy it and has not (AGL-3601). */\nexport interface AiCreateUpsellView {\n title: string\n does: string\n offer: string\n /** The reader may add the add-on (`billing.manage`); the action is drawn only then. */\n canManageBilling: boolean\n /** \"Add Aglyn AI\", or `null` for a reader who cannot. */\n action: string | null\n dismiss: string\n}\n\nexport interface AiCreateDoorView {\n kind: AiCreateKind\n /** `create`: the brief; `upsell`: the add-on's offer; `hidden`: the entry is not drawn. */\n door: 'create' | 'upsell' | 'hidden'\n /** The entry's label, \"Create with AI\". */\n label: string\n copy: AiBriefCopy | null\n briefMaxChars: number\n choices: AiCreateChoiceView[]\n collections: AiCreateCollectionsView | null\n credits: AiCreateCreditsView | null\n /** Another page already being built on this site (AGL-3765): a warning, not a stop. */\n building: string | null\n upsell: AiCreateUpsellView | null\n}\n\n/** What the door knows about the month's credits. */\nexport type AiCreateCreditsFacts =\n | { kind: 'free'; left: AiFreeCreditsLeft }\n | { kind: 'band'; used: number; limit: number; remaining: number; stopsAtBand: boolean }\n | { kind: 'unlimited' }\n | { kind: 'none' }\n\nconst n = (value: number) => Math.max(0, Math.round(value)).toLocaleString('en-US')\n\n/** The credits line above the brief, or `null` where the workspace has no band to read. */\nexport function aiCreateCreditsView(facts: AiCreateCreditsFacts): AiCreateCreditsView | null {\n if (facts.kind === 'unlimited') return { text: 'AI credits are not capped on this workspace.', exhausted: false }\n if (facts.kind === 'free') {\n const until = aiFreeCreditsResetLabel(facts.left.resetsOn)\n return facts.left.left > 0\n ? {\n text: `You have ${n(facts.left.left)} of your ${n(facts.left.total)} free AI credits left this month, until ${until}.`,\n exhausted: false,\n }\n : { text: `You have used your free AI credits for this month. They come back on ${until}.`, exhausted: true }\n }\n if (facts.kind === 'band') {\n if (facts.remaining > 0) {\n return { text: `${n(facts.remaining)} of ${n(facts.limit)} AI credits left this month.`, exhausted: false }\n }\n return facts.stopsAtBand\n ? { text: 'This workspace used its AI credits for the month.', exhausted: true }\n : {\n text: `This workspace used its ${n(facts.limit)} AI credits for the month; more are billed as overage.`,\n exhausted: false,\n }\n }\n return null\n}\n\n/** The chips each kind offers beside its brief, as the console's dialog draws them. */\nexport function aiCreateChoices(kind: AiCreateKind): AiCreateChoiceView[] {\n if (kind === 'page') {\n return [\n {\n key: 'pageType',\n label: 'Page type (optional)',\n required: false,\n options: AI_PAGE_TYPES.map((type) => ({ id: type.id, label: type.label })),\n },\n ]\n }\n if (kind === 'template') {\n return [\n {\n key: 'subject',\n label: 'One page for each (required)',\n required: true,\n options: AI_TEMPLATE_SUBJECTS.map((subject) => ({ id: subject, label: AI_TEMPLATE_SUBJECT_LABELS[subject] })),\n },\n ]\n }\n return []\n}\n\nexport interface AiCreateDoorInput {\n kind: AiCreateKind\n /** The gates that draw no entry at all: `ai.generate`, the release flag, the site's AI switch. */\n shown: boolean\n /** The plan carries `aiGenerative`. */\n entitled: boolean\n /** The workspace could buy the add-on that grants it, and the add-on store is open to it. */\n purchasable: boolean\n canManageBilling: boolean\n credits: AiCreateCreditsFacts\n /** The site's content collections, read only for a template's door. */\n collections?: { status: 'success' | 'error'; options: AiTemplateCollectionOption[] } | null\n /** Another page build in flight on the site, for a page's door. */\n buildInFlight?: boolean\n addonName: string\n}\n\nconst HIDDEN = (kind: AiCreateKind): AiCreateDoorView => ({\n kind,\n door: 'hidden',\n label: AI_UPSELL_CREATE_LABEL,\n copy: null,\n briefMaxChars: AI_BRIEF_MAX_CHARS,\n choices: [],\n collections: null,\n credits: null,\n building: null,\n upsell: null,\n})\n\n/** The door one list's Create with AI reads. */\nexport function aiCreateDoorView(input: AiCreateDoorInput): AiCreateDoorView {\n const { kind } = input\n if (!input.shown) return HIDDEN(kind)\n if (!input.entitled) {\n if (!input.purchasable) return HIDDEN(kind)\n const copy = AI_UPSELL_COPY[kind]\n return {\n ...HIDDEN(kind),\n door: 'upsell',\n upsell: {\n title: copy.title,\n does: copy.does,\n offer: aiUpsellOffer(input.canManageBilling),\n canManageBilling: input.canManageBilling,\n action: input.canManageBilling ? `Add ${input.addonName}` : null,\n dismiss: input.canManageBilling ? 'Not now' : 'Close',\n },\n }\n }\n const collections =\n kind === 'template' && input.collections\n ? {\n when: 'entry' as const,\n label: 'Collection',\n options: input.collections.options,\n helper:\n input.collections.status === 'error'\n ? 'This site’s collections could not be loaded. Close this and try again.'\n : input.collections.options.length\n ? 'Each page shows one entry of this collection.'\n : 'This site has no content collections yet. Add one in Content first.',\n }\n : null\n return {\n ...HIDDEN(kind),\n door: 'create',\n copy: AI_BRIEF_COPY[kind],\n choices: aiCreateChoices(kind),\n collections,\n credits: aiCreateCreditsView(input.credits),\n building: kind === 'page' && input.buildInFlight ? AI_SITE_BUILD_IN_FLIGHT_COPY : null,\n }\n}\n\n// ── The job it started ──────────────────────────────────────────────────\n\nexport { AI_JOB_FOLLOW_PHASE_LINES, type AiFollowTone }\n\n/**\n * Where one output opens in an app, as the console addresses the apps' link\n * resolver answers natively: a page opens its page (the console's preview\n * address, which the apps' Pages claims), a component, layout or template its\n * details, and anything else the page its owner publishes through the\n * record-route seam (a form's own page). `besigner` is the version the job\n * wrote, opened in the Besigner. `null` where the output has no page of its own,\n * names no site, or its owner is not loaded.\n */\nexport function aiJobOutputAppLinks(\n output: Pick<AiJobOutput, 'resource' | 'id' | 'versionId' | 'hostSubdomain'>,\n orgSlug: string,\n): { detail: string; besigner: string | null } | null {\n const host = output.hostSubdomain\n if (!host || !orgSlug) return null\n const site = `/${orgSlug}/hosts/${host}`\n const id = encodeURIComponent(output.id)\n const segment = AI_JOB_BESIGNER_SEGMENT[output.resource]\n if (segment) {\n const version = output.versionId ? encodeURIComponent(output.versionId) : null\n const besigner = version ? `${site}/${segment}/${id}/versions/${version}/besigner` : null\n if (output.resource === 'screen' || output.resource === 'emailScreen') {\n return { detail: version ? `${site}/screens/${id}/versions/${version}/view` : `${site}/screens`, besigner }\n }\n return { detail: `${site}/${segment}/${id}`, besigner }\n }\n // A new form has no version to design yet: it opens on its own page.\n const record = pluginRecordHref(output.resource, { orgSlug, host }, output.id)\n return record ? { detail: record, besigner: null } : null\n}\n\n/** The one thing a finished job leads with, as the console's `aiJobPrimaryLink` picks it. */\nexport interface AiJobFollowOpen {\n label: string\n detail: string\n besigner: string | null\n}\n\nexport function aiJobFollowOpen(\n job: Pick<AiJobSummary, 'status' | 'outputs'>,\n orgSlug: string,\n): AiJobFollowOpen | null {\n if (job.status !== 'done' || !orgSlug) return null\n const screens = job.outputs.filter((output) => output.resource === 'screen' && output.hostSubdomain)\n if (screens.length > 1) {\n return {\n label: 'Open your draft pages',\n detail: `/${orgSlug}/hosts/${screens[0].hostSubdomain}/screens`,\n besigner: null,\n }\n }\n for (const output of job.outputs) {\n const links = aiJobOutputAppLinks(output, orgSlug)\n if (links) {\n return { label: output.resource === 'screen' ? 'Open the draft page' : `Open ${output.label}`, ...links }\n }\n }\n return null\n}\n\nexport interface AiJobFollowView {\n id: string\n kind: string\n status: string\n hostId: string | null\n phase: AiJobPhase\n phaseLabel: string\n line: { tone: AiFollowTone; text: string }\n /** Still moving, so the apps read it again shortly. */\n moving: boolean\n /** Finished, failed or canceled: nothing more happens to it. */\n settled: boolean\n /** \"2 of 3 steps\", for a job of more than one step. */\n steps: string | null\n plan: { heading: string; lines: string[] } | null\n /** The building rules a refused answer broke, as a member reads them. */\n findings: string[]\n /** The checks' own sentence on a refused answer, for staff. */\n detail: string | null\n /** The plan's estimated cost, read before it is confirmed. */\n estimate: string | null\n retryRefusal: string | null\n /** \"Confirm plan\" or \"Try again\" while the job waits for the person. */\n confirm: { label: string; enabled: boolean } | null\n /** Another page build in flight on the site, said beside Confirm (AGL-3765). */\n building: string | null\n /** Why a stopped job has nothing to show. */\n problem: string | null\n /** What the job spent so far. */\n spent: string | null\n open: AiJobFollowOpen | null\n}\n\n/** The follow card's view of one job, for the apps (the console's `AiJobFollow` and `AiJobPlan`). */\nexport function aiJobFollowView(\n job: AiJobSummary,\n options: { orgSlug?: string | null; staff?: boolean; buildInFlight?: boolean; now?: number } = {},\n): AiJobFollowView {\n const phase = aiJobPhase(job)\n const line = AI_JOB_FOLLOW_PHASE_LINES[phase]\n const { plan, review } = job\n const waiting = job.status === 'needs_review' && review !== null && !aiJobConfirmingOwnPlan(job, options.now)\n const range = plan && waiting && review.reason === 'plan' ? aiJobCreditRange(job.kind, plan) : null\n const freeCredits = review?.freeCredits ?? null\n const estimate =\n range && range.ceiling > 0\n ? `Estimated cost: ${aiCreditRangeText(range)}. What it costs is what its steps spend.` +\n (freeCredits\n ? ` You have ${freeCredits.left.toLocaleString('en-US')} of your free AI credits left this month, until ${aiFreeCreditsResetLabel(freeCredits.resetsOn)}.`\n : '')\n : null\n const stepsDone = job.steps.filter((step) => step.status === 'done').length\n const problem = phase === 'attention' || phase === 'failed' ? aiJobProblem(job, line.text) : null\n return {\n id: job.id,\n kind: job.kind,\n status: job.status,\n hostId: job.hostId,\n phase,\n phaseLabel: AI_JOB_PHASE_LABELS[phase],\n line: { tone: line.severity, text: problem ?? line.text },\n moving: phase === 'queued' || phase === 'planning' || phase === 'building',\n settled: AI_JOB_TERMINAL_STATUSES.includes(job.status),\n steps: job.steps.length > 1 ? `${stepsDone} of ${job.steps.length} steps` : null,\n plan: plan ? { heading: aiJobPlanHeading(plan), lines: aiJobPlanLines(plan).map((entry) => entry.text) } : null,\n findings:\n review?.reason === 'doctrine' && review.findings.length > 0 && (options.staff || !review.detail)\n ? review.findings.map((finding) => finding.message)\n : [],\n detail: options.staff && review?.detail ? review.detail : null,\n estimate,\n retryRefusal: waiting && review.retryRefusal && review.reason !== 'plan' ? review.retryRefusal : null,\n confirm: waiting\n ? {\n label: review.reason === 'plan' ? 'Confirm plan' : 'Try again',\n enabled: review.reason === 'plan' || !review.retryRefusal,\n }\n : null,\n building: waiting && review.reason === 'plan' && options.buildInFlight ? AI_SITE_BUILD_IN_FLIGHT_COPY : null,\n problem,\n spent:\n job.creditsSpent > 0\n ? `Used ${n(job.creditsSpent)} ${job.creditsSpent === 1 ? 'credit' : 'credits'}${phase === 'done' || AI_JOB_TERMINAL_STATUSES.includes(job.status) ? '' : ' so far'}.`\n : null,\n open: aiJobFollowOpen(job, options.orgSlug ?? ''),\n }\n}\n"],"names":["pluginRecordHref","AI_JOB_BESIGNER_SEGMENT","AI_BRIEF_COPY","AI_BRIEF_MAX_CHARS","AI_TEMPLATE_SUBJECT_LABELS","aiJobCreditRange","aiCreditRangeText","AI_JOB_FOLLOW_PHASE_LINES","AI_JOB_PHASE_LABELS","AI_SITE_BUILD_IN_FLIGHT_COPY","aiJobConfirmingOwnPlan","aiJobPhase","aiJobProblem","aiJobPlanHeading","aiJobPlanLines","AI_JOB_TERMINAL_STATUSES","AI_PAGE_TYPES","aiFreeCreditsResetLabel","AI_TEMPLATE_SUBJECTS","AI_UPSELL_COPY","AI_UPSELL_CREATE_LABEL","aiUpsellOffer","AI_CREATE_KINDS","isAiCreateKind","value","includes","n","Math","max","round","toLocaleString","aiCreateCreditsView","facts","kind","text","exhausted","until","left","resetsOn","total","remaining","limit","stopsAtBand","aiCreateChoices","key","label","required","options","map","type","id","subject","HIDDEN","door","copy","briefMaxChars","choices","collections","credits","building","upsell","aiCreateDoorView","input","shown","entitled","purchasable","title","does","offer","canManageBilling","action","addonName","dismiss","when","helper","status","length","buildInFlight","aiJobOutputAppLinks","output","orgSlug","host","hostSubdomain","site","encodeURIComponent","segment","resource","version","versionId","besigner","detail","record","aiJobFollowOpen","job","screens","outputs","filter","links","aiJobFollowView","phase","line","plan","review","waiting","now","range","reason","freeCredits","estimate","ceiling","stepsDone","steps","step","problem","hostId","phaseLabel","tone","severity","moving","settled","heading","lines","entry","findings","staff","finding","message","retryRefusal","confirm","enabled","spent","creditsSpent","open"],"mappings":"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"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import type
|
|
17
|
+
import { type PluginFigureTable } from '@aglyn/aglyn/plugin-manager/plugin-figures';
|
|
18
18
|
import type { AiJob, AiJobOutput } from './ai-jobs.types';
|
|
19
19
|
/**
|
|
20
20
|
* Insights (AGL-2915): what an `insight` job answers, in the shape the step
|
|
@@ -13,7 +13,8 @@
|
|
|
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
|
-
*/
|
|
16
|
+
*/ import { pluginFigureCellText, pluginFigureIsoWeek } from "@aglyn/aglyn/plugin-manager/plugin-figures";
|
|
17
|
+
export const AI_INSIGHT_SURFACES = [
|
|
17
18
|
'analytics',
|
|
18
19
|
'datasets',
|
|
19
20
|
'crm-reports',
|
|
@@ -214,37 +215,7 @@
|
|
|
214
215
|
return where.host ? `/${where.orgSlug}/hosts/${where.host}/${path}` : null;
|
|
215
216
|
}
|
|
216
217
|
/** How a cell reads to a person, by its column's kind; `null` is "—". */ export function aiInsightCellText(table, key, value) {
|
|
217
|
-
|
|
218
|
-
const column = table.columns.find((entry)=>entry.key === key);
|
|
219
|
-
if (typeof value === 'string' || !column) return String(value);
|
|
220
|
-
switch(column.kind){
|
|
221
|
-
case 'money':
|
|
222
|
-
var _column_currency;
|
|
223
|
-
return new Intl.NumberFormat('en-US', {
|
|
224
|
-
style: 'currency',
|
|
225
|
-
currency: (_column_currency = column.currency) != null ? _column_currency : 'USD'
|
|
226
|
-
}).format(value);
|
|
227
|
-
case 'percent':
|
|
228
|
-
return `${value.toLocaleString('en-US', {
|
|
229
|
-
maximumFractionDigits: 1
|
|
230
|
-
})}%`;
|
|
231
|
-
case 'change':
|
|
232
|
-
return `${value > 0 ? '+' : ''}${value.toLocaleString('en-US', {
|
|
233
|
-
maximumFractionDigits: 1
|
|
234
|
-
})}%`;
|
|
235
|
-
case 'duration':
|
|
236
|
-
{
|
|
237
|
-
const seconds = Math.round(value);
|
|
238
|
-
if (seconds < 60) return `${seconds}s`;
|
|
239
|
-
const minutes = Math.floor(seconds / 60);
|
|
240
|
-
if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, '0')}s`;
|
|
241
|
-
return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`;
|
|
242
|
-
}
|
|
243
|
-
default:
|
|
244
|
-
return value.toLocaleString('en-US', {
|
|
245
|
-
maximumFractionDigits: 2
|
|
246
|
-
});
|
|
247
|
-
}
|
|
218
|
+
return pluginFigureCellText(table, key, value);
|
|
248
219
|
}
|
|
249
220
|
/** One cited row as a line a person reads: every column, labeled. */ export function aiInsightRowText(table, index) {
|
|
250
221
|
const row = table.rows[index];
|
|
@@ -252,13 +223,7 @@
|
|
|
252
223
|
return table.columns.map((column)=>`${column.label}: ${aiInsightCellText(table, column.key, row[column.key])}`).join(' · ');
|
|
253
224
|
}
|
|
254
225
|
/** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */ export function aiInsightIsoWeek(at) {
|
|
255
|
-
|
|
256
|
-
// Thursday decides the week's year.
|
|
257
|
-
const weekday = date.getUTCDay() || 7;
|
|
258
|
-
date.setUTCDate(date.getUTCDate() + 4 - weekday);
|
|
259
|
-
const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1);
|
|
260
|
-
const week = Math.ceil(((date.getTime() - yearStart) / 86400000 + 1) / 7);
|
|
261
|
-
return `${date.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
|
|
226
|
+
return pluginFigureIsoWeek(at);
|
|
262
227
|
}
|
|
263
228
|
|
|
264
229
|
//# sourceMappingURL=ai-insight.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-insight.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginFigureTable } from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport type { AiJob, AiJobOutput } from './ai-jobs.types'\n\n/**\n * Insights (AGL-2915): what an `insight` job answers, in the shape the step\n * writes and the console reads. Pure data — no server import — so the answer\n * card and the digest email type what they render from the module the step\n * writes it with.\n *\n * ## A number is read, never written\n *\n * The model never runs a query and never sees a record. Code reads figures —\n * traffic, orders, bookings, forms, campaigns, datasets, experiments — through\n * fixed, typed readers, each answering with one compact table that says where\n * its figures come from. The model picks the readers and phrases what their\n * tables say, and every insight it writes cites the rows its figures come from.\n * An insight whose numbers cannot be found in the rows it cites is left out\n * (`runtime/ai-insight-check.ts`), so each one a person reads can be traced to\n * a number they can open in the console.\n *\n * ## Read-only, and kept apart from the job\n *\n * An insight job writes one document besides its own record: the answer, at\n * `orgs/{orgId}/aiInsights/{jobId}`, which no rule lets a client read. The\n * job's output names it and carries no figure, because a job document is\n * readable by every member of the workspace and the figures are not: a\n * collaborator on one site may not see another site's revenue. The answer is\n * served by `GET /api/ai/insights/{jobId}` to the member who asked and, for a\n * weekly digest, to the members who reach its site.\n */\n\n/**\n * Where an insight was asked: the console page whose Assist panel — or\n * whose \"Ask AI about these numbers\" card — asked it, or the weekly digest,\n * which no person asked. A site's Marketing, Automation and Bookings pages\n * and the workspace's own pages joined at AGL-3603.\n */\nexport type AiInsightSurface =\n | 'analytics'\n | 'datasets'\n | 'crm-reports'\n | 'marketing'\n | 'automations'\n | 'bookings'\n | 'workspace'\n | 'digest'\n\nexport const AI_INSIGHT_SURFACES: readonly AiInsightSurface[] = [\n 'analytics',\n 'datasets',\n 'crm-reports',\n 'marketing',\n 'automations',\n 'bookings',\n 'workspace',\n 'digest',\n]\n\n/** The surfaces a person asks from. */\nexport const AI_INSIGHT_ASK_SURFACES: readonly AiInsightSurface[] = [\n 'analytics',\n 'datasets',\n 'crm-reports',\n 'marketing',\n 'automations',\n 'bookings',\n 'workspace',\n]\n\n/** The surface an insight job's inputs name, or `null` for one no insight job is asked from. */\nexport function aiInsightSurface(inputs: Readonly<Record<string, unknown>> | null | undefined): AiInsightSurface | null {\n const surface = inputs?.['surface']\n return typeof surface === 'string' && (AI_INSIGHT_SURFACES as readonly string[]).includes(surface)\n ? (surface as AiInsightSurface)\n : null\n}\n\n/**\n * The readers a surface may offer, by the first word of a reader's id. The\n * Analytics page — and a site's dashboard — answers about the site's traffic,\n * what it sold, took and collected, its automations' runs and its CRM\n * pipeline; a Data page about its datasets; CRM Reports about where people\n * came from, what they did and the pipeline; the Marketing page about what\n * its campaigns, overlays and tests caused and earned, beside the traffic and\n * the forms they drove; the Automation page about its runs; the Bookings page\n * about its bookings; and the workspace's own pages about its pipeline and\n * its datasets.\n */\nexport const AI_INSIGHT_SURFACE_READERS: Readonly<Record<AiInsightSurface, readonly string[]>> = {\n // `funnels` (AGL-3605): a site's funnels, step by step, beside its traffic.\n analytics: ['automations', 'bookings', 'commerce', 'crm', 'forms', 'funnels', 'marketing', 'traffic'],\n datasets: ['datasets'],\n 'crm-reports': ['bookings', 'commerce', 'crm', 'forms', 'marketing', 'traffic'],\n marketing: ['commerce', 'forms', 'marketing', 'traffic'],\n automations: ['automations'],\n bookings: ['bookings'],\n workspace: ['crm', 'datasets'],\n digest: ['bookings', 'commerce', 'forms', 'marketing', 'traffic'],\n}\n\n/** Whether a surface answers about one site; the Data page and the workspace's pages may answer about the whole workspace. */\nexport function aiInsightSurfaceNeedsSite(surface: AiInsightSurface): boolean {\n return surface !== 'datasets' && surface !== 'workspace'\n}\n\n/**\n * The readers a weekly digest reads, in the order the digest names them:\n * the traffic change, the top page, the days that stood out, the forms, the\n * campaigns, and what the site sold and took. Code reads these; no model\n * picks them.\n */\nexport const AI_INSIGHT_DIGEST_READERS: readonly string[] = [\n 'traffic.summary',\n 'traffic.pages',\n 'traffic.daily',\n 'forms.performance',\n 'marketing.campaigns',\n 'commerce.sales',\n 'bookings.services',\n]\n\n/** The windows, in days, an insight reads: the Traffic card's own ranges. */\nexport const AI_INSIGHT_WINDOWS: readonly number[] = [7, 14, 30, 90]\n\n/** The window a question that names none reads: the Traffic card's default. */\nexport const AI_INSIGHT_DEFAULT_DAYS = 14\n\n/** The window a weekly digest reads. */\nexport const AI_INSIGHT_DIGEST_DAYS = 7\n\n/** A window an insight's inputs or a read names, held to the readers' own ranges. */\nexport function aiInsightDays(value: unknown): number {\n const days = typeof value === 'number' ? value : Number(value)\n return AI_INSIGHT_WINDOWS.includes(days) ? days : AI_INSIGHT_DEFAULT_DAYS\n}\n\n/** The most insights one answer carries: a person reads them at a glance. */\nexport const AI_INSIGHT_MAX_INSIGHTS = 5\n\n/** The longest one insight may run, in characters. */\nexport const AI_INSIGHT_MAX_CHARS = 280\n\n/** The longest the sentence about what the figures could not answer may run. */\nexport const AI_INSIGHT_GAP_MAX_CHARS = 200\n\n/** The most tables one citation list names. */\nexport const AI_INSIGHT_MAX_CITES = 3\n\n/** The most rows one citation names. */\nexport const AI_INSIGHT_MAX_CITED_ROWS = 10\n\n/** The most readers one answer reads. */\nexport const AI_INSIGHT_MAX_READS = 4\n\n/** The longest question an insight job admits. */\nexport const AI_INSIGHT_QUESTION_MAX_CHARS = 1_000\n\n/**\n * A table as an insight job keeps it: the figures a reader returned, the\n * handle the model cites them by, and the reader they came from.\n */\nexport interface AiInsightTable extends PluginFigureTable {\n /** `t1`, `t2`, …, in the order the job read them. */\n ref: string\n /** The reader's id (`commerce.sales`). */\n reader: string\n /** The window the reader was asked for, in days; 0 for a reader of current counts. */\n days: number\n /** Whether the table is about one site (its path under that site) or the workspace. */\n scope: 'site' | 'org'\n}\n\n/** The rows of one table an insight's figures come from, by index. */\nexport interface AiInsightCitation {\n table: string\n rows: number[]\n}\n\nexport interface AiInsight {\n /** One or two plain sentences. */\n text: string\n cites: AiInsightCitation[]\n}\n\n/** What the model answers with. */\nexport interface AiInsightAnswer {\n insights: AiInsight[]\n /** What the figures could not answer, in one sentence without numbers; `null` when nothing. */\n gap: string | null\n}\n\n/** The answer collection under the org: server-written, and read through the insights door alone. */\nexport const AI_INSIGHTS_COLLECTION = 'aiInsights'\n\n/** An insight job's answer, as the step stores it. */\nexport interface AiInsightRecord {\n jobId: string\n orgId: string\n hostId: string | null\n surface: AiInsightSurface\n /** The member whose question it answers; for a digest, the member it was made for. */\n createdBy: string\n /** The question, verbatim; a digest's is the digest's own title. */\n question: string\n /** The window the answer was asked about, in days. */\n days: number\n /** The insights that trace to the rows they cite; every other one was left out. */\n insights: AiInsight[]\n gap: string | null\n /** Every table the job read, so each citation opens the figures it names. */\n tables: AiInsightTable[]\n /** How many insights the answer carried that were left out as not traceable. */\n left: number\n /** The ISO week a digest covers (`2026-W38`); `null` on a question. */\n week: string | null\n createdAtMs: number\n}\n\n/** The output id an answer is named by. */\nexport const AI_INSIGHT_OUTPUT_LABEL = 'Answer'\n\n/**\n * The output an insight job records: the answer's address and how many\n * insights it holds, and no figure. The count is a statement about the\n * answer rather than about the site, so every member may read it.\n */\nexport function aiInsightOutput(\n job: Pick<AiJob, '$id' | 'hostId'>,\n record: Pick<AiInsightRecord, 'insights' | 'surface'>,\n hostSubdomain: string | null,\n): AiJobOutput {\n const count = record.insights.length\n return {\n resource: 'insight',\n id: job.$id,\n versionId: null,\n hostId: job.hostId ?? null,\n hostSubdomain,\n label: record.surface === 'digest' ? 'Weekly insights' : AI_INSIGHT_OUTPUT_LABEL,\n note: count\n ? `${count} ${count === 1 ? 'insight' : 'insights'}, each traced to the figures it cites.`\n : 'The figures did not answer this question.',\n }\n}\n\n/** The answer on the wire: the stored record, with nothing a client does not render. */\nexport type AiInsightAnswerWire = AiInsightRecord\n\n/**\n * The Assist panel's insight surface for a console path, or `null` off one:\n * a site's dashboard, Analytics, Data, CRM Reports, Marketing, Automation and\n * Bookings pages; the workspace's Data page; and the workspace's sites page\n * and CRM Reports, which read the workspace's pipeline and datasets. The\n * organization's Marketing page reads site figures, so it is not one — it\n * asks about the site it is showing, through its own in-page entry.\n */\nexport function aiInsightSurfaceForPath(\n pathname: string | null | undefined,\n): { surface: Exclude<AiInsightSurface, 'digest'>; host: string | null } | null {\n const segments = String(pathname ?? '')\n .split('?')[0]\n .split('/')\n .filter(Boolean)\n // `/{org}/hosts/{host}/…` on a site, `/{org}/…` on the workspace.\n if (segments[1] === 'hosts' && segments.length >= 4) {\n const host = segments[2]\n const rest = segments.slice(3)\n if (rest[0] === 'analytics') return { surface: 'analytics', host }\n if (rest[0] === 'data') return { surface: 'datasets', host }\n if (rest[0] === 'crm' && rest[1] === 'reports') return { surface: 'crm-reports', host }\n if (rest[0] === 'marketing') return { surface: 'marketing', host }\n if (rest[0] === 'automation') return { surface: 'automations', host }\n if (rest[0] === 'bookings') return { surface: 'bookings', host }\n return null\n }\n // The site's dashboard is `/{org}/hosts/{host}`.\n if (segments[1] === 'hosts' && segments.length === 3) return { surface: 'analytics', host: segments[2] }\n if (segments.length >= 2 && segments[1] === 'data') return { surface: 'datasets', host: null }\n // The workspace's sites page, `/{org}/hosts`, which carries its dashboard row.\n if (segments.length === 2 && segments[1] === 'hosts') return { surface: 'workspace', host: null }\n if (segments[1] === 'crm' && segments[2] === 'reports') return { surface: 'workspace', host: null }\n return null\n}\n\n/** The console page a table's figures come from, or `null` when the table names none. */\nexport function aiInsightSourceHref(\n table: Pick<AiInsightTable, 'source' | 'scope'>,\n where: { orgSlug: string; host: string | null },\n): string | null {\n const path = table.source.path\n if (!path || !where.orgSlug) return null\n if (table.scope === 'org') return `/${where.orgSlug}/${path}`\n return where.host ? `/${where.orgSlug}/hosts/${where.host}/${path}` : null\n}\n\n/** How a cell reads to a person, by its column's kind; `null` is \"—\". */\nexport function aiInsightCellText(\n table: Pick<AiInsightTable, 'columns'>,\n key: string,\n value: string | number | null | undefined,\n): string {\n if (value === null || value === undefined || value === '') return '—'\n const column = table.columns.find((entry) => entry.key === key)\n if (typeof value === 'string' || !column) return String(value)\n switch (column.kind) {\n case 'money':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: column.currency ?? 'USD',\n }).format(value)\n case 'percent':\n return `${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'change':\n return `${value > 0 ? '+' : ''}${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'duration': {\n const seconds = Math.round(value)\n if (seconds < 60) return `${seconds}s`\n const minutes = Math.floor(seconds / 60)\n if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, '0')}s`\n return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`\n }\n default:\n return value.toLocaleString('en-US', { maximumFractionDigits: 2 })\n }\n}\n\n/** One cited row as a line a person reads: every column, labeled. */\nexport function aiInsightRowText(table: Pick<AiInsightTable, 'columns' | 'rows'>, index: number): string {\n const row = table.rows[index]\n if (!row) return ''\n return table.columns\n .map((column) => `${column.label}: ${aiInsightCellText(table, column.key, row[column.key])}`)\n .join(' · ')\n}\n\n/** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */\nexport function aiInsightIsoWeek(at: Date): string {\n const date = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate()))\n // Thursday decides the week's year.\n const weekday = date.getUTCDay() || 7\n date.setUTCDate(date.getUTCDate() + 4 - weekday)\n const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1)\n const week = Math.ceil(((date.getTime() - yearStart) / 86_400_000 + 1) / 7)\n return `${date.getUTCFullYear()}-W${String(week).padStart(2, '0')}`\n}\n"],"names":["AI_INSIGHT_SURFACES","AI_INSIGHT_ASK_SURFACES","aiInsightSurface","inputs","surface","includes","AI_INSIGHT_SURFACE_READERS","analytics","datasets","marketing","automations","bookings","workspace","digest","aiInsightSurfaceNeedsSite","AI_INSIGHT_DIGEST_READERS","AI_INSIGHT_WINDOWS","AI_INSIGHT_DEFAULT_DAYS","AI_INSIGHT_DIGEST_DAYS","aiInsightDays","value","days","Number","AI_INSIGHT_MAX_INSIGHTS","AI_INSIGHT_MAX_CHARS","AI_INSIGHT_GAP_MAX_CHARS","AI_INSIGHT_MAX_CITES","AI_INSIGHT_MAX_CITED_ROWS","AI_INSIGHT_MAX_READS","AI_INSIGHT_QUESTION_MAX_CHARS","AI_INSIGHTS_COLLECTION","AI_INSIGHT_OUTPUT_LABEL","aiInsightOutput","job","record","hostSubdomain","count","insights","length","resource","id","$id","versionId","hostId","label","note","aiInsightSurfaceForPath","pathname","segments","String","split","filter","Boolean","host","rest","slice","aiInsightSourceHref","table","where","path","source","orgSlug","scope","aiInsightCellText","key","undefined","column","columns","find","entry","kind","Intl","NumberFormat","style","currency","format","toLocaleString","maximumFractionDigits","seconds","Math","round","minutes","floor","padStart","aiInsightRowText","index","row","rows","map","join","aiInsightIsoWeek","at","date","Date","UTC","getUTCFullYear","getUTCMonth","getUTCDate","weekday","getUTCDay","setUTCDate","yearStart","week","ceil","getTime"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-insight.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 pluginFigureCellText,\n pluginFigureIsoWeek,\n type PluginFigureTable,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport type { AiJob, AiJobOutput } from './ai-jobs.types'\n\n/**\n * Insights (AGL-2915): what an `insight` job answers, in the shape the step\n * writes and the console reads. Pure data — no server import — so the answer\n * card and the digest email type what they render from the module the step\n * writes it with.\n *\n * ## A number is read, never written\n *\n * The model never runs a query and never sees a record. Code reads figures —\n * traffic, orders, bookings, forms, campaigns, datasets, experiments — through\n * fixed, typed readers, each answering with one compact table that says where\n * its figures come from. The model picks the readers and phrases what their\n * tables say, and every insight it writes cites the rows its figures come from.\n * An insight whose numbers cannot be found in the rows it cites is left out\n * (`runtime/ai-insight-check.ts`), so each one a person reads can be traced to\n * a number they can open in the console.\n *\n * ## Read-only, and kept apart from the job\n *\n * An insight job writes one document besides its own record: the answer, at\n * `orgs/{orgId}/aiInsights/{jobId}`, which no rule lets a client read. The\n * job's output names it and carries no figure, because a job document is\n * readable by every member of the workspace and the figures are not: a\n * collaborator on one site may not see another site's revenue. The answer is\n * served by `GET /api/ai/insights/{jobId}` to the member who asked and, for a\n * weekly digest, to the members who reach its site.\n */\n\n/**\n * Where an insight was asked: the console page whose Assist panel — or\n * whose \"Ask AI about these numbers\" card — asked it, or the weekly digest,\n * which no person asked. A site's Marketing, Automation and Bookings pages\n * and the workspace's own pages joined at AGL-3603.\n */\nexport type AiInsightSurface =\n | 'analytics'\n | 'datasets'\n | 'crm-reports'\n | 'marketing'\n | 'automations'\n | 'bookings'\n | 'workspace'\n | 'digest'\n\nexport const AI_INSIGHT_SURFACES: readonly AiInsightSurface[] = [\n 'analytics',\n 'datasets',\n 'crm-reports',\n 'marketing',\n 'automations',\n 'bookings',\n 'workspace',\n 'digest',\n]\n\n/** The surfaces a person asks from. */\nexport const AI_INSIGHT_ASK_SURFACES: readonly AiInsightSurface[] = [\n 'analytics',\n 'datasets',\n 'crm-reports',\n 'marketing',\n 'automations',\n 'bookings',\n 'workspace',\n]\n\n/** The surface an insight job's inputs name, or `null` for one no insight job is asked from. */\nexport function aiInsightSurface(inputs: Readonly<Record<string, unknown>> | null | undefined): AiInsightSurface | null {\n const surface = inputs?.['surface']\n return typeof surface === 'string' && (AI_INSIGHT_SURFACES as readonly string[]).includes(surface)\n ? (surface as AiInsightSurface)\n : null\n}\n\n/**\n * The readers a surface may offer, by the first word of a reader's id. The\n * Analytics page — and a site's dashboard — answers about the site's traffic,\n * what it sold, took and collected, its automations' runs and its CRM\n * pipeline; a Data page about its datasets; CRM Reports about where people\n * came from, what they did and the pipeline; the Marketing page about what\n * its campaigns, overlays and tests caused and earned, beside the traffic and\n * the forms they drove; the Automation page about its runs; the Bookings page\n * about its bookings; and the workspace's own pages about its pipeline and\n * its datasets.\n */\nexport const AI_INSIGHT_SURFACE_READERS: Readonly<Record<AiInsightSurface, readonly string[]>> = {\n // `funnels` (AGL-3605): a site's funnels, step by step, beside its traffic.\n analytics: ['automations', 'bookings', 'commerce', 'crm', 'forms', 'funnels', 'marketing', 'traffic'],\n datasets: ['datasets'],\n 'crm-reports': ['bookings', 'commerce', 'crm', 'forms', 'marketing', 'traffic'],\n marketing: ['commerce', 'forms', 'marketing', 'traffic'],\n automations: ['automations'],\n bookings: ['bookings'],\n workspace: ['crm', 'datasets'],\n digest: ['bookings', 'commerce', 'forms', 'marketing', 'traffic'],\n}\n\n/** Whether a surface answers about one site; the Data page and the workspace's pages may answer about the whole workspace. */\nexport function aiInsightSurfaceNeedsSite(surface: AiInsightSurface): boolean {\n return surface !== 'datasets' && surface !== 'workspace'\n}\n\n/**\n * The readers a weekly digest reads, in the order the digest names them:\n * the traffic change, the top page, the days that stood out, the forms, the\n * campaigns, and what the site sold and took. Code reads these; no model\n * picks them.\n */\nexport const AI_INSIGHT_DIGEST_READERS: readonly string[] = [\n 'traffic.summary',\n 'traffic.pages',\n 'traffic.daily',\n 'forms.performance',\n 'marketing.campaigns',\n 'commerce.sales',\n 'bookings.services',\n]\n\n/** The windows, in days, an insight reads: the Traffic card's own ranges. */\nexport const AI_INSIGHT_WINDOWS: readonly number[] = [7, 14, 30, 90]\n\n/** The window a question that names none reads: the Traffic card's default. */\nexport const AI_INSIGHT_DEFAULT_DAYS = 14\n\n/** The window a weekly digest reads. */\nexport const AI_INSIGHT_DIGEST_DAYS = 7\n\n/** A window an insight's inputs or a read names, held to the readers' own ranges. */\nexport function aiInsightDays(value: unknown): number {\n const days = typeof value === 'number' ? value : Number(value)\n return AI_INSIGHT_WINDOWS.includes(days) ? days : AI_INSIGHT_DEFAULT_DAYS\n}\n\n/** The most insights one answer carries: a person reads them at a glance. */\nexport const AI_INSIGHT_MAX_INSIGHTS = 5\n\n/** The longest one insight may run, in characters. */\nexport const AI_INSIGHT_MAX_CHARS = 280\n\n/** The longest the sentence about what the figures could not answer may run. */\nexport const AI_INSIGHT_GAP_MAX_CHARS = 200\n\n/** The most tables one citation list names. */\nexport const AI_INSIGHT_MAX_CITES = 3\n\n/** The most rows one citation names. */\nexport const AI_INSIGHT_MAX_CITED_ROWS = 10\n\n/** The most readers one answer reads. */\nexport const AI_INSIGHT_MAX_READS = 4\n\n/** The longest question an insight job admits. */\nexport const AI_INSIGHT_QUESTION_MAX_CHARS = 1_000\n\n/**\n * A table as an insight job keeps it: the figures a reader returned, the\n * handle the model cites them by, and the reader they came from.\n */\nexport interface AiInsightTable extends PluginFigureTable {\n /** `t1`, `t2`, …, in the order the job read them. */\n ref: string\n /** The reader's id (`commerce.sales`). */\n reader: string\n /** The window the reader was asked for, in days; 0 for a reader of current counts. */\n days: number\n /** Whether the table is about one site (its path under that site) or the workspace. */\n scope: 'site' | 'org'\n}\n\n/** The rows of one table an insight's figures come from, by index. */\nexport interface AiInsightCitation {\n table: string\n rows: number[]\n}\n\nexport interface AiInsight {\n /** One or two plain sentences. */\n text: string\n cites: AiInsightCitation[]\n}\n\n/** What the model answers with. */\nexport interface AiInsightAnswer {\n insights: AiInsight[]\n /** What the figures could not answer, in one sentence without numbers; `null` when nothing. */\n gap: string | null\n}\n\n/** The answer collection under the org: server-written, and read through the insights door alone. */\nexport const AI_INSIGHTS_COLLECTION = 'aiInsights'\n\n/** An insight job's answer, as the step stores it. */\nexport interface AiInsightRecord {\n jobId: string\n orgId: string\n hostId: string | null\n surface: AiInsightSurface\n /** The member whose question it answers; for a digest, the member it was made for. */\n createdBy: string\n /** The question, verbatim; a digest's is the digest's own title. */\n question: string\n /** The window the answer was asked about, in days. */\n days: number\n /** The insights that trace to the rows they cite; every other one was left out. */\n insights: AiInsight[]\n gap: string | null\n /** Every table the job read, so each citation opens the figures it names. */\n tables: AiInsightTable[]\n /** How many insights the answer carried that were left out as not traceable. */\n left: number\n /** The ISO week a digest covers (`2026-W38`); `null` on a question. */\n week: string | null\n createdAtMs: number\n}\n\n/** The output id an answer is named by. */\nexport const AI_INSIGHT_OUTPUT_LABEL = 'Answer'\n\n/**\n * The output an insight job records: the answer's address and how many\n * insights it holds, and no figure. The count is a statement about the\n * answer rather than about the site, so every member may read it.\n */\nexport function aiInsightOutput(\n job: Pick<AiJob, '$id' | 'hostId'>,\n record: Pick<AiInsightRecord, 'insights' | 'surface'>,\n hostSubdomain: string | null,\n): AiJobOutput {\n const count = record.insights.length\n return {\n resource: 'insight',\n id: job.$id,\n versionId: null,\n hostId: job.hostId ?? null,\n hostSubdomain,\n label: record.surface === 'digest' ? 'Weekly insights' : AI_INSIGHT_OUTPUT_LABEL,\n note: count\n ? `${count} ${count === 1 ? 'insight' : 'insights'}, each traced to the figures it cites.`\n : 'The figures did not answer this question.',\n }\n}\n\n/** The answer on the wire: the stored record, with nothing a client does not render. */\nexport type AiInsightAnswerWire = AiInsightRecord\n\n/**\n * The Assist panel's insight surface for a console path, or `null` off one:\n * a site's dashboard, Analytics, Data, CRM Reports, Marketing, Automation and\n * Bookings pages; the workspace's Data page; and the workspace's sites page\n * and CRM Reports, which read the workspace's pipeline and datasets. The\n * organization's Marketing page reads site figures, so it is not one — it\n * asks about the site it is showing, through its own in-page entry.\n */\nexport function aiInsightSurfaceForPath(\n pathname: string | null | undefined,\n): { surface: Exclude<AiInsightSurface, 'digest'>; host: string | null } | null {\n const segments = String(pathname ?? '')\n .split('?')[0]\n .split('/')\n .filter(Boolean)\n // `/{org}/hosts/{host}/…` on a site, `/{org}/…` on the workspace.\n if (segments[1] === 'hosts' && segments.length >= 4) {\n const host = segments[2]\n const rest = segments.slice(3)\n if (rest[0] === 'analytics') return { surface: 'analytics', host }\n if (rest[0] === 'data') return { surface: 'datasets', host }\n if (rest[0] === 'crm' && rest[1] === 'reports') return { surface: 'crm-reports', host }\n if (rest[0] === 'marketing') return { surface: 'marketing', host }\n if (rest[0] === 'automation') return { surface: 'automations', host }\n if (rest[0] === 'bookings') return { surface: 'bookings', host }\n return null\n }\n // The site's dashboard is `/{org}/hosts/{host}`.\n if (segments[1] === 'hosts' && segments.length === 3) return { surface: 'analytics', host: segments[2] }\n if (segments.length >= 2 && segments[1] === 'data') return { surface: 'datasets', host: null }\n // The workspace's sites page, `/{org}/hosts`, which carries its dashboard row.\n if (segments.length === 2 && segments[1] === 'hosts') return { surface: 'workspace', host: null }\n if (segments[1] === 'crm' && segments[2] === 'reports') return { surface: 'workspace', host: null }\n return null\n}\n\n/** The console page a table's figures come from, or `null` when the table names none. */\nexport function aiInsightSourceHref(\n table: Pick<AiInsightTable, 'source' | 'scope'>,\n where: { orgSlug: string; host: string | null },\n): string | null {\n const path = table.source.path\n if (!path || !where.orgSlug) return null\n if (table.scope === 'org') return `/${where.orgSlug}/${path}`\n return where.host ? `/${where.orgSlug}/hosts/${where.host}/${path}` : null\n}\n\n/** How a cell reads to a person, by its column's kind; `null` is \"—\". */\nexport function aiInsightCellText(\n table: Pick<AiInsightTable, 'columns'>,\n key: string,\n value: string | number | null | undefined,\n): string {\n return pluginFigureCellText(table, key, value)\n}\n\n/** One cited row as a line a person reads: every column, labeled. */\nexport function aiInsightRowText(table: Pick<AiInsightTable, 'columns' | 'rows'>, index: number): string {\n const row = table.rows[index]\n if (!row) return ''\n return table.columns\n .map((column) => `${column.label}: ${aiInsightCellText(table, column.key, row[column.key])}`)\n .join(' · ')\n}\n\n/** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */\nexport function aiInsightIsoWeek(at: Date): string {\n return pluginFigureIsoWeek(at)\n}\n"],"names":["pluginFigureCellText","pluginFigureIsoWeek","AI_INSIGHT_SURFACES","AI_INSIGHT_ASK_SURFACES","aiInsightSurface","inputs","surface","includes","AI_INSIGHT_SURFACE_READERS","analytics","datasets","marketing","automations","bookings","workspace","digest","aiInsightSurfaceNeedsSite","AI_INSIGHT_DIGEST_READERS","AI_INSIGHT_WINDOWS","AI_INSIGHT_DEFAULT_DAYS","AI_INSIGHT_DIGEST_DAYS","aiInsightDays","value","days","Number","AI_INSIGHT_MAX_INSIGHTS","AI_INSIGHT_MAX_CHARS","AI_INSIGHT_GAP_MAX_CHARS","AI_INSIGHT_MAX_CITES","AI_INSIGHT_MAX_CITED_ROWS","AI_INSIGHT_MAX_READS","AI_INSIGHT_QUESTION_MAX_CHARS","AI_INSIGHTS_COLLECTION","AI_INSIGHT_OUTPUT_LABEL","aiInsightOutput","job","record","hostSubdomain","count","insights","length","resource","id","$id","versionId","hostId","label","note","aiInsightSurfaceForPath","pathname","segments","String","split","filter","Boolean","host","rest","slice","aiInsightSourceHref","table","where","path","source","orgSlug","scope","aiInsightCellText","key","aiInsightRowText","index","row","rows","columns","map","column","join","aiInsightIsoWeek","at"],"mappings":"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"}
|
|
@@ -162,3 +162,27 @@ export declare function aiSiteBuildInFlight<T extends AiJobPhaseSource & {
|
|
|
162
162
|
exceptJobId?: string | null;
|
|
163
163
|
now?: number;
|
|
164
164
|
}): T | null;
|
|
165
|
+
/** The fields of a job {@link aiJobSettled} and {@link aiJobProblem} read. */
|
|
166
|
+
export interface AiJobSettleSource {
|
|
167
|
+
status: AiJobStatus;
|
|
168
|
+
error?: string | null;
|
|
169
|
+
review?: {
|
|
170
|
+
message?: string | null;
|
|
171
|
+
} | null;
|
|
172
|
+
}
|
|
173
|
+
/** Whether a job has stopped moving: finished, or waiting on something only a person can change. */
|
|
174
|
+
export declare function aiJobSettled(job: AiJobSettleSource | null): boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Why a settled job has no result to show, in words a person can act on, or
|
|
177
|
+
* `null` for a job that finished.
|
|
178
|
+
*/
|
|
179
|
+
export declare function aiJobProblem(job: AiJobSettleSource | null, failed: string): string | null;
|
|
180
|
+
export type AiFollowTone = 'info' | 'warning' | 'success' | 'error';
|
|
181
|
+
/**
|
|
182
|
+
* What the follow card's status line says in each phase, and how loudly
|
|
183
|
+
* (AGL-3593): the dialog that started a job, and the apps' follow view.
|
|
184
|
+
*/
|
|
185
|
+
export declare const AI_JOB_FOLLOW_PHASE_LINES: Readonly<Record<AiJobPhase, {
|
|
186
|
+
severity: AiFollowTone;
|
|
187
|
+
text: string;
|
|
188
|
+
}>>;
|
|
@@ -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_JOB_PLAN_STEP } from "./ai-jobs.types.js";
|
|
16
|
+
*/ import { AI_JOB_PLAN_STEP, AI_JOB_TERMINAL_STATUSES } from "./ai-jobs.types.js";
|
|
17
17
|
/**
|
|
18
18
|
* How long a self-confirming plan may sit parked before it reads as waiting
|
|
19
19
|
* for a person after all: a confirmation that never came.
|
|
@@ -204,5 +204,56 @@ export function aiJobsActivity(jobs) {
|
|
|
204
204
|
return Number.isFinite(at) && now - at < AI_SITE_BUILD_PLAN_FRESH_MS;
|
|
205
205
|
})) != null ? _jobs_find : null;
|
|
206
206
|
}
|
|
207
|
+
/** Whether a job has stopped moving: finished, or waiting on something only a person can change. */ export function aiJobSettled(job) {
|
|
208
|
+
if (!job) return false;
|
|
209
|
+
return AI_JOB_TERMINAL_STATUSES.includes(job.status) || job.status === 'needs_input' || job.status === 'needs_review';
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Why a settled job has no result to show, in words a person can act on, or
|
|
213
|
+
* `null` for a job that finished.
|
|
214
|
+
*/ export function aiJobProblem(job, failed) {
|
|
215
|
+
var _job_review;
|
|
216
|
+
if (!job || !aiJobSettled(job) || job.status === 'done') return null;
|
|
217
|
+
if (job.status === 'canceled') return 'The job was canceled.';
|
|
218
|
+
if (job.status === 'needs_review') return ((_job_review = job.review) == null ? void 0 : _job_review.message) || job.error || failed;
|
|
219
|
+
return job.error || failed;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* What the follow card's status line says in each phase, and how loudly
|
|
223
|
+
* (AGL-3593): the dialog that started a job, and the apps' follow view.
|
|
224
|
+
*/ export const AI_JOB_FOLLOW_PHASE_LINES = {
|
|
225
|
+
queued: {
|
|
226
|
+
severity: 'info',
|
|
227
|
+
text: 'Queued. It starts in a moment.'
|
|
228
|
+
},
|
|
229
|
+
planning: {
|
|
230
|
+
severity: 'info',
|
|
231
|
+
text: 'Planning. The plan appears here when it is ready.'
|
|
232
|
+
},
|
|
233
|
+
'plan-ready': {
|
|
234
|
+
severity: 'warning',
|
|
235
|
+
text: 'Your plan is ready. Read it below and confirm it to build — nothing is built until you do.'
|
|
236
|
+
},
|
|
237
|
+
building: {
|
|
238
|
+
severity: 'info',
|
|
239
|
+
text: 'Building. Everything it builds is an unpublished draft.'
|
|
240
|
+
},
|
|
241
|
+
attention: {
|
|
242
|
+
severity: 'warning',
|
|
243
|
+
text: 'The job stopped and needs you.'
|
|
244
|
+
},
|
|
245
|
+
done: {
|
|
246
|
+
severity: 'success',
|
|
247
|
+
text: 'Done. Your drafts are ready to open.'
|
|
248
|
+
},
|
|
249
|
+
failed: {
|
|
250
|
+
severity: 'error',
|
|
251
|
+
text: 'The job stopped before it finished.'
|
|
252
|
+
},
|
|
253
|
+
canceled: {
|
|
254
|
+
severity: 'info',
|
|
255
|
+
text: 'The job was canceled.'
|
|
256
|
+
}
|
|
257
|
+
};
|
|
207
258
|
|
|
208
259
|
//# sourceMappingURL=ai-job-activity.js.map
|