@aglyn/plugins-ai 1.0.0-beta.233 → 1.0.0-beta.235

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 (199) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +25 -0
  3. package/src/lib/activity/ai-activity-actions.js +85 -4
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-brief-dialog.component.js +46 -2
  6. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  7. package/src/lib/components/ai-campaign-create.component.js +1 -1
  8. package/src/lib/components/ai-campaign-create.component.js.map +1 -1
  9. package/src/lib/components/ai-credits-card.component.js +3 -3
  10. package/src/lib/components/ai-credits-card.component.js.map +1 -1
  11. package/src/lib/components/ai-crm-email-draft.component.js +1 -1
  12. package/src/lib/components/ai-crm-email-draft.component.js.map +1 -1
  13. package/src/lib/components/ai-crm-import-mapping.component.js +1 -1
  14. package/src/lib/components/ai-crm-import-mapping.component.js.map +1 -1
  15. package/src/lib/components/ai-crm-record-card.component.js +1 -1
  16. package/src/lib/components/ai-crm-record-card.component.js.map +1 -1
  17. package/src/lib/components/ai-describe-automation.component.js +4 -2
  18. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  19. package/src/lib/components/ai-describe-org-automation.component.js +1 -1
  20. package/src/lib/components/ai-describe-org-automation.component.js.map +1 -1
  21. package/src/lib/components/ai-experiment-cards.component.js +2 -2
  22. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  23. package/src/lib/components/ai-explain-automation.component.js +1 -1
  24. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  25. package/src/lib/components/ai-insight-card.component.js +3 -1
  26. package/src/lib/components/ai-insight-card.component.js.map +1 -1
  27. package/src/lib/components/ai-job-links.d.ts +6 -0
  28. package/src/lib/components/ai-job-links.js +9 -0
  29. package/src/lib/components/ai-job-links.js.map +1 -1
  30. package/src/lib/components/ai-jobs-list-page.component.d.ts +6 -1
  31. package/src/lib/components/ai-jobs-list-page.component.js +15 -2
  32. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  33. package/src/lib/components/ai-logic.component.js +5 -3
  34. package/src/lib/components/ai-logic.component.js.map +1 -1
  35. package/src/lib/components/ai-media-create.component.js +88 -16
  36. package/src/lib/components/ai-media-create.component.js.map +1 -1
  37. package/src/lib/components/ai-model-selector.component.js +8 -0
  38. package/src/lib/components/ai-model-selector.component.js.map +1 -1
  39. package/src/lib/components/ai-overlay-cards.component.js +2 -2
  40. package/src/lib/components/ai-overlay-cards.component.js.map +1 -1
  41. package/src/lib/components/ai-product-copy-card.component.js +2 -1
  42. package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
  43. package/src/lib/components/ai-products-hub-card.component.js +2 -1
  44. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  45. package/src/lib/components/ai-revise-automation.component.js +1 -1
  46. package/src/lib/components/ai-revise-automation.component.js.map +1 -1
  47. package/src/lib/components/ai-seo-fields-card.component.js +7 -3
  48. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  49. package/src/lib/components/ai-site-batch-card.component.js +7 -4
  50. package/src/lib/components/ai-site-batch-card.component.js.map +1 -1
  51. package/src/lib/components/ai-site-build-page.component.js +48 -6
  52. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  53. package/src/lib/components/ai-site-seo-start-card.component.js +7 -4
  54. package/src/lib/components/ai-site-seo-start-card.component.js.map +1 -1
  55. package/src/lib/components/ai-site-start-card.component.js +152 -36
  56. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  57. package/src/lib/components/ai-theme-proposal-card.component.js +1 -0
  58. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  59. package/src/lib/components/ai-upsell-dialog.component.js +17 -3
  60. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  61. package/src/lib/components/ai-usage-strip.component.js +13 -2
  62. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  63. package/src/lib/components/assist-build-card.component.js +20 -3
  64. package/src/lib/components/assist-build-card.component.js.map +1 -1
  65. package/src/lib/components/assist-jobs-drawer.component.js +7 -1
  66. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  67. package/src/lib/components/billing-ai-allotments.component.js +2 -2
  68. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  69. package/src/lib/components/billing-ai-top-users.component.js +1 -0
  70. package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
  71. package/src/lib/components/billing-assist-overage-card.component.js +2 -2
  72. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  73. package/src/lib/components/host-ai-allotments.component.js +2 -2
  74. package/src/lib/components/host-ai-allotments.component.js.map +1 -1
  75. package/src/lib/components/member-ai-allotment-card.component.js +2 -2
  76. package/src/lib/components/member-ai-allotment-card.component.js.map +1 -1
  77. package/src/lib/components/staff-org-ai-card.component.js +1 -0
  78. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  79. package/src/lib/components/staff-org-ai-conversations.component.d.ts +9 -3
  80. package/src/lib/components/staff-org-ai-conversations.component.js +174 -83
  81. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
  82. package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
  83. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  84. package/src/lib/jobs/ai-job-admission.d.ts +2 -1
  85. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  86. package/src/lib/jobs/ai-job-email-step.d.ts +39 -0
  87. package/src/lib/jobs/ai-job-email-step.js +116 -5
  88. package/src/lib/jobs/ai-job-email-step.js.map +1 -1
  89. package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
  90. package/src/lib/jobs/ai-job-layout-language.js +6 -1
  91. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  92. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +14 -0
  93. package/src/lib/jobs/ai-job-layout-site-pages.js +43 -11
  94. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
  95. package/src/lib/jobs/ai-job-layout-step.js +3 -2
  96. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  97. package/src/lib/jobs/ai-job-page-language.d.ts +26 -1
  98. package/src/lib/jobs/ai-job-page-language.js +75 -13
  99. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  100. package/src/lib/jobs/ai-job-page-step.d.ts +3 -0
  101. package/src/lib/jobs/ai-job-page-step.js +32 -7
  102. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  103. package/src/lib/jobs/ai-job-plan-step.d.ts +34 -0
  104. package/src/lib/jobs/ai-job-plan-step.js +106 -14
  105. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  106. package/src/lib/jobs/ai-job-site-content.d.ts +81 -13
  107. package/src/lib/jobs/ai-job-site-content.js +210 -43
  108. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  109. package/src/lib/jobs/ai-job-site-step.d.ts +19 -0
  110. package/src/lib/jobs/ai-job-site-step.js +152 -51
  111. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  112. package/src/lib/jobs/ai-jobs.d.ts +8 -4
  113. package/src/lib/jobs/ai-jobs.js +19 -2
  114. package/src/lib/jobs/ai-jobs.js.map +1 -1
  115. package/src/lib/jobs/ai-layout-stock-photos.d.ts +98 -0
  116. package/src/lib/jobs/ai-layout-stock-photos.js +248 -0
  117. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -0
  118. package/src/lib/jobs/ai-site-publish.d.ts +11 -0
  119. package/src/lib/jobs/ai-site-publish.js +31 -1
  120. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  121. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.d.ts +151 -0
  122. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js +183 -0
  123. package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js.map +1 -0
  124. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.d.ts +27 -0
  125. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js +192 -0
  126. package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js.map +1 -0
  127. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.d.ts +27 -0
  128. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js +222 -0
  129. package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js.map +1 -0
  130. package/src/lib/layout-language/ai-layout-compiler.d.ts +37 -0
  131. package/src/lib/layout-language/ai-layout-compiler.js +1003 -19
  132. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  133. package/src/lib/layout-language/ai-layout-design.d.ts +114 -0
  134. package/src/lib/layout-language/ai-layout-design.js +209 -0
  135. package/src/lib/layout-language/ai-layout-design.js.map +1 -0
  136. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  137. package/src/lib/layout-language/ai-layout-frame.js +33 -8
  138. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  139. package/src/lib/layout-language/ai-layout-gaps.js +18 -2
  140. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  141. package/src/lib/layout-language/ai-layout-language.d.ts +2 -0
  142. package/src/lib/layout-language/ai-layout-language.js +15 -1
  143. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  144. package/src/lib/layout-language/ai-layout-links.d.ts +36 -0
  145. package/src/lib/layout-language/ai-layout-links.js +26 -2
  146. package/src/lib/layout-language/ai-layout-links.js.map +1 -1
  147. package/src/lib/layout-language/ai-layout-listings.d.ts +125 -0
  148. package/src/lib/layout-language/ai-layout-listings.js +187 -0
  149. package/src/lib/layout-language/ai-layout-listings.js.map +1 -0
  150. package/src/lib/layout-language/ai-layout-pictures.d.ts +11 -5
  151. package/src/lib/layout-language/ai-layout-pictures.js +80 -17
  152. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  153. package/src/lib/model/ai-job-notice.d.ts +9 -1
  154. package/src/lib/model/ai-job-notice.js +23 -0
  155. package/src/lib/model/ai-job-notice.js.map +1 -1
  156. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  157. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  158. package/src/lib/model/ai-site-build-progress.d.ts +12 -2
  159. package/src/lib/model/ai-site-build-progress.js +104 -15
  160. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  161. package/src/lib/model/ai-site-job.d.ts +236 -24
  162. package/src/lib/model/ai-site-job.js +269 -22
  163. package/src/lib/model/ai-site-job.js.map +1 -1
  164. package/src/lib/model/ai-site-kinds.d.ts +30 -8
  165. package/src/lib/model/ai-site-kinds.js +735 -116
  166. package/src/lib/model/ai-site-kinds.js.map +1 -1
  167. package/src/lib/model/ai-site-look.d.ts +21 -0
  168. package/src/lib/model/ai-site-look.js +63 -14
  169. package/src/lib/model/ai-site-look.js.map +1 -1
  170. package/src/lib/model/ai-site-start-seo.d.ts +30 -0
  171. package/src/lib/model/ai-site-start-seo.js +132 -11
  172. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  173. package/src/lib/model/ai-site-start.d.ts +2 -2
  174. package/src/lib/model/ai-site-start.js +3 -3
  175. package/src/lib/model/ai-site-start.js.map +1 -1
  176. package/src/lib/plugin.js +15 -3
  177. package/src/lib/plugin.js.map +1 -1
  178. package/src/lib/runtime/ai-doctrine-validators.d.ts +32 -1
  179. package/src/lib/runtime/ai-doctrine-validators.js +227 -4
  180. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  181. package/src/lib/runtime/ai-doctrine.js +5 -3
  182. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  183. package/src/lib/runtime/ai-palette.generated.d.ts +2 -0
  184. package/src/lib/runtime/ai-palette.generated.js +31 -5
  185. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  186. package/src/lib/server/ai-admin-conversations.d.ts +9 -0
  187. package/src/lib/server/ai-admin-conversations.js +151 -9
  188. package/src/lib/server/ai-admin-conversations.js.map +1 -1
  189. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  190. package/src/lib/server/ai-jobs-route.js +15 -3
  191. package/src/lib/server/ai-jobs-route.js.map +1 -1
  192. package/src/lib/server/assist-edit.js +5 -2
  193. package/src/lib/server/assist-edit.js.map +1 -1
  194. package/src/lib/usage/free-ai-credits-left.d.ts +54 -0
  195. package/src/lib/usage/free-ai-credits-left.js +62 -0
  196. package/src/lib/usage/free-ai-credits-left.js.map +1 -0
  197. package/src/lib/usage/staff-org-ai-conversations.d.ts +23 -0
  198. package/src/lib/usage/staff-org-ai-conversations.js +45 -0
  199. package/src/lib/usage/staff-org-ai-conversations.js.map +1 -1
