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

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 (124) hide show
  1. package/package.json +18 -18
  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-job-site.d.ts +8 -1
  24. package/src/lib/components/ai-job-site.js +7 -0
  25. package/src/lib/components/ai-job-site.js.map +1 -1
  26. package/src/lib/components/ai-site-build-page.component.js +37 -0
  27. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  28. package/src/lib/components/ai-site-start-card.component.d.ts +12 -0
  29. package/src/lib/components/ai-site-start-card.component.js +279 -240
  30. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  31. package/src/lib/components/ai-site-start-steps.component.d.ts +114 -0
  32. package/src/lib/components/ai-site-start-steps.component.js +480 -0
  33. package/src/lib/components/ai-site-start-steps.component.js.map +1 -0
  34. package/src/lib/components/ai-site-start-tracking.d.ts +122 -0
  35. package/src/lib/components/ai-site-start-tracking.js +296 -0
  36. package/src/lib/components/ai-site-start-tracking.js.map +1 -0
  37. package/src/lib/components/ai-site-style-previews.d.ts +26 -0
  38. package/src/lib/components/ai-site-style-previews.js +115 -0
  39. package/src/lib/components/ai-site-style-previews.js.map +1 -0
  40. package/src/lib/components/assist-jobs-drawer.component.js +24 -2
  41. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  42. package/src/lib/insights/ai-insight-digest.d.ts +9 -0
  43. package/src/lib/insights/ai-insight-digest.js +35 -11
  44. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  45. package/src/lib/jobs/ai-empty-site-sweep.d.ts +154 -0
  46. package/src/lib/jobs/ai-empty-site-sweep.js +346 -0
  47. package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -0
  48. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  49. package/src/lib/jobs/ai-job-page-language.js +20 -2
  50. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  51. package/src/lib/jobs/ai-job-plan-step.d.ts +43 -8
  52. package/src/lib/jobs/ai-job-plan-step.js +99 -18
  53. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-site-datasets.d.ts +82 -1
  55. package/src/lib/jobs/ai-job-site-datasets.js +276 -14
  56. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  57. package/src/lib/jobs/ai-jobs-beat.d.ts +11 -3
  58. package/src/lib/jobs/ai-jobs-beat.js +23 -6
  59. package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
  60. package/src/lib/jobs/ai-jobs.js +46 -7
  61. package/src/lib/jobs/ai-jobs.js.map +1 -1
  62. package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -10
  63. package/src/lib/jobs/ai-layout-stock-photos.js +1046 -102
  64. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  65. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.d.ts +58 -0
  66. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js +678 -0
  67. package/src/lib/jobs/fixtures/ai-site-foreign-datasets-recording.js.map +1 -0
  68. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.d.ts +33 -0
  69. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js +270 -0
  70. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-beta265-recordings.js.map +1 -0
  71. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.d.ts +33 -0
  72. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js +277 -0
  73. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-people-recordings.js.map +1 -0
  74. package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.d.ts +35 -0
  75. package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.js +379 -0
  76. package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.js.map +1 -0
  77. package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.d.ts +31 -0
  78. package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.js +208 -0
  79. package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.js.map +1 -0
  80. package/src/lib/layout-language/ai-layout-compiler.js +49 -12
  81. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  82. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  83. package/src/lib/layout-language/ai-layout-design.js +19 -13
  84. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  85. package/src/lib/layout-language/ai-layout-listings.d.ts +48 -0
  86. package/src/lib/layout-language/ai-layout-listings.js +91 -5
  87. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  88. package/src/lib/layout-language/ai-layout-pictures.d.ts +15 -0
  89. package/src/lib/layout-language/ai-layout-pictures.js +6 -2
  90. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  91. package/src/lib/model/ai-jobs.types.d.ts +20 -0
  92. package/src/lib/model/ai-jobs.types.js.map +1 -1
  93. package/src/lib/model/ai-site-home-arcs.d.ts +75 -0
  94. package/src/lib/model/ai-site-home-arcs.js +741 -0
  95. package/src/lib/model/ai-site-home-arcs.js.map +1 -0
  96. package/src/lib/model/ai-site-inventory.d.ts +18 -0
  97. package/src/lib/model/ai-site-inventory.js.map +1 -1
  98. package/src/lib/model/ai-site-job.d.ts +9 -6
  99. package/src/lib/model/ai-site-job.js +8 -5
  100. package/src/lib/model/ai-site-job.js.map +1 -1
  101. package/src/lib/model/ai-site-kinds.js +24 -24
  102. package/src/lib/model/ai-site-kinds.js.map +1 -1
  103. package/src/lib/model/ai-site-start-handoff.d.ts +49 -0
  104. package/src/lib/model/ai-site-start-handoff.js +88 -0
  105. package/src/lib/model/ai-site-start-handoff.js.map +1 -0
  106. package/src/lib/model/ai-site-start-steps.d.ts +67 -0
  107. package/src/lib/model/ai-site-start-steps.js +127 -0
  108. package/src/lib/model/ai-site-start-steps.js.map +1 -0
  109. package/src/lib/model/ai-site-start.js +4 -1
  110. package/src/lib/model/ai-site-start.js.map +1 -1
  111. package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -0
  112. package/src/lib/runtime/ai-doctrine-validators.js +119 -2
  113. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  114. package/src/lib/runtime/ai-doctrine.d.ts +6 -1
  115. package/src/lib/runtime/ai-doctrine.js +9 -5
  116. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  117. package/src/lib/runtime/ai-palette.generated.js +5 -2
  118. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  119. package/src/lib/runtime/site-inventory.js +6 -0
  120. package/src/lib/runtime/site-inventory.js.map +1 -1
  121. package/src/lib/server/ai-insight-digest-route.js +2 -2
  122. package/src/lib/server/ai-insight-digest-route.js.map +1 -1
  123. package/src/lib/usage/assist-free-taste.js +3 -0
  124. package/src/lib/usage/assist-free-taste.js.map +1 -1
