@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.
- package/README.md +2 -1
- package/package.json +17 -17
- package/src/lib/billing/ai-month-meter.js +5 -3
- package/src/lib/billing/ai-month-meter.js.map +1 -1
- package/src/lib/billing/ai-overage-ledger.js +3 -1
- package/src/lib/billing/ai-overage-ledger.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
- package/src/lib/components/ai-brief-dialog.component.js +35 -17
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +17 -2
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-insight-dialog.component.js +24 -2
- package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-events.d.ts +14 -0
- package/src/lib/components/ai-job-events.js +76 -0
- package/src/lib/components/ai-job-events.js.map +1 -0
- package/src/lib/components/ai-job-follow.component.d.ts +49 -0
- package/src/lib/components/ai-job-follow.component.js +210 -0
- package/src/lib/components/ai-job-follow.component.js.map +1 -0
- package/src/lib/components/ai-job-links.d.ts +42 -0
- package/src/lib/components/ai-job-links.js +140 -0
- package/src/lib/components/ai-job-links.js.map +1 -0
- package/src/lib/components/ai-job-plan.component.d.ts +13 -6
- package/src/lib/components/ai-job-plan.component.js +29 -3
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +24 -0
- package/src/lib/components/ai-job-requests.js +76 -0
- package/src/lib/components/ai-job-requests.js.map +1 -0
- package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
- package/src/lib/components/ai-jobs-indicator.component.js +80 -0
- package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
- package/src/lib/components/ai-jobs-store.d.ts +81 -0
- package/src/lib/components/ai-jobs-store.js +218 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -0
- package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
- package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
- package/src/lib/components/ai-site-build-page.component.js +377 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +522 -130
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
- package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
- package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
- package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
- package/src/lib/components/assist-jobs-drawer.component.js +152 -172
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +62 -6
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
- package/src/lib/components/staff-ai-credit-return.component.js +346 -0
- package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
- package/src/lib/components/staff-org-ai-card.component.js +364 -279
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
- package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
- package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.js +5 -18
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
- package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
- package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
- package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
- package/src/lib/jobs/ai-job-plan-step.js +161 -17
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-site-step.js +34 -4
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
- package/src/lib/jobs/ai-jobs-notify.js +51 -0
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
- package/src/lib/jobs/ai-jobs.d.ts +31 -4
- package/src/lib/jobs/ai-jobs.js +196 -32
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +112 -0
- package/src/lib/model/ai-job-activity.js +144 -0
- package/src/lib/model/ai-job-activity.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
- package/src/lib/model/ai-job-failure-copy.js +78 -0
- package/src/lib/model/ai-job-failure-copy.js.map +1 -0
- package/src/lib/model/ai-job-notice.d.ts +61 -0
- package/src/lib/model/ai-job-notice.js +112 -0
- package/src/lib/model/ai-job-notice.js.map +1 -0
- package/src/lib/model/ai-jobs.types.d.ts +26 -0
- package/src/lib/model/ai-jobs.types.js +1 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +45 -0
- package/src/lib/model/ai-site-build-progress.js +76 -0
- package/src/lib/model/ai-site-build-progress.js.map +1 -0
- package/src/lib/model/ai-site-inventory.d.ts +9 -0
- package/src/lib/model/ai-site-inventory.js +7 -2
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +93 -2
- package/src/lib/model/ai-site-job.js +88 -7
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +3 -1
- package/src/lib/model/ai-site-start.js +6 -4
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/ai-template-subjects.d.ts +4 -3
- package/src/lib/model/ai-template-subjects.js +5 -4
- package/src/lib/model/ai-template-subjects.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +3 -3
- package/src/lib/plan-entitlements.js +3 -3
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +28 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
- package/src/lib/runtime/ai-abuse-guards.js +10 -7
- package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.js +31 -4
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-gate.d.ts +3 -2
- package/src/lib/runtime/ai-gate.js +3 -2
- package/src/lib/runtime/ai-gate.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +12 -4
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +16 -1
- package/src/lib/runtime/site-inventory.js +43 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-admin-credits.d.ts +21 -0
- package/src/lib/server/ai-admin-credits.js +213 -0
- package/src/lib/server/ai-admin-credits.js.map +1 -0
- package/src/lib/server/ai-admin-org.js +12 -5
- package/src/lib/server/ai-admin-org.js.map +1 -1
- package/src/lib/server/ai-admin-signals.js +3 -1
- package/src/lib/server/ai-admin-signals.js.map +1 -1
- package/src/lib/server/ai-admin-user.js +12 -1
- package/src/lib/server/ai-admin-user.js.map +1 -1
- package/src/lib/server/ai-allotments.js +3 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +20 -2
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-chat.js +3 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/billing-credits.js +4 -3
- package/src/lib/server/billing-credits.js.map +1 -1
- package/src/lib/server/billing-overage.js +3 -2
- package/src/lib/server/billing-overage.js.map +1 -1
- package/src/lib/server.js +11 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
- package/src/lib/usage/assist-credit-returns-write.js +150 -0
- package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
- package/src/lib/usage/assist-credit-returns.d.ts +68 -0
- package/src/lib/usage/assist-credit-returns.js +79 -0
- package/src/lib/usage/assist-credit-returns.js.map +1 -0
- package/src/lib/usage/assist-free-taste.js +6 -3
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage/assist-job-refund.d.ts +100 -0
- package/src/lib/usage/assist-job-refund.js +109 -0
- package/src/lib/usage/assist-job-refund.js.map +1 -0
- package/src/lib/usage/assist-usage.js +5 -2
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/staff-org-ai.d.ts +31 -1
- package/src/lib/usage/staff-org-ai.js +19 -2
- 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 "./
|
|
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
|
|
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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,EAAEC,oBAAoB,QAAQ,eAAc;AACxE,SAASC,qBAAqB,EAAEC,uBAAuB,QAAQ,uCAAsC;AACrG,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SACEC,KAAK,EACLC,GAAG,EACHC,MAAM,EACNC,IAAI,EACJC,gBAAgB,EAChBC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,WAAW,EACXC,gBAAgB,EAChBC,KAAK,EACLC,MAAM,EACNC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,GAAG,EAAEC,MAAM,EAAEC,MAAM,QAAQ,qBAAoB;AACxD,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAChE,SACEC,uBAAuB,EACvBC,6BAA6B,EAC7BC,kBAAkB,EAClBC,gBAAgB,EAChBC,mBAAmB,QAGd,yBAAqB;AAC5B,SAASC,wBAAwB,QAA2B,4BAAwB;AACpF,SAASC,eAAe,QAAQ,oCAAgC;AAEhE;;;;;;;;;;;CAWC,GAED,uEAAuE,GACvE,MAAMC,eAAoE;IACxEC,WAAW;IACXC,UAAU;IACV,eAAe;AACjB;AAsBA,OAAO,SAASC,gBAAgBC,KAA2B;IACzD,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAEC,KAAK,EAAEC,OAAO,EAAEC,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAEC,KAAK,EAAE,GAAGR;IACxE,2EAA2E;IAC3E,uCAAuC;IACvC,MAAMS,UAAUtB,OAAOa,MAAMU,IAAI;IACjCD,QAAQE,OAAO,GAAGX,MAAMU,IAAI;IAC5B,MAAME,MAAMZ,MAAMY,GAAG;IACrB,MAAMC,YAAY9C;IAClB,MAAM,CAAC+C,OAAOC,SAAS,GAAG3B,SAAgB;IAC1C,MAAM,CAAC4B,UAAUC,YAAY,GAAG7B,SAAS;IACzC,MAAM,CAAC8B,MAAMC,QAAQ,GAAG/B,SAASC;IACjC,MAAM,CAAC+B,QAAQC,UAAU,GAAGjC,SAAwB;IACpD,MAAM,CAACkC,QAAQC,UAAU,GAAGnC,SAAqC;IACjE,MAAM,CAACoC,eAAeC,iBAAiB,GAAGrC,SAAwB;IAClE,MAAM,CAACsC,YAAYC,cAAc,GAAGvC,SAAyB;IAC7D,MAAMwC,WAAWzC,OAA+B;IAEhD,MAAM0C,aAAa5C,YACjB,OAAO6C;QACL,MAAMC,WAAW,MAAMjE,gBACrB2C,QAAQE,OAAO,EACf,CAAC,iBAAiB,EAAEqB,mBAAmBF,IAAI,OAAO,EAAEE,mBAAmB7B,QAAQ;QAEjF,MAAM8B,UAAU,MAAMF,SAASG,IAAI,GAAGC,KAAK,CAAC,IAAM;QAClD,IAAI,CAACJ,SAASK,EAAE,IAAI,EAACH,2BAAAA,QAASX,MAAM,GAAE;;YACpCD,UAAUgB,eAAOJ,2BAAAA,QAASK,KAAK,mBAAI;YACnCvB,SAAS;YACT;QACF;QACAQ,UAAUU,QAAQX,MAAM;QACxBP,SAAS;IACX,GACA;QAACZ;KAAM;IAGT,qFAAqF,GACrF,MAAMoC,SAAStD,YACb,OAAOuD;YAWLZ;QAVA,MAAMa,SAAS,OAAOC;YACpB,IAAIA,QAAQC,MAAM,KAAK,QAAQ,MAAMd,WAAWa,QAAQZ,EAAE;iBACrD;oBACOY;gBAAVrB,WAAUqB,iBAAAA,QAAQJ,KAAK,YAAbI,iBAAiB;gBAC3B3B,SAAS;YACX;QACF;QACA,IAAIrB,yBAAyBkD,QAAQ,CAACJ,IAAIG,MAAM,GAAG,OAAOF,OAAOD;QACjEzB,SAAS;QACT,MAAM8B,aAAa,IAAIC;SACvBlB,oBAAAA,SAASjB,OAAO,qBAAhBiB,kBAAkBmB,KAAK;QACvBnB,SAASjB,OAAO,GAAGkC;QACnB,IAAIG,QAAQ;QACZ,MAAOA,SAAS,CAACH,WAAWI,MAAM,CAACC,OAAO,CAAE;YAC1CF,QAAQ;YACR,IAAIN,UAA+B;YACnC,IAAI;gBACF,MAAMX,WAAW,MAAMjE,gBACrB2C,QAAQE,OAAO,EACf,CAAC,aAAa,EAAEqB,mBAAmBQ,IAAIV,EAAE,EAAE,cAAc,EAAEE,mBAAmB7B,QAAQ,EACtF;oBAAE8C,QAAQJ,WAAWI,MAAM;gBAAC;gBAE9B,IAAI,CAAClB,SAASK,EAAE,IAAI,CAACL,SAASoB,IAAI,EAAE;gBACpC,MAAMxD,gBAAgBoC,SAASoB,IAAI,EAAE,CAACC;oBACpC,IAAIA,KAAK,CAAC,OAAO,KAAK,aAAaJ,QAAQ;oBAC3C,IAAII,KAAK,CAAC,OAAO,KAAK,SAAS;oBAC/B,MAAMC,OAAOD,KAAK,CAAC,MAAM;oBACzB,IAAIC,KAAKV,MAAM,KAAK,eAAe;4BACvBU;wBAAVhC,WAAUgC,cAAAA,KAAKf,KAAK,YAAVe,cAAc;oBAC1B;oBACA,IAAI3D,yBAAyBkD,QAAQ,CAACS,KAAKV,MAAM,GAAGD,UAAUW;gBAChE;YACF,EAAE,eAAM;gBACN,IAAIR,WAAWI,MAAM,CAACC,OAAO,EAAE;gBAC/B;YACF;YACA,IAAIR,SAAS,OAAOD,OAAOC;QAC7B;QACA,IAAI,CAACG,WAAWI,MAAM,CAACC,OAAO,EAAE;YAC9B7B,UAAU;YACVN,SAAS;QACX;IACF,GACA;QAACZ;QAAO0B;KAAW;IAGrB3C,UAAU;QACR,IAAI,CAACe,MAAM;gBACT2B;aAAAA,oBAAAA,SAASjB,OAAO,qBAAhBiB,kBAAkBmB,KAAK;YACvB;QACF;QACA1B,UAAU;QACVI,iBAAiB;QACjB,IAAIjB,OAAO;YACTO,SAAS;YACT,KAAKc,WAAWrB;QAClB,OAAO;YACLe,UAAU;YACVR,SAAS;QACX;IACF,GAAG;QAACd;QAAMO;QAAOqB;KAAW;IAE5B,uEAAuE;IACvE3C,UAAU;QACR,IAAI,CAACe,QAAQ,CAACW,OAAO,CAACT,OAAO;QAC7B,IAAImD,SAAS;QACb,KAAKvE,OAAOD,IAAI+B,WAAW,SAASD,MACjC2C,IAAI,CAAC,CAACC;YACL,IAAIF,QAAQ;gBACV3B,cACE/D,wBAAwB4F,SAASC,GAAG,CAAC9F,wBAAmDwC;YAE5F;QACF,GACCgC,KAAK,CAAC,IAAMuB;QACf,OAAO;YACLJ,SAAS;QACX;IACF,GAAG;QAACrD;QAAMW;QAAKT;QAAOU;KAAU;IAEhC,MAAM8C,eAAe1E,YAAY;QAC/B,IAAI,CAAC2B,OAAOc,eAAe,MAAM;QACjC,MAAM2B,OAAO,CAAC3B;QACdC,cAAc0B;QACd,IAAI;YACF,MAAMrE,OAAOF,IAAI+B,WAAW,SAASD,MAAM;gBAAE,CAACjD,sBAAsB,EAAE;oBAAE,CAACwC,MAAM,EAAEkD;gBAAK;YAAE,GAAG;gBAAEO,OAAO;YAAK;QAC3G,EAAE,eAAM;YACNjC,cAAc,CAAC0B;YACfhC,UAAU;QACZ;IACF,GAAG;QAACT;QAAKc;QAAYb;QAAWV;KAAM;IAEtC,MAAM0D,MAAM5E,YAAY;QACtB,MAAM6E,OAAO9C,SAAS+C,IAAI;QAC1B,IAAI,CAACD,MAAM;QACXzC,UAAU;QACVN,SAAS;QACT,IAAI;YACF,MAAMgB,WAAW,MAAMjE,gBAAgB2C,QAAQE,OAAO,EAAE,gBAAgB;gBACtEqD,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9Cd,MAAMe,KAAKC,SAAS,CAAC;oBACnBhE;oBACAE;oBACA+D,MAAM;oBACNC,OAAOP;oBACPQ,QAAQ;wBAAE/D;wBAASW;oBAAK;gBAC1B;YACF;YACA,MAAMe,UAAU,MAAMF,SAASG,IAAI,GAAGC,KAAK,CAAC,IAAM;YAClD,IAAI,CAACJ,SAASK,EAAE,IAAI,EAACH,2BAAAA,QAASO,GAAG,GAAE;;gBACjC,MAAM+B,SAAS7G,qBAAqBqE,SAASY,MAAM,EAAEV;gBACrDZ,UAAUkD,SAAS9G,oBAAoB8G,UAAUlC,eAAOJ,2BAAAA,QAASK,KAAK,mBAAI;gBAC1EvB,SAAS;gBACT;YACF;YACA,MAAMwB,OAAON,QAAQO,GAAG;QAC1B,EAAE,eAAM;YACNnB,UAAU;YACVN,SAAS;QACX;IACF,GAAG;QAACC;QAAUb;QAAOE;QAAQE;QAASW;QAAMqB;KAAO;IAEnD,MAAMiC,aAAa,CAACC;;uBAAgBnD,0BAAAA,OAAQoD,MAAM,CAACC,IAAI,CAAC,CAACC,QAAUA,MAAMH,GAAG,KAAKA,uBAAQ;;IAEzF,qBACE,MAACpG;QAAO4B,MAAMA;QAAMC,SAASA;QAAS2E,SAAS;QAACC,UAAS;QAAKC,mBAAgB;;0BAC5E,KAACvG;gBAAYsD,IAAG;0BAAoB;;0BACpC,KAACvD;0BACC,cAAA,MAACG;oBAAMsG,SAAS;oBAAGC,IAAI;wBAAEC,IAAI;oBAAE;;wBAC5BpE,UAAU,sBACT;;8CACE,KAAClC;oCACCuG,OAAM;oCACNC,aAAaxF,YAAY,CAACW,QAAQ;oCAClC8E,SAAS;oCACTC,SAAS;oCACTC,OAAOvE;oCACPwE,UAAU,CAACpC,QAAUnC,YAAYmC,MAAMqC,MAAM,CAACF,KAAK,CAACG,KAAK,CAAC,GAAGpG;oCAC7DqG,SAAS;;gCAEVpF,YAAY,2BACX,MAAC7B;oCAAMsG,SAAS;;sDACd,KAACnG;4CAAW+G,SAAQ;4CAAQC,OAAM;4CAAiB/D,IAAG;sDACnD;;sDAEH,KAACpD;4CAAMoH,WAAU;4CAAMC,UAAU;4CAACC,MAAK;4CAAQjB,mBAAgB;4CAA0BE,IAAI;gDAAEgB,UAAU;gDAAQC,KAAK;4CAAE;sDACrH3G,mBAAmB4G,GAAG,CAAC,CAACC,uBACvB,KAACjI;oDAECgH,OAAO,GAAGiB,OAAO,KAAK,CAAC;oDACvBP,OAAO3E,SAASkF,SAAS,YAAY;oDACrCR,SAAS1E,SAASkF,SAAS,WAAW;oDACtCC,SAAS,IAAMlF,QAAQiF;oDACvBE,gBAAcpF,SAASkF;mDALlBA;;;qCAUX;8CACJ,MAACvH;oCAAW+G,SAAQ;oCAAQC,OAAM;;wCAC/B;wCACA;;;;6BAGH;wBACH/E,UAAU,0BACT,MAACpC;4BAAMoH,WAAU;4BAAMd,SAAS;4BAAKC,IAAI;gCAAEsB,YAAY;4BAAS;4BAAGP,MAAK;;8CACtE,KAAC5H;oCAAiBoI,MAAM;;8CACxB,KAAC3H;oCAAW+G,SAAQ;8CAAS;;;6BAE7B;wBACH9E,UAAU,YAAYQ,uBACrB,MAAC5C;4BAAMsG,SAAS;;8CACd,KAACnG;oCAAW+G,SAAQ;8CAAatE,OAAON,QAAQ;;gCAC/CM,OAAOmF,QAAQ,CAACC,MAAM,iBACrB,KAAChI;oCAAMiI,WAAU;oCAAK3B,SAAS;oCAAKC,IAAI;wCAAE2B,IAAI;wCAAKC,GAAG;oCAAE;8CACrDvF,OAAOmF,QAAQ,CAACN,GAAG,CAAC,CAACW,SAASC,sBAC7B,MAAC9I;4CAAI0I,WAAU;;8DACb,KAAC9H;oDAAW+G,SAAQ;8DAASkB,QAAQhD,IAAI;;8DACzC,KAAC5F;oDACCsI,MAAK;oDACLH,SAAS,IAAM5E,iBAAiB,CAACd,UAAaA,YAAYoG,QAAQ,OAAOA;oDACzEC,iBAAexF,kBAAkBuF;8DAEhCvF,kBAAkBuF,QAAQ,qBAAqB;;gDAEjDvF,kBAAkBuF,sBACjB,KAACrI;oDAAMsG,SAAS;oDAAGC,IAAI;wDAAE2B,IAAI;wDAAGK,YAAY;wDAAGC,aAAa;oDAAU;8DACnEJ,QAAQK,KAAK,CAAChB,GAAG,CAAC,CAACiB;wDAClB,MAAMxC,QAAQJ,WAAW4C,KAAKxC,KAAK;wDACnC,IAAI,CAACA,OAAO,OAAO;wDACnB,MAAMyC,OAAO5H,oBAAoBmF,OAAO;4DAAExE;4DAASE;wDAAK;wDACxD,qBACE,MAACrC;;8EACC,MAACY;oEAAW+G,SAAQ;oEAAUC,OAAM;oEAAiBc,WAAU;;wEAC5D/B,MAAM0C,MAAM,GACT,GAAG1C,MAAM2C,KAAK,CAAC,GAAG,EAAE3C,MAAM0C,MAAM,CAACE,IAAI,CAAC,IAAI,EAAE5C,MAAM0C,MAAM,CAACG,EAAE,EAAE,GAC7D7C,MAAM2C,KAAK;wEACdF,qBACC;;gFACG;8FACD,KAACxJ;oFAAQ6J,kBAAiB;oFAAQL,MAAMA;8FACrC,CAAC,KAAK,EAAEzC,MAAM+C,MAAM,CAACxC,KAAK,EAAE;;;6EAG/B;;;gEAELiC,KAAKQ,IAAI,CAACzB,GAAG,CAAC,CAAC0B,oBACd,KAAChJ;wEAAqB+G,SAAQ;wEAAUe,WAAU;kFAC/CnH,iBAAiBoF,OAAOiD;uEADVA;;2DAfXT,KAAKxC,KAAK;oDAqBxB;qDAEA;;2CAvCmBmC;mDA4C7B,KAAC/I;oCAAM8J,UAAS;8CAAQ;;gCAEzBxG,OAAO4E,GAAG,iBAAG,KAAClI;oCAAM8J,UAAS;8CAAQxG,OAAO4E,GAAG;qCAAY;gCAC3D5E,OAAOyG,IAAI,GAAG,kBACb,KAAClJ;oCAAW+G,SAAQ;oCAAUC,OAAM;8CACjC,GAAGvE,OAAOyG,IAAI,CAAC,CAAC,EAAEzG,OAAOyG,IAAI,KAAK,IAAI,gBAAgB,gBAAgB,kBAAkB,EACvFzG,OAAOyG,IAAI,KAAK,IAAI,gBAAgB,gBACrC,oCAAoC,CAAC;qCAEtC;;6BAEJ;wBACH3G,uBAAS,KAACpD;4BAAM8J,UAAS;sCAAW1G;6BAAkB;wBACtDb,YAAY,cAAcmB,eAAe,qBACxC,KAACjD;4BACCuJ,uBAAS,KAACrJ;gCAAOsJ,SAASvG;gCAAY8D,UAAU,IAAM,KAAK7B;;4BAC3DwB,OAAM;6BAEN;;;;0BAGR,MAAC7G;;oBACEwC,UAAU,YAAY,CAACN,sBACtB,KAACtC;wBACCmI,SAAS;4BACP9E,UAAU;4BACVN,YAAY;4BACZF,SAAS;wBACX;kCAEC;yBAED;kCACJ,KAAC7C;wBAAOmI,SAASnG;kCAAU;;oBAC1BY,UAAU,sBACT,KAAC5C;wBAAO0H,SAAQ;wBAAYS,SAAS,IAAM,KAAKxC;wBAAOqE,UAAU,CAAClH,SAAS+C,IAAI;kCAC5E;yBAED;;;;;AAIZ;AAEA,eAAehE,gBAAe"}
|
|
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;
|