@aglyn/plugins-ai 1.0.0-beta.226 → 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/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/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
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Peer dependencies: `react`, `next`, `@mui/material`, `@mui/x-data-grid`, `fireba
|
|
|
17
17
|
**Console** (`registerAiConsole`, the `console` and `staff` registrar in `plugins.config.json`). The plugin adds no nav item. Everything is mounted through zones the console shell and other plugins host, so no console page imports it:
|
|
18
18
|
|
|
19
19
|
- The assistant, drawn in the console dock (`consoleDock`) and a provider the shell mounts around every console page for the Besigner copy assistant, with controls in the `besignerToolbar` and `besignerInspector` slots.
|
|
20
|
+
- The AI jobs indicator in the console's top bar (`consoleTopBar`): what the workspace's jobs are doing, and the way to them.
|
|
20
21
|
- "Describe it" entry points and cards on a site's pages list and its components, layouts, templates, forms, theme, SEO and automations pages (`hostScreens`, `hostComponents`, `hostLayouts`, `hostTemplates`, `hostForms`, `hostTheme`, `hostSeo`, `seoFields`, `hostAutomations`, `automationEditor`, `automationRun`, `hostFirstRun`, `orgSites`).
|
|
21
22
|
- Cards in zones other plugins host: product copy, import and the products hub (`productEditor`, `productImport`, `productsHub`), record insights, email drafts and import column matching (`recordInsights`, `recordEmail`, `importMapping`), and A/B test variants and results (`experimentVariants`, `experimentResult`).
|
|
22
23
|
- Usage and access: credit and usage cards on billing (`orgBillingUsage`), per-member cards and columns (`orgMember`, `orgMembersListColumn`, `hostMembers`), and staff cards, columns and one staff page (`staffOrg`, `staffUser`, `staffOrgsListColumn`, `staffOrgUsageColumn`).
|
|
@@ -26,7 +27,7 @@ Peer dependencies: `react`, `next`, `@mui/material`, `@mui/x-data-grid`, `fireba
|
|
|
26
27
|
- Assistant: `assist/chat`, `assist/feedback`, `assist/edit-applied`, `ai/assist`.
|
|
27
28
|
- Jobs: `ai/jobs`, `ai/jobs/batch`, `ai/jobs/:jobId/cancel`, `ai/jobs/:jobId/resume`, `ai/jobs/:jobId/events`, the scheduled jobs beat, `ai/insights/:jobId`, `ai/crm/:jobId`, `ai/seo/apply`, `ai/generate/component`.
|
|
28
29
|
- Usage and access: `ai/usage`, `ai/allotments`, `ai/models`, `ai/host-permissions`, `ai/billing/credits`, `ai/billing/overage`.
|
|
29
|
-
- Staff: `ai/admin/org`, `ai/admin/orgs-spend`, `ai/admin/user`, `ai/admin/signals`, `ai/admin/overage`.
|
|
30
|
+
- Staff: `ai/admin/org`, `ai/admin/orgs-spend`, `ai/admin/user`, `ai/admin/signals`, `ai/admin/overage`, `ai/admin/credits`.
|
|
30
31
|
|
|
31
32
|
**Declarations.** Three light entries the core reads before any surface loads:
|
|
32
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-ai",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.227",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,22 +25,22 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-errors": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
42
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
43
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.227",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.227",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.227",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.227",
|
|
32
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.227",
|
|
33
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.227",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.227",
|
|
35
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.227",
|
|
36
|
+
"@aglyn/shared-util-email": "1.0.0-beta.227",
|
|
37
|
+
"@aglyn/shared-util-errors": "1.0.0-beta.227",
|
|
38
|
+
"@aglyn/shared-util-http": "1.0.0-beta.227",
|
|
39
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.227",
|
|
40
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.227",
|
|
41
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.227",
|
|
42
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.227",
|
|
43
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.227",
|
|
44
44
|
"@swc/helpers": "0.5.23"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/ import { ASSIST_PROVIDER_COST_FIELD, assistMonthOverage, assistProviderCostUsd } from "../usage/assist-credits.js";
|
|
17
17
|
import { aiOverageBillsByInvoice } from "./ai-overage-cutover.js";
|
|
18
18
|
import { aiOverageFirestore } from "./ai-overage-trigger.js";
|
|
19
|
+
import { ASSIST_RETURNED_USD_FIELD, assistSpendAfterReturnsUsd } from "../usage/assist-credit-returns.js";
|
|
19
20
|
/**
|
|
20
21
|
* One workspace's month of Aglyn Assist, as the usage sweep records and bills
|
|
21
22
|
* it (AGL-2280, AGL-2653, AGL-3011).
|
|
@@ -42,11 +43,12 @@ import { aiOverageFirestore } from "./ai-overage-trigger.js";
|
|
|
42
43
|
* way, so a month billed through the other channel never reads as a month
|
|
43
44
|
* inside its band.
|
|
44
45
|
*/ export async function measureAiMonth(context) {
|
|
45
|
-
var _snapshot_get;
|
|
46
46
|
const snapshot = await aiOverageFirestore().collection('orgs').doc(context.orgId).collection('assistUsage').doc(context.month).get();
|
|
47
47
|
const assistCostUsd = assistProviderCostUsd(snapshot.get('estCostUsd'), snapshot.get(ASSIST_PROVIDER_COST_FIELD));
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// The credits drawn are net of any given back (AGL-3595): a give-back is
|
|
49
|
+
// compensation, and billing the credits it returned would take it back.
|
|
50
|
+
// The provider figure above is not — that money was spent.
|
|
51
|
+
const overage = assistMonthOverage(context.org, assistSpendAfterReturnsUsd(snapshot.get('estCostUsd'), snapshot.get(ASSIST_RETURNED_USD_FIELD)));
|
|
50
52
|
const billedByInvoice = aiOverageBillsByInvoice(context.month);
|
|
51
53
|
const meteredUsd = billedByInvoice ? 0 : overage.overageMonthlyUsd;
|
|
52
54
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/billing/ai-month-meter.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\nimport {\n ASSIST_PROVIDER_COST_FIELD,\n assistMonthOverage,\n assistProviderCostUsd,\n} from '../usage/assist-credits'\nimport type {\n PluginUsageMeterCloseContext,\n PluginUsageMeterContext,\n PluginUsageMeterReading,\n} from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { aiOverageBillsByInvoice } from './ai-overage-cutover'\nimport { aiOverageFirestore } from './ai-overage-trigger'\n\n/**\n * One workspace's month of Aglyn Assist, as the usage sweep records and bills\n * it (AGL-2280, AGL-2653, AGL-3011).\n *\n * THE SPEND is recorded and never billed. `assistCostUsd` is the provider\n * figure of the two the month's document keeps (AGL-3015) — what the tokens\n * cost US, at the provider's rates — and enters the cost model at ×1 through\n * this plugin's cost axis, because the discount guardrail's whole job is to\n * compare revenue with what a workspace costs. On the invoice it would charge\n * for what the subscription already bought.\n *\n * THE OVERAGE is billed. The credits past the plan's band were sold at the\n * plan's per-1,000 rate, so they enter the month's billed figure beside the\n * platform's plan-priced overages. One derivation — `assistMonthOverage` turns\n * the BILLED spend (`estCostUsd`) into credits, subtracts the band the\n * workspace resolves to and prices the rest — so the customer's meter and this\n * line cannot disagree about what was over. The workspace's hard-cap switch is\n * not read here: it lives at the gate, and reading it at sweep time would let a\n * flip on the 1st erase a month's overage.\n *\n * From the month `AI_OVERAGE_INVOICED_FROM` names, the overage is billed by\n * invoice as it accrues instead, and leaves the meter figure so one month's\n * usage reaches exactly one invoice. It is still priced and recorded either\n * way, so a month billed through the other channel never reads as a month\n * inside its band.\n */\nexport async function measureAiMonth(\n context: PluginUsageMeterContext,\n): Promise<PluginUsageMeterReading> {\n const snapshot = await aiOverageFirestore()\n .collection('orgs')\n .doc(context.orgId)\n .collection('assistUsage')\n .doc(context.month)\n .get()\n const assistCostUsd = assistProviderCostUsd(\n snapshot.get('estCostUsd'),\n snapshot.get(ASSIST_PROVIDER_COST_FIELD),\n )\n
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/billing/ai-month-meter.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\nimport {\n ASSIST_PROVIDER_COST_FIELD,\n assistMonthOverage,\n assistProviderCostUsd,\n} from '../usage/assist-credits'\nimport type {\n PluginUsageMeterCloseContext,\n PluginUsageMeterContext,\n PluginUsageMeterReading,\n} from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { aiOverageBillsByInvoice } from './ai-overage-cutover'\nimport { aiOverageFirestore } from './ai-overage-trigger'\nimport {\n ASSIST_RETURNED_USD_FIELD,\n assistSpendAfterReturnsUsd,\n} from '../usage/assist-credit-returns'\n\n/**\n * One workspace's month of Aglyn Assist, as the usage sweep records and bills\n * it (AGL-2280, AGL-2653, AGL-3011).\n *\n * THE SPEND is recorded and never billed. `assistCostUsd` is the provider\n * figure of the two the month's document keeps (AGL-3015) — what the tokens\n * cost US, at the provider's rates — and enters the cost model at ×1 through\n * this plugin's cost axis, because the discount guardrail's whole job is to\n * compare revenue with what a workspace costs. On the invoice it would charge\n * for what the subscription already bought.\n *\n * THE OVERAGE is billed. The credits past the plan's band were sold at the\n * plan's per-1,000 rate, so they enter the month's billed figure beside the\n * platform's plan-priced overages. One derivation — `assistMonthOverage` turns\n * the BILLED spend (`estCostUsd`) into credits, subtracts the band the\n * workspace resolves to and prices the rest — so the customer's meter and this\n * line cannot disagree about what was over. The workspace's hard-cap switch is\n * not read here: it lives at the gate, and reading it at sweep time would let a\n * flip on the 1st erase a month's overage.\n *\n * From the month `AI_OVERAGE_INVOICED_FROM` names, the overage is billed by\n * invoice as it accrues instead, and leaves the meter figure so one month's\n * usage reaches exactly one invoice. It is still priced and recorded either\n * way, so a month billed through the other channel never reads as a month\n * inside its band.\n */\nexport async function measureAiMonth(\n context: PluginUsageMeterContext,\n): Promise<PluginUsageMeterReading> {\n const snapshot = await aiOverageFirestore()\n .collection('orgs')\n .doc(context.orgId)\n .collection('assistUsage')\n .doc(context.month)\n .get()\n const assistCostUsd = assistProviderCostUsd(\n snapshot.get('estCostUsd'),\n snapshot.get(ASSIST_PROVIDER_COST_FIELD),\n )\n // The credits drawn are net of any given back (AGL-3595): a give-back is\n // compensation, and billing the credits it returned would take it back.\n // The provider figure above is not — that money was spent.\n const overage = assistMonthOverage(\n context.org as never,\n assistSpendAfterReturnsUsd(\n snapshot.get('estCostUsd'),\n snapshot.get(ASSIST_RETURNED_USD_FIELD),\n ),\n )\n const billedByInvoice = aiOverageBillsByInvoice(context.month)\n const meteredUsd = billedByInvoice ? 0 : overage.overageMonthlyUsd\n return {\n fields: {\n assistCostUsd,\n // The credit view of the same month: what was drawn, the band it was\n // drawn against (`null` where the plan sells none), the part past it\n // and the rate it was priced at. Recorded always, zeros included, so a\n // month inside its band is legible as one.\n assistCredits: overage.usedCredits,\n assistCreditsBand: overage.bandCredits,\n assistCreditsOverage: overage.overageCredits,\n assistOverageUsd: overage.overageMonthlyUsd,\n assistOverageRateUsd: overage.overageRateUsd,\n // What entered the meter figure, and whether the invoice channel took\n // it instead — countable from the rows rather than inferred from a\n // deployment variable.\n assistOverageMeteredUsd: meteredUsd,\n assistOverageBilledByPlugin: billedByInvoice,\n },\n billedUsd: meteredUsd,\n }\n}\n\n/**\n * The close-out of a month whose overage is billed by invoice: the remainder\n * that never reached the charge threshold, reconciled and invoiced\n * (`ai-overage-close.ts`). Before the cutover month there is nothing of this\n * plugin's to settle, and the workspace's billing document is never read.\n */\nexport async function closeAiMonth(\n context: PluginUsageMeterCloseContext,\n): Promise<void> {\n if (!aiOverageBillsByInvoice(context.month)) return\n const { closeAiOverageMonth } = await import('./ai-overage-close')\n await closeAiOverageMonth({\n orgId: context.orgId,\n month: context.month,\n org: context.org,\n stripeCustomerId: await context.stripeCustomerId(),\n })\n}\n"],"names":["ASSIST_PROVIDER_COST_FIELD","assistMonthOverage","assistProviderCostUsd","aiOverageBillsByInvoice","aiOverageFirestore","ASSIST_RETURNED_USD_FIELD","assistSpendAfterReturnsUsd","measureAiMonth","context","snapshot","collection","doc","orgId","month","get","assistCostUsd","overage","org","billedByInvoice","meteredUsd","overageMonthlyUsd","fields","assistCredits","usedCredits","assistCreditsBand","bandCredits","assistCreditsOverage","overageCredits","assistOverageUsd","assistOverageRateUsd","overageRateUsd","assistOverageMeteredUsd","assistOverageBilledByPlugin","billedUsd","closeAiMonth","closeAiOverageMonth","stripeCustomerId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,0BAA0B,EAC1BC,kBAAkB,EAClBC,qBAAqB,QAChB,6BAAyB;AAMhC,SAASC,uBAAuB,QAAQ,0BAAsB;AAC9D,SAASC,kBAAkB,QAAQ,0BAAsB;AACzD,SACEC,yBAAyB,EACzBC,0BAA0B,QACrB,oCAAgC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GACD,OAAO,eAAeC,eACpBC,OAAgC;IAEhC,MAAMC,WAAW,MAAML,qBACpBM,UAAU,CAAC,QACXC,GAAG,CAACH,QAAQI,KAAK,EACjBF,UAAU,CAAC,eACXC,GAAG,CAACH,QAAQK,KAAK,EACjBC,GAAG;IACN,MAAMC,gBAAgBb,sBACpBO,SAASK,GAAG,CAAC,eACbL,SAASK,GAAG,CAACd;IAEf,yEAAyE;IACzE,wEAAwE;IACxE,2DAA2D;IAC3D,MAAMgB,UAAUf,mBACdO,QAAQS,GAAG,EACXX,2BACEG,SAASK,GAAG,CAAC,eACbL,SAASK,GAAG,CAACT;IAGjB,MAAMa,kBAAkBf,wBAAwBK,QAAQK,KAAK;IAC7D,MAAMM,aAAaD,kBAAkB,IAAIF,QAAQI,iBAAiB;IAClE,OAAO;QACLC,QAAQ;YACNN;YACA,qEAAqE;YACrE,qEAAqE;YACrE,uEAAuE;YACvE,2CAA2C;YAC3CO,eAAeN,QAAQO,WAAW;YAClCC,mBAAmBR,QAAQS,WAAW;YACtCC,sBAAsBV,QAAQW,cAAc;YAC5CC,kBAAkBZ,QAAQI,iBAAiB;YAC3CS,sBAAsBb,QAAQc,cAAc;YAC5C,sEAAsE;YACtE,mEAAmE;YACnE,uBAAuB;YACvBC,yBAAyBZ;YACzBa,6BAA6Bd;QAC/B;QACAe,WAAWd;IACb;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAee,aACpB1B,OAAqC;IAErC,IAAI,CAACL,wBAAwBK,QAAQK,KAAK,GAAG;IAC7C,MAAM,EAAEsB,mBAAmB,EAAE,GAAG,MAAM,MAAM,CAAC;IAC7C,MAAMA,oBAAoB;QACxBvB,OAAOJ,QAAQI,KAAK;QACpBC,OAAOL,QAAQK,KAAK;QACpBI,KAAKT,QAAQS,GAAG;QAChBmB,kBAAkB,MAAM5B,QAAQ4B,gBAAgB;IAClD;AACF"}
|
|
@@ -56,6 +56,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
56
56
|
* `aiAllotments` and no writer at all.
|
|
57
57
|
*/ import { FieldValue } from "firebase-admin/firestore";
|
|
58
58
|
import { assistMonthOverage } from "../usage/assist-credits.js";
|
|
59
|
+
import { ASSIST_RETURNED_USD_FIELD, assistSpendAfterReturnsUsd } from "../usage/assist-credit-returns.js";
|
|
59
60
|
import { AI_OVERAGE_MIN_CHARGE_USD, AI_OVERAGE_THRESHOLD_USD, aiOverageCardOnFile, readAiOverageStanding } from "./ai-overage-standing.js";
|
|
60
61
|
/** The subcollection and document the standing lives at. */ export const AI_BILLING_SUBCOLLECTION = 'aiBilling';
|
|
61
62
|
export const AI_BILLING_STANDING_DOC = 'standing';
|
|
@@ -127,7 +128,8 @@ function money(value) {
|
|
|
127
128
|
refused: 'charge-already-open'
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
// Net of credits given back (AGL-3595), as the meter and the gate read it.
|
|
132
|
+
const priced = assistMonthOverage(request.org, assistSpendAfterReturnsUsd(usageSnapshot.get('estCostUsd'), usageSnapshot.get(ASSIST_RETURNED_USD_FIELD)));
|
|
131
133
|
// A plan that sells nothing past its band prices its overage to zero, so
|
|
132
134
|
// this is structural rather than a check that could be forgotten — but
|
|
133
135
|
// it is named, because "we billed nothing" and "there is nothing to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/billing/ai-overage-ledger.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\n/**\n * THE OVERAGE CHARGE LEDGER (AGL-3011).\n *\n * Every dollar of AI overage a workspace is charged mid-period passes\n * through here twice: once as a CLAIM, taken in a transaction before any\n * money is asked for, and once as a SETTLEMENT, recorded after Stripe\n * answers. Nothing in this module talks to Stripe.\n *\n * ## Why claim first\n *\n * The figure that decides a charge — the month's overage — is a counter many\n * requests move. Two requests crossing the threshold a millisecond apart\n * both read \"$25 unbilled\" and would both charge for it. The claim closes\n * that: inside one transaction it moves `overageInvoicedUsd` forward by the\n * amount it is about to charge and records an open charge, so the second\n * request reads an already-claimed balance and stands down. The invoice is\n * created afterwards, outside the transaction, because a Stripe call inside\n * a Firestore transaction would be retried by the transaction.\n *\n * ## What a lost process costs\n *\n * A claim that never became an invoice leaves `overageInvoicedUsd` ahead of\n * what was billed, which UNDER-charges rather than over-charges, and leaves\n * an open charge that blocks the next one. Both are repaired by the\n * reconcile sweep, which asks Stripe whether the claim ever became an\n * invoice and either records it or releases the claim. Under-charging while\n * confused is the direction this must err in.\n *\n * ## The documents\n *\n * `orgs/{orgId}/assistUsage/{YYYY-MM}` — the month's rollup, which the\n * reservation already reads, gains four fields so the gate costs no extra\n * read: `overageInvoicedUsd`, `overagePaidUsd`, `overageCharges` and\n * `overageInvoiceOpen`.\n *\n * `orgs/{orgId}/aiOverageCharges/{chargeId}` — one row per charge, the\n * audit trail staff read and the reconcile sweep walks.\n *\n * Both are server-written: the rules allow the same readers as\n * `aiAllotments` and no writer at all.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { assistMonthOverage } from '../usage/assist-credits'\nimport {\n AI_OVERAGE_MIN_CHARGE_USD,\n AI_OVERAGE_THRESHOLD_USD,\n aiOverageCardOnFile,\n readAiOverageStanding,\n type AiOverageOrg,\n} from './ai-overage-standing'\n\n/** The subcollection and document the standing lives at. */\nexport const AI_BILLING_SUBCOLLECTION = 'aiBilling'\nexport const AI_BILLING_STANDING_DOC = 'standing'\n/** The subcollection one row per charge lives in. */\nexport const AI_OVERAGE_CHARGES_SUBCOLLECTION = 'aiOverageCharges'\n\n/** Which sweep asked for the charge. */\nexport type AiOverageChargeKind = 'threshold' | 'closeout'\n\n/** Where a charge is in its life. */\nexport type AiOverageChargeStatus =\n | 'claimed'\n | 'open'\n | 'paid'\n | 'failed'\n | 'void'\n\n/** A claim taken, ready to be charged. */\nexport interface AiOverageClaim {\n chargeId: string\n orgId: string\n month: string\n seq: number\n kind: AiOverageChargeKind\n /** Floored to the cent — Stripe bills whole cents. */\n amountUsd: number\n /** The credits and rate the amount came from, for the audit row. */\n credits: number\n rateUsdPer1k: number | null\n}\n\n/** Why no claim was taken. Each one is a different thing to do about it. */\nexport type AiOverageClaimRefusal =\n | 'below-threshold'\n | 'charge-already-open'\n | 'no-card'\n | 'paused'\n | 'not-sold'\n\nexport type AiOverageClaimResult =\n | { claimed: AiOverageClaim; refused: null }\n | { claimed: null; refused: AiOverageClaimRefusal }\n\nfunction money(value: unknown): number {\n const number = Number(value)\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/** Down to the cent. Never up: a rounded-up cent is a cent never accrued. */\nexport function floorToCent(usd: number): number {\n return Math.floor(money(usd) * 100) / 100\n}\n\n/**\n * `{month}-{seq}` — readable, sortable, and unique per workspace-month.\n *\n * The id is also the Stripe idempotency key's payload and the metadata the\n * reconcile search matches on, so it must survive a URL and a Stripe search\n * query unescaped: digits, a hyphen, nothing else.\n */\nexport function aiOverageChargeId(month: string, seq: number): string {\n return `${month}-${String(Math.max(1, Math.floor(seq))).padStart(3, '0')}`\n}\n\n/**\n * Takes a claim on the workspace's unbilled overage, or explains why not.\n *\n * The four conditions are the design's, in one transaction with the reads\n * they are decided from:\n *\n * - enough unbilled overage (the threshold, or the minimum for a close-out);\n * - no charge already open — one at a time, so a failure has one owner;\n * - a card on file, since the bound assumes a payment method that settles\n * now rather than in days;\n * - not paused.\n *\n * The card condition abstains when the workspace's payment method has never\n * been observed, for the reason `AiOverageStanding.paymentMethodType` gives.\n * A charge attempted against a customer with no default simply fails, and\n * the failure pauses accrual like any other — the guard is an optimization\n * here, not the safety property.\n */\nexport async function claimAiOverageCharge(\n firestore: FirebaseFirestore.Firestore,\n request: {\n orgId: string\n org: AiOverageOrg\n month: string\n kind: AiOverageChargeKind\n now?: Date\n },\n): Promise<AiOverageClaimResult> {\n const now = request.now ?? new Date()\n const orgRef = firestore.collection('orgs').doc(request.orgId)\n const usageRef = orgRef.collection('assistUsage').doc(request.month)\n const standingRef = orgRef\n .collection(AI_BILLING_SUBCOLLECTION)\n .doc(AI_BILLING_STANDING_DOC)\n // A close-out settles a month that is over: there is nothing left to\n // accrue, so anything Stripe will accept is worth billing. A threshold\n // charge waits for the threshold, which is what keeps the invoice count\n // and its fees down.\n const floorUsd =\n request.kind === 'closeout' ? AI_OVERAGE_MIN_CHARGE_USD : AI_OVERAGE_THRESHOLD_USD\n\n return firestore.runTransaction(async (tx) => {\n const [usageSnapshot, standingSnapshot] = await Promise.all([\n tx.get(usageRef),\n tx.get(standingRef),\n ])\n const standing = readAiOverageStanding(\n standingSnapshot.exists ? (standingSnapshot.data() ?? null) : null,\n )\n if (aiOverageCardOnFile(standing) === false) {\n return { claimed: null, refused: 'no-card' as const }\n }\n if (standing.pause) return { claimed: null, refused: 'paused' as const }\n if (usageSnapshot.get('overageInvoiceOpen')) {\n return { claimed: null, refused: 'charge-already-open' as const }\n }\n const priced = assistMonthOverage(\n request.org,\n money(usageSnapshot.get('estCostUsd')),\n )\n // A plan that sells nothing past its band prices its overage to zero, so\n // this is structural rather than a check that could be forgotten — but\n // it is named, because \"we billed nothing\" and \"there is nothing to\n // bill\" are different answers to a staff question.\n if (priced.overageRateUsd === null) {\n return { claimed: null, refused: 'not-sold' as const }\n }\n const invoicedUsd = money(usageSnapshot.get('overageInvoicedUsd'))\n const unbilledUsd = floorToCent(priced.overageMonthlyUsd - invoicedUsd)\n if (!(unbilledUsd >= floorUsd)) {\n return { claimed: null, refused: 'below-threshold' as const }\n }\n const seq = Math.max(0, Math.floor(money(usageSnapshot.get('overageCharges')))) + 1\n const chargeId = aiOverageChargeId(request.month, seq)\n const claim: AiOverageClaim = {\n chargeId,\n orgId: request.orgId,\n month: request.month,\n seq,\n kind: request.kind,\n amountUsd: unbilledUsd,\n credits: priced.overageCredits,\n rateUsdPer1k: priced.overageRateUsd,\n }\n tx.set(\n usageRef,\n {\n month: request.month,\n overageInvoicedUsd: FieldValue.increment(unbilledUsd),\n overageCharges: FieldValue.increment(1),\n overageInvoiceOpen: {\n chargeId,\n invoiceId: null,\n amountUsd: unbilledUsd,\n since: now.toISOString(),\n },\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n tx.set(\n orgRef.collection(AI_OVERAGE_CHARGES_SUBCOLLECTION).doc(chargeId),\n {\n month: request.month,\n seq,\n kind: request.kind,\n amountUsd: unbilledUsd,\n credits: priced.overageCredits,\n rateUsdPer1k: priced.overageRateUsd,\n invoiceId: null,\n status: 'claimed' satisfies AiOverageChargeStatus,\n createdAt: FieldValue.serverTimestamp(),\n paidAt: null,\n failedAt: null,\n },\n { merge: true },\n )\n return { claimed: claim, refused: null }\n })\n}\n\n/** What Stripe said, as the ledger records it. */\nexport interface AiOverageSettlement {\n chargeId: string\n month: string\n /** `null` only when no invoice was ever created. */\n invoiceId: string | null\n status: AiOverageChargeStatus\n /**\n * What Stripe actually collected, in dollars (AGL-3023).\n *\n * The month's paid total moves by THIS and never by the claim. The two are\n * normally equal, and the case where they are not is the one that matters:\n * an invoice that finalized at zero or collected less than it billed would\n * otherwise clear a balance nobody paid, and the gate's unpaid bound would\n * be satisfied by our own bookkeeping instead of by money.\n *\n * Omitted on a settlement that is not a payment, where it is not read.\n */\n paidUsd?: number\n}\n\n/**\n * Records what became of a claimed charge.\n *\n * `paid` adds the amount to the month's `overagePaidUsd`, which is what the\n * gate's unpaid bound subtracts — so paying is what lets a workspace keep\n * going, and nothing else does.\n *\n * An invoice that exists but is not paid KEEPS its claim on\n * `overageInvoicedUsd`: it is a real bill, Stripe's retries may still settle\n * it, and re-billing the same dollars on a second invoice would charge the\n * customer twice for one month's usage. Only a claim that never became an\n * invoice at all releases what it claimed, so those dollars can be charged\n * by a later attempt.\n *\n * The open-charge slot is always cleared. Leaving it set on a failure would\n * block the close-out and every later charge behind an invoice that is\n * never going to move on its own.\n */\nexport async function settleAiOverageCharge(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n settlement: AiOverageSettlement,\n): Promise<void> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const usageRef = orgRef.collection('assistUsage').doc(settlement.month)\n const chargeRef = orgRef\n .collection(AI_OVERAGE_CHARGES_SUBCOLLECTION)\n .doc(settlement.chargeId)\n\n await firestore.runTransaction(async (tx) => {\n const [usageSnapshot, chargeSnapshot] = await Promise.all([\n tx.get(usageRef),\n tx.get(chargeRef),\n ])\n const amountUsd = money(chargeSnapshot.get('amountUsd'))\n // What the month's paid total moves by: what Stripe took. A settlement\n // that claims to be a payment and names no collected figure moves\n // nothing — silence is not evidence of money (AGL-3023).\n const paidUsd = money(settlement.paidUsd)\n const open = usageSnapshot.get('overageInvoiceOpen') as\n | { chargeId?: unknown }\n | null\n | undefined\n const ownsTheSlot = String(open?.chargeId ?? '') === settlement.chargeId\n // Already settled, or settled by another path (the webhook and the\n // synchronous answer both arrive). Recording the status again is\n // harmless; moving the money counters again is not.\n const alreadyPaid = chargeSnapshot.get('status') === 'paid'\n const releases = settlement.status === 'failed' && !settlement.invoiceId\n tx.set(\n chargeRef,\n {\n invoiceId: settlement.invoiceId,\n status: settlement.status,\n ...(settlement.status === 'paid' ? { paidUsd } : {}),\n ...(settlement.status === 'paid'\n ? { paidAt: FieldValue.serverTimestamp() }\n : {}),\n ...(settlement.status === 'failed'\n ? { failedAt: FieldValue.serverTimestamp() }\n : {}),\n },\n { merge: true },\n )\n tx.set(\n usageRef,\n {\n month: settlement.month,\n ...(settlement.status === 'paid' && !alreadyPaid && paidUsd > 0\n ? { overagePaidUsd: FieldValue.increment(paidUsd) }\n : {}),\n ...(releases\n ? {\n overageInvoicedUsd: FieldValue.increment(-amountUsd),\n overageCharges: FieldValue.increment(-1),\n }\n : {}),\n ...(ownsTheSlot ? { overageInvoiceOpen: null } : {}),\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n })\n}\n\n/** The month's charge counters, as the customer card and staff card read them. */\nexport interface AiOverageMonthLedger {\n invoicedUsd: number\n paidUsd: number\n charges: number\n open: { chargeId: string; invoiceId: string | null; amountUsd: number } | null\n}\n\n/** Reads the four fields off a month's rollup, defensively. */\nexport function readAiOverageMonthLedger(\n data: Record<string, unknown> | null | undefined,\n): AiOverageMonthLedger {\n const open = data?.['overageInvoiceOpen'] as Record<string, unknown> | null | undefined\n const chargeId = typeof open?.['chargeId'] === 'string' ? open['chargeId'] : ''\n return {\n invoicedUsd: money(data?.['overageInvoicedUsd']),\n paidUsd: money(data?.['overagePaidUsd']),\n charges: Math.max(0, Math.floor(money(data?.['overageCharges']))),\n open: chargeId\n ? {\n chargeId,\n invoiceId:\n typeof open?.['invoiceId'] === 'string' ? (open['invoiceId'] as string) : null,\n amountUsd: money(open?.['amountUsd']),\n }\n : null,\n }\n}\n"],"names":["FieldValue","assistMonthOverage","AI_OVERAGE_MIN_CHARGE_USD","AI_OVERAGE_THRESHOLD_USD","aiOverageCardOnFile","readAiOverageStanding","AI_BILLING_SUBCOLLECTION","AI_BILLING_STANDING_DOC","AI_OVERAGE_CHARGES_SUBCOLLECTION","money","value","number","Number","isFinite","floorToCent","usd","Math","floor","aiOverageChargeId","month","seq","String","max","padStart","claimAiOverageCharge","firestore","request","now","Date","orgRef","collection","doc","orgId","usageRef","standingRef","floorUsd","kind","runTransaction","tx","standingSnapshot","usageSnapshot","Promise","all","get","standing","exists","data","claimed","refused","pause","priced","org","overageRateUsd","invoicedUsd","unbilledUsd","overageMonthlyUsd","chargeId","claim","amountUsd","credits","overageCredits","rateUsdPer1k","set","overageInvoicedUsd","increment","overageCharges","overageInvoiceOpen","invoiceId","since","toISOString","updatedAt","serverTimestamp","merge","status","createdAt","paidAt","failedAt","settleAiOverageCharge","settlement","chargeRef","chargeSnapshot","paidUsd","open","ownsTheSlot","alreadyPaid","releases","overagePaidUsd","readAiOverageMonthLedger","charges"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/billing/ai-overage-ledger.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\n/**\n * THE OVERAGE CHARGE LEDGER (AGL-3011).\n *\n * Every dollar of AI overage a workspace is charged mid-period passes\n * through here twice: once as a CLAIM, taken in a transaction before any\n * money is asked for, and once as a SETTLEMENT, recorded after Stripe\n * answers. Nothing in this module talks to Stripe.\n *\n * ## Why claim first\n *\n * The figure that decides a charge — the month's overage — is a counter many\n * requests move. Two requests crossing the threshold a millisecond apart\n * both read \"$25 unbilled\" and would both charge for it. The claim closes\n * that: inside one transaction it moves `overageInvoicedUsd` forward by the\n * amount it is about to charge and records an open charge, so the second\n * request reads an already-claimed balance and stands down. The invoice is\n * created afterwards, outside the transaction, because a Stripe call inside\n * a Firestore transaction would be retried by the transaction.\n *\n * ## What a lost process costs\n *\n * A claim that never became an invoice leaves `overageInvoicedUsd` ahead of\n * what was billed, which UNDER-charges rather than over-charges, and leaves\n * an open charge that blocks the next one. Both are repaired by the\n * reconcile sweep, which asks Stripe whether the claim ever became an\n * invoice and either records it or releases the claim. Under-charging while\n * confused is the direction this must err in.\n *\n * ## The documents\n *\n * `orgs/{orgId}/assistUsage/{YYYY-MM}` — the month's rollup, which the\n * reservation already reads, gains four fields so the gate costs no extra\n * read: `overageInvoicedUsd`, `overagePaidUsd`, `overageCharges` and\n * `overageInvoiceOpen`.\n *\n * `orgs/{orgId}/aiOverageCharges/{chargeId}` — one row per charge, the\n * audit trail staff read and the reconcile sweep walks.\n *\n * Both are server-written: the rules allow the same readers as\n * `aiAllotments` and no writer at all.\n */\n\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { assistMonthOverage } from '../usage/assist-credits'\nimport {\n ASSIST_RETURNED_USD_FIELD,\n assistSpendAfterReturnsUsd,\n} from '../usage/assist-credit-returns'\nimport {\n AI_OVERAGE_MIN_CHARGE_USD,\n AI_OVERAGE_THRESHOLD_USD,\n aiOverageCardOnFile,\n readAiOverageStanding,\n type AiOverageOrg,\n} from './ai-overage-standing'\n\n/** The subcollection and document the standing lives at. */\nexport const AI_BILLING_SUBCOLLECTION = 'aiBilling'\nexport const AI_BILLING_STANDING_DOC = 'standing'\n/** The subcollection one row per charge lives in. */\nexport const AI_OVERAGE_CHARGES_SUBCOLLECTION = 'aiOverageCharges'\n\n/** Which sweep asked for the charge. */\nexport type AiOverageChargeKind = 'threshold' | 'closeout'\n\n/** Where a charge is in its life. */\nexport type AiOverageChargeStatus =\n | 'claimed'\n | 'open'\n | 'paid'\n | 'failed'\n | 'void'\n\n/** A claim taken, ready to be charged. */\nexport interface AiOverageClaim {\n chargeId: string\n orgId: string\n month: string\n seq: number\n kind: AiOverageChargeKind\n /** Floored to the cent — Stripe bills whole cents. */\n amountUsd: number\n /** The credits and rate the amount came from, for the audit row. */\n credits: number\n rateUsdPer1k: number | null\n}\n\n/** Why no claim was taken. Each one is a different thing to do about it. */\nexport type AiOverageClaimRefusal =\n | 'below-threshold'\n | 'charge-already-open'\n | 'no-card'\n | 'paused'\n | 'not-sold'\n\nexport type AiOverageClaimResult =\n | { claimed: AiOverageClaim; refused: null }\n | { claimed: null; refused: AiOverageClaimRefusal }\n\nfunction money(value: unknown): number {\n const number = Number(value)\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/** Down to the cent. Never up: a rounded-up cent is a cent never accrued. */\nexport function floorToCent(usd: number): number {\n return Math.floor(money(usd) * 100) / 100\n}\n\n/**\n * `{month}-{seq}` — readable, sortable, and unique per workspace-month.\n *\n * The id is also the Stripe idempotency key's payload and the metadata the\n * reconcile search matches on, so it must survive a URL and a Stripe search\n * query unescaped: digits, a hyphen, nothing else.\n */\nexport function aiOverageChargeId(month: string, seq: number): string {\n return `${month}-${String(Math.max(1, Math.floor(seq))).padStart(3, '0')}`\n}\n\n/**\n * Takes a claim on the workspace's unbilled overage, or explains why not.\n *\n * The four conditions are the design's, in one transaction with the reads\n * they are decided from:\n *\n * - enough unbilled overage (the threshold, or the minimum for a close-out);\n * - no charge already open — one at a time, so a failure has one owner;\n * - a card on file, since the bound assumes a payment method that settles\n * now rather than in days;\n * - not paused.\n *\n * The card condition abstains when the workspace's payment method has never\n * been observed, for the reason `AiOverageStanding.paymentMethodType` gives.\n * A charge attempted against a customer with no default simply fails, and\n * the failure pauses accrual like any other — the guard is an optimization\n * here, not the safety property.\n */\nexport async function claimAiOverageCharge(\n firestore: FirebaseFirestore.Firestore,\n request: {\n orgId: string\n org: AiOverageOrg\n month: string\n kind: AiOverageChargeKind\n now?: Date\n },\n): Promise<AiOverageClaimResult> {\n const now = request.now ?? new Date()\n const orgRef = firestore.collection('orgs').doc(request.orgId)\n const usageRef = orgRef.collection('assistUsage').doc(request.month)\n const standingRef = orgRef\n .collection(AI_BILLING_SUBCOLLECTION)\n .doc(AI_BILLING_STANDING_DOC)\n // A close-out settles a month that is over: there is nothing left to\n // accrue, so anything Stripe will accept is worth billing. A threshold\n // charge waits for the threshold, which is what keeps the invoice count\n // and its fees down.\n const floorUsd =\n request.kind === 'closeout' ? AI_OVERAGE_MIN_CHARGE_USD : AI_OVERAGE_THRESHOLD_USD\n\n return firestore.runTransaction(async (tx) => {\n const [usageSnapshot, standingSnapshot] = await Promise.all([\n tx.get(usageRef),\n tx.get(standingRef),\n ])\n const standing = readAiOverageStanding(\n standingSnapshot.exists ? (standingSnapshot.data() ?? null) : null,\n )\n if (aiOverageCardOnFile(standing) === false) {\n return { claimed: null, refused: 'no-card' as const }\n }\n if (standing.pause) return { claimed: null, refused: 'paused' as const }\n if (usageSnapshot.get('overageInvoiceOpen')) {\n return { claimed: null, refused: 'charge-already-open' as const }\n }\n // Net of credits given back (AGL-3595), as the meter and the gate read it.\n const priced = assistMonthOverage(\n request.org,\n assistSpendAfterReturnsUsd(\n usageSnapshot.get('estCostUsd'),\n usageSnapshot.get(ASSIST_RETURNED_USD_FIELD),\n ),\n )\n // A plan that sells nothing past its band prices its overage to zero, so\n // this is structural rather than a check that could be forgotten — but\n // it is named, because \"we billed nothing\" and \"there is nothing to\n // bill\" are different answers to a staff question.\n if (priced.overageRateUsd === null) {\n return { claimed: null, refused: 'not-sold' as const }\n }\n const invoicedUsd = money(usageSnapshot.get('overageInvoicedUsd'))\n const unbilledUsd = floorToCent(priced.overageMonthlyUsd - invoicedUsd)\n if (!(unbilledUsd >= floorUsd)) {\n return { claimed: null, refused: 'below-threshold' as const }\n }\n const seq = Math.max(0, Math.floor(money(usageSnapshot.get('overageCharges')))) + 1\n const chargeId = aiOverageChargeId(request.month, seq)\n const claim: AiOverageClaim = {\n chargeId,\n orgId: request.orgId,\n month: request.month,\n seq,\n kind: request.kind,\n amountUsd: unbilledUsd,\n credits: priced.overageCredits,\n rateUsdPer1k: priced.overageRateUsd,\n }\n tx.set(\n usageRef,\n {\n month: request.month,\n overageInvoicedUsd: FieldValue.increment(unbilledUsd),\n overageCharges: FieldValue.increment(1),\n overageInvoiceOpen: {\n chargeId,\n invoiceId: null,\n amountUsd: unbilledUsd,\n since: now.toISOString(),\n },\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n tx.set(\n orgRef.collection(AI_OVERAGE_CHARGES_SUBCOLLECTION).doc(chargeId),\n {\n month: request.month,\n seq,\n kind: request.kind,\n amountUsd: unbilledUsd,\n credits: priced.overageCredits,\n rateUsdPer1k: priced.overageRateUsd,\n invoiceId: null,\n status: 'claimed' satisfies AiOverageChargeStatus,\n createdAt: FieldValue.serverTimestamp(),\n paidAt: null,\n failedAt: null,\n },\n { merge: true },\n )\n return { claimed: claim, refused: null }\n })\n}\n\n/** What Stripe said, as the ledger records it. */\nexport interface AiOverageSettlement {\n chargeId: string\n month: string\n /** `null` only when no invoice was ever created. */\n invoiceId: string | null\n status: AiOverageChargeStatus\n /**\n * What Stripe actually collected, in dollars (AGL-3023).\n *\n * The month's paid total moves by THIS and never by the claim. The two are\n * normally equal, and the case where they are not is the one that matters:\n * an invoice that finalized at zero or collected less than it billed would\n * otherwise clear a balance nobody paid, and the gate's unpaid bound would\n * be satisfied by our own bookkeeping instead of by money.\n *\n * Omitted on a settlement that is not a payment, where it is not read.\n */\n paidUsd?: number\n}\n\n/**\n * Records what became of a claimed charge.\n *\n * `paid` adds the amount to the month's `overagePaidUsd`, which is what the\n * gate's unpaid bound subtracts — so paying is what lets a workspace keep\n * going, and nothing else does.\n *\n * An invoice that exists but is not paid KEEPS its claim on\n * `overageInvoicedUsd`: it is a real bill, Stripe's retries may still settle\n * it, and re-billing the same dollars on a second invoice would charge the\n * customer twice for one month's usage. Only a claim that never became an\n * invoice at all releases what it claimed, so those dollars can be charged\n * by a later attempt.\n *\n * The open-charge slot is always cleared. Leaving it set on a failure would\n * block the close-out and every later charge behind an invoice that is\n * never going to move on its own.\n */\nexport async function settleAiOverageCharge(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n settlement: AiOverageSettlement,\n): Promise<void> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const usageRef = orgRef.collection('assistUsage').doc(settlement.month)\n const chargeRef = orgRef\n .collection(AI_OVERAGE_CHARGES_SUBCOLLECTION)\n .doc(settlement.chargeId)\n\n await firestore.runTransaction(async (tx) => {\n const [usageSnapshot, chargeSnapshot] = await Promise.all([\n tx.get(usageRef),\n tx.get(chargeRef),\n ])\n const amountUsd = money(chargeSnapshot.get('amountUsd'))\n // What the month's paid total moves by: what Stripe took. A settlement\n // that claims to be a payment and names no collected figure moves\n // nothing — silence is not evidence of money (AGL-3023).\n const paidUsd = money(settlement.paidUsd)\n const open = usageSnapshot.get('overageInvoiceOpen') as\n | { chargeId?: unknown }\n | null\n | undefined\n const ownsTheSlot = String(open?.chargeId ?? '') === settlement.chargeId\n // Already settled, or settled by another path (the webhook and the\n // synchronous answer both arrive). Recording the status again is\n // harmless; moving the money counters again is not.\n const alreadyPaid = chargeSnapshot.get('status') === 'paid'\n const releases = settlement.status === 'failed' && !settlement.invoiceId\n tx.set(\n chargeRef,\n {\n invoiceId: settlement.invoiceId,\n status: settlement.status,\n ...(settlement.status === 'paid' ? { paidUsd } : {}),\n ...(settlement.status === 'paid'\n ? { paidAt: FieldValue.serverTimestamp() }\n : {}),\n ...(settlement.status === 'failed'\n ? { failedAt: FieldValue.serverTimestamp() }\n : {}),\n },\n { merge: true },\n )\n tx.set(\n usageRef,\n {\n month: settlement.month,\n ...(settlement.status === 'paid' && !alreadyPaid && paidUsd > 0\n ? { overagePaidUsd: FieldValue.increment(paidUsd) }\n : {}),\n ...(releases\n ? {\n overageInvoicedUsd: FieldValue.increment(-amountUsd),\n overageCharges: FieldValue.increment(-1),\n }\n : {}),\n ...(ownsTheSlot ? { overageInvoiceOpen: null } : {}),\n updatedAt: FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n })\n}\n\n/** The month's charge counters, as the customer card and staff card read them. */\nexport interface AiOverageMonthLedger {\n invoicedUsd: number\n paidUsd: number\n charges: number\n open: { chargeId: string; invoiceId: string | null; amountUsd: number } | null\n}\n\n/** Reads the four fields off a month's rollup, defensively. */\nexport function readAiOverageMonthLedger(\n data: Record<string, unknown> | null | undefined,\n): AiOverageMonthLedger {\n const open = data?.['overageInvoiceOpen'] as Record<string, unknown> | null | undefined\n const chargeId = typeof open?.['chargeId'] === 'string' ? open['chargeId'] : ''\n return {\n invoicedUsd: money(data?.['overageInvoicedUsd']),\n paidUsd: money(data?.['overagePaidUsd']),\n charges: Math.max(0, Math.floor(money(data?.['overageCharges']))),\n open: chargeId\n ? {\n chargeId,\n invoiceId:\n typeof open?.['invoiceId'] === 'string' ? (open['invoiceId'] as string) : null,\n amountUsd: money(open?.['amountUsd']),\n }\n : null,\n }\n}\n"],"names":["FieldValue","assistMonthOverage","ASSIST_RETURNED_USD_FIELD","assistSpendAfterReturnsUsd","AI_OVERAGE_MIN_CHARGE_USD","AI_OVERAGE_THRESHOLD_USD","aiOverageCardOnFile","readAiOverageStanding","AI_BILLING_SUBCOLLECTION","AI_BILLING_STANDING_DOC","AI_OVERAGE_CHARGES_SUBCOLLECTION","money","value","number","Number","isFinite","floorToCent","usd","Math","floor","aiOverageChargeId","month","seq","String","max","padStart","claimAiOverageCharge","firestore","request","now","Date","orgRef","collection","doc","orgId","usageRef","standingRef","floorUsd","kind","runTransaction","tx","standingSnapshot","usageSnapshot","Promise","all","get","standing","exists","data","claimed","refused","pause","priced","org","overageRateUsd","invoicedUsd","unbilledUsd","overageMonthlyUsd","chargeId","claim","amountUsd","credits","overageCredits","rateUsdPer1k","set","overageInvoicedUsd","increment","overageCharges","overageInvoiceOpen","invoiceId","since","toISOString","updatedAt","serverTimestamp","merge","status","createdAt","paidAt","failedAt","settleAiOverageCharge","settlement","chargeRef","chargeSnapshot","paidUsd","open","ownsTheSlot","alreadyPaid","releases","overagePaidUsd","readAiOverageMonthLedger","charges"],"mappings":"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"}
|
|
@@ -14,7 +14,7 @@ export interface AiBriefCopy {
|
|
|
14
14
|
next: string;
|
|
15
15
|
/** The action that starts the job. */
|
|
16
16
|
submit: string;
|
|
17
|
-
/** What the dialog says once the job exists. */
|
|
17
|
+
/** What the dialog says once the job exists, above its live status. */
|
|
18
18
|
started: string;
|
|
19
19
|
/** What it says when the door gave no reason, or could not be reached. */
|
|
20
20
|
failed: string;
|
|
@@ -51,6 +51,10 @@ export interface AiBriefDialogProps {
|
|
|
51
51
|
hostId: string;
|
|
52
52
|
/** Who is signed in, whose token the request carries. */
|
|
53
53
|
user: Parameters<typeof authorizedFetch>[0];
|
|
54
|
+
/** Path slug for the link to the draft; read from the address when omitted. */
|
|
55
|
+
orgSlug?: string | null;
|
|
56
|
+
/** The viewer is staff (AGL-3078), for a refused plan's details. */
|
|
57
|
+
isStaff?: boolean;
|
|
54
58
|
}
|
|
55
|
-
export declare function AiBriefDialog({ kind, open, onClose, orgId, hostId, user }: AiBriefDialogProps): import("react").JSX.Element;
|
|
59
|
+
export declare function AiBriefDialog({ kind, open, onClose, orgId, hostId, user, orgSlug, isStaff, }: AiBriefDialogProps): import("react").JSX.Element;
|
|
56
60
|
export default AiBriefDialog;
|
|
@@ -22,6 +22,8 @@ import { Alert, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle,
|
|
|
22
22
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
23
23
|
import { AI_PAGE_TYPES } from "../model/ai-page-job.js";
|
|
24
24
|
import { AI_TEMPLATE_SUBJECTS, parseAiTemplateJobInputs } from "../model/ai-template-subjects.js";
|
|
25
|
+
import { AiJobFollow } from "./ai-job-follow.component.js";
|
|
26
|
+
import { publishAiJob } from "./ai-jobs-store.js";
|
|
25
27
|
import { AiTemplateCollectionField } from "./ai-template-collection-field.component.js";
|
|
26
28
|
/**
|
|
27
29
|
* The brief dialog every "Describe it" opens (AGL-2907, AGL-3043, AGL-3051):
|
|
@@ -32,9 +34,11 @@ import { AiTemplateCollectionField } from "./ai-template-collection-field.compon
|
|
|
32
34
|
* One text box, and whatever the kind's door reads beside it, start one job of
|
|
33
35
|
* that kind: a page's optional page type; a template's subject and, for a
|
|
34
36
|
* collection entry's page, its collection. A layout, a form and a component
|
|
35
|
-
* read nothing but the brief and the site. The job plans first
|
|
36
|
-
*
|
|
37
|
-
*
|
|
37
|
+
* read nothing but the brief and the site. The job plans first; once it
|
|
38
|
+
* exists the dialog follows it live (AGL-3593) — planning, the plan with its
|
|
39
|
+
* Confirm, building, then the draft to open — and "Open AI jobs" follows it
|
|
40
|
+
* anywhere else in the console. Nothing here builds or writes anything until
|
|
41
|
+
* the member confirms the plan.
|
|
38
42
|
*/ /** The longest brief a job admits. */ const BRIEF_MAX_CHARS = 4000;
|
|
39
43
|
export const AI_BRIEF_COPY = {
|
|
40
44
|
page: {
|
|
@@ -43,7 +47,7 @@ export const AI_BRIEF_COPY = {
|
|
|
43
47
|
placeholder: 'A landing page for our spring roof inspection offer, with the quote request form',
|
|
44
48
|
next: 'A plan comes first, and nothing is built until you confirm it. The page uses your ' + 'theme and what the site already has, and the plan lists anything the job builds first.',
|
|
45
49
|
submit: 'Plan the page',
|
|
46
|
-
started: 'The page is being planned.
|
|
50
|
+
started: 'The page is being planned. Review the plan here or in AI jobs, and confirm it to ' + 'build. The page is built as an unpublished draft.',
|
|
47
51
|
failed: 'The page could not be started. Try again.'
|
|
48
52
|
},
|
|
49
53
|
template: {
|
|
@@ -52,7 +56,7 @@ export const AI_BRIEF_COPY = {
|
|
|
52
56
|
placeholder: 'A page for each of our team members: their name and photo, their role, a short ' + 'biography and a way to get in touch',
|
|
53
57
|
next: 'A plan comes first, and nothing is built until you confirm it. What changes from one ' + 'page to the next, such as a name or a photo, is filled in from each record rather ' + 'than typed.',
|
|
54
58
|
submit: 'Plan the template',
|
|
55
|
-
started: 'The template is being planned.
|
|
59
|
+
started: 'The template is being planned. Review the plan here or in AI jobs, and confirm it ' + 'to build. The template is built as a draft in your library, and nothing on your ' + 'site uses it until you do.',
|
|
56
60
|
failed: 'The template could not be started. Try again.'
|
|
57
61
|
},
|
|
58
62
|
layout: {
|
|
@@ -61,7 +65,7 @@ export const AI_BRIEF_COPY = {
|
|
|
61
65
|
placeholder: 'A header with our name and links to Services, About and Contact, and a footer with ' + 'our address and opening hours',
|
|
62
66
|
next: 'A plan comes first, and nothing is built until you confirm it. The layout links your ' + 'pages, and places your navigation or menu component when the site has one.',
|
|
63
67
|
submit: 'Plan the layout',
|
|
64
|
-
started: 'The layout is being planned.
|
|
68
|
+
started: 'The layout is being planned. Review the plan here or in AI jobs, and confirm it to ' + 'build. The layout is built as a draft, and no page uses it until you assign it.',
|
|
65
69
|
failed: 'The layout could not be started. Try again.'
|
|
66
70
|
},
|
|
67
71
|
form: {
|
|
@@ -70,7 +74,7 @@ export const AI_BRIEF_COPY = {
|
|
|
70
74
|
placeholder: 'A quote request form for a roofing company, asking what needs fixing and the best ' + 'time to call',
|
|
71
75
|
next: 'A plan comes first, and nothing is built until you confirm it. The form’s fields, its ' + 'marketing consent and where each submission goes are planned together.',
|
|
72
76
|
submit: 'Plan the form',
|
|
73
|
-
started: 'The form is being planned.
|
|
77
|
+
started: 'The form is being planned. Review the plan here or in AI jobs, and confirm it to ' + 'build. The form is built as a draft, and it collects nothing until you place it on ' + 'a page.',
|
|
74
78
|
failed: 'The form could not be started. Try again.'
|
|
75
79
|
},
|
|
76
80
|
component: {
|
|
@@ -79,7 +83,7 @@ export const AI_BRIEF_COPY = {
|
|
|
79
83
|
placeholder: 'A testimonial card with the customer’s name, their role, their quote and a photo',
|
|
80
84
|
next: 'A plan comes first, and nothing is built until you confirm it. What each page can ' + 'change, such as a name or a photo, becomes a property of the component.',
|
|
81
85
|
submit: 'Plan the component',
|
|
82
|
-
started: 'The component is being planned.
|
|
86
|
+
started: 'The component is being planned. Review the plan here or in AI jobs, and confirm it ' + 'to build. The component is built as a draft, and no page shows it until you insert ' + 'it.',
|
|
83
87
|
failed: 'The component could not be started. Try again.'
|
|
84
88
|
}
|
|
85
89
|
};
|
|
@@ -120,7 +124,7 @@ export const AI_BRIEF_NO_CHOICE = {
|
|
|
120
124
|
}
|
|
121
125
|
return {};
|
|
122
126
|
}
|
|
123
|
-
export function AiBriefDialog({ kind, open, onClose, orgId, hostId, user }) {
|
|
127
|
+
export function AiBriefDialog({ kind, open, onClose, orgId, hostId, user, orgSlug, isStaff }) {
|
|
124
128
|
const copy = AI_BRIEF_COPY[kind];
|
|
125
129
|
// Held in a ref so the request reads WHO is signed in, and nothing keys on
|
|
126
130
|
// the identity of the object that says so.
|
|
@@ -130,11 +134,11 @@ export function AiBriefDialog({ kind, open, onClose, orgId, hostId, user }) {
|
|
|
130
134
|
const [choice, setChoice] = useState(AI_BRIEF_NO_CHOICE);
|
|
131
135
|
const [busy, setBusy] = useState(false);
|
|
132
136
|
const [notice, setNotice] = useState(null);
|
|
133
|
-
const [started, setStarted] = useState(
|
|
137
|
+
/** The job the brief started, as the create door answered with it. */ const [started, setStarted] = useState(null);
|
|
134
138
|
useEffect(()=>{
|
|
135
139
|
if (!open) return;
|
|
136
140
|
setNotice(null);
|
|
137
|
-
setStarted(
|
|
141
|
+
setStarted(null);
|
|
138
142
|
}, [
|
|
139
143
|
open
|
|
140
144
|
]);
|
|
@@ -146,6 +150,7 @@ export function AiBriefDialog({ kind, open, onClose, orgId, hostId, user }) {
|
|
|
146
150
|
setBusy(true);
|
|
147
151
|
setNotice(null);
|
|
148
152
|
try {
|
|
153
|
+
var _ref;
|
|
149
154
|
const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {
|
|
150
155
|
method: 'POST',
|
|
151
156
|
headers: {
|
|
@@ -161,12 +166,19 @@ export function AiBriefDialog({ kind, open, onClose, orgId, hostId, user }) {
|
|
|
161
166
|
});
|
|
162
167
|
const payload = await response.json().catch(()=>null);
|
|
163
168
|
if (!response.ok) {
|
|
164
|
-
var
|
|
169
|
+
var _ref1;
|
|
165
170
|
const locked = parseLockdownRefusal(response.status, payload);
|
|
166
|
-
setNotice(locked ? lockdownRefusalText(locked) : String((
|
|
171
|
+
setNotice(locked ? lockdownRefusalText(locked) : String((_ref1 = payload == null ? void 0 : payload.error) != null ? _ref1 : copy.failed));
|
|
167
172
|
return;
|
|
168
173
|
}
|
|
169
|
-
|
|
174
|
+
const created = (_ref = payload == null ? void 0 : payload.job) != null ? _ref : null;
|
|
175
|
+
if (!created) {
|
|
176
|
+
setNotice(copy.failed);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// The indicator and the launcher count it from the moment it exists.
|
|
180
|
+
publishAiJob(created);
|
|
181
|
+
setStarted(created);
|
|
170
182
|
setBrief('');
|
|
171
183
|
setChoice(AI_BRIEF_NO_CHOICE);
|
|
172
184
|
} catch (unused) {
|
|
@@ -192,9 +204,15 @@ export function AiBriefDialog({ kind, open, onClose, orgId, hostId, user }) {
|
|
|
192
204
|
children: copy.title
|
|
193
205
|
}),
|
|
194
206
|
/*#__PURE__*/ _jsx(DialogContent, {
|
|
195
|
-
children: started ? /*#__PURE__*/ _jsx(
|
|
196
|
-
|
|
197
|
-
|
|
207
|
+
children: started && orgId ? /*#__PURE__*/ _jsx(AiJobFollow, {
|
|
208
|
+
job: started,
|
|
209
|
+
orgId: orgId,
|
|
210
|
+
orgSlug: orgSlug,
|
|
211
|
+
user: user,
|
|
212
|
+
intro: copy.started,
|
|
213
|
+
staff: isStaff,
|
|
214
|
+
// The dialog covers the panel AI jobs opens in.
|
|
215
|
+
onOpenJobs: onClose
|
|
198
216
|
}) : /*#__PURE__*/ _jsxs(Stack, {
|
|
199
217
|
spacing: 2,
|
|
200
218
|
sx: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-brief-dialog.component.tsx"],"sourcesContent":["'use client'\n\n/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n Alert,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { AiJobKind } from '../model/ai-jobs.types'\nimport { AI_PAGE_TYPES, type AiPageType } from '../model/ai-page-job'\nimport {\n AI_TEMPLATE_SUBJECTS,\n parseAiTemplateJobInputs,\n type AiTemplateSubject,\n} from '../model/ai-template-subjects'\nimport { AiTemplateCollectionField } from './ai-template-collection-field.component'\n\n/**\n * The brief dialog every \"Describe it\" opens (AGL-2907, AGL-3043, AGL-3051):\n * a page from the Screens page and from the Assist panel's AI jobs, a page\n * template from Templates, a layout from Layouts, a form from Forms and a\n * reusable component from Components.\n *\n * One text box, and whatever the kind's door reads beside it, start one job of\n * that kind: a page's optional page type; a template's subject and, for a\n * collection entry's page, its collection. A layout, a form and a component\n * read nothing but the brief and the site. The job plans first and waits in\n * AI jobs, where the member confirms the plan and opens the draft once it is\n * built; nothing here builds or writes anything.\n */\n\n/** The longest brief a job admits. */\nconst BRIEF_MAX_CHARS = 4_000\n\n/** The job kinds a member describes from a console page. */\nexport type AiBriefKind = Extract<\n AiJobKind,\n 'page' | 'template' | 'layout' | 'form' | 'component'\n>\n\n/** What the dialog says for one kind. */\nexport interface AiBriefCopy {\n title: string\n /** The brief box's label. */\n briefLabel: string\n placeholder: string\n /** What happens after the member starts the job, under the fields. */\n next: string\n /** The action that starts the job. */\n submit: string\n /** What the dialog says once the job exists. */\n started: string\n /** What it says when the door gave no reason, or could not be reached. */\n failed: string\n}\n\nexport const AI_BRIEF_COPY: Readonly<Record<AiBriefKind, AiBriefCopy>> = {\n page: {\n title: 'Describe a page',\n briefLabel: 'What is the page for?',\n placeholder:\n 'A landing page for our spring roof inspection offer, with the quote request form',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The page uses your ' +\n 'theme and what the site already has, and the plan lists anything the job builds first.',\n submit: 'Plan the page',\n started:\n 'The page is being planned. Open AI jobs in the Assist panel to review the plan and ' +\n 'confirm it. The page is built as an unpublished draft.',\n failed: 'The page could not be started. Try again.',\n },\n template: {\n title: 'Describe a page template',\n briefLabel: 'What should each page show?',\n placeholder:\n 'A page for each of our team members: their name and photo, their role, a short ' +\n 'biography and a way to get in touch',\n next:\n 'A plan comes first, and nothing is built until you confirm it. What changes from one ' +\n 'page to the next, such as a name or a photo, is filled in from each record rather ' +\n 'than typed.',\n submit: 'Plan the template',\n started:\n 'The template is being planned. Open AI jobs in the Assist panel to review the plan ' +\n 'and confirm it. The template is built as a draft in your library, and nothing on ' +\n 'your site uses it until you do.',\n failed: 'The template could not be started. Try again.',\n },\n layout: {\n title: 'Describe a layout',\n briefLabel: 'What should the layout hold?',\n placeholder:\n 'A header with our name and links to Services, About and Contact, and a footer with ' +\n 'our address and opening hours',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The layout links your ' +\n 'pages, and places your navigation or menu component when the site has one.',\n submit: 'Plan the layout',\n started:\n 'The layout is being planned. Open AI jobs in the Assist panel to review the plan and ' +\n 'confirm it. The layout is built as a draft, and no page uses it until you assign it.',\n failed: 'The layout could not be started. Try again.',\n },\n form: {\n title: 'Describe a form',\n briefLabel: 'What is the form for?',\n placeholder:\n 'A quote request form for a roofing company, asking what needs fixing and the best ' +\n 'time to call',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The form’s fields, its ' +\n 'marketing consent and where each submission goes are planned together.',\n submit: 'Plan the form',\n started:\n 'The form is being planned. Open AI jobs in the Assist panel to review the plan and ' +\n 'confirm it. The form is built as a draft, and it collects nothing until you place it ' +\n 'on a page.',\n failed: 'The form could not be started. Try again.',\n },\n component: {\n title: 'Describe a reusable component',\n briefLabel: 'What should the component show?',\n placeholder:\n 'A testimonial card with the customer’s name, their role, their quote and a photo',\n next:\n 'A plan comes first, and nothing is built until you confirm it. What each page can ' +\n 'change, such as a name or a photo, becomes a property of the component.',\n submit: 'Plan the component',\n started:\n 'The component is being planned. Open AI jobs in the Assist panel to review the plan ' +\n 'and confirm it. The component is built as a draft, and no page shows it until you ' +\n 'insert it.',\n failed: 'The component could not be started. Try again.',\n },\n}\n\n/** Each template subject as its chip names it: what one page is drawn for. */\nexport const AI_TEMPLATE_SUBJECT_LABELS: Readonly<Record<AiTemplateSubject, string>> = {\n entry: 'Collection entry',\n product: 'Product',\n author: 'Author',\n}\n\n/** What a member picked beside the brief; each kind reads only its own. */\nexport interface AiBriefChoice {\n /** A page's type, or `null` for none. */\n pageType: AiPageType | null\n /** What a template's pages are drawn for, or `null` until one is picked. */\n subject: AiTemplateSubject | null\n /** A collection entry template's collection, or `null` until one is picked. */\n collectionId: string | null\n}\n\nexport const AI_BRIEF_NO_CHOICE: AiBriefChoice = {\n pageType: null,\n subject: null,\n collectionId: null,\n}\n\n/**\n * The job's `inputs` for what the member picked, or `null` while the kind's\n * door would refuse them.\n *\n * A template's are read by the parser its door reads, so the dialog cannot\n * send a subject the door does not know or an entry page with no collection.\n * A page's type is optional. A layout, a form and a component read no inputs\n * at all (AGL-2909, AGL-2913, AGL-2908), so theirs are empty.\n */\nexport function aiBriefJobInputs(\n kind: AiBriefKind,\n choice: AiBriefChoice,\n): Record<string, string> | null {\n if (kind === 'page') return choice.pageType ? { pageType: choice.pageType } : {}\n if (kind === 'template') {\n const inputs = parseAiTemplateJobInputs({\n subject: choice.subject,\n collectionId: choice.collectionId,\n })\n if (typeof inputs === 'string') return null\n return inputs.collectionId\n ? { subject: inputs.subject, collectionId: inputs.collectionId }\n : { subject: inputs.subject }\n }\n return {}\n}\n\nexport interface AiBriefDialogProps {\n /** The job the brief starts. */\n kind: AiBriefKind\n open: boolean\n onClose: () => void\n orgId: string | undefined\n hostId: string\n /** Who is signed in, whose token the request carries. */\n user: Parameters<typeof authorizedFetch>[0]\n}\n\nexport function AiBriefDialog({ kind, open, onClose, orgId, hostId, user }: AiBriefDialogProps) {\n const copy = AI_BRIEF_COPY[kind]\n // Held in a ref so the request reads WHO is signed in, and nothing keys on\n // the identity of the object that says so.\n const userRef = useRef(user)\n userRef.current = user\n const [brief, setBrief] = useState('')\n const [choice, setChoice] = useState<AiBriefChoice>(AI_BRIEF_NO_CHOICE)\n const [busy, setBusy] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n const [started, setStarted] = useState(false)\n\n useEffect(() => {\n if (!open) return\n setNotice(null)\n setStarted(false)\n }, [open])\n\n const pick = useCallback(\n (patch: Partial<AiBriefChoice>) => setChoice((current) => ({ ...current, ...patch })),\n [],\n )\n const ready = aiBriefJobInputs(kind, choice) !== null\n\n const start = useCallback(async () => {\n const inputs = aiBriefJobInputs(kind, choice)\n if (!orgId || !brief.trim() || !inputs) return\n setBusy(true)\n setNotice(null)\n try {\n const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, hostId, kind, brief: brief.trim(), inputs }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(locked ? lockdownRefusalText(locked) : String(payload?.error ?? copy.failed))\n return\n }\n setStarted(true)\n setBrief('')\n setChoice(AI_BRIEF_NO_CHOICE)\n } catch {\n setNotice(copy.failed)\n } finally {\n setBusy(false)\n }\n }, [orgId, hostId, kind, brief, choice, copy])\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} fullWidth maxWidth=\"sm\">\n <DialogTitle>{copy.title}</DialogTitle>\n <DialogContent>\n {started ? (\n <Alert severity=\"success\">{copy.started}</Alert>\n ) : (\n <Stack spacing={2} sx={{ pt: 1 }}>\n <TextField\n label={copy.briefLabel}\n placeholder={copy.placeholder}\n multiline\n minRows={4}\n value={brief}\n onChange={(event) => setBrief(event.target.value.slice(0, BRIEF_MAX_CHARS))}\n helperText={`${brief.length.toLocaleString('en-US')} / ${BRIEF_MAX_CHARS.toLocaleString('en-US')}`}\n disabled={busy}\n autoFocus\n />\n {kind === 'page' ? (\n <ChipGroup\n id=\"ai-page-type-label\"\n label=\"Page type (optional)\"\n options={AI_PAGE_TYPES.map((type) => ({ id: type.id, label: type.label }))}\n value={choice.pageType}\n // Optional: pressing the pressed type again sends none.\n onPress={(id) =>\n pick({ pageType: choice.pageType === id ? null : (id as AiPageType) })\n }\n disabled={busy}\n />\n ) : null}\n {kind === 'template' ? (\n <>\n <ChipGroup\n id=\"ai-template-subject-label\"\n // Marked required because it IS (AGL-3143 §12): \"Plan the\n // template\" stays disabled until one is picked, and the page\n // dialog's chip group beside it reads \"(optional)\" — so an\n // unmarked label here read as optional too, and a member who\n // wrote a brief and stopped had nothing telling them why the\n // button would not light.\n label=\"One page for each (required)\"\n options={AI_TEMPLATE_SUBJECTS.map((subject) => ({\n id: subject,\n label: AI_TEMPLATE_SUBJECT_LABELS[subject],\n }))}\n value={choice.subject}\n // Required, so a press picks and never clears.\n onPress={(id) => pick({ subject: id as AiTemplateSubject })}\n disabled={busy}\n />\n {choice.subject === 'entry' ? (\n <AiTemplateCollectionField\n hostId={hostId}\n value={choice.collectionId}\n onChange={(collectionId) => pick({ collectionId })}\n disabled={busy}\n />\n ) : null}\n </>\n ) : null}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {copy.next}\n </Typography>\n {notice ? <Alert severity=\"warning\">{notice}</Alert> : null}\n </Stack>\n )}\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {started ? 'Close' : 'Cancel'}\n </Button>\n {started ? null : (\n <Button\n variant=\"contained\"\n onClick={() => void start()}\n disabled={busy || !orgId || !brief.trim() || !ready}\n >\n {copy.submit}\n </Button>\n )}\n </DialogActions>\n </Dialog>\n )\n}\n\n/** A labeled row of chips, each a toggle button that says whether it is pressed. */\nfunction ChipGroup(props: {\n id: string\n label: string\n options: ReadonlyArray<{ id: string; label: string }>\n value: string | null\n onPress: (id: string) => void\n disabled: boolean\n}) {\n const { id, label, options, value, onPress, disabled } = props\n return (\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\" id={id}>\n {label}\n </Typography>\n <Stack\n direction=\"row\"\n useFlexGap\n role=\"group\"\n aria-labelledby={id}\n sx={{ flexWrap: 'wrap', gap: 1 }}\n >\n {options.map((option) => (\n <Chip\n key={option.id}\n label={option.label}\n color={value === option.id ? 'primary' : 'default'}\n variant={value === option.id ? 'filled' : 'outlined'}\n onClick={() => onPress(option.id)}\n aria-pressed={value === option.id}\n disabled={disabled}\n />\n ))}\n </Stack>\n </Stack>\n )\n}\n\nexport default AiBriefDialog\n"],"names":["lockdownRefusalText","parseLockdownRefusal","authorizedFetch","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","useCallback","useEffect","useRef","useState","AI_PAGE_TYPES","AI_TEMPLATE_SUBJECTS","parseAiTemplateJobInputs","AiTemplateCollectionField","BRIEF_MAX_CHARS","AI_BRIEF_COPY","page","title","briefLabel","placeholder","next","submit","started","failed","template","layout","form","component","AI_TEMPLATE_SUBJECT_LABELS","entry","product","author","AI_BRIEF_NO_CHOICE","pageType","subject","collectionId","aiBriefJobInputs","kind","choice","inputs","AiBriefDialog","open","onClose","orgId","hostId","user","copy","userRef","current","brief","setBrief","setChoice","busy","setBusy","notice","setNotice","setStarted","pick","patch","ready","start","trim","response","method","headers","body","JSON","stringify","payload","json","catch","ok","locked","status","String","error","undefined","fullWidth","maxWidth","severity","spacing","sx","pt","label","multiline","minRows","value","onChange","event","target","slice","helperText","length","toLocaleString","disabled","autoFocus","ChipGroup","id","options","map","type","onPress","variant","color","onClick","props","direction","useFlexGap","role","aria-labelledby","flexWrap","gap","option","aria-pressed"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-brief-dialog.component.tsx"],"sourcesContent":["'use client'\n\n/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n Alert,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { AiJobKind, AiJobSummary } from '../model/ai-jobs.types'\nimport { AI_PAGE_TYPES, type AiPageType } from '../model/ai-page-job'\nimport {\n AI_TEMPLATE_SUBJECTS,\n parseAiTemplateJobInputs,\n type AiTemplateSubject,\n} from '../model/ai-template-subjects'\nimport { AiJobFollow } from './ai-job-follow.component'\nimport { publishAiJob } from './ai-jobs-store'\nimport { AiTemplateCollectionField } from './ai-template-collection-field.component'\n\n/**\n * The brief dialog every \"Describe it\" opens (AGL-2907, AGL-3043, AGL-3051):\n * a page from the Screens page and from the Assist panel's AI jobs, a page\n * template from Templates, a layout from Layouts, a form from Forms and a\n * reusable component from Components.\n *\n * One text box, and whatever the kind's door reads beside it, start one job of\n * that kind: a page's optional page type; a template's subject and, for a\n * collection entry's page, its collection. A layout, a form and a component\n * read nothing but the brief and the site. The job plans first; once it\n * exists the dialog follows it live (AGL-3593) — planning, the plan with its\n * Confirm, building, then the draft to open — and \"Open AI jobs\" follows it\n * anywhere else in the console. Nothing here builds or writes anything until\n * the member confirms the plan.\n */\n\n/** The longest brief a job admits. */\nconst BRIEF_MAX_CHARS = 4_000\n\n/** The job kinds a member describes from a console page. */\nexport type AiBriefKind = Extract<\n AiJobKind,\n 'page' | 'template' | 'layout' | 'form' | 'component'\n>\n\n/** What the dialog says for one kind. */\nexport interface AiBriefCopy {\n title: string\n /** The brief box's label. */\n briefLabel: string\n placeholder: string\n /** What happens after the member starts the job, under the fields. */\n next: string\n /** The action that starts the job. */\n submit: string\n /** What the dialog says once the job exists, above its live status. */\n started: string\n /** What it says when the door gave no reason, or could not be reached. */\n failed: string\n}\n\nexport const AI_BRIEF_COPY: Readonly<Record<AiBriefKind, AiBriefCopy>> = {\n page: {\n title: 'Describe a page',\n briefLabel: 'What is the page for?',\n placeholder:\n 'A landing page for our spring roof inspection offer, with the quote request form',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The page uses your ' +\n 'theme and what the site already has, and the plan lists anything the job builds first.',\n submit: 'Plan the page',\n started:\n 'The page is being planned. Review the plan here or in AI jobs, and confirm it to ' +\n 'build. The page is built as an unpublished draft.',\n failed: 'The page could not be started. Try again.',\n },\n template: {\n title: 'Describe a page template',\n briefLabel: 'What should each page show?',\n placeholder:\n 'A page for each of our team members: their name and photo, their role, a short ' +\n 'biography and a way to get in touch',\n next:\n 'A plan comes first, and nothing is built until you confirm it. What changes from one ' +\n 'page to the next, such as a name or a photo, is filled in from each record rather ' +\n 'than typed.',\n submit: 'Plan the template',\n started:\n 'The template is being planned. Review the plan here or in AI jobs, and confirm it ' +\n 'to build. The template is built as a draft in your library, and nothing on your ' +\n 'site uses it until you do.',\n failed: 'The template could not be started. Try again.',\n },\n layout: {\n title: 'Describe a layout',\n briefLabel: 'What should the layout hold?',\n placeholder:\n 'A header with our name and links to Services, About and Contact, and a footer with ' +\n 'our address and opening hours',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The layout links your ' +\n 'pages, and places your navigation or menu component when the site has one.',\n submit: 'Plan the layout',\n started:\n 'The layout is being planned. Review the plan here or in AI jobs, and confirm it to ' +\n 'build. The layout is built as a draft, and no page uses it until you assign it.',\n failed: 'The layout could not be started. Try again.',\n },\n form: {\n title: 'Describe a form',\n briefLabel: 'What is the form for?',\n placeholder:\n 'A quote request form for a roofing company, asking what needs fixing and the best ' +\n 'time to call',\n next:\n 'A plan comes first, and nothing is built until you confirm it. The form’s fields, its ' +\n 'marketing consent and where each submission goes are planned together.',\n submit: 'Plan the form',\n started:\n 'The form is being planned. Review the plan here or in AI jobs, and confirm it to ' +\n 'build. The form is built as a draft, and it collects nothing until you place it on ' +\n 'a page.',\n failed: 'The form could not be started. Try again.',\n },\n component: {\n title: 'Describe a reusable component',\n briefLabel: 'What should the component show?',\n placeholder:\n 'A testimonial card with the customer’s name, their role, their quote and a photo',\n next:\n 'A plan comes first, and nothing is built until you confirm it. What each page can ' +\n 'change, such as a name or a photo, becomes a property of the component.',\n submit: 'Plan the component',\n started:\n 'The component is being planned. Review the plan here or in AI jobs, and confirm it ' +\n 'to build. The component is built as a draft, and no page shows it until you insert ' +\n 'it.',\n failed: 'The component could not be started. Try again.',\n },\n}\n\n/** Each template subject as its chip names it: what one page is drawn for. */\nexport const AI_TEMPLATE_SUBJECT_LABELS: Readonly<Record<AiTemplateSubject, string>> = {\n entry: 'Collection entry',\n product: 'Product',\n author: 'Author',\n}\n\n/** What a member picked beside the brief; each kind reads only its own. */\nexport interface AiBriefChoice {\n /** A page's type, or `null` for none. */\n pageType: AiPageType | null\n /** What a template's pages are drawn for, or `null` until one is picked. */\n subject: AiTemplateSubject | null\n /** A collection entry template's collection, or `null` until one is picked. */\n collectionId: string | null\n}\n\nexport const AI_BRIEF_NO_CHOICE: AiBriefChoice = {\n pageType: null,\n subject: null,\n collectionId: null,\n}\n\n/**\n * The job's `inputs` for what the member picked, or `null` while the kind's\n * door would refuse them.\n *\n * A template's are read by the parser its door reads, so the dialog cannot\n * send a subject the door does not know or an entry page with no collection.\n * A page's type is optional. A layout, a form and a component read no inputs\n * at all (AGL-2909, AGL-2913, AGL-2908), so theirs are empty.\n */\nexport function aiBriefJobInputs(\n kind: AiBriefKind,\n choice: AiBriefChoice,\n): Record<string, string> | null {\n if (kind === 'page') return choice.pageType ? { pageType: choice.pageType } : {}\n if (kind === 'template') {\n const inputs = parseAiTemplateJobInputs({\n subject: choice.subject,\n collectionId: choice.collectionId,\n })\n if (typeof inputs === 'string') return null\n return inputs.collectionId\n ? { subject: inputs.subject, collectionId: inputs.collectionId }\n : { subject: inputs.subject }\n }\n return {}\n}\n\nexport interface AiBriefDialogProps {\n /** The job the brief starts. */\n kind: AiBriefKind\n open: boolean\n onClose: () => void\n orgId: string | undefined\n hostId: string\n /** Who is signed in, whose token the request carries. */\n user: Parameters<typeof authorizedFetch>[0]\n /** Path slug for the link to the draft; read from the address when omitted. */\n orgSlug?: string | null\n /** The viewer is staff (AGL-3078), for a refused plan's details. */\n isStaff?: boolean\n}\n\nexport function AiBriefDialog({\n kind,\n open,\n onClose,\n orgId,\n hostId,\n user,\n orgSlug,\n isStaff,\n}: AiBriefDialogProps) {\n const copy = AI_BRIEF_COPY[kind]\n // Held in a ref so the request reads WHO is signed in, and nothing keys on\n // the identity of the object that says so.\n const userRef = useRef(user)\n userRef.current = user\n const [brief, setBrief] = useState('')\n const [choice, setChoice] = useState<AiBriefChoice>(AI_BRIEF_NO_CHOICE)\n const [busy, setBusy] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n /** The job the brief started, as the create door answered with it. */\n const [started, setStarted] = useState<AiJobSummary | null>(null)\n\n useEffect(() => {\n if (!open) return\n setNotice(null)\n setStarted(null)\n }, [open])\n\n const pick = useCallback(\n (patch: Partial<AiBriefChoice>) => setChoice((current) => ({ ...current, ...patch })),\n [],\n )\n const ready = aiBriefJobInputs(kind, choice) !== null\n\n const start = useCallback(async () => {\n const inputs = aiBriefJobInputs(kind, choice)\n if (!orgId || !brief.trim() || !inputs) return\n setBusy(true)\n setNotice(null)\n try {\n const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, hostId, kind, brief: brief.trim(), inputs }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(locked ? lockdownRefusalText(locked) : String(payload?.error ?? copy.failed))\n return\n }\n const created = (payload?.job as AiJobSummary | undefined) ?? null\n if (!created) {\n setNotice(copy.failed)\n return\n }\n // The indicator and the launcher count it from the moment it exists.\n publishAiJob(created)\n setStarted(created)\n setBrief('')\n setChoice(AI_BRIEF_NO_CHOICE)\n } catch {\n setNotice(copy.failed)\n } finally {\n setBusy(false)\n }\n }, [orgId, hostId, kind, brief, choice, copy])\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} fullWidth maxWidth=\"sm\">\n <DialogTitle>{copy.title}</DialogTitle>\n <DialogContent>\n {started && orgId ? (\n <AiJobFollow\n job={started}\n orgId={orgId}\n orgSlug={orgSlug}\n user={user}\n intro={copy.started}\n staff={isStaff}\n // The dialog covers the panel AI jobs opens in.\n onOpenJobs={onClose}\n />\n ) : (\n <Stack spacing={2} sx={{ pt: 1 }}>\n <TextField\n label={copy.briefLabel}\n placeholder={copy.placeholder}\n multiline\n minRows={4}\n value={brief}\n onChange={(event) => setBrief(event.target.value.slice(0, BRIEF_MAX_CHARS))}\n helperText={`${brief.length.toLocaleString('en-US')} / ${BRIEF_MAX_CHARS.toLocaleString('en-US')}`}\n disabled={busy}\n autoFocus\n />\n {kind === 'page' ? (\n <ChipGroup\n id=\"ai-page-type-label\"\n label=\"Page type (optional)\"\n options={AI_PAGE_TYPES.map((type) => ({ id: type.id, label: type.label }))}\n value={choice.pageType}\n // Optional: pressing the pressed type again sends none.\n onPress={(id) =>\n pick({ pageType: choice.pageType === id ? null : (id as AiPageType) })\n }\n disabled={busy}\n />\n ) : null}\n {kind === 'template' ? (\n <>\n <ChipGroup\n id=\"ai-template-subject-label\"\n // Marked required because it IS (AGL-3143 §12): \"Plan the\n // template\" stays disabled until one is picked, and the page\n // dialog's chip group beside it reads \"(optional)\" — so an\n // unmarked label here read as optional too, and a member who\n // wrote a brief and stopped had nothing telling them why the\n // button would not light.\n label=\"One page for each (required)\"\n options={AI_TEMPLATE_SUBJECTS.map((subject) => ({\n id: subject,\n label: AI_TEMPLATE_SUBJECT_LABELS[subject],\n }))}\n value={choice.subject}\n // Required, so a press picks and never clears.\n onPress={(id) => pick({ subject: id as AiTemplateSubject })}\n disabled={busy}\n />\n {choice.subject === 'entry' ? (\n <AiTemplateCollectionField\n hostId={hostId}\n value={choice.collectionId}\n onChange={(collectionId) => pick({ collectionId })}\n disabled={busy}\n />\n ) : null}\n </>\n ) : null}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {copy.next}\n </Typography>\n {notice ? <Alert severity=\"warning\">{notice}</Alert> : null}\n </Stack>\n )}\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {started ? 'Close' : 'Cancel'}\n </Button>\n {started ? null : (\n <Button\n variant=\"contained\"\n onClick={() => void start()}\n disabled={busy || !orgId || !brief.trim() || !ready}\n >\n {copy.submit}\n </Button>\n )}\n </DialogActions>\n </Dialog>\n )\n}\n\n/** A labeled row of chips, each a toggle button that says whether it is pressed. */\nfunction ChipGroup(props: {\n id: string\n label: string\n options: ReadonlyArray<{ id: string; label: string }>\n value: string | null\n onPress: (id: string) => void\n disabled: boolean\n}) {\n const { id, label, options, value, onPress, disabled } = props\n return (\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\" id={id}>\n {label}\n </Typography>\n <Stack\n direction=\"row\"\n useFlexGap\n role=\"group\"\n aria-labelledby={id}\n sx={{ flexWrap: 'wrap', gap: 1 }}\n >\n {options.map((option) => (\n <Chip\n key={option.id}\n label={option.label}\n color={value === option.id ? 'primary' : 'default'}\n variant={value === option.id ? 'filled' : 'outlined'}\n onClick={() => onPress(option.id)}\n aria-pressed={value === option.id}\n disabled={disabled}\n />\n ))}\n </Stack>\n </Stack>\n )\n}\n\nexport default AiBriefDialog\n"],"names":["lockdownRefusalText","parseLockdownRefusal","authorizedFetch","Alert","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","useCallback","useEffect","useRef","useState","AI_PAGE_TYPES","AI_TEMPLATE_SUBJECTS","parseAiTemplateJobInputs","AiJobFollow","publishAiJob","AiTemplateCollectionField","BRIEF_MAX_CHARS","AI_BRIEF_COPY","page","title","briefLabel","placeholder","next","submit","started","failed","template","layout","form","component","AI_TEMPLATE_SUBJECT_LABELS","entry","product","author","AI_BRIEF_NO_CHOICE","pageType","subject","collectionId","aiBriefJobInputs","kind","choice","inputs","AiBriefDialog","open","onClose","orgId","hostId","user","orgSlug","isStaff","copy","userRef","current","brief","setBrief","setChoice","busy","setBusy","notice","setNotice","setStarted","pick","patch","ready","start","trim","response","method","headers","body","JSON","stringify","payload","json","catch","ok","locked","status","String","error","created","job","undefined","fullWidth","maxWidth","intro","staff","onOpenJobs","spacing","sx","pt","label","multiline","minRows","value","onChange","event","target","slice","helperText","length","toLocaleString","disabled","autoFocus","ChipGroup","id","options","map","type","onPress","variant","color","severity","onClick","props","direction","useFlexGap","role","aria-labelledby","flexWrap","gap","option","aria-pressed"],"mappings":"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"}
|
|
@@ -19,6 +19,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
19
19
|
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
20
20
|
import { Alert, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, Link, Stack, TextField, Typography } from "@mui/material";
|
|
21
21
|
import { useEffect, useState } from "react";
|
|
22
|
+
import { openAiJobs } from "./ai-jobs-store.js";
|
|
22
23
|
import { aiJobProblem, useAiJobRun, useAiJobsVerdict } from "./use-ai-job-run.js";
|
|
23
24
|
/**
|
|
24
25
|
* "Describe it" on the Automation page's Actions (AGL-2919), beside Add action
|
|
@@ -36,7 +37,7 @@ export const AI_AUTOMATION_BRIEF_COPY = {
|
|
|
36
37
|
placeholder: 'When someone submits the newsletter form, add them to the newsletter list, make them ' + 'a lead and send them a welcome email',
|
|
37
38
|
next: 'The automation is drafted switched off, using only the triggers and steps your plan ' + 'includes. Anything it needs that your site does not have yet is left for you to fill in.',
|
|
38
39
|
submit: 'Draft the automation',
|
|
39
|
-
started: 'The automation is being drafted. This can take a few minutes. You can close this ' + 'window: the draft appears in the list, switched off, when it is ready.',
|
|
40
|
+
started: 'The automation is being drafted. This can take a few minutes. You can close this ' + 'window: the draft appears in the list, switched off, when it is ready. Open AI ' + 'jobs to follow it meanwhile.',
|
|
40
41
|
drafted: 'Drafted',
|
|
41
42
|
review: 'Review it',
|
|
42
43
|
notListed: 'The draft is saved. It appears in the list in a moment.',
|
|
@@ -137,12 +138,26 @@ export function AiAutomationBriefDialog(props) {
|
|
|
137
138
|
pt: 1
|
|
138
139
|
},
|
|
139
140
|
children: [
|
|
140
|
-
running ? /*#__PURE__*/ _jsx(Alert, {
|
|
141
|
+
running && job ? /*#__PURE__*/ _jsx(Alert, {
|
|
141
142
|
severity: "info",
|
|
142
143
|
icon: /*#__PURE__*/ _jsx(CircularProgress, {
|
|
143
144
|
size: 18,
|
|
144
145
|
"aria-label": "Drafting"
|
|
145
146
|
}),
|
|
147
|
+
action: // Follows the job in the Assist panel (AGL-3593); the dialog
|
|
148
|
+
// covers it, so it closes.
|
|
149
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
150
|
+
color: "inherit",
|
|
151
|
+
size: "small",
|
|
152
|
+
variant: "outlined",
|
|
153
|
+
onClick: ()=>{
|
|
154
|
+
openAiJobs({
|
|
155
|
+
jobId: job.id
|
|
156
|
+
});
|
|
157
|
+
onClose();
|
|
158
|
+
},
|
|
159
|
+
children: 'Open AI jobs'
|
|
160
|
+
}),
|
|
146
161
|
children: copy.started
|
|
147
162
|
}) : null,
|
|
148
163
|
draft ? /*#__PURE__*/ _jsxs(Alert, {
|