@aglyn/plugins-ai 1.0.0-beta.264 → 1.0.0-beta.266

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/package.json +17 -17
  2. package/src/lib/assets/site-styles/beauty.webp +0 -0
  3. package/src/lib/assets/site-styles/blog.webp +0 -0
  4. package/src/lib/assets/site-styles/business.webp +0 -0
  5. package/src/lib/assets/site-styles/coming-soon.webp +0 -0
  6. package/src/lib/assets/site-styles/education.webp +0 -0
  7. package/src/lib/assets/site-styles/events.webp +0 -0
  8. package/src/lib/assets/site-styles/fitness.webp +0 -0
  9. package/src/lib/assets/site-styles/landing.webp +0 -0
  10. package/src/lib/assets/site-styles/music.webp +0 -0
  11. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  12. package/src/lib/assets/site-styles/personal.webp +0 -0
  13. package/src/lib/assets/site-styles/photography.webp +0 -0
  14. package/src/lib/assets/site-styles/portfolio.webp +0 -0
  15. package/src/lib/assets/site-styles/professional.webp +0 -0
  16. package/src/lib/assets/site-styles/realestate.webp +0 -0
  17. package/src/lib/assets/site-styles/restaurant.webp +0 -0
  18. package/src/lib/assets/site-styles/store.webp +0 -0
  19. package/src/lib/assets/site-styles/studio.webp +0 -0
  20. package/src/lib/assets/site-styles/trades.webp +0 -0
  21. package/src/lib/assets/site-styles/wellness.webp +0 -0
  22. package/src/lib/assets/site-styles/yoga.webp +0 -0
  23. package/src/lib/components/ai-site-start-card.component.d.ts +12 -0
  24. package/src/lib/components/ai-site-start-card.component.js +256 -236
  25. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  26. package/src/lib/components/ai-site-start-steps.component.d.ts +108 -0
  27. package/src/lib/components/ai-site-start-steps.component.js +451 -0
  28. package/src/lib/components/ai-site-start-steps.component.js.map +1 -0
  29. package/src/lib/components/ai-site-start-tracking.d.ts +114 -0
  30. package/src/lib/components/ai-site-start-tracking.js +277 -0
  31. package/src/lib/components/ai-site-start-tracking.js.map +1 -0
  32. package/src/lib/components/ai-site-style-previews.d.ts +26 -0
  33. package/src/lib/components/ai-site-style-previews.js +115 -0
  34. package/src/lib/components/ai-site-style-previews.js.map +1 -0
  35. package/src/lib/insights/ai-insight-digest.d.ts +9 -0
  36. package/src/lib/insights/ai-insight-digest.js +35 -11
  37. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  38. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  39. package/src/lib/jobs/ai-job-page-language.js +20 -2
  40. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  41. package/src/lib/jobs/ai-job-plan-step.d.ts +43 -8
  42. package/src/lib/jobs/ai-job-plan-step.js +99 -18
  43. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-site-datasets.d.ts +82 -1
  45. package/src/lib/jobs/ai-job-site-datasets.js +276 -14
  46. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  47. package/src/lib/jobs/ai-layout-stock-photos.d.ts +108 -10
  48. package/src/lib/jobs/ai-layout-stock-photos.js +729 -87
  49. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  50. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.d.ts +58 -0
  51. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js +678 -0
  52. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js.map +1 -0
  53. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.d.ts +33 -0
  54. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js +270 -0
  55. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js.map +1 -0
  56. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.d.ts +33 -0
  57. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js +277 -0
  58. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js.map +1 -0
  59. package/src/lib/layout-language/ai-layout-compiler.js +49 -12
  60. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  61. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  62. package/src/lib/layout-language/ai-layout-design.js +14 -13
  63. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  64. package/src/lib/layout-language/ai-layout-listings.d.ts +48 -0
  65. package/src/lib/layout-language/ai-layout-listings.js +91 -5
  66. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  67. package/src/lib/layout-language/ai-layout-pictures.d.ts +15 -0
  68. package/src/lib/layout-language/ai-layout-pictures.js +6 -2
  69. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  70. package/src/lib/model/ai-site-home-arcs.d.ts +75 -0
  71. package/src/lib/model/ai-site-home-arcs.js +741 -0
  72. package/src/lib/model/ai-site-home-arcs.js.map +1 -0
  73. package/src/lib/model/ai-site-inventory.d.ts +18 -0
  74. package/src/lib/model/ai-site-inventory.js.map +1 -1
  75. package/src/lib/model/ai-site-job.d.ts +9 -6
  76. package/src/lib/model/ai-site-job.js +8 -5
  77. package/src/lib/model/ai-site-job.js.map +1 -1
  78. package/src/lib/model/ai-site-kinds.js +24 -24
  79. package/src/lib/model/ai-site-kinds.js.map +1 -1
  80. package/src/lib/model/ai-site-start-steps.d.ts +67 -0
  81. package/src/lib/model/ai-site-start-steps.js +127 -0
  82. package/src/lib/model/ai-site-start-steps.js.map +1 -0
  83. package/src/lib/model/ai-site-start.js +4 -1
  84. package/src/lib/model/ai-site-start.js.map +1 -1
  85. package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -0
  86. package/src/lib/runtime/ai-doctrine-validators.js +119 -2
  87. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  88. package/src/lib/runtime/ai-doctrine.d.ts +6 -1
  89. package/src/lib/runtime/ai-doctrine.js +9 -5
  90. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  91. package/src/lib/runtime/ai-palette.generated.js +5 -2
  92. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  93. package/src/lib/runtime/site-inventory.js +6 -0
  94. package/src/lib/runtime/site-inventory.js.map +1 -1
  95. package/src/lib/server/ai-insight-digest-route.js +2 -2
  96. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  97. package/src/lib/usage/assist-free-taste.js +3 -0
  98. package/src/lib/usage/assist-free-taste.js.map +1 -1
