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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +2 -1
  2. package/package.json +17 -17
  3. package/src/lib/billing/ai-month-meter.js +5 -3
  4. package/src/lib/billing/ai-month-meter.js.map +1 -1
  5. package/src/lib/billing/ai-overage-ledger.js +3 -1
  6. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  7. package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
  8. package/src/lib/components/ai-brief-dialog.component.js +35 -17
  9. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  10. package/src/lib/components/ai-describe-automation.component.js +17 -2
  11. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  12. package/src/lib/components/ai-insight-dialog.component.js +24 -2
  13. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  14. package/src/lib/components/ai-job-events.d.ts +14 -0
  15. package/src/lib/components/ai-job-events.js +76 -0
  16. package/src/lib/components/ai-job-events.js.map +1 -0
  17. package/src/lib/components/ai-job-follow.component.d.ts +49 -0
  18. package/src/lib/components/ai-job-follow.component.js +210 -0
  19. package/src/lib/components/ai-job-follow.component.js.map +1 -0
  20. package/src/lib/components/ai-job-links.d.ts +42 -0
  21. package/src/lib/components/ai-job-links.js +140 -0
  22. package/src/lib/components/ai-job-links.js.map +1 -0
  23. package/src/lib/components/ai-job-plan.component.d.ts +13 -6
  24. package/src/lib/components/ai-job-plan.component.js +29 -3
  25. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  26. package/src/lib/components/ai-job-requests.d.ts +24 -0
  27. package/src/lib/components/ai-job-requests.js +76 -0
  28. package/src/lib/components/ai-job-requests.js.map +1 -0
  29. package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
  30. package/src/lib/components/ai-jobs-indicator.component.js +80 -0
  31. package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
  32. package/src/lib/components/ai-jobs-store.d.ts +81 -0
  33. package/src/lib/components/ai-jobs-store.js +218 -0
  34. package/src/lib/components/ai-jobs-store.js.map +1 -0
  35. package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
  36. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  37. package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
  38. package/src/lib/components/ai-site-build-page.component.js +377 -0
  39. package/src/lib/components/ai-site-build-page.component.js.map +1 -0
  40. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  41. package/src/lib/components/ai-site-start-card.component.js +522 -130
  42. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  43. package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
  44. package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
  45. package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
  46. package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
  47. package/src/lib/components/assist-jobs-drawer.component.js +152 -172
  48. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  49. package/src/lib/components/assist-panel.component.js +62 -6
  50. package/src/lib/components/assist-panel.component.js.map +1 -1
  51. package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
  52. package/src/lib/components/staff-ai-credit-return.component.js +346 -0
  53. package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
  54. package/src/lib/components/staff-org-ai-card.component.js +364 -279
  55. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  56. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
  57. package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
  58. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  59. package/src/lib/components/use-ai-job-run.js +5 -18
  60. package/src/lib/components/use-ai-job-run.js.map +1 -1
  61. package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
  62. package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
  63. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
  64. package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
  65. package/src/lib/jobs/ai-job-plan-step.js +161 -17
  66. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
  68. package/src/lib/jobs/ai-job-site-step.js +34 -4
  69. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
  71. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
  73. package/src/lib/jobs/ai-jobs-notify.js +51 -0
  74. package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
  75. package/src/lib/jobs/ai-jobs.d.ts +31 -4
  76. package/src/lib/jobs/ai-jobs.js +196 -32
  77. package/src/lib/jobs/ai-jobs.js.map +1 -1
  78. package/src/lib/model/ai-job-activity.d.ts +112 -0
  79. package/src/lib/model/ai-job-activity.js +144 -0
  80. package/src/lib/model/ai-job-activity.js.map +1 -0
  81. package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
  82. package/src/lib/model/ai-job-failure-copy.js +78 -0
  83. package/src/lib/model/ai-job-failure-copy.js.map +1 -0
  84. package/src/lib/model/ai-job-notice.d.ts +61 -0
  85. package/src/lib/model/ai-job-notice.js +112 -0
  86. package/src/lib/model/ai-job-notice.js.map +1 -0
  87. package/src/lib/model/ai-jobs.types.d.ts +26 -0
  88. package/src/lib/model/ai-jobs.types.js +1 -0
  89. package/src/lib/model/ai-jobs.types.js.map +1 -1
  90. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  91. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  92. package/src/lib/model/ai-site-build-progress.d.ts +45 -0
  93. package/src/lib/model/ai-site-build-progress.js +76 -0
  94. package/src/lib/model/ai-site-build-progress.js.map +1 -0
  95. package/src/lib/model/ai-site-inventory.d.ts +9 -0
  96. package/src/lib/model/ai-site-inventory.js +7 -2
  97. package/src/lib/model/ai-site-inventory.js.map +1 -1
  98. package/src/lib/model/ai-site-job.d.ts +93 -2
  99. package/src/lib/model/ai-site-job.js +88 -7
  100. package/src/lib/model/ai-site-job.js.map +1 -1
  101. package/src/lib/model/ai-site-start.d.ts +3 -1
  102. package/src/lib/model/ai-site-start.js +6 -4
  103. package/src/lib/model/ai-site-start.js.map +1 -1
  104. package/src/lib/model/ai-template-subjects.d.ts +4 -3
  105. package/src/lib/model/ai-template-subjects.js +5 -4
  106. package/src/lib/model/ai-template-subjects.js.map +1 -1
  107. package/src/lib/plan-entitlements.d.ts +3 -3
  108. package/src/lib/plan-entitlements.js +3 -3
  109. package/src/lib/plan-entitlements.js.map +1 -1
  110. package/src/lib/plugin.js +28 -0
  111. package/src/lib/plugin.js.map +1 -1
  112. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  113. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  114. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  115. package/src/lib/runtime/ai-doctrine-validators.js +31 -4
  116. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  117. package/src/lib/runtime/ai-gate.d.ts +3 -2
  118. package/src/lib/runtime/ai-gate.js +3 -2
  119. package/src/lib/runtime/ai-gate.js.map +1 -1
  120. package/src/lib/runtime/ai-palette.generated.js +12 -4
  121. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  122. package/src/lib/runtime/site-inventory.d.ts +16 -1
  123. package/src/lib/runtime/site-inventory.js +43 -3
  124. package/src/lib/runtime/site-inventory.js.map +1 -1
  125. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  126. package/src/lib/server/ai-admin-credits.js +213 -0
  127. package/src/lib/server/ai-admin-credits.js.map +1 -0
  128. package/src/lib/server/ai-admin-org.js +12 -5
  129. package/src/lib/server/ai-admin-org.js.map +1 -1
  130. package/src/lib/server/ai-admin-signals.js +3 -1
  131. package/src/lib/server/ai-admin-signals.js.map +1 -1
  132. package/src/lib/server/ai-admin-user.js +12 -1
  133. package/src/lib/server/ai-admin-user.js.map +1 -1
  134. package/src/lib/server/ai-allotments.js +3 -2
  135. package/src/lib/server/ai-allotments.js.map +1 -1
  136. package/src/lib/server/ai-jobs-route.js +20 -2
  137. package/src/lib/server/ai-jobs-route.js.map +1 -1
  138. package/src/lib/server/assist-chat.js +3 -2
  139. package/src/lib/server/assist-chat.js.map +1 -1
  140. package/src/lib/server/billing-credits.js +4 -3
  141. package/src/lib/server/billing-credits.js.map +1 -1
  142. package/src/lib/server/billing-overage.js +3 -2
  143. package/src/lib/server/billing-overage.js.map +1 -1
  144. package/src/lib/server.js +11 -0
  145. package/src/lib/server.js.map +1 -1
  146. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  147. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  148. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  149. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  150. package/src/lib/usage/assist-credit-returns.js +79 -0
  151. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  152. package/src/lib/usage/assist-free-taste.js +6 -3
  153. package/src/lib/usage/assist-free-taste.js.map +1 -1
  154. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  155. package/src/lib/usage/assist-job-refund.js +109 -0
  156. package/src/lib/usage/assist-job-refund.js.map +1 -0
  157. package/src/lib/usage/assist-usage.js +5 -2
  158. package/src/lib/usage/assist-usage.js.map +1 -1
  159. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  160. package/src/lib/usage/staff-org-ai.js +19 -2
  161. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-describe-automation.component.tsx"],"sourcesContent":["'use client'\n\n/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport type { ConsoleHostAutomationsZoneProps } from './ai-automation-zones'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n CircularProgress,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Link,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { aiJobProblem, useAiJobRun, useAiJobsVerdict } from './use-ai-job-run'\n\n/**\n * \"Describe it\" on the Automation page's Actions (AGL-2919), beside Add action\n * and Recipes through the `hostAutomations` zone: a description becomes a\n * `workflow` job that drafts one automation, switched off.\n *\n * The dialog follows the shared brief dialog's shape — one text box, what\n * happens next, and the action that starts the job — and then follows the\n * job, because an automation's draft needs no plan to confirm: when it is\n * written, the dialog says what to fill in and opens it in the Actions editor.\n */\n\n/** The longest description a job admits. */\nconst BRIEF_MAX_CHARS = 4_000\n\nexport const AI_AUTOMATION_BRIEF_COPY = {\n title: 'Describe an automation',\n briefLabel: 'What should happen, and when?',\n placeholder:\n 'When someone submits the newsletter form, add them to the newsletter list, make them ' +\n 'a lead and send them a welcome email',\n next:\n 'The automation is drafted switched off, using only the triggers and steps your plan ' +\n 'includes. Anything it needs that your site does not have yet is left for you to fill in.',\n submit: 'Draft the automation',\n started:\n 'The automation is being drafted. This can take a few minutes. You can close this ' +\n 'window: the draft appears in the list, switched off, when it is ready.',\n drafted: 'Drafted',\n review: 'Review it',\n notListed: 'The draft is saved. It appears in the list in a moment.',\n failed: 'The automation could not be drafted. Try again.',\n} as const\n\nexport interface AiAutomationBriefDialogProps {\n open: boolean\n onClose: () => void\n orgId: string | undefined\n hostId: string\n /** Who is signed in, whose token the requests carry. */\n user: Parameters<typeof useAiJobRun>[0]\n /** Opens a listed action in the Actions editor; `false` when the list has not read it yet. */\n openAction: (actionId: string) => boolean\n}\n\nexport function AiAutomationBriefDialog(props: AiAutomationBriefDialogProps) {\n const { open, onClose, orgId, hostId, user, openAction } = props\n const copy = AI_AUTOMATION_BRIEF_COPY\n const help = pluginDocsHelp('aiAutomations')\n const [brief, setBrief] = useState('')\n const [notListed, setNotListed] = useState(false)\n const run = useAiJobRun(user, copy.failed)\n const { job, starting, running, notice, reset } = run\n\n // Opening or closing starts over and stops following: a job still drafting\n // carries on in AI jobs, and its draft appears in the list when it is written.\n useEffect(() => {\n reset()\n setNotListed(false)\n }, [open, reset])\n\n const draft = job?.status === 'done' ? job.outputs.find((output) => output.resource === 'workflow') : undefined\n const problem = aiJobProblem(job, copy.failed) ?? (job?.status === 'done' && !draft ? copy.failed : null)\n const asking = !job || Boolean(notice)\n\n const start = async () => {\n const trimmed = brief.trim()\n if (!orgId || !trimmed) return\n setNotListed(false)\n if (await run.start({ orgId, hostId, kind: 'workflow', brief: trimmed, inputs: { mode: 'draft' } })) {\n setBrief('')\n }\n }\n\n const review = () => {\n if (!draft) return\n if (openAction(draft.id)) onClose()\n else setNotListed(true)\n }\n\n return (\n <Dialog open={open} onClose={starting ? undefined : onClose} fullWidth maxWidth=\"sm\">\n <DialogTitle>{copy.title}</DialogTitle>\n <DialogContent>\n {asking ? (\n <Stack spacing={2} sx={{ pt: 1 }}>\n <TextField\n label={copy.briefLabel}\n placeholder={copy.placeholder}\n multiline\n minRows={4}\n value={brief}\n onChange={(event) => setBrief(event.target.value.slice(0, BRIEF_MAX_CHARS))}\n helperText={`${brief.length.toLocaleString('en-US')} / ${BRIEF_MAX_CHARS.toLocaleString('en-US')}`}\n disabled={starting}\n autoFocus\n />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {copy.next}{' '}\n <Link href={help.href} target=\"_blank\" rel=\"noopener\" title={help.excerpt}>\n {'How it works'}\n </Link>\n </Typography>\n {notice ? <Alert severity=\"warning\">{notice}</Alert> : null}\n </Stack>\n ) : (\n <Stack spacing={2} sx={{ pt: 1 }}>\n {running ? (\n <Alert severity=\"info\" icon={<CircularProgress size={18} aria-label=\"Drafting\" />}>\n {copy.started}\n </Alert>\n ) : null}\n {draft ? (\n <Alert severity=\"success\">\n <Typography variant=\"body2\" sx={{ fontWeight: 600 }}>\n {`${copy.drafted}: ${draft.label}`}\n </Typography>\n {draft.note ? <Typography variant=\"body2\">{draft.note}</Typography> : null}\n </Alert>\n ) : null}\n {problem ? <Alert severity={job?.status === 'failed' ? 'error' : 'warning'}>{problem}</Alert> : null}\n {notListed ? <Alert severity=\"info\">{copy.notListed}</Alert> : null}\n </Stack>\n )}\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={starting}>\n {asking ? 'Cancel' : 'Close'}\n </Button>\n {asking ? (\n <Button\n variant=\"contained\"\n onClick={() => void start()}\n disabled={starting || !orgId || !brief.trim()}\n >\n {copy.submit}\n </Button>\n ) : draft ? (\n <Button variant=\"contained\" onClick={review}>\n {copy.review}\n </Button>\n ) : null}\n </DialogActions>\n </Dialog>\n )\n}\n\n/**\n * Renders nothing until the jobs route has answered for this workspace, and\n * nothing when it says the feature is not this workspace's.\n */\nexport function AiDescribeAutomationButton({ hostId, orgId, openAction }: ConsoleHostAutomationsZoneProps) {\n const { data: user } = useUser()\n const verdict = useAiJobsVerdict(user, orgId)\n const [open, setOpen] = useState(false)\n\n if (verdict !== 'ready') return null\n return (\n <>\n <Button size=\"small\" variant=\"outlined\" onClick={() => setOpen(true)}>\n {'Describe it'}\n </Button>\n <AiAutomationBriefDialog\n open={open}\n onClose={() => setOpen(false)}\n orgId={orgId}\n hostId={hostId}\n user={user}\n openAction={openAction}\n />\n </>\n )\n}\n\nexport default AiDescribeAutomationButton\n"],"names":["pluginDocsHelp","useUser","Alert","Button","CircularProgress","Dialog","DialogActions","DialogContent","DialogTitle","Link","Stack","TextField","Typography","useEffect","useState","aiJobProblem","useAiJobRun","useAiJobsVerdict","BRIEF_MAX_CHARS","AI_AUTOMATION_BRIEF_COPY","title","briefLabel","placeholder","next","submit","started","drafted","review","notListed","failed","AiAutomationBriefDialog","props","open","onClose","orgId","hostId","user","openAction","copy","help","brief","setBrief","setNotListed","run","job","starting","running","notice","reset","draft","status","outputs","find","output","resource","undefined","problem","asking","Boolean","start","trimmed","trim","kind","inputs","mode","id","fullWidth","maxWidth","spacing","sx","pt","label","multiline","minRows","value","onChange","event","target","slice","helperText","length","toLocaleString","disabled","autoFocus","variant","color","href","rel","excerpt","severity","icon","size","aria-label","fontWeight","note","onClick","AiDescribeAutomationButton","data","verdict","setOpen"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-describe-automation.component.tsx"],"sourcesContent":["'use client'\n\n/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport type { ConsoleHostAutomationsZoneProps } from './ai-automation-zones'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n CircularProgress,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Link,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { openAiJobs } from './ai-jobs-store'\nimport { aiJobProblem, useAiJobRun, useAiJobsVerdict } from './use-ai-job-run'\n\n/**\n * \"Describe it\" on the Automation page's Actions (AGL-2919), beside Add action\n * and Recipes through the `hostAutomations` zone: a description becomes a\n * `workflow` job that drafts one automation, switched off.\n *\n * The dialog follows the shared brief dialog's shape — one text box, what\n * happens next, and the action that starts the job — and then follows the\n * job, because an automation's draft needs no plan to confirm: when it is\n * written, the dialog says what to fill in and opens it in the Actions editor.\n */\n\n/** The longest description a job admits. */\nconst BRIEF_MAX_CHARS = 4_000\n\nexport const AI_AUTOMATION_BRIEF_COPY = {\n title: 'Describe an automation',\n briefLabel: 'What should happen, and when?',\n placeholder:\n 'When someone submits the newsletter form, add them to the newsletter list, make them ' +\n 'a lead and send them a welcome email',\n next:\n 'The automation is drafted switched off, using only the triggers and steps your plan ' +\n 'includes. Anything it needs that your site does not have yet is left for you to fill in.',\n submit: 'Draft the automation',\n started:\n 'The automation is being drafted. This can take a few minutes. You can close this ' +\n 'window: the draft appears in the list, switched off, when it is ready. Open AI ' +\n 'jobs to follow it meanwhile.',\n drafted: 'Drafted',\n review: 'Review it',\n notListed: 'The draft is saved. It appears in the list in a moment.',\n failed: 'The automation could not be drafted. Try again.',\n} as const\n\nexport interface AiAutomationBriefDialogProps {\n open: boolean\n onClose: () => void\n orgId: string | undefined\n hostId: string\n /** Who is signed in, whose token the requests carry. */\n user: Parameters<typeof useAiJobRun>[0]\n /** Opens a listed action in the Actions editor; `false` when the list has not read it yet. */\n openAction: (actionId: string) => boolean\n}\n\nexport function AiAutomationBriefDialog(props: AiAutomationBriefDialogProps) {\n const { open, onClose, orgId, hostId, user, openAction } = props\n const copy = AI_AUTOMATION_BRIEF_COPY\n const help = pluginDocsHelp('aiAutomations')\n const [brief, setBrief] = useState('')\n const [notListed, setNotListed] = useState(false)\n const run = useAiJobRun(user, copy.failed)\n const { job, starting, running, notice, reset } = run\n\n // Opening or closing starts over and stops following: a job still drafting\n // carries on in AI jobs, and its draft appears in the list when it is written.\n useEffect(() => {\n reset()\n setNotListed(false)\n }, [open, reset])\n\n const draft = job?.status === 'done' ? job.outputs.find((output) => output.resource === 'workflow') : undefined\n const problem = aiJobProblem(job, copy.failed) ?? (job?.status === 'done' && !draft ? copy.failed : null)\n const asking = !job || Boolean(notice)\n\n const start = async () => {\n const trimmed = brief.trim()\n if (!orgId || !trimmed) return\n setNotListed(false)\n if (await run.start({ orgId, hostId, kind: 'workflow', brief: trimmed, inputs: { mode: 'draft' } })) {\n setBrief('')\n }\n }\n\n const review = () => {\n if (!draft) return\n if (openAction(draft.id)) onClose()\n else setNotListed(true)\n }\n\n return (\n <Dialog open={open} onClose={starting ? undefined : onClose} fullWidth maxWidth=\"sm\">\n <DialogTitle>{copy.title}</DialogTitle>\n <DialogContent>\n {asking ? (\n <Stack spacing={2} sx={{ pt: 1 }}>\n <TextField\n label={copy.briefLabel}\n placeholder={copy.placeholder}\n multiline\n minRows={4}\n value={brief}\n onChange={(event) => setBrief(event.target.value.slice(0, BRIEF_MAX_CHARS))}\n helperText={`${brief.length.toLocaleString('en-US')} / ${BRIEF_MAX_CHARS.toLocaleString('en-US')}`}\n disabled={starting}\n autoFocus\n />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {copy.next}{' '}\n <Link href={help.href} target=\"_blank\" rel=\"noopener\" title={help.excerpt}>\n {'How it works'}\n </Link>\n </Typography>\n {notice ? <Alert severity=\"warning\">{notice}</Alert> : null}\n </Stack>\n ) : (\n <Stack spacing={2} sx={{ pt: 1 }}>\n {running && job ? (\n <Alert\n severity=\"info\"\n icon={<CircularProgress size={18} aria-label=\"Drafting\" />}\n action={\n // Follows the job in the Assist panel (AGL-3593); the dialog\n // covers it, so it closes.\n <Button\n color=\"inherit\"\n size=\"small\"\n variant=\"outlined\"\n onClick={() => {\n openAiJobs({ jobId: job.id })\n onClose()\n }}\n >\n {'Open AI jobs'}\n </Button>\n }\n >\n {copy.started}\n </Alert>\n ) : null}\n {draft ? (\n <Alert severity=\"success\">\n <Typography variant=\"body2\" sx={{ fontWeight: 600 }}>\n {`${copy.drafted}: ${draft.label}`}\n </Typography>\n {draft.note ? <Typography variant=\"body2\">{draft.note}</Typography> : null}\n </Alert>\n ) : null}\n {problem ? <Alert severity={job?.status === 'failed' ? 'error' : 'warning'}>{problem}</Alert> : null}\n {notListed ? <Alert severity=\"info\">{copy.notListed}</Alert> : null}\n </Stack>\n )}\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={starting}>\n {asking ? 'Cancel' : 'Close'}\n </Button>\n {asking ? (\n <Button\n variant=\"contained\"\n onClick={() => void start()}\n disabled={starting || !orgId || !brief.trim()}\n >\n {copy.submit}\n </Button>\n ) : draft ? (\n <Button variant=\"contained\" onClick={review}>\n {copy.review}\n </Button>\n ) : null}\n </DialogActions>\n </Dialog>\n )\n}\n\n/**\n * Renders nothing until the jobs route has answered for this workspace, and\n * nothing when it says the feature is not this workspace's.\n */\nexport function AiDescribeAutomationButton({ hostId, orgId, openAction }: ConsoleHostAutomationsZoneProps) {\n const { data: user } = useUser()\n const verdict = useAiJobsVerdict(user, orgId)\n const [open, setOpen] = useState(false)\n\n if (verdict !== 'ready') return null\n return (\n <>\n <Button size=\"small\" variant=\"outlined\" onClick={() => setOpen(true)}>\n {'Describe it'}\n </Button>\n <AiAutomationBriefDialog\n open={open}\n onClose={() => setOpen(false)}\n orgId={orgId}\n hostId={hostId}\n user={user}\n openAction={openAction}\n />\n </>\n )\n}\n\nexport default AiDescribeAutomationButton\n"],"names":["pluginDocsHelp","useUser","Alert","Button","CircularProgress","Dialog","DialogActions","DialogContent","DialogTitle","Link","Stack","TextField","Typography","useEffect","useState","openAiJobs","aiJobProblem","useAiJobRun","useAiJobsVerdict","BRIEF_MAX_CHARS","AI_AUTOMATION_BRIEF_COPY","title","briefLabel","placeholder","next","submit","started","drafted","review","notListed","failed","AiAutomationBriefDialog","props","open","onClose","orgId","hostId","user","openAction","copy","help","brief","setBrief","setNotListed","run","job","starting","running","notice","reset","draft","status","outputs","find","output","resource","undefined","problem","asking","Boolean","start","trimmed","trim","kind","inputs","mode","id","fullWidth","maxWidth","spacing","sx","pt","label","multiline","minRows","value","onChange","event","target","slice","helperText","length","toLocaleString","disabled","autoFocus","variant","color","href","rel","excerpt","severity","icon","size","aria-label","action","onClick","jobId","fontWeight","note","AiDescribeAutomationButton","data","verdict","setOpen"],"mappings":"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"}
@@ -24,7 +24,8 @@ import { doc, getDoc, setDoc } from "firebase/firestore";
24
24
  import { useCallback, useEffect, useRef, useState } from "react";
25
25
  import { AI_INSIGHT_DEFAULT_DAYS, AI_INSIGHT_QUESTION_MAX_CHARS, AI_INSIGHT_WINDOWS, aiInsightRowText, aiInsightSourceHref } from "../model/ai-insight.js";
26
26
  import { AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
27
- import { readEventFrames } from "./assist-jobs-drawer.component.js";
27
+ import { readEventFrames } from "./ai-job-events.js";
28
+ import { openAiJobs, publishAiJob } from "./ai-jobs-store.js";
28
29
  /**
29
30
  * "Ask about your numbers" (AGL-2915): the Assist panel's question box on a
30
31
  * site's Analytics, Data and CRM Reports pages and the workspace's Data page,
@@ -56,6 +57,10 @@ export function AiInsightDialog(props) {
56
57
  const [answer, setAnswer] = useState(null);
57
58
  const [openCitations, setOpenCitations] = useState(null);
58
59
  const [subscribed, setSubscribed] = useState(null);
60
+ /**
61
+ * The job whose answer was still being written when the dialog stopped
62
+ * waiting (AGL-3593): its notice offers to open AI jobs on it.
63
+ */ const [stillWriting, setStillWriting] = useState(null);
59
64
  const watchRef = useRef(null);
60
65
  const readAnswer = useCallback(async (id)=>{
61
66
  const response = await authorizedFetch(userRef.current, `/api/ai/insights/${encodeURIComponent(id)}?orgId=${encodeURIComponent(orgId)}`);
@@ -99,6 +104,7 @@ export function AiInsightDialog(props) {
99
104
  if (event['type'] === 'reconnect') again = true;
100
105
  if (event['type'] !== 'state') return;
101
106
  const next = event['job'];
107
+ publishAiJob(next);
102
108
  if (next.status === 'needs_input') {
103
109
  var _next_error;
104
110
  setNotice((_next_error = next.error) != null ? _next_error : 'This workspace cannot run AI jobs right now.');
@@ -112,7 +118,8 @@ export function AiInsightDialog(props) {
112
118
  if (settled) return settle(settled);
113
119
  }
114
120
  if (!controller.signal.aborted) {
115
- setNotice('The answer is still being written. Find it under AI jobs in a minute.');
121
+ setNotice('The answer is still being written. Find it in AI jobs in a minute.');
122
+ setStillWriting(job.id);
116
123
  setPhase('ask');
117
124
  }
118
125
  }, [
@@ -126,6 +133,7 @@ export function AiInsightDialog(props) {
126
133
  return;
127
134
  }
128
135
  setNotice(null);
136
+ setStillWriting(null);
129
137
  setOpenCitations(null);
130
138
  if (jobId) {
131
139
  setPhase('working');
@@ -183,6 +191,7 @@ export function AiInsightDialog(props) {
183
191
  const text = question.trim();
184
192
  if (!text) return;
185
193
  setNotice(null);
194
+ setStillWriting(null);
186
195
  setPhase('working');
187
196
  try {
188
197
  const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {
@@ -209,6 +218,7 @@ export function AiInsightDialog(props) {
209
218
  setPhase('ask');
210
219
  return;
211
220
  }
221
+ publishAiJob(payload.job);
212
222
  await follow(payload.job);
213
223
  } catch (unused) {
214
224
  setNotice('The question could not be asked. Try again.');
@@ -399,6 +409,18 @@ export function AiInsightDialog(props) {
399
409
  }) : null,
400
410
  notice ? /*#__PURE__*/ _jsx(Alert, {
401
411
  severity: "warning",
412
+ action: stillWriting ? /*#__PURE__*/ _jsx(Button, {
413
+ color: "inherit",
414
+ size: "small",
415
+ variant: "outlined",
416
+ onClick: ()=>{
417
+ openAiJobs({
418
+ jobId: stillWriting
419
+ });
420
+ onClose();
421
+ },
422
+ children: 'Open AI jobs'
423
+ }) : undefined,
402
424
  children: notice
403
425
  }) : null,
404
426
  surface !== 'datasets' && subscribed !== null ? /*#__PURE__*/ _jsx(FormControlLabel, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-insight-dialog.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\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { INSIGHT_DIGESTS_FIELD, insightDigestSubscribed } from '@aglyn/aglyn/app-utils/notifications'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n CircularProgress,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControlLabel,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, getDoc, setDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n AI_INSIGHT_DEFAULT_DAYS,\n AI_INSIGHT_QUESTION_MAX_CHARS,\n AI_INSIGHT_WINDOWS,\n aiInsightRowText,\n aiInsightSourceHref,\n type AiInsightAnswerWire,\n type AiInsightSurface,\n} from '../model/ai-insight'\nimport { AI_JOB_TERMINAL_STATUSES, type AiJobSummary } from '../model/ai-jobs.types'\nimport { readEventFrames } from './assist-jobs-drawer.component'\n\n/**\n * \"Ask about your numbers\" (AGL-2915): the Assist panel's question box on a\n * site's Analytics, Data and CRM Reports pages and the workspace's Data page,\n * and the answer an `insight` job wrote.\n *\n * A question starts a job, which reads the figures and answers on the jobs\n * beat; the dialog watches the job and then reads the answer through\n * `/api/ai/insights/{jobId}`, the only door to it. Every insight shows the\n * rows it was traced to and links the console page they come from, so a\n * person can check a number before acting on it. Nothing here writes anything\n * but the person's own weekly-insights switch.\n */\n\n/** Placeholder questions by surface, in the words a person would use. */\nconst PLACEHOLDERS: Record<Exclude<AiInsightSurface, 'digest'>, string> = {\n analytics: 'Which pages brought in the most visitors, and did anything change?',\n datasets: 'What is the average order total by state?',\n 'crm-reports': 'Which campaign or form brought in the most people this month?',\n}\n\nexport interface AiInsightDialogProps {\n open: boolean\n onClose: () => void\n orgId: string\n /** The org's slug, for the links to the pages a table's figures come from. */\n orgSlug: string\n /** The site the question is about; `null` on the workspace's Data page. */\n hostId: string | null\n /** The site's subdomain, which a console link names a site by. */\n host: string | null\n surface: Exclude<AiInsightSurface, 'digest'>\n user: Parameters<typeof authorizedFetch>[0]\n /** The signed-in person, whose weekly-insights switch the dialog shows. */\n uid: string | null\n /** An answered job to show rather than a question to ask. */\n jobId?: string | null\n}\n\ntype Phase = 'ask' | 'working' | 'answer'\n\nexport function AiInsightDialog(props: AiInsightDialogProps) {\n const { open, onClose, orgId, orgSlug, hostId, host, surface, jobId } = props\n // Held in a ref: a request reads who is signed in, and nothing keys on the\n // identity of the object that says so.\n const userRef = useRef(props.user)\n userRef.current = props.user\n const uid = props.uid\n const firestore = useFirestore()\n const [phase, setPhase] = useState<Phase>('ask')\n const [question, setQuestion] = useState('')\n const [days, setDays] = useState(AI_INSIGHT_DEFAULT_DAYS)\n const [notice, setNotice] = useState<string | null>(null)\n const [answer, setAnswer] = useState<AiInsightAnswerWire | null>(null)\n const [openCitations, setOpenCitations] = useState<number | null>(null)\n const [subscribed, setSubscribed] = useState<boolean | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n const readAnswer = useCallback(\n async (id: string) => {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/insights/${encodeURIComponent(id)}?orgId=${encodeURIComponent(orgId)}`,\n )\n const payload = await response.json().catch(() => null)\n if (!response.ok || !payload?.answer) {\n setNotice(String(payload?.error ?? 'The answer could not be loaded. Try again.'))\n setPhase('ask')\n return\n }\n setAnswer(payload.answer as AiInsightAnswerWire)\n setPhase('answer')\n },\n [orgId],\n )\n\n /** Follows a job until it settles, then reads its answer or says why there is none. */\n const follow = useCallback(\n async (job: AiJobSummary) => {\n const settle = async (settled: AiJobSummary) => {\n if (settled.status === 'done') await readAnswer(settled.id)\n else {\n setNotice(settled.error ?? 'The question could not be answered. Try again.')\n setPhase('ask')\n }\n }\n if (AI_JOB_TERMINAL_STATUSES.includes(job.status)) return settle(job)\n setPhase('working')\n const controller = new AbortController()\n watchRef.current?.abort()\n watchRef.current = controller\n let again = true\n while (again && !controller.signal.aborted) {\n again = false\n let settled: AiJobSummary | null = null\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs/${encodeURIComponent(job.id)}/events?orgId=${encodeURIComponent(orgId)}`,\n { signal: controller.signal },\n )\n if (!response.ok || !response.body) break\n await readEventFrames(response.body, (event) => {\n if (event['type'] === 'reconnect') again = true\n if (event['type'] !== 'state') return\n const next = event['job'] as AiJobSummary\n if (next.status === 'needs_input') {\n setNotice(next.error ?? 'This workspace cannot run AI jobs right now.')\n }\n if (AI_JOB_TERMINAL_STATUSES.includes(next.status)) settled = next\n })\n } catch {\n if (controller.signal.aborted) return\n break\n }\n if (settled) return settle(settled)\n }\n if (!controller.signal.aborted) {\n setNotice('The answer is still being written. Find it under AI jobs in a minute.')\n setPhase('ask')\n }\n },\n [orgId, readAnswer],\n )\n\n useEffect(() => {\n if (!open) {\n watchRef.current?.abort()\n return\n }\n setNotice(null)\n setOpenCitations(null)\n if (jobId) {\n setPhase('working')\n void readAnswer(jobId)\n } else {\n setAnswer(null)\n setPhase('ask')\n }\n }, [open, jobId, readAnswer])\n\n // The person's own weekly-insights switch, read when the dialog opens.\n useEffect(() => {\n if (!open || !uid || !orgId) return\n let active = true\n void getDoc(doc(firestore, 'users', uid))\n .then((snapshot) => {\n if (active) {\n setSubscribed(\n insightDigestSubscribed(snapshot.get(INSIGHT_DIGESTS_FIELD) as Record<string, boolean>, orgId),\n )\n }\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [open, uid, orgId, firestore])\n\n const toggleDigest = useCallback(async () => {\n if (!uid || subscribed === null) return\n const next = !subscribed\n setSubscribed(next)\n try {\n await setDoc(doc(firestore, 'users', uid), { [INSIGHT_DIGESTS_FIELD]: { [orgId]: next } }, { merge: true })\n } catch {\n setSubscribed(!next)\n setNotice('Your weekly insights setting could not be saved. Try again.')\n }\n }, [uid, subscribed, firestore, orgId])\n\n const ask = useCallback(async () => {\n const text = question.trim()\n if (!text) return\n setNotice(null)\n setPhase('working')\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: 'insight',\n brief: text,\n inputs: { surface, days },\n }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok || !payload?.job) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(locked ? lockdownRefusalText(locked) : String(payload?.error ?? 'The question could not be asked. Try again.'))\n setPhase('ask')\n return\n }\n await follow(payload.job as AiJobSummary)\n } catch {\n setNotice('The question could not be asked. Try again.')\n setPhase('ask')\n }\n }, [question, orgId, hostId, surface, days, follow])\n\n const tableByRef = (ref: string) => answer?.tables.find((table) => table.ref === ref) ?? null\n\n return (\n <Dialog open={open} onClose={onClose} fullWidth maxWidth=\"sm\" aria-labelledby=\"ai-insight-title\">\n <DialogTitle id=\"ai-insight-title\">{'Ask about your numbers'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {phase === 'ask' ? (\n <>\n <TextField\n label=\"What do you want to know?\"\n placeholder={PLACEHOLDERS[surface]}\n multiline\n minRows={2}\n value={question}\n onChange={(event) => setQuestion(event.target.value.slice(0, AI_INSIGHT_QUESTION_MAX_CHARS))}\n autoFocus\n />\n {surface !== 'datasets' ? (\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\" id=\"ai-insight-window-label\">\n {'Over the last'}\n </Typography>\n <Stack direction=\"row\" useFlexGap role=\"group\" aria-labelledby=\"ai-insight-window-label\" sx={{ flexWrap: 'wrap', gap: 1 }}>\n {AI_INSIGHT_WINDOWS.map((window) => (\n <Chip\n key={window}\n label={`${window} days`}\n color={days === window ? 'primary' : 'default'}\n variant={days === window ? 'filled' : 'outlined'}\n onClick={() => setDays(window)}\n aria-pressed={days === window}\n />\n ))}\n </Stack>\n </Stack>\n ) : null}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The answer is read from your own figures, and every number in it links to where it comes from. '}\n {'Nothing is changed or sent.'}\n </Typography>\n </>\n ) : null}\n {phase === 'working' ? (\n <Stack direction=\"row\" spacing={1.5} sx={{ alignItems: 'center' }} role=\"status\">\n <CircularProgress size={18} />\n <Typography variant=\"body2\">{'Reading your figures. This usually takes a minute.'}</Typography>\n </Stack>\n ) : null}\n {phase === 'answer' && answer ? (\n <Stack spacing={1.5}>\n <Typography variant=\"subtitle2\">{answer.question}</Typography>\n {answer.insights.length ? (\n <Stack component=\"ol\" spacing={1.5} sx={{ pl: 2.5, m: 0 }}>\n {answer.insights.map((insight, index) => (\n <Box component=\"li\" key={index}>\n <Typography variant=\"body2\">{insight.text}</Typography>\n <Button\n size=\"small\"\n onClick={() => setOpenCitations((current) => (current === index ? null : index))}\n aria-expanded={openCitations === index}\n >\n {openCitations === index ? 'Hide the figures' : 'Show the figures'}\n </Button>\n {openCitations === index ? (\n <Stack spacing={1} sx={{ pl: 1, borderLeft: 2, borderColor: 'divider' }}>\n {insight.cites.map((cite) => {\n const table = tableByRef(cite.table)\n if (!table) return null\n const href = aiInsightSourceHref(table, { orgSlug, host })\n return (\n <Box key={cite.table}>\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\">\n {table.period\n ? `${table.title} · ${table.period.from} to ${table.period.to}`\n : table.title}\n {href ? (\n <>\n {' · '}\n <AppLink componentVariant=\"naked\" href={href}>\n {`Open ${table.source.label}`}\n </AppLink>\n </>\n ) : null}\n </Typography>\n {cite.rows.map((row) => (\n <Typography key={row} variant=\"caption\" component=\"div\">\n {aiInsightRowText(table, row)}\n </Typography>\n ))}\n </Box>\n )\n })}\n </Stack>\n ) : null}\n </Box>\n ))}\n </Stack>\n ) : (\n <Alert severity=\"info\">{'The figures read here did not answer this question.'}</Alert>\n )}\n {answer.gap ? <Alert severity=\"info\">{answer.gap}</Alert> : null}\n {answer.left > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${answer.left} ${answer.left === 1 ? 'insight was' : 'insights were'} left out because ${\n answer.left === 1 ? 'its numbers' : 'their numbers'\n } could not be traced to the figures.`}\n </Typography>\n ) : null}\n </Stack>\n ) : null}\n {notice ? <Alert severity=\"warning\">{notice}</Alert> : null}\n {surface !== 'datasets' && subscribed !== null ? (\n <FormControlLabel\n control={<Switch checked={subscribed} onChange={() => void toggleDigest()} />}\n label=\"Send me weekly insights for this workspace every Monday\"\n />\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n {phase === 'answer' && !jobId ? (\n <Button\n onClick={() => {\n setAnswer(null)\n setQuestion('')\n setPhase('ask')\n }}\n >\n {'Ask another'}\n </Button>\n ) : null}\n <Button onClick={onClose}>{'Close'}</Button>\n {phase === 'ask' ? (\n <Button variant=\"contained\" onClick={() => void ask()} disabled={!question.trim()}>\n {'Ask'}\n </Button>\n ) : null}\n </DialogActions>\n </Dialog>\n )\n}\n\nexport default AiInsightDialog\n"],"names":["lockdownRefusalText","parseLockdownRefusal","INSIGHT_DIGESTS_FIELD","insightDigestSubscribed","AppLink","authorizedFetch","useFirestore","Alert","Box","Button","Chip","CircularProgress","Dialog","DialogActions","DialogContent","DialogTitle","FormControlLabel","Stack","Switch","TextField","Typography","doc","getDoc","setDoc","useCallback","useEffect","useRef","useState","AI_INSIGHT_DEFAULT_DAYS","AI_INSIGHT_QUESTION_MAX_CHARS","AI_INSIGHT_WINDOWS","aiInsightRowText","aiInsightSourceHref","AI_JOB_TERMINAL_STATUSES","readEventFrames","PLACEHOLDERS","analytics","datasets","AiInsightDialog","props","open","onClose","orgId","orgSlug","hostId","host","surface","jobId","userRef","user","current","uid","firestore","phase","setPhase","question","setQuestion","days","setDays","notice","setNotice","answer","setAnswer","openCitations","setOpenCitations","subscribed","setSubscribed","watchRef","readAnswer","id","response","encodeURIComponent","payload","json","catch","ok","String","error","follow","job","settle","settled","status","includes","controller","AbortController","abort","again","signal","aborted","body","event","next","active","then","snapshot","get","undefined","toggleDigest","merge","ask","text","trim","method","headers","JSON","stringify","kind","brief","inputs","locked","tableByRef","ref","tables","find","table","fullWidth","maxWidth","aria-labelledby","spacing","sx","pt","label","placeholder","multiline","minRows","value","onChange","target","slice","autoFocus","variant","color","direction","useFlexGap","role","flexWrap","gap","map","window","onClick","aria-pressed","alignItems","size","insights","length","component","pl","m","insight","index","aria-expanded","borderLeft","borderColor","cites","cite","href","period","title","from","to","componentVariant","source","rows","row","severity","left","control","checked","disabled"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-insight-dialog.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\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { INSIGHT_DIGESTS_FIELD, insightDigestSubscribed } from '@aglyn/aglyn/app-utils/notifications'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n CircularProgress,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControlLabel,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, getDoc, setDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n AI_INSIGHT_DEFAULT_DAYS,\n AI_INSIGHT_QUESTION_MAX_CHARS,\n AI_INSIGHT_WINDOWS,\n aiInsightRowText,\n aiInsightSourceHref,\n type AiInsightAnswerWire,\n type AiInsightSurface,\n} from '../model/ai-insight'\nimport { AI_JOB_TERMINAL_STATUSES, type AiJobSummary } from '../model/ai-jobs.types'\nimport { readEventFrames } from './ai-job-events'\nimport { openAiJobs, publishAiJob } from './ai-jobs-store'\n\n/**\n * \"Ask about your numbers\" (AGL-2915): the Assist panel's question box on a\n * site's Analytics, Data and CRM Reports pages and the workspace's Data page,\n * and the answer an `insight` job wrote.\n *\n * A question starts a job, which reads the figures and answers on the jobs\n * beat; the dialog watches the job and then reads the answer through\n * `/api/ai/insights/{jobId}`, the only door to it. Every insight shows the\n * rows it was traced to and links the console page they come from, so a\n * person can check a number before acting on it. Nothing here writes anything\n * but the person's own weekly-insights switch.\n */\n\n/** Placeholder questions by surface, in the words a person would use. */\nconst PLACEHOLDERS: Record<Exclude<AiInsightSurface, 'digest'>, string> = {\n analytics: 'Which pages brought in the most visitors, and did anything change?',\n datasets: 'What is the average order total by state?',\n 'crm-reports': 'Which campaign or form brought in the most people this month?',\n}\n\nexport interface AiInsightDialogProps {\n open: boolean\n onClose: () => void\n orgId: string\n /** The org's slug, for the links to the pages a table's figures come from. */\n orgSlug: string\n /** The site the question is about; `null` on the workspace's Data page. */\n hostId: string | null\n /** The site's subdomain, which a console link names a site by. */\n host: string | null\n surface: Exclude<AiInsightSurface, 'digest'>\n user: Parameters<typeof authorizedFetch>[0]\n /** The signed-in person, whose weekly-insights switch the dialog shows. */\n uid: string | null\n /** An answered job to show rather than a question to ask. */\n jobId?: string | null\n}\n\ntype Phase = 'ask' | 'working' | 'answer'\n\nexport function AiInsightDialog(props: AiInsightDialogProps) {\n const { open, onClose, orgId, orgSlug, hostId, host, surface, jobId } = props\n // Held in a ref: a request reads who is signed in, and nothing keys on the\n // identity of the object that says so.\n const userRef = useRef(props.user)\n userRef.current = props.user\n const uid = props.uid\n const firestore = useFirestore()\n const [phase, setPhase] = useState<Phase>('ask')\n const [question, setQuestion] = useState('')\n const [days, setDays] = useState(AI_INSIGHT_DEFAULT_DAYS)\n const [notice, setNotice] = useState<string | null>(null)\n const [answer, setAnswer] = useState<AiInsightAnswerWire | null>(null)\n const [openCitations, setOpenCitations] = useState<number | null>(null)\n const [subscribed, setSubscribed] = useState<boolean | null>(null)\n /**\n * The job whose answer was still being written when the dialog stopped\n * waiting (AGL-3593): its notice offers to open AI jobs on it.\n */\n const [stillWriting, setStillWriting] = useState<string | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n const readAnswer = useCallback(\n async (id: string) => {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/insights/${encodeURIComponent(id)}?orgId=${encodeURIComponent(orgId)}`,\n )\n const payload = await response.json().catch(() => null)\n if (!response.ok || !payload?.answer) {\n setNotice(String(payload?.error ?? 'The answer could not be loaded. Try again.'))\n setPhase('ask')\n return\n }\n setAnswer(payload.answer as AiInsightAnswerWire)\n setPhase('answer')\n },\n [orgId],\n )\n\n /** Follows a job until it settles, then reads its answer or says why there is none. */\n const follow = useCallback(\n async (job: AiJobSummary) => {\n const settle = async (settled: AiJobSummary) => {\n if (settled.status === 'done') await readAnswer(settled.id)\n else {\n setNotice(settled.error ?? 'The question could not be answered. Try again.')\n setPhase('ask')\n }\n }\n if (AI_JOB_TERMINAL_STATUSES.includes(job.status)) return settle(job)\n setPhase('working')\n const controller = new AbortController()\n watchRef.current?.abort()\n watchRef.current = controller\n let again = true\n while (again && !controller.signal.aborted) {\n again = false\n let settled: AiJobSummary | null = null\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs/${encodeURIComponent(job.id)}/events?orgId=${encodeURIComponent(orgId)}`,\n { signal: controller.signal },\n )\n if (!response.ok || !response.body) break\n await readEventFrames(response.body, (event) => {\n if (event['type'] === 'reconnect') again = true\n if (event['type'] !== 'state') return\n const next = event['job'] as AiJobSummary\n publishAiJob(next)\n if (next.status === 'needs_input') {\n setNotice(next.error ?? 'This workspace cannot run AI jobs right now.')\n }\n if (AI_JOB_TERMINAL_STATUSES.includes(next.status)) settled = next\n })\n } catch {\n if (controller.signal.aborted) return\n break\n }\n if (settled) return settle(settled)\n }\n if (!controller.signal.aborted) {\n setNotice('The answer is still being written. Find it in AI jobs in a minute.')\n setStillWriting(job.id)\n setPhase('ask')\n }\n },\n [orgId, readAnswer],\n )\n\n useEffect(() => {\n if (!open) {\n watchRef.current?.abort()\n return\n }\n setNotice(null)\n setStillWriting(null)\n setOpenCitations(null)\n if (jobId) {\n setPhase('working')\n void readAnswer(jobId)\n } else {\n setAnswer(null)\n setPhase('ask')\n }\n }, [open, jobId, readAnswer])\n\n // The person's own weekly-insights switch, read when the dialog opens.\n useEffect(() => {\n if (!open || !uid || !orgId) return\n let active = true\n void getDoc(doc(firestore, 'users', uid))\n .then((snapshot) => {\n if (active) {\n setSubscribed(\n insightDigestSubscribed(snapshot.get(INSIGHT_DIGESTS_FIELD) as Record<string, boolean>, orgId),\n )\n }\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [open, uid, orgId, firestore])\n\n const toggleDigest = useCallback(async () => {\n if (!uid || subscribed === null) return\n const next = !subscribed\n setSubscribed(next)\n try {\n await setDoc(doc(firestore, 'users', uid), { [INSIGHT_DIGESTS_FIELD]: { [orgId]: next } }, { merge: true })\n } catch {\n setSubscribed(!next)\n setNotice('Your weekly insights setting could not be saved. Try again.')\n }\n }, [uid, subscribed, firestore, orgId])\n\n const ask = useCallback(async () => {\n const text = question.trim()\n if (!text) return\n setNotice(null)\n setStillWriting(null)\n setPhase('working')\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: 'insight',\n brief: text,\n inputs: { surface, days },\n }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok || !payload?.job) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(locked ? lockdownRefusalText(locked) : String(payload?.error ?? 'The question could not be asked. Try again.'))\n setPhase('ask')\n return\n }\n publishAiJob(payload.job as AiJobSummary)\n await follow(payload.job as AiJobSummary)\n } catch {\n setNotice('The question could not be asked. Try again.')\n setPhase('ask')\n }\n }, [question, orgId, hostId, surface, days, follow])\n\n const tableByRef = (ref: string) => answer?.tables.find((table) => table.ref === ref) ?? null\n\n return (\n <Dialog open={open} onClose={onClose} fullWidth maxWidth=\"sm\" aria-labelledby=\"ai-insight-title\">\n <DialogTitle id=\"ai-insight-title\">{'Ask about your numbers'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {phase === 'ask' ? (\n <>\n <TextField\n label=\"What do you want to know?\"\n placeholder={PLACEHOLDERS[surface]}\n multiline\n minRows={2}\n value={question}\n onChange={(event) => setQuestion(event.target.value.slice(0, AI_INSIGHT_QUESTION_MAX_CHARS))}\n autoFocus\n />\n {surface !== 'datasets' ? (\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\" id=\"ai-insight-window-label\">\n {'Over the last'}\n </Typography>\n <Stack direction=\"row\" useFlexGap role=\"group\" aria-labelledby=\"ai-insight-window-label\" sx={{ flexWrap: 'wrap', gap: 1 }}>\n {AI_INSIGHT_WINDOWS.map((window) => (\n <Chip\n key={window}\n label={`${window} days`}\n color={days === window ? 'primary' : 'default'}\n variant={days === window ? 'filled' : 'outlined'}\n onClick={() => setDays(window)}\n aria-pressed={days === window}\n />\n ))}\n </Stack>\n </Stack>\n ) : null}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The answer is read from your own figures, and every number in it links to where it comes from. '}\n {'Nothing is changed or sent.'}\n </Typography>\n </>\n ) : null}\n {phase === 'working' ? (\n <Stack direction=\"row\" spacing={1.5} sx={{ alignItems: 'center' }} role=\"status\">\n <CircularProgress size={18} />\n <Typography variant=\"body2\">{'Reading your figures. This usually takes a minute.'}</Typography>\n </Stack>\n ) : null}\n {phase === 'answer' && answer ? (\n <Stack spacing={1.5}>\n <Typography variant=\"subtitle2\">{answer.question}</Typography>\n {answer.insights.length ? (\n <Stack component=\"ol\" spacing={1.5} sx={{ pl: 2.5, m: 0 }}>\n {answer.insights.map((insight, index) => (\n <Box component=\"li\" key={index}>\n <Typography variant=\"body2\">{insight.text}</Typography>\n <Button\n size=\"small\"\n onClick={() => setOpenCitations((current) => (current === index ? null : index))}\n aria-expanded={openCitations === index}\n >\n {openCitations === index ? 'Hide the figures' : 'Show the figures'}\n </Button>\n {openCitations === index ? (\n <Stack spacing={1} sx={{ pl: 1, borderLeft: 2, borderColor: 'divider' }}>\n {insight.cites.map((cite) => {\n const table = tableByRef(cite.table)\n if (!table) return null\n const href = aiInsightSourceHref(table, { orgSlug, host })\n return (\n <Box key={cite.table}>\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\">\n {table.period\n ? `${table.title} · ${table.period.from} to ${table.period.to}`\n : table.title}\n {href ? (\n <>\n {' · '}\n <AppLink componentVariant=\"naked\" href={href}>\n {`Open ${table.source.label}`}\n </AppLink>\n </>\n ) : null}\n </Typography>\n {cite.rows.map((row) => (\n <Typography key={row} variant=\"caption\" component=\"div\">\n {aiInsightRowText(table, row)}\n </Typography>\n ))}\n </Box>\n )\n })}\n </Stack>\n ) : null}\n </Box>\n ))}\n </Stack>\n ) : (\n <Alert severity=\"info\">{'The figures read here did not answer this question.'}</Alert>\n )}\n {answer.gap ? <Alert severity=\"info\">{answer.gap}</Alert> : null}\n {answer.left > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${answer.left} ${answer.left === 1 ? 'insight was' : 'insights were'} left out because ${\n answer.left === 1 ? 'its numbers' : 'their numbers'\n } could not be traced to the figures.`}\n </Typography>\n ) : null}\n </Stack>\n ) : null}\n {notice ? (\n <Alert\n severity=\"warning\"\n action={\n stillWriting ? (\n <Button\n color=\"inherit\"\n size=\"small\"\n variant=\"outlined\"\n onClick={() => {\n openAiJobs({ jobId: stillWriting })\n onClose()\n }}\n >\n {'Open AI jobs'}\n </Button>\n ) : undefined\n }\n >\n {notice}\n </Alert>\n ) : null}\n {surface !== 'datasets' && subscribed !== null ? (\n <FormControlLabel\n control={<Switch checked={subscribed} onChange={() => void toggleDigest()} />}\n label=\"Send me weekly insights for this workspace every Monday\"\n />\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n {phase === 'answer' && !jobId ? (\n <Button\n onClick={() => {\n setAnswer(null)\n setQuestion('')\n setPhase('ask')\n }}\n >\n {'Ask another'}\n </Button>\n ) : null}\n <Button onClick={onClose}>{'Close'}</Button>\n {phase === 'ask' ? (\n <Button variant=\"contained\" onClick={() => void ask()} disabled={!question.trim()}>\n {'Ask'}\n </Button>\n ) : null}\n </DialogActions>\n </Dialog>\n )\n}\n\nexport default AiInsightDialog\n"],"names":["lockdownRefusalText","parseLockdownRefusal","INSIGHT_DIGESTS_FIELD","insightDigestSubscribed","AppLink","authorizedFetch","useFirestore","Alert","Box","Button","Chip","CircularProgress","Dialog","DialogActions","DialogContent","DialogTitle","FormControlLabel","Stack","Switch","TextField","Typography","doc","getDoc","setDoc","useCallback","useEffect","useRef","useState","AI_INSIGHT_DEFAULT_DAYS","AI_INSIGHT_QUESTION_MAX_CHARS","AI_INSIGHT_WINDOWS","aiInsightRowText","aiInsightSourceHref","AI_JOB_TERMINAL_STATUSES","readEventFrames","openAiJobs","publishAiJob","PLACEHOLDERS","analytics","datasets","AiInsightDialog","props","open","onClose","orgId","orgSlug","hostId","host","surface","jobId","userRef","user","current","uid","firestore","phase","setPhase","question","setQuestion","days","setDays","notice","setNotice","answer","setAnswer","openCitations","setOpenCitations","subscribed","setSubscribed","stillWriting","setStillWriting","watchRef","readAnswer","id","response","encodeURIComponent","payload","json","catch","ok","String","error","follow","job","settle","settled","status","includes","controller","AbortController","abort","again","signal","aborted","body","event","next","active","then","snapshot","get","undefined","toggleDigest","merge","ask","text","trim","method","headers","JSON","stringify","kind","brief","inputs","locked","tableByRef","ref","tables","find","table","fullWidth","maxWidth","aria-labelledby","spacing","sx","pt","label","placeholder","multiline","minRows","value","onChange","target","slice","autoFocus","variant","color","direction","useFlexGap","role","flexWrap","gap","map","window","onClick","aria-pressed","alignItems","size","insights","length","component","pl","m","insight","index","aria-expanded","borderLeft","borderColor","cites","cite","href","period","title","from","to","componentVariant","source","rows","row","severity","left","action","control","checked","disabled"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token';
2
+ import type { AiJobSummary } from '../model/ai-jobs.types';
3
+ /**
4
+ * `data:` frames out of an SSE body, one parsed event per frame. Shared with
5
+ * every panel that watches a job through the events route.
6
+ */
7
+ export declare function readEventFrames(body: ReadableStream<Uint8Array>, onEvent: (event: Record<string, unknown>) => void): Promise<void>;
8
+ /**
9
+ * Follows one job through the events route until it settles, the signal
10
+ * aborts or the route stops answering, handing each state to `onJob` and to
11
+ * every surface counting the workspace's jobs (AGL-3593). The server closes a
12
+ * stream at its deadline with `reconnect`; this re-opens it.
13
+ */
14
+ export declare function followAiJobEvents(user: () => MaybeTokenSource, orgId: string, jobId: string, signal: AbortSignal, onJob: (job: AiJobSummary) => void): Promise<void>;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
18
+ import { publishAiJob } from "./ai-jobs-store.js";
19
+ // Reading a job's events stream (AGL-2904), shared by the AI jobs drawer and
20
+ // every dialog that follows the job it started (AGL-3593).
21
+ /**
22
+ * `data:` frames out of an SSE body, one parsed event per frame. Shared with
23
+ * every panel that watches a job through the events route.
24
+ */ export async function readEventFrames(body, onEvent) {
25
+ const reader = body.getReader();
26
+ const decoder = new TextDecoder();
27
+ let buffer = '';
28
+ for(;;){
29
+ var _frames_pop;
30
+ const { done, value } = await reader.read();
31
+ if (done) break;
32
+ buffer += decoder.decode(value, {
33
+ stream: true
34
+ });
35
+ const frames = buffer.split('\n\n');
36
+ buffer = (_frames_pop = frames.pop()) != null ? _frames_pop : '';
37
+ for (const frame of frames){
38
+ const line = frame.split('\n').find((part)=>part.startsWith('data: '));
39
+ if (!line) continue;
40
+ try {
41
+ onEvent(JSON.parse(line.slice('data: '.length)));
42
+ } catch (unused) {
43
+ // A torn frame is dropped; the next state event carries the whole job.
44
+ }
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * Follows one job through the events route until it settles, the signal
50
+ * aborts or the route stops answering, handing each state to `onJob` and to
51
+ * every surface counting the workspace's jobs (AGL-3593). The server closes a
52
+ * stream at its deadline with `reconnect`; this re-opens it.
53
+ */ export async function followAiJobEvents(user, orgId, jobId, signal, onJob) {
54
+ let again = true;
55
+ while(again && !signal.aborted){
56
+ again = false;
57
+ try {
58
+ const response = await authorizedFetch(user(), `/api/ai/jobs/${encodeURIComponent(jobId)}/events?orgId=${encodeURIComponent(orgId)}`, {
59
+ signal
60
+ });
61
+ if (!response.ok || !response.body) return;
62
+ await readEventFrames(response.body, (event)=>{
63
+ if (signal.aborted) return;
64
+ if (event['type'] === 'state') {
65
+ const next = event['job'];
66
+ publishAiJob(next);
67
+ onJob(next);
68
+ } else if (event['type'] === 'reconnect') again = true;
69
+ });
70
+ } catch (unused) {
71
+ return;
72
+ }
73
+ }
74
+ }
75
+
76
+ //# sourceMappingURL=ai-job-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-job-events.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport type { AiJobSummary } from '../model/ai-jobs.types'\nimport { publishAiJob } from './ai-jobs-store'\n\n// Reading a job's events stream (AGL-2904), shared by the AI jobs drawer and\n// every dialog that follows the job it started (AGL-3593).\n\n/**\n * `data:` frames out of an SSE body, one parsed event per frame. Shared with\n * every panel that watches a job through the events route.\n */\nexport async function readEventFrames(\n body: ReadableStream<Uint8Array>,\n onEvent: (event: Record<string, unknown>) => void,\n): Promise<void> {\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n const frames = buffer.split('\\n\\n')\n buffer = frames.pop() ?? ''\n for (const frame of frames) {\n const line = frame.split('\\n').find((part) => part.startsWith('data: '))\n if (!line) continue\n try {\n onEvent(JSON.parse(line.slice('data: '.length)))\n } catch {\n // A torn frame is dropped; the next state event carries the whole job.\n }\n }\n }\n}\n\n/**\n * Follows one job through the events route until it settles, the signal\n * aborts or the route stops answering, handing each state to `onJob` and to\n * every surface counting the workspace's jobs (AGL-3593). The server closes a\n * stream at its deadline with `reconnect`; this re-opens it.\n */\nexport async function followAiJobEvents(\n user: () => MaybeTokenSource,\n orgId: string,\n jobId: string,\n signal: AbortSignal,\n onJob: (job: AiJobSummary) => void,\n): Promise<void> {\n let again = true\n while (again && !signal.aborted) {\n again = false\n try {\n const response = await authorizedFetch(\n user(),\n `/api/ai/jobs/${encodeURIComponent(jobId)}/events?orgId=${encodeURIComponent(orgId)}`,\n { signal },\n )\n if (!response.ok || !response.body) return\n await readEventFrames(response.body, (event) => {\n if (signal.aborted) return\n if (event['type'] === 'state') {\n const next = event['job'] as AiJobSummary\n publishAiJob(next)\n onJob(next)\n } else if (event['type'] === 'reconnect') again = true\n })\n } catch {\n return\n }\n }\n}\n"],"names":["authorizedFetch","publishAiJob","readEventFrames","body","onEvent","reader","getReader","decoder","TextDecoder","buffer","frames","done","value","read","decode","stream","split","pop","frame","line","find","part","startsWith","JSON","parse","slice","length","followAiJobEvents","user","orgId","jobId","signal","onJob","again","aborted","response","encodeURIComponent","ok","event","next"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAGA,SAASA,eAAe,QAAQ,2CAA0C;AAE1E,SAASC,YAAY,QAAQ,qBAAiB;AAE9C,6EAA6E;AAC7E,2DAA2D;AAE3D;;;CAGC,GACD,OAAO,eAAeC,gBACpBC,IAAgC,EAChCC,OAAiD;IAEjD,MAAMC,SAASF,KAAKG,SAAS;IAC7B,MAAMC,UAAU,IAAIC;IACpB,IAAIC,SAAS;IACb,OAAS;YAKEC;QAJT,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAE,GAAG,MAAMP,OAAOQ,IAAI;QACzC,IAAIF,MAAM;QACVF,UAAUF,QAAQO,MAAM,CAACF,OAAO;YAAEG,QAAQ;QAAK;QAC/C,MAAML,SAASD,OAAOO,KAAK,CAAC;QAC5BP,UAASC,cAAAA,OAAOO,GAAG,cAAVP,cAAgB;QACzB,KAAK,MAAMQ,SAASR,OAAQ;YAC1B,MAAMS,OAAOD,MAAMF,KAAK,CAAC,MAAMI,IAAI,CAAC,CAACC,OAASA,KAAKC,UAAU,CAAC;YAC9D,IAAI,CAACH,MAAM;YACX,IAAI;gBACFf,QAAQmB,KAAKC,KAAK,CAACL,KAAKM,KAAK,CAAC,SAASC,MAAM;YAC/C,EAAE,eAAM;YACN,uEAAuE;YACzE;QACF;IACF;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeC,kBACpBC,IAA4B,EAC5BC,KAAa,EACbC,KAAa,EACbC,MAAmB,EACnBC,KAAkC;IAElC,IAAIC,QAAQ;IACZ,MAAOA,SAAS,CAACF,OAAOG,OAAO,CAAE;QAC/BD,QAAQ;QACR,IAAI;YACF,MAAME,WAAW,MAAMnC,gBACrB4B,QACA,CAAC,aAAa,EAAEQ,mBAAmBN,OAAO,cAAc,EAAEM,mBAAmBP,QAAQ,EACrF;gBAAEE;YAAO;YAEX,IAAI,CAACI,SAASE,EAAE,IAAI,CAACF,SAAShC,IAAI,EAAE;YACpC,MAAMD,gBAAgBiC,SAAShC,IAAI,EAAE,CAACmC;gBACpC,IAAIP,OAAOG,OAAO,EAAE;gBACpB,IAAII,KAAK,CAAC,OAAO,KAAK,SAAS;oBAC7B,MAAMC,OAAOD,KAAK,CAAC,MAAM;oBACzBrC,aAAasC;oBACbP,MAAMO;gBACR,OAAO,IAAID,KAAK,CAAC,OAAO,KAAK,aAAaL,QAAQ;YACpD;QACF,EAAE,eAAM;YACN;QACF;IACF;AACF"}
@@ -0,0 +1,49 @@
1
+ import type { MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token';
2
+ import { type AiJobSummary } from '../model/ai-jobs.types';
3
+ /**
4
+ * A JOB, WATCHED AND DECIDED WHERE IT WAS STARTED (AGL-3593).
5
+ *
6
+ * The dialog that starts a planned job — "Plan my site", "Plan the page" —
7
+ * used to end on a sentence pointing at a drawer a person had to go and find,
8
+ * and a person who could not find it never got their site built. This is
9
+ * what such a dialog shows instead, once its job exists:
10
+ *
11
+ * - where the job stands, live — queued, planning, plan ready, building,
12
+ * then done or failed — read from the same events route the AI jobs
13
+ * drawer follows;
14
+ * - the plan, once it is ready, with the drawer's own plan component and its
15
+ * Confirm, through the same request the drawer sends
16
+ * ({@link resumeAiJobRequest}), and Cancel beside it;
17
+ * - once it is done, the one action that opens what it built;
18
+ * - and always "Open AI jobs", which follows the job anywhere else in the
19
+ * console.
20
+ *
21
+ * Closing the dialog stops following; the job carries on, and the top-bar
22
+ * indicator, the Assist launcher and the notification the machine raises say
23
+ * when it needs the person again.
24
+ */
25
+ export interface AiJobFollowProps {
26
+ /** The job as the create door answered with it. */
27
+ job: AiJobSummary;
28
+ orgId: string;
29
+ /**
30
+ * Path slug for the link to what the job built. A dialog mounted on a
31
+ * console page that names its org can leave it out: the slug is the first
32
+ * segment of every org-scoped console address.
33
+ */
34
+ orgSlug?: string | null;
35
+ user: MaybeTokenSource;
36
+ /** What the dialog says about the job it started, above its live status. */
37
+ intro: string;
38
+ /**
39
+ * Closes the dialog. Called beside opening AI jobs, because a dialog
40
+ * covers the panel AI jobs opens in.
41
+ */
42
+ onOpenJobs?: () => void;
43
+ /** The viewer is staff (AGL-3078): a refused answer's details are theirs to open. */
44
+ staff?: boolean;
45
+ }
46
+ /** Follows `initial` through the events route while mounted, and lets a decision replace it. */
47
+ export declare function useFollowedAiJob(user: MaybeTokenSource, orgId: string, initial: AiJobSummary): [AiJobSummary, (job: AiJobSummary) => void];
48
+ export declare function AiJobFollow({ job: initial, orgId, orgSlug, user, intro, onOpenJobs, staff, }: AiJobFollowProps): import("react").JSX.Element;
49
+ export default AiJobFollow;