@@ -303,8 +303,12 @@ export function AiSeoFieldsCard(props) {
303
303
  const running = Boolean(job && !isTerminal(job));
304
304
  const saveWord = subject.kind === 'product' ? 'Save product' : 'Save SEO';
305
305
  const unnamed = subject.kind === 'product' && !subject.name.trim();
306
- const help = pluginDocsHelp('aiSeo', {
307
- excerpt: 'Write a search listing from what the page says. It is put in the fields as unsaved changes; nothing is saved until you save.'
306
+ const help = subject.kind === 'product' ? pluginDocsHelp('aiSeo', {
307
+ anchor: '#write-a-products-listing',
308
+ excerpt: 'Write this product’s search title and description from its name, copy and options. They go in the fields unsaved; Save product keeps them.'
309
+ }) : pluginDocsHelp('aiSeo', {
310
+ anchor: '#write-a-pages-listing',
311
+ excerpt: 'Write this page’s search title and description from what the page says. They go in the fields unsaved; Save SEO keeps them.'
308
312
  });
309
313
  const stage = (offer, key)=>{
310
314
  proposeValues(offer, key);
@@ -340,7 +344,7 @@ export function AiSeoFieldsCard(props) {
340
344
  target: "_blank",
341
345
  rel: "noopener",
342
346
  variant: "caption",
343
- title: help.excerpt,
347
+ title: help.excerptText,
344
348
  children: 'How it works'
345
349
  })
346
350
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-seo-fields-card.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 { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport type { SeoKeywordCoverage } from '@aglyn/aglyn/app-utils/seo-keywords'\nimport {\n SEO_LISTING_FIELDS,\n seoListingFieldCount,\n seoListingFieldTooLong,\n type SeoListingFieldKey,\n} from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport type {\n ConsoleSeoFieldsZoneProps,\n ConsoleSeoFieldValues,\n} from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useHostOrgId, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n CircularProgress,\n Link,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { AI_JOB_TERMINAL_STATUSES, type AiJobSummary } from '../model/ai-jobs.types'\nimport {\n aiSeoAuditView,\n aiSeoFieldsProposalOf,\n type AiSeoFieldValues,\n} from '../model/ai-seo'\nimport { readEventFrames } from './assist-jobs-drawer.component'\n\n/**\n * \"Write SEO\" (AGL-2910), inside a search listing editor through the\n * `seoFields` zone: the screen detail page's SEO card, and the product\n * editor's search engine listing. A person asks; an `seo` job writes the\n * listing from what the page or the product says; the proposal is shown with\n * each field's count, and \"Put in the fields\" stages it in the editor above\n * as unsaved changes. The editor's own Save is the write — this card never\n * writes a listing, and a page's listing is what its live head serves.\n *\n * On a page, the card also offers what a site audit proposed for it, once\n * that audit was applied: its listing values wait here rather than being\n * written by the audit.\n *\n * It renders nothing until the jobs route has answered for this workspace:\n * the shell has decided the plan and the permission, but the release flag is\n * the route's, and a 404 there is this card staying absent.\n */\n\n/** How many recent jobs the card reads to find this listing's proposals. */\nconst RECENT_JOBS_READ = 20\n\n/** How much of a product's description a job carries. */\nconst PRODUCT_TEXT_MAX_CHARS = 2_000\n\nconst KEYWORDS_MAX_CHARS = 300\n\ntype Verdict = 'checking' | 'ready' | 'hidden'\n\nconst isTerminal = (job: AiJobSummary) => AI_JOB_TERMINAL_STATUSES.includes(job.status)\n\n/** The proposed values this editor can take: its own fields, and an image description only beside an image. */\nexport function aiSeoValuesForEditor(\n values: AiSeoFieldValues,\n fields: readonly SeoListingFieldKey[],\n hasImage: boolean,\n): ConsoleSeoFieldValues {\n const out: ConsoleSeoFieldValues = {}\n for (const key of fields) {\n const value = values[key]\n if (typeof value !== 'string' || !value.trim()) continue\n if (key === 'imageAlt' && !hasImage) continue\n out[key] = value\n }\n return out\n}\n\nfunction Coverage({ keywords }: { keywords: readonly SeoKeywordCoverage[] }) {\n if (!keywords.length) return null\n return (\n <Stack useFlexGap direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }} aria-label=\"Keyword coverage\">\n {keywords.map((entry) => {\n const covered = entry.inTitle || entry.inDescription\n return (\n <Chip\n key={entry.keyword}\n size=\"small\"\n variant=\"outlined\"\n color={covered ? 'success' : 'default'}\n label={covered ? entry.keyword : `${entry.keyword} — not used`}\n />\n )\n })}\n </Stack>\n )\n}\n\nfunction ProposedValues(props: {\n heading: string\n values: ConsoleSeoFieldValues\n fields: readonly SeoListingFieldKey[]\n}) {\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{props.heading}</Typography>\n {props.fields\n .filter((key) => props.values[key])\n .map((key) => (\n <Stack key={key} spacing={0.25}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{ flexGrow: 1 }}>\n {SEO_LISTING_FIELDS[key].label}\n </Typography>\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={seoListingFieldTooLong(key, props.values[key]) ? 'error' : 'default'}\n label={seoListingFieldCount(key, props.values[key])}\n />\n </Stack>\n <Typography variant=\"body2\" sx={{ wordBreak: 'break-word' }}>\n {props.values[key]}\n </Typography>\n </Stack>\n ))}\n </Stack>\n )\n}\n\nexport function AiSeoFieldsCard(props: ConsoleSeoFieldsZoneProps) {\n const { hostId, subject, fields, values, hasImage, proposeValues } = props\n // A plugin-hosted editor may not know the org; the site does.\n const hostOrgId = useHostOrgId(props.orgId ? undefined : hostId)\n const orgId = props.orgId ?? hostOrgId ?? undefined\n const { data: user } = useUser()\n // Held in a ref so the reads below key on WHO is signed in, never on the\n // identity of the object that says so.\n const userRef = useRef(user)\n userRef.current = user\n const uid = user?.uid ?? null\n const subjectKind = subject.kind\n const subjectId = subject.id ?? null\n\n const [verdict, setVerdict] = useState<Verdict>('checking')\n const [notice, setNotice] = useState<string | null>(null)\n const [keywords, setKeywords] = useState('')\n const [busy, setBusy] = useState(false)\n const [job, setJob] = useState<AiJobSummary | null>(null)\n const [fromAudit, setFromAudit] = useState<{ jobId: string; values: AiSeoFieldValues } | null>(null)\n const [staged, setStaged] = useState<string | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n useEffect(() => () => watchRef.current?.abort(), [])\n\n useEffect(() => {\n if (!orgId || !hostId || !uid) return\n let active = true\n setVerdict('checking')\n setJob(null)\n setFromAudit(null)\n void (async () => {\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs?orgId=${encodeURIComponent(orgId)}&limit=${RECENT_JOBS_READ}`,\n )\n if (!active) return\n // The flag off (404) and the plan without generation (403): the\n // feature is not this workspace's, so the card is not here at all.\n if (response.status === 404 || response.status === 403) {\n setVerdict('hidden')\n return\n }\n const payload = await response.json().catch(() => null)\n if (!active) return\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked\n ? lockdownRefusalText(locked)\n : String(payload?.error ?? 'Recent SEO proposals could not be loaded.'),\n )\n } else if (subjectId) {\n const jobs = ((payload?.jobs ?? []) as AiJobSummary[]).filter(\n (entry) => entry.kind === 'seo' && entry.hostId === hostId,\n )\n const latest = jobs.find((entry) => {\n const proposal = aiSeoFieldsProposalOf(entry)\n return proposal?.subject.kind === subjectKind && proposal.subject.id === subjectId\n })\n if (latest) setJob(latest)\n if (subjectKind === 'screen') {\n const audit = jobs.find((entry) => entry.applied?.staged.includes(subjectId))\n const fix = audit ? aiSeoAuditView(audit.outputs)?.fixes[subjectId] : undefined\n if (audit && fix && Object.keys(fix.values).length) {\n setFromAudit({ jobId: audit.id, values: fix.values })\n }\n }\n }\n setVerdict('ready')\n } catch {\n if (!active) return\n setNotice('Recent SEO proposals could not be loaded.')\n setVerdict('ready')\n }\n })()\n return () => {\n active = false\n }\n }, [orgId, hostId, uid, subjectKind, subjectId])\n\n /** Follows a job through the events route until it settles. */\n const watch = useCallback(async (jobId: string, org: string) => {\n watchRef.current?.abort()\n const controller = new AbortController()\n watchRef.current = controller\n let again = true\n while (again && !controller.signal.aborted) {\n again = false\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs/${encodeURIComponent(jobId)}/events?orgId=${encodeURIComponent(org)}`,\n { signal: controller.signal },\n )\n if (!response.ok || !response.body) return\n await readEventFrames(response.body, (event) => {\n if (event['type'] === 'state') setJob(event['job'] as AiJobSummary)\n else if (event['type'] === 'reconnect') again = true\n })\n } catch {\n return\n }\n }\n }, [])\n\n const write = useCallback(async () => {\n if (!orgId) return\n setBusy(true)\n setNotice(null)\n setStaged(null)\n const common = { fields: fields.join(','), keywords: keywords.trim() }\n const inputs =\n subject.kind === 'screen'\n ? { target: 'screen', screenId: subject.id, versionId: subject.versionId ?? '', ...common }\n : {\n target: 'product',\n productId: subject.id ?? '',\n name: subject.name.trim(),\n text: subject.description.slice(0, PRODUCT_TEXT_MAX_CHARS),\n currentTitle: values.title ?? '',\n currentDescription: values.description ?? '',\n ...common,\n }\n try {\n const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n orgId,\n hostId,\n kind: 'seo',\n brief: `Write the search listing for ${subject.name.trim() || 'this page'}`,\n inputs,\n }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked\n ? lockdownRefusalText(locked)\n : String(payload?.error ?? 'The SEO proposal could not be started — try again.'),\n )\n return\n }\n const next = payload?.job as AiJobSummary | undefined\n if (!next) return\n setJob(next)\n if (!isTerminal(next)) await watch(next.id, orgId)\n } catch {\n setNotice('The SEO proposal could not be started — try again.')\n } finally {\n setBusy(false)\n }\n }, [orgId, hostId, subject, fields, keywords, values.title, values.description, watch])\n\n if (verdict !== 'ready') return null\n\n const proposal = aiSeoFieldsProposalOf(job)\n const offered = proposal ? aiSeoValuesForEditor(proposal.values, fields, hasImage) : {}\n const audited = fromAudit ? aiSeoValuesForEditor(fromAudit.values, fields, hasImage) : {}\n const running = Boolean(job && !isTerminal(job))\n const saveWord = subject.kind === 'product' ? 'Save product' : 'Save SEO'\n const unnamed = subject.kind === 'product' && !subject.name.trim()\n const help = pluginDocsHelp('aiSeo', {\n excerpt: 'Write a search listing from what the page says. It is put in the fields as unsaved changes; nothing is saved until you save.',\n })\n\n const stage = (offer: ConsoleSeoFieldValues, key: string) => {\n proposeValues(offer, key)\n setStaged(key)\n }\n\n return (\n <Box sx={{ border: 1, borderColor: 'divider', borderRadius: 1, p: 1.5 }} aria-label=\"SEO assistant\">\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'baseline' }}>\n <Typography variant=\"subtitle2\" sx={{ flexGrow: 1 }}>\n {'Write with AI'}\n </Typography>\n <Link href={help.href} target=\"_blank\" rel=\"noopener\" variant=\"caption\" title={help.excerpt}>\n {'How it works'}\n </Link>\n </Stack>\n <TextField\n size=\"small\"\n label=\"Target keywords (optional)\"\n placeholder=\"brass desk lamps, dimmable\"\n helperText=\"Comma-separated. Used only where the page is about them, never repeated to rank.\"\n value={keywords}\n onChange={(event) => setKeywords(event.target.value.slice(0, KEYWORDS_MAX_CHARS))}\n disabled={busy || running}\n />\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button size=\"small\" variant=\"outlined\" disabled={busy || running || unnamed || !orgId} onClick={() => void write()}>\n {proposal ? 'Write again' : 'Write SEO'}\n </Button>\n {running ? <CircularProgress size={16} aria-label=\"Writing the listing\" /> : null}\n {unnamed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Name the product first.'}\n </Typography>\n ) : null}\n </Stack>\n {notice ? <Alert severity=\"warning\">{notice}</Alert> : null}\n {job?.error && !proposal ? (\n <Alert severity={job.status === 'failed' ? 'error' : 'warning'}>{job.error}</Alert>\n ) : null}\n\n {!proposal && fromAudit && Object.keys(audited).length ? (\n <Stack spacing={1}>\n <ProposedValues heading=\"Proposed by the site audit\" values={audited} fields={fields} />\n <Button\n size=\"small\"\n variant=\"contained\"\n sx={{ alignSelf: 'flex-start' }}\n disabled={staged === `audit:${fromAudit.jobId}`}\n onClick={() => stage(audited, `audit:${fromAudit.jobId}`)}\n >\n {'Put in the fields'}\n </Button>\n </Stack>\n ) : null}\n\n {proposal && job ? (\n <Stack spacing={1}>\n <ProposedValues heading=\"Proposed listing\" values={offered} fields={fields} />\n <Coverage keywords={proposal.keywords} />\n {proposal.notes.map((note) => (\n <Typography key={note} variant=\"caption\" color=\"text.secondary\">\n {note}\n </Typography>\n ))}\n {job.creditsSpent > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Used ${job.creditsSpent} ${job.creditsSpent === 1 ? 'credit' : 'credits'}.`}\n </Typography>\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n sx={{ alignSelf: 'flex-start' }}\n disabled={!Object.keys(offered).length || staged === job.id}\n onClick={() => stage(offered, job.id)}\n >\n {'Put in the fields'}\n </Button>\n </Stack>\n ) : null}\n\n {staged ? (\n <Alert severity=\"success\">\n {`In the fields as unsaved changes. Review them, then press ${saveWord}.`}\n </Alert>\n ) : null}\n </Stack>\n </Box>\n )\n}\nAiSeoFieldsCard.displayName = 'AiSeoFieldsCard'\n\nexport default AiSeoFieldsCard\n"],"names":["lockdownRefusalText","parseLockdownRefusal","pluginDocsHelp","SEO_LISTING_FIELDS","seoListingFieldCount","seoListingFieldTooLong","authorizedFetch","useHostOrgId","useUser","Alert","Box","Button","Chip","CircularProgress","Link","Stack","TextField","Typography","useCallback","useEffect","useRef","useState","AI_JOB_TERMINAL_STATUSES","aiSeoAuditView","aiSeoFieldsProposalOf","readEventFrames","RECENT_JOBS_READ","PRODUCT_TEXT_MAX_CHARS","KEYWORDS_MAX_CHARS","isTerminal","job","includes","status","aiSeoValuesForEditor","values","fields","hasImage","out","key","value","trim","Coverage","keywords","length","useFlexGap","direction","spacing","sx","flexWrap","rowGap","aria-label","map","entry","covered","inTitle","inDescription","size","variant","color","label","keyword","ProposedValues","props","heading","filter","alignItems","flexGrow","wordBreak","AiSeoFieldsCard","subject","hostId","proposeValues","hostOrgId","orgId","undefined","data","user","userRef","current","uid","subjectKind","kind","subjectId","id","verdict","setVerdict","notice","setNotice","setKeywords","busy","setBusy","setJob","fromAudit","setFromAudit","staged","setStaged","watchRef","abort","active","response","encodeURIComponent","payload","json","catch","ok","locked","String","error","jobs","latest","find","proposal","audit","applied","fix","outputs","fixes","Object","keys","jobId","watch","org","controller","AbortController","again","signal","aborted","body","event","write","common","join","inputs","target","screenId","versionId","productId","name","text","description","slice","currentTitle","title","currentDescription","method","headers","JSON","stringify","brief","next","offered","audited","running","Boolean","saveWord","unnamed","help","excerpt","stage","offer","border","borderColor","borderRadius","p","href","rel","placeholder","helperText","onChange","disabled","onClick","severity","alignSelf","notes","note","creditsSpent","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-seo-fields-card.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 { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport type { SeoKeywordCoverage } from '@aglyn/aglyn/app-utils/seo-keywords'\nimport {\n SEO_LISTING_FIELDS,\n seoListingFieldCount,\n seoListingFieldTooLong,\n type SeoListingFieldKey,\n} from '@aglyn/aglyn/app-utils/seo-listing-fields'\nimport type {\n ConsoleSeoFieldsZoneProps,\n ConsoleSeoFieldValues,\n} from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useHostOrgId, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n CircularProgress,\n Link,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { AI_JOB_TERMINAL_STATUSES, type AiJobSummary } from '../model/ai-jobs.types'\nimport {\n aiSeoAuditView,\n aiSeoFieldsProposalOf,\n type AiSeoFieldValues,\n} from '../model/ai-seo'\nimport { readEventFrames } from './assist-jobs-drawer.component'\n\n/**\n * \"Write SEO\" (AGL-2910), inside a search listing editor through the\n * `seoFields` zone: the screen detail page's SEO card, and the product\n * editor's search engine listing. A person asks; an `seo` job writes the\n * listing from what the page or the product says; the proposal is shown with\n * each field's count, and \"Put in the fields\" stages it in the editor above\n * as unsaved changes. The editor's own Save is the write — this card never\n * writes a listing, and a page's listing is what its live head serves.\n *\n * On a page, the card also offers what a site audit proposed for it, once\n * that audit was applied: its listing values wait here rather than being\n * written by the audit.\n *\n * It renders nothing until the jobs route has answered for this workspace:\n * the shell has decided the plan and the permission, but the release flag is\n * the route's, and a 404 there is this card staying absent.\n */\n\n/** How many recent jobs the card reads to find this listing's proposals. */\nconst RECENT_JOBS_READ = 20\n\n/** How much of a product's description a job carries. */\nconst PRODUCT_TEXT_MAX_CHARS = 2_000\n\nconst KEYWORDS_MAX_CHARS = 300\n\ntype Verdict = 'checking' | 'ready' | 'hidden'\n\nconst isTerminal = (job: AiJobSummary) => AI_JOB_TERMINAL_STATUSES.includes(job.status)\n\n/** The proposed values this editor can take: its own fields, and an image description only beside an image. */\nexport function aiSeoValuesForEditor(\n values: AiSeoFieldValues,\n fields: readonly SeoListingFieldKey[],\n hasImage: boolean,\n): ConsoleSeoFieldValues {\n const out: ConsoleSeoFieldValues = {}\n for (const key of fields) {\n const value = values[key]\n if (typeof value !== 'string' || !value.trim()) continue\n if (key === 'imageAlt' && !hasImage) continue\n out[key] = value\n }\n return out\n}\n\nfunction Coverage({ keywords }: { keywords: readonly SeoKeywordCoverage[] }) {\n if (!keywords.length) return null\n return (\n <Stack useFlexGap direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }} aria-label=\"Keyword coverage\">\n {keywords.map((entry) => {\n const covered = entry.inTitle || entry.inDescription\n return (\n <Chip\n key={entry.keyword}\n size=\"small\"\n variant=\"outlined\"\n color={covered ? 'success' : 'default'}\n label={covered ? entry.keyword : `${entry.keyword} — not used`}\n />\n )\n })}\n </Stack>\n )\n}\n\nfunction ProposedValues(props: {\n heading: string\n values: ConsoleSeoFieldValues\n fields: readonly SeoListingFieldKey[]\n}) {\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{props.heading}</Typography>\n {props.fields\n .filter((key) => props.values[key])\n .map((key) => (\n <Stack key={key} spacing={0.25}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{ flexGrow: 1 }}>\n {SEO_LISTING_FIELDS[key].label}\n </Typography>\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color={seoListingFieldTooLong(key, props.values[key]) ? 'error' : 'default'}\n label={seoListingFieldCount(key, props.values[key])}\n />\n </Stack>\n <Typography variant=\"body2\" sx={{ wordBreak: 'break-word' }}>\n {props.values[key]}\n </Typography>\n </Stack>\n ))}\n </Stack>\n )\n}\n\nexport function AiSeoFieldsCard(props: ConsoleSeoFieldsZoneProps) {\n const { hostId, subject, fields, values, hasImage, proposeValues } = props\n // A plugin-hosted editor may not know the org; the site does.\n const hostOrgId = useHostOrgId(props.orgId ? undefined : hostId)\n const orgId = props.orgId ?? hostOrgId ?? undefined\n const { data: user } = useUser()\n // Held in a ref so the reads below key on WHO is signed in, never on the\n // identity of the object that says so.\n const userRef = useRef(user)\n userRef.current = user\n const uid = user?.uid ?? null\n const subjectKind = subject.kind\n const subjectId = subject.id ?? null\n\n const [verdict, setVerdict] = useState<Verdict>('checking')\n const [notice, setNotice] = useState<string | null>(null)\n const [keywords, setKeywords] = useState('')\n const [busy, setBusy] = useState(false)\n const [job, setJob] = useState<AiJobSummary | null>(null)\n const [fromAudit, setFromAudit] = useState<{ jobId: string; values: AiSeoFieldValues } | null>(null)\n const [staged, setStaged] = useState<string | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n useEffect(() => () => watchRef.current?.abort(), [])\n\n useEffect(() => {\n if (!orgId || !hostId || !uid) return\n let active = true\n setVerdict('checking')\n setJob(null)\n setFromAudit(null)\n void (async () => {\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs?orgId=${encodeURIComponent(orgId)}&limit=${RECENT_JOBS_READ}`,\n )\n if (!active) return\n // The flag off (404) and the plan without generation (403): the\n // feature is not this workspace's, so the card is not here at all.\n if (response.status === 404 || response.status === 403) {\n setVerdict('hidden')\n return\n }\n const payload = await response.json().catch(() => null)\n if (!active) return\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked\n ? lockdownRefusalText(locked)\n : String(payload?.error ?? 'Recent SEO proposals could not be loaded.'),\n )\n } else if (subjectId) {\n const jobs = ((payload?.jobs ?? []) as AiJobSummary[]).filter(\n (entry) => entry.kind === 'seo' && entry.hostId === hostId,\n )\n const latest = jobs.find((entry) => {\n const proposal = aiSeoFieldsProposalOf(entry)\n return proposal?.subject.kind === subjectKind && proposal.subject.id === subjectId\n })\n if (latest) setJob(latest)\n if (subjectKind === 'screen') {\n const audit = jobs.find((entry) => entry.applied?.staged.includes(subjectId))\n const fix = audit ? aiSeoAuditView(audit.outputs)?.fixes[subjectId] : undefined\n if (audit && fix && Object.keys(fix.values).length) {\n setFromAudit({ jobId: audit.id, values: fix.values })\n }\n }\n }\n setVerdict('ready')\n } catch {\n if (!active) return\n setNotice('Recent SEO proposals could not be loaded.')\n setVerdict('ready')\n }\n })()\n return () => {\n active = false\n }\n }, [orgId, hostId, uid, subjectKind, subjectId])\n\n /** Follows a job through the events route until it settles. */\n const watch = useCallback(async (jobId: string, org: string) => {\n watchRef.current?.abort()\n const controller = new AbortController()\n watchRef.current = controller\n let again = true\n while (again && !controller.signal.aborted) {\n again = false\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs/${encodeURIComponent(jobId)}/events?orgId=${encodeURIComponent(org)}`,\n { signal: controller.signal },\n )\n if (!response.ok || !response.body) return\n await readEventFrames(response.body, (event) => {\n if (event['type'] === 'state') setJob(event['job'] as AiJobSummary)\n else if (event['type'] === 'reconnect') again = true\n })\n } catch {\n return\n }\n }\n }, [])\n\n const write = useCallback(async () => {\n if (!orgId) return\n setBusy(true)\n setNotice(null)\n setStaged(null)\n const common = { fields: fields.join(','), keywords: keywords.trim() }\n const inputs =\n subject.kind === 'screen'\n ? { target: 'screen', screenId: subject.id, versionId: subject.versionId ?? '', ...common }\n : {\n target: 'product',\n productId: subject.id ?? '',\n name: subject.name.trim(),\n text: subject.description.slice(0, PRODUCT_TEXT_MAX_CHARS),\n currentTitle: values.title ?? '',\n currentDescription: values.description ?? '',\n ...common,\n }\n try {\n const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n orgId,\n hostId,\n kind: 'seo',\n brief: `Write the search listing for ${subject.name.trim() || 'this page'}`,\n inputs,\n }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked\n ? lockdownRefusalText(locked)\n : String(payload?.error ?? 'The SEO proposal could not be started — try again.'),\n )\n return\n }\n const next = payload?.job as AiJobSummary | undefined\n if (!next) return\n setJob(next)\n if (!isTerminal(next)) await watch(next.id, orgId)\n } catch {\n setNotice('The SEO proposal could not be started — try again.')\n } finally {\n setBusy(false)\n }\n }, [orgId, hostId, subject, fields, keywords, values.title, values.description, watch])\n\n if (verdict !== 'ready') return null\n\n const proposal = aiSeoFieldsProposalOf(job)\n const offered = proposal ? aiSeoValuesForEditor(proposal.values, fields, hasImage) : {}\n const audited = fromAudit ? aiSeoValuesForEditor(fromAudit.values, fields, hasImage) : {}\n const running = Boolean(job && !isTerminal(job))\n const saveWord = subject.kind === 'product' ? 'Save product' : 'Save SEO'\n const unnamed = subject.kind === 'product' && !subject.name.trim()\n const help =\n subject.kind === 'product'\n ? pluginDocsHelp('aiSeo', {\n anchor: '#write-a-products-listing',\n excerpt:\n 'Write this product’s search title and description from its name, copy and options. They go in the fields unsaved; Save product keeps them.',\n })\n : pluginDocsHelp('aiSeo', {\n anchor: '#write-a-pages-listing',\n excerpt:\n 'Write this page’s search title and description from what the page says. They go in the fields unsaved; Save SEO keeps them.',\n })\n\n const stage = (offer: ConsoleSeoFieldValues, key: string) => {\n proposeValues(offer, key)\n setStaged(key)\n }\n\n return (\n <Box sx={{ border: 1, borderColor: 'divider', borderRadius: 1, p: 1.5 }} aria-label=\"SEO assistant\">\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'baseline' }}>\n <Typography variant=\"subtitle2\" sx={{ flexGrow: 1 }}>\n {'Write with AI'}\n </Typography>\n <Link href={help.href} target=\"_blank\" rel=\"noopener\" variant=\"caption\" title={help.excerptText}>\n {'How it works'}\n </Link>\n </Stack>\n <TextField\n size=\"small\"\n label=\"Target keywords (optional)\"\n placeholder=\"brass desk lamps, dimmable\"\n helperText=\"Comma-separated. Used only where the page is about them, never repeated to rank.\"\n value={keywords}\n onChange={(event) => setKeywords(event.target.value.slice(0, KEYWORDS_MAX_CHARS))}\n disabled={busy || running}\n />\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button size=\"small\" variant=\"outlined\" disabled={busy || running || unnamed || !orgId} onClick={() => void write()}>\n {proposal ? 'Write again' : 'Write SEO'}\n </Button>\n {running ? <CircularProgress size={16} aria-label=\"Writing the listing\" /> : null}\n {unnamed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Name the product first.'}\n </Typography>\n ) : null}\n </Stack>\n {notice ? <Alert severity=\"warning\">{notice}</Alert> : null}\n {job?.error && !proposal ? (\n <Alert severity={job.status === 'failed' ? 'error' : 'warning'}>{job.error}</Alert>\n ) : null}\n\n {!proposal && fromAudit && Object.keys(audited).length ? (\n <Stack spacing={1}>\n <ProposedValues heading=\"Proposed by the site audit\" values={audited} fields={fields} />\n <Button\n size=\"small\"\n variant=\"contained\"\n sx={{ alignSelf: 'flex-start' }}\n disabled={staged === `audit:${fromAudit.jobId}`}\n onClick={() => stage(audited, `audit:${fromAudit.jobId}`)}\n >\n {'Put in the fields'}\n </Button>\n </Stack>\n ) : null}\n\n {proposal && job ? (\n <Stack spacing={1}>\n <ProposedValues heading=\"Proposed listing\" values={offered} fields={fields} />\n <Coverage keywords={proposal.keywords} />\n {proposal.notes.map((note) => (\n <Typography key={note} variant=\"caption\" color=\"text.secondary\">\n {note}\n </Typography>\n ))}\n {job.creditsSpent > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Used ${job.creditsSpent} ${job.creditsSpent === 1 ? 'credit' : 'credits'}.`}\n </Typography>\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n sx={{ alignSelf: 'flex-start' }}\n disabled={!Object.keys(offered).length || staged === job.id}\n onClick={() => stage(offered, job.id)}\n >\n {'Put in the fields'}\n </Button>\n </Stack>\n ) : null}\n\n {staged ? (\n <Alert severity=\"success\">\n {`In the fields as unsaved changes. Review them, then press ${saveWord}.`}\n </Alert>\n ) : null}\n </Stack>\n </Box>\n )\n}\nAiSeoFieldsCard.displayName = 'AiSeoFieldsCard'\n\nexport default AiSeoFieldsCard\n"],"names":["lockdownRefusalText","parseLockdownRefusal","pluginDocsHelp","SEO_LISTING_FIELDS","seoListingFieldCount","seoListingFieldTooLong","authorizedFetch","useHostOrgId","useUser","Alert","Box","Button","Chip","CircularProgress","Link","Stack","TextField","Typography","useCallback","useEffect","useRef","useState","AI_JOB_TERMINAL_STATUSES","aiSeoAuditView","aiSeoFieldsProposalOf","readEventFrames","RECENT_JOBS_READ","PRODUCT_TEXT_MAX_CHARS","KEYWORDS_MAX_CHARS","isTerminal","job","includes","status","aiSeoValuesForEditor","values","fields","hasImage","out","key","value","trim","Coverage","keywords","length","useFlexGap","direction","spacing","sx","flexWrap","rowGap","aria-label","map","entry","covered","inTitle","inDescription","size","variant","color","label","keyword","ProposedValues","props","heading","filter","alignItems","flexGrow","wordBreak","AiSeoFieldsCard","subject","hostId","proposeValues","hostOrgId","orgId","undefined","data","user","userRef","current","uid","subjectKind","kind","subjectId","id","verdict","setVerdict","notice","setNotice","setKeywords","busy","setBusy","setJob","fromAudit","setFromAudit","staged","setStaged","watchRef","abort","active","response","encodeURIComponent","payload","json","catch","ok","locked","String","error","jobs","latest","find","proposal","audit","applied","fix","outputs","fixes","Object","keys","jobId","watch","org","controller","AbortController","again","signal","aborted","body","event","write","common","join","inputs","target","screenId","versionId","productId","name","text","description","slice","currentTitle","title","currentDescription","method","headers","JSON","stringify","brief","next","offered","audited","running","Boolean","saveWord","unnamed","help","anchor","excerpt","stage","offer","border","borderColor","borderRadius","p","href","rel","excerptText","placeholder","helperText","onChange","disabled","onClick","severity","alignSelf","notes","note","creditsSpent","displayName"],"mappings":"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"}
@@ -17,6 +17,7 @@
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  import { lockdownRefusalText, parseLockdownRefusal } from "@aglyn/aglyn";
20
+ import { pluginDocsHelp } from "@aglyn/aglyn/app-utils/docs-help";
20
21
  import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
21
22
  import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
22
23
  import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
@@ -291,10 +292,12 @@ export function AiSiteBatchCard(props) {
291
292
  return /*#__PURE__*/ _jsx(CardDisplay, {
292
293
  contentGutterX: true,
293
294
  contentGutterY: true,
294
- HeaderProps: {
295
- title: 'Generate sites with AI',
296
- subheader: 'One brief across many of your sites. Each site plans first and waits for you to ' + 'confirm it; everything it builds is a draft.'
297
- },
295
+ help: pluginDocsHelp('aiSite', {
296
+ anchor: '#generate-for-several-sites-at-once',
297
+ excerpt: 'One brief across many of your sites, with each site’s own name, city and brand. Each site plans first, waits for you to confirm it, and builds drafts.'
298
+ }),
299
+ header: 'Generate sites with AI',
300
+ subheader: 'One brief across many of your sites. Each site plans first and waits for you to ' + 'confirm it; everything it builds is a draft.',
298
301
  children: /*#__PURE__*/ _jsxs(Stack, {
299
302
  spacing: 2,
300
303
  children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-site-batch-card.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\n'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport type { ConsoleOrgSitesZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Checkbox,\n Chip,\n CircularProgress,\n Collapse,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n AI_JOB_TERMINAL_STATUSES,\n type AiJobStatus,\n type AiJobSummary,\n} from '../model/ai-jobs.types'\nimport {\n AI_SITE_BATCH_MAX,\n AI_SITE_PAGES,\n AI_SITE_SUBMISSION_CHOICES,\n aiSiteCreditEstimate,\n type AiSiteSubmissions,\n} from '../model/ai-site-job'\n\n/**\n * The agency batch (AGL-2911), on the organization's Sites page: one brief,\n * with the business name, the city and the brand changed per site, started\n * across many of the org's sites at once.\n *\n * What it starts are ordinary site scaffolds. Each plans first and waits for\n * its own confirmation in the Assist panel before it builds anything, and\n * everything it then builds is a draft — so this card publishes nothing and\n * changes no live site, however many sites it is pointed at.\n *\n * The estimate beside the button is the guard rail the issue asks for: what\n * one site is estimated to cost, and what the whole run is, before a credit\n * is spent on any of it.\n *\n * Where a contact form's submissions go is asked once for the run and binds\n * every site's form step (AGL-2918), in the words the guided start asks it\n * in: one decision, worded once, whether it is asked of one site or twenty.\n */\n\n/** Jobs the card reads to find the batches this workspace has run. */\nconst RECENT_JOBS_READ = 100\n\n/** A batch over one site is a scaffold; the card is for the other case. */\nconst MIN_SITES = 2\n\nconst STATUS_LABEL: Record<AiJobStatus, string> = {\n queued: 'Queued',\n running: 'Running',\n needs_input: 'Needs attention',\n needs_review: 'Confirm the plan',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\nconst STATUS_COLOR: Record<\n AiJobStatus,\n 'default' | 'primary' | 'success' | 'error' | 'warning'\n> = {\n queued: 'default',\n running: 'primary',\n needs_input: 'warning',\n needs_review: 'warning',\n done: 'success',\n failed: 'error',\n canceled: 'default',\n}\n\n/** A site's per-run variables, as the member typed them. */\nexport interface AiSiteBatchRow {\n hostId: string\n picked: boolean\n businessName: string\n city: string\n brand: string\n}\n\ntype Verdict = 'checking' | 'ready' | 'hidden'\n\nconst isTerminal = (job: AiJobSummary) =>\n AI_JOB_TERMINAL_STATUSES.includes(job.status)\n\n/** The jobs of one batch, newest batch first, as the list route serves them. */\nexport function aiSiteBatchJobs(\n jobs: readonly AiJobSummary[],\n batchId: string | null,\n): AiJobSummary[] {\n if (!batchId) return []\n return jobs.filter((job) => job.batch === batchId)\n}\n\n/** The batch a list of jobs most recently ran, or `null` when none did. */\nexport function aiLatestSiteBatch(\n jobs: readonly AiJobSummary[],\n): string | null {\n return jobs.find((job) => job.kind === 'site' && job.batch)?.batch ?? null\n}\n\nexport function AiSiteBatchCard(\n props: ConsoleOrgSitesZoneProps,\n): JSX.Element | null {\n const { orgMount, basePath } = props\n const { orgId, hosts, hostsReady } = orgMount\n const { data: user } = useUser()\n const userRef = useRef(user)\n userRef.current = user\n const uid = user?.uid ?? null\n const orgSlug = basePath.split('/').filter(Boolean)[0] ?? ''\n\n const [verdict, setVerdict] = useState<Verdict>('checking')\n const [open, setOpen] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n const [brief, setBrief] = useState('')\n const [businessType, setBusinessType] = useState('')\n const [pages, setPages] = useState<number>(AI_SITE_PAGES.min + 1)\n const [welcomeEmail, setWelcomeEmail] = useState(true)\n /*\n * One routing answer for the whole run. It starts on the Inbox — what every\n * submission does anyway — because filing a lead as well is the addition,\n * and an addition is the thing somebody should have to choose.\n */\n const [submissions, setSubmissions] = useState<AiSiteSubmissions>('inbox')\n const [rows, setRows] = useState<AiSiteBatchRow[]>([])\n const [batchId, setBatchId] = useState<string | null>(null)\n const [jobs, setJobs] = useState<AiJobSummary[]>([])\n\n // One row per site the page resolved for this reader, kept as the list moves.\n useEffect(() => {\n setRows((current) => {\n const byId = new Map(current.map((row) => [row.hostId, row]))\n return hosts.map(\n (host) =>\n byId.get(host.id) ?? {\n hostId: host.id,\n picked: false,\n businessName: host.name,\n city: '',\n brand: '',\n },\n )\n })\n }, [hosts])\n\n const load = useCallback(async () => {\n if (!orgId) return\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs?orgId=${encodeURIComponent(orgId)}&limit=${RECENT_JOBS_READ}`,\n )\n // The generative doors answer 404 behind their release flag and 403\n // without the add-on: a feature that is not released does not exist.\n if (response.status === 404 || response.status === 403)\n return 'hidden' as const\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked ? lockdownRefusalText(locked) : String(payload?.error ?? ''),\n )\n return 'ready' as const\n }\n const listed = (payload?.jobs ?? []) as AiJobSummary[]\n setJobs(listed)\n setBatchId((current) => current ?? aiLatestSiteBatch(listed))\n return 'ready' as const\n }, [orgId])\n\n useEffect(() => {\n if (!orgId || !uid) return\n let active = true\n setVerdict('checking')\n void (async () => {\n try {\n const answer = await load()\n if (active && answer) setVerdict(answer)\n } catch {\n if (active) setVerdict('ready')\n }\n })()\n return () => {\n active = false\n }\n }, [orgId, uid, load])\n\n const picked = useMemo(() => rows.filter((row) => row.picked), [rows])\n const perSite = aiSiteCreditEstimate(pages, { welcomeEmail })\n const runJobs = useMemo(() => aiSiteBatchJobs(jobs, batchId), [jobs, batchId])\n const siteName = useCallback(\n (hostId: string | null) =>\n hosts.find((host) => host.id === hostId)?.name ?? 'This site',\n [hosts],\n )\n const siteHref = useCallback(\n (hostId: string | null) => {\n const subdomain = hosts.find((host) => host.id === hostId)?.subdomain\n return subdomain ? `${orgMount.hostsPath}/${subdomain}` : null\n },\n [hosts, orgMount.hostsPath],\n )\n /*\n * One row per job of the run. A job has no page of its own — it is\n * confirmed and followed in the Assist panel — so the row opens nothing,\n * and the site it built for is a link when the console has an address for\n * that site.\n */\n const runColumns = useMemo<GridColDef<AiJobSummary>[]>(\n () => [\n {\n field: 'site',\n headerName: 'Site',\n flex: 1,\n minWidth: 180,\n valueGetter: (_value, job) => siteName(job.hostId),\n renderCell: ({ row: job }) => {\n const href = siteHref(job.hostId)\n return href ? (\n <AppLink href={href}>{siteName(job.hostId)}</AppLink>\n ) : (\n siteName(job.hostId)\n )\n },\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 180,\n valueGetter: (_value, job) => STATUS_LABEL[job.status],\n renderCell: ({ row: job }) => (\n <Chip\n size=\"small\"\n label={STATUS_LABEL[job.status]}\n color={STATUS_COLOR[job.status]}\n />\n ),\n },\n {\n field: 'drafts',\n headerName: 'Drafts',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 100,\n valueGetter: (_value, job) =>\n isTerminal(job) || job.outputs.length ? job.outputs.length : null,\n valueFormatter: (value: number | null) =>\n value === null ? '—' : String(value),\n },\n ],\n [siteHref, siteName],\n )\n\n const update = (hostId: string, patch: Partial<AiSiteBatchRow>) =>\n setRows((current) =>\n current.map((row) =>\n row.hostId === hostId ? { ...row, ...patch } : row,\n ),\n )\n\n const start = useCallback(async () => {\n if (!orgId || !picked.length) return\n setBusy(true)\n setNotice(null)\n try {\n const response = await authorizedFetch(\n userRef.current,\n '/api/ai/jobs/batch',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n orgId,\n brief,\n businessType,\n pages,\n welcomeEmail,\n submissions,\n sites: picked.map((row) => ({\n hostId: row.hostId,\n businessName: row.businessName,\n city: row.city,\n brand: row.brand,\n })),\n }),\n },\n )\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked\n ? lockdownRefusalText(locked)\n : String(payload?.error ?? 'The sites could not be started.'),\n )\n return\n }\n setBatchId(String(payload?.batchId ?? '') || null)\n setJobs((payload?.jobs ?? []) as AiJobSummary[])\n setOpen(false)\n const refused = (payload?.refused ?? []) as Array<{\n hostId: string\n error: string\n }>\n if (refused.length) {\n setNotice(\n refused\n .map((entry) => `${siteName(entry.hostId)}: ${entry.error}`)\n .join(' · '),\n )\n }\n } catch {\n setNotice('The sites could not be started.')\n } finally {\n setBusy(false)\n }\n }, [\n orgId,\n picked,\n brief,\n businessType,\n pages,\n welcomeEmail,\n submissions,\n siteName,\n ])\n\n if (verdict !== 'ready' || !hostsReady || hosts.length < MIN_SITES)\n return null\n\n return (\n <CardDisplay\n contentGutterX\n contentGutterY\n HeaderProps={{\n title: 'Generate sites with AI',\n subheader:\n 'One brief across many of your sites. Each site plans first and waits for you to ' +\n 'confirm it; everything it builds is a draft.',\n }}\n >\n <Stack spacing={2}>\n {notice && <Alert severity=\"info\">{notice}</Alert>}\n {runJobs.length > 0 && (\n <Box>\n <Typography variant=\"subtitle2\" gutterBottom>\n {`Last run · ${runJobs.length} ${runJobs.length === 1 ? 'site' : 'sites'}`}\n </Typography>\n <ListTable\n aria-label=\"Sites in this run\"\n rows={runJobs}\n columns={runColumns}\n getRowId={(job: AiJobSummary) => job.id}\n rowHeight={TABLE_ROW_HEIGHT}\n // One run's sites, at most `AI_SITE_BATCH_MAX`, read whole: a\n // bounded readout offers no filter to narrow it (AGL-3321).\n disableColumnFilter\n quickFilter={false}\n />\n </Box>\n )}\n <Box>\n <Button\n variant={open ? 'text' : 'contained'}\n onClick={() => setOpen(!open)}\n >\n {open ? 'Close' : 'Generate for several sites'}\n </Button>\n </Box>\n <Collapse in={open} unmountOnExit>\n <Stack spacing={2}>\n <TextField\n label=\"What these sites are for\"\n placeholder=\"A neighborhood dog groomer that takes bookings\"\n multiline\n minRows={2}\n value={brief}\n onChange={(event) => setBrief(event.target.value)}\n />\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>\n <TextField\n label=\"Kind of business\"\n placeholder=\"dog groomer\"\n value={businessType}\n onChange={(event) => setBusinessType(event.target.value)}\n sx={{ flex: 1 }}\n />\n <TextField\n select\n label=\"Pages per site\"\n value={pages}\n onChange={(event) => setPages(Number(event.target.value))}\n sx={{ minWidth: 160 }}\n >\n {Array.from(\n { length: AI_SITE_PAGES.max - AI_SITE_PAGES.min + 1 },\n (_, index) => AI_SITE_PAGES.min + index,\n ).map((count) => (\n <MenuItem key={count} value={count}>\n {count}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Welcome email\"\n value={welcomeEmail ? 'yes' : 'no'}\n onChange={(event) =>\n setWelcomeEmail(event.target.value === 'yes')\n }\n sx={{ minWidth: 160 }}\n >\n <MenuItem value=\"yes\">{'Draft one'}</MenuItem>\n <MenuItem value=\"no\">{'No'}</MenuItem>\n </TextField>\n </Stack>\n {/*\n Asked of the run, not of each site (AGL-2918): the forms a batch\n makes are the same form, and where a submission goes is the one\n thing about a contact form a model cannot know. Left unasked,\n twenty sites each kept a proposal nobody made.\n */}\n <TextField\n select\n fullWidth\n label=\"Where do form submissions go?\"\n value={submissions}\n onChange={(event) =>\n setSubmissions(event.target.value as AiSiteSubmissions)\n }\n helperText=\"The same for every site in this run. You can change it on any form afterwards.\"\n >\n {AI_SITE_SUBMISSION_CHOICES.map((option) => (\n <MenuItem key={option.id} value={option.id}>\n {`${option.label} — ${option.blurb}`}\n </MenuItem>\n ))}\n </TextField>\n <ScrollTable size=\"small\" aria-label=\"Sites to generate for\">\n <TableHead>\n <TableRow>\n <TableCell padding=\"checkbox\" />\n <TableCell>{'Site'}</TableCell>\n <TableCell>{'Business name'}</TableCell>\n <TableCell>{'City'}</TableCell>\n <TableCell>{'Brand'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {rows.map((row) => (\n <TableRow key={row.hostId}>\n <TableCell padding=\"checkbox\">\n <Checkbox\n checked={row.picked}\n slotProps={{\n input: {\n 'aria-label': `Generate for ${siteName(row.hostId)}`,\n },\n }}\n onChange={(event) =>\n update(row.hostId, { picked: event.target.checked })\n }\n />\n </TableCell>\n <TableCell>{siteName(row.hostId)}</TableCell>\n <TableCell>\n <TextField\n size=\"small\"\n variant=\"standard\"\n value={row.businessName}\n slotProps={{\n htmlInput: {\n 'aria-label': `Business name for ${siteName(row.hostId)}`,\n },\n }}\n onChange={(event) =>\n update(row.hostId, {\n businessName: event.target.value,\n })\n }\n />\n </TableCell>\n <TableCell>\n <TextField\n size=\"small\"\n variant=\"standard\"\n value={row.city}\n slotProps={{\n htmlInput: {\n 'aria-label': `City for ${siteName(row.hostId)}`,\n },\n }}\n onChange={(event) =>\n update(row.hostId, { city: event.target.value })\n }\n />\n </TableCell>\n <TableCell>\n <TextField\n size=\"small\"\n variant=\"standard\"\n value={row.brand}\n slotProps={{\n htmlInput: {\n 'aria-label': `Brand for ${siteName(row.hostId)}`,\n },\n }}\n onChange={(event) =>\n update(row.hostId, { brand: event.target.value })\n }\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {picked.length\n ? `Estimated cost: about ${perSite.toLocaleString('en-US')} credits a site, ` +\n `${(perSite * picked.length).toLocaleString('en-US')} for ${picked.length} ` +\n `${picked.length === 1 ? 'site' : 'sites'}. What each site really costs is ` +\n 'what its own steps spend.'\n : `Pick up to ${AI_SITE_BATCH_MAX} sites.`}\n </Typography>\n <Box>\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n !brief.trim() ||\n !businessType.trim() ||\n picked.length === 0 ||\n picked.length > AI_SITE_BATCH_MAX\n }\n onClick={() => void start()}\n startIcon={busy ? <CircularProgress size={16} /> : undefined}\n >\n {picked.length\n ? `Generate for ${picked.length} ${picked.length === 1 ? 'site' : 'sites'}`\n : 'Generate'}\n </Button>\n </Box>\n {orgSlug && (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {\n 'Each site waits for you to confirm its plan in the assistant panel.'\n }\n </Typography>\n )}\n </Stack>\n </Collapse>\n </Stack>\n </CardDisplay>\n )\n}\n\nexport default AiSiteBatchCard\n"],"names":["lockdownRefusalText","parseLockdownRefusal","AppLink","CardDisplay","ListTable","ScrollTable","TABLE_ROW_HEIGHT","authorizedFetch","useUser","Alert","Box","Button","Checkbox","Chip","CircularProgress","Collapse","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useCallback","useEffect","useMemo","useRef","useState","AI_JOB_TERMINAL_STATUSES","AI_SITE_BATCH_MAX","AI_SITE_PAGES","AI_SITE_SUBMISSION_CHOICES","aiSiteCreditEstimate","RECENT_JOBS_READ","MIN_SITES","STATUS_LABEL","queued","running","needs_input","needs_review","done","failed","canceled","STATUS_COLOR","isTerminal","job","includes","status","aiSiteBatchJobs","jobs","batchId","filter","batch","aiLatestSiteBatch","find","kind","AiSiteBatchCard","props","basePath","orgMount","orgId","hosts","hostsReady","data","user","userRef","current","uid","orgSlug","split","Boolean","verdict","setVerdict","open","setOpen","notice","setNotice","busy","setBusy","brief","setBrief","businessType","setBusinessType","pages","setPages","min","welcomeEmail","setWelcomeEmail","submissions","setSubmissions","rows","setRows","setBatchId","setJobs","byId","Map","map","row","hostId","host","get","id","picked","businessName","name","city","brand","load","response","encodeURIComponent","payload","json","catch","ok","locked","String","error","listed","active","answer","perSite","runJobs","siteName","siteHref","subdomain","hostsPath","runColumns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","href","width","size","label","color","type","align","headerAlign","outputs","length","valueFormatter","value","update","patch","start","method","headers","body","JSON","stringify","sites","refused","entry","join","contentGutterX","contentGutterY","HeaderProps","title","subheader","spacing","severity","variant","gutterBottom","aria-label","columns","getRowId","rowHeight","disableColumnFilter","quickFilter","onClick","in","unmountOnExit","placeholder","multiline","minRows","onChange","event","target","direction","xs","sm","sx","select","Number","Array","from","max","_","index","count","fullWidth","helperText","option","blurb","padding","checked","slotProps","input","htmlInput","toLocaleString","disabled","trim","startIcon","undefined"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-site-batch-card.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\n'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport type { ConsoleOrgSitesZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Checkbox,\n Chip,\n CircularProgress,\n Collapse,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n AI_JOB_TERMINAL_STATUSES,\n type AiJobStatus,\n type AiJobSummary,\n} from '../model/ai-jobs.types'\nimport {\n AI_SITE_BATCH_MAX,\n AI_SITE_PAGES,\n AI_SITE_SUBMISSION_CHOICES,\n aiSiteCreditEstimate,\n type AiSiteSubmissions,\n} from '../model/ai-site-job'\n\n/**\n * The agency batch (AGL-2911), on the organization's Sites page: one brief,\n * with the business name, the city and the brand changed per site, started\n * across many of the org's sites at once.\n *\n * What it starts are ordinary site scaffolds. Each plans first and waits for\n * its own confirmation in the Assist panel before it builds anything, and\n * everything it then builds is a draft — so this card publishes nothing and\n * changes no live site, however many sites it is pointed at.\n *\n * The estimate beside the button is the guard rail the issue asks for: what\n * one site is estimated to cost, and what the whole run is, before a credit\n * is spent on any of it.\n *\n * Where a contact form's submissions go is asked once for the run and binds\n * every site's form step (AGL-2918), in the words the guided start asks it\n * in: one decision, worded once, whether it is asked of one site or twenty.\n */\n\n/** Jobs the card reads to find the batches this workspace has run. */\nconst RECENT_JOBS_READ = 100\n\n/** A batch over one site is a scaffold; the card is for the other case. */\nconst MIN_SITES = 2\n\nconst STATUS_LABEL: Record<AiJobStatus, string> = {\n queued: 'Queued',\n running: 'Running',\n needs_input: 'Needs attention',\n needs_review: 'Confirm the plan',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\nconst STATUS_COLOR: Record<\n AiJobStatus,\n 'default' | 'primary' | 'success' | 'error' | 'warning'\n> = {\n queued: 'default',\n running: 'primary',\n needs_input: 'warning',\n needs_review: 'warning',\n done: 'success',\n failed: 'error',\n canceled: 'default',\n}\n\n/** A site's per-run variables, as the member typed them. */\nexport interface AiSiteBatchRow {\n hostId: string\n picked: boolean\n businessName: string\n city: string\n brand: string\n}\n\ntype Verdict = 'checking' | 'ready' | 'hidden'\n\nconst isTerminal = (job: AiJobSummary) =>\n AI_JOB_TERMINAL_STATUSES.includes(job.status)\n\n/** The jobs of one batch, newest batch first, as the list route serves them. */\nexport function aiSiteBatchJobs(\n jobs: readonly AiJobSummary[],\n batchId: string | null,\n): AiJobSummary[] {\n if (!batchId) return []\n return jobs.filter((job) => job.batch === batchId)\n}\n\n/** The batch a list of jobs most recently ran, or `null` when none did. */\nexport function aiLatestSiteBatch(\n jobs: readonly AiJobSummary[],\n): string | null {\n return jobs.find((job) => job.kind === 'site' && job.batch)?.batch ?? null\n}\n\nexport function AiSiteBatchCard(\n props: ConsoleOrgSitesZoneProps,\n): JSX.Element | null {\n const { orgMount, basePath } = props\n const { orgId, hosts, hostsReady } = orgMount\n const { data: user } = useUser()\n const userRef = useRef(user)\n userRef.current = user\n const uid = user?.uid ?? null\n const orgSlug = basePath.split('/').filter(Boolean)[0] ?? ''\n\n const [verdict, setVerdict] = useState<Verdict>('checking')\n const [open, setOpen] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n const [brief, setBrief] = useState('')\n const [businessType, setBusinessType] = useState('')\n const [pages, setPages] = useState<number>(AI_SITE_PAGES.min + 1)\n const [welcomeEmail, setWelcomeEmail] = useState(true)\n /*\n * One routing answer for the whole run. It starts on the Inbox — what every\n * submission does anyway — because filing a lead as well is the addition,\n * and an addition is the thing somebody should have to choose.\n */\n const [submissions, setSubmissions] = useState<AiSiteSubmissions>('inbox')\n const [rows, setRows] = useState<AiSiteBatchRow[]>([])\n const [batchId, setBatchId] = useState<string | null>(null)\n const [jobs, setJobs] = useState<AiJobSummary[]>([])\n\n // One row per site the page resolved for this reader, kept as the list moves.\n useEffect(() => {\n setRows((current) => {\n const byId = new Map(current.map((row) => [row.hostId, row]))\n return hosts.map(\n (host) =>\n byId.get(host.id) ?? {\n hostId: host.id,\n picked: false,\n businessName: host.name,\n city: '',\n brand: '',\n },\n )\n })\n }, [hosts])\n\n const load = useCallback(async () => {\n if (!orgId) return\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs?orgId=${encodeURIComponent(orgId)}&limit=${RECENT_JOBS_READ}`,\n )\n // The generative doors answer 404 behind their release flag and 403\n // without the add-on: a feature that is not released does not exist.\n if (response.status === 404 || response.status === 403)\n return 'hidden' as const\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked ? lockdownRefusalText(locked) : String(payload?.error ?? ''),\n )\n return 'ready' as const\n }\n const listed = (payload?.jobs ?? []) as AiJobSummary[]\n setJobs(listed)\n setBatchId((current) => current ?? aiLatestSiteBatch(listed))\n return 'ready' as const\n }, [orgId])\n\n useEffect(() => {\n if (!orgId || !uid) return\n let active = true\n setVerdict('checking')\n void (async () => {\n try {\n const answer = await load()\n if (active && answer) setVerdict(answer)\n } catch {\n if (active) setVerdict('ready')\n }\n })()\n return () => {\n active = false\n }\n }, [orgId, uid, load])\n\n const picked = useMemo(() => rows.filter((row) => row.picked), [rows])\n const perSite = aiSiteCreditEstimate(pages, { welcomeEmail })\n const runJobs = useMemo(() => aiSiteBatchJobs(jobs, batchId), [jobs, batchId])\n const siteName = useCallback(\n (hostId: string | null) =>\n hosts.find((host) => host.id === hostId)?.name ?? 'This site',\n [hosts],\n )\n const siteHref = useCallback(\n (hostId: string | null) => {\n const subdomain = hosts.find((host) => host.id === hostId)?.subdomain\n return subdomain ? `${orgMount.hostsPath}/${subdomain}` : null\n },\n [hosts, orgMount.hostsPath],\n )\n /*\n * One row per job of the run. A job has no page of its own — it is\n * confirmed and followed in the Assist panel — so the row opens nothing,\n * and the site it built for is a link when the console has an address for\n * that site.\n */\n const runColumns = useMemo<GridColDef<AiJobSummary>[]>(\n () => [\n {\n field: 'site',\n headerName: 'Site',\n flex: 1,\n minWidth: 180,\n valueGetter: (_value, job) => siteName(job.hostId),\n renderCell: ({ row: job }) => {\n const href = siteHref(job.hostId)\n return href ? (\n <AppLink href={href}>{siteName(job.hostId)}</AppLink>\n ) : (\n siteName(job.hostId)\n )\n },\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 180,\n valueGetter: (_value, job) => STATUS_LABEL[job.status],\n renderCell: ({ row: job }) => (\n <Chip\n size=\"small\"\n label={STATUS_LABEL[job.status]}\n color={STATUS_COLOR[job.status]}\n />\n ),\n },\n {\n field: 'drafts',\n headerName: 'Drafts',\n type: 'number',\n align: 'right',\n headerAlign: 'right',\n width: 100,\n valueGetter: (_value, job) =>\n isTerminal(job) || job.outputs.length ? job.outputs.length : null,\n valueFormatter: (value: number | null) =>\n value === null ? '—' : String(value),\n },\n ],\n [siteHref, siteName],\n )\n\n const update = (hostId: string, patch: Partial<AiSiteBatchRow>) =>\n setRows((current) =>\n current.map((row) =>\n row.hostId === hostId ? { ...row, ...patch } : row,\n ),\n )\n\n const start = useCallback(async () => {\n if (!orgId || !picked.length) return\n setBusy(true)\n setNotice(null)\n try {\n const response = await authorizedFetch(\n userRef.current,\n '/api/ai/jobs/batch',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n orgId,\n brief,\n businessType,\n pages,\n welcomeEmail,\n submissions,\n sites: picked.map((row) => ({\n hostId: row.hostId,\n businessName: row.businessName,\n city: row.city,\n brand: row.brand,\n })),\n }),\n },\n )\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked\n ? lockdownRefusalText(locked)\n : String(payload?.error ?? 'The sites could not be started.'),\n )\n return\n }\n setBatchId(String(payload?.batchId ?? '') || null)\n setJobs((payload?.jobs ?? []) as AiJobSummary[])\n setOpen(false)\n const refused = (payload?.refused ?? []) as Array<{\n hostId: string\n error: string\n }>\n if (refused.length) {\n setNotice(\n refused\n .map((entry) => `${siteName(entry.hostId)}: ${entry.error}`)\n .join(' · '),\n )\n }\n } catch {\n setNotice('The sites could not be started.')\n } finally {\n setBusy(false)\n }\n }, [\n orgId,\n picked,\n brief,\n businessType,\n pages,\n welcomeEmail,\n submissions,\n siteName,\n ])\n\n if (verdict !== 'ready' || !hostsReady || hosts.length < MIN_SITES)\n return null\n\n return (\n <CardDisplay\n contentGutterX\n contentGutterY\n help={pluginDocsHelp('aiSite', {\n anchor: '#generate-for-several-sites-at-once',\n excerpt:\n 'One brief across many of your sites, with each site’s own name, city and brand. Each site plans first, waits for you to confirm it, and builds drafts.',\n })}\n header={'Generate sites with AI'}\n subheader={\n 'One brief across many of your sites. Each site plans first and waits for you to ' +\n 'confirm it; everything it builds is a draft.'\n }\n >\n <Stack spacing={2}>\n {notice && <Alert severity=\"info\">{notice}</Alert>}\n {runJobs.length > 0 && (\n <Box>\n <Typography variant=\"subtitle2\" gutterBottom>\n {`Last run · ${runJobs.length} ${runJobs.length === 1 ? 'site' : 'sites'}`}\n </Typography>\n <ListTable\n aria-label=\"Sites in this run\"\n rows={runJobs}\n columns={runColumns}\n getRowId={(job: AiJobSummary) => job.id}\n rowHeight={TABLE_ROW_HEIGHT}\n // One run's sites, at most `AI_SITE_BATCH_MAX`, read whole: a\n // bounded readout offers no filter to narrow it (AGL-3321).\n disableColumnFilter\n quickFilter={false}\n />\n </Box>\n )}\n <Box>\n <Button\n variant={open ? 'text' : 'contained'}\n onClick={() => setOpen(!open)}\n >\n {open ? 'Close' : 'Generate for several sites'}\n </Button>\n </Box>\n <Collapse in={open} unmountOnExit>\n <Stack spacing={2}>\n <TextField\n label=\"What these sites are for\"\n placeholder=\"A neighborhood dog groomer that takes bookings\"\n multiline\n minRows={2}\n value={brief}\n onChange={(event) => setBrief(event.target.value)}\n />\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>\n <TextField\n label=\"Kind of business\"\n placeholder=\"dog groomer\"\n value={businessType}\n onChange={(event) => setBusinessType(event.target.value)}\n sx={{ flex: 1 }}\n />\n <TextField\n select\n label=\"Pages per site\"\n value={pages}\n onChange={(event) => setPages(Number(event.target.value))}\n sx={{ minWidth: 160 }}\n >\n {Array.from(\n { length: AI_SITE_PAGES.max - AI_SITE_PAGES.min + 1 },\n (_, index) => AI_SITE_PAGES.min + index,\n ).map((count) => (\n <MenuItem key={count} value={count}>\n {count}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Welcome email\"\n value={welcomeEmail ? 'yes' : 'no'}\n onChange={(event) =>\n setWelcomeEmail(event.target.value === 'yes')\n }\n sx={{ minWidth: 160 }}\n >\n <MenuItem value=\"yes\">{'Draft one'}</MenuItem>\n <MenuItem value=\"no\">{'No'}</MenuItem>\n </TextField>\n </Stack>\n {/*\n Asked of the run, not of each site (AGL-2918): the forms a batch\n makes are the same form, and where a submission goes is the one\n thing about a contact form a model cannot know. Left unasked,\n twenty sites each kept a proposal nobody made.\n */}\n <TextField\n select\n fullWidth\n label=\"Where do form submissions go?\"\n value={submissions}\n onChange={(event) =>\n setSubmissions(event.target.value as AiSiteSubmissions)\n }\n helperText=\"The same for every site in this run. You can change it on any form afterwards.\"\n >\n {AI_SITE_SUBMISSION_CHOICES.map((option) => (\n <MenuItem key={option.id} value={option.id}>\n {`${option.label} — ${option.blurb}`}\n </MenuItem>\n ))}\n </TextField>\n <ScrollTable size=\"small\" aria-label=\"Sites to generate for\">\n <TableHead>\n <TableRow>\n <TableCell padding=\"checkbox\" />\n <TableCell>{'Site'}</TableCell>\n <TableCell>{'Business name'}</TableCell>\n <TableCell>{'City'}</TableCell>\n <TableCell>{'Brand'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {rows.map((row) => (\n <TableRow key={row.hostId}>\n <TableCell padding=\"checkbox\">\n <Checkbox\n checked={row.picked}\n slotProps={{\n input: {\n 'aria-label': `Generate for ${siteName(row.hostId)}`,\n },\n }}\n onChange={(event) =>\n update(row.hostId, { picked: event.target.checked })\n }\n />\n </TableCell>\n <TableCell>{siteName(row.hostId)}</TableCell>\n <TableCell>\n <TextField\n size=\"small\"\n variant=\"standard\"\n value={row.businessName}\n slotProps={{\n htmlInput: {\n 'aria-label': `Business name for ${siteName(row.hostId)}`,\n },\n }}\n onChange={(event) =>\n update(row.hostId, {\n businessName: event.target.value,\n })\n }\n />\n </TableCell>\n <TableCell>\n <TextField\n size=\"small\"\n variant=\"standard\"\n value={row.city}\n slotProps={{\n htmlInput: {\n 'aria-label': `City for ${siteName(row.hostId)}`,\n },\n }}\n onChange={(event) =>\n update(row.hostId, { city: event.target.value })\n }\n />\n </TableCell>\n <TableCell>\n <TextField\n size=\"small\"\n variant=\"standard\"\n value={row.brand}\n slotProps={{\n htmlInput: {\n 'aria-label': `Brand for ${siteName(row.hostId)}`,\n },\n }}\n onChange={(event) =>\n update(row.hostId, { brand: event.target.value })\n }\n />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {picked.length\n ? `Estimated cost: about ${perSite.toLocaleString('en-US')} credits a site, ` +\n `${(perSite * picked.length).toLocaleString('en-US')} for ${picked.length} ` +\n `${picked.length === 1 ? 'site' : 'sites'}. What each site really costs is ` +\n 'what its own steps spend.'\n : `Pick up to ${AI_SITE_BATCH_MAX} sites.`}\n </Typography>\n <Box>\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n !brief.trim() ||\n !businessType.trim() ||\n picked.length === 0 ||\n picked.length > AI_SITE_BATCH_MAX\n }\n onClick={() => void start()}\n startIcon={busy ? <CircularProgress size={16} /> : undefined}\n >\n {picked.length\n ? `Generate for ${picked.length} ${picked.length === 1 ? 'site' : 'sites'}`\n : 'Generate'}\n </Button>\n </Box>\n {orgSlug && (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {\n 'Each site waits for you to confirm its plan in the assistant panel.'\n }\n </Typography>\n )}\n </Stack>\n </Collapse>\n </Stack>\n </CardDisplay>\n )\n}\n\nexport default AiSiteBatchCard\n"],"names":["lockdownRefusalText","parseLockdownRefusal","pluginDocsHelp","AppLink","CardDisplay","ListTable","ScrollTable","TABLE_ROW_HEIGHT","authorizedFetch","useUser","Alert","Box","Button","Checkbox","Chip","CircularProgress","Collapse","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useCallback","useEffect","useMemo","useRef","useState","AI_JOB_TERMINAL_STATUSES","AI_SITE_BATCH_MAX","AI_SITE_PAGES","AI_SITE_SUBMISSION_CHOICES","aiSiteCreditEstimate","RECENT_JOBS_READ","MIN_SITES","STATUS_LABEL","queued","running","needs_input","needs_review","done","failed","canceled","STATUS_COLOR","isTerminal","job","includes","status","aiSiteBatchJobs","jobs","batchId","filter","batch","aiLatestSiteBatch","find","kind","AiSiteBatchCard","props","basePath","orgMount","orgId","hosts","hostsReady","data","user","userRef","current","uid","orgSlug","split","Boolean","verdict","setVerdict","open","setOpen","notice","setNotice","busy","setBusy","brief","setBrief","businessType","setBusinessType","pages","setPages","min","welcomeEmail","setWelcomeEmail","submissions","setSubmissions","rows","setRows","setBatchId","setJobs","byId","Map","map","row","hostId","host","get","id","picked","businessName","name","city","brand","load","response","encodeURIComponent","payload","json","catch","ok","locked","String","error","listed","active","answer","perSite","runJobs","siteName","siteHref","subdomain","hostsPath","runColumns","field","headerName","flex","minWidth","valueGetter","_value","renderCell","href","width","size","label","color","type","align","headerAlign","outputs","length","valueFormatter","value","update","patch","start","method","headers","body","JSON","stringify","sites","refused","entry","join","contentGutterX","contentGutterY","help","anchor","excerpt","header","subheader","spacing","severity","variant","gutterBottom","aria-label","columns","getRowId","rowHeight","disableColumnFilter","quickFilter","onClick","in","unmountOnExit","placeholder","multiline","minRows","onChange","event","target","direction","xs","sm","sx","select","Number","Array","from","max","_","index","count","fullWidth","helperText","option","blurb","padding","checked","slotProps","input","htmlInput","toLocaleString","disabled","trim","startIcon","undefined"],"mappings":"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"}
@@ -18,8 +18,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn";
20
20
  import { PageHeaderRecord } from "@aglyn/aglyn/app-utils/page-header-record-context";
21
- import { mdiAlertCircle, mdiCheckCircle, mdiChevronDown, mdiClockOutline, mdiMinusCircleOutline } from "@aglyn/shared-data-mdi";
21
+ import { mdiAlertCircle, mdiCheckCircle, mdiChevronDown, mdiClockOutline, mdiMinusCircleOutline, mdiPauseCircle } from "@aglyn/shared-data-mdi";
22
22
  import { AppLink, MdiIcon } from "@aglyn/shared-ui-jsx";
23
+ import { newTabLinkProps } from "@aglyn/shared-ui-jsx/utils/new-tab";
23
24
  import { SITE_LIVE_NOTICE_BODY, SITE_LIVE_NOTICE_TITLE, SiteLiveNotice } from "@aglyn/shared-ui-jsx/components/site-live-notice.component";
24
25
  import { useUser } from "@aglyn/tenant-feature-instance";
25
26
  import { Accordion, AccordionDetails, AccordionSummary, Alert, Box, Button, Card, CircularProgress, Container, LinearProgress, Stack, Typography } from "@mui/material";
@@ -30,7 +31,7 @@ import { aiSiteStartAnswersFromInputs } from "../model/ai-site-start.js";
30
31
  import { AiSiteStartCard } from "./ai-site-start-card.component.js";
31
32
  import { AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
32
33
  import { followAiJobEvents } from "./ai-job-events.js";
33
- import { aiSiteBuildDoneLinks } from "./ai-job-links.js";
34
+ import { aiCreditsBillingHref, aiSiteBuildDoneLinks } from "./ai-job-links.js";
34
35
  import { AiJobPlan } from "./ai-job-plan.component.js";
35
36
  import { aiBuildCanRetry } from "../model/ai-build-progress.js";
36
37
  import { resumeAiJobRequest } from "./ai-job-requests.js";
@@ -122,6 +123,12 @@ const ROW_ICON = {
122
123
  path: mdiMinusCircleOutline.path,
123
124
  color: 'text.disabled',
124
125
  label: 'Not built'
126
+ },
127
+ // Paused by the meter (AGL-3660): no spinner — it is not running.
128
+ paused: {
129
+ path: mdiPauseCircle.path,
130
+ color: 'warning.main',
131
+ label: 'Paused'
125
132
  }
126
133
  };
127
134
  function RowIcon({ state }) {
@@ -339,6 +346,8 @@ export function AiSiteBuildPage({ hostId, segments, basePath }) {
339
346
  };
340
347
  const sitePublish = ready.kind === 'site' && phase === 'done' ? (_ready_sitePublish = ready.sitePublish) != null ? _ready_sitePublish : null : null;
341
348
  const liveUrl = sitePublish && sitePublish.published.length > 0 ? sitePublish.liveUrl : null;
349
+ // Paused by the meter (AGL-3660): out of credits or at a cap, carried on by Resume.
350
+ const paused = ready.status === 'needs_input';
342
351
  const retryRefusal = (_ready_review = ready.review) == null ? void 0 : _ready_review.retryRefusal;
343
352
  const buildRetry = aiBuildCanRetry(ready);
344
353
  const canRetry = phase === 'stopped' && ((_ready_review1 = ready.review) == null ? void 0 : _ready_review1.reason) === 'doctrine' || buildRetry;
@@ -554,13 +563,16 @@ export function AiSiteBuildPage({ hostId, segments, basePath }) {
554
563
  target: "_blank",
555
564
  rel: "noopener noreferrer",
556
565
  children: 'View your site'
557
- }) : links.view ? /*#__PURE__*/ _jsx(Button, {
566
+ }) : links.view ? // The console's own view of the first page, but still a VIEW, so
567
+ // it opens beside this page like the live one above (AGL-3660).
568
+ /*#__PURE__*/ _jsx(Button, _extends({
558
569
  variant: "contained",
559
570
  size: "large",
560
571
  component: AppLink,
561
- href: links.view,
572
+ href: links.view
573
+ }, newTabLinkProps, {
562
574
  children: 'View your site'
563
- }) : null,
575
+ })) : null,
564
576
  links.pages && /*#__PURE__*/ _jsx(Button, {
565
577
  variant: "outlined",
566
578
  size: "large",
@@ -570,7 +582,37 @@ export function AiSiteBuildPage({ hostId, segments, basePath }) {
570
582
  })
571
583
  ]
572
584
  }),
573
- (phase === 'stopped' || phase === 'failed' || phase === 'canceled') && /*#__PURE__*/ _jsxs(Stack, {
585
+ paused && // Paused by the meter (AGL-3660): the way to more credits — the
586
+ // workspace's own Billing page, where plans and the AI add-on are
587
+ // sold — and Resume, which carries on this same job from its paused
588
+ // step once credits are there again.
589
+ /*#__PURE__*/ _jsxs(Stack, {
590
+ direction: {
591
+ xs: 'column',
592
+ sm: 'row'
593
+ },
594
+ spacing: 2,
595
+ sx: {
596
+ alignItems: {
597
+ sm: 'center'
598
+ }
599
+ },
600
+ children: [
601
+ /*#__PURE__*/ _jsx(Button, {
602
+ variant: "contained",
603
+ disabled: busy,
604
+ onClick: ()=>void confirmPlan(ready),
605
+ children: busy ? 'Resuming…' : 'Resume'
606
+ }),
607
+ orgSlug ? /*#__PURE__*/ _jsx(Button, {
608
+ variant: "outlined",
609
+ component: AppLink,
610
+ href: aiCreditsBillingHref(orgSlug),
611
+ children: 'Get more AI credits'
612
+ }) : null
613
+ ]
614
+ }),
615
+ !paused && (phase === 'stopped' || phase === 'failed' || phase === 'canceled') && /*#__PURE__*/ _jsxs(Stack, {
574
616
  spacing: 1,
575
617
  children: [
576
618
  retryRefusal && /*#__PURE__*/ _jsx(Typography, {