@aglyn/plugins-ai 1.0.0-beta.226 → 1.0.0-beta.227

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 (156) hide show
  1. package/README.md +2 -1
  2. package/package.json +17 -17
  3. package/src/lib/billing/ai-month-meter.js +5 -3
  4. package/src/lib/billing/ai-month-meter.js.map +1 -1
  5. package/src/lib/billing/ai-overage-ledger.js +3 -1
  6. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  7. package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
  8. package/src/lib/components/ai-brief-dialog.component.js +35 -17
  9. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  10. package/src/lib/components/ai-describe-automation.component.js +17 -2
  11. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  12. package/src/lib/components/ai-insight-dialog.component.js +24 -2
  13. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  14. package/src/lib/components/ai-job-events.d.ts +14 -0
  15. package/src/lib/components/ai-job-events.js +76 -0
  16. package/src/lib/components/ai-job-events.js.map +1 -0
  17. package/src/lib/components/ai-job-follow.component.d.ts +49 -0
  18. package/src/lib/components/ai-job-follow.component.js +210 -0
  19. package/src/lib/components/ai-job-follow.component.js.map +1 -0
  20. package/src/lib/components/ai-job-links.d.ts +42 -0
  21. package/src/lib/components/ai-job-links.js +140 -0
  22. package/src/lib/components/ai-job-links.js.map +1 -0
  23. package/src/lib/components/ai-job-plan.component.d.ts +13 -6
  24. package/src/lib/components/ai-job-plan.component.js +29 -3
  25. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  26. package/src/lib/components/ai-job-requests.d.ts +24 -0
  27. package/src/lib/components/ai-job-requests.js +76 -0
  28. package/src/lib/components/ai-job-requests.js.map +1 -0
  29. package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
  30. package/src/lib/components/ai-jobs-indicator.component.js +80 -0
  31. package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
  32. package/src/lib/components/ai-jobs-store.d.ts +81 -0
  33. package/src/lib/components/ai-jobs-store.js +218 -0
  34. package/src/lib/components/ai-jobs-store.js.map +1 -0
  35. package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
  36. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  37. package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
  38. package/src/lib/components/ai-site-build-page.component.js +377 -0
  39. package/src/lib/components/ai-site-build-page.component.js.map +1 -0
  40. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  41. package/src/lib/components/ai-site-start-card.component.js +522 -130
  42. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  43. package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
  44. package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
  45. package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
  46. package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
  47. package/src/lib/components/assist-jobs-drawer.component.js +152 -172
  48. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  49. package/src/lib/components/assist-panel.component.js +62 -6
  50. package/src/lib/components/assist-panel.component.js.map +1 -1
  51. package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
  52. package/src/lib/components/staff-ai-credit-return.component.js +346 -0
  53. package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
  54. package/src/lib/components/staff-org-ai-card.component.js +364 -279
  55. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  56. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
  57. package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
  58. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  59. package/src/lib/components/use-ai-job-run.js +5 -18
  60. package/src/lib/components/use-ai-job-run.js.map +1 -1
  61. package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
  62. package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
  63. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
  64. package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
  65. package/src/lib/jobs/ai-job-plan-step.js +161 -17
  66. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
  68. package/src/lib/jobs/ai-job-site-step.js +34 -4
  69. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
  71. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
  73. package/src/lib/jobs/ai-jobs-notify.js +51 -0
  74. package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
  75. package/src/lib/jobs/ai-jobs.d.ts +31 -4
  76. package/src/lib/jobs/ai-jobs.js +196 -32
  77. package/src/lib/jobs/ai-jobs.js.map +1 -1
  78. package/src/lib/model/ai-job-activity.d.ts +112 -0
  79. package/src/lib/model/ai-job-activity.js +144 -0
  80. package/src/lib/model/ai-job-activity.js.map +1 -0
  81. package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
  82. package/src/lib/model/ai-job-failure-copy.js +78 -0
  83. package/src/lib/model/ai-job-failure-copy.js.map +1 -0
  84. package/src/lib/model/ai-job-notice.d.ts +61 -0
  85. package/src/lib/model/ai-job-notice.js +112 -0
  86. package/src/lib/model/ai-job-notice.js.map +1 -0
  87. package/src/lib/model/ai-jobs.types.d.ts +26 -0
  88. package/src/lib/model/ai-jobs.types.js +1 -0
  89. package/src/lib/model/ai-jobs.types.js.map +1 -1
  90. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  91. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  92. package/src/lib/model/ai-site-build-progress.d.ts +45 -0
  93. package/src/lib/model/ai-site-build-progress.js +76 -0
  94. package/src/lib/model/ai-site-build-progress.js.map +1 -0
  95. package/src/lib/model/ai-site-inventory.d.ts +9 -0
  96. package/src/lib/model/ai-site-inventory.js +7 -2
  97. package/src/lib/model/ai-site-inventory.js.map +1 -1
  98. package/src/lib/model/ai-site-job.d.ts +93 -2
  99. package/src/lib/model/ai-site-job.js +88 -7
  100. package/src/lib/model/ai-site-job.js.map +1 -1
  101. package/src/lib/model/ai-site-start.d.ts +3 -1
  102. package/src/lib/model/ai-site-start.js +6 -4
  103. package/src/lib/model/ai-site-start.js.map +1 -1
  104. package/src/lib/plan-entitlements.d.ts +3 -3
  105. package/src/lib/plan-entitlements.js +3 -3
  106. package/src/lib/plan-entitlements.js.map +1 -1
  107. package/src/lib/plugin.js +28 -0
  108. package/src/lib/plugin.js.map +1 -1
  109. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  110. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  111. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  112. package/src/lib/runtime/ai-doctrine-validators.js +31 -4
  113. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  114. package/src/lib/runtime/ai-gate.d.ts +3 -2
  115. package/src/lib/runtime/ai-gate.js +3 -2
  116. package/src/lib/runtime/ai-gate.js.map +1 -1
  117. package/src/lib/runtime/site-inventory.d.ts +16 -1
  118. package/src/lib/runtime/site-inventory.js +43 -3
  119. package/src/lib/runtime/site-inventory.js.map +1 -1
  120. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  121. package/src/lib/server/ai-admin-credits.js +213 -0
  122. package/src/lib/server/ai-admin-credits.js.map +1 -0
  123. package/src/lib/server/ai-admin-org.js +12 -5
  124. package/src/lib/server/ai-admin-org.js.map +1 -1
  125. package/src/lib/server/ai-admin-signals.js +3 -1
  126. package/src/lib/server/ai-admin-signals.js.map +1 -1
  127. package/src/lib/server/ai-admin-user.js +12 -1
  128. package/src/lib/server/ai-admin-user.js.map +1 -1
  129. package/src/lib/server/ai-allotments.js +3 -2
  130. package/src/lib/server/ai-allotments.js.map +1 -1
  131. package/src/lib/server/ai-jobs-route.js +20 -2
  132. package/src/lib/server/ai-jobs-route.js.map +1 -1
  133. package/src/lib/server/assist-chat.js +3 -2
  134. package/src/lib/server/assist-chat.js.map +1 -1
  135. package/src/lib/server/billing-credits.js +4 -3
  136. package/src/lib/server/billing-credits.js.map +1 -1
  137. package/src/lib/server/billing-overage.js +3 -2
  138. package/src/lib/server/billing-overage.js.map +1 -1
  139. package/src/lib/server.js +11 -0
  140. package/src/lib/server.js.map +1 -1
  141. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  142. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  143. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  144. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  145. package/src/lib/usage/assist-credit-returns.js +79 -0
  146. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  147. package/src/lib/usage/assist-free-taste.js +6 -3
  148. package/src/lib/usage/assist-free-taste.js.map +1 -1
  149. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  150. package/src/lib/usage/assist-job-refund.js +109 -0
  151. package/src/lib/usage/assist-job-refund.js.map +1 -0
  152. package/src/lib/usage/assist-usage.js +5 -2
  153. package/src/lib/usage/assist-usage.js.map +1 -1
  154. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  155. package/src/lib/usage/staff-org-ai.js +19 -2
  156. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -16,36 +16,169 @@
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
- import { lockdownRefusalText, parseLockdownRefusal } from "@aglyn/aglyn";
19
+ import { lockdownRefusalText, parseLockdownRefusal, PLATFORM_BRAND_NAME } from "@aglyn/aglyn";
20
+ import { trackEvent } from "@aglyn/aglyn/app-utils/analytics-events";
20
21
  import { ICON_VARIANT_CLOSE } from "@aglyn/shared-data-enums";