@@ -0,0 +1,277 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { trackEvent } from "@aglyn/aglyn/app-utils/analytics-events";
18
+ import { useEffect, useRef } from "react";
19
+ import { AI_SITE_START_STEPS, aiSiteStartStepIndex } from "../model/ai-site-start-steps.js";
20
+ /**
21
+ * Where people get lost in the guided start (AGL-3898), as the console's own
22
+ * product analytics: each step drawn, left forward or back and how long it
23
+ * took, the step the start was abandoned on, the field a step refused on,
24
+ * and the signs of a struggle — one field changed again and again, a step
25
+ * left idle, fast repeated clicks on one spot (rage) and clicks on parts of
26
+ * the form that do nothing (dead).
27
+ *
28
+ * Every event goes through `trackEvent`, the one door the console's GA4
29
+ * taxonomy has, so it is consent-gated exactly as every other console event
30
+ * is (`firebase-app.layout.tsx` registers the transport, or the refusal that
31
+ * drops it) and its params are sanitized there too. What is sent is only ever
32
+ * a step id, a field id, a closed error code, a closed area id, a count or a
33
+ * duration: never anything typed, picked or named.
34
+ *
35
+ * The detector is a plain object over a clock and a timer so its rules are
36
+ * tested with fake timers; {@link useAiSiteStartTracking} binds it to the
37
+ * dialog.
38
+ */ /** No input on a step for this long is a struggle. */ export const AI_SITE_START_IDLE_MS = 30000;
39
+ /** This many clicks on one spot… */ export const AI_SITE_START_RAGE_CLICKS = 3;
40
+ /** …within this long… */ export const AI_SITE_START_RAGE_WINDOW_MS = 700;
41
+ /** …and within this distance of the first, is rage. */ export const AI_SITE_START_RAGE_RADIUS_PX = 30;
42
+ /** One field changed this many times is a struggle. */ export const AI_SITE_START_STRUGGLE_EDITS = 3;
43
+ /** At most this many dead clicks are reported on one visit to a step. */ export const AI_SITE_START_DEAD_CLICKS_PER_STEP = 3;
44
+ export function createAiSiteStartTracker(options = {}) {
45
+ var _options_send, _options_now, _options_hidden;
46
+ const send = (_options_send = options.send) != null ? _options_send : (name, params)=>trackEvent(name, params);
47
+ const now = (_options_now = options.now) != null ? _options_now : ()=>Date.now();
48
+ const hidden = (_options_hidden = options.hidden) != null ? _options_hidden : ()=>typeof document !== 'undefined' && document.visibilityState === 'hidden';
49
+ const opened = now();
50
+ let step = null;
51
+ let enteredAt = opened;
52
+ let done = false;
53
+ let idleTimer = null;
54
+ let idleReported = false;
55
+ let deadClicks = 0;
56
+ let burst = null;
57
+ const edits = new Map();
58
+ const dirty = new Set();
59
+ const struggled = new Set();
60
+ const index = ()=>step ? aiSiteStartStepIndex(step) : 0;
61
+ const spent = ()=>Math.max(0, Math.round(now() - enteredAt));
62
+ const stopIdle = ()=>{
63
+ if (idleTimer) clearTimeout(idleTimer);
64
+ idleTimer = null;
65
+ };
66
+ const armIdle = ()=>{
67
+ stopIdle();
68
+ if (done || !step || idleReported) return;
69
+ idleTimer = setTimeout(()=>{
70
+ idleTimer = null;
71
+ if (done || !step || idleReported || hidden()) return;
72
+ idleReported = true;
73
+ send('site_start_struggle', {
74
+ step,
75
+ signal: 'idle',
76
+ duration_ms: AI_SITE_START_IDLE_MS
77
+ });
78
+ }, AI_SITE_START_IDLE_MS);
79
+ };
80
+ const edited = (field)=>{
81
+ var _edits_get;
82
+ if (!step) return;
83
+ const count = ((_edits_get = edits.get(field)) != null ? _edits_get : 0) + 1;
84
+ edits.set(field, count);
85
+ if (count >= AI_SITE_START_STRUGGLE_EDITS && !struggled.has(field)) {
86
+ struggled.add(field);
87
+ send('site_start_struggle', {
88
+ step,
89
+ signal: 'edits',
90
+ field,
91
+ count
92
+ });
93
+ }
94
+ };
95
+ return {
96
+ view (next) {
97
+ if (done) return;
98
+ step = next;
99
+ enteredAt = now();
100
+ idleReported = false;
101
+ deadClicks = 0;
102
+ burst = null;
103
+ send('site_start_step_viewed', {
104
+ step: next,
105
+ step_index: aiSiteStartStepIndex(next),
106
+ step_count: AI_SITE_START_STEPS.length
107
+ });
108
+ armIdle();
109
+ },
110
+ complete () {
111
+ if (done || !step) return;
112
+ send('site_start_step_completed', {
113
+ step,
114
+ step_index: index(),
115
+ duration_ms: spent()
116
+ });
117
+ },
118
+ back () {
119
+ if (done || !step) return;
120
+ send('site_start_step_back', {
121
+ step,
122
+ step_index: index(),
123
+ duration_ms: spent()
124
+ });
125
+ },
126
+ fieldError (field, code) {
127
+ if (done || !step) return;
128
+ send('site_start_field_error', {
129
+ step,
130
+ field,
131
+ error: code
132
+ });
133
+ },
134
+ fieldChanged (field) {
135
+ dirty.add(field);
136
+ },
137
+ fieldLeft (field) {
138
+ if (!dirty.delete(field)) return;
139
+ edited(field);
140
+ },
141
+ fieldPicked (field) {
142
+ edited(field);
143
+ },
144
+ activity () {
145
+ armIdle();
146
+ },
147
+ click (click) {
148
+ if (done || !step) return;
149
+ const at = now();
150
+ const near = burst && at - burst.at <= AI_SITE_START_RAGE_WINDOW_MS && Math.hypot(click.x - burst.x, click.y - burst.y) <= AI_SITE_START_RAGE_RADIUS_PX;
151
+ if (near && burst) {
152
+ burst.count += 1;
153
+ if (burst.count >= AI_SITE_START_RAGE_CLICKS && !burst.reported) {
154
+ burst.reported = true;
155
+ send('site_start_rage_click', {
156
+ step,
157
+ area: click.area,
158
+ clicks: burst.count
159
+ });
160
+ }
161
+ } else {
162
+ burst = {
163
+ x: click.x,
164
+ y: click.y,
165
+ at,
166
+ count: 1,
167
+ reported: false
168
+ };
169
+ }
170
+ if (!click.interactive && deadClicks < AI_SITE_START_DEAD_CLICKS_PER_STEP) {
171
+ deadClicks += 1;
172
+ send('site_start_dead_click', {
173
+ step,
174
+ area: click.area
175
+ });
176
+ }
177
+ armIdle();
178
+ },
179
+ abandon (reason) {
180
+ if (done) return;
181
+ done = true;
182
+ stopIdle();
183
+ const at = step != null ? step : AI_SITE_START_STEPS[0];
184
+ send('site_start_abandoned', {
185
+ step: at,
186
+ step_index: aiSiteStartStepIndex(at),
187
+ reason,
188
+ duration_ms: spent(),
189
+ total_ms: Math.max(0, Math.round(now() - opened))
190
+ });
191
+ },
192
+ finish () {
193
+ done = true;
194
+ stopIdle();
195
+ },
196
+ dispose () {
197
+ stopIdle();
198
+ }
199
+ };
200
+ }
201
+ /** What clicking acts on: a click on anything else in the form is a dead click. */ const INTERACTIVE = [
202
+ 'button',
203
+ 'a[href]',
204
+ 'input',
205
+ 'select',
206
+ 'textarea',
207
+ 'label',
208
+ 'summary',
209
+ '[role="button"]',
210
+ '[role="radio"]',
211
+ '[role="checkbox"]',
212
+ '[role="switch"]',
213
+ '[role="option"]',
214
+ '[role="tab"]',
215
+ '[role="link"]',
216
+ '[role="combobox"]',
217
+ '[role="menuitem"]',
218
+ '[role="slider"]',
219
+ '[contenteditable="true"]',
220
+ '.MuiInputBase-root',
221
+ '[tabindex]:not([tabindex="-1"])'
222
+ ].join(', ');
223
+ /** The attribute naming the part of the form an element is in (a closed id, never content). */ export const AI_SITE_START_AREA_ATTRIBUTE = 'data-start-area';
224
+ /** A DOM click, as the detector reads it. */ export function aiSiteStartClickOf(event) {
225
+ var _ref;
226
+ var _target_closest;
227
+ const target = event.target instanceof Element ? event.target : null;
228
+ const area = (_ref = target == null ? void 0 : (_target_closest = target.closest(`[${AI_SITE_START_AREA_ATTRIBUTE}]`)) == null ? void 0 : _target_closest.getAttribute(AI_SITE_START_AREA_ATTRIBUTE)) != null ? _ref : 'form';
229
+ return {
230
+ x: event.clientX,
231
+ y: event.clientY,
232
+ interactive: Boolean(target == null ? void 0 : target.closest(INTERACTIVE)),
233
+ area
234
+ };
235
+ }
236
+ /**
237
+ * The tracker for one open guided start: views each step as it is drawn, and
238
+ * reports the start abandoned when the tab goes away or the page changes
239
+ * under it. A start that is finished ({@link AiSiteStartTracker.finish}) or
240
+ * already abandoned reports nothing more.
241
+ *
242
+ * The unmount report waits a tick, so a development double mount (React's
243
+ * StrictMode runs an effect's cleanup and then the effect again) is not read
244
+ * as the person leaving.
245
+ */ export function useAiSiteStartTracking(step, active) {
246
+ const ref = useRef(null);
247
+ if (!ref.current) ref.current = createAiSiteStartTracker();
248
+ const tracker = ref.current;
249
+ const leaving = useRef(null);
250
+ useEffect(()=>{
251
+ if (active && step) tracker.view(step);
252
+ }, [
253
+ active,
254
+ step,
255
+ tracker
256
+ ]);
257
+ useEffect(()=>{
258
+ if (!active) return;
259
+ if (leaving.current) clearTimeout(leaving.current);
260
+ leaving.current = null;
261
+ const onPageHide = ()=>tracker.abandon('unload');
262
+ window.addEventListener('pagehide', onPageHide);
263
+ return ()=>{
264
+ window.removeEventListener('pagehide', onPageHide);
265
+ leaving.current = setTimeout(()=>{
266
+ tracker.abandon('route');
267
+ tracker.dispose();
268
+ }, 0);
269
+ };
270
+ }, [
271
+ active,
272
+ tracker
273
+ ]);
274
+ return tracker;
275
+ }
276
+
277
+ //# sourceMappingURL=ai-site-start-tracking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-site-start-tracking.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'use client'\n\nimport { trackEvent, type AnalyticsEventName, type AnalyticsEventParams } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { useEffect, useRef } from 'react'\nimport {\n AI_SITE_START_STEPS,\n aiSiteStartStepIndex,\n type AiSiteStartErrorCode,\n type AiSiteStartField,\n type AiSiteStartStep,\n} from '../model/ai-site-start-steps'\n\n/**\n * Where people get lost in the guided start (AGL-3898), as the console's own\n * product analytics: each step drawn, left forward or back and how long it\n * took, the step the start was abandoned on, the field a step refused on,\n * and the signs of a struggle — one field changed again and again, a step\n * left idle, fast repeated clicks on one spot (rage) and clicks on parts of\n * the form that do nothing (dead).\n *\n * Every event goes through `trackEvent`, the one door the console's GA4\n * taxonomy has, so it is consent-gated exactly as every other console event\n * is (`firebase-app.layout.tsx` registers the transport, or the refusal that\n * drops it) and its params are sanitized there too. What is sent is only ever\n * a step id, a field id, a closed error code, a closed area id, a count or a\n * duration: never anything typed, picked or named.\n *\n * The detector is a plain object over a clock and a timer so its rules are\n * tested with fake timers; {@link useAiSiteStartTracking} binds it to the\n * dialog.\n */\n\n/** No input on a step for this long is a struggle. */\nexport const AI_SITE_START_IDLE_MS = 30_000\n/** This many clicks on one spot… */\nexport const AI_SITE_START_RAGE_CLICKS = 3\n/** …within this long… */\nexport const AI_SITE_START_RAGE_WINDOW_MS = 700\n/** …and within this distance of the first, is rage. */\nexport const AI_SITE_START_RAGE_RADIUS_PX = 30\n/** One field changed this many times is a struggle. */\nexport const AI_SITE_START_STRUGGLE_EDITS = 3\n/** At most this many dead clicks are reported on one visit to a step. */\nexport const AI_SITE_START_DEAD_CLICKS_PER_STEP = 3\n\n/** Why the start was left without a site started. */\nexport type AiSiteStartAbandonReason = 'skip' | 'close' | 'unload' | 'route'\n\ntype Send = <K extends AnalyticsEventName>(name: K, params: AnalyticsEventParams[K]) => void\n\nexport interface AiSiteStartTrackerOptions {\n send?: Send\n now?: () => number\n /** Whether the page is out of sight, when an idle step is not a struggle. */\n hidden?: () => boolean\n}\n\n/** A click as the detector reads it: where, and what it landed on. */\nexport interface AiSiteStartClick {\n x: number\n y: number\n /** Whether it landed on something that acts on a click. */\n interactive: boolean\n /** The closed id of the part of the form it landed in. */\n area: string\n}\n\nexport interface AiSiteStartTracker {\n /** A step was drawn. */\n view(step: AiSiteStartStep): void\n /** The person went forward from the step on screen. */\n complete(): void\n /** The person went back from the step on screen. */\n back(): void\n /** A step refused to go forward on a field. */\n fieldError(field: AiSiteStartField, code: AiSiteStartErrorCode): void\n /** A text answer changed; counted as one edit when the field is left. */\n fieldChanged(field: AiSiteStartField): void\n /** A text answer was left. */\n fieldLeft(field: AiSiteStartField): void\n /** A choice was picked, which is one edit. */\n fieldPicked(field: AiSiteStartField): void\n /** Any input on the step, which puts off the idle signal. */\n activity(): void\n /** A click anywhere in the dialog. */\n click(click: AiSiteStartClick): void\n /** The start was left without a site started; reported once. */\n abandon(reason: AiSiteStartAbandonReason): void\n /** A site was started (or the starter chosen): nothing after it is an abandonment. */\n finish(): void\n /** Stops the idle timer. */\n dispose(): void\n}\n\nexport function createAiSiteStartTracker(options: AiSiteStartTrackerOptions = {}): AiSiteStartTracker {\n const send: Send = options.send ?? ((name, params) => trackEvent(name, params))\n const now = options.now ?? (() => Date.now())\n const hidden = options.hidden ?? (() => typeof document !== 'undefined' && document.visibilityState === 'hidden')\n const opened = now()\n let step: AiSiteStartStep | null = null\n let enteredAt = opened\n let done = false\n let idleTimer: ReturnType<typeof setTimeout> | null = null\n let idleReported = false\n let deadClicks = 0\n let burst: { x: number; y: number; at: number; count: number; reported: boolean } | null = null\n const edits = new Map<AiSiteStartField, number>()\n const dirty = new Set<AiSiteStartField>()\n const struggled = new Set<AiSiteStartField>()\n\n const index = () => (step ? aiSiteStartStepIndex(step) : 0)\n const spent = () => Math.max(0, Math.round(now() - enteredAt))\n const stopIdle = () => {\n if (idleTimer) clearTimeout(idleTimer)\n idleTimer = null\n }\n const armIdle = () => {\n stopIdle()\n if (done || !step || idleReported) return\n idleTimer = setTimeout(() => {\n idleTimer = null\n if (done || !step || idleReported || hidden()) return\n idleReported = true\n send('site_start_struggle', { step, signal: 'idle', duration_ms: AI_SITE_START_IDLE_MS })\n }, AI_SITE_START_IDLE_MS)\n }\n const edited = (field: AiSiteStartField) => {\n if (!step) return\n const count = (edits.get(field) ?? 0) + 1\n edits.set(field, count)\n if (count >= AI_SITE_START_STRUGGLE_EDITS && !struggled.has(field)) {\n struggled.add(field)\n send('site_start_struggle', { step, signal: 'edits', field, count })\n }\n }\n\n return {\n view(next) {\n if (done) return\n step = next\n enteredAt = now()\n idleReported = false\n deadClicks = 0\n burst = null\n send('site_start_step_viewed', { step: next, step_index: aiSiteStartStepIndex(next), step_count: AI_SITE_START_STEPS.length })\n armIdle()\n },\n complete() {\n if (done || !step) return\n send('site_start_step_completed', { step, step_index: index(), duration_ms: spent() })\n },\n back() {\n if (done || !step) return\n send('site_start_step_back', { step, step_index: index(), duration_ms: spent() })\n },\n fieldError(field, code) {\n if (done || !step) return\n send('site_start_field_error', { step, field, error: code })\n },\n fieldChanged(field) {\n dirty.add(field)\n },\n fieldLeft(field) {\n if (!dirty.delete(field)) return\n edited(field)\n },\n fieldPicked(field) {\n edited(field)\n },\n activity() {\n armIdle()\n },\n click(click) {\n if (done || !step) return\n const at = now()\n const near =\n burst &&\n at - burst.at <= AI_SITE_START_RAGE_WINDOW_MS &&\n Math.hypot(click.x - burst.x, click.y - burst.y) <= AI_SITE_START_RAGE_RADIUS_PX\n if (near && burst) {\n burst.count += 1\n if (burst.count >= AI_SITE_START_RAGE_CLICKS && !burst.reported) {\n burst.reported = true\n send('site_start_rage_click', { step, area: click.area, clicks: burst.count })\n }\n } else {\n burst = { x: click.x, y: click.y, at, count: 1, reported: false }\n }\n if (!click.interactive && deadClicks < AI_SITE_START_DEAD_CLICKS_PER_STEP) {\n deadClicks += 1\n send('site_start_dead_click', { step, area: click.area })\n }\n armIdle()\n },\n abandon(reason) {\n if (done) return\n done = true\n stopIdle()\n const at = step ?? AI_SITE_START_STEPS[0]\n send('site_start_abandoned', {\n step: at,\n step_index: aiSiteStartStepIndex(at),\n reason,\n duration_ms: spent(),\n total_ms: Math.max(0, Math.round(now() - opened)),\n })\n },\n finish() {\n done = true\n stopIdle()\n },\n dispose() {\n stopIdle()\n },\n }\n}\n\n/** What clicking acts on: a click on anything else in the form is a dead click. */\nconst INTERACTIVE = [\n 'button',\n 'a[href]',\n 'input',\n 'select',\n 'textarea',\n 'label',\n 'summary',\n '[role=\"button\"]',\n '[role=\"radio\"]',\n '[role=\"checkbox\"]',\n '[role=\"switch\"]',\n '[role=\"option\"]',\n '[role=\"tab\"]',\n '[role=\"link\"]',\n '[role=\"combobox\"]',\n '[role=\"menuitem\"]',\n '[role=\"slider\"]',\n '[contenteditable=\"true\"]',\n '.MuiInputBase-root',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(', ')\n\n/** The attribute naming the part of the form an element is in (a closed id, never content). */\nexport const AI_SITE_START_AREA_ATTRIBUTE = 'data-start-area'\n\n/** A DOM click, as the detector reads it. */\nexport function aiSiteStartClickOf(event: { clientX: number; clientY: number; target: EventTarget | null }): AiSiteStartClick {\n const target = event.target instanceof Element ? event.target : null\n const area = target?.closest(`[${AI_SITE_START_AREA_ATTRIBUTE}]`)?.getAttribute(AI_SITE_START_AREA_ATTRIBUTE) ?? 'form'\n return { x: event.clientX, y: event.clientY, interactive: Boolean(target?.closest(INTERACTIVE)), area }\n}\n\n/**\n * The tracker for one open guided start: views each step as it is drawn, and\n * reports the start abandoned when the tab goes away or the page changes\n * under it. A start that is finished ({@link AiSiteStartTracker.finish}) or\n * already abandoned reports nothing more.\n *\n * The unmount report waits a tick, so a development double mount (React's\n * StrictMode runs an effect's cleanup and then the effect again) is not read\n * as the person leaving.\n */\nexport function useAiSiteStartTracking(step: AiSiteStartStep | null, active: boolean): AiSiteStartTracker {\n const ref = useRef<AiSiteStartTracker | null>(null)\n if (!ref.current) ref.current = createAiSiteStartTracker()\n const tracker = ref.current\n const leaving = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n useEffect(() => {\n if (active && step) tracker.view(step)\n }, [active, step, tracker])\n\n useEffect(() => {\n if (!active) return\n if (leaving.current) clearTimeout(leaving.current)\n leaving.current = null\n const onPageHide = () => tracker.abandon('unload')\n window.addEventListener('pagehide', onPageHide)\n return () => {\n window.removeEventListener('pagehide', onPageHide)\n leaving.current = setTimeout(() => {\n tracker.abandon('route')\n tracker.dispose()\n }, 0)\n }\n }, [active, tracker])\n\n return tracker\n}\n"],"names":["trackEvent","useEffect","useRef","AI_SITE_START_STEPS","aiSiteStartStepIndex","AI_SITE_START_IDLE_MS","AI_SITE_START_RAGE_CLICKS","AI_SITE_START_RAGE_WINDOW_MS","AI_SITE_START_RAGE_RADIUS_PX","AI_SITE_START_STRUGGLE_EDITS","AI_SITE_START_DEAD_CLICKS_PER_STEP","createAiSiteStartTracker","options","send","name","params","now","Date","hidden","document","visibilityState","opened","step","enteredAt","done","idleTimer","idleReported","deadClicks","burst","edits","Map","dirty","Set","struggled","index","spent","Math","max","round","stopIdle","clearTimeout","armIdle","setTimeout","signal","duration_ms","edited","field","count","get","set","has","add","view","next","step_index","step_count","length","complete","back","fieldError","code","error","fieldChanged","fieldLeft","delete","fieldPicked","activity","click","at","near","hypot","x","y","reported","area","clicks","interactive","abandon","reason","total_ms","finish","dispose","INTERACTIVE","join","AI_SITE_START_AREA_ATTRIBUTE","aiSiteStartClickOf","event","target","Element","closest","getAttribute","clientX","clientY","Boolean","useAiSiteStartTracking","active","ref","current","tracker","leaving","onPageHide","window","addEventListener","removeEventListener"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { OptionCardImage } from '@aglyn/shared-ui-jsx/components/option-card-grid.component';
18
+ /** What each picture shows, as its alt text: the example page and what leads it. */
19
+ export declare const AI_SITE_STYLE_PREVIEW_ALTS: Readonly<Record<string, string>>;
20
+ /** The width and height every preview is drawn at. */
21
+ export declare const AI_SITE_STYLE_PREVIEW_SIZE: {
22
+ readonly width: 800;
23
+ readonly height: 800;
24
+ };
25
+ /** The style card picture for a kind of site, by its `AI_SITE_KINDS` id; `null` for a kind with none. */
26
+ export declare function aiSiteStylePreview(kindId: string): OptionCardImage | null;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { AI_SITE_KINDS } from "../model/ai-site-kinds.js";
18
+ import businessPreview from "../assets/site-styles/business.webp";
19
+ import tradesPreview from "../assets/site-styles/trades.webp";
20
+ import professionalPreview from "../assets/site-styles/professional.webp";
21
+ import wellnessPreview from "../assets/site-styles/wellness.webp";
22
+ import restaurantPreview from "../assets/site-styles/restaurant.webp";
23
+ import storePreview from "../assets/site-styles/store.webp";
24
+ import portfolioPreview from "../assets/site-styles/portfolio.webp";
25
+ import studioPreview from "../assets/site-styles/studio.webp";
26
+ import photographyPreview from "../assets/site-styles/photography.webp";
27
+ import blogPreview from "../assets/site-styles/blog.webp";
28
+ import eventsPreview from "../assets/site-styles/events.webp";
29
+ import fitnessPreview from "../assets/site-styles/fitness.webp";
30
+ import yogaPreview from "../assets/site-styles/yoga.webp";
31
+ import beautyPreview from "../assets/site-styles/beauty.webp";
32
+ import realestatePreview from "../assets/site-styles/realestate.webp";
33
+ import educationPreview from "../assets/site-styles/education.webp";
34
+ import nonprofitPreview from "../assets/site-styles/nonprofit.webp";
35
+ import musicPreview from "../assets/site-styles/music.webp";
36
+ import personalPreview from "../assets/site-styles/personal.webp";
37
+ import landingPreview from "../assets/site-styles/landing.webp";
38
+ import comingSoonPreview from "../assets/site-styles/coming-soon.webp";
39
+ /**
40
+ * The picture on each style card of the guided start (AGL-3898): an example
41
+ * home page of that kind of site, in its own shape — a café's menu, a
42
+ * store's products, a photographer's gallery, a band's tour dates — drawn in
43
+ * the kind's look by `libs/plugins/ai/scripts/render-site-style-previews.mts`
44
+ * from the stand-in pages in `site-style-previews.json`, with the real
45
+ * compiler and node renderer and the starter photos. Each is 800 × 800;
46
+ * regenerate them all with that script when a kind's look changes.
47
+ */ const PREVIEWS = {
48
+ business: businessPreview,
49
+ trades: tradesPreview,
50
+ professional: professionalPreview,
51
+ wellness: wellnessPreview,
52
+ restaurant: restaurantPreview,
53
+ store: storePreview,
54
+ portfolio: portfolioPreview,
55
+ studio: studioPreview,
56
+ photography: photographyPreview,
57
+ blog: blogPreview,
58
+ events: eventsPreview,
59
+ fitness: fitnessPreview,
60
+ yoga: yogaPreview,
61
+ beauty: beautyPreview,
62
+ realestate: realestatePreview,
63
+ education: educationPreview,
64
+ nonprofit: nonprofitPreview,
65
+ music: musicPreview,
66
+ personal: personalPreview,
67
+ landing: landingPreview,
68
+ 'coming-soon': comingSoonPreview
69
+ };
70
+ /** What each picture shows, as its alt text: the example page and what leads it. */ export const AI_SITE_STYLE_PREVIEW_ALTS = {
71
+ business: "A cleaning company's home page: its heading beside a photo of a tidy desk, then its services",
72
+ trades: "A roofer's home page: a bold heading on a dark band, then roofing services as cards",
73
+ professional: "A consulting firm's home page: its heading beside a photo of a team at a planning board",
74
+ wellness: "A therapy practice's home page: its heading over a plant-filled room, then a list of sessions",
75
+ restaurant: "A café's home page: its heading over a photo of the owner at the counter, then menu highlights",
76
+ store: "A handmade goods shop's home page: its heading beside a photo of a workbench, then featured products",
77
+ portfolio: "A prop stylist's home page: a large heading over a grid of three styled sets",
78
+ studio: "A design studio's home page: a large heading on a dark band, then its selected work",
79
+ photography: "A photographer's home page: its heading on black over a gallery of three photos",
80
+ blog: "A food journal's home page: its heading on a dark band, then a list of the latest posts",
81
+ events: "A festival's home page: its heading beside the three days' program on a colored band",
82
+ fitness: "A gym's home page: a bold heading on a dark band, then this week's class times",
83
+ yoga: "A yoga studio's home page: its heading beside this week's class schedule",
84
+ beauty: "A hair salon's home page: its heading beside a photo of roses and jars at a styling station",
85
+ realestate: "A real estate agent's home page: its heading over three listings with photos",
86
+ education: "A tutoring center's home page: its heading over a photo of a tutor at the board, then its courses",
87
+ nonprofit: "A food bank's home page: its heading on a dark band, then the ways to help",
88
+ music: "A band's home page: its name on a dark band, then its tour dates",
89
+ personal: "A product manager's personal page: her introduction beside a photo of her with a teammate",
90
+ landing: "A budgeting app's landing page: its pitch and sign-up button on a dark band, then its benefits",
91
+ 'coming-soon': "A café's coming soon page: its heading and notify button over a photo of the café shelves"
92
+ };
93
+ /** The width and height every preview is drawn at. */ export const AI_SITE_STYLE_PREVIEW_SIZE = {
94
+ width: 800,
95
+ height: 800
96
+ };
97
+ /** A bundler hands an imported picture over as its URL, or as an object carrying it. */ function urlOf(imported) {
98
+ if (typeof imported === 'string') return imported;
99
+ if (imported && typeof imported === 'object' && typeof imported.src === 'string') {
100
+ return imported.src;
101
+ }
102
+ return null;
103
+ }
104
+ /** The style card picture for a kind of site, by its `AI_SITE_KINDS` id; `null` for a kind with none. */ export function aiSiteStylePreview(kindId) {
105
+ var _AI_SITE_STYLE_PREVIEW_ALTS_kindId;
106
+ const src = urlOf(PREVIEWS[kindId]);
107
+ const kind = AI_SITE_KINDS.find((entry)=>entry.id === kindId);
108
+ if (!src || !kind) return null;
109
+ return _extends({
110
+ src,
111
+ alt: (_AI_SITE_STYLE_PREVIEW_ALTS_kindId = AI_SITE_STYLE_PREVIEW_ALTS[kindId]) != null ? _AI_SITE_STYLE_PREVIEW_ALTS_kindId : `An example home page in the ${kind.label} style`
112
+ }, AI_SITE_STYLE_PREVIEW_SIZE);
113
+ }
114
+
115
+ //# sourceMappingURL=ai-site-style-previews.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-site-style-previews.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 { OptionCardImage } from '@aglyn/shared-ui-jsx/components/option-card-grid.component'\nimport { AI_SITE_KINDS } from '../model/ai-site-kinds'\nimport businessPreview from '../assets/site-styles/business.webp'\nimport tradesPreview from '../assets/site-styles/trades.webp'\nimport professionalPreview from '../assets/site-styles/professional.webp'\nimport wellnessPreview from '../assets/site-styles/wellness.webp'\nimport restaurantPreview from '../assets/site-styles/restaurant.webp'\nimport storePreview from '../assets/site-styles/store.webp'\nimport portfolioPreview from '../assets/site-styles/portfolio.webp'\nimport studioPreview from '../assets/site-styles/studio.webp'\nimport photographyPreview from '../assets/site-styles/photography.webp'\nimport blogPreview from '../assets/site-styles/blog.webp'\nimport eventsPreview from '../assets/site-styles/events.webp'\nimport fitnessPreview from '../assets/site-styles/fitness.webp'\nimport yogaPreview from '../assets/site-styles/yoga.webp'\nimport beautyPreview from '../assets/site-styles/beauty.webp'\nimport realestatePreview from '../assets/site-styles/realestate.webp'\nimport educationPreview from '../assets/site-styles/education.webp'\nimport nonprofitPreview from '../assets/site-styles/nonprofit.webp'\nimport musicPreview from '../assets/site-styles/music.webp'\nimport personalPreview from '../assets/site-styles/personal.webp'\nimport landingPreview from '../assets/site-styles/landing.webp'\nimport comingSoonPreview from '../assets/site-styles/coming-soon.webp'\n\n/**\n * The picture on each style card of the guided start (AGL-3898): an example\n * home page of that kind of site, in its own shape — a café's menu, a\n * store's products, a photographer's gallery, a band's tour dates — drawn in\n * the kind's look by `libs/plugins/ai/scripts/render-site-style-previews.mts`\n * from the stand-in pages in `site-style-previews.json`, with the real\n * compiler and node renderer and the starter photos. Each is 800 × 800;\n * regenerate them all with that script when a kind's look changes.\n */\nconst PREVIEWS: Readonly<Record<string, unknown>> = {\n business: businessPreview,\n trades: tradesPreview,\n professional: professionalPreview,\n wellness: wellnessPreview,\n restaurant: restaurantPreview,\n store: storePreview,\n portfolio: portfolioPreview,\n studio: studioPreview,\n photography: photographyPreview,\n blog: blogPreview,\n events: eventsPreview,\n fitness: fitnessPreview,\n yoga: yogaPreview,\n beauty: beautyPreview,\n realestate: realestatePreview,\n education: educationPreview,\n nonprofit: nonprofitPreview,\n music: musicPreview,\n personal: personalPreview,\n landing: landingPreview,\n 'coming-soon': comingSoonPreview,\n}\n\n/** What each picture shows, as its alt text: the example page and what leads it. */\nexport const AI_SITE_STYLE_PREVIEW_ALTS: Readonly<Record<string, string>> = {\n business: \"A cleaning company's home page: its heading beside a photo of a tidy desk, then its services\",\n trades: \"A roofer's home page: a bold heading on a dark band, then roofing services as cards\",\n professional: \"A consulting firm's home page: its heading beside a photo of a team at a planning board\",\n wellness: \"A therapy practice's home page: its heading over a plant-filled room, then a list of sessions\",\n restaurant: \"A café's home page: its heading over a photo of the owner at the counter, then menu highlights\",\n store: \"A handmade goods shop's home page: its heading beside a photo of a workbench, then featured products\",\n portfolio: \"A prop stylist's home page: a large heading over a grid of three styled sets\",\n studio: \"A design studio's home page: a large heading on a dark band, then its selected work\",\n photography: \"A photographer's home page: its heading on black over a gallery of three photos\",\n blog: \"A food journal's home page: its heading on a dark band, then a list of the latest posts\",\n events: \"A festival's home page: its heading beside the three days' program on a colored band\",\n fitness: \"A gym's home page: a bold heading on a dark band, then this week's class times\",\n yoga: \"A yoga studio's home page: its heading beside this week's class schedule\",\n beauty: \"A hair salon's home page: its heading beside a photo of roses and jars at a styling station\",\n realestate: \"A real estate agent's home page: its heading over three listings with photos\",\n education: \"A tutoring center's home page: its heading over a photo of a tutor at the board, then its courses\",\n nonprofit: \"A food bank's home page: its heading on a dark band, then the ways to help\",\n music: \"A band's home page: its name on a dark band, then its tour dates\",\n personal: \"A product manager's personal page: her introduction beside a photo of her with a teammate\",\n landing: \"A budgeting app's landing page: its pitch and sign-up button on a dark band, then its benefits\",\n 'coming-soon': \"A café's coming soon page: its heading and notify button over a photo of the café shelves\",\n}\n\n/** The width and height every preview is drawn at. */\nexport const AI_SITE_STYLE_PREVIEW_SIZE = { width: 800, height: 800 } as const\n\n/** A bundler hands an imported picture over as its URL, or as an object carrying it. */\nfunction urlOf(imported: unknown): string | null {\n if (typeof imported === 'string') return imported\n if (imported && typeof imported === 'object' && typeof (imported as { src?: unknown }).src === 'string') {\n return (imported as { src: string }).src\n }\n return null\n}\n\n/** The style card picture for a kind of site, by its `AI_SITE_KINDS` id; `null` for a kind with none. */\nexport function aiSiteStylePreview(kindId: string): OptionCardImage | null {\n const src = urlOf(PREVIEWS[kindId])\n const kind = AI_SITE_KINDS.find((entry) => entry.id === kindId)\n if (!src || !kind) return null\n return {\n src,\n alt: AI_SITE_STYLE_PREVIEW_ALTS[kindId] ?? `An example home page in the ${kind.label} style`,\n ...AI_SITE_STYLE_PREVIEW_SIZE,\n }\n}\n"],"names":["AI_SITE_KINDS","businessPreview","tradesPreview","professionalPreview","wellnessPreview","restaurantPreview","storePreview","portfolioPreview","studioPreview","photographyPreview","blogPreview","eventsPreview","fitnessPreview","yogaPreview","beautyPreview","realestatePreview","educationPreview","nonprofitPreview","musicPreview","personalPreview","landingPreview","comingSoonPreview","PREVIEWS","business","trades","professional","wellness","restaurant","store","portfolio","studio","photography","blog","events","fitness","yoga","beauty","realestate","education","nonprofit","music","personal","landing","AI_SITE_STYLE_PREVIEW_ALTS","AI_SITE_STYLE_PREVIEW_SIZE","width","height","urlOf","imported","src","aiSiteStylePreview","kindId","kind","find","entry","id","alt","label"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,aAAa,QAAQ,4BAAwB;AACtD,OAAOC,qBAAqB,sCAAqC;AACjE,OAAOC,mBAAmB,oCAAmC;AAC7D,OAAOC,yBAAyB,0CAAyC;AACzE,OAAOC,qBAAqB,sCAAqC;AACjE,OAAOC,uBAAuB,wCAAuC;AACrE,OAAOC,kBAAkB,mCAAkC;AAC3D,OAAOC,sBAAsB,uCAAsC;AACnE,OAAOC,mBAAmB,oCAAmC;AAC7D,OAAOC,wBAAwB,yCAAwC;AACvE,OAAOC,iBAAiB,kCAAiC;AACzD,OAAOC,mBAAmB,oCAAmC;AAC7D,OAAOC,oBAAoB,qCAAoC;AAC/D,OAAOC,iBAAiB,kCAAiC;AACzD,OAAOC,mBAAmB,oCAAmC;AAC7D,OAAOC,uBAAuB,wCAAuC;AACrE,OAAOC,sBAAsB,uCAAsC;AACnE,OAAOC,sBAAsB,uCAAsC;AACnE,OAAOC,kBAAkB,mCAAkC;AAC3D,OAAOC,qBAAqB,sCAAqC;AACjE,OAAOC,oBAAoB,qCAAoC;AAC/D,OAAOC,uBAAuB,yCAAwC;AAEtE;;;;;;;;CAQC,GACD,MAAMC,WAA8C;IAClDC,UAAUtB;IACVuB,QAAQtB;IACRuB,cAActB;IACduB,UAAUtB;IACVuB,YAAYtB;IACZuB,OAAOtB;IACPuB,WAAWtB;IACXuB,QAAQtB;IACRuB,aAAatB;IACbuB,MAAMtB;IACNuB,QAAQtB;IACRuB,SAAStB;IACTuB,MAAMtB;IACNuB,QAAQtB;IACRuB,YAAYtB;IACZuB,WAAWtB;IACXuB,WAAWtB;IACXuB,OAAOtB;IACPuB,UAAUtB;IACVuB,SAAStB;IACT,eAAeC;AACjB;AAEA,kFAAkF,GAClF,OAAO,MAAMsB,6BAA+D;IAC1EpB,UAAU;IACVC,QAAQ;IACRC,cAAc;IACdC,UAAU;IACVC,YAAY;IACZC,OAAO;IACPC,WAAW;IACXC,QAAQ;IACRC,aAAa;IACbC,MAAM;IACNC,QAAQ;IACRC,SAAS;IACTC,MAAM;IACNC,QAAQ;IACRC,YAAY;IACZC,WAAW;IACXC,WAAW;IACXC,OAAO;IACPC,UAAU;IACVC,SAAS;IACT,eAAe;AACjB,EAAC;AAED,oDAAoD,GACpD,OAAO,MAAME,6BAA6B;IAAEC,OAAO;IAAKC,QAAQ;AAAI,EAAU;AAE9E,sFAAsF,GACtF,SAASC,MAAMC,QAAiB;IAC9B,IAAI,OAAOA,aAAa,UAAU,OAAOA;IACzC,IAAIA,YAAY,OAAOA,aAAa,YAAY,OAAO,AAACA,SAA+BC,GAAG,KAAK,UAAU;QACvG,OAAO,AAACD,SAA6BC,GAAG;IAC1C;IACA,OAAO;AACT;AAEA,uGAAuG,GACvG,OAAO,SAASC,mBAAmBC,MAAc;QAMxCR;IALP,MAAMM,MAAMF,MAAMzB,QAAQ,CAAC6B,OAAO;IAClC,MAAMC,OAAOpD,cAAcqD,IAAI,CAAC,CAACC,QAAUA,MAAMC,EAAE,KAAKJ;IACxD,IAAI,CAACF,OAAO,CAACG,MAAM,OAAO;IAC1B,OAAO;QACLH;QACAO,GAAG,GAAEb,qCAAAA,0BAA0B,CAACQ,OAAO,YAAlCR,qCAAsC,CAAC,4BAA4B,EAAES,KAAKK,KAAK,CAAC,MAAM,CAAC;OACzFb;AAEP"}
@@ -126,6 +126,8 @@ export interface AiInsightDigestDeps {
126
126
  /** The site the digest is about, and the org whose brand it wears (AGL-3367). */
127
127
  siteName: string;
128
128
  org: Record<string, unknown>;
129
+ /** The designed email's merge values, from {@link aiInsightDigestEmailParts}. */
130
+ merge?: Record<string, string>;
129
131
  }) => Promise<AiInsightDigestSendResult>;