@@ -0,0 +1,296 @@
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 words, a
26
+ * picture or an icon in the form that do nothing (dead). A click on empty
27
+ * space or on a popup's backdrop is not a dead click (2026-10-18).
28
+ *
29
+ * Every event goes through `trackEvent`, the one door the console's GA4
30
+ * taxonomy has, so it is consent-gated exactly as every other console event
31
+ * is (`firebase-app.layout.tsx` registers the transport, or the refusal that
32
+ * drops it) and its params are sanitized there too. What is sent is only ever
33
+ * a step id, a field id, a closed error code, a closed area id, a count or a
34
+ * duration: never anything typed, picked or named.
35
+ *
36
+ * The detector is a plain object over a clock and a timer so its rules are
37
+ * tested with fake timers; {@link useAiSiteStartTracking} binds it to the
38
+ * dialog.
39
+ */ /** No input on a step for this long is a struggle. */ export const AI_SITE_START_IDLE_MS = 30000;
40
+ /** This many clicks on one spot… */ export const AI_SITE_START_RAGE_CLICKS = 3;
41
+ /** …within this long… */ export const AI_SITE_START_RAGE_WINDOW_MS = 700;
42
+ /** …and within this distance of the first, is rage. */ export const AI_SITE_START_RAGE_RADIUS_PX = 30;
43
+ /** One field changed this many times is a struggle. */ export const AI_SITE_START_STRUGGLE_EDITS = 3;
44
+ /** At most this many dead clicks are reported on one visit to a step. */ export const AI_SITE_START_DEAD_CLICKS_PER_STEP = 3;
45
+ export function createAiSiteStartTracker(options = {}) {
46
+ var _options_send, _options_now, _options_hidden;
47
+ const send = (_options_send = options.send) != null ? _options_send : (name, params)=>trackEvent(name, params);
48
+ const now = (_options_now = options.now) != null ? _options_now : ()=>Date.now();
49
+ const hidden = (_options_hidden = options.hidden) != null ? _options_hidden : ()=>typeof document !== 'undefined' && document.visibilityState === 'hidden';
50
+ const opened = now();
51
+ let step = null;
52
+ let enteredAt = opened;
53
+ let done = false;
54
+ let idleTimer = null;
55
+ let idleReported = false;
56
+ let deadClicks = 0;
57
+ let burst = null;
58
+ const edits = new Map();
59
+ const dirty = new Set();
60
+ const struggled = new Set();
61
+ const index = ()=>step ? aiSiteStartStepIndex(step) : 0;
62
+ const spent = ()=>Math.max(0, Math.round(now() - enteredAt));
63
+ const stopIdle = ()=>{
64
+ if (idleTimer) clearTimeout(idleTimer);
65
+ idleTimer = null;
66
+ };
67
+ const armIdle = ()=>{
68
+ stopIdle();
69
+ if (done || !step || idleReported) return;
70
+ idleTimer = setTimeout(()=>{
71
+ idleTimer = null;
72
+ if (done || !step || idleReported || hidden()) return;
73
+ idleReported = true;
74
+ send('site_start_struggle', {
75
+ step,
76
+ signal: 'idle',
77
+ duration_ms: AI_SITE_START_IDLE_MS
78
+ });
79
+ }, AI_SITE_START_IDLE_MS);
80
+ };
81
+ const edited = (field)=>{
82
+ var _edits_get;
83
+ if (!step) return;
84
+ const count = ((_edits_get = edits.get(field)) != null ? _edits_get : 0) + 1;
85
+ edits.set(field, count);
86
+ if (count >= AI_SITE_START_STRUGGLE_EDITS && !struggled.has(field)) {
87
+ struggled.add(field);
88
+ send('site_start_struggle', {
89
+ step,
90
+ signal: 'edits',
91
+ field,
92
+ count
93
+ });
94
+ }
95
+ };
96
+ return {
97
+ view (next) {
98
+ if (done) return;
99
+ step = next;
100
+ enteredAt = now();
101
+ idleReported = false;
102
+ deadClicks = 0;
103
+ burst = null;
104
+ send('site_start_step_viewed', {
105
+ step: next,
106
+ step_index: aiSiteStartStepIndex(next),
107
+ step_count: AI_SITE_START_STEPS.length
108
+ });
109
+ armIdle();
110
+ },
111
+ complete () {
112
+ if (done || !step) return;
113
+ send('site_start_step_completed', {
114
+ step,
115
+ step_index: index(),
116
+ duration_ms: spent()
117
+ });
118
+ },
119
+ back () {
120
+ if (done || !step) return;
121
+ send('site_start_step_back', {
122
+ step,
123
+ step_index: index(),
124
+ duration_ms: spent()
125
+ });
126
+ },
127
+ fieldError (field, code) {
128
+ if (done || !step) return;
129
+ send('site_start_field_error', {
130
+ step,
131
+ field,
132
+ error: code
133
+ });
134
+ },
135
+ fieldChanged (field) {
136
+ dirty.add(field);
137
+ },
138
+ fieldLeft (field) {
139
+ if (!dirty.delete(field)) return;
140
+ edited(field);
141
+ },
142
+ fieldPicked () {
143
+ // Picking card after card is how a person compares styles or kinds; it
144
+ // read as "one answer changed again and again" (2026-10-18) and is not.
145
+ // Only a typed answer rewritten is a struggle.
146
+ armIdle();
147
+ },
148
+ activity () {
149
+ armIdle();
150
+ },
151
+ click (click) {
152
+ if (done || !step) return;
153
+ const at = now();
154
+ 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;
155
+ if (near && burst) {
156
+ burst.count += 1;
157
+ if (burst.count >= AI_SITE_START_RAGE_CLICKS && !burst.reported) {
158
+ burst.reported = true;
159
+ send('site_start_rage_click', {
160
+ step,
161
+ area: click.area,
162
+ clicks: burst.count
163
+ });
164
+ }
165
+ } else {
166
+ burst = {
167
+ x: click.x,
168
+ y: click.y,
169
+ at,
170
+ count: 1,
171
+ reported: false
172
+ };
173
+ }
174
+ if (!click.interactive && !click.blank && deadClicks < AI_SITE_START_DEAD_CLICKS_PER_STEP) {
175
+ deadClicks += 1;
176
+ send('site_start_dead_click', {
177
+ step,
178
+ area: click.area
179
+ });
180
+ }
181
+ armIdle();
182
+ },
183
+ abandon (reason) {
184
+ if (done) return;
185
+ done = true;
186
+ stopIdle();
187
+ const at = step != null ? step : AI_SITE_START_STEPS[0];
188
+ send('site_start_abandoned', {
189
+ step: at,
190
+ step_index: aiSiteStartStepIndex(at),
191
+ reason,
192
+ duration_ms: spent(),
193
+ total_ms: Math.max(0, Math.round(now() - opened))
194
+ });
195
+ },
196
+ finish () {
197
+ done = true;
198
+ stopIdle();
199
+ },
200
+ dispose () {
201
+ stopIdle();
202
+ }
203
+ };
204
+ }
205
+ /** What clicking acts on: a click on anything else in the form is a dead click. */ const INTERACTIVE = [
206
+ 'button',
207
+ 'a[href]',
208
+ 'input',
209
+ 'select',
210
+ 'textarea',
211
+ 'label',
212
+ 'summary',
213
+ '[role="button"]',
214
+ '[role="radio"]',
215
+ '[role="checkbox"]',
216
+ '[role="switch"]',
217
+ '[role="option"]',
218
+ '[role="tab"]',
219
+ '[role="link"]',
220
+ '[role="combobox"]',
221
+ '[role="menuitem"]',
222
+ '[role="slider"]',
223
+ '[contenteditable="true"]',
224
+ '.MuiInputBase-root',
225
+ '[tabindex]:not([tabindex="-1"])'
226
+ ].join(', ');
227
+ /** 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';
228
+ /** A popup's backdrop (a menu's, a select's, a help popover's): a click on it closes the popup. */ const BACKDROP = '.MuiBackdrop-root';
229
+ /**
230
+ * Whether an element is something a person sees and might click: its own
231
+ * words, a picture or an icon. A bare layout box (the space around and
232
+ * between things) is not.
233
+ */ function isContent(element) {
234
+ if (element.closest('img, svg, picture, video, canvas')) return true;
235
+ for (const node of Array.from(element.childNodes)){
236
+ var _node_textContent;
237
+ if (node.nodeType === 3 && ((_node_textContent = node.textContent) != null ? _node_textContent : '').trim()) return true;
238
+ }
239
+ return false;
240
+ }
241
+ /** A DOM click, as the detector reads it. */ export function aiSiteStartClickOf(event) {
242
+ var _ref, _ref1;
243
+ const target = event.target instanceof Element ? event.target : null;
244
+ const named = (_ref = target == null ? void 0 : target.closest(`[${AI_SITE_START_AREA_ATTRIBUTE}]`)) != null ? _ref : null;
245
+ const area = (_ref1 = named == null ? void 0 : named.getAttribute(AI_SITE_START_AREA_ATTRIBUTE)) != null ? _ref1 : 'form';
246
+ const blank = !target || !named || Boolean(target.closest(BACKDROP)) || !isContent(target);
247
+ return {
248
+ x: event.clientX,
249
+ y: event.clientY,
250
+ interactive: Boolean(target == null ? void 0 : target.closest(INTERACTIVE)),
251
+ area,
252
+ blank
253
+ };
254
+ }
255
+ /**
256
+ * The tracker for one open guided start: views each step as it is drawn, and
257
+ * reports the start abandoned when the tab goes away or the page changes
258
+ * under it. A start that is finished ({@link AiSiteStartTracker.finish}) or
259
+ * already abandoned reports nothing more.
260
+ *
261
+ * The unmount report waits a tick, so a development double mount (React's
262
+ * StrictMode runs an effect's cleanup and then the effect again) is not read
263
+ * as the person leaving.
264
+ */ export function useAiSiteStartTracking(step, active) {
265
+ const ref = useRef(null);
266
+ if (!ref.current) ref.current = createAiSiteStartTracker();
267
+ const tracker = ref.current;
268
+ const leaving = useRef(null);
269
+ useEffect(()=>{
270
+ if (active && step) tracker.view(step);
271
+ }, [
272
+ active,
273
+ step,
274
+ tracker
275
+ ]);
276
+ useEffect(()=>{
277
+ if (!active) return;
278
+ if (leaving.current) clearTimeout(leaving.current);
279
+ leaving.current = null;
280
+ const onPageHide = ()=>tracker.abandon('unload');
281
+ window.addEventListener('pagehide', onPageHide);
282
+ return ()=>{
283
+ window.removeEventListener('pagehide', onPageHide);
284
+ leaving.current = setTimeout(()=>{
285
+ tracker.abandon('route');
286
+ tracker.dispose();
287
+ }, 0);
288
+ };
289
+ }, [
290
+ active,
291
+ tracker
292
+ ]);
293
+ return tracker;
294
+ }
295
+
296
+ //# 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 words, a\n * picture or an icon in the form that do nothing (dead). A click on empty\n * space or on a popup's backdrop is not a dead click (2026-10-18).\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 * Whether it landed on nothing a person could mistake for a control: empty\n * space, a popup's backdrop (the click that closes a menu or a help\n * popover), or anything outside the form's named parts. A blank click is\n * never a dead click.\n */\n blank?: boolean\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: input, but not an edit (comparing cards is not a struggle). */\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() {\n // Picking card after card is how a person compares styles or kinds; it\n // read as \"one answer changed again and again\" (2026-10-18) and is not.\n // Only a typed answer rewritten is a struggle.\n armIdle()\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 && !click.blank && 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 popup's backdrop (a menu's, a select's, a help popover's): a click on it closes the popup. */\nconst BACKDROP = '.MuiBackdrop-root'\n\n/**\n * Whether an element is something a person sees and might click: its own\n * words, a picture or an icon. A bare layout box (the space around and\n * between things) is not.\n */\nfunction isContent(element: Element): boolean {\n if (element.closest('img, svg, picture, video, canvas')) return true\n for (const node of Array.from(element.childNodes)) {\n if (node.nodeType === 3 && (node.textContent ?? '').trim()) return true\n }\n return false\n}\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 named = target?.closest(`[${AI_SITE_START_AREA_ATTRIBUTE}]`) ?? null\n const area = named?.getAttribute(AI_SITE_START_AREA_ATTRIBUTE) ?? 'form'\n const blank = !target || !named || Boolean(target.closest(BACKDROP)) || !isContent(target)\n return { x: event.clientX, y: event.clientY, interactive: Boolean(target?.closest(INTERACTIVE)), area, blank }\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","blank","abandon","reason","total_ms","finish","dispose","INTERACTIVE","join","AI_SITE_START_AREA_ATTRIBUTE","BACKDROP","isContent","element","closest","node","Array","from","childNodes","nodeType","textContent","trim","aiSiteStartClickOf","event","target","Element","named","getAttribute","Boolean","clientX","clientY","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"}
@@ -1,5 +1,5 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  /**
4
4
  * @license
5
5
  * Copyright 2026 Aglyn LLC
@@ -37,6 +37,7 @@ import { AiInsightDialog } from "./ai-insight-dialog.component.js";
37
37
  import { AiJobPlan } from "./ai-job-plan.component.js";
38
38
  import { aiCreditsBillingHref, aiJobOutputHref, aiJobPrimaryLink, aiSiteBuildHref } from "./ai-job-links.js";
39
39
  import { aiJobRefundCopy } from "../model/ai-job-failure-copy.js";
40
+ import { AI_SITE_REMOVED_ROW_COPY, AI_SITE_START_AGAIN_LABEL, aiCreateSiteHref, stashAiSiteStartAnswers } from "../model/ai-site-start-handoff.js";
40
41
  import { readEventFrames } from "./ai-job-events.js";
41
42
  /** Try again, with what it is likely to cost (AGL-3722). */ function aiBuildRetryLabel(job) {
42
43
  const range = aiBuildRetryCreditRange(job);
@@ -612,7 +613,28 @@ export function AssistJobsDrawer({ orgId, org, orgReady, orgSlug, user, visible,
612
613
  })
613
614
  ]
614
615
  }) : null,
615
- restartHref(job) ? /*#__PURE__*/ _jsx(Button, {
616
+ job.siteRemoved ? // Its empty site was removed (AGL-3936): Create site, with the same answers.
617
+ /*#__PURE__*/ _jsxs(_Fragment, {
618
+ children: [
619
+ /*#__PURE__*/ _jsx(Typography, {
620
+ variant: "caption",
621
+ color: "text.secondary",
622
+ component: "div",
623
+ children: AI_SITE_REMOVED_ROW_COPY
624
+ }),
625
+ aiCreateSiteHref(orgSlug) && job.siteInputs ? /*#__PURE__*/ _jsx(Button, {
626
+ size: "small",
627
+ variant: "contained",
628
+ component: AppLink,
629
+ href: aiCreateSiteHref(orgSlug),
630
+ onClick: ()=>stashAiSiteStartAnswers(job.siteInputs),
631
+ sx: {
632
+ mt: 0.5
633
+ },
634
+ children: AI_SITE_START_AGAIN_LABEL
635
+ }) : null
636
+ ]
637
+ }) : restartHref(job) ? /*#__PURE__*/ _jsx(Button, {
616
638
  size: "small",
617
639
  variant: "contained",
618
640
  component: AppLink,