@aglyn/plugins-ai 1.0.0-beta.225 → 1.0.0-beta.227
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +17 -17
- package/src/lib/billing/ai-month-meter.js +5 -3
- package/src/lib/billing/ai-month-meter.js.map +1 -1
- package/src/lib/billing/ai-overage-ledger.js +3 -1
- package/src/lib/billing/ai-overage-ledger.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
- package/src/lib/components/ai-brief-dialog.component.js +35 -17
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +17 -2
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-insight-dialog.component.js +24 -2
- package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-events.d.ts +14 -0
- package/src/lib/components/ai-job-events.js +76 -0
- package/src/lib/components/ai-job-events.js.map +1 -0
- package/src/lib/components/ai-job-follow.component.d.ts +49 -0
- package/src/lib/components/ai-job-follow.component.js +210 -0
- package/src/lib/components/ai-job-follow.component.js.map +1 -0
- package/src/lib/components/ai-job-links.d.ts +42 -0
- package/src/lib/components/ai-job-links.js +140 -0
- package/src/lib/components/ai-job-links.js.map +1 -0
- package/src/lib/components/ai-job-plan.component.d.ts +13 -6
- package/src/lib/components/ai-job-plan.component.js +29 -3
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +24 -0
- package/src/lib/components/ai-job-requests.js +76 -0
- package/src/lib/components/ai-job-requests.js.map +1 -0
- package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
- package/src/lib/components/ai-jobs-indicator.component.js +80 -0
- package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
- package/src/lib/components/ai-jobs-store.d.ts +81 -0
- package/src/lib/components/ai-jobs-store.js +218 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -0
- package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
- package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
- package/src/lib/components/ai-site-build-page.component.js +377 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +522 -130
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
- package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
- package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
- package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
- package/src/lib/components/assist-jobs-drawer.component.js +152 -172
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +62 -6
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
- package/src/lib/components/staff-ai-credit-return.component.js +346 -0
- package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
- package/src/lib/components/staff-org-ai-card.component.js +364 -279
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
- package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
- package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.js +5 -18
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
- package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
- package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
- package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
- package/src/lib/jobs/ai-job-plan-step.js +161 -17
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-site-step.js +34 -4
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
- package/src/lib/jobs/ai-jobs-notify.js +51 -0
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
- package/src/lib/jobs/ai-jobs.d.ts +31 -4
- package/src/lib/jobs/ai-jobs.js +196 -32
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +112 -0
- package/src/lib/model/ai-job-activity.js +144 -0
- package/src/lib/model/ai-job-activity.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
- package/src/lib/model/ai-job-failure-copy.js +78 -0
- package/src/lib/model/ai-job-failure-copy.js.map +1 -0
- package/src/lib/model/ai-job-notice.d.ts +61 -0
- package/src/lib/model/ai-job-notice.js +112 -0
- package/src/lib/model/ai-job-notice.js.map +1 -0
- package/src/lib/model/ai-jobs.types.d.ts +26 -0
- package/src/lib/model/ai-jobs.types.js +1 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +45 -0
- package/src/lib/model/ai-site-build-progress.js +76 -0
- package/src/lib/model/ai-site-build-progress.js.map +1 -0
- package/src/lib/model/ai-site-inventory.d.ts +9 -0
- package/src/lib/model/ai-site-inventory.js +7 -2
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +93 -2
- package/src/lib/model/ai-site-job.js +88 -7
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +3 -1
- package/src/lib/model/ai-site-start.js +6 -4
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/ai-template-subjects.d.ts +4 -3
- package/src/lib/model/ai-template-subjects.js +5 -4
- package/src/lib/model/ai-template-subjects.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +3 -3
- package/src/lib/plan-entitlements.js +3 -3
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +28 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
- package/src/lib/runtime/ai-abuse-guards.js +10 -7
- package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.js +31 -4
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-gate.d.ts +3 -2
- package/src/lib/runtime/ai-gate.js +3 -2
- package/src/lib/runtime/ai-gate.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +12 -4
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +16 -1
- package/src/lib/runtime/site-inventory.js +43 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-admin-credits.d.ts +21 -0
- package/src/lib/server/ai-admin-credits.js +213 -0
- package/src/lib/server/ai-admin-credits.js.map +1 -0
- package/src/lib/server/ai-admin-org.js +12 -5
- package/src/lib/server/ai-admin-org.js.map +1 -1
- package/src/lib/server/ai-admin-signals.js +3 -1
- package/src/lib/server/ai-admin-signals.js.map +1 -1
- package/src/lib/server/ai-admin-user.js +12 -1
- package/src/lib/server/ai-admin-user.js.map +1 -1
- package/src/lib/server/ai-allotments.js +3 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +20 -2
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-chat.js +3 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/billing-credits.js +4 -3
- package/src/lib/server/billing-credits.js.map +1 -1
- package/src/lib/server/billing-overage.js +3 -2
- package/src/lib/server/billing-overage.js.map +1 -1
- package/src/lib/server.js +11 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
- package/src/lib/usage/assist-credit-returns-write.js +150 -0
- package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
- package/src/lib/usage/assist-credit-returns.d.ts +68 -0
- package/src/lib/usage/assist-credit-returns.js +79 -0
- package/src/lib/usage/assist-credit-returns.js.map +1 -0
- package/src/lib/usage/assist-free-taste.js +6 -3
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage/assist-job-refund.d.ts +100 -0
- package/src/lib/usage/assist-job-refund.js +109 -0
- package/src/lib/usage/assist-job-refund.js.map +1 -0
- package/src/lib/usage/assist-usage.js +5 -2
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/staff-org-ai.d.ts +31 -1
- package/src/lib/usage/staff-org-ai.js +19 -2
- package/src/lib/usage/staff-org-ai.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-job-plan.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 {\n AI_BUILD_PLAN_CREATION_NOUNS,\n AI_BUILD_PLAN_EMBED_HOST_NAMES,\n isAiPlanNewRef,\n type AiBuildPlanEmbed,\n} from '../model/ai-build-plan'\nimport type { AiJobReview, AiJobSummary } from '../model/ai-jobs.types'\nimport { aiJobPlanCreditEstimate } from '../model/ai-site-job'\nimport { Box, Button, Collapse, Stack, Typography } from '@mui/material'\nimport { useState } from 'react'\n\n/**\n * A job's plan and what it waits for (AGL-2935), inside the AI jobs drawer:\n * what the job reuses from the site, what it creates and why, the screens it\n * builds — and, while the job waits for a person, the one button that\n * confirms the plan or tries a refused step again. The drawer owns the\n * request; this renders.\n */\nexport interface AiJobPlanProps {\n job: AiJobSummary\n /** Confirms the plan, or tries the refused step again. */\n onResume: (job: AiJobSummary) => void\n /** A resume for this job is in flight. */\n busy?: boolean\n /**\n * The viewer is staff (AGL-3078), who can open where a refused answer broke\n * its rules and the outline of those parts, to tell a shape the rules should\n * allow from an answer that ignored its re-ask. A member reads the findings.\n */\n staff?: boolean\n}\n\n/**\n * Where a refused answer broke its rules, and the outline of those parts, as\n * lines staff read (AGL-3078): each finding with the nodes or plan entries it\n * names, then each outlined node indented by its depth, with its element, a\n * Grid's layout as written, and the names of what it sets and holds.\n */\nexport function aiJobReviewDetails(review: AiJobReview | null): string[] {\n if (review?.reason !== 'doctrine') return []\n const lines = review.findings.flatMap((finding) => {\n const where = finding.nodeIds?.length\n ? `nodes ${finding.nodeIds.join(', ')}`\n : finding.paths?.length\n ? `at ${finding.paths.join(', ')}`\n : null\n return where ? [`${finding.rule === null ? '' : `Rule ${finding.rule} `}${finding.code}: ${where}`] : []\n })\n for (const node of review.outline ?? []) {\n lines.push(\n [\n `${' '.repeat(node.depth)}${node.id} ${node.componentId}`,\n ...Object.entries(node.grid ?? {}).map(([name, value]) => `${name}=${JSON.stringify(value)}`),\n ...(node.props.length ? [`props ${node.props.join(', ')}`] : []),\n ...(node.sx?.length ? [`sx ${node.sx.join(', ')}`] : []),\n ...(node.children.length ? [`holds ${node.children.join(', ')}`] : []),\n ].join(' · '),\n )\n }\n return lines\n}\n\n/**\n * A third-party player the plan lists, with what it costs the page, read\n * before the member confirms it (rule 16, AGL-3433). The cost is the\n * platform's sentence, never the model's: the player is framed lazily, so its\n * host's code loads when a visitor reaches it, whether or not they press play.\n */\nexport function aiPlanEmbedLine(\n embed: AiBuildPlanEmbed,\n named: (ref: string) => string,\n): string {\n const host = AI_BUILD_PLAN_EMBED_HOST_NAMES[embed.host]\n const where = isAiPlanNewRef(embed.where) ? `the component ${named(embed.where)}` : embed.where\n const link = embed.url ? ` playing ${embed.url}` : ', its link left for you to paste'\n return `Embeds a ${host} player on ${where}${link}, as you asked (“${embed.asked}”). It loads ${host}’s own code when a visitor reaches it, whether or not they press play.`\n}\n\nexport function AiJobPlan({\n job,\n onResume,\n busy = false,\n staff = false,\n}: AiJobPlanProps): JSX.Element | null {\n const [detailsOpen, setDetailsOpen] = useState(false)\n const { plan, review } = job\n if (!plan && !review) return null\n const details = staff ? aiJobReviewDetails(review) : []\n /** A reference as a person reads it: the record's name, or what the plan creates. */\n const named = (ref: string | null): string =>\n !ref\n ? ''\n : isAiPlanNewRef(ref)\n ? ref.slice('new:'.length)\n : (plan?.labels[ref] ?? ref)\n const waiting = job.status === 'needs_review' && review !== null\n // The guard rail (AGL-2911): what the plan is estimated to cost is read\n // before it is confirmed, not after it has been spent. An estimate, and\n // said to be one — the plan's own passes at the nominal credits a step\n // holds, where what a step really costs is its model's tokens. A page job\n // counts the creations it builds before its page (AGL-3031).\n const estimate =\n plan && waiting && review.reason === 'plan' ? aiJobPlanCreditEstimate(job.kind, plan) : 0\n return (\n <Box sx={{ mt: 1 }}>\n {plan && (\n <Stack spacing={0.5} role=\"list\" aria-label=\"Plan\">\n <Typography variant=\"caption\" color=\"text.secondary\">\n {plan.status === 'confirmed' ? 'Confirmed plan' : 'Proposed plan'}\n </Typography>\n {plan.reuse.map((entry, index) => (\n <Typography key={`reuse-${index}`} variant=\"body2\" role=\"listitem\">\n Reuses the {entry.kind === 'screen' ? 'page' : entry.kind} {named(entry.id)} — {entry.purpose}\n </Typography>\n ))}\n {plan.create.map((entry, index) => (\n <Typography key={`create-${index}`} variant=\"body2\" role=\"listitem\">\n Creates the {AI_BUILD_PLAN_CREATION_NOUNS[entry.kind].noun} {entry.name}\n {entry.duplicateOf\n ? `, from a copy of ${named(entry.duplicateOf)}`\n : ''}{' '}\n — {entry.why}\n </Typography>\n ))}\n {plan.screens.map((screen, index) => (\n <Typography key={`screen-${index}`} variant=\"body2\" role=\"listitem\">\n {screen.record\n ? `Builds the record template ${screen.title}, a page per record of ${named(screen.record.dataset)} at /${screen.record.base}/…`\n : `Builds the page ${screen.title} at ${screen.slug}`}\n {screen.duplicateOf\n ? `, from a copy of ${named(screen.duplicateOf)}`\n : ''}\n {screen.layout ? ` in ${named(screen.layout)}` : ''}\n {screen.sections.length\n ? `: ${screen.sections.map((section) => section.name).join(', ')}`\n : ''}\n </Typography>\n ))}\n {(plan.embeds ?? []).map((embed, index) => (\n <Typography key={`embed-${index}`} variant=\"body2\" role=\"listitem\">\n {aiPlanEmbedLine(embed, named)}\n </Typography>\n ))}\n </Stack>\n )}\n {review?.reason === 'doctrine' && review.findings.length > 0 && (\n <Box\n component=\"ul\"\n sx={{ my: 0.5, pl: 2.5 }}\n aria-label=\"Building rules broken\"\n >\n {review.findings.map((finding, index) => (\n <Typography\n key={`finding-${index}`}\n component=\"li\"\n variant=\"caption\"\n >\n {finding.message}\n </Typography>\n ))}\n </Box>\n )}\n {details.length > 0 && (\n <Box sx={{ mt: 0.5 }}>\n <Button\n size=\"small\"\n aria-expanded={detailsOpen}\n onClick={() => setDetailsOpen((prior) => !prior)}\n >\n {detailsOpen ? 'Hide what was refused' : 'Show what was refused'}\n </Button>\n <Collapse in={detailsOpen} unmountOnExit>\n <Typography\n variant=\"caption\"\n component=\"pre\"\n aria-label=\"What was refused\"\n sx={{ m: 0, fontFamily: 'monospace', whiteSpace: 'pre', overflowX: 'auto' }}\n >\n {details.join('\\n')}\n </Typography>\n </Collapse>\n </Box>\n )}\n {estimate > 0 && (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ display: 'block', mt: 1 }}\n >\n {`Estimated cost: about ${estimate.toLocaleString('en-US')} credits. What it costs is what its steps spend.`}\n </Typography>\n )}\n {waiting && (\n <Button\n size=\"small\"\n variant=\"contained\"\n disabled={busy}\n onClick={() => onResume(job)}\n sx={{ mt: 1 }}\n >\n {review.reason === 'plan' ? 'Confirm plan' : 'Try again'}\n </Button>\n )}\n </Box>\n )\n}\n\nexport default AiJobPlan\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","AI_BUILD_PLAN_EMBED_HOST_NAMES","isAiPlanNewRef","aiJobPlanCreditEstimate","Box","Button","Collapse","Stack","Typography","useState","aiJobReviewDetails","review","reason","lines","findings","flatMap","finding","where","nodeIds","length","join","paths","rule","code","node","outline","push","repeat","depth","id","componentId","Object","entries","grid","map","name","value","JSON","stringify","props","sx","children","aiPlanEmbedLine","embed","named","host","link","url","asked","AiJobPlan","job","onResume","busy","staff","plan","detailsOpen","setDetailsOpen","details","ref","slice","labels","waiting","status","estimate","kind","mt","spacing","role","aria-label","variant","color","reuse","entry","index","purpose","create","noun","duplicateOf","why","screens","screen","record","title","dataset","base","slug","layout","sections","section","embeds","component","my","pl","message","size","aria-expanded","onClick","prior","in","unmountOnExit","m","fontFamily","whiteSpace","overflowX","display","toLocaleString","disabled"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-job-plan.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 {\n AI_BUILD_PLAN_CREATION_NOUNS,\n AI_BUILD_PLAN_EMBED_HOST_NAMES,\n isAiPlanNewRef,\n type AiBuildPlanEmbed,\n} from '../model/ai-build-plan'\nimport type { AiJobReview, AiJobSummary } from '../model/ai-jobs.types'\nimport { aiJobPlanCreditEstimate } from '../model/ai-site-job'\nimport { aiSiteStarterFallbackOffered } from '../model/ai-job-failure-copy'\nimport { AiSiteStarterFallback } from './ai-site-starter-fallback.component'\nimport { Box, Button, Collapse, Stack, Typography } from '@mui/material'\nimport { useState } from 'react'\n\n/**\n * A job's plan and what it waits for (AGL-2935), inside the AI jobs drawer\n * and the dialog that started the job (AGL-3593): what the job reuses from\n * the site, what it creates and why, the screens it builds — and, while the\n * job waits for a person, the one button that confirms the plan or tries a\n * refused step again. Both send the same request (`resumeAiJobRequest`);\n * this renders.\n */\nexport interface AiJobPlanProps {\n job: AiJobSummary\n /** Confirms the plan, or tries the refused step again. */\n onResume: (job: AiJobSummary) => void\n /** A resume for this job is in flight. */\n busy?: boolean\n /**\n * The viewer is staff (AGL-3078), who can open where a refused answer broke\n * its rules and the outline of those parts, to tell a shape the rules should\n * allow from an answer that ignored its re-ask. A member reads the findings.\n */\n staff?: boolean\n /**\n * The signed-in user, for \"Use the starter site instead\" on a guided start\n * that did not work out (AGL-3594); the action is drawn only when given.\n */\n user?: Parameters<typeof AiSiteStarterFallback>[0]['user']\n}\n\n/**\n * Where a refused answer broke its rules, and the outline of those parts, as\n * lines staff read (AGL-3078): each finding with the nodes or plan entries it\n * names, then each outlined node indented by its depth, with its element, a\n * Grid's layout as written, and the names of what it sets and holds.\n */\nexport function aiJobReviewDetails(review: AiJobReview | null): string[] {\n if (review?.reason !== 'doctrine') return []\n const lines = review.findings.flatMap((finding) => {\n const where = finding.nodeIds?.length\n ? `nodes ${finding.nodeIds.join(', ')}`\n : finding.paths?.length\n ? `at ${finding.paths.join(', ')}`\n : null\n return where ? [`${finding.rule === null ? '' : `Rule ${finding.rule} `}${finding.code}: ${where}`] : []\n })\n for (const node of review.outline ?? []) {\n lines.push(\n [\n `${' '.repeat(node.depth)}${node.id} ${node.componentId}`,\n ...Object.entries(node.grid ?? {}).map(([name, value]) => `${name}=${JSON.stringify(value)}`),\n ...(node.props.length ? [`props ${node.props.join(', ')}`] : []),\n ...(node.sx?.length ? [`sx ${node.sx.join(', ')}`] : []),\n ...(node.children.length ? [`holds ${node.children.join(', ')}`] : []),\n ].join(' · '),\n )\n }\n return lines\n}\n\n/**\n * A third-party player the plan lists, with what it costs the page, read\n * before the member confirms it (rule 16, AGL-3433). The cost is the\n * platform's sentence, never the model's: the player is framed lazily, so its\n * host's code loads when a visitor reaches it, whether or not they press play.\n */\nexport function aiPlanEmbedLine(\n embed: AiBuildPlanEmbed,\n named: (ref: string) => string,\n): string {\n const host = AI_BUILD_PLAN_EMBED_HOST_NAMES[embed.host]\n const where = isAiPlanNewRef(embed.where) ? `the component ${named(embed.where)}` : embed.where\n const link = embed.url ? ` playing ${embed.url}` : ', its link left for you to paste'\n return `Embeds a ${host} player on ${where}${link}, as you asked (“${embed.asked}”). It loads ${host}’s own code when a visitor reaches it, whether or not they press play.`\n}\n\nexport function AiJobPlan({\n job,\n onResume,\n busy = false,\n staff = false,\n user,\n}: AiJobPlanProps): JSX.Element | null {\n const [detailsOpen, setDetailsOpen] = useState(false)\n const { plan, review } = job\n if (!plan && !review) return null\n const details = staff ? aiJobReviewDetails(review) : []\n /** A reference as a person reads it: the record's name, or what the plan creates. */\n const named = (ref: string | null): string =>\n !ref\n ? ''\n : isAiPlanNewRef(ref)\n ? ref.slice('new:'.length)\n : (plan?.labels[ref] ?? ref)\n const waiting = job.status === 'needs_review' && review !== null\n // The guard rail (AGL-2911): what the plan is estimated to cost is read\n // before it is confirmed, not after it has been spent. An estimate, and\n // said to be one — the plan's own passes at the nominal credits a step\n // holds, where what a step really costs is its model's tokens. A page job\n // counts the creations it builds before its page (AGL-3031).\n const estimate =\n plan && waiting && review.reason === 'plan' ? aiJobPlanCreditEstimate(job.kind, plan) : 0\n return (\n <Box sx={{ mt: 1 }}>\n {plan && (\n <Stack spacing={0.5} role=\"list\" aria-label=\"Plan\">\n <Typography variant=\"caption\" color=\"text.secondary\">\n {plan.status === 'confirmed' ? 'Confirmed plan' : 'Proposed plan'}\n </Typography>\n {plan.reuse.map((entry, index) => (\n <Typography key={`reuse-${index}`} variant=\"body2\" role=\"listitem\">\n Reuses the {entry.kind === 'screen' ? 'page' : entry.kind} {named(entry.id)} — {entry.purpose}\n </Typography>\n ))}\n {plan.create.map((entry, index) => (\n <Typography key={`create-${index}`} variant=\"body2\" role=\"listitem\">\n Creates the {AI_BUILD_PLAN_CREATION_NOUNS[entry.kind].noun} {entry.name}\n {entry.duplicateOf\n ? `, from a copy of ${named(entry.duplicateOf)}`\n : ''}{' '}\n — {entry.why}\n </Typography>\n ))}\n {plan.screens.map((screen, index) => (\n <Typography key={`screen-${index}`} variant=\"body2\" role=\"listitem\">\n {screen.record\n ? `Builds the record template ${screen.title}, a page per record of ${named(screen.record.dataset)} at /${screen.record.base}/…`\n : `Builds the page ${screen.title} at ${screen.slug}`}\n {screen.duplicateOf\n ? `, from a copy of ${named(screen.duplicateOf)}`\n : ''}\n {screen.layout ? ` in ${named(screen.layout)}` : ''}\n {screen.sections.length\n ? `: ${screen.sections.map((section) => section.name).join(', ')}`\n : ''}\n </Typography>\n ))}\n {(plan.embeds ?? []).map((embed, index) => (\n <Typography key={`embed-${index}`} variant=\"body2\" role=\"listitem\">\n {aiPlanEmbedLine(embed, named)}\n </Typography>\n ))}\n </Stack>\n )}\n {/*\n A review written for the member (AGL-3594) carries the checks' own\n sentence as `detail`: its findings are then staff reading too, and the\n member reads the job's one sentence alone.\n */}\n {staff && review?.detail && (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\" sx={{ mt: 0.5 }}>\n {review.detail}\n </Typography>\n )}\n {review?.reason === 'doctrine' && review.findings.length > 0 && (staff || !review.detail) && (\n <Box\n component=\"ul\"\n sx={{ my: 0.5, pl: 2.5 }}\n aria-label=\"Building rules broken\"\n >\n {review.findings.map((finding, index) => (\n <Typography\n key={`finding-${index}`}\n component=\"li\"\n variant=\"caption\"\n >\n {finding.message}\n </Typography>\n ))}\n </Box>\n )}\n {details.length > 0 && (\n <Box sx={{ mt: 0.5 }}>\n <Button\n size=\"small\"\n aria-expanded={detailsOpen}\n onClick={() => setDetailsOpen((prior) => !prior)}\n >\n {detailsOpen ? 'Hide what was refused' : 'Show what was refused'}\n </Button>\n <Collapse in={detailsOpen} unmountOnExit>\n <Typography\n variant=\"caption\"\n component=\"pre\"\n aria-label=\"What was refused\"\n sx={{ m: 0, fontFamily: 'monospace', whiteSpace: 'pre', overflowX: 'auto' }}\n >\n {details.join('\\n')}\n </Typography>\n </Collapse>\n </Box>\n )}\n {estimate > 0 && (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ display: 'block', mt: 1 }}\n >\n {`Estimated cost: about ${estimate.toLocaleString('en-US')} credits. What it costs is what its steps spend.`}\n </Typography>\n )}\n {waiting && review.retryRefusal && review.reason !== 'plan' && (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"div\" sx={{ mt: 1 }}>\n {review.retryRefusal}\n </Typography>\n )}\n {waiting && (\n <Button\n size=\"small\"\n variant=\"contained\"\n // Disabled with its reason above when the Free allowance left\n // cannot pay for another try (AGL-3594).\n disabled={busy || (review.reason !== 'plan' && Boolean(review.retryRefusal))}\n onClick={() => onResume(job)}\n sx={{ mt: 1 }}\n >\n {review.reason === 'plan' ? 'Confirm plan' : 'Try again'}\n </Button>\n )}\n {/* A guided start that did not work out can take the starter instead (AGL-3594). */}\n {waiting && user && aiSiteStarterFallbackOffered(job) && <AiSiteStarterFallback job={job} user={user} />}\n </Box>\n )\n}\n\nexport default AiJobPlan\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","AI_BUILD_PLAN_EMBED_HOST_NAMES","isAiPlanNewRef","aiJobPlanCreditEstimate","aiSiteStarterFallbackOffered","AiSiteStarterFallback","Box","Button","Collapse","Stack","Typography","useState","aiJobReviewDetails","review","reason","lines","findings","flatMap","finding","where","nodeIds","length","join","paths","rule","code","node","outline","push","repeat","depth","id","componentId","Object","entries","grid","map","name","value","JSON","stringify","props","sx","children","aiPlanEmbedLine","embed","named","host","link","url","asked","AiJobPlan","job","onResume","busy","staff","user","plan","detailsOpen","setDetailsOpen","details","ref","slice","labels","waiting","status","estimate","kind","mt","spacing","role","aria-label","variant","color","reuse","entry","index","purpose","create","noun","duplicateOf","why","screens","screen","record","title","dataset","base","slug","layout","sections","section","embeds","detail","component","my","pl","message","size","aria-expanded","onClick","prior","in","unmountOnExit","m","fontFamily","whiteSpace","overflowX","display","toLocaleString","retryRefusal","disabled","Boolean"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,4BAA4B,EAC5BC,8BAA8B,EAC9BC,cAAc,QAET,4BAAwB;AAE/B,SAASC,uBAAuB,QAAQ,0BAAsB;AAC9D,SAASC,4BAA4B,QAAQ,kCAA8B;AAC3E,SAASC,qBAAqB,QAAQ,0CAAsC;AAC5E,SAASC,GAAG,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACxE,SAASC,QAAQ,QAAQ,QAAO;AA6BhC;;;;;CAKC,GACD,OAAO,SAASC,mBAAmBC,MAA0B;QAUxCA;IATnB,IAAIA,CAAAA,0BAAAA,OAAQC,MAAM,MAAK,YAAY,OAAO,EAAE;IAC5C,MAAMC,QAAQF,OAAOG,QAAQ,CAACC,OAAO,CAAC,CAACC;YACvBA,kBAEVA;QAFJ,MAAMC,QAAQD,EAAAA,mBAAAA,QAAQE,OAAO,qBAAfF,iBAAiBG,MAAM,IACjC,CAAC,MAAM,EAAEH,QAAQE,OAAO,CAACE,IAAI,CAAC,OAAO,GACrCJ,EAAAA,iBAAAA,QAAQK,KAAK,qBAAbL,eAAeG,MAAM,IACnB,CAAC,GAAG,EAAEH,QAAQK,KAAK,CAACD,IAAI,CAAC,OAAO,GAChC;QACN,OAAOH,QAAQ;YAAC,GAAGD,QAAQM,IAAI,KAAK,OAAO,KAAK,CAAC,KAAK,EAAEN,QAAQM,IAAI,CAAC,CAAC,CAAC,GAAGN,QAAQO,IAAI,CAAC,EAAE,EAAEN,OAAO;SAAC,GAAG,EAAE;IAC1G;IACA,KAAK,MAAMO,SAAQb,kBAAAA,OAAOc,OAAO,YAAdd,kBAAkB,EAAE,CAAE;YAIjBa;YAEdA;QALRX,MAAMa,IAAI,CACR;YACE,GAAG,KAAKC,MAAM,CAACH,KAAKI,KAAK,IAAIJ,KAAKK,EAAE,CAAC,CAAC,EAAEL,KAAKM,WAAW,EAAE;eACvDC,OAAOC,OAAO,EAACR,aAAAA,KAAKS,IAAI,YAATT,aAAa,CAAC,GAAGU,GAAG,CAAC,CAAC,CAACC,MAAMC,MAAM,GAAK,GAAGD,KAAK,CAAC,EAAEE,KAAKC,SAAS,CAACF,QAAQ;eACxFZ,KAAKe,KAAK,CAACpB,MAAM,GAAG;gBAAC,CAAC,MAAM,EAAEK,KAAKe,KAAK,CAACnB,IAAI,CAAC,OAAO;aAAC,GAAG,EAAE;eAC3DI,EAAAA,WAAAA,KAAKgB,EAAE,qBAAPhB,SAASL,MAAM,IAAG;gBAAC,CAAC,GAAG,EAAEK,KAAKgB,EAAE,CAACpB,IAAI,CAAC,OAAO;aAAC,GAAG,EAAE;eACnDI,KAAKiB,QAAQ,CAACtB,MAAM,GAAG;gBAAC,CAAC,MAAM,EAAEK,KAAKiB,QAAQ,CAACrB,IAAI,CAAC,OAAO;aAAC,GAAG,EAAE;SACtE,CAACA,IAAI,CAAC;IAEX;IACA,OAAOP;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAAS6B,gBACdC,KAAuB,EACvBC,KAA8B;IAE9B,MAAMC,OAAO9C,8BAA8B,CAAC4C,MAAME,IAAI,CAAC;IACvD,MAAM5B,QAAQjB,eAAe2C,MAAM1B,KAAK,IAAI,CAAC,cAAc,EAAE2B,MAAMD,MAAM1B,KAAK,GAAG,GAAG0B,MAAM1B,KAAK;IAC/F,MAAM6B,OAAOH,MAAMI,GAAG,GAAG,CAAC,SAAS,EAAEJ,MAAMI,GAAG,EAAE,GAAG;IACnD,OAAO,CAAC,SAAS,EAAEF,KAAK,WAAW,EAAE5B,QAAQ6B,KAAK,iBAAiB,EAAEH,MAAMK,KAAK,CAAC,aAAa,EAAEH,KAAK,sEAAsE,CAAC;AAC9K;AAEA,OAAO,SAASI,UAAU,EACxBC,GAAG,EACHC,QAAQ,EACRC,OAAO,KAAK,EACZC,QAAQ,KAAK,EACbC,IAAI,EACW;QAuDLC;IAtDV,MAAM,CAACC,aAAaC,eAAe,GAAGhD,SAAS;IAC/C,MAAM,EAAE8C,IAAI,EAAE5C,MAAM,EAAE,GAAGuC;IACzB,IAAI,CAACK,QAAQ,CAAC5C,QAAQ,OAAO;IAC7B,MAAM+C,UAAUL,QAAQ3C,mBAAmBC,UAAU,EAAE;IACvD,mFAAmF,GACnF,MAAMiC,QAAQ,CAACe;;eACb,CAACA,MACG,KACA3D,eAAe2D,OACbA,IAAIC,KAAK,CAAC,OAAOzC,MAAM,YACtBoC,wBAAAA,KAAMM,MAAM,CAACF,IAAI,mBAAIA;;IAC9B,MAAMG,UAAUZ,IAAIa,MAAM,KAAK,kBAAkBpD,WAAW;IAC5D,wEAAwE;IACxE,wEAAwE;IACxE,uEAAuE;IACvE,0EAA0E;IAC1E,6DAA6D;IAC7D,MAAMqD,WACJT,QAAQO,WAAWnD,OAAOC,MAAM,KAAK,SAASX,wBAAwBiD,IAAIe,IAAI,EAAEV,QAAQ;IAC1F,qBACE,MAACnD;QAAIoC,IAAI;YAAE0B,IAAI;QAAE;;YACdX,sBACC,MAAChD;gBAAM4D,SAAS;gBAAKC,MAAK;gBAAOC,cAAW;;kCAC1C,KAAC7D;wBAAW8D,SAAQ;wBAAUC,OAAM;kCACjChB,KAAKQ,MAAM,KAAK,cAAc,mBAAmB;;oBAEnDR,KAAKiB,KAAK,CAACtC,GAAG,CAAC,CAACuC,OAAOC,sBACtB,MAAClE;4BAAkC8D,SAAQ;4BAAQF,MAAK;;gCAAW;gCACrDK,MAAMR,IAAI,KAAK,WAAW,SAASQ,MAAMR,IAAI;gCAAC;gCAAErB,MAAM6B,MAAM5C,EAAE;gCAAE;gCAAI4C,MAAME,OAAO;;2BAD9E,CAAC,MAAM,EAAED,OAAO;oBAIlCnB,KAAKqB,MAAM,CAAC1C,GAAG,CAAC,CAACuC,OAAOC,sBACvB,MAAClE;4BAAmC8D,SAAQ;4BAAQF,MAAK;;gCAAW;gCACrDtE,4BAA4B,CAAC2E,MAAMR,IAAI,CAAC,CAACY,IAAI;gCAAC;gCAAEJ,MAAMtC,IAAI;gCACtEsC,MAAMK,WAAW,GACd,CAAC,iBAAiB,EAAElC,MAAM6B,MAAMK,WAAW,GAAG,GAC9C;gCAAI;gCAAI;gCACTL,MAAMM,GAAG;;2BALG,CAAC,OAAO,EAAEL,OAAO;oBAQnCnB,KAAKyB,OAAO,CAAC9C,GAAG,CAAC,CAAC+C,QAAQP,sBACzB,MAAClE;4BAAmC8D,SAAQ;4BAAQF,MAAK;;gCACtDa,OAAOC,MAAM,GACV,CAAC,2BAA2B,EAAED,OAAOE,KAAK,CAAC,uBAAuB,EAAEvC,MAAMqC,OAAOC,MAAM,CAACE,OAAO,EAAE,KAAK,EAAEH,OAAOC,MAAM,CAACG,IAAI,CAAC,EAAE,CAAC,GAC9H,CAAC,gBAAgB,EAAEJ,OAAOE,KAAK,CAAC,IAAI,EAAEF,OAAOK,IAAI,EAAE;gCACtDL,OAAOH,WAAW,GACf,CAAC,iBAAiB,EAAElC,MAAMqC,OAAOH,WAAW,GAAG,GAC/C;gCACHG,OAAOM,MAAM,GAAG,CAAC,IAAI,EAAE3C,MAAMqC,OAAOM,MAAM,GAAG,GAAG;gCAChDN,OAAOO,QAAQ,CAACrE,MAAM,GACnB,CAAC,EAAE,EAAE8D,OAAOO,QAAQ,CAACtD,GAAG,CAAC,CAACuD,UAAYA,QAAQtD,IAAI,EAAEf,IAAI,CAAC,OAAO,GAChE;;2BAVW,CAAC,OAAO,EAAEsD,OAAO;oBAanC,EAACnB,eAAAA,KAAKmC,MAAM,YAAXnC,eAAe,EAAE,EAAErB,GAAG,CAAC,CAACS,OAAO+B,sBAC/B,KAAClE;4BAAkC8D,SAAQ;4BAAQF,MAAK;sCACrD1B,gBAAgBC,OAAOC;2BADT,CAAC,MAAM,EAAE8B,OAAO;;;YAWtCrB,UAAS1C,0BAAAA,OAAQgF,MAAM,mBACtB,KAACnF;gBAAW8D,SAAQ;gBAAUC,OAAM;gBAAiBqB,WAAU;gBAAMpD,IAAI;oBAAE0B,IAAI;gBAAI;0BAChFvD,OAAOgF,MAAM;;YAGjBhF,CAAAA,0BAAAA,OAAQC,MAAM,MAAK,cAAcD,OAAOG,QAAQ,CAACK,MAAM,GAAG,KAAMkC,CAAAA,SAAS,CAAC1C,OAAOgF,MAAM,AAAD,mBACrF,KAACvF;gBACCwF,WAAU;gBACVpD,IAAI;oBAAEqD,IAAI;oBAAKC,IAAI;gBAAI;gBACvBzB,cAAW;0BAEV1D,OAAOG,QAAQ,CAACoB,GAAG,CAAC,CAAClB,SAAS0D,sBAC7B,KAAClE;wBAECoF,WAAU;wBACVtB,SAAQ;kCAEPtD,QAAQ+E,OAAO;uBAJX,CAAC,QAAQ,EAAErB,OAAO;;YAS9BhB,QAAQvC,MAAM,GAAG,mBAChB,MAACf;gBAAIoC,IAAI;oBAAE0B,IAAI;gBAAI;;kCACjB,KAAC7D;wBACC2F,MAAK;wBACLC,iBAAezC;wBACf0C,SAAS,IAAMzC,eAAe,CAAC0C,QAAU,CAACA;kCAEzC3C,cAAc,0BAA0B;;kCAE3C,KAAClD;wBAAS8F,IAAI5C;wBAAa6C,aAAa;kCACtC,cAAA,KAAC7F;4BACC8D,SAAQ;4BACRsB,WAAU;4BACVvB,cAAW;4BACX7B,IAAI;gCAAE8D,GAAG;gCAAGC,YAAY;gCAAaC,YAAY;gCAAOC,WAAW;4BAAO;sCAEzE/C,QAAQtC,IAAI,CAAC;;;;;YAKrB4C,WAAW,mBACV,KAACxD;gBACC8D,SAAQ;gBACRC,OAAM;gBACN/B,IAAI;oBAAEkE,SAAS;oBAASxC,IAAI;gBAAE;0BAE7B,CAAC,sBAAsB,EAAEF,SAAS2C,cAAc,CAAC,SAAS,gDAAgD,CAAC;;YAG/G7C,WAAWnD,OAAOiG,YAAY,IAAIjG,OAAOC,MAAM,KAAK,wBACnD,KAACJ;gBAAW8D,SAAQ;gBAAUC,OAAM;gBAAiBqB,WAAU;gBAAMpD,IAAI;oBAAE0B,IAAI;gBAAE;0BAC9EvD,OAAOiG,YAAY;;YAGvB9C,yBACC,KAACzD;gBACC2F,MAAK;gBACL1B,SAAQ;gBACR,8DAA8D;gBAC9D,yCAAyC;gBACzCuC,UAAUzD,QAASzC,OAAOC,MAAM,KAAK,UAAUkG,QAAQnG,OAAOiG,YAAY;gBAC1EV,SAAS,IAAM/C,SAASD;gBACxBV,IAAI;oBAAE0B,IAAI;gBAAE;0BAEXvD,OAAOC,MAAM,KAAK,SAAS,iBAAiB;;YAIhDkD,WAAWR,QAAQpD,6BAA6BgD,sBAAQ,KAAC/C;gBAAsB+C,KAAKA;gBAAKI,MAAMA;;;;AAGtG;AAEA,eAAeL,UAAS"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token';
|
|
2
|
+
import type { AiJobSummary } from '../model/ai-jobs.types';
|
|
3
|
+
/**
|
|
4
|
+
* The two decisions a person makes about a job — confirm its plan (or try a
|
|
5
|
+
* refused step again), and cancel it — as ONE request each, whichever
|
|
6
|
+
* surface they press it on (AGL-3593). The AI jobs drawer and the dialog that
|
|
7
|
+
* started the job both call these, so confirming in the dialog is the same
|
|
8
|
+
* door, the same body and the same answer as confirming in the drawer.
|
|
9
|
+
*
|
|
10
|
+
* Each hands the job it gets back to every counting surface, so the top-bar
|
|
11
|
+
* indicator stops saying "plan ready" the moment the plan is confirmed.
|
|
12
|
+
*/
|
|
13
|
+
/** A door's answer: the job as it now stands, and the refusal when there is one. */
|
|
14
|
+
export interface AiJobDecision {
|
|
15
|
+
job: AiJobSummary | null;
|
|
16
|
+
error: string | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Confirms a plan, or tries again a step whose answer broke a building rule
|
|
20
|
+
* (AGL-2935). The door runs the next step inline and answers with the job.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resumeAiJobRequest(user: MaybeTokenSource, orgId: string, job: Pick<AiJobSummary, 'id' | 'hostId'>): Promise<AiJobDecision>;
|
|
23
|
+
/** Cancels a job that has not settled. */
|
|
24
|
+
export declare function cancelAiJobRequest(user: MaybeTokenSource, orgId: string, jobId: string): Promise<AiJobDecision>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
18
|
+
import { publishAiJob } from "./ai-jobs-store.js";
|
|
19
|
+
/**
|
|
20
|
+
* Confirms a plan, or tries again a step whose answer broke a building rule
|
|
21
|
+
* (AGL-2935). The door runs the next step inline and answers with the job.
|
|
22
|
+
*/ export async function resumeAiJobRequest(user, orgId, job) {
|
|
23
|
+
try {
|
|
24
|
+
var _ref, _ref1;
|
|
25
|
+
const response = await authorizedFetch(user, `/api/ai/jobs/${encodeURIComponent(job.id)}/resume`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json'
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
orgId,
|
|
32
|
+
hostId: job.hostId
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
const payload = await response.json().catch(()=>null);
|
|
36
|
+
const next = (_ref = payload == null ? void 0 : payload.job) != null ? _ref : null;
|
|
37
|
+
publishAiJob(next);
|
|
38
|
+
return {
|
|
39
|
+
job: next,
|
|
40
|
+
error: response.ok ? null : String((_ref1 = payload == null ? void 0 : payload.error) != null ? _ref1 : 'The job could not be resumed — try again.')
|
|
41
|
+
};
|
|
42
|
+
} catch (unused) {
|
|
43
|
+
return {
|
|
44
|
+
job: null,
|
|
45
|
+
error: 'The job could not be resumed — try again.'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Cancels a job that has not settled. */ export async function cancelAiJobRequest(user, orgId, jobId) {
|
|
50
|
+
try {
|
|
51
|
+
var _ref, _ref1;
|
|
52
|
+
const response = await authorizedFetch(user, `/api/ai/jobs/${encodeURIComponent(jobId)}/cancel`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json'
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
orgId
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
const payload = await response.json().catch(()=>null);
|
|
62
|
+
const next = (_ref = payload == null ? void 0 : payload.job) != null ? _ref : null;
|
|
63
|
+
publishAiJob(next);
|
|
64
|
+
return {
|
|
65
|
+
job: next,
|
|
66
|
+
error: response.ok ? null : String((_ref1 = payload == null ? void 0 : payload.error) != null ? _ref1 : 'The job could not be canceled — try again.')
|
|
67
|
+
};
|
|
68
|
+
} catch (unused) {
|
|
69
|
+
return {
|
|
70
|
+
job: null,
|
|
71
|
+
error: 'The job could not be canceled — try again.'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=ai-job-requests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-job-requests.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 { authorizedFetch, type MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token'\nimport type { AiJobSummary } from '../model/ai-jobs.types'\nimport { publishAiJob } from './ai-jobs-store'\n\n/**\n * The two decisions a person makes about a job — confirm its plan (or try a\n * refused step again), and cancel it — as ONE request each, whichever\n * surface they press it on (AGL-3593). The AI jobs drawer and the dialog that\n * started the job both call these, so confirming in the dialog is the same\n * door, the same body and the same answer as confirming in the drawer.\n *\n * Each hands the job it gets back to every counting surface, so the top-bar\n * indicator stops saying \"plan ready\" the moment the plan is confirmed.\n */\n\n/** A door's answer: the job as it now stands, and the refusal when there is one. */\nexport interface AiJobDecision {\n job: AiJobSummary | null\n error: string | null\n}\n\n/**\n * Confirms a plan, or tries again a step whose answer broke a building rule\n * (AGL-2935). The door runs the next step inline and answers with the job.\n */\nexport async function resumeAiJobRequest(\n user: MaybeTokenSource,\n orgId: string,\n job: Pick<AiJobSummary, 'id' | 'hostId'>,\n): Promise<AiJobDecision> {\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 const next = (payload?.job as AiJobSummary | undefined) ?? null\n publishAiJob(next)\n return {\n job: next,\n error: response.ok\n ? null\n : String(payload?.error ?? 'The job could not be resumed — try again.'),\n }\n } catch {\n return { job: null, error: 'The job could not be resumed — try again.' }\n }\n}\n\n/** Cancels a job that has not settled. */\nexport async function cancelAiJobRequest(\n user: MaybeTokenSource,\n orgId: string,\n jobId: string,\n): Promise<AiJobDecision> {\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 const next = (payload?.job as AiJobSummary | undefined) ?? null\n publishAiJob(next)\n return {\n job: next,\n error: response.ok\n ? null\n : String(payload?.error ?? 'The job could not be canceled — try again.'),\n }\n } catch {\n return { job: null, error: 'The job could not be canceled — try again.' }\n }\n}\n"],"names":["authorizedFetch","publishAiJob","resumeAiJobRequest","user","orgId","job","response","encodeURIComponent","id","method","headers","body","JSON","stringify","hostId","payload","json","catch","next","error","ok","String","cancelAiJobRequest","jobId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,eAAe,QAA+B,2CAA0C;AAEjG,SAASC,YAAY,QAAQ,qBAAiB;AAmB9C;;;CAGC,GACD,OAAO,eAAeC,mBACpBC,IAAsB,EACtBC,KAAa,EACbC,GAAwC;IAExC,IAAI;;QACF,MAAMC,WAAW,MAAMN,gBACrBG,MACA,CAAC,aAAa,EAAEI,mBAAmBF,IAAIG,EAAE,EAAE,OAAO,CAAC,EACnD;YACEC,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CC,MAAMC,KAAKC,SAAS,CAAC;gBAAET;gBAAOU,QAAQT,IAAIS,MAAM;YAAC;QACnD;QAEF,MAAMC,UAAU,MAAMT,SAASU,IAAI,GAAGC,KAAK,CAAC,IAAM;QAClD,MAAMC,eAAQH,2BAAAA,QAASV,GAAG,mBAAiC;QAC3DJ,aAAaiB;QACb,OAAO;YACLb,KAAKa;YACLC,OAAOb,SAASc,EAAE,GACd,OACAC,gBAAON,2BAAAA,QAASI,KAAK,oBAAI;QAC/B;IACF,EAAE,eAAM;QACN,OAAO;YAAEd,KAAK;YAAMc,OAAO;QAA4C;IACzE;AACF;AAEA,wCAAwC,GACxC,OAAO,eAAeG,mBACpBnB,IAAsB,EACtBC,KAAa,EACbmB,KAAa;IAEb,IAAI;;QACF,MAAMjB,WAAW,MAAMN,gBACrBG,MACA,CAAC,aAAa,EAAEI,mBAAmBgB,OAAO,OAAO,CAAC,EAClD;YACEd,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CC,MAAMC,KAAKC,SAAS,CAAC;gBAAET;YAAM;QAC/B;QAEF,MAAMW,UAAU,MAAMT,SAASU,IAAI,GAAGC,KAAK,CAAC,IAAM;QAClD,MAAMC,eAAQH,2BAAAA,QAASV,GAAG,mBAAiC;QAC3DJ,aAAaiB;QACb,OAAO;YACLb,KAAKa;YACLC,OAAOb,SAASc,EAAE,GACd,OACAC,gBAAON,2BAAAA,QAASI,KAAK,oBAAI;QAC/B;IACF,EAAE,eAAM;QACN,OAAO;YAAEd,KAAK;YAAMc,OAAO;QAA6C;IAC1E;AACF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ConsoleTopBarZoneProps } from '@aglyn/aglyn';
|
|
2
|
+
/**
|
|
3
|
+
* The AI jobs indicator in the console's top bar (AGL-3593), on the
|
|
4
|
+
* `consoleTopBar` zone: one small chip while any of the workspace's jobs is
|
|
5
|
+
* moving or waiting on a person, saying which — `AI · plan ready` before
|
|
6
|
+
* `AI · needs attention` before `AI · planning` — and how many. Pressing it
|
|
7
|
+
* opens AI jobs in the Assist panel on the job that has waited longest.
|
|
8
|
+
*
|
|
9
|
+
* Nothing at all when nothing is in flight, so a workspace with no AI work
|
|
10
|
+
* sees the bar it always had. It reads the shared list the launcher's badge
|
|
11
|
+
* and the drawer read ({@link useAiJobsActivity}): one read per page load,
|
|
12
|
+
* and a re-read only while something is moving.
|
|
13
|
+
*
|
|
14
|
+
* Gated as the plugin's other generative widgets are — the plan's
|
|
15
|
+
* `aiGenerative` and the reader's `ai.generate` at registration — and, here,
|
|
16
|
+
* on the two release flags the drawer it opens needs: `release_ai_generative`
|
|
17
|
+
* for the jobs, `release_assist` for the panel they are listed in. The
|
|
18
|
+
* reader's `ai.generate` on the site in view is asked again, because on a
|
|
19
|
+
* site's pages a collaborator's permission is the site's, not the org's.
|
|
20
|
+
*/
|
|
21
|
+
export declare function AiJobsTopBarIndicator({ scopedOrgId, org, orgReady, releaseVerdict, permissionsOnHost, productName, }: ConsoleTopBarZoneProps): import("react").JSX.Element;
|
|
22
|
+
export declare namespace AiJobsTopBarIndicator {
|
|
23
|
+
var displayName: string;
|
|
24
|
+
}
|
|
25
|
+
export default AiJobsTopBarIndicator;
|
|
@@ -0,0 +1,80 @@
|
|
|
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 { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
+
import { checkEntitlement } from "@aglyn/aglyn";
|
|
19
|
+
import { mdiAutoFix } from "@aglyn/shared-data-mdi";
|
|
20
|
+
import { MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
21
|
+
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
22
|
+
import { Chip, CircularProgress, Tooltip } from "@mui/material";
|
|
23
|
+
import { aiJobsActivityLabel } from "../model/ai-job-activity.js";
|
|
24
|
+
import { aiPermissionsOf } from "../model/ai-permissions.js";
|
|
25
|
+
import { openAiJobs, useAiJobsActivity } from "./ai-jobs-store.js";
|
|
26
|
+
/**
|
|
27
|
+
* The AI jobs indicator in the console's top bar (AGL-3593), on the
|
|
28
|
+
* `consoleTopBar` zone: one small chip while any of the workspace's jobs is
|
|
29
|
+
* moving or waiting on a person, saying which — `AI · plan ready` before
|
|
30
|
+
* `AI · needs attention` before `AI · planning` — and how many. Pressing it
|
|
31
|
+
* opens AI jobs in the Assist panel on the job that has waited longest.
|
|
32
|
+
*
|
|
33
|
+
* Nothing at all when nothing is in flight, so a workspace with no AI work
|
|
34
|
+
* sees the bar it always had. It reads the shared list the launcher's badge
|
|
35
|
+
* and the drawer read ({@link useAiJobsActivity}): one read per page load,
|
|
36
|
+
* and a re-read only while something is moving.
|
|
37
|
+
*
|
|
38
|
+
* Gated as the plugin's other generative widgets are — the plan's
|
|
39
|
+
* `aiGenerative` and the reader's `ai.generate` at registration — and, here,
|
|
40
|
+
* on the two release flags the drawer it opens needs: `release_ai_generative`
|
|
41
|
+
* for the jobs, `release_assist` for the panel they are listed in. The
|
|
42
|
+
* reader's `ai.generate` on the site in view is asked again, because on a
|
|
43
|
+
* site's pages a collaborator's permission is the site's, not the org's.
|
|
44
|
+
*/ export function AiJobsTopBarIndicator({ scopedOrgId, org, orgReady, releaseVerdict, permissionsOnHost, productName }) {
|
|
45
|
+
const { data: user } = useUser();
|
|
46
|
+
const generate = (permissionsOnHost == null ? void 0 : permissionsOnHost.loaded) === true && aiPermissionsOf(permissionsOnHost.granted)['ai.generate'] === true;
|
|
47
|
+
const visible = Boolean(scopedOrgId) && orgReady && checkEntitlement(org, 'aiGenerative') && releaseVerdict('release_ai_generative').visible && releaseVerdict('release_assist').visible && generate;
|
|
48
|
+
const activity = useAiJobsActivity(user, scopedOrgId, visible);
|
|
49
|
+
if (!visible || !activity) return null;
|
|
50
|
+
const waiting = activity.state !== 'running';
|
|
51
|
+
return /*#__PURE__*/ _jsx(Tooltip, {
|
|
52
|
+
title: `Open AI jobs in ${productName} Assist`,
|
|
53
|
+
children: /*#__PURE__*/ _jsx(Chip, {
|
|
54
|
+
size: "small",
|
|
55
|
+
clickable: true,
|
|
56
|
+
color: waiting ? 'warning' : 'primary',
|
|
57
|
+
variant: waiting ? 'filled' : 'outlined',
|
|
58
|
+
icon: waiting ? /*#__PURE__*/ _jsx(MdiIcon, {
|
|
59
|
+
path: mdiAutoFix.path
|
|
60
|
+
}) : /*#__PURE__*/ _jsx(CircularProgress, {
|
|
61
|
+
size: 12,
|
|
62
|
+
color: "inherit",
|
|
63
|
+
"aria-hidden": true
|
|
64
|
+
}),
|
|
65
|
+
label: aiJobsActivityLabel(activity),
|
|
66
|
+
"aria-label": `${aiJobsActivityLabel(activity)}. Open AI jobs`,
|
|
67
|
+
onClick: ()=>openAiJobs({
|
|
68
|
+
jobId: activity.job.id
|
|
69
|
+
}),
|
|
70
|
+
"data-ai-jobs-state": activity.state,
|
|
71
|
+
sx: {
|
|
72
|
+
mx: 0.5
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
AiJobsTopBarIndicator.displayName = 'AiJobsTopBarIndicator';
|
|
78
|
+
export default AiJobsTopBarIndicator;
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=ai-jobs-indicator.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-jobs-indicator.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { checkEntitlement, type ConsoleTopBarZoneProps } from '@aglyn/aglyn'\nimport { mdiAutoFix } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { Chip, CircularProgress, Tooltip } from '@mui/material'\nimport { aiJobsActivityLabel } from '../model/ai-job-activity'\nimport { aiPermissionsOf } from '../model/ai-permissions'\nimport { openAiJobs, useAiJobsActivity } from './ai-jobs-store'\n\n/**\n * The AI jobs indicator in the console's top bar (AGL-3593), on the\n * `consoleTopBar` zone: one small chip while any of the workspace's jobs is\n * moving or waiting on a person, saying which — `AI · plan ready` before\n * `AI · needs attention` before `AI · planning` — and how many. Pressing it\n * opens AI jobs in the Assist panel on the job that has waited longest.\n *\n * Nothing at all when nothing is in flight, so a workspace with no AI work\n * sees the bar it always had. It reads the shared list the launcher's badge\n * and the drawer read ({@link useAiJobsActivity}): one read per page load,\n * and a re-read only while something is moving.\n *\n * Gated as the plugin's other generative widgets are — the plan's\n * `aiGenerative` and the reader's `ai.generate` at registration — and, here,\n * on the two release flags the drawer it opens needs: `release_ai_generative`\n * for the jobs, `release_assist` for the panel they are listed in. The\n * reader's `ai.generate` on the site in view is asked again, because on a\n * site's pages a collaborator's permission is the site's, not the org's.\n */\nexport function AiJobsTopBarIndicator({\n scopedOrgId,\n org,\n orgReady,\n releaseVerdict,\n permissionsOnHost,\n productName,\n}: ConsoleTopBarZoneProps) {\n const { data: user } = useUser()\n const generate =\n permissionsOnHost?.loaded === true &&\n aiPermissionsOf(permissionsOnHost.granted)['ai.generate'] === true\n const visible =\n Boolean(scopedOrgId) &&\n orgReady &&\n checkEntitlement(org as never, 'aiGenerative') &&\n releaseVerdict('release_ai_generative').visible &&\n releaseVerdict('release_assist').visible &&\n generate\n const activity = useAiJobsActivity(user, scopedOrgId, visible)\n if (!visible || !activity) return null\n\n const waiting = activity.state !== 'running'\n return (\n <Tooltip title={`Open AI jobs in ${productName} Assist`}>\n <Chip\n size=\"small\"\n clickable\n color={waiting ? 'warning' : 'primary'}\n variant={waiting ? 'filled' : 'outlined'}\n icon={\n waiting ? (\n <MdiIcon path={mdiAutoFix.path} />\n ) : (\n <CircularProgress size={12} color=\"inherit\" aria-hidden />\n )\n }\n label={aiJobsActivityLabel(activity)}\n aria-label={`${aiJobsActivityLabel(activity)}. Open AI jobs`}\n onClick={() => openAiJobs({ jobId: activity.job.id })}\n data-ai-jobs-state={activity.state}\n sx={{ mx: 0.5 }}\n />\n </Tooltip>\n )\n}\nAiJobsTopBarIndicator.displayName = 'AiJobsTopBarIndicator'\n\nexport default AiJobsTopBarIndicator\n"],"names":["checkEntitlement","mdiAutoFix","MdiIcon","useUser","Chip","CircularProgress","Tooltip","aiJobsActivityLabel","aiPermissionsOf","openAiJobs","useAiJobsActivity","AiJobsTopBarIndicator","scopedOrgId","org","orgReady","releaseVerdict","permissionsOnHost","productName","data","user","generate","loaded","granted","visible","Boolean","activity","waiting","state","title","size","clickable","color","variant","icon","path","aria-hidden","label","aria-label","onClick","jobId","job","id","data-ai-jobs-state","sx","mx","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,gBAAgB,QAAqC,eAAc;AAC5E,SAASC,UAAU,QAAQ,yBAAwB;AACnD,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,OAAO,QAAQ,iCAAgC;AACxD,SAASC,IAAI,EAAEC,gBAAgB,EAAEC,OAAO,QAAQ,gBAAe;AAC/D,SAASC,mBAAmB,QAAQ,8BAA0B;AAC9D,SAASC,eAAe,QAAQ,6BAAyB;AACzD,SAASC,UAAU,EAAEC,iBAAiB,QAAQ,qBAAiB;AAE/D;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,sBAAsB,EACpCC,WAAW,EACXC,GAAG,EACHC,QAAQ,EACRC,cAAc,EACdC,iBAAiB,EACjBC,WAAW,EACY;IACvB,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGhB;IACvB,MAAMiB,WACJJ,CAAAA,qCAAAA,kBAAmBK,MAAM,MAAK,QAC9Bb,gBAAgBQ,kBAAkBM,OAAO,CAAC,CAAC,cAAc,KAAK;IAChE,MAAMC,UACJC,QAAQZ,gBACRE,YACAd,iBAAiBa,KAAc,mBAC/BE,eAAe,yBAAyBQ,OAAO,IAC/CR,eAAe,kBAAkBQ,OAAO,IACxCH;IACF,MAAMK,WAAWf,kBAAkBS,MAAMP,aAAaW;IACtD,IAAI,CAACA,WAAW,CAACE,UAAU,OAAO;IAElC,MAAMC,UAAUD,SAASE,KAAK,KAAK;IACnC,qBACE,KAACrB;QAAQsB,OAAO,CAAC,gBAAgB,EAAEX,YAAY,OAAO,CAAC;kBACrD,cAAA,KAACb;YACCyB,MAAK;YACLC,SAAS;YACTC,OAAOL,UAAU,YAAY;YAC7BM,SAASN,UAAU,WAAW;YAC9BO,MACEP,wBACE,KAACxB;gBAAQgC,MAAMjC,WAAWiC,IAAI;+BAE9B,KAAC7B;gBAAiBwB,MAAM;gBAAIE,OAAM;gBAAUI,aAAW;;YAG3DC,OAAO7B,oBAAoBkB;YAC3BY,cAAY,GAAG9B,oBAAoBkB,UAAU,cAAc,CAAC;YAC5Da,SAAS,IAAM7B,WAAW;oBAAE8B,OAAOd,SAASe,GAAG,CAACC,EAAE;gBAAC;YACnDC,sBAAoBjB,SAASE,KAAK;YAClCgB,IAAI;gBAAEC,IAAI;YAAI;;;AAItB;AACAjC,sBAAsBkC,WAAW,GAAG;AAEpC,eAAelC,sBAAqB"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type TokenSource } from '@aglyn/shared-util-http/authorized-token';
|
|
2
|
+
import { type AiJobsActivity } from '../model/ai-job-activity';
|
|
3
|
+
import { type AiJobSummary } from '../model/ai-jobs.types';
|
|
4
|
+
/**
|
|
5
|
+
* WHERE A PERSON FINDS THEIR AI JOBS (AGL-3593).
|
|
6
|
+
*
|
|
7
|
+
* Two things every AI surface in the console shares, kept in this module
|
|
8
|
+
* rather than in any one component because the surfaces that need them — a
|
|
9
|
+
* brief dialog on the Screens page, the guided start's full-screen dialog,
|
|
10
|
+
* the top-bar indicator, the Assist launcher and the drawer inside the
|
|
11
|
+
* panel — are mounted in different zones and know nothing of one another.
|
|
12
|
+
* All of them are this plugin's, so no shell, zone or event bus carries it.
|
|
13
|
+
*
|
|
14
|
+
* ── Opening AI jobs ──────────────────────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* {@link openAiJobs} is the one action every "Open AI jobs" button, the
|
|
17
|
+
* indicator and a notification's link take: the Assist panel subscribes, opens
|
|
18
|
+
* itself, expands its AI jobs drawer, and scrolls to and highlights the job
|
|
19
|
+
* named. A request is a sequence number as well as a job, so asking twice for
|
|
20
|
+
* the same job opens it twice.
|
|
21
|
+
*
|
|
22
|
+
* ── The workspace's unsettled jobs ───────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* The indicator, the launcher's badge and the drawer's open-by-default rule
|
|
25
|
+
* read the same list: the workspace's jobs that are not yet settled, from
|
|
26
|
+
* the jobs route's `status=active` (a Firestore `in` query under the
|
|
27
|
+
* (status, createdAt) index, never a filter of a loaded window). ONE list per
|
|
28
|
+
* person and workspace however many surfaces read it, re-read on a timer only
|
|
29
|
+
* while something in it is still moving or waiting and the tab is in view —
|
|
30
|
+
* a workspace with nothing running costs one read per page load and no more.
|
|
31
|
+
*
|
|
32
|
+
* Every surface that learns a job's state some other way — the dialog that
|
|
33
|
+
* created it, the drawer's event stream, the in-dialog confirm — hands it in
|
|
34
|
+
* through {@link publishAiJob}, so the indicator moves the moment the job
|
|
35
|
+
* does rather than on the next read.
|
|
36
|
+
*/
|
|
37
|
+
/** The latest request to open AI jobs; `seq` is 0 until anything asked. */
|
|
38
|
+
export interface AiJobsOpenRequest {
|
|
39
|
+
seq: number;
|
|
40
|
+
/** The job to scroll to and highlight, or `null` for the drawer alone. */
|
|
41
|
+
jobId: string | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Opens the Assist panel on its AI jobs drawer, expanded, scrolled to
|
|
45
|
+
* `jobId` when one is named. A surface that covers the panel — a dialog —
|
|
46
|
+
* closes itself beside the call; this opens, it does not close anything.
|
|
47
|
+
*/
|
|
48
|
+
export declare function openAiJobs(options?: {
|
|
49
|
+
jobId?: string | null;
|
|
50
|
+
}): void;
|
|
51
|
+
/** The latest open request, for the panel; a new `seq` is a new request. */
|
|
52
|
+
export declare function useAiJobsOpenRequest(): AiJobsOpenRequest;
|
|
53
|
+
/**
|
|
54
|
+
* The query parameter a notification's link carries: a console page opened
|
|
55
|
+
* with `?aiJob={id}` opens AI jobs on that job, the way pressing the
|
|
56
|
+
* indicator does.
|
|
57
|
+
*/
|
|
58
|
+
export { AI_JOB_LINK_PARAM } from '../model/ai-job-notice';
|
|
59
|
+
/** How often a list with something in it is read again, while the tab is in view. */
|
|
60
|
+
export declare const AI_JOBS_ACTIVE_POLL_MS = 15000;
|
|
61
|
+
/**
|
|
62
|
+
* Hands a job's latest state to every surface counting this workspace's jobs:
|
|
63
|
+
* a new job joins the list, a settled one leaves it. Keyed on the job's own
|
|
64
|
+
* workspace, for every person whose list holds that workspace in this tab.
|
|
65
|
+
*/
|
|
66
|
+
export declare function publishAiJob(job: AiJobSummary | null | undefined): void;
|
|
67
|
+
/** Forget every list and request; specs start each case from nothing. */
|
|
68
|
+
export declare function resetAiJobsStoreForTests(): void;
|
|
69
|
+
/**
|
|
70
|
+
* The workspace's jobs that are not yet settled, newest first, for the
|
|
71
|
+
* signed-in person — empty while the first read is in flight, where the
|
|
72
|
+
* route refuses, and when `enabled` is false (a surface whose own gates have
|
|
73
|
+
* not passed reads nothing at all).
|
|
74
|
+
*/
|
|
75
|
+
export declare function useAiJobsInFlight(user: (TokenSource & {
|
|
76
|
+
uid?: string | null;
|
|
77
|
+
}) | null | undefined, orgId: string | null | undefined, enabled?: boolean): readonly AiJobSummary[];
|
|
78
|
+
/** What the indicator and the launcher show for the workspace, or `null` for nothing. */
|
|
79
|
+
export declare function useAiJobsActivity(user: (TokenSource & {
|
|
80
|
+
uid?: string | null;
|
|
81
|
+
}) | null | undefined, orgId: string | null | undefined, enabled?: boolean): AiJobsActivity<AiJobSummary> | null;
|
|
@@ -0,0 +1,218 @@
|
|
|
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 { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
19
|
+
import { aiJobsActivity } from "../model/ai-job-activity.js";
|
|
20
|
+
import { AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
|
|
21
|
+
let openRequest = {
|
|
22
|
+
seq: 0,
|
|
23
|
+
jobId: null
|
|
24
|
+
};
|
|
25
|
+
const openListeners = new Set();
|
|
26
|
+
/**
|
|
27
|
+
* Opens the Assist panel on its AI jobs drawer, expanded, scrolled to
|
|
28
|
+
* `jobId` when one is named. A surface that covers the panel — a dialog —
|
|
29
|
+
* closes itself beside the call; this opens, it does not close anything.
|
|
30
|
+
*/ export function openAiJobs(options = {}) {
|
|
31
|
+
var _options_jobId;
|
|
32
|
+
openRequest = {
|
|
33
|
+
seq: openRequest.seq + 1,
|
|
34
|
+
jobId: (_options_jobId = options.jobId) != null ? _options_jobId : null
|
|
35
|
+
};
|
|
36
|
+
for (const listener of openListeners)listener();
|
|
37
|
+
}
|
|
38
|
+
function subscribeOpen(listener) {
|
|
39
|
+
openListeners.add(listener);
|
|
40
|
+
return ()=>{
|
|
41
|
+
openListeners.delete(listener);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const readOpen = ()=>openRequest;
|
|
45
|
+
/** The latest open request, for the panel; a new `seq` is a new request. */ export function useAiJobsOpenRequest() {
|
|
46
|
+
return useSyncExternalStore(subscribeOpen, readOpen, readOpen);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The query parameter a notification's link carries: a console page opened
|
|
50
|
+
* with `?aiJob={id}` opens AI jobs on that job, the way pressing the
|
|
51
|
+
* indicator does.
|
|
52
|
+
*/ export { AI_JOB_LINK_PARAM } from "../model/ai-job-notice.js";
|
|
53
|
+
// ── The workspace's unsettled jobs ────────────────────────────────────────
|
|
54
|
+
/** How often a list with something in it is read again, while the tab is in view. */ export const AI_JOBS_ACTIVE_POLL_MS = 15000;
|
|
55
|
+
/** The most unsettled jobs one read lists; a glance, not an archive. */ const ACTIVE_LIMIT = 20;
|
|
56
|
+
const NONE = Object.freeze([]);
|
|
57
|
+
const entries = new Map();
|
|
58
|
+
const keyOf = (uid, orgId)=>`${uid}\n${orgId}`;
|
|
59
|
+
function emit(entry) {
|
|
60
|
+
for (const listener of entry.listeners)listener();
|
|
61
|
+
}
|
|
62
|
+
function isSettled(job) {
|
|
63
|
+
return AI_JOB_TERMINAL_STATUSES.includes(job.status);
|
|
64
|
+
}
|
|
65
|
+
function schedule(entry) {
|
|
66
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
67
|
+
entry.timer = null;
|
|
68
|
+
if (entry.closed || entry.subscribers === 0 || entry.jobs.length === 0) return;
|
|
69
|
+
entry.timer = setTimeout(()=>{
|
|
70
|
+
entry.timer = null;
|
|
71
|
+
const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden';
|
|
72
|
+
// A hidden tab reads nothing; coming back into view reads at once.
|
|
73
|
+
if (hidden) return;
|
|
74
|
+
void read(entry);
|
|
75
|
+
}, AI_JOBS_ACTIVE_POLL_MS);
|
|
76
|
+
}
|
|
77
|
+
async function read(entry) {
|
|
78
|
+
if (entry.inflight || entry.closed) return;
|
|
79
|
+
entry.inflight = true;
|
|
80
|
+
try {
|
|
81
|
+
var _ref;
|
|
82
|
+
const response = await authorizedFetch(entry.user, `/api/ai/jobs?orgId=${encodeURIComponent(entry.orgId)}&status=active&limit=${ACTIVE_LIMIT}`);
|
|
83
|
+
if (response.status === 404 || response.status === 403) {
|
|
84
|
+
// The release flag off, or a plan or member without generation: there
|
|
85
|
+
// is nothing to count, now or on the next tick.
|
|
86
|
+
entry.closed = true;
|
|
87
|
+
entry.jobs = NONE;
|
|
88
|
+
emit(entry);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!response.ok) return;
|
|
92
|
+
const payload = await response.json().catch(()=>null);
|
|
93
|
+
entry.jobs = Object.freeze(((_ref = payload == null ? void 0 : payload.jobs) != null ? _ref : []).filter((job)=>!isSettled(job)));
|
|
94
|
+
emit(entry);
|
|
95
|
+
} catch (unused) {
|
|
96
|
+
// An unreachable route keeps what was last read; the next tick asks again.
|
|
97
|
+
} finally{
|
|
98
|
+
entry.inflight = false;
|
|
99
|
+
schedule(entry);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function entryFor(uid, orgId, user) {
|
|
103
|
+
const key = keyOf(uid, orgId);
|
|
104
|
+
let entry = entries.get(key);
|
|
105
|
+
if (!entry) {
|
|
106
|
+
entry = {
|
|
107
|
+
jobs: NONE,
|
|
108
|
+
user,
|
|
109
|
+
orgId,
|
|
110
|
+
subscribers: 0,
|
|
111
|
+
inflight: false,
|
|
112
|
+
timer: null,
|
|
113
|
+
closed: false,
|
|
114
|
+
listeners: new Set()
|
|
115
|
+
};
|
|
116
|
+
entries.set(key, entry);
|
|
117
|
+
}
|
|
118
|
+
return entry;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Hands a job's latest state to every surface counting this workspace's jobs:
|
|
122
|
+
* a new job joins the list, a settled one leaves it. Keyed on the job's own
|
|
123
|
+
* workspace, for every person whose list holds that workspace in this tab.
|
|
124
|
+
*/ export function publishAiJob(job) {
|
|
125
|
+
if (!(job == null ? void 0 : job.orgId)) return;
|
|
126
|
+
for (const entry of entries.values()){
|
|
127
|
+
if (entry.orgId !== job.orgId || entry.closed) continue;
|
|
128
|
+
const index = entry.jobs.findIndex((known)=>known.id === job.id);
|
|
129
|
+
let next;
|
|
130
|
+
if (isSettled(job)) {
|
|
131
|
+
if (index === -1) continue;
|
|
132
|
+
next = entry.jobs.filter((known)=>known.id !== job.id);
|
|
133
|
+
} else if (index === -1) {
|
|
134
|
+
next = [
|
|
135
|
+
job,
|
|
136
|
+
...entry.jobs
|
|
137
|
+
];
|
|
138
|
+
} else {
|
|
139
|
+
next = [
|
|
140
|
+
...entry.jobs
|
|
141
|
+
];
|
|
142
|
+
next[index] = job;
|
|
143
|
+
}
|
|
144
|
+
entry.jobs = Object.freeze(next);
|
|
145
|
+
emit(entry);
|
|
146
|
+
// A list that just gained its first job starts its timer.
|
|
147
|
+
if (!entry.timer && !entry.inflight) schedule(entry);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Forget every list and request; specs start each case from nothing. */ export function resetAiJobsStoreForTests() {
|
|
151
|
+
for (const entry of entries.values()){
|
|
152
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
153
|
+
}
|
|
154
|
+
entries.clear();
|
|
155
|
+
openListeners.clear();
|
|
156
|
+
openRequest = {
|
|
157
|
+
seq: 0,
|
|
158
|
+
jobId: null
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The workspace's jobs that are not yet settled, newest first, for the
|
|
163
|
+
* signed-in person — empty while the first read is in flight, where the
|
|
164
|
+
* route refuses, and when `enabled` is false (a surface whose own gates have
|
|
165
|
+
* not passed reads nothing at all).
|
|
166
|
+
*/ export function useAiJobsInFlight(user, orgId, enabled = true) {
|
|
167
|
+
var _ref;
|
|
168
|
+
const uid = (_ref = user == null ? void 0 : user.uid) != null ? _ref : null;
|
|
169
|
+
const userRef = useRef(user);
|
|
170
|
+
userRef.current = user;
|
|
171
|
+
const entry = useMemo(()=>enabled && uid && orgId ? entryFor(uid, orgId, userRef.current) : null, [
|
|
172
|
+
enabled,
|
|
173
|
+
uid,
|
|
174
|
+
orgId
|
|
175
|
+
]);
|
|
176
|
+
useEffect(()=>{
|
|
177
|
+
if (!entry) return undefined;
|
|
178
|
+
entry.user = userRef.current;
|
|
179
|
+
entry.subscribers += 1;
|
|
180
|
+
if (entry.subscribers === 1) void read(entry);
|
|
181
|
+
const onVisible = ()=>{
|
|
182
|
+
if (document.visibilityState === 'visible' && entry.jobs.length > 0) void read(entry);
|
|
183
|
+
};
|
|
184
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
185
|
+
return ()=>{
|
|
186
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
187
|
+
entry.subscribers -= 1;
|
|
188
|
+
if (entry.subscribers === 0 && entry.timer) {
|
|
189
|
+
clearTimeout(entry.timer);
|
|
190
|
+
entry.timer = null;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}, [
|
|
194
|
+
entry
|
|
195
|
+
]);
|
|
196
|
+
const subscribe = useMemo(()=>(listener)=>{
|
|
197
|
+
if (!entry) return ()=>undefined;
|
|
198
|
+
entry.listeners.add(listener);
|
|
199
|
+
return ()=>{
|
|
200
|
+
entry.listeners.delete(listener);
|
|
201
|
+
};
|
|
202
|
+
}, [
|
|
203
|
+
entry
|
|
204
|
+
]);
|
|
205
|
+
const snapshot = ()=>{
|
|
206
|
+
var _ref;
|
|
207
|
+
return (_ref = entry == null ? void 0 : entry.jobs) != null ? _ref : NONE;
|
|
208
|
+
};
|
|
209
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
210
|
+
}
|
|
211
|
+
/** What the indicator and the launcher show for the workspace, or `null` for nothing. */ export function useAiJobsActivity(user, orgId, enabled = true) {
|
|
212
|
+
const jobs = useAiJobsInFlight(user, orgId, enabled);
|
|
213
|
+
return useMemo(()=>aiJobsActivity(jobs), [
|
|
214
|
+
jobs
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
//# sourceMappingURL=ai-jobs-store.js.map
|