130
132
  /** The console's absolute origin for an email link, or `''` when none is configured. */
131
133
  consoleOrigin: string;
@@ -167,6 +169,13 @@ export declare function aiInsightDigestEmailText(input: {
167
169
  /** The week the figures cover, "Oct 5 – Oct 11"; absent says "this week". */
168
170
  periodLabel?: string;
169
171
  }): string;
172
+ /**
173
+ * The same digest as {@link aiInsightDigestEmailText}, cut into the parts the
174
+ * `ai-insights-digest` system email draws: the workspace and week above the
175
+ * heading, the insights as a numbered list, a button to the site's
176
+ * Analytics page, and why it came.
177
+ */
178
+ export declare function aiInsightDigestEmailParts(input: Parameters<typeof aiInsightDigestEmailText>[0]): Record<'digest.eyebrow' | 'digest.intro' | 'digest.insights' | 'digest.analyticsUrl' | 'digest.why', string>;
170
179
  /**
171
180
  * One call of the sweep: delivery for this week and the last on the first
172
181
  * call, then — on a Monday — one chunk of workspaces' creation, resumed by
@@ -66,6 +66,28 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
66
66
  input.settingsUrl ? `You asked for weekly insights for ${forWhom}. Turn them off in Notifications: ${input.settingsUrl}` : `You asked for weekly insights for ${forWhom}. Turn them off in your Notifications settings.`
67
67
  ].join('\n');
68
68
  }
69
+ /**
70
+ * The same digest as {@link aiInsightDigestEmailText}, cut into the parts the
71
+ * `ai-insights-digest` system email draws: the workspace and week above the
72
+ * heading, the insights as a numbered list, a button to the site's
73
+ * Analytics page, and why it came.
74
+ */ export function aiInsightDigestEmailParts(input) {
75
+ var _input_analyticsUrl;
76
+ var _input_workspaceName, _input_periodLabel;
77
+ const workspace = ((_input_workspaceName = input.workspaceName) == null ? void 0 : _input_workspaceName.trim()) || '';
78
+ const credits = Math.max(0, Math.round(Number(input.creditsSpent) || 0));
79
+ const forWhom = workspace || 'this workspace';
80
+ return {
81
+ 'digest.eyebrow': [
82
+ workspace,
83
+ (_input_periodLabel = input.periodLabel) == null ? void 0 : _input_periodLabel.trim()
84
+ ].filter(Boolean).join(' · '),
85
+ 'digest.intro': `What ${input.siteName}'s figures showed ${input.periodLabel ? 'last week' : 'this week'}. ` + 'Each one is traced to the figures it cites, so you can check it on the Analytics page.',
86
+ 'digest.insights': input.insights.map((insight, index)=>`${index + 1}. ${insight.text}`).join('\n\n'),
87
+ 'digest.analyticsUrl': (_input_analyticsUrl = input.analyticsUrl) != null ? _input_analyticsUrl : '',
88
+ 'digest.why': `You asked for weekly insights for ${forWhom}.` + (credits ? ` Writing these used ${credits.toLocaleString('en-US')} of ${workspace ? `${workspace}’s` : 'the workspace’s'} AI credits.` : '') + (input.settingsUrl ? ` Turn them off in Notifications: ${input.settingsUrl}` : ' Turn them off in your Notifications settings.')
89
+ };
90
+ }
69
91
  async function readUsers(firestore, uids) {
70
92
  if (!uids.length) return new Map();
71
93
  const docs = await firestore.getAll(...uids.map((uid)=>firestore.collection('users').doc(uid)));
@@ -232,22 +254,24 @@ async function readUsers(firestore, uids) {
232
254
  }
233
255
  const address = str((_member_email = member.email) != null ? _member_email : user == null ? void 0 : user.get('email')).toLowerCase();
234
256
  if (address.includes('@')) {
257
+ const emailInput = {
258
+ siteName: site.name,
259
+ productName: branding.productName,
260
+ insights: record.insights,
261
+ analyticsUrl,
262
+ settingsUrl: origin ? `${origin}/manage/notifications` : null,
263
+ workspaceName: str(org['name']),
264
+ creditsSpent: job.creditsSpent,
265
+ periodLabel: aiInsightDigestPeriod(job.inputs, new Date(Number(marker.createdAtMs) || now.getTime())).label
266
+ };
235
267
  const sent = await deps.send({
236
268
  to: address,
237
269
  subject: `Your weekly insights for ${site.name}`,
238
- text: aiInsightDigestEmailText({
239
- siteName: site.name,
240
- productName: branding.productName,
241
- insights: record.insights,
242
- analyticsUrl,
243
- settingsUrl: origin ? `${origin}/manage/notifications` : null,
244
- workspaceName: str(org['name']),
245
- creditsSpent: job.creditsSpent,
246
- periodLabel: aiInsightDigestPeriod(job.inputs, new Date(Number(marker.createdAtMs) || now.getTime())).label
247
- }),
270
+ text: aiInsightDigestEmailText(emailInput),
248
271
  fromName: branding.fromName,
249
272
  siteName: site.name,
250
- org
273
+ org,
274
+ merge: aiInsightDigestEmailParts(emailInput)
251
275
  });
252
276
  if (sent.rateLimited) {
253
277
  // Nothing is marked, so the next run delivers this site whole.