22
+ import { mdiCheckCircle, mdiCreation, mdiInformationOutline, mdiPageLayoutHeaderFooter } from "@aglyn/shared-data-mdi";
21
23
  import { MdiIcon } from "@aglyn/shared-ui-jsx";
22
24
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
23
25
  import { useUser } from "@aglyn/tenant-feature-instance";
24
- import { Alert, AppBar, Box, Button, Chip, Container, Dialog, DialogActions, DialogContent, Divider, IconButton, MenuItem, Stack, TextField, Toolbar, Typography } from "@mui/material";
25
- import { useCallback, useEffect, useRef, useState } from "react";
26
- import { AI_SITE_PAGES, AI_SITE_SUBMISSION_CHOICES, aiSiteCreditEstimate } from "../model/ai-site-job.js";
26
+ import { Alert, AppBar, Box, Button, Card, CardActionArea, Chip, CircularProgress, Container, Dialog, DialogActions, DialogContent, IconButton, MenuItem, Stack, Step, StepLabel, Stepper, TextField, Toolbar, Tooltip, Typography } from "@mui/material";
27
+ import { alpha } from "@mui/material/styles";
28
+ import { useRouter } from "next/navigation";
29
+ import { useCallback, useEffect, useId, useRef, useState } from "react";
30
+ import { AI_JOB_AUTO_CONFIRM_INPUT, AI_SITE_FREE_PAGES, AI_SITE_FREE_PAGES_NOTE, AI_SITE_SUBMISSION_CHOICES, aiFreeSiteCreditEstimate, aiSiteCreditEstimate, aiSitePagesBand } from "../model/ai-site-job.js";
31
+ import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
27
32
  import { AI_SITE_START_ANSWERS, AI_SITE_START_EXAMPLES, AI_SITE_START_TYPES, aiSiteStartBrief, aiSiteStartInputs, aiSiteStartRefusal } from "../model/ai-site-start.js";
33
+ import { AiJobFollow } from "./ai-job-follow.component.js";
34
+ import { aiSiteBuildHref } from "./ai-job-links.js";
35
+ import { publishAiJob } from "./ai-jobs-store.js";
28
36
  /** Labels the dialog for a reader, from its own heading. */ const TITLE_ID = 'ai-site-start-title';
