@aglyn/plugins-ai 1.0.0-beta.225 → 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 (161) 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/model/ai-template-subjects.d.ts +4 -3
  105. package/src/lib/model/ai-template-subjects.js +5 -4
  106. package/src/lib/model/ai-template-subjects.js.map +1 -1
  107. package/src/lib/plan-entitlements.d.ts +3 -3
  108. package/src/lib/plan-entitlements.js +3 -3
  109. package/src/lib/plan-entitlements.js.map +1 -1
  110. package/src/lib/plugin.js +28 -0
  111. package/src/lib/plugin.js.map +1 -1
  112. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  113. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  114. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  115. package/src/lib/runtime/ai-doctrine-validators.js +31 -4
  116. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  117. package/src/lib/runtime/ai-gate.d.ts +3 -2
  118. package/src/lib/runtime/ai-gate.js +3 -2
  119. package/src/lib/runtime/ai-gate.js.map +1 -1
  120. package/src/lib/runtime/ai-palette.generated.js +12 -4
  121. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  122. package/src/lib/runtime/site-inventory.d.ts +16 -1
  123. package/src/lib/runtime/site-inventory.js +43 -3
  124. package/src/lib/runtime/site-inventory.js.map +1 -1
  125. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  126. package/src/lib/server/ai-admin-credits.js +213 -0
  127. package/src/lib/server/ai-admin-credits.js.map +1 -0
  128. package/src/lib/server/ai-admin-org.js +12 -5
  129. package/src/lib/server/ai-admin-org.js.map +1 -1
  130. package/src/lib/server/ai-admin-signals.js +3 -1
  131. package/src/lib/server/ai-admin-signals.js.map +1 -1
  132. package/src/lib/server/ai-admin-user.js +12 -1
  133. package/src/lib/server/ai-admin-user.js.map +1 -1
  134. package/src/lib/server/ai-allotments.js +3 -2
  135. package/src/lib/server/ai-allotments.js.map +1 -1
  136. package/src/lib/server/ai-jobs-route.js +20 -2
  137. package/src/lib/server/ai-jobs-route.js.map +1 -1
  138. package/src/lib/server/assist-chat.js +3 -2
  139. package/src/lib/server/assist-chat.js.map +1 -1
  140. package/src/lib/server/billing-credits.js +4 -3
  141. package/src/lib/server/billing-credits.js.map +1 -1
  142. package/src/lib/server/billing-overage.js +3 -2
  143. package/src/lib/server/billing-overage.js.map +1 -1
  144. package/src/lib/server.js +11 -0
  145. package/src/lib/server.js.map +1 -1
  146. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  147. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  148. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  149. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  150. package/src/lib/usage/assist-credit-returns.js +79 -0
  151. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  152. package/src/lib/usage/assist-free-taste.js +6 -3
  153. package/src/lib/usage/assist-free-taste.js.map +1 -1
  154. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  155. package/src/lib/usage/assist-job-refund.js +109 -0
  156. package/src/lib/usage/assist-job-refund.js.map +1 -0
  157. package/src/lib/usage/assist-usage.js +5 -2
  158. package/src/lib/usage/assist-usage.js.map +1 -1
  159. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  160. package/src/lib/usage/staff-org-ai.js +19 -2
  161. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -0,0 +1,210 @@
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import { AppLink } from "@aglyn/shared-ui-jsx";
19
+ import { Alert, Box, Button, Chip, CircularProgress, Stack, Typography } from "@mui/material";
20
+ import { useCallback, useEffect, useRef, useState } from "react";
21
+ import { AI_JOB_PHASE_LABELS, aiJobPhase } from "../model/ai-job-activity.js";
22
+ import { AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
23
+ import { AiJobPlan } from "./ai-job-plan.component.js";
24
+ import { cancelAiJobRequest, resumeAiJobRequest } from "./ai-job-requests.js";
25
+ import { openAiJobs, publishAiJob } from "./ai-jobs-store.js";
26
+ import { followAiJobEvents } from "./ai-job-events.js";
27
+ import { aiJobPrimaryLink } from "./ai-job-links.js";
28
+ import { aiJobProblem } from "./use-ai-job-run.js";
29
+ /** What the status line says in each phase, and how loudly. */ const PHASE_LINE = {
30
+ queued: {
31
+ severity: 'info',
32
+ text: 'Queued. It starts in a moment.'
33
+ },
34
+ planning: {
35
+ severity: 'info',
36
+ text: 'Planning. The plan appears here when it is ready.'
37
+ },
38
+ 'plan-ready': {
39
+ severity: 'warning',
40
+ text: 'Your plan is ready. Read it below and confirm it to build — nothing is built until you do.'
41
+ },
42
+ building: {
43
+ severity: 'info',
44
+ text: 'Building. Everything it builds is an unpublished draft.'
45
+ },
46
+ attention: {
47
+ severity: 'warning',
48
+ text: 'The job stopped and needs you.'
49
+ },
50
+ done: {
51
+ severity: 'success',
52
+ text: 'Done. Your drafts are ready to open.'
53
+ },
54
+ failed: {
55
+ severity: 'error',
56
+ text: 'The job stopped before it finished.'
57
+ },
58
+ canceled: {
59
+ severity: 'info',
60
+ text: 'The job was canceled.'
61
+ }
62
+ };
63
+ /** The org slug a console address names: its first segment. */ function orgSlugFromLocation() {
64
+ var _path_split_;
65
+ var _globalThis_location;
66
+ const path = typeof ((_globalThis_location = globalThis.location) == null ? void 0 : _globalThis_location.pathname) === 'string' ? globalThis.location.pathname : '';
67
+ return (_path_split_ = path.split('/')[1]) != null ? _path_split_ : '';
68
+ }
69
+ /** Follows `initial` through the events route while mounted, and lets a decision replace it. */ export function useFollowedAiJob(user, orgId, initial) {
70
+ const userRef = useRef(user);
71
+ userRef.current = user;
72
+ const [job, setJob] = useState(initial);
73
+ useEffect(()=>setJob(initial), [
74
+ initial
75
+ ]);
76
+ // One stream while the job is moving. A job parked for the person settles
77
+ // the stream; confirming moves it again, and the status change re-opens it.
78
+ const moving = job.status === 'queued' || job.status === 'running';
79
+ useEffect(()=>{
80
+ if (!moving) return undefined;
81
+ const controller = new AbortController();
82
+ void followAiJobEvents(()=>userRef.current, orgId, job.id, controller.signal, setJob);
83
+ return ()=>controller.abort();
84
+ }, [
85
+ moving,
86
+ orgId,
87
+ job.id
88
+ ]);
89
+ const replace = useCallback((next)=>{
90
+ publishAiJob(next);
91
+ setJob(next);
92
+ }, []);
93
+ return [
94
+ job,
95
+ replace
96
+ ];
97
+ }
98
+ export function AiJobFollow({ job: initial, orgId, orgSlug, user, intro, onOpenJobs, staff = false }) {
99
+ const [job, replace] = useFollowedAiJob(user, orgId, initial);
100
+ const [busy, setBusy] = useState(false);
101
+ const [notice, setNotice] = useState(null);
102
+ const phase = aiJobPhase(job);
103
+ const line = PHASE_LINE[phase];
104
+ const moving = phase === 'queued' || phase === 'planning' || phase === 'building';
105
+ const settled = AI_JOB_TERMINAL_STATUSES.includes(job.status);
106
+ const primary = aiJobPrimaryLink(job, orgSlug || orgSlugFromLocation());
107
+ const problem = phase === 'attention' || phase === 'failed' ? aiJobProblem(job, line.text) : null;
108
+ const stepsDone = job.steps.filter((step)=>step.status === 'done').length;
109
+ const decide = useCallback(async (request)=>{
110
+ setBusy(true);
111
+ setNotice(null);
112
+ const { job: next, error } = await request();
113
+ if (next) replace(next);
114
+ if (error) setNotice(error);
115
+ setBusy(false);
116
+ }, [
117
+ replace
118
+ ]);
119
+ const openJobs = ()=>{
120
+ openAiJobs({
121
+ jobId: job.id
122
+ });
123
+ onOpenJobs == null ? void 0 : onOpenJobs();
124
+ };
125
+ return /*#__PURE__*/ _jsxs(Stack, {
126
+ spacing: 2,
127
+ "aria-live": "polite",
128
+ children: [
129
+ /*#__PURE__*/ _jsx(Typography, {
130
+ variant: "body2",
131
+ color: "text.secondary",
132
+ children: intro
133
+ }),
134
+ /*#__PURE__*/ _jsxs(Alert, {
135
+ severity: line.severity,
136
+ icon: moving ? /*#__PURE__*/ _jsx(CircularProgress, {
137
+ size: 18,
138
+ "aria-label": AI_JOB_PHASE_LABELS[phase]
139
+ }) : undefined,
140
+ action: /*#__PURE__*/ _jsx(Button, {
141
+ color: "inherit",
142
+ size: "small",
143
+ variant: "outlined",
144
+ onClick: openJobs,
145
+ children: 'Open AI jobs'
146
+ }),
147
+ children: [
148
+ /*#__PURE__*/ _jsxs(Stack, {
149
+ direction: "row",
150
+ spacing: 1,
151
+ sx: {
152
+ alignItems: 'center',
153
+ flexWrap: 'wrap',
154
+ rowGap: 0.5
155
+ },
156
+ children: [
157
+ /*#__PURE__*/ _jsx(Chip, {
158
+ size: "small",
159
+ label: AI_JOB_PHASE_LABELS[phase]
160
+ }),
161
+ job.steps.length > 1 ? /*#__PURE__*/ _jsx(Typography, {
162
+ variant: "caption",
163
+ color: "text.secondary",
164
+ children: `${stepsDone} of ${job.steps.length} steps`
165
+ }) : null
166
+ ]
167
+ }),
168
+ /*#__PURE__*/ _jsx(Typography, {
169
+ variant: "body2",
170
+ sx: {
171
+ mt: 0.5
172
+ },
173
+ children: problem != null ? problem : line.text
174
+ })
175
+ ]
176
+ }),
177
+ primary ? /*#__PURE__*/ _jsx(Box, {
178
+ children: /*#__PURE__*/ _jsx(Button, {
179
+ variant: "contained",
180
+ component: AppLink,
181
+ href: primary.href,
182
+ children: primary.label
183
+ })
184
+ }) : null,
185
+ /*#__PURE__*/ _jsx(AiJobPlan, {
186
+ job: job,
187
+ onResume: (target)=>void decide(()=>resumeAiJobRequest(user, orgId, target)),
188
+ busy: busy,
189
+ staff: staff,
190
+ // "Use the starter site instead" on a guided start that did not work out (AGL-3594).
191
+ user: user != null ? user : undefined
192
+ }),
193
+ !settled ? /*#__PURE__*/ _jsx(Box, {
194
+ children: /*#__PURE__*/ _jsx(Button, {
195
+ size: "small",
196
+ disabled: busy,
197
+ onClick: ()=>void decide(()=>cancelAiJobRequest(user, orgId, job.id)),
198
+ children: 'Cancel job'
199
+ })
200
+ }) : null,
201
+ notice ? /*#__PURE__*/ _jsx(Alert, {
202
+ severity: "warning",
203
+ children: notice
204
+ }) : null
205
+ ]
206
+ });
207
+ }
208
+ export default AiJobFollow;
209
+
210
+ //# sourceMappingURL=ai-job-follow.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-job-follow.component.tsx"],"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'use client'\n\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport type { MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token'\nimport { Alert, Box, Button, Chip, CircularProgress, Stack, Typography } from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n AI_JOB_PHASE_LABELS,\n aiJobPhase,\n type AiJobPhase,\n} from '../model/ai-job-activity'\nimport { AI_JOB_TERMINAL_STATUSES, type AiJobSummary } from '../model/ai-jobs.types'\nimport { AiJobPlan } from './ai-job-plan.component'\nimport { cancelAiJobRequest, resumeAiJobRequest } from './ai-job-requests'\nimport { openAiJobs, publishAiJob } from './ai-jobs-store'\nimport { followAiJobEvents } from './ai-job-events'\nimport { aiJobPrimaryLink } from './ai-job-links'\nimport { aiJobProblem } from './use-ai-job-run'\n\n/**\n * A JOB, WATCHED AND DECIDED WHERE IT WAS STARTED (AGL-3593).\n *\n * The dialog that starts a planned job — \"Plan my site\", \"Plan the page\" —\n * used to end on a sentence pointing at a drawer a person had to go and find,\n * and a person who could not find it never got their site built. This is\n * what such a dialog shows instead, once its job exists:\n *\n * - where the job stands, live — queued, planning, plan ready, building,\n * then done or failed — read from the same events route the AI jobs\n * drawer follows;\n * - the plan, once it is ready, with the drawer's own plan component and its\n * Confirm, through the same request the drawer sends\n * ({@link resumeAiJobRequest}), and Cancel beside it;\n * - once it is done, the one action that opens what it built;\n * - and always \"Open AI jobs\", which follows the job anywhere else in the\n * console.\n *\n * Closing the dialog stops following; the job carries on, and the top-bar\n * indicator, the Assist launcher and the notification the machine raises say\n * when it needs the person again.\n */\n\nexport interface AiJobFollowProps {\n /** The job as the create door answered with it. */\n job: AiJobSummary\n orgId: string\n /**\n * Path slug for the link to what the job built. A dialog mounted on a\n * console page that names its org can leave it out: the slug is the first\n * segment of every org-scoped console address.\n */\n orgSlug?: string | null\n user: MaybeTokenSource\n /** What the dialog says about the job it started, above its live status. */\n intro: string\n /**\n * Closes the dialog. Called beside opening AI jobs, because a dialog\n * covers the panel AI jobs opens in.\n */\n onOpenJobs?: () => void\n /** The viewer is staff (AGL-3078): a refused answer's details are theirs to open. */\n staff?: boolean\n}\n\n/** What the status line says in each phase, and how loudly. */\nconst PHASE_LINE: Readonly<\n Record<AiJobPhase, { severity: 'info' | 'warning' | 'success' | 'error'; text: string }>\n> = {\n queued: { severity: 'info', text: 'Queued. It starts in a moment.' },\n planning: { severity: 'info', text: 'Planning. The plan appears here when it is ready.' },\n 'plan-ready': {\n severity: 'warning',\n text: 'Your plan is ready. Read it below and confirm it to build — nothing is built until you do.',\n },\n building: { severity: 'info', text: 'Building. Everything it builds is an unpublished draft.' },\n attention: { severity: 'warning', text: 'The job stopped and needs you.' },\n done: { severity: 'success', text: 'Done. Your drafts are ready to open.' },\n failed: { severity: 'error', text: 'The job stopped before it finished.' },\n canceled: { severity: 'info', text: 'The job was canceled.' },\n}\n\n/** The org slug a console address names: its first segment. */\nfunction orgSlugFromLocation(): string {\n const path = typeof globalThis.location?.pathname === 'string' ? globalThis.location.pathname : ''\n return path.split('/')[1] ?? ''\n}\n\n/** Follows `initial` through the events route while mounted, and lets a decision replace it. */\nexport function useFollowedAiJob(\n user: MaybeTokenSource,\n orgId: string,\n initial: AiJobSummary,\n): [AiJobSummary, (job: AiJobSummary) => void] {\n const userRef = useRef(user)\n userRef.current = user\n const [job, setJob] = useState(initial)\n useEffect(() => setJob(initial), [initial])\n\n // One stream while the job is moving. A job parked for the person settles\n // the stream; confirming moves it again, and the status change re-opens it.\n const moving = job.status === 'queued' || job.status === 'running'\n useEffect(() => {\n if (!moving) return undefined\n const controller = new AbortController()\n void followAiJobEvents(() => userRef.current, orgId, job.id, controller.signal, setJob)\n return () => controller.abort()\n }, [moving, orgId, job.id])\n\n const replace = useCallback((next: AiJobSummary) => {\n publishAiJob(next)\n setJob(next)\n }, [])\n return [job, replace]\n}\n\nexport function AiJobFollow({\n job: initial,\n orgId,\n orgSlug,\n user,\n intro,\n onOpenJobs,\n staff = false,\n}: AiJobFollowProps) {\n const [job, replace] = useFollowedAiJob(user, orgId, initial)\n const [busy, setBusy] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n const phase = aiJobPhase(job)\n const line = PHASE_LINE[phase]\n const moving = phase === 'queued' || phase === 'planning' || phase === 'building'\n const settled = AI_JOB_TERMINAL_STATUSES.includes(job.status)\n const primary = aiJobPrimaryLink(job, orgSlug || orgSlugFromLocation())\n const problem = phase === 'attention' || phase === 'failed' ? aiJobProblem(job, line.text) : null\n const stepsDone = job.steps.filter((step) => step.status === 'done').length\n\n const decide = useCallback(\n async (request: () => ReturnType<typeof resumeAiJobRequest>) => {\n setBusy(true)\n setNotice(null)\n const { job: next, error } = await request()\n if (next) replace(next)\n if (error) setNotice(error)\n setBusy(false)\n },\n [replace],\n )\n\n const openJobs = () => {\n openAiJobs({ jobId: job.id })\n onOpenJobs?.()\n }\n\n return (\n <Stack spacing={2} aria-live=\"polite\">\n <Typography variant=\"body2\" color=\"text.secondary\">\n {intro}\n </Typography>\n <Alert\n severity={line.severity}\n icon={moving ? <CircularProgress size={18} aria-label={AI_JOB_PHASE_LABELS[phase]} /> : undefined}\n action={\n <Button color=\"inherit\" size=\"small\" variant=\"outlined\" onClick={openJobs}>\n {'Open AI jobs'}\n </Button>\n }\n >\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}>\n <Chip size=\"small\" label={AI_JOB_PHASE_LABELS[phase]} />\n {job.steps.length > 1 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${stepsDone} of ${job.steps.length} steps`}\n </Typography>\n ) : null}\n </Stack>\n <Typography variant=\"body2\" sx={{ mt: 0.5 }}>\n {problem ?? line.text}\n </Typography>\n </Alert>\n {primary ? (\n <Box>\n <Button variant=\"contained\" component={AppLink} href={primary.href}>\n {primary.label}\n </Button>\n </Box>\n ) : null}\n {/* The drawer's own plan and its Confirm, on the drawer's request. */}\n <AiJobPlan\n job={job}\n onResume={(target) => void decide(() => resumeAiJobRequest(user, orgId, target))}\n busy={busy}\n staff={staff}\n // \"Use the starter site instead\" on a guided start that did not work out (AGL-3594).\n user={user ?? undefined}\n />\n {!settled ? (\n <Box>\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => void decide(() => cancelAiJobRequest(user, orgId, job.id))}\n >\n {'Cancel job'}\n </Button>\n </Box>\n ) : null}\n {notice ? <Alert severity=\"warning\">{notice}</Alert> : null}\n </Stack>\n )\n}\n\nexport default AiJobFollow\n"],"names":["AppLink","Alert","Box","Button","Chip","CircularProgress","Stack","Typography","useCallback","useEffect","useRef","useState","AI_JOB_PHASE_LABELS","aiJobPhase","AI_JOB_TERMINAL_STATUSES","AiJobPlan","cancelAiJobRequest","resumeAiJobRequest","openAiJobs","publishAiJob","followAiJobEvents","aiJobPrimaryLink","aiJobProblem","PHASE_LINE","queued","severity","text","planning","building","attention","done","failed","canceled","orgSlugFromLocation","path","globalThis","location","pathname","split","useFollowedAiJob","user","orgId","initial","userRef","current","job","setJob","moving","status","undefined","controller","AbortController","id","signal","abort","replace","next","AiJobFollow","orgSlug","intro","onOpenJobs","staff","busy","setBusy","notice","setNotice","phase","line","settled","includes","primary","problem","stepsDone","steps","filter","step","length","decide","request","error","openJobs","jobId","spacing","aria-live","variant","color","icon","size","aria-label","action","onClick","direction","sx","alignItems","flexWrap","rowGap","label","mt","component","href","onResume","target","disabled"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { AI_SITE_BUILD_HREF } from '../model/ai-job-notice';
2
+ import type { AiJobOutput, AiJobSummary } from '../model/ai-jobs.types';
3
+ /**
4
+ * Where "open draft" goes, or `null` when the output has no page of its
5
+ * own — a `text` output carries its copy on the job and is shown inline.
6
+ * A versioned resource opens in the besigner on the version the job wrote;
7
+ * one the console lists without a detail page opens its list; a `theme`
8
+ * proposal opens the site's Theme section, where it is put in the editor.
9
+ *
10
+ * A console URL names a site by its SUBDOMAIN: the `[host]` segment resolves
11
+ * through the member's host projection by subdomain, never by document id.
12
+ * So an output that carries no `hostSubdomain` gets no link rather than one
13
+ * that opens no site.
14
+ */
15
+ export declare function aiJobOutputHref(output: AiJobOutput, orgSlug: string): string | null;
16
+ /**
17
+ * The ONE action a finished job's row, its notification and the dialog that
18
+ * started it lead with (AGL-3593): what the job built. Several pages from one
19
+ * job — a site's — open the site's Pages list, where they wait as drafts; one
20
+ * output with a page of its own opens that page; anything else has no single
21
+ * place to go, and the row's own links stand. `null` until the job is done.
22
+ */
23
+ export declare function aiJobPrimaryLink(job: Pick<AiJobSummary, 'status' | 'outputs'>, orgSlug: string): {
24
+ href: string;
25
+ label: string;
26
+ } | null;
27
+ export { AI_SITE_BUILD_HREF };
28
+ /**
29
+ * The "Building your site" page for one site job (AGL-3594): linkable, so the
30
+ * guided start lands on it, a reload keeps it and a notification opens it.
31
+ * `null` without the org slug or the site's subdomain a console URL needs.
32
+ */
33
+ export declare function aiSiteBuildHref(orgSlug: string | null | undefined, hostSubdomain: string | null | undefined, jobId: string): string | null;
34
+ /**
35
+ * A finished site's two ways in (AGL-3594): its first page's draft, opened in
36
+ * the console's preview of that version, and the Pages list every draft waits
37
+ * in. Either is `null` where the job reported nothing to open.
38
+ */
39
+ export declare function aiSiteBuildDoneLinks(job: Pick<AiJobSummary, 'outputs'>, orgSlug: string): {
40
+ view: string | null;
41
+ pages: string | null;
42
+ };
@@ -0,0 +1,140 @@
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 { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
17
+ import { pluginRecordHref, pluginRecordListHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
18
+ import { AI_JOB_BESIGNER_SEGMENT as BESIGNER_SEGMENT, AI_SITE_BUILD_HREF } from "../model/ai-job-notice.js";
19
+ // Where a job's work opens in the console (AGL-2904), shared by the AI jobs
20
+ // drawer and every dialog that follows the job it started (AGL-3593).
21
+ /**
22
+ * Where "open draft" goes, or `null` when the output has no page of its
23
+ * own — a `text` output carries its copy on the job and is shown inline.
24
+ * A versioned resource opens in the besigner on the version the job wrote;
25
+ * one the console lists without a detail page opens its list; a `theme`
26
+ * proposal opens the site's Theme section, where it is put in the editor.
27
+ *
28
+ * A console URL names a site by its SUBDOMAIN: the `[host]` segment resolves
29
+ * through the member's host projection by subdomain, never by document id.
30
+ * So an output that carries no `hostSubdomain` gets no link rather than one
31
+ * that opens no site.
32
+ */ export function aiJobOutputHref(output, orgSlug) {
33
+ const host = output.hostSubdomain;
34
+ if (!orgSlug || !host) return null;
35
+ const segment = BESIGNER_SEGMENT[output.resource];
36
+ if (segment) {
37
+ const base = `/${orgSlug}/hosts/${host}/${segment}/${output.id}`;
38
+ return output.versionId ? `${base}/versions/${output.versionId}/besigner` : base;
39
+ }
40
+ if (output.resource === 'product') {
41
+ // The catalog a drafted product lands in: the commerce plugin's address
42
+ // for it (AGL-3080), or no link where commerce is not loaded.
43
+ return pluginRecordListHref('product', {
44
+ orgSlug,
45
+ host
46
+ });
47
+ }
48
+ if (output.resource === 'workflow') {
49
+ // A drafted automation is an action (AGL-2919), listed switched off on
50
+ // the workflows plugin's list of actions, or no link where it is not loaded.
51
+ return pluginRecordListHref('action', {
52
+ orgSlug,
53
+ host
54
+ });
55
+ }
56
+ if (output.resource === 'theme') {
57
+ return buildRoute(Route.HOST_SETUP_THEME, {
58
+ orgSlug,
59
+ host
60
+ });
61
+ }
62
+ if (output.resource === 'form') {
63
+ // A new form has no version for the besigner to open: its own page — the
64
+ // forms plugin's address for it — mints the first one, and holds the
65
+ // routing and consent it declares.
66
+ return pluginRecordHref('form', {
67
+ orgSlug,
68
+ host
69
+ }, output.id);
70
+ }
71
+ if (output.resource === 'campaign') {
72
+ // A campaign's page is the marketing plugin's, under the site: its
73
+ // address for the campaign (AGL-3080), or no link where it is not loaded.
74
+ return pluginRecordHref('campaign', {
75
+ orgSlug,
76
+ host
77
+ }, output.id);
78
+ }
79
+ return null;
80
+ }
81
+ /**
82
+ * The ONE action a finished job's row, its notification and the dialog that
83
+ * started it lead with (AGL-3593): what the job built. Several pages from one
84
+ * job — a site's — open the site's Pages list, where they wait as drafts; one
85
+ * output with a page of its own opens that page; anything else has no single
86
+ * place to go, and the row's own links stand. `null` until the job is done.
87
+ */ export function aiJobPrimaryLink(job, orgSlug) {
88
+ if (job.status !== 'done' || !orgSlug) return null;
89
+ const screens = job.outputs.filter((output)=>output.resource === 'screen' && output.hostSubdomain);
90
+ if (screens.length > 1) {
91
+ return {
92
+ href: buildRoute(Route.HOST_SCREENS, {
93
+ orgSlug,
94
+ host: String(screens[0].hostSubdomain)
95
+ }),
96
+ label: 'Open your draft pages'
97
+ };
98
+ }
99
+ for (const output of job.outputs){
100
+ const href = aiJobOutputHref(output, orgSlug);
101
+ if (href) {
102
+ return {
103
+ href,
104
+ label: output.resource === 'screen' ? 'Open the draft page' : `Open ${output.label}`
105
+ };
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+ export { AI_SITE_BUILD_HREF };
111
+ /**
112
+ * The "Building your site" page for one site job (AGL-3594): linkable, so the
113
+ * guided start lands on it, a reload keeps it and a notification opens it.
114
+ * `null` without the org slug or the site's subdomain a console URL needs.
115
+ */ export function aiSiteBuildHref(orgSlug, hostSubdomain, jobId) {
116
+ if (!orgSlug || !hostSubdomain || !jobId) return null;
117
+ return `/${orgSlug}/hosts/${hostSubdomain}${AI_SITE_BUILD_HREF}/${encodeURIComponent(jobId)}`;
118
+ }
119
+ /**
120
+ * A finished site's two ways in (AGL-3594): its first page's draft, opened in
121
+ * the console's preview of that version, and the Pages list every draft waits
122
+ * in. Either is `null` where the job reported nothing to open.
123
+ */ export function aiSiteBuildDoneLinks(job, orgSlug) {
124
+ const screens = job.outputs.filter((output)=>output.resource === 'screen' && output.hostSubdomain);
125
+ const first = screens[0];
126
+ if (!first || !orgSlug) return {
127
+ view: null,
128
+ pages: null
129
+ };
130
+ const host = String(first.hostSubdomain);
131
+ return {
132
+ view: first.versionId ? `/${orgSlug}/hosts/${host}/screens/${first.id}/versions/${first.versionId}/view` : null,
133
+ pages: buildRoute(Route.HOST_SCREENS, {
134
+ orgSlug,
135
+ host
136
+ })
137
+ };
138
+ }
139
+
140
+ //# sourceMappingURL=ai-job-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-job-links.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 */\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n pluginRecordHref,\n pluginRecordListHref,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AI_JOB_BESIGNER_SEGMENT as BESIGNER_SEGMENT, AI_SITE_BUILD_HREF } from '../model/ai-job-notice'\nimport type { AiJobOutput, AiJobSummary } from '../model/ai-jobs.types'\n\n// Where a job's work opens in the console (AGL-2904), shared by the AI jobs\n// drawer and every dialog that follows the job it started (AGL-3593).\n\n/**\n * Where \"open draft\" goes, or `null` when the output has no page of its\n * own — a `text` output carries its copy on the job and is shown inline.\n * A versioned resource opens in the besigner on the version the job wrote;\n * one the console lists without a detail page opens its list; a `theme`\n * proposal opens the site's Theme section, where it is put in the editor.\n *\n * A console URL names a site by its SUBDOMAIN: the `[host]` segment resolves\n * through the member's host projection by subdomain, never by document id.\n * So an output that carries no `hostSubdomain` gets no link rather than one\n * that opens no site.\n */\nexport function aiJobOutputHref(output: AiJobOutput, orgSlug: string): string | null {\n const host = output.hostSubdomain\n if (!orgSlug || !host) return null\n const segment = BESIGNER_SEGMENT[output.resource]\n if (segment) {\n const base = `/${orgSlug}/hosts/${host}/${segment}/${output.id}`\n return output.versionId ? `${base}/versions/${output.versionId}/besigner` : base\n }\n if (output.resource === 'product') {\n // The catalog a drafted product lands in: the commerce plugin's address\n // for it (AGL-3080), or no link where commerce is not loaded.\n return pluginRecordListHref('product', { orgSlug, host })\n }\n if (output.resource === 'workflow') {\n // A drafted automation is an action (AGL-2919), listed switched off on\n // the workflows plugin's list of actions, or no link where it is not loaded.\n return pluginRecordListHref('action', { orgSlug, host })\n }\n if (output.resource === 'theme') {\n return buildRoute(Route.HOST_SETUP_THEME, { orgSlug, host })\n }\n if (output.resource === 'form') {\n // A new form has no version for the besigner to open: its own page — the\n // forms plugin's address for it — mints the first one, and holds the\n // routing and consent it declares.\n return pluginRecordHref('form', { orgSlug, host }, output.id)\n }\n if (output.resource === 'campaign') {\n // A campaign's page is the marketing plugin's, under the site: its\n // address for the campaign (AGL-3080), or no link where it is not loaded.\n return pluginRecordHref('campaign', { orgSlug, host }, output.id)\n }\n return null\n}\n\n/**\n * The ONE action a finished job's row, its notification and the dialog that\n * started it lead with (AGL-3593): what the job built. Several pages from one\n * job — a site's — open the site's Pages list, where they wait as drafts; one\n * output with a page of its own opens that page; anything else has no single\n * place to go, and the row's own links stand. `null` until the job is done.\n */\nexport function aiJobPrimaryLink(\n job: Pick<AiJobSummary, 'status' | 'outputs'>,\n orgSlug: string,\n): { href: string; label: string } | null {\n if (job.status !== 'done' || !orgSlug) return null\n const screens = job.outputs.filter(\n (output) => output.resource === 'screen' && output.hostSubdomain,\n )\n if (screens.length > 1) {\n return {\n href: buildRoute(Route.HOST_SCREENS, { orgSlug, host: String(screens[0].hostSubdomain) }),\n label: 'Open your draft pages',\n }\n }\n for (const output of job.outputs) {\n const href = aiJobOutputHref(output, orgSlug)\n if (href) {\n return {\n href,\n label: output.resource === 'screen' ? 'Open the draft page' : `Open ${output.label}`,\n }\n }\n }\n return null\n}\n\nexport { AI_SITE_BUILD_HREF }\n\n/**\n * The \"Building your site\" page for one site job (AGL-3594): linkable, so the\n * guided start lands on it, a reload keeps it and a notification opens it.\n * `null` without the org slug or the site's subdomain a console URL needs.\n */\nexport function aiSiteBuildHref(\n orgSlug: string | null | undefined,\n hostSubdomain: string | null | undefined,\n jobId: string,\n): string | null {\n if (!orgSlug || !hostSubdomain || !jobId) return null\n return `/${orgSlug}/hosts/${hostSubdomain}${AI_SITE_BUILD_HREF}/${encodeURIComponent(jobId)}`\n}\n\n/**\n * A finished site's two ways in (AGL-3594): its first page's draft, opened in\n * the console's preview of that version, and the Pages list every draft waits\n * in. Either is `null` where the job reported nothing to open.\n */\nexport function aiSiteBuildDoneLinks(\n job: Pick<AiJobSummary, 'outputs'>,\n orgSlug: string,\n): { view: string | null; pages: string | null } {\n const screens = job.outputs.filter((output) => output.resource === 'screen' && output.hostSubdomain)\n const first = screens[0]\n if (!first || !orgSlug) return { view: null, pages: null }\n const host = String(first.hostSubdomain)\n return {\n view: first.versionId\n ? `/${orgSlug}/hosts/${host}/screens/${first.id}/versions/${first.versionId}/view`\n : null,\n pages: buildRoute(Route.HOST_SCREENS, { orgSlug, host }),\n }\n}\n"],"names":["buildRoute","Route","pluginRecordHref","pluginRecordListHref","AI_JOB_BESIGNER_SEGMENT","BESIGNER_SEGMENT","AI_SITE_BUILD_HREF","aiJobOutputHref","output","orgSlug","host","hostSubdomain","segment","resource","base","id","versionId","HOST_SETUP_THEME","aiJobPrimaryLink","job","status","screens","outputs","filter","length","href","HOST_SCREENS","String","label","aiSiteBuildHref","jobId","encodeURIComponent","aiSiteBuildDoneLinks","first","view","pages"],"mappings":"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"}
@@ -16,12 +16,14 @@
16
16
  */
17
17
  import { type AiBuildPlanEmbed } from '../model/ai-build-plan';
18
18
  import type { AiJobReview, AiJobSummary } from '../model/ai-jobs.types';
19
+ import { AiSiteStarterFallback } from './ai-site-starter-fallback.component';
19
20
  /**
20
- * A job's plan and what it waits for (AGL-2935), inside the AI jobs drawer:
21
- * what the job reuses from the site, what it creates and why, the screens it
22
- * builds — and, while the job waits for a person, the one button that
23
- * confirms the plan or tries a refused step again. The drawer owns the
24
- * request; this renders.
21
+ * A job's plan and what it waits for (AGL-2935), inside the AI jobs drawer
22
+ * and the dialog that started the job (AGL-3593): what the job reuses from
23
+ * the site, what it creates and why, the screens it builds — and, while the
24
+ * job waits for a person, the one button that confirms the plan or tries a
25
+ * refused step again. Both send the same request (`resumeAiJobRequest`);
26
+ * this renders.
25
27
  */
26
28
  export interface AiJobPlanProps {
27
29
  job: AiJobSummary;
@@ -35,6 +37,11 @@ export interface AiJobPlanProps {
35
37
  * allow from an answer that ignored its re-ask. A member reads the findings.
36
38
  */
37
39
  staff?: boolean;
40
+ /**
41
+ * The signed-in user, for "Use the starter site instead" on a guided start
42
+ * that did not work out (AGL-3594); the action is drawn only when given.
43
+ */
44
+ user?: Parameters<typeof AiSiteStarterFallback>[0]['user'];
38
45
  }
39
46
  /**
40
47
  * Where a refused answer broke its rules, and the outline of those parts, as
@@ -50,5 +57,5 @@ export declare function aiJobReviewDetails(review: AiJobReview | null): string[]
50
57
  * host's code loads when a visitor reaches it, whether or not they press play.
51
58
  */
52
59
  export declare function aiPlanEmbedLine(embed: AiBuildPlanEmbed, named: (ref: string) => string): string;
53
- export declare function AiJobPlan({ job, onResume, busy, staff, }: AiJobPlanProps): JSX.Element | null;
60
+ export declare function AiJobPlan({ job, onResume, busy, staff, user, }: AiJobPlanProps): JSX.Element | null;
54
61
  export default AiJobPlan;
@@ -16,6 +16,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
16
  * limitations under the License.
17
17
  */ import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, isAiPlanNewRef } from "../model/ai-build-plan.js";
18
18
  import { aiJobPlanCreditEstimate } from "../model/ai-site-job.js";
19
+ import { aiSiteStarterFallbackOffered } from "../model/ai-job-failure-copy.js";
20
+ import { AiSiteStarterFallback } from "./ai-site-starter-fallback.component.js";
19
21
  import { Box, Button, Collapse, Stack, Typography } from "@mui/material";
20
22
  import { useState } from "react";
21
23
  /**
@@ -63,7 +65,7 @@ import { useState } from "react";
63
65
  const link = embed.url ? ` playing ${embed.url}` : ', its link left for you to paste';
64
66
  return `Embeds a ${host} player on ${where}${link}, as you asked (“${embed.asked}”). It loads ${host}’s own code when a visitor reaches it, whether or not they press play.`;
65
67
  }
66
- export function AiJobPlan({ job, onResume, busy = false, staff = false }) {
68
+ export function AiJobPlan({ job, onResume, busy = false, staff = false, user }) {
67
69
  var _plan_embeds;
68
70
  const [detailsOpen, setDetailsOpen] = useState(false);
69
71
  const { plan, review } = job;
@@ -138,7 +140,16 @@ export function AiJobPlan({ job, onResume, busy = false, staff = false }) {
138
140
  }, `embed-${index}`))
139
141
  ]
140
142
  }),
141
- (review == null ? void 0 : review.reason) === 'doctrine' && review.findings.length > 0 && /*#__PURE__*/ _jsx(Box, {
143
+ staff && (review == null ? void 0 : review.detail) && /*#__PURE__*/ _jsx(Typography, {
144
+ variant: "caption",
145
+ color: "text.secondary",
146
+ component: "div",
147
+ sx: {
148
+ mt: 0.5
149
+ },
150
+ children: review.detail
151
+ }),
152
+ (review == null ? void 0 : review.reason) === 'doctrine' && review.findings.length > 0 && (staff || !review.detail) && /*#__PURE__*/ _jsx(Box, {
142
153
  component: "ul",
143
154
  sx: {
144
155
  my: 0.5,
@@ -189,15 +200,30 @@ export function AiJobPlan({ job, onResume, busy = false, staff = false }) {
189
200
  },
190
201
  children: `Estimated cost: about ${estimate.toLocaleString('en-US')} credits. What it costs is what its steps spend.`
191
202
  }),
203
+ waiting && review.retryRefusal && review.reason !== 'plan' && /*#__PURE__*/ _jsx(Typography, {
204
+ variant: "caption",
205
+ color: "text.secondary",
206
+ component: "div",
207
+ sx: {
208
+ mt: 1
209
+ },
210
+ children: review.retryRefusal
211
+ }),
192
212
  waiting && /*#__PURE__*/ _jsx(Button, {
193
213
  size: "small",
194
214
  variant: "contained",
195
- disabled: busy,
215
+ // Disabled with its reason above when the Free allowance left
216
+ // cannot pay for another try (AGL-3594).
217
+ disabled: busy || review.reason !== 'plan' && Boolean(review.retryRefusal),
196
218
  onClick: ()=>onResume(job),
197
219
  sx: {
198
220
  mt: 1
199
221
  },
200
222
  children: review.reason === 'plan' ? 'Confirm plan' : 'Try again'
223
+ }),
224
+ waiting && user && aiSiteStarterFallbackOffered(job) && /*#__PURE__*/ _jsx(AiSiteStarterFallback, {
225
+ job: job,
226
+ user: user
201
227
  })
202
228
  ]
203
229
  });