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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +2 -1
  2. package/package.json +17 -17
  3. package/src/lib/billing/ai-month-meter.js +5 -3
  4. package/src/lib/billing/ai-month-meter.js.map +1 -1
  5. package/src/lib/billing/ai-overage-ledger.js +3 -1
  6. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  7. package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
  8. package/src/lib/components/ai-brief-dialog.component.js +35 -17
  9. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  10. package/src/lib/components/ai-describe-automation.component.js +17 -2
  11. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  12. package/src/lib/components/ai-insight-dialog.component.js +24 -2
  13. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  14. package/src/lib/components/ai-job-events.d.ts +14 -0
  15. package/src/lib/components/ai-job-events.js +76 -0
  16. package/src/lib/components/ai-job-events.js.map +1 -0
  17. package/src/lib/components/ai-job-follow.component.d.ts +49 -0
  18. package/src/lib/components/ai-job-follow.component.js +210 -0
  19. package/src/lib/components/ai-job-follow.component.js.map +1 -0
  20. package/src/lib/components/ai-job-links.d.ts +42 -0
  21. package/src/lib/components/ai-job-links.js +140 -0
  22. package/src/lib/components/ai-job-links.js.map +1 -0
  23. package/src/lib/components/ai-job-plan.component.d.ts +13 -6
  24. package/src/lib/components/ai-job-plan.component.js +29 -3
  25. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  26. package/src/lib/components/ai-job-requests.d.ts +24 -0
  27. package/src/lib/components/ai-job-requests.js +76 -0
  28. package/src/lib/components/ai-job-requests.js.map +1 -0
  29. package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
  30. package/src/lib/components/ai-jobs-indicator.component.js +80 -0
  31. package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
  32. package/src/lib/components/ai-jobs-store.d.ts +81 -0
  33. package/src/lib/components/ai-jobs-store.js +218 -0
  34. package/src/lib/components/ai-jobs-store.js.map +1 -0
  35. package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
  36. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  37. package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
  38. package/src/lib/components/ai-site-build-page.component.js +377 -0
  39. package/src/lib/components/ai-site-build-page.component.js.map +1 -0
  40. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  41. package/src/lib/components/ai-site-start-card.component.js +522 -130
  42. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  43. package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
  44. package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
  45. package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
  46. package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
  47. package/src/lib/components/assist-jobs-drawer.component.js +152 -172
  48. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  49. package/src/lib/components/assist-panel.component.js +62 -6
  50. package/src/lib/components/assist-panel.component.js.map +1 -1
  51. package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
  52. package/src/lib/components/staff-ai-credit-return.component.js +346 -0
  53. package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
  54. package/src/lib/components/staff-org-ai-card.component.js +364 -279
  55. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  56. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
  57. package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
  58. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  59. package/src/lib/components/use-ai-job-run.js +5 -18
  60. package/src/lib/components/use-ai-job-run.js.map +1 -1
  61. package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
  62. package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
  63. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
  64. package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
  65. package/src/lib/jobs/ai-job-plan-step.js +161 -17
  66. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
  68. package/src/lib/jobs/ai-job-site-step.js +34 -4
  69. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
  71. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
  73. package/src/lib/jobs/ai-jobs-notify.js +51 -0
  74. package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
  75. package/src/lib/jobs/ai-jobs.d.ts +31 -4
  76. package/src/lib/jobs/ai-jobs.js +196 -32
  77. package/src/lib/jobs/ai-jobs.js.map +1 -1
  78. package/src/lib/model/ai-job-activity.d.ts +112 -0
  79. package/src/lib/model/ai-job-activity.js +144 -0
  80. package/src/lib/model/ai-job-activity.js.map +1 -0
  81. package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
  82. package/src/lib/model/ai-job-failure-copy.js +78 -0
  83. package/src/lib/model/ai-job-failure-copy.js.map +1 -0
  84. package/src/lib/model/ai-job-notice.d.ts +61 -0
  85. package/src/lib/model/ai-job-notice.js +112 -0
  86. package/src/lib/model/ai-job-notice.js.map +1 -0
  87. package/src/lib/model/ai-jobs.types.d.ts +26 -0
  88. package/src/lib/model/ai-jobs.types.js +1 -0
  89. package/src/lib/model/ai-jobs.types.js.map +1 -1
  90. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  91. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  92. package/src/lib/model/ai-site-build-progress.d.ts +45 -0
  93. package/src/lib/model/ai-site-build-progress.js +76 -0
  94. package/src/lib/model/ai-site-build-progress.js.map +1 -0
  95. package/src/lib/model/ai-site-inventory.d.ts +9 -0
  96. package/src/lib/model/ai-site-inventory.js +7 -2
  97. package/src/lib/model/ai-site-inventory.js.map +1 -1
  98. package/src/lib/model/ai-site-job.d.ts +93 -2
  99. package/src/lib/model/ai-site-job.js +88 -7
  100. package/src/lib/model/ai-site-job.js.map +1 -1
  101. package/src/lib/model/ai-site-start.d.ts +3 -1
  102. package/src/lib/model/ai-site-start.js +6 -4
  103. package/src/lib/model/ai-site-start.js.map +1 -1
  104. package/src/lib/model/ai-template-subjects.d.ts +4 -3
  105. package/src/lib/model/ai-template-subjects.js +5 -4
  106. package/src/lib/model/ai-template-subjects.js.map +1 -1
  107. package/src/lib/plan-entitlements.d.ts +3 -3
  108. package/src/lib/plan-entitlements.js +3 -3
  109. package/src/lib/plan-entitlements.js.map +1 -1
  110. package/src/lib/plugin.js +28 -0
  111. package/src/lib/plugin.js.map +1 -1
  112. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  113. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  114. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  115. package/src/lib/runtime/ai-doctrine-validators.js +31 -4
  116. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  117. package/src/lib/runtime/ai-gate.d.ts +3 -2
  118. package/src/lib/runtime/ai-gate.js +3 -2
  119. package/src/lib/runtime/ai-gate.js.map +1 -1
  120. package/src/lib/runtime/ai-palette.generated.js +12 -4
  121. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  122. package/src/lib/runtime/site-inventory.d.ts +16 -1
  123. package/src/lib/runtime/site-inventory.js +43 -3
  124. package/src/lib/runtime/site-inventory.js.map +1 -1
  125. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  126. package/src/lib/server/ai-admin-credits.js +213 -0
  127. package/src/lib/server/ai-admin-credits.js.map +1 -0
  128. package/src/lib/server/ai-admin-org.js +12 -5
  129. package/src/lib/server/ai-admin-org.js.map +1 -1
  130. package/src/lib/server/ai-admin-signals.js +3 -1
  131. package/src/lib/server/ai-admin-signals.js.map +1 -1
  132. package/src/lib/server/ai-admin-user.js +12 -1
  133. package/src/lib/server/ai-admin-user.js.map +1 -1
  134. package/src/lib/server/ai-allotments.js +3 -2
  135. package/src/lib/server/ai-allotments.js.map +1 -1
  136. package/src/lib/server/ai-jobs-route.js +20 -2
  137. package/src/lib/server/ai-jobs-route.js.map +1 -1
  138. package/src/lib/server/assist-chat.js +3 -2
  139. package/src/lib/server/assist-chat.js.map +1 -1
  140. package/src/lib/server/billing-credits.js +4 -3
  141. package/src/lib/server/billing-credits.js.map +1 -1
  142. package/src/lib/server/billing-overage.js +3 -2
  143. package/src/lib/server/billing-overage.js.map +1 -1
  144. package/src/lib/server.js +11 -0
  145. package/src/lib/server.js.map +1 -1
  146. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  147. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  148. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  149. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  150. package/src/lib/usage/assist-credit-returns.js +79 -0
  151. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  152. package/src/lib/usage/assist-free-taste.js +6 -3
  153. package/src/lib/usage/assist-free-taste.js.map +1 -1
  154. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  155. package/src/lib/usage/assist-job-refund.js +109 -0
  156. package/src/lib/usage/assist-job-refund.js.map +1 -0
  157. package/src/lib/usage/assist-usage.js +5 -2
  158. package/src/lib/usage/assist-usage.js.map +1 -1
  159. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  160. package/src/lib/usage/staff-org-ai.js +19 -2
  161. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/assist-jobs-drawer.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 { checkEntitlement } from '@aglyn/aglyn'\nimport { trackEvent } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport { formatBytes } from '@aglyn/aglyn/app-utils/measure-node-map'\nimport {\n pluginRecordHref,\n pluginRecordListHref,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport {\n AI_JOB_TERMINAL_STATUSES,\n type AiJobOutput,\n type AiJobStatus,\n type AiJobSummary,\n} from '../model/ai-jobs.types'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { mdiChevronDown, mdiChevronUp } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n Box,\n Button,\n Chip,\n CircularProgress,\n Collapse,\n IconButton,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { AiInsightSurface } from '../model/ai-insight'\nimport { AiPageBriefDialog } from './ai-page-brief-dialog.component'\nimport { AiInsightDialog } from './ai-insight-dialog.component'\nimport { AiJobPlan } from './ai-job-plan.component'\n\n/**\n * The AI jobs drawer inside the Assist panel (AGL-2904): the workspace's\n * running and recent generation jobs, each step's progress, an \"open\n * draft\" link per output, and cancel.\n *\n * Reads through the routes, never Firestore directly, so the same\n * membership, flag and entitlement rungs answer every read; progress comes\n * from the events route's server-sent stream, re-opened when the server\n * says `reconnect`. Everything loads on EXPAND rather than on mount: the\n * panel opens on every console page, and a list nobody asked for is a read\n * on every page view.\n *\n * Hidden — not disabled — unless the flag shows it AND the plan carries\n * `aiGenerative`: a released-off feature does not exist, and a plan without\n * it is sold the add-on on the Billing page, not here.\n */\n\n/** Jobs the drawer lists; the route caps higher, the drawer is a glance. */\nconst LIST_LIMIT = 10\n\n/** How much of a brief a row shows. */\nconst BRIEF_PREVIEW_CHARS = 90\n\nconst STATUS_LABEL: Record<AiJobStatus, string> = {\n queued: 'Queued',\n running: 'Running',\n needs_input: 'Needs attention',\n needs_review: 'Needs review',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\nconst STATUS_COLOR: Record<\n AiJobStatus,\n 'default' | 'primary' | 'success' | 'error' | 'warning'\n> = {\n queued: 'default',\n running: 'primary',\n needs_input: 'warning',\n needs_review: 'warning',\n done: 'success',\n failed: 'error',\n canceled: 'default',\n}\n\n/** The besigner segment each versioned resource lives under. */\nconst BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>> = {\n screen: 'screens',\n reusableComponent: 'components',\n layout: 'layouts',\n template: 'templates',\n // An email design is a screen: it opens in the screen besigner, as the\n // Emails page's own Edit design does.\n emailScreen: 'screens',\n}\n\n/**\n * Where \"open draft\" goes, or `null` when the output has no page of its\n * own — a `text` output carries its copy on the job and is shown inline.\n * A versioned resource opens in the besigner on the version the job wrote;\n * one the console lists without a detail page opens its list; a `theme`\n * proposal opens the site's Theme section, where it is put in the editor.\n *\n * A console URL names a site by its SUBDOMAIN: the `[host]` segment resolves\n * through the member's host projection by subdomain, never by document id.\n * So an output that carries no `hostSubdomain` gets no link rather than one\n * that opens no site.\n */\nexport function aiJobOutputHref(output: AiJobOutput, orgSlug: string): string | null {\n const host = output.hostSubdomain\n if (!orgSlug || !host) return null\n const segment = BESIGNER_SEGMENT[output.resource]\n if (segment) {\n const base = `/${orgSlug}/hosts/${host}/${segment}/${output.id}`\n return output.versionId ? `${base}/versions/${output.versionId}/besigner` : base\n }\n if (output.resource === 'product') {\n // The catalog a drafted product lands in: the commerce plugin's address\n // for it (AGL-3080), or no link where commerce is not loaded.\n return pluginRecordListHref('product', { orgSlug, host })\n }\n if (output.resource === 'workflow') {\n // A drafted automation is an action (AGL-2919), listed switched off on\n // the workflows plugin's list of actions, or no link where it is not loaded.\n return pluginRecordListHref('action', { orgSlug, host })\n }\n if (output.resource === 'theme') {\n return buildRoute(Route.HOST_SETUP_THEME, { orgSlug, host })\n }\n if (output.resource === 'form') {\n // A new form has no version for the besigner to open: its own page — the\n // forms plugin's address for it — mints the first one, and holds the\n // routing and consent it declares.\n return pluginRecordHref('form', { orgSlug, host }, output.id)\n }\n if (output.resource === 'campaign') {\n // A campaign's page is the marketing plugin's, under the site: its\n // address for the campaign (AGL-3080), or no link where it is not loaded.\n return pluginRecordHref('campaign', { orgSlug, host }, output.id)\n }\n return null\n}\n\n/**\n * The navigation entry a page job proposes for the page it built (AGL-2907),\n * or `null`. The job writes no menu: the member adds the entry once the page\n * is live.\n */\nexport function aiJobNavigationProposal(output: AiJobOutput): string | null {\n if (output.resource !== 'screen') return null\n const navigation = output.proposal?.['navigation']\n if (!navigation || typeof navigation !== 'object') return null\n const label = (navigation as Record<string, unknown>)['label']\n return typeof label === 'string' && label.trim() ? label.trim() : null\n}\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\nexport interface AssistJobsDrawerProps {\n /** The org the panel is scoped to — `undefined` where the page named none. */\n orgId: string | undefined\n org: Partial<AglynOrgBilling> | undefined\n orgReady: boolean\n orgSlug: string\n user: Parameters<typeof authorizedFetch>[0]\n /** The `release_ai_generative` verdict, staff bypass applied (the shell's). */\n visible: boolean\n /**\n * The viewer is staff (AGL-3078): a job that stopped for a broken rule shows\n * them where the refused answer broke it. Who may read a job is unchanged.\n */\n isStaff?: boolean\n /** The site the console page is on, when it is on one: a page is described for a site. */\n hostId?: string | null\n /**\n * The page's insight surface (AGL-2915) — a site's Analytics, Data or CRM\n * Reports page, or the workspace's Data page — with the site's subdomain;\n * `null` elsewhere, where no question about the figures is offered.\n */\n insight?: { surface: Exclude<AiInsightSurface, 'digest'>; host: string | null } | null\n}\n\nexport function AssistJobsDrawer({\n orgId,\n org,\n orgReady,\n orgSlug,\n user,\n visible,\n isStaff = false,\n hostId,\n insight,\n}: AssistJobsDrawerProps): JSX.Element | null {\n const entitled = orgReady && checkEntitlement(org as never, 'aiGenerative')\n const [expanded, setExpanded] = useState(false)\n const [jobs, setJobs] = useState<AiJobSummary[]>([])\n const [loading, setLoading] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n /** Whether the page brief dialog is open (AGL-2907). */\n const [describing, setDescribing] = useState(false)\n /**\n * The insight dialog (AGL-2915): `'ask'` for a new question, a job id for an\n * answer a job already wrote, `null` when closed.\n */\n const [insightOpen, setInsightOpen] = useState<string | null>(null)\n /** Job ids whose terminal event has been tracked, so a re-read does not count twice. */\n const trackedRef = useRef(new Set<string>())\n /** The job whose stream is open, so a re-render does not open a second. */\n const watchingRef = useRef<string | null>(null)\n\n const patchJob = useCallback((next: AiJobSummary) => {\n setJobs((prior) => {\n const index = prior.findIndex((job) => job.id === next.id)\n if (index === -1) return [next, ...prior].slice(0, LIST_LIMIT)\n const copy = [...prior]\n copy[index] = next\n return copy\n })\n if (\n AI_JOB_TERMINAL_STATUSES.includes(next.status) &&\n !trackedRef.current.has(next.id)\n ) {\n trackedRef.current.add(next.id)\n if (next.status === 'done') {\n trackEvent('ai_job_completed', { kind: next.kind, credits: next.creditsSpent })\n } else if (next.status === 'failed') {\n trackEvent('ai_job_failed', { kind: next.kind })\n }\n }\n }, [])\n\n const load = useCallback(async () => {\n if (!orgId) return\n setLoading(true)\n setNotice(null)\n try {\n const response = await authorizedFetch(\n user,\n `/api/ai/jobs?orgId=${encodeURIComponent(orgId)}&limit=${LIST_LIMIT}`,\n )\n if (!response.ok) {\n const payload = await response.json().catch(() => null)\n setNotice(String(payload?.error ?? 'The AI jobs could not be loaded — try again.'))\n return\n }\n const payload = (await response.json()) as { jobs: AiJobSummary[] }\n // Every job that arrives already terminal is one the drawer did not\n // watch finish, so its outcome is not this session's to count.\n for (const job of payload.jobs) {\n if (AI_JOB_TERMINAL_STATUSES.includes(job.status)) trackedRef.current.add(job.id)\n }\n setJobs(payload.jobs)\n } catch {\n setNotice('The AI jobs could not be loaded — try again.')\n } finally {\n setLoading(false)\n }\n }, [orgId, user])\n\n useEffect(() => {\n if (expanded) void load()\n }, [expanded, load])\n\n // Reset when the panel's org changes: a list from one workspace must not\n // show under another's key while the new one loads.\n useEffect(() => {\n setJobs([])\n setExpanded(false)\n watchingRef.current = null\n }, [orgId])\n\n // Watch the most recent job that is still moving. One stream at a time:\n // the route re-reads a document every two seconds per open stream, and a\n // drawer with three running jobs does not need three of them.\n const watched = jobs.find((job) => !AI_JOB_TERMINAL_STATUSES.includes(job.status))\n const watchedId = watched?.id ?? null\n useEffect(() => {\n if (!expanded || !orgId || !watchedId || watchingRef.current === watchedId) return\n watchingRef.current = watchedId\n const controller = new AbortController()\n const watch = async () => {\n // The server closes a stream at its deadline with `reconnect`; the\n // loop here re-opens it until the job settles or the drawer unmounts.\n let again = true\n while (again && !controller.signal.aborted) {\n again = false\n try {\n const response = await authorizedFetch(\n user,\n `/api/ai/jobs/${encodeURIComponent(watchedId)}/events?orgId=${encodeURIComponent(orgId)}`,\n { signal: controller.signal },\n )\n if (!response.ok || !response.body) return\n await readEventFrames(response.body, (event) => {\n if (event['type'] === 'state') patchJob(event['job'] as AiJobSummary)\n else if (event['type'] === 'reconnect') again = true\n else if (event['type'] === 'gone') {\n setJobs((prior) => prior.filter((job) => job.id !== watchedId))\n }\n })\n } catch {\n // The stream ended without a terminal state — a network blip or\n // the drawer closing. The next expand reloads the list.\n return\n }\n }\n }\n void watch().finally(() => {\n if (watchingRef.current === watchedId) watchingRef.current = null\n })\n return () => {\n controller.abort()\n if (watchingRef.current === watchedId) watchingRef.current = null\n }\n }, [expanded, orgId, watchedId, user, patchJob])\n\n const cancel = useCallback(\n async (jobId: string) => {\n if (!orgId) return\n try {\n const response = await authorizedFetch(\n user,\n `/api/ai/jobs/${encodeURIComponent(jobId)}/cancel`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId }),\n },\n )\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n setNotice(String(payload?.error ?? 'The job could not be canceled — try again.'))\n return\n }\n if (payload?.job) patchJob(payload.job as AiJobSummary)\n } catch {\n setNotice('The job could not be canceled — try again.')\n }\n },\n [orgId, user, patchJob],\n )\n\n /** The job whose resume is in flight, so its button cannot send twice. */\n const [resuming, setResuming] = useState<string | null>(null)\n\n // Confirms a plan, or tries again a step whose answer broke a building\n // rule (AGL-2935). The door runs the next step inline and answers with the\n // job, so the row moves on without waiting for the stream.\n const resume = useCallback(\n async (job: AiJobSummary) => {\n if (!orgId) return\n setResuming(job.id)\n setNotice(null)\n try {\n const response = await authorizedFetch(\n user,\n `/api/ai/jobs/${encodeURIComponent(job.id)}/resume`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, hostId: job.hostId }),\n },\n )\n const payload = await response.json().catch(() => null)\n if (payload?.job) patchJob(payload.job as AiJobSummary)\n if (!response.ok) {\n setNotice(String(payload?.error ?? 'The job could not be resumed — try again.'))\n }\n } catch {\n setNotice('The job could not be resumed — try again.')\n } finally {\n setResuming(null)\n }\n },\n [orgId, user, patchJob],\n )\n\n if (!visible || !orgId || !entitled) return null\n\n const active = jobs.filter((job) => !AI_JOB_TERMINAL_STATUSES.includes(job.status)).length\n\n return (\n <Box sx={{ mb: 2, borderBottom: 1, borderColor: 'divider', pb: 1 }}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"subtitle2\" sx={{ flexGrow: 1 }}>\n AI jobs\n {active > 0 && (\n <Chip size=\"small\" color=\"primary\" label={`${active} running`} sx={{ ml: 1 }} />\n )}\n </Typography>\n {/* A question about the page's figures (AGL-2915). */}\n {insight && (\n <Button size=\"small\" onClick={() => setInsightOpen('ask')}>\n Ask about your numbers\n </Button>\n )}\n {/* A page from a brief (AGL-2907), on a site's own routes. */}\n {hostId && (\n <Button size=\"small\" onClick={() => setDescribing(true)}>\n Describe a page\n </Button>\n )}\n <IconButton\n size=\"small\"\n aria-label={expanded ? 'Hide AI jobs' : 'Show AI jobs'}\n aria-expanded={expanded}\n onClick={() => setExpanded((prior) => !prior)}\n >\n <MdiIcon\n path={expanded ? mdiChevronUp.path : mdiChevronDown.path}\n sx={{ fontSize: 18 }}\n />\n </IconButton>\n </Stack>\n <Collapse in={expanded} unmountOnExit>\n {notice && (\n <Typography variant=\"caption\" color=\"error\" component=\"div\" sx={{ mt: 0.5 }}>\n {notice}\n </Typography>\n )}\n {loading && !jobs.length && (\n <CircularProgress size={16} sx={{ mt: 1 }} aria-label=\"Loading AI jobs\" />\n )}\n {!loading && !jobs.length && !notice && (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\" sx={{ mt: 0.5 }}>\n No AI jobs yet.\n </Typography>\n )}\n <Stack spacing={1} sx={{ mt: 1 }} role=\"list\" aria-label=\"AI jobs\">\n {jobs.map((job) => {\n const stepsDone = job.steps.filter((step) => step.status === 'done').length\n const current = job.steps.find((step) => step.status === 'running')\n const terminal = AI_JOB_TERMINAL_STATUSES.includes(job.status)\n return (\n <Box\n key={job.id}\n role=\"listitem\"\n sx={{ borderRadius: 1, bgcolor: 'action.hover', px: 1.5, py: 1 }}\n >\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Chip size=\"small\" variant=\"outlined\" label={job.kind} />\n <Chip size=\"small\" color={STATUS_COLOR[job.status]} label={STATUS_LABEL[job.status]} />\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{ flexGrow: 1 }}>\n {stepsDone}/{job.steps.length} steps\n {current ? ` — ${current.name}` : ''}\n {job.creditsSpent > 0 ? ` · ${job.creditsSpent} credits` : ''}\n </Typography>\n {!terminal && (\n <Button size=\"small\" onClick={() => void cancel(job.id)}>\n Cancel\n </Button>\n )}\n </Stack>\n <Typography variant=\"body2\" sx={{ mt: 0.5, wordBreak: 'break-word' }}>\n {job.brief.length > BRIEF_PREVIEW_CHARS\n ? `${job.brief.slice(0, BRIEF_PREVIEW_CHARS)}…`\n : job.brief}\n </Typography>\n {job.error && (\n <Typography variant=\"caption\" color={job.status === 'failed' ? 'error' : 'warning.main'} component=\"div\">\n {job.error}\n </Typography>\n )}\n {job.outputs.map((output, index) => {\n const href = aiJobOutputHref(output, orgSlug)\n const navigation = aiJobNavigationProposal(output)\n return (\n <Box key={`${output.resource}:${output.id}:${index}`} sx={{ mt: 0.5 }}>\n {output.resource === 'insight' ? (\n <Button size=\"small\" onClick={() => setInsightOpen(output.id)}>\n View answer\n </Button>\n ) : href ? (\n <AppLink componentVariant=\"naked\" href={href}>\n Open draft — {output.label}\n </AppLink>\n ) : output.text ? (\n <Typography\n variant=\"body2\"\n component=\"pre\"\n sx={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit', m: 0 }}\n >\n {output.text}\n </Typography>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {output.label}\n </Typography>\n )}\n {output.load ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\">\n About {formatBytes(output.load.totalBytes)} on a first visit\n </Typography>\n ) : null}\n {output.note ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\">\n {output.note}\n </Typography>\n ) : null}\n {navigation ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\">\n Add “{navigation}” to your navigation once the page is live.\n </Typography>\n ) : null}\n </Box>\n )\n })}\n <AiJobPlan\n job={job}\n onResume={(target) => void resume(target)}\n busy={resuming === job.id}\n staff={isStaff}\n />\n </Box>\n )\n })}\n </Stack>\n </Collapse>\n {/*\n The brief dialog (AGL-2907). It starts a job and closes; the list\n below it is the only place the job is watched, so an open list reads\n the new job back as soon as the dialog hands it over.\n */}\n {insightOpen !== null && (\n <AiInsightDialog\n open\n onClose={() => {\n setInsightOpen(null)\n if (expanded) void load()\n }}\n orgId={orgId}\n orgSlug={orgSlug}\n hostId={insight?.host ? (hostId ?? null) : null}\n host={insight?.host ?? null}\n surface={insight?.surface ?? 'analytics'}\n user={user}\n uid={(user as { uid?: string } | null | undefined)?.uid ?? null}\n jobId={insightOpen === 'ask' ? null : insightOpen}\n />\n )}\n {hostId && (\n <AiPageBriefDialog\n open={describing}\n onClose={() => {\n setDescribing(false)\n if (expanded) void load()\n }}\n orgId={orgId}\n hostId={hostId}\n user={user}\n />\n )}\n </Box>\n )\n}\n\nexport default AssistJobsDrawer\n"],"names":["checkEntitlement","trackEvent","buildRoute","Route","formatBytes","pluginRecordHref","pluginRecordListHref","AI_JOB_TERMINAL_STATUSES","mdiChevronDown","mdiChevronUp","AppLink","MdiIcon","authorizedFetch","Box","Button","Chip","CircularProgress","Collapse","IconButton","Stack","Typography","useCallback","useEffect","useRef","useState","AiPageBriefDialog","AiInsightDialog","AiJobPlan","LIST_LIMIT","BRIEF_PREVIEW_CHARS","STATUS_LABEL","queued","running","needs_input","needs_review","done","failed","canceled","STATUS_COLOR","BESIGNER_SEGMENT","screen","reusableComponent","layout","template","emailScreen","aiJobOutputHref","output","orgSlug","host","hostSubdomain","segment","resource","base","id","versionId","HOST_SETUP_THEME","aiJobNavigationProposal","navigation","proposal","label","trim","readEventFrames","body","onEvent","reader","getReader","decoder","TextDecoder","buffer","frames","value","read","decode","stream","split","pop","frame","line","find","part","startsWith","JSON","parse","slice","length","AssistJobsDrawer","orgId","org","orgReady","user","visible","isStaff","hostId","insight","entitled","expanded","setExpanded","jobs","setJobs","loading","setLoading","notice","setNotice","describing","setDescribing","insightOpen","setInsightOpen","trackedRef","Set","watchingRef","patchJob","next","prior","index","findIndex","job","copy","includes","status","current","has","add","kind","credits","creditsSpent","load","response","encodeURIComponent","ok","payload","json","catch","String","error","watched","watchedId","controller","AbortController","watch","again","signal","aborted","event","filter","finally","abort","cancel","jobId","method","headers","stringify","resuming","setResuming","resume","active","sx","mb","borderBottom","borderColor","pb","direction","spacing","alignItems","variant","flexGrow","size","color","ml","onClick","aria-label","aria-expanded","path","fontSize","in","unmountOnExit","component","mt","role","map","stepsDone","steps","step","terminal","borderRadius","bgcolor","px","py","name","wordBreak","brief","outputs","href","componentVariant","text","whiteSpace","fontFamily","m","totalBytes","note","onResume","target","busy","staff","open","onClose","surface","uid"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/assist-jobs-drawer.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 { checkEntitlement } from '@aglyn/aglyn'\nimport { trackEvent } from '@aglyn/aglyn/app-utils/analytics-events'\nimport { formatBytes } from '@aglyn/aglyn/app-utils/measure-node-map'\nimport {\n AI_JOB_TERMINAL_STATUSES,\n type AiJobOutput,\n type AiJobSummary,\n} from '../model/ai-jobs.types'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { mdiChevronDown, mdiChevronUp } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n Box,\n Button,\n Chip,\n CircularProgress,\n Collapse,\n IconButton,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n AI_JOB_PHASE_LABELS,\n aiJobActivityCounts,\n aiJobPhase,\n aiJobsHeaderChips,\n type AiJobActivityState,\n type AiJobPhase,\n} from '../model/ai-job-activity'\nimport type { AiInsightSurface } from '../model/ai-insight'\nimport { cancelAiJobRequest, resumeAiJobRequest } from './ai-job-requests'\nimport { publishAiJob, type AiJobsOpenRequest } from './ai-jobs-store'\nimport { AiPageBriefDialog } from './ai-page-brief-dialog.component'\nimport { AiInsightDialog } from './ai-insight-dialog.component'\nimport { AiJobPlan } from './ai-job-plan.component'\nimport { aiJobOutputHref, aiJobPrimaryLink } from './ai-job-links'\nimport { readEventFrames } from './ai-job-events'\n\n// Where an output opens, and the stream reader, live beside the dialogs that\n// share them (AGL-3593); this module has always exported them.\nexport { aiJobOutputHref, aiJobPrimaryLink, readEventFrames }\n\n/**\n * The AI jobs drawer inside the Assist panel (AGL-2904): the workspace's\n * running and recent generation jobs, each step's progress, an \"open\n * draft\" link per output, and cancel.\n *\n * Reads through the routes, never Firestore directly, so the same\n * membership, flag and entitlement rungs answer every read; progress comes\n * from the events route's server-sent stream, re-opened when the server\n * says `reconnect`. Everything loads on EXPAND rather than on mount: the\n * panel opens on every console page, and a list nobody asked for is a read\n * on every page view.\n *\n * It opens EXPANDED while any of the workspace's jobs is moving or waiting\n * for the person (AGL-3593) — the shared list the top-bar indicator reads\n * says so — because a plan waiting behind a collapsed row is a site that\n * never gets built. An \"Open AI jobs\" anywhere in the console expands it,\n * scrolls to the job it names and highlights it for a moment.\n *\n * Hidden — not disabled — unless the flag shows it AND the plan carries\n * `aiGenerative`: a released-off feature does not exist, and a plan without\n * it is sold the add-on on the Billing page, not here.\n */\n\n/** Jobs the drawer lists; the route caps higher, the drawer is a glance. */\nconst LIST_LIMIT = 10\n\n/** How much of a brief a row shows. */\nconst BRIEF_PREVIEW_CHARS = 90\n\n/**\n * A row's chip says where the job stands in the words every AI surface uses\n * (AGL-3593): a plan waiting to be confirmed is \"Plan ready\", never \"Running\"\n * and never the machine's `needs_review`.\n */\nconst PHASE_COLOR: Record<AiJobPhase, 'default' | 'primary' | 'success' | 'error' | 'warning'> = {\n queued: 'default',\n planning: 'primary',\n 'plan-ready': 'warning',\n building: 'primary',\n attention: 'warning',\n done: 'success',\n failed: 'error',\n canceled: 'default',\n}\n\n/** The header's count chips, colored as the rows they count. */\nconst STATE_COLOR: Record<AiJobActivityState, 'primary' | 'warning'> = {\n 'needs-you': 'warning',\n attention: 'warning',\n running: 'primary',\n}\n\n/** How long a job AI jobs was opened on stays highlighted. */\nconst HIGHLIGHT_MS = 2_500\n\n/** What a job cost the person: what it spent, less what we refunded. */\nfunction netCredits(job: Pick<AiJobSummary, 'creditsSpent' | 'refundedCredits'>): number {\n return Math.max(0, (job.creditsSpent ?? 0) - (job.refundedCredits ?? 0))\n}\n\n/**\n * The navigation entry a page job proposes for the page it built (AGL-2907),\n * or `null`. The job writes no menu: the member adds the entry once the page\n * is live.\n */\nexport function aiJobNavigationProposal(output: AiJobOutput): string | null {\n if (output.resource !== 'screen') return null\n const navigation = output.proposal?.['navigation']\n if (!navigation || typeof navigation !== 'object') return null\n const label = (navigation as Record<string, unknown>)['label']\n return typeof label === 'string' && label.trim() ? label.trim() : null\n}\n\n\nexport interface AssistJobsDrawerProps {\n /** The org the panel is scoped to — `undefined` where the page named none. */\n orgId: string | undefined\n org: Partial<AglynOrgBilling> | undefined\n orgReady: boolean\n orgSlug: string\n user: Parameters<typeof authorizedFetch>[0]\n /** The `release_ai_generative` verdict, staff bypass applied (the shell's). */\n visible: boolean\n /**\n * The viewer is staff (AGL-3078): a job that stopped for a broken rule shows\n * them where the refused answer broke it. Who may read a job is unchanged.\n */\n isStaff?: boolean\n /** The site the console page is on, when it is on one: a page is described for a site. */\n hostId?: string | null\n /**\n * The page's insight surface (AGL-2915) — a site's Analytics, Data or CRM\n * Reports page, or the workspace's Data page — with the site's subdomain;\n * `null` elsewhere, where no question about the figures is offered.\n */\n insight?: { surface: Exclude<AiInsightSurface, 'digest'>; host: string | null } | null\n /**\n * The workspace's jobs that are not yet settled, from the shared store the\n * top-bar indicator reads (AGL-3593). The header counts them, and the\n * drawer opens expanded while there is any. Absent, the drawer counts what\n * it loaded itself.\n */\n inFlight?: readonly AiJobSummary[]\n /**\n * The latest request to open AI jobs (AGL-3593): a new `seq` expands the\n * drawer, and a named job is scrolled to and highlighted.\n */\n focus?: AiJobsOpenRequest | null\n}\n\nexport function AssistJobsDrawer({\n orgId,\n org,\n orgReady,\n orgSlug,\n user,\n visible,\n isStaff = false,\n hostId,\n insight,\n inFlight,\n focus,\n}: AssistJobsDrawerProps): JSX.Element | null {\n const entitled = orgReady && checkEntitlement(org as never, 'aiGenerative')\n const [expanded, setExpanded] = useState(false)\n /**\n * The person opened or closed the list themselves: from then on the\n * expanded-by-default rule leaves their choice alone until the workspace\n * changes.\n */\n const toggledRef = useRef(false)\n /** The job AI jobs was opened on, highlighted for a moment once it is listed. */\n const [highlight, setHighlight] = useState<string | null>(null)\n const listRef = useRef<HTMLDivElement | null>(null)\n const [jobs, setJobs] = useState<AiJobSummary[]>([])\n const [loading, setLoading] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n /** Whether the page brief dialog is open (AGL-2907). */\n const [describing, setDescribing] = useState(false)\n /**\n * The insight dialog (AGL-2915): `'ask'` for a new question, a job id for an\n * answer a job already wrote, `null` when closed.\n */\n const [insightOpen, setInsightOpen] = useState<string | null>(null)\n /** Job ids whose terminal event has been tracked, so a re-read does not count twice. */\n const trackedRef = useRef(new Set<string>())\n /** The job whose stream is open, so a re-render does not open a second. */\n const watchingRef = useRef<string | null>(null)\n\n const patchJob = useCallback((next: AiJobSummary) => {\n // Every counting surface moves with the row (AGL-3593).\n publishAiJob(next)\n setJobs((prior) => {\n const index = prior.findIndex((job) => job.id === next.id)\n if (index === -1) return [next, ...prior].slice(0, LIST_LIMIT)\n const copy = [...prior]\n copy[index] = next\n return copy\n })\n if (\n AI_JOB_TERMINAL_STATUSES.includes(next.status) &&\n !trackedRef.current.has(next.id)\n ) {\n trackedRef.current.add(next.id)\n if (next.status === 'done') {\n trackEvent('ai_job_completed', { kind: next.kind, credits: next.creditsSpent })\n } else if (next.status === 'failed') {\n trackEvent('ai_job_failed', { kind: next.kind })\n }\n }\n }, [])\n\n const load = useCallback(async () => {\n if (!orgId) return\n setLoading(true)\n setNotice(null)\n try {\n const response = await authorizedFetch(\n user,\n `/api/ai/jobs?orgId=${encodeURIComponent(orgId)}&limit=${LIST_LIMIT}`,\n )\n if (!response.ok) {\n const payload = await response.json().catch(() => null)\n setNotice(String(payload?.error ?? 'The AI jobs could not be loaded — try again.'))\n return\n }\n const payload = (await response.json()) as { jobs: AiJobSummary[] }\n // Every job that arrives already terminal is one the drawer did not\n // watch finish, so its outcome is not this session's to count.\n for (const job of payload.jobs) {\n if (AI_JOB_TERMINAL_STATUSES.includes(job.status)) trackedRef.current.add(job.id)\n // A job the indicator still counts that has settled since leaves it.\n publishAiJob(job)\n }\n setJobs(payload.jobs)\n } catch {\n setNotice('The AI jobs could not be loaded — try again.')\n } finally {\n setLoading(false)\n }\n }, [orgId, user])\n\n useEffect(() => {\n if (expanded) void load()\n }, [expanded, load])\n\n // Reset when the panel's org changes: a list from one workspace must not\n // show under another's key while the new one loads.\n useEffect(() => {\n setJobs([])\n setExpanded(false)\n toggledRef.current = false\n watchingRef.current = null\n }, [orgId])\n\n // Expanded by default while anything is moving or waiting (AGL-3593): a\n // plan waiting to be confirmed behind a collapsed row is a site that never\n // gets built. Otherwise collapsed, which reads nothing until asked.\n const anyInFlight = (inFlight?.length ?? 0) > 0\n useEffect(() => {\n if (anyInFlight && !toggledRef.current) setExpanded(true)\n }, [anyInFlight])\n\n // A request to open AI jobs (AGL-3593): expand, read the list again so a\n // job created a moment ago is in it, and highlight the job named.\n const focusSeq = focus?.seq ?? 0\n const focusJobId = focus?.jobId ?? null\n const expandedRef = useRef(expanded)\n expandedRef.current = expanded\n useEffect(() => {\n if (!focusSeq) return\n toggledRef.current = true\n setHighlight(focusJobId)\n // An open list does not re-run the expand effect's read, so it reads here.\n if (expandedRef.current) void load()\n else setExpanded(true)\n // Only a new request runs this; `load` and the job are read as they stand.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [focusSeq])\n\n // Scroll to the highlighted job once it is listed, then let it fade.\n const highlightListed = highlight !== null && jobs.some((job) => job.id === highlight)\n useEffect(() => {\n if (!highlight || !highlightListed) return undefined\n const row = Array.from(\n listRef.current?.querySelectorAll<HTMLElement>('[data-ai-job-id]') ?? [],\n ).find((element) => element.dataset['aiJobId'] === highlight)\n row?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' })\n const timer = setTimeout(() => setHighlight(null), HIGHLIGHT_MS)\n return () => clearTimeout(timer)\n }, [highlight, highlightListed])\n\n // Watch the most recent job that is still moving. One stream at a time:\n // the route re-reads a document every two seconds per open stream, and a\n // drawer with three running jobs does not need three of them.\n const watched = jobs.find((job) => !AI_JOB_TERMINAL_STATUSES.includes(job.status))\n const watchedId = watched?.id ?? null\n useEffect(() => {\n if (!expanded || !orgId || !watchedId || watchingRef.current === watchedId) return\n watchingRef.current = watchedId\n const controller = new AbortController()\n const watch = async () => {\n // The server closes a stream at its deadline with `reconnect`; the\n // loop here re-opens it until the job settles or the drawer unmounts.\n let again = true\n while (again && !controller.signal.aborted) {\n again = false\n try {\n const response = await authorizedFetch(\n user,\n `/api/ai/jobs/${encodeURIComponent(watchedId)}/events?orgId=${encodeURIComponent(orgId)}`,\n { signal: controller.signal },\n )\n if (!response.ok || !response.body) return\n await readEventFrames(response.body, (event) => {\n if (event['type'] === 'state') patchJob(event['job'] as AiJobSummary)\n else if (event['type'] === 'reconnect') again = true\n else if (event['type'] === 'gone') {\n setJobs((prior) => prior.filter((job) => job.id !== watchedId))\n }\n })\n } catch {\n // The stream ended without a terminal state — a network blip or\n // the drawer closing. The next expand reloads the list.\n return\n }\n }\n }\n void watch().finally(() => {\n if (watchingRef.current === watchedId) watchingRef.current = null\n })\n return () => {\n controller.abort()\n if (watchingRef.current === watchedId) watchingRef.current = null\n }\n }, [expanded, orgId, watchedId, user, patchJob])\n\n // The same door the dialog that started the job cancels through (AGL-3593).\n const cancel = useCallback(\n async (jobId: string) => {\n if (!orgId) return\n const { job, error } = await cancelAiJobRequest(user, orgId, jobId)\n if (error) {\n setNotice(error)\n return\n }\n if (job) patchJob(job)\n },\n [orgId, user, patchJob],\n )\n\n /** The job whose resume is in flight, so its button cannot send twice. */\n const [resuming, setResuming] = useState<string | null>(null)\n\n // Confirms a plan, or tries again a step whose answer broke a building\n // rule (AGL-2935). The door runs the next step inline and answers with the\n // job, so the row moves on without waiting for the stream.\n const resume = useCallback(\n async (job: AiJobSummary) => {\n if (!orgId) return\n setResuming(job.id)\n setNotice(null)\n // The same door the dialog that started the job confirms through (AGL-3593).\n const { job: next, error } = await resumeAiJobRequest(user, orgId, job)\n if (next) patchJob(next)\n if (error) setNotice(error)\n setResuming(null)\n },\n [orgId, user, patchJob],\n )\n\n if (!visible || !orgId || !entitled) return null\n\n // Counted by state, never as one \"running\" figure (AGL-3593): a plan waiting\n // to be confirmed needs the person, and a stopped job is not moving.\n const chips = aiJobsHeaderChips(aiJobActivityCounts(inFlight ?? jobs))\n\n return (\n <Box sx={{ mb: 2, borderBottom: 1, borderColor: 'divider', pb: 1 }}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"subtitle2\" sx={{ flexGrow: 1 }}>\n AI jobs\n {chips.map((chip) => (\n <Chip\n key={chip.state}\n size=\"small\"\n color={STATE_COLOR[chip.state]}\n label={chip.label}\n sx={{ ml: 1 }}\n />\n ))}\n </Typography>\n {/* A question about the page's figures (AGL-2915). */}\n {insight && (\n <Button size=\"small\" onClick={() => setInsightOpen('ask')}>\n Ask about your numbers\n </Button>\n )}\n {/* A page from a brief (AGL-2907), on a site's own routes. */}\n {hostId && (\n <Button size=\"small\" onClick={() => setDescribing(true)}>\n Describe a page\n </Button>\n )}\n <IconButton\n size=\"small\"\n aria-label={expanded ? 'Hide AI jobs' : 'Show AI jobs'}\n aria-expanded={expanded}\n onClick={() => {\n toggledRef.current = true\n setExpanded((prior) => !prior)\n }}\n >\n <MdiIcon\n path={expanded ? mdiChevronUp.path : mdiChevronDown.path}\n sx={{ fontSize: 18 }}\n />\n </IconButton>\n </Stack>\n <Collapse in={expanded} unmountOnExit>\n {notice && (\n <Typography variant=\"caption\" color=\"error\" component=\"div\" sx={{ mt: 0.5 }}>\n {notice}\n </Typography>\n )}\n {loading && !jobs.length && (\n <CircularProgress size={16} sx={{ mt: 1 }} aria-label=\"Loading AI jobs\" />\n )}\n {!loading && !jobs.length && !notice && (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\" sx={{ mt: 0.5 }}>\n No AI jobs yet.\n </Typography>\n )}\n <Stack ref={listRef} spacing={1} sx={{ mt: 1 }} role=\"list\" aria-label=\"AI jobs\">\n {jobs.map((job) => {\n const stepsDone = job.steps.filter((step) => step.status === 'done').length\n const current = job.steps.find((step) => step.status === 'running')\n const terminal = AI_JOB_TERMINAL_STATUSES.includes(job.status)\n const phase = aiJobPhase(job)\n const primary = aiJobPrimaryLink(job, orgSlug)\n const highlighted = highlight === job.id\n return (\n <Box\n key={job.id}\n role=\"listitem\"\n data-ai-job-id={job.id}\n aria-current={highlighted ? 'true' : undefined}\n sx={{\n borderRadius: 1,\n bgcolor: highlighted ? 'action.selected' : 'action.hover',\n outline: 2,\n outlineColor: highlighted ? 'primary.main' : 'transparent',\n transition: (theme) =>\n theme.transitions.create(['background-color', 'outline-color'], {\n duration: theme.transitions.duration.standard,\n }),\n px: 1.5,\n py: 1,\n }}\n >\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Chip size=\"small\" variant=\"outlined\" label={job.kind} />\n <Chip size=\"small\" color={PHASE_COLOR[phase]} label={AI_JOB_PHASE_LABELS[phase]} />\n <Typography variant=\"caption\" color=\"text.secondary\" sx={{ flexGrow: 1 }}>\n {stepsDone}/{job.steps.length} steps\n {current ? ` — ${current.name}` : ''}\n {/* Net of what a failure on our side gave back (AGL-3594). */}\n {netCredits(job) > 0 ? ` · ${netCredits(job)} credits` : ''}\n </Typography>\n {!terminal && (\n <Button size=\"small\" onClick={() => void cancel(job.id)}>\n Cancel\n </Button>\n )}\n </Stack>\n <Typography variant=\"body2\" sx={{ mt: 0.5, wordBreak: 'break-word' }}>\n {job.brief.length > BRIEF_PREVIEW_CHARS\n ? `${job.brief.slice(0, BRIEF_PREVIEW_CHARS)}…`\n : job.brief}\n </Typography>\n {job.error && (\n <Typography variant=\"caption\" color={job.status === 'failed' ? 'error' : 'warning.main'} component=\"div\">\n {job.error}\n </Typography>\n )}\n {/* What a finished job built, first (AGL-3593). */}\n {primary ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n component={AppLink}\n href={primary.href}\n sx={{ mt: 0.5 }}\n >\n {primary.label}\n </Button>\n ) : null}\n {job.outputs.map((output, index) => {\n const href = aiJobOutputHref(output, orgSlug)\n const navigation = aiJobNavigationProposal(output)\n return (\n <Box key={`${output.resource}:${output.id}:${index}`} sx={{ mt: 0.5 }}>\n {output.resource === 'insight' ? (\n <Button size=\"small\" onClick={() => setInsightOpen(output.id)}>\n View answer\n </Button>\n ) : href ? (\n <AppLink componentVariant=\"naked\" href={href}>\n Open draft — {output.label}\n </AppLink>\n ) : output.text ? (\n <Typography\n variant=\"body2\"\n component=\"pre\"\n sx={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit', m: 0 }}\n >\n {output.text}\n </Typography>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {output.label}\n </Typography>\n )}\n {output.load ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\">\n About {formatBytes(output.load.totalBytes)} on a first visit\n </Typography>\n ) : null}\n {output.note ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\">\n {output.note}\n </Typography>\n ) : null}\n {navigation ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\">\n Add “{navigation}” to your navigation once the page is live.\n </Typography>\n ) : null}\n </Box>\n )\n })}\n <AiJobPlan\n job={job}\n onResume={(target) => void resume(target)}\n busy={resuming === job.id}\n staff={isStaff}\n user={user}\n />\n </Box>\n )\n })}\n </Stack>\n </Collapse>\n {/*\n The brief dialog (AGL-2907). It starts a job and closes; the list\n below it is the only place the job is watched, so an open list reads\n the new job back as soon as the dialog hands it over.\n */}\n {insightOpen !== null && (\n <AiInsightDialog\n open\n onClose={() => {\n setInsightOpen(null)\n if (expanded) void load()\n }}\n orgId={orgId}\n orgSlug={orgSlug}\n hostId={insight?.host ? (hostId ?? null) : null}\n host={insight?.host ?? null}\n surface={insight?.surface ?? 'analytics'}\n user={user}\n uid={(user as { uid?: string } | null | undefined)?.uid ?? null}\n jobId={insightOpen === 'ask' ? null : insightOpen}\n />\n )}\n {hostId && (\n <AiPageBriefDialog\n open={describing}\n onClose={() => {\n setDescribing(false)\n if (expanded) void load()\n }}\n orgId={orgId}\n hostId={hostId}\n user={user}\n orgSlug={orgSlug}\n isStaff={isStaff}\n />\n )}\n </Box>\n )\n}\n\nexport default AssistJobsDrawer\n"],"names":["checkEntitlement","trackEvent","formatBytes","AI_JOB_TERMINAL_STATUSES","mdiChevronDown","mdiChevronUp","AppLink","MdiIcon","authorizedFetch","Box","Button","Chip","CircularProgress","Collapse","IconButton","Stack","Typography","useCallback","useEffect","useRef","useState","AI_JOB_PHASE_LABELS","aiJobActivityCounts","aiJobPhase","aiJobsHeaderChips","cancelAiJobRequest","resumeAiJobRequest","publishAiJob","AiPageBriefDialog","AiInsightDialog","AiJobPlan","aiJobOutputHref","aiJobPrimaryLink","readEventFrames","LIST_LIMIT","BRIEF_PREVIEW_CHARS","PHASE_COLOR","queued","planning","building","attention","done","failed","canceled","STATE_COLOR","running","HIGHLIGHT_MS","netCredits","job","Math","max","creditsSpent","refundedCredits","aiJobNavigationProposal","output","resource","navigation","proposal","label","trim","AssistJobsDrawer","orgId","org","orgReady","orgSlug","user","visible","isStaff","hostId","insight","inFlight","focus","entitled","expanded","setExpanded","toggledRef","highlight","setHighlight","listRef","jobs","setJobs","loading","setLoading","notice","setNotice","describing","setDescribing","insightOpen","setInsightOpen","trackedRef","Set","watchingRef","patchJob","next","prior","index","findIndex","id","slice","copy","includes","status","current","has","add","kind","credits","load","response","encodeURIComponent","ok","payload","json","catch","String","error","anyInFlight","length","focusSeq","seq","focusJobId","jobId","expandedRef","highlightListed","some","row","undefined","Array","from","querySelectorAll","find","element","dataset","scrollIntoView","block","behavior","timer","setTimeout","clearTimeout","watched","watchedId","controller","AbortController","watch","again","signal","aborted","body","event","filter","finally","abort","cancel","resuming","setResuming","resume","chips","sx","mb","borderBottom","borderColor","pb","direction","spacing","alignItems","variant","flexGrow","map","chip","size","color","state","ml","onClick","aria-label","aria-expanded","path","fontSize","in","unmountOnExit","component","mt","ref","role","stepsDone","steps","step","terminal","phase","primary","highlighted","data-ai-job-id","aria-current","borderRadius","bgcolor","outline","outlineColor","transition","theme","transitions","create","duration","standard","px","py","name","wordBreak","brief","href","outputs","componentVariant","text","whiteSpace","fontFamily","m","totalBytes","note","onResume","target","busy","staff","open","onClose","host","surface","uid"],"mappings":"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"}
@@ -22,7 +22,7 @@ import { trackEvent } from "@aglyn/aglyn/app-utils/analytics-events";
22
22
  import { mdiChatQuestionOutline, mdiChevronDown, mdiChevronUp, mdiClose, mdiOpenInNew, mdiSend, mdiThumbDownOutline, mdiThumbUpOutline } from "@aglyn/shared-data-mdi";
23
23
  import { AppLink, MdiIcon } from "@aglyn/shared-ui-jsx";
24
24
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
25
- import { Alert, Box, Button, Chip, CircularProgress, Collapse, Drawer, Fab, IconButton, Link, Paper, Stack, TextField, Tooltip, Typography } from "@mui/material";
25
+ import { Alert, Badge, Box, Button, Chip, CircularProgress, Collapse, Drawer, Fab, IconButton, Link, Paper, Stack, TextField, Tooltip, Typography } from "@mui/material";
26
26
  import { usePathname } from "next/navigation";
27
27
  import { Fragment, useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
28
28
  import { useUser } from "@aglyn/tenant-feature-instance";
@@ -32,6 +32,8 @@ import { applyAssistEdit, describeAssistEditCanvas } from "./assist-edit-canvas.
32
32
  import { AssistEditCard } from "./assist-edit-card.component.js";
33
33
  import { aiInsightSurfaceForPath } from "../model/ai-insight.js";
34
34
  import { AssistJobsDrawer } from "./assist-jobs-drawer.component.js";
35
+ import { AI_JOB_LINK_PARAM, openAiJobs, useAiJobsInFlight, useAiJobsOpenRequest } from "./ai-jobs-store.js";
36
+ import { aiJobsActivity, aiJobsLauncherLabel } from "../model/ai-job-activity.js";
35
37
  import { AiModelSelector } from "./ai-model-selector.component.js";
36
38
  import { AiUsageStrip } from "./ai-usage-strip.component.js";
37
39
  import { useAiModelChoice } from "./use-ai-model-choice.js";
@@ -371,6 +373,7 @@ function saveThread(orgId, messages) {
371
373
  });
372
374
  }
373
375
  export function AssistPanelComponent(props) {
376
+ var _ref;
374
377
  const { org, orgId, orgReady, scopedOrgId, orgSlug, hostId, productName, assistVisible, assistStaffPreview, generativeVisible, isStaff, aiPermissions: ai } = props;
375
378
  const verdict = {
376
379
  visible: assistVisible,
@@ -426,6 +429,48 @@ export function AssistPanelComponent(props) {
426
429
  // (AGL-2906). A hint, not a grant: the server decides the edit rung again
427
430
  // from its own reads, and drops the outline below it.
428
431
  const editRungHint = generativeVisible && ai.loaded && ai.generate && orgReady && checkEntitlement(org, 'aiGenerative');
432
+ // ── AI jobs, findable from anywhere (AGL-3593) ─────────────────────────
433
+ //
434
+ // The workspace's unsettled jobs, from the one list the top-bar indicator
435
+ // reads — read only where the reader could run a job at all: the flag, the
436
+ // plan's `aiGenerative` and their own `ai.generate`, as the drawer's gate.
437
+ const jobsVisible = Boolean(scopedOrgId) && editRungHint;
438
+ const inFlight = useAiJobsInFlight(user, scopedOrgId, jobsVisible);
439
+ const jobsActivity = aiJobsActivity(inFlight);
440
+ // Any surface asking to open AI jobs — an "Open AI jobs" button, the
441
+ // indicator, a notification's link — opens the panel on its drawer.
442
+ // The request is handed to the drawer until the panel closes, so opening
443
+ // the panel again later does not re-highlight a job asked for before.
444
+ const openRequest = useAiJobsOpenRequest();
445
+ const [jobsFocus, setJobsFocus] = useState(null);
446
+ useEffect(()=>{
447
+ if (openRequest.seq === 0) return;
448
+ setJobsFocus(openRequest);
449
+ setOpen(true);
450
+ // A new `seq` is a new request; the object is read as it stands.
451
+ // eslint-disable-next-line react-hooks/exhaustive-deps
452
+ }, [
453
+ openRequest.seq
454
+ ]);
455
+ useEffect(()=>{
456
+ if (!open) setJobsFocus(null);
457
+ }, [
458
+ open
459
+ ]);
460
+ // A notification's link names the job as `?aiJob={id}` on a console page:
461
+ // arriving on it opens AI jobs on that job, once per address.
462
+ useEffect(()=>{
463
+ var _ref;
464
+ var _globalThis_location;
465
+ if (!jobsVisible) return;
466
+ const jobId = new URLSearchParams((_ref = (_globalThis_location = globalThis.location) == null ? void 0 : _globalThis_location.search) != null ? _ref : '').get(AI_JOB_LINK_PARAM);
467
+ if (jobId) openAiJobs({
468
+ jobId
469
+ });
470
+ }, [
471
+ jobsVisible,
472
+ pathname
473
+ ]);
429
474
  const send = useCallback(async ()=>{
430
475
  const question = input.trim();
431
476
  if (!question || busy || !scopedOrgId || !ai.use) return;
@@ -741,7 +786,8 @@ export function AssistPanelComponent(props) {
741
786
  children: /*#__PURE__*/ _jsx(Fab, {
742
787
  color: "primary",
743
788
  size: "medium",
744
- "aria-label": `Open ${branding.productName} Assist`,
789
+ // Says what waits inside as well as what it opens (AGL-3593).
790
+ "aria-label": aiJobsLauncherLabel(branding.productName, jobsActivity),
745
791
  onClick: ()=>setOpen(true),
746
792
  sx: {
747
793
  position: 'fixed',
@@ -760,9 +806,17 @@ export function AssistPanelComponent(props) {
760
806
  bottom: 'var(--aglyn-dock-inset-bottom, 20px)',
761
807
  zIndex: (theme)=>theme.zIndex.drawer - 1
762
808
  },
763
- children: /*#__PURE__*/ _jsx(MdiIcon, {
764
- path: mdiChatQuestionOutline.path,
765
- fontSize: "medium"
809
+ children: /*#__PURE__*/ _jsx(Badge, {
810
+ color: jobsActivity && jobsActivity.state !== 'running' ? 'warning' : 'secondary',
811
+ variant: (jobsActivity == null ? void 0 : jobsActivity.state) === 'running' ? 'dot' : 'standard',
812
+ badgeContent: jobsActivity ? jobsActivity.count : 0,
813
+ invisible: !jobsActivity,
814
+ overlap: "circular",
815
+ "data-ai-jobs-badge": (_ref = jobsActivity == null ? void 0 : jobsActivity.state) != null ? _ref : undefined,
816
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
817
+ path: mdiChatQuestionOutline.path,
818
+ fontSize: "medium"
819
+ })
766
820
  })
767
821
  })
768
822
  }),
@@ -853,7 +907,9 @@ export function AssistPanelComponent(props) {
853
907
  // A question about the figures, where the page reports them
854
908
  // (AGL-2915); hidden with the rest of AI jobs, and refused by
855
909
  // the jobs door where the member may not generate.
856
- insight: ai.generate ? aiInsightSurfaceForPath(pathname) : null
910
+ insight: ai.generate ? aiInsightSurfaceForPath(pathname) : null,
911
+ inFlight: jobsVisible ? inFlight : undefined,
912
+ focus: jobsFocus
857
913
  }),
858
914
  !messages.length && /*#__PURE__*/ _jsxs(Alert, {
859
915
  severity: "info",