29
- export function AiSiteStartCard({ hostId, orgId, startBlank }) {
37
+ /** The two steps, as the indicator under the header names them. */ const STEP_LABELS = [
38
+ 'Choose',
39
+ 'Describe'
40
+ ];
41
+ /** The soft round badge an icon sits in, in its accent's tint. */ function IconBadge({ accent, children }) {
42
+ return /*#__PURE__*/ _jsx(Box, {
43
+ "aria-hidden": true,
44
+ sx: (theme)=>({
45
+ width: theme.spacing(9),
46
+ height: theme.spacing(9),
47
+ borderRadius: '50%',
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ bgcolor: alpha(theme.palette[accent].main, theme.palette.mode === 'dark' ? 0.24 : 0.12),
52
+ color: `${accent}.main`,
53
+ fontSize: theme.spacing(5.5)
54
+ }),
55
+ children: children
56
+ });
57
+ }
58
+ /**
59
+ * One way to start, as a card the whole of which is the button. Its accessible
60
+ * name is its title; its description describes it.
61
+ */ function ChoiceCard({ accent, icon, iconId, title, description, busy = false, onChoose, children }) {
62
+ const id = useId();
63
+ return /*#__PURE__*/ _jsx(Card, {
64
+ variant: "outlined",
65
+ sx: (theme)=>({
66
+ borderRadius: 2,
67
+ height: '100%',
68
+ transition: theme.transitions.create([
69
+ 'border-color',
70
+ 'box-shadow',
71
+ 'transform'
72
+ ], {
73
+ duration: theme.transitions.duration.shorter
74
+ }),
75
+ '&:hover, &:focus-within': {
76
+ borderColor: `${accent}.main`,
77
+ boxShadow: theme.shadows[4],
78
+ transform: `translateY(-${theme.spacing(0.25)})`
79
+ },
80
+ '@media (prefers-reduced-motion: reduce)': {
81
+ transition: 'none',
82
+ '&:hover, &:focus-within': {
83
+ transform: 'none'
84
+ }
85
+ }
86
+ }),
87
+ children: /*#__PURE__*/ _jsx(CardActionArea, {
88
+ onClick: onChoose,
89
+ disabled: busy,
90
+ "aria-labelledby": `${id}-title`,
91
+ "aria-describedby": `${id}-description`,
92
+ "aria-busy": busy || undefined,
93
+ sx: {
94
+ height: '100%',
95
+ p: {
96
+ xs: 3,
97
+ sm: 4
98
+ }
99
+ },
100
+ children: /*#__PURE__*/ _jsxs(Stack, {
101
+ spacing: 2,
102
+ sx: {
103
+ alignItems: 'center',
104
+ textAlign: 'center'
105
+ },
106
+ children: [
107
+ /*#__PURE__*/ _jsx(IconBadge, {
108
+ accent: accent,
109
+ children: busy ? /*#__PURE__*/ _jsx(CircularProgress, {
110
+ size: "1em",
111
+ color: accent,
112
+ "aria-label": `${title}…`
113
+ }) : /*#__PURE__*/ _jsx(MdiIcon, {
114
+ path: icon,
115
+ "data-icon": iconId,
116
+ "aria-hidden": true
117
+ })
118
+ }),
119
+ /*#__PURE__*/ _jsx(Typography, {
120
+ id: `${id}-title`,
121
+ variant: "h6",
122
+ component: "h2",
123
+ children: title
124
+ }),
125
+ /*#__PURE__*/ _jsx(Typography, {
126
+ id: `${id}-description`,
127
+ variant: "body2",
128
+ color: "text.secondary",
129
+ children: description
130
+ }),
131
+ children
132
+ ]
133
+ })
134
+ })
135
+ });
136
+ }
137
+ /** A small label above a group of questions. */ function SectionLabel({ children }) {
138
+ return /*#__PURE__*/ _jsx(Typography, {
139
+ variant: "overline",
140
+ color: "text.secondary",
141
+ component: "h2",
142
+ sx: {
143
+ display: 'block'
144
+ },
145
+ children: children
146
+ });
147
+ }
148
+ export function AiSiteStartCard({ hostId, orgId, orgSlug, host, startBlank, leave }) {
30
149
  var _ref;
31
150
  const { data: user } = useUser();
151
+ const router = useRouter();
32
152
  // Held in a ref so a request reads WHO is signed in, never the identity of
33
153
  // the object that says so.
34
154
  const userRef = useRef(user);
35
155
  userRef.current = user;
36
156
  const uid = (_ref = user == null ? void 0 : user.uid) != null ? _ref : null;
37
157
  const [verdict, setVerdict] = useState('checking');
158
+ const [step, setStep] = useState('choose');
38
159
  const [answers, setAnswers] = useState(AI_SITE_START_ANSWERS);
39
160
  const [busy, setBusy] = useState(false);
161
+ // The starter card's own busy state while `startBlank` runs (AGL-3594).
162
+ const [startingStarter, setStartingStarter] = useState(false);
40
163
  const [notice, setNotice] = useState(null);
41
- const [started, setStarted] = useState(false);
164
+ /** The site job "Plan my site" started, as the create door answered with it. */ const [started, setStarted] = useState(null);
165
+ // The Free taste's page band (AGL-3594), read off the verdict request.
166
+ const [freeTaste, setFreeTaste] = useState(false);
42
167
  useEffect(()=>{
43
168
  if (!orgId || !uid) return;
44
169
  let active = true;
45
170
  void (async ()=>{
46
171
  try {
47
172
  const response = await authorizedFetch(userRef.current, `/api/ai/jobs?orgId=${encodeURIComponent(orgId)}&limit=1`);
173
+ const payload = response.ok ? await response.json().catch(()=>null) : null;
48
174
  if (active) {
175
+ if ((payload == null ? void 0 : payload.freeTaste) === true) {
176
+ setFreeTaste(true);
177
+ setAnswers((current)=>_extends({}, current, {
178
+ pages: AI_SITE_FREE_PAGES.max,
179
+ welcomeEmail: false
180
+ }));
181
+ }
49
182
  setVerdict(response.status === 404 || response.status === 403 ? 'hidden' : 'ready');
50
183
  }
51
184
  } catch (unused) {
@@ -60,12 +193,18 @@ export function AiSiteStartCard({ hostId, orgId, startBlank }) {
60
193
  uid
61
194
  ]);
62
195
  const answer = useCallback((patch)=>setAnswers((current)=>_extends({}, current, patch)), []);
63
- const refusal = aiSiteStartRefusal(answers);
196
+ const refusal = aiSiteStartRefusal(answers, {
197
+ freeTaste
198
+ });
199
+ const band = aiSitePagesBand(freeTaste);
64
200
  const plan = useCallback(async ()=>{
65
- if (!orgId || aiSiteStartRefusal(answers)) return;
201
+ if (!orgId || aiSiteStartRefusal(answers, {
202
+ freeTaste
203
+ })) return;
66
204
  setBusy(true);
67
205
  setNotice(null);
68
206
  try {
207
+ var _ref;
69
208
  const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {
70
209
  method: 'POST',
71
210
  headers: {
@@ -76,17 +215,38 @@ export function AiSiteStartCard({ hostId, orgId, startBlank }) {
76
215
  hostId,
77
216
  kind: 'site',
78
217
  brief: aiSiteStartBrief(answers),
79
- inputs: aiSiteStartInputs(answers)
218
+ // The guided start confirms its own plan (AGL-3594): the build
219
+ // follows the plan with no approval to make.
220
+ inputs: _extends({}, aiSiteStartInputs(answers), {
221
+ [AI_JOB_AUTO_CONFIRM_INPUT]: true
222
+ })
80
223
  })
81
224
  });
82
225
  const payload = await response.json().catch(()=>null);
83
226
  if (!response.ok) {
84
- var _ref;
227
+ var _ref1;
85
228
  const locked = parseLockdownRefusal(response.status, payload);
86
- setNotice(locked ? lockdownRefusalText(locked) : String((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The site could not be started. Try again.'));
229
+ setNotice(locked ? lockdownRefusalText(locked) : String((_ref1 = payload == null ? void 0 : payload.error) != null ? _ref1 : 'The site could not be started. Try again.'));
230
+ return;
231
+ }
232
+ const created = (_ref = payload == null ? void 0 : payload.job) != null ? _ref : null;
233
+ if (!created) {
234
+ setNotice('The site could not be started. Try again.');
235
+ return;
236
+ }
237
+ // The indicator and the launcher count it from the moment it exists.
238
+ publishAiJob(created);
239
+ // Where the next step is (AGL-3594): the site's "Building your site"
240
+ // page for this job. The zone closes without the starter first, so the
241
+ // setup page does not ask again behind it.
242
+ const href = aiSiteBuildHref(orgSlug, host, created.id);
243
+ if (href) {
244
+ ;
245
+ (leave != null ? leave : ()=>undefined)();
246
+ router.push(href);
87
247
  return;
88
248
  }
89
- setStarted(true);
249
+ setStarted(created);
90
250
  } catch (unused) {
91
251
  setNotice('The site could not be started. Try again.');
92
252
  } finally{
@@ -95,28 +255,70 @@ export function AiSiteStartCard({ hostId, orgId, startBlank }) {
95
255
  }, [
96
256
  orgId,
97
257
  hostId,
98
- answers
258
+ answers,
259
+ freeTaste,
260
+ orgSlug,
261
+ host,
262
+ leave,
263
+ router
264
+ ]);
265
+ const chooseStarter = useCallback(()=>{
266
+ setStartingStarter(true);
267
+ trackEvent('site_start_choice', {
268
+ choice: 'starter'
269
+ });
270
+ startBlank();
271
+ }, [
272
+ startBlank
99
273
  ]);
274
+ const chooseAi = useCallback(()=>{
275
+ trackEvent('site_start_choice', {
276
+ choice: 'ai'
277
+ });
278
+ setStep('describe');
279
+ }, []);
100
280
  if (verdict !== 'ready') return null;
101
- const estimate = aiSiteCreditEstimate(answers.pages, {
281
+ // Before a job is started every way out is the blank site, which writes the
282
+ // starter (AGL-3594); after, it only closes — the job builds the site.
283
+ const exit = started ? leave != null ? leave : startBlank : startBlank;
284
+ const estimate = freeTaste ? aiFreeSiteCreditEstimate(answers.pages) : aiSiteCreditEstimate(answers.pages, {
102
285
  welcomeEmail: answers.welcomeEmail
103
286
  });
287
+ const estimateText = freeTaste ? `Up to about ${estimate.toLocaleString('en-US')} of the ${FREE_AI_TASTE_CREDITS_PER_MONTH} AI credits your Free workspace has each month` : `About ${estimate.toLocaleString('en-US')} credits, estimated`;
288
+ const choosing = step === 'choose' && !started;
104
289
  return /*#__PURE__*/ _jsxs(Dialog, {
105
290
  open: true,
106
291
  fullScreen: true,
107
- onClose: startBlank,
292
+ onClose: exit,
108
293
  "aria-labelledby": TITLE_ID,
294
+ slotProps: {
295
+ paper: {
296
+ sx: {
297
+ bgcolor: 'background.paper'
298
+ }
299
+ }
300
+ },
109
301
  children: [
110
302
  /*#__PURE__*/ _jsx(AppBar, {
111
303
  position: "relative",
112
- color: "surface",
304
+ color: "inherit",
305
+ elevation: 0,
113
306
  enableColorOnDark: true,
307
+ sx: {
308
+ bgcolor: 'background.paper',
309
+ borderBottom: 1,
310
+ borderColor: 'divider'
311
+ },
114
312
  children: /*#__PURE__*/ _jsxs(Toolbar, {
313
+ variant: "dense",
314
+ sx: {
315
+ gap: 1
316
+ },
115
317
  children: [
116
318
  /*#__PURE__*/ _jsx(IconButton, {
117
319
  edge: "start",
118
320
  color: "inherit",
119
- onClick: startBlank,
321
+ onClick: exit,
120
322
  "aria-label": "Close the guided start",
121
323
  children: /*#__PURE__*/ _jsx(MdiIcon, {
122
324
  path: ICON_VARIANT_CLOSE.path
@@ -124,190 +326,336 @@ export function AiSiteStartCard({ hostId, orgId, startBlank }) {
124
326
  }),
125
327
  /*#__PURE__*/ _jsx(Typography, {
126
328
  id: TITLE_ID,
127
- variant: "h6",
329
+ variant: "subtitle1",
128
330
  component: "div",
129
331
  noWrap: true,
130
332
  sx: {
131
333
  textOverflow: 'ellipsis',
132
- ml: 2,
133
334
  flex: 1
134
335
  },
135
- children: 'Start this site with AI'
136
- }),
137
- /*#__PURE__*/ _jsx(Divider, {
138
- sx: {
139
- height: 28,
140
- m: 0.5
141
- },
142
- orientation: "vertical"
336
+ children: 'Start your site'
143
337
  }),
144
338
  /*#__PURE__*/ _jsx(Button, {
145
339
  color: "inherit",
146
- onClick: startBlank,
147
- children: started ? 'Close' : 'Skip and start blank'
340
+ onClick: exit,
341
+ "aria-label": started ? 'Close' : 'Skip and start blank',
342
+ children: started ? 'Close' : 'Skip'
148
343
  })
149
344
  ]
150
345
  })
151
346
  }),
152
347
  /*#__PURE__*/ _jsx(DialogContent, {
348
+ sx: {
349
+ px: {
350
+ xs: 2,
351
+ sm: 3
352
+ }
353
+ },
153
354
  children: /*#__PURE__*/ _jsx(Container, {
154
- maxWidth: "md",
355
+ maxWidth: choosing ? 'md' : 'sm',
155
356
  disableGutters: true,
156
357
  sx: {
157
- py: 2
358
+ py: {
359
+ xs: 3,
360
+ sm: 5
361
+ }
158
362
  },
159
363
  children: /*#__PURE__*/ _jsxs(Stack, {
160
- spacing: 3,
364
+ spacing: 4,
161
365
  children: [
162
- /*#__PURE__*/ _jsx(Typography, {
163
- variant: "body2",
164
- color: "text.secondary",
165
- children: 'Answer a few questions and AI plans the whole site — its pages, its ' + 'navigation, its layout and a contact form. Nothing is built until you ' + 'confirm the plan, and everything it builds is an unpublished draft.'
366
+ /*#__PURE__*/ _jsx(Stepper, {
367
+ activeStep: choosing ? 0 : 1,
368
+ "aria-label": choosing ? 'Step 1 of 2' : 'Step 2 of 2',
369
+ sx: {
370
+ maxWidth: (theme)=>theme.spacing(40),
371
+ width: '100%',
372
+ mx: 'auto'
373
+ },
374
+ children: STEP_LABELS.map((label)=>/*#__PURE__*/ _jsx(Step, {
375
+ children: /*#__PURE__*/ _jsx(StepLabel, {
376
+ children: label
377
+ })
378
+ }, label))
379
+ }),
380
+ /*#__PURE__*/ _jsxs(Stack, {
381
+ spacing: 1,
382
+ sx: {
383
+ textAlign: choosing ? 'center' : 'left'
384
+ },
385
+ children: [
386
+ /*#__PURE__*/ _jsx(Typography, {
387
+ variant: "h4",
388
+ component: "h1",
389
+ children: choosing ? 'How do you want to start?' : 'Tell us about your site'
390
+ }),
391
+ /*#__PURE__*/ _jsx(Typography, {
392
+ variant: "body1",
393
+ color: "text.secondary",
394
+ children: choosing ? 'You can change everything later.' : `A few answers and ${PLATFORM_BRAND_NAME} AI plans your pages. Nothing is published until you say so.`
395
+ })
396
+ ]
166
397
  }),
167
398
  notice && /*#__PURE__*/ _jsx(Alert, {
168
399
  severity: "info",
169
400
  children: notice
170
401
  }),
171
- started ? /*#__PURE__*/ _jsx(Alert, {
172
- severity: "success",
173
- children: 'Your site is being planned. Open AI jobs in the Assist panel to read the plan ' + 'and confirm it — nothing is built, and nothing is published, until you do.'
402
+ choosing ? /*#__PURE__*/ _jsxs(Box, {
403
+ sx: {
404
+ display: 'grid',
405
+ gridTemplateColumns: {
406
+ xs: '1fr',
407
+ sm: '1fr 1fr'
408
+ },
409
+ gap: 3
410
+ },
411
+ children: [
412
+ /*#__PURE__*/ _jsx(ChoiceCard, {
413
+ accent: "secondary",
414
+ icon: mdiPageLayoutHeaderFooter.path,
415
+ iconId: "page-layout-header-footer",
416
+ title: "Start from the starter site",
417
+ description: "A ready-made home page with a header, footer and contact form you can edit.",
418
+ busy: startingStarter,
419
+ onChoose: chooseStarter
420
+ }),
421
+ /*#__PURE__*/ _jsxs(ChoiceCard, {
422
+ accent: "primary",
423
+ icon: mdiCreation.path,
424
+ iconId: "creation",
425
+ title: "Start with AI",
426
+ description: `Answer a few questions and ${PLATFORM_BRAND_NAME} AI plans your pages.`,
427
+ onChoose: chooseAi,
428
+ children: [
429
+ /*#__PURE__*/ _jsx(Chip, {
430
+ size: "small",
431
+ color: "primary",
432
+ label: "Recommended"
433
+ }),
434
+ freeTaste && /*#__PURE__*/ _jsx(Typography, {
435
+ variant: "caption",
436
+ color: "text.secondary",
437
+ children: `Up to ${AI_SITE_FREE_PAGES.max} pages on the Free plan`
438
+ })
439
+ ]
440
+ })
441
+ ]
442
+ }) : started && orgId ? /*#__PURE__*/ _jsx(Card, {
443
+ variant: "outlined",
444
+ sx: {
445
+ borderRadius: 2,
446
+ p: {
447
+ xs: 2,
448
+ sm: 3
449
+ }
450
+ },
451
+ children: /*#__PURE__*/ _jsx(AiJobFollow, {
452
+ job: started,
453
+ orgId: orgId,
454
+ orgSlug: orgSlug,
455
+ user: user,
456
+ intro: 'Your site is being planned. Review the plan here or in AI jobs, and confirm ' + 'it to build — nothing is built, and nothing is published, until you do.',
457
+ // Full screen: AI jobs opens in the panel this dialog covers.
458
+ onOpenJobs: exit
459
+ })
174
460
  }) : /*#__PURE__*/ _jsxs(_Fragment, {
175
461
  children: [
176
- /*#__PURE__*/ _jsxs(Box, {
462
+ /*#__PURE__*/ _jsxs(Stack, {
463
+ spacing: 2,
464
+ component: "section",
465
+ "aria-label": "Your business",
177
466
  children: [
467
+ /*#__PURE__*/ _jsx(SectionLabel, {
468
+ children: 'Your business'
469
+ }),
178
470
  /*#__PURE__*/ _jsx(TextField, {
179
471
  fullWidth: true,
472
+ required: true,
180
473
  label: "What kind of site are you creating?",
181
474
  placeholder: "a neighborhood dog groomer that takes bookings",
182
475
  value: answers.siteType,
183
476
  onChange: (event)=>answer({
184
477
  siteType: event.target.value
185
- })
478
+ }),
479
+ helperText: "A few words is enough, or pick one below."
186
480
  }),
187
- /*#__PURE__*/ _jsx(Stack, {
188
- direction: "row",
189
- spacing: 1,
481
+ /*#__PURE__*/ _jsx(Box, {
190
482
  sx: {
483
+ display: 'flex',
191
484
  flexWrap: 'wrap',
192
- mt: 1,
193
- rowGap: 1
485
+ gap: 1
194
486
  },
195
487
  children: AI_SITE_START_TYPES.map((type)=>/*#__PURE__*/ _jsx(Chip, {
196
488
  label: type,
197
489
  size: "small",
490
+ color: answers.siteType === type ? 'primary' : 'default',
198
491
  variant: answers.siteType === type ? 'filled' : 'outlined',
199
492
  onClick: ()=>answer({
200
493
  siteType: type
201
494
  })
202
495
  }, type))
496
+ }),
497
+ /*#__PURE__*/ _jsx(TextField, {
498
+ fullWidth: true,
499
+ label: "Who is it for?",
500
+ placeholder: "local dog owners who want a regular groom booked online",
501
+ value: answers.audience,
502
+ onChange: (event)=>answer({
503
+ audience: event.target.value
504
+ }),
505
+ helperText: "Optional. It narrows who the pages are written for."
203
506
  })
204
507
  ]
205
508
  }),
206
- /*#__PURE__*/ _jsx(TextField, {
207
- fullWidth: true,
208
- label: "Who is it for?",
209
- placeholder: "local dog owners who want a regular groom booked online",
210
- value: answers.audience,
211
- onChange: (event)=>answer({
212
- audience: event.target.value
213
- }),
214
- helperText: "Optional. It narrows who the pages are written for."
215
- }),
216
- /*#__PURE__*/ _jsxs(Box, {
509
+ /*#__PURE__*/ _jsxs(Stack, {
510
+ spacing: 2,
511
+ component: "section",
512
+ "aria-label": "Look & layout",
217
513
  children: [
218
- /*#__PURE__*/ _jsx(Typography, {
219
- variant: "subtitle2",
220
- gutterBottom: true,
221
- children: 'Which of these do you like?'
514
+ /*#__PURE__*/ _jsx(SectionLabel, {
515
+ children: 'Look & layout'
222
516
  }),
223
517
  /*#__PURE__*/ _jsx(Typography, {
224
518
  variant: "body2",
225
519
  color: "text.secondary",
226
- gutterBottom: true,
227
520
  children: 'Optional. Picking one steers the shape of the site, not its words.'
228
521
  }),
229
- /*#__PURE__*/ _jsx(Stack, {
230
- direction: "row",
231
- spacing: 1,
522
+ /*#__PURE__*/ _jsx(Box, {
232
523
  sx: {
233
- flexWrap: 'wrap',
234
- rowGap: 1
524
+ display: 'grid',
525
+ gridTemplateColumns: {
526
+ xs: '1fr',
527
+ sm: '1fr 1fr'
528
+ },
529
+ gap: 2
235
530
  },
236
- children: AI_SITE_START_EXAMPLES.map((example)=>/*#__PURE__*/ _jsx(Chip, {
237
- label: `${example.label} — ${example.blurb}`,
238
- variant: answers.example === example.id ? 'filled' : 'outlined',
239
- onClick: ()=>answer({
240
- example: answers.example === example.id ? null : example.id
531
+ children: AI_SITE_START_EXAMPLES.map((example)=>{
532
+ const selected = answers.example === example.id;
533
+ return /*#__PURE__*/ _jsx(Card, {
534
+ variant: "outlined",
535
+ sx: (theme)=>({
536
+ borderRadius: 2,
537
+ borderColor: selected ? 'primary.main' : 'divider',
538
+ bgcolor: selected ? alpha(theme.palette.primary.main, 0.08) : 'transparent'
539
+ }),
540
+ children: /*#__PURE__*/ _jsx(CardActionArea, {
541
+ "aria-pressed": selected,
542
+ "aria-label": `${example.label} — ${example.blurb}`,
543
+ onClick: ()=>answer({
544
+ example: selected ? null : example.id
545
+ }),
546
+ sx: {
547
+ p: 2,
548
+ height: '100%'
549
+ },
550
+ children: /*#__PURE__*/ _jsxs(Stack, {
551
+ direction: "row",
552
+ spacing: 1.5,
553
+ sx: {
554
+ alignItems: 'flex-start'
555
+ },
556
+ children: [
557
+ /*#__PURE__*/ _jsxs(Box, {
558
+ sx: {
559
+ flex: 1,
560
+ minWidth: 0
561
+ },
562
+ children: [
563
+ /*#__PURE__*/ _jsx(Typography, {
564
+ variant: "subtitle2",
565
+ children: example.label
566
+ }),
567
+ /*#__PURE__*/ _jsx(Typography, {
568
+ variant: "body2",
569
+ color: "text.secondary",
570
+ children: example.blurb
571
+ })
572
+ ]
573
+ }),
574
+ selected && /*#__PURE__*/ _jsx(Box, {
575
+ sx: {
576
+ color: 'primary.main',
577
+ display: 'flex'
578
+ },
579
+ "aria-hidden": true,
580
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
581
+ path: mdiCheckCircle.path
582
+ })
583
+ })
584
+ ]
241
585
  })
242
- }, example.id))
586
+ })
587
+ }, example.id);
588
+ })
243
589
  })
244
590
  ]
245
591
  }),
246
- /*#__PURE__*/ _jsx(TextField, {
247
- select: true,
248
- fullWidth: true,
249
- label: "Where do form submissions go?",
250
- value: answers.submissions,
251
- onChange: (event)=>answer({
252
- submissions: event.target.value
253
- }),
254
- helperText: "You can change this on the form itself afterwards.",
255
- children: AI_SITE_SUBMISSION_CHOICES.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
256
- value: option.id,
257
- children: `${option.label} — ${option.blurb}`
258
- }, option.id))
259
- }),
260
592
  /*#__PURE__*/ _jsxs(Stack, {
261
- direction: {
262
- xs: 'column',
263
- sm: 'row'
264
- },
265
593
  spacing: 2,
594
+ component: "section",
595
+ "aria-label": "Details",
266
596
  children: [
597
+ /*#__PURE__*/ _jsx(SectionLabel, {
598
+ children: 'Details'
599
+ }),
267
600
  /*#__PURE__*/ _jsx(TextField, {
268
601
  select: true,
269
- label: "Pages",
270
- value: answers.pages,
602
+ fullWidth: true,
603
+ label: "Where do form submissions go?",
604
+ value: answers.submissions,
271
605
  onChange: (event)=>answer({
272
- pages: Number(event.target.value)
606
+ submissions: event.target.value
273
607
  }),
274
- sx: {
275
- minWidth: 160
276
- },
277
- children: Array.from({
278
- length: AI_SITE_PAGES.max - AI_SITE_PAGES.min + 1
279
- }, (_, index)=>AI_SITE_PAGES.min + index).map((count)=>/*#__PURE__*/ _jsx(MenuItem, {
280
- value: count,
281
- children: count
282
- }, count))
608
+ helperText: "You can change this on the form itself afterwards.",
609
+ children: AI_SITE_SUBMISSION_CHOICES.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
610
+ value: option.id,
611
+ children: `${option.label} — ${option.blurb}`
612
+ }, option.id))
283
613
  }),
284
- /*#__PURE__*/ _jsxs(TextField, {
285
- select: true,
286
- label: "Welcome email",
287
- value: answers.welcomeEmail ? 'yes' : 'no',
288
- onChange: (event)=>answer({
289
- welcomeEmail: event.target.value === 'yes'
290
- }),
291
- sx: {
292
- minWidth: 160
614
+ /*#__PURE__*/ _jsxs(Stack, {
615
+ direction: {
616
+ xs: 'column',
617
+ sm: 'row'
293
618
  },
619
+ spacing: 2,
294
620
  children: [
295
- /*#__PURE__*/ _jsx(MenuItem, {
296
- value: "yes",
297
- children: 'Draft one'
621
+ /*#__PURE__*/ _jsx(TextField, {
622
+ select: true,
623
+ fullWidth: true,
624
+ label: "Pages",
625
+ value: answers.pages,
626
+ onChange: (event)=>answer({
627
+ pages: Number(event.target.value)
628
+ }),
629
+ helperText: freeTaste ? AI_SITE_FREE_PAGES_NOTE : undefined,
630
+ children: Array.from({
631
+ length: band.max - band.min + 1
632
+ }, (_, index)=>band.min + index).map((count)=>/*#__PURE__*/ _jsx(MenuItem, {
633
+ value: count,
634
+ children: count
635
+ }, count))
298
636
  }),
299
- /*#__PURE__*/ _jsx(MenuItem, {
300
- value: "no",
301
- children: 'No'
637
+ !freeTaste && /*#__PURE__*/ _jsxs(TextField, {
638
+ select: true,
639
+ fullWidth: true,
640
+ label: "Welcome email",
641
+ value: answers.welcomeEmail ? 'yes' : 'no',
642
+ onChange: (event)=>answer({
643
+ welcomeEmail: event.target.value === 'yes'
644
+ }),
645
+ children: [
646
+ /*#__PURE__*/ _jsx(MenuItem, {
647
+ value: "yes",
648
+ children: 'Draft one'
649
+ }),
650
+ /*#__PURE__*/ _jsx(MenuItem, {
651
+ value: "no",
652
+ children: 'No'
653
+ })
654
+ ]
302
655
  })
303
656
  ]
304
657
  })
305
658
  ]
306
- }),
307
- /*#__PURE__*/ _jsx(Typography, {
308
- variant: "body2",
309
- color: "text.secondary",
310
- children: `About ${estimate.toLocaleString('en-US')} credits, estimated. What it really ` + 'costs is what each step spends, and you can watch that add up while it runs.'
311
659
  })
312
660
  ]
313
661
  })
@@ -315,27 +663,71 @@ export function AiSiteStartCard({ hostId, orgId, startBlank }) {
315
663
  })
316
664
  })
317
665
  }),
318
- !started && /* Wraps, because the reason and the button together are wider than a
319
- phone: on one line the reason would push the button off the edge. */ /*#__PURE__*/ _jsxs(DialogActions, {
666
+ step === 'describe' && !started && /* The footer stays put while the questions scroll. It wraps, because
667
+ the estimate, the reason and the buttons are wider than a phone. */ /*#__PURE__*/ _jsxs(DialogActions, {
320
668
  sx: {
321
- px: 3,
669
+ px: {
670
+ xs: 2,
671
+ sm: 3
672
+ },
322
673
  py: 2,
323
674
  flexWrap: 'wrap',
324
- rowGap: 1
675
+ rowGap: 1,
676
+ columnGap: 2,
677
+ borderTop: 1,
678
+ borderColor: 'divider',
679
+ bgcolor: 'background.paper'
325
680
  },
326
681
  children: [
682
+ /*#__PURE__*/ _jsxs(Stack, {
683
+ direction: "row",
684
+ spacing: 0.5,
685
+ sx: {
686
+ alignItems: 'center',
687
+ mr: 'auto',
688
+ minWidth: 0
689
+ },
690
+ children: [
691
+ /*#__PURE__*/ _jsx(Typography, {
692
+ variant: "body2",
693
+ color: "text.secondary",
694
+ children: `${estimateText}.`
695
+ }),
696
+ /*#__PURE__*/ _jsx(Tooltip, {
697
+ title: "What it really costs is what each step spends, and you can watch that add up while it runs.",
698
+ children: /*#__PURE__*/ _jsx(Box, {
699
+ component: "span",
700
+ tabIndex: 0,
701
+ "aria-label": "How the estimate works",
702
+ sx: {
703
+ display: 'inline-flex',
704
+ color: 'text.secondary'
705
+ },
706
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
707
+ path: mdiInformationOutline.path,
708
+ fontSize: "small"
709
+ })
710
+ })
711
+ })
712
+ ]
713
+ }),
327
714
  refusal && /*#__PURE__*/ _jsx(Typography, {
328
715
  variant: "body2",
329
716
  color: "text.secondary",
330
- sx: {
331
- mr: 'auto'
332
- },
333
717
  children: refusal
334
718
  }),
719
+ /*#__PURE__*/ _jsx(Button, {
720
+ onClick: ()=>setStep('choose'),
721
+ children: 'Back'
722
+ }),
335
723
  /*#__PURE__*/ _jsx(Button, {
336
724
  variant: "contained",
725
+ size: "large",
337
726
  disabled: busy || Boolean(refusal),
338
727
  onClick: plan,
728
+ startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
729
+ path: mdiCreation.path
730
+ }),
339
731
  children: busy ? 'Starting…' : 'Plan my site'
340
732
  })
341
733
  ]