@aglyn/plugins-ai 1.0.0-beta.226 → 1.0.0-beta.228
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 +3 -2
- 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 +36 -18
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +24 -4
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-button.component.js +6 -1
- package/src/lib/components/ai-describe-button.component.js.map +1 -1
- package/src/lib/components/ai-describe-page.component.d.ts +1 -1
- package/src/lib/components/ai-describe-page.component.js +1 -1
- package/src/lib/components/ai-describe-page.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 +20 -0
- package/src/lib/components/ai-job-events.js +78 -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-job-site.d.ts +22 -0
- package/src/lib/components/ai-job-site.js +73 -0
- package/src/lib/components/ai-job-site.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-list-page.component.d.ts +23 -0
- package/src/lib/components/ai-jobs-list-page.component.js +303 -0
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -0
- package/src/lib/components/ai-jobs-page.component.d.ts +7 -0
- package/src/lib/components/ai-jobs-page.component.js +30 -0
- package/src/lib/components/ai-jobs-page.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 +4 -4
- package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.d.ts +51 -0
- package/src/lib/components/ai-site-build-page.component.js +495 -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 +9 -1
- package/src/lib/components/ai-site-start-card.component.js +533 -132
- 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 +69 -8
- 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-draft-ids.d.ts +6 -0
- package/src/lib/jobs/ai-job-draft-ids.js +9 -0
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +7 -0
- package/src/lib/jobs/ai-job-drafts.js +16 -7
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +2 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +6 -5
- package/src/lib/jobs/ai-job-page-sections.js +3 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +19 -1
- package/src/lib/jobs/ai-job-page-step.js +50 -8
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
- package/src/lib/jobs/ai-job-plan-step.js +167 -18
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +28 -0
- package/src/lib/jobs/ai-job-site-step.js +130 -21
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +14 -1
- 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 +39 -4
- package/src/lib/jobs/ai-jobs.js +205 -34
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +98 -0
- package/src/lib/jobs/ai-site-publish.js +372 -0
- package/src/lib/jobs/ai-site-publish.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-page-briefs.d.ts +7 -6
- package/src/lib/jobs/fixtures/ai-page-briefs.js +23 -56
- package/src/lib/jobs/fixtures/ai-page-briefs.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 +63 -0
- package/src/lib/model/ai-job-notice.js +129 -0
- package/src/lib/model/ai-job-notice.js.map +1 -0
- package/src/lib/model/ai-jobs.types.d.ts +53 -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 +32 -10
- package/src/lib/model/ai-plan-capabilities.js +15 -2
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +61 -0
- package/src/lib/model/ai-site-build-progress.js +136 -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-seo.d.ts +19 -0
- package/src/lib/model/ai-site-start-seo.js +91 -3
- package/src/lib/model/ai-site-start-seo.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/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 +38 -2
- 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.d.ts +16 -12
- package/src/lib/runtime/ai-doctrine-validators.js +50 -40
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +1 -1
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.d.ts +1 -1
- package/src/lib/runtime/ai-eval-live.js +1 -1
- package/src/lib/runtime/ai-eval-live.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/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-live-sites.d.ts +26 -0
- package/src/lib/server/ai-jobs-live-sites.js +58 -0
- package/src/lib/server/ai-jobs-live-sites.js.map +1 -0
- package/src/lib/server/ai-jobs-route.js +42 -5
- 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
|
@@ -16,12 +16,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
16
16
|
* See the License for the specific language governing permissions and
|
|
17
17
|
* limitations under the License.
|
|
18
18
|
*/ import { pluginDocsHelp } from "@aglyn/aglyn/app-utils/docs-help";
|
|
19
|
+
import { mdiCreation } from "@aglyn/shared-data-mdi";
|
|
20
|
+
import { MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
19
21
|
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
20
22
|
import { Alert, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, Link, Stack, TextField, Typography } from "@mui/material";
|
|
21
23
|
import { useEffect, useState } from "react";
|
|
24
|
+
import { openAiJobs } from "./ai-jobs-store.js";
|
|
22
25
|
import { aiJobProblem, useAiJobRun, useAiJobsVerdict } from "./use-ai-job-run.js";
|
|
23
26
|
/**
|
|
24
|
-
* "
|
|
27
|
+
* "Create with AI" on the Automation page's Actions (AGL-2919), beside Add action
|
|
25
28
|
* and Recipes through the `hostAutomations` zone: a description becomes a
|
|
26
29
|
* `workflow` job that drafts one automation, switched off.
|
|
27
30
|
*
|
|
@@ -36,7 +39,7 @@ export const AI_AUTOMATION_BRIEF_COPY = {
|
|
|
36
39
|
placeholder: 'When someone submits the newsletter form, add them to the newsletter list, make them ' + 'a lead and send them a welcome email',
|
|
37
40
|
next: 'The automation is drafted switched off, using only the triggers and steps your plan ' + 'includes. Anything it needs that your site does not have yet is left for you to fill in.',
|
|
38
41
|
submit: 'Draft the automation',
|
|
39
|
-
started: 'The automation is being drafted. This can take a few minutes. You can close this ' + 'window: the draft appears in the list, switched off, when it is ready.',
|
|
42
|
+
started: 'The automation is being drafted. This can take a few minutes. You can close this ' + 'window: the draft appears in the list, switched off, when it is ready. Open AI ' + 'jobs to follow it meanwhile.',
|
|
40
43
|
drafted: 'Drafted',
|
|
41
44
|
review: 'Review it',
|
|
42
45
|
notListed: 'The draft is saved. It appears in the list in a moment.',
|
|
@@ -137,12 +140,26 @@ export function AiAutomationBriefDialog(props) {
|
|
|
137
140
|
pt: 1
|
|
138
141
|
},
|
|
139
142
|
children: [
|
|
140
|
-
running ? /*#__PURE__*/ _jsx(Alert, {
|
|
143
|
+
running && job ? /*#__PURE__*/ _jsx(Alert, {
|
|
141
144
|
severity: "info",
|
|
142
145
|
icon: /*#__PURE__*/ _jsx(CircularProgress, {
|
|
143
146
|
size: 18,
|
|
144
147
|
"aria-label": "Drafting"
|
|
145
148
|
}),
|
|
149
|
+
action: // Follows the job in the Assist panel (AGL-3593); the dialog
|
|
150
|
+
// covers it, so it closes.
|
|
151
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
152
|
+
color: "inherit",
|
|
153
|
+
size: "small",
|
|
154
|
+
variant: "outlined",
|
|
155
|
+
onClick: ()=>{
|
|
156
|
+
openAiJobs({
|
|
157
|
+
jobId: job.id
|
|
158
|
+
});
|
|
159
|
+
onClose();
|
|
160
|
+
},
|
|
161
|
+
children: 'Open AI jobs'
|
|
162
|
+
}),
|
|
146
163
|
children: copy.started
|
|
147
164
|
}) : null,
|
|
148
165
|
draft ? /*#__PURE__*/ _jsxs(Alert, {
|
|
@@ -207,8 +224,11 @@ export function AiAutomationBriefDialog(props) {
|
|
|
207
224
|
/*#__PURE__*/ _jsx(Button, {
|
|
208
225
|
size: "small",
|
|
209
226
|
variant: "outlined",
|
|
227
|
+
startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
228
|
+
path: mdiCreation.path
|
|
229
|
+
}),
|
|
210
230
|
onClick: ()=>setOpen(true),
|
|
211
|
-
children: '
|
|
231
|
+
children: 'Create with AI'
|
|
212
232
|
}),
|
|
213
233
|
/*#__PURE__*/ _jsx(AiAutomationBriefDialog, {
|
|
214
234
|
open: open,
|
|
@@ -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 { mdiCreation } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\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 * \"Create with AI\" 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\n size=\"small\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiCreation.path} />}\n onClick={() => setOpen(true)}\n >\n {'Create with AI'}\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","mdiCreation","MdiIcon","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","startIcon","path"],"mappings":"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"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
3
3
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
4
|
+
import { mdiCreation } from "@aglyn/shared-data-mdi";
|
|
5
|
+
import { MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
4
6
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
5
7
|
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
6
8
|
import { Button } from "@mui/material";
|
|
@@ -48,8 +50,11 @@ import { AiBriefDialog } from "./ai-brief-dialog.component.js";
|
|
|
48
50
|
/*#__PURE__*/ _jsx(Button, {
|
|
49
51
|
size: "small",
|
|
50
52
|
variant: "outlined",
|
|
53
|
+
startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
54
|
+
path: mdiCreation.path
|
|
55
|
+
}),
|
|
51
56
|
onClick: ()=>setOpen(true),
|
|
52
|
-
children: '
|
|
57
|
+
children: 'Create with AI'
|
|
53
58
|
}),
|
|
54
59
|
/*#__PURE__*/ _jsx(AiBriefDialog, {
|
|
55
60
|
kind: kind,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-describe-button.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 type {\n ConsoleHostComponentsZoneProps,\n ConsoleHostLayoutsZoneProps,\n ConsoleHostScreensZoneProps,\n ConsoleHostTemplatesZoneProps,\n} from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { Button } from '@mui/material'\nimport { useEffect, useRef, useState } from 'react'\nimport { AiBriefDialog, type AiBriefKind } from './ai-brief-dialog.component'\n\n/**\n * \"
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-describe-button.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 type {\n ConsoleHostComponentsZoneProps,\n ConsoleHostLayoutsZoneProps,\n ConsoleHostScreensZoneProps,\n ConsoleHostTemplatesZoneProps,\n} from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { mdiCreation } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { Button } from '@mui/material'\nimport { useEffect, useRef, useState } from 'react'\nimport { AiBriefDialog, type AiBriefKind } from './ai-brief-dialog.component'\n\n/**\n * \"Create with AI\" (AGL-2907, AGL-3043, AGL-3051): a job from a brief, beside the\n * create actions of the page that lists what the job makes — a page on\n * Screens, a page template on Templates, a layout on Layouts, a form on Forms\n * and a reusable component on Components — each mounted through that page's\n * zone. The Assist panel's AI jobs opens the same dialog for a page.\n */\n\ntype Verdict = 'checking' | 'ready' | 'hidden'\n\nexport interface AiDescribeButtonProps extends ConsoleHostScreensZoneProps {\n /** The job the brief starts: what the page the button sits on lists. */\n kind: AiBriefKind\n}\n\n/**\n * Renders nothing until the jobs route has answered for this workspace: the\n * shell decided the plan, the member's permission and the site's AI switch\n * before mounting it, and the release flag is the route's to decide, so a 404\n * or a 403 there is this control staying absent. A lockdown is the start\n * door's to say, in the dialog, in its own words.\n */\nexport function AiDescribeButton({ kind, hostId, orgId }: AiDescribeButtonProps) {\n const { data: user } = useUser()\n // Held in a ref so the probe keys on WHO is signed in, never on the\n // identity of the object that says so.\n const userRef = useRef(user)\n userRef.current = user\n const uid = user?.uid ?? null\n const [verdict, setVerdict] = useState<Verdict>('checking')\n const [open, setOpen] = useState(false)\n\n useEffect(() => {\n if (!orgId || !uid) return\n let active = true\n void (async () => {\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs?orgId=${encodeURIComponent(orgId)}&limit=1`,\n )\n if (active) {\n setVerdict(response.status === 404 || response.status === 403 ? 'hidden' : 'ready')\n }\n } catch {\n if (active) setVerdict('hidden')\n }\n })()\n return () => {\n active = false\n }\n }, [orgId, uid])\n\n if (verdict !== 'ready') return null\n return (\n <>\n <Button\n size=\"small\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiCreation.path} />}\n onClick={() => setOpen(true)}\n >\n {'Create with AI'}\n </Button>\n <AiBriefDialog\n kind={kind}\n open={open}\n onClose={() => setOpen(false)}\n orgId={orgId}\n hostId={hostId}\n user={user}\n />\n </>\n )\n}\n\n/** A page template from a brief, on the Templates page (`hostTemplates`). */\nexport function AiDescribeTemplateButton(props: ConsoleHostTemplatesZoneProps) {\n return <AiDescribeButton {...props} kind=\"template\" />\n}\n\n/** A layout from a brief, on the Layouts page (`hostLayouts`). */\nexport function AiDescribeLayoutButton(props: ConsoleHostLayoutsZoneProps) {\n return <AiDescribeButton {...props} kind=\"layout\" />\n}\n\n/**\n * A form from a brief, on the Forms page (`hostForms`, which the forms plugin\n * hosts on the `hostScreens` contract).\n */\nexport function AiDescribeFormButton(props: ConsoleHostScreensZoneProps) {\n return <AiDescribeButton {...props} kind=\"form\" />\n}\n\n/** A reusable component from a brief, on the Components page (`hostComponents`). */\nexport function AiDescribeComponentButton(props: ConsoleHostComponentsZoneProps) {\n return <AiDescribeButton {...props} kind=\"component\" />\n}\n\nexport default AiDescribeButton\n"],"names":["mdiCreation","MdiIcon","authorizedFetch","useUser","Button","useEffect","useRef","useState","AiBriefDialog","AiDescribeButton","kind","hostId","orgId","data","user","userRef","current","uid","verdict","setVerdict","open","setOpen","active","response","encodeURIComponent","status","size","variant","startIcon","path","onClick","onClose","AiDescribeTemplateButton","props","AiDescribeLayoutButton","AiDescribeFormButton","AiDescribeComponentButton"],"mappings":"AAAA;;;AAyBA,SAASA,WAAW,QAAQ,yBAAwB;AACpD,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,OAAO,QAAQ,iCAAgC;AACxD,SAASC,MAAM,QAAQ,gBAAe;AACtC,SAASC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AACnD,SAASC,aAAa,QAA0B,iCAA6B;AAiB7E;;;;;;CAMC,GACD,OAAO,SAASC,iBAAiB,EAAEC,IAAI,EAAEC,MAAM,EAAEC,KAAK,EAAyB;;IAC7E,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGX;IACvB,oEAAoE;IACpE,uCAAuC;IACvC,MAAMY,UAAUT,OAAOQ;IACvBC,QAAQC,OAAO,GAAGF;IAClB,MAAMG,cAAMH,wBAAAA,KAAMG,GAAG,mBAAI;IACzB,MAAM,CAACC,SAASC,WAAW,GAAGZ,SAAkB;IAChD,MAAM,CAACa,MAAMC,QAAQ,GAAGd,SAAS;IAEjCF,UAAU;QACR,IAAI,CAACO,SAAS,CAACK,KAAK;QACpB,IAAIK,SAAS;QACb,KAAK,AAAC,CAAA;YACJ,IAAI;gBACF,MAAMC,WAAW,MAAMrB,gBACrBa,QAAQC,OAAO,EACf,CAAC,mBAAmB,EAAEQ,mBAAmBZ,OAAO,QAAQ,CAAC;gBAE3D,IAAIU,QAAQ;oBACVH,WAAWI,SAASE,MAAM,KAAK,OAAOF,SAASE,MAAM,KAAK,MAAM,WAAW;gBAC7E;YACF,EAAE,eAAM;gBACN,IAAIH,QAAQH,WAAW;YACzB;QACF,CAAA;QACA,OAAO;YACLG,SAAS;QACX;IACF,GAAG;QAACV;QAAOK;KAAI;IAEf,IAAIC,YAAY,SAAS,OAAO;IAChC,qBACE;;0BACE,KAACd;gBACCsB,MAAK;gBACLC,SAAQ;gBACRC,yBAAW,KAAC3B;oBAAQ4B,MAAM7B,YAAY6B,IAAI;;gBAC1CC,SAAS,IAAMT,QAAQ;0BAEtB;;0BAEH,KAACb;gBACCE,MAAMA;gBACNU,MAAMA;gBACNW,SAAS,IAAMV,QAAQ;gBACvBT,OAAOA;gBACPD,QAAQA;gBACRG,MAAMA;;;;AAId;AAEA,2EAA2E,GAC3E,OAAO,SAASkB,yBAAyBC,KAAoC;IAC3E,qBAAO,KAACxB,+BAAqBwB;QAAOvB,MAAK;;AAC3C;AAEA,gEAAgE,GAChE,OAAO,SAASwB,uBAAuBD,KAAkC;IACvE,qBAAO,KAACxB,+BAAqBwB;QAAOvB,MAAK;;AAC3C;AAEA;;;CAGC,GACD,OAAO,SAASyB,qBAAqBF,KAAkC;IACrE,qBAAO,KAACxB,+BAAqBwB;QAAOvB,MAAK;;AAC3C;AAEA,kFAAkF,GAClF,OAAO,SAAS0B,0BAA0BH,KAAqC;IAC7E,qBAAO,KAACxB,+BAAqBwB;QAAOvB,MAAK;;AAC3C;AAEA,eAAeD,iBAAgB"}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { ConsoleHostScreensZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins';
|
|
18
18
|
/**
|
|
19
|
-
* "
|
|
19
|
+
* "Create with AI" (AGL-2907): a page from a brief, beside Templates and Create
|
|
20
20
|
* New Screen on a site's Screens page, mounted through the `hostScreens`
|
|
21
21
|
* zone. The Assist panel's AI jobs opens the same dialog. The button, its
|
|
22
22
|
* probe and its dialog are the ones every describe entry shares
|
|
@@ -3,7 +3,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
3
3
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
4
4
|
import { AiDescribeButton } from "./ai-describe-button.component.js";
|
|
5
5
|
/**
|
|
6
|
-
* "
|
|
6
|
+
* "Create with AI" (AGL-2907): a page from a brief, beside Templates and Create
|
|
7
7
|
* New Screen on a site's Screens page, mounted through the `hostScreens`
|
|
8
8
|
* zone. The Assist panel's AI jobs opens the same dialog. The button, its
|
|
9
9
|
* probe and its dialog are the ones every describe entry shares
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-describe-page.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 type { ConsoleHostScreensZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { AiDescribeButton } from './ai-describe-button.component'\n\n/**\n * \"
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-describe-page.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 type { ConsoleHostScreensZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { AiDescribeButton } from './ai-describe-button.component'\n\n/**\n * \"Create with AI\" (AGL-2907): a page from a brief, beside Templates and Create\n * New Screen on a site's Screens page, mounted through the `hostScreens`\n * zone. The Assist panel's AI jobs opens the same dialog. The button, its\n * probe and its dialog are the ones every describe entry shares\n * (`ai-describe-button.component.tsx`, AGL-3043).\n */\nexport function AiDescribePageButton(props: ConsoleHostScreensZoneProps) {\n return <AiDescribeButton {...props} kind=\"page\" />\n}\n\nexport default AiDescribePageButton\n"],"names":["AiDescribeButton","AiDescribePageButton","props","kind"],"mappings":"AAAA;;;AAoBA,SAASA,gBAAgB,QAAQ,oCAAgC;AAEjE;;;;;;CAMC,GACD,OAAO,SAASC,qBAAqBC,KAAkC;IACrE,qBAAO,KAACF,+BAAqBE;QAAOC,MAAK;;AAC3C;AAEA,eAAeF,qBAAoB"}
|
|
@@ -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":"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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,20 @@
|
|
|
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
|
+
* How a follow ended (AGL-3596): the stream ran its course (`ok`), the route
|
|
10
|
+
* answered that the job is not this reader's to see or does not exist
|
|
11
|
+
* (`not-found`), or the route could not be reached or failed (`error`).
|
|
12
|
+
*/
|
|
13
|
+
export type AiJobFollowEnd = 'ok' | 'not-found' | 'error';
|
|
14
|
+
/**
|
|
15
|
+
* Follows one job through the events route until it settles, the signal
|
|
16
|
+
* aborts or the route stops answering, handing each state to `onJob` and to
|
|
17
|
+
* every surface counting the workspace's jobs (AGL-3593). The server closes a
|
|
18
|
+
* stream at its deadline with `reconnect`; this re-opens it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function followAiJobEvents(user: () => MaybeTokenSource, orgId: string, jobId: string, signal: AbortSignal, onJob: (job: AiJobSummary) => void): Promise<AiJobFollowEnd>;
|
|
@@ -0,0 +1,78 @@
|
|
|
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.status === 404 || response.status === 403) return 'not-found';
|
|
62
|
+
if (!response.ok || !response.body) return 'error';
|
|
63
|
+
await readEventFrames(response.body, (event)=>{
|
|
64
|
+
if (signal.aborted) return;
|
|
65
|
+
if (event['type'] === 'state') {
|
|
66
|
+
const next = event['job'];
|
|
67
|
+
publishAiJob(next);
|
|
68
|
+
onJob(next);
|
|
69
|
+
} else if (event['type'] === 'reconnect') again = true;
|
|
70
|
+
});
|
|
71
|
+
} catch (unused) {
|
|
72
|
+
return 'error';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return 'ok';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//# 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 * How a follow ended (AGL-3596): the stream ran its course (`ok`), the route\n * answered that the job is not this reader's to see or does not exist\n * (`not-found`), or the route could not be reached or failed (`error`).\n */\nexport type AiJobFollowEnd = 'ok' | 'not-found' | 'error'\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<AiJobFollowEnd> {\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.status === 404 || response.status === 403) return 'not-found'\n if (!response.ok || !response.body) return 'error'\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 'error'\n }\n }\n return 'ok'\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","status","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;AASA;;;;;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,IAAII,SAASE,MAAM,KAAK,OAAOF,SAASE,MAAM,KAAK,KAAK,OAAO;YAC/D,IAAI,CAACF,SAASG,EAAE,IAAI,CAACH,SAAShC,IAAI,EAAE,OAAO;YAC3C,MAAMD,gBAAgBiC,SAAShC,IAAI,EAAE,CAACoC;gBACpC,IAAIR,OAAOG,OAAO,EAAE;gBACpB,IAAIK,KAAK,CAAC,OAAO,KAAK,SAAS;oBAC7B,MAAMC,OAAOD,KAAK,CAAC,MAAM;oBACzBtC,aAAauC;oBACbR,MAAMQ;gBACR,OAAO,IAAID,KAAK,CAAC,OAAO,KAAK,aAAaN,QAAQ;YACpD;QACF,EAAE,eAAM;YACN,OAAO;QACT;IACF;IACA,OAAO;AACT"}
|