@aglyn/plugins-ai 1.0.0-beta.284 → 1.0.0-beta.287
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/package.json +17 -17
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/components/ai-theme-proposal-card.component.js +12 -3
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/declarations.js +5 -2
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +12 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.d.ts +3 -1
- package/src/lib/jobs/ai-build-capabilities.js +27 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-image-reads.d.ts +59 -0
- package/src/lib/jobs/ai-image-reads.js +138 -0
- package/src/lib/jobs/ai-image-reads.js.map +1 -0
- package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
- package/src/lib/jobs/ai-job-build-step.js +83 -12
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-import-step.js +15 -11
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
- package/src/lib/jobs/ai-job-insight-step.js +23 -5
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +2 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-media-step.js +6 -8
- package/src/lib/jobs/ai-job-media-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-navigation-step.js +7 -9
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.js +7 -1
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +16 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +9 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
- package/src/lib/jobs/ai-job-plan-step.js +78 -13
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-privacy-step.js +14 -15
- package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.js +5 -3
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-settings-step.js +7 -8
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
- package/src/lib/jobs/ai-job-site-content.js +67 -8
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
- package/src/lib/jobs/ai-job-site-datasets.js +298 -30
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +6 -3
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
- package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-tracking-step.js +7 -8
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
- package/src/lib/jobs/ai-jobs-beat.js +18 -8
- package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
- package/src/lib/jobs/ai-jobs-notify.js +136 -3
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +59 -4
- package/src/lib/jobs/ai-layout-stock-photos.js +897 -81
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-product-image.d.ts +14 -0
- package/src/lib/jobs/ai-product-image.js +73 -21
- package/src/lib/jobs/ai-product-image.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
- package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
- package/src/lib/layout-language/ai-layout-compiler.js +194 -18
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
- package/src/lib/layout-language/ai-layout-copy.js +383 -3
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
- package/src/lib/layout-language/ai-layout-design.js +31 -11
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
- package/src/lib/layout-language/ai-layout-listings.js +9 -1
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +30 -0
- package/src/lib/layout-language/ai-layout-pictures.js +131 -3
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
- package/src/lib/layout-language/ai-layout-storefront.js +100 -20
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +7 -0
- package/src/lib/model/ai-build-job.js +36 -7
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +28 -4
- package/src/lib/model/ai-build-plan.js +49 -9
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +3 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +8 -0
- package/src/lib/model/ai-job-notice.js +33 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
- package/src/lib/model/ai-job-plan-lines.js +6 -1
- package/src/lib/model/ai-job-plan-lines.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +8 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-permissions.d.ts +13 -0
- package/src/lib/model/ai-permissions.js +29 -1
- package/src/lib/model/ai-permissions.js.map +1 -1
- package/src/lib/model/ai-products.d.ts +18 -0
- package/src/lib/model/ai-products.js +32 -0
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inspiration.d.ts +3 -2
- package/src/lib/model/ai-site-inspiration.js +3 -2
- package/src/lib/model/ai-site-inspiration.js.map +1 -1
- package/src/lib/plugin.js +4 -13
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/providers/anthropic.js +9 -2
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/catalog.js +14 -5
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/contract.d.ts +24 -1
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/routing.js +5 -2
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +99 -1
- package/src/lib/runtime/ai-dataset-generation.js +251 -3
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
- package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
- package/src/lib/runtime/ai-dev-replay.js +4 -2
- package/src/lib/runtime/ai-dev-replay.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
- package/src/lib/runtime/ai-doctrine-validators.js +343 -27
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +27 -1
- package/src/lib/runtime/ai-icon-library.js +30 -3
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +11 -1
- package/src/lib/runtime/ai-products-generation.js +51 -4
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
- package/src/lib/runtime/assist-answer-cache.js +5 -2
- package/src/lib/runtime/assist-answer-cache.js.map +1 -1
- package/src/lib/server/ai-allotments.js +7 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-experiment-versions.js +5 -5
- package/src/lib/server/ai-experiment-versions.js.map +1 -1
- package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
- package/src/lib/server/ai-host-permissions-roster.js +33 -0
- package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
- package/src/lib/server/ai-host-permissions.js +8 -6
- package/src/lib/server/ai-host-permissions.js.map +1 -1
- package/src/lib/server/ai-image-reads-route.d.ts +17 -0
- package/src/lib/server/ai-image-reads-route.js +106 -0
- package/src/lib/server/ai-image-reads-route.js.map +1 -0
- package/src/lib/server/ai-seo-apply.js +5 -5
- package/src/lib/server/ai-seo-apply.js.map +1 -1
- package/src/lib/server/ai-site-import-route.js +7 -4
- package/src/lib/server/ai-site-import-route.js.map +1 -1
- package/src/lib/server/assist-build.js +1 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.d.ts +11 -7
- package/src/lib/server/assist-chat.js +185 -48
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-docs-query.d.ts +51 -0
- package/src/lib/server/assist-docs-query.js +57 -0
- package/src/lib/server/assist-docs-query.js.map +1 -0
- package/src/lib/server/assist-jobs-context.d.ts +102 -0
- package/src/lib/server/assist-jobs-context.js +227 -0
- package/src/lib/server/assist-jobs-context.js.map +1 -0
- package/src/lib/server/assist-view-context.d.ts +8 -4
- package/src/lib/server/assist-view-context.js +12 -7
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.js +12 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
- package/src/lib/tools/ai-dataset-tool.js +24 -7
- package/src/lib/tools/ai-dataset-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
- package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
- package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -1
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.287",
|
|
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.287",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.287",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.287",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.287",
|
|
32
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.287",
|
|
33
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.287",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.287",
|
|
35
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.287",
|
|
36
|
+
"@aglyn/shared-util-email": "1.0.0-beta.287",
|
|
37
|
+
"@aglyn/shared-util-errors": "1.0.0-beta.287",
|
|
38
|
+
"@aglyn/shared-util-http": "1.0.0-beta.287",
|
|
39
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.287",
|
|
40
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.287",
|
|
41
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.287",
|
|
42
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.287",
|
|
43
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.287",
|
|
44
44
|
"@swc/helpers": "0.5.23"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -50,7 +50,15 @@ const SOURCE_COPY = {
|
|
|
50
50
|
default: 'Only these values are stored. Everything else keeps following the default theme.'
|
|
51
51
|
};
|
|
52
52
|
const isTerminal = (job)=>AI_JOB_TERMINAL_STATUSES.includes(job.status);
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The jobs whose theme proposals this card offers: a theme job's, and a
|
|
55
|
+
* build's from the chat that planned a theme change (AGL-4077), which the
|
|
56
|
+
* owner applies here like any other.
|
|
57
|
+
*/ const PROPOSING_KINDS = [
|
|
58
|
+
'theme',
|
|
59
|
+
'build'
|
|
60
|
+
];
|
|
61
|
+
/** The proposal a theme job, or a build's theme change, produced, when it produced one. */ function proposalOf(job) {
|
|
54
62
|
const output = job == null ? void 0 : job.outputs.find((entry)=>entry.resource === 'theme');
|
|
55
63
|
return output ? readAiThemeProposal(output.proposal) : null;
|
|
56
64
|
}
|
|
@@ -151,7 +159,7 @@ export function AiThemeProposalCard(props) {
|
|
|
151
159
|
} else {
|
|
152
160
|
var _ref1;
|
|
153
161
|
const jobs = (_ref1 = payload == null ? void 0 : payload.jobs) != null ? _ref1 : [];
|
|
154
|
-
setRecent(jobs.filter((entry)=>entry.kind
|
|
162
|
+
setRecent(jobs.filter((entry)=>PROPOSING_KINDS.includes(entry.kind) && entry.hostId === hostId).filter((entry)=>proposalOf(entry) !== null).slice(0, RECENT_PROPOSALS_SHOWN));
|
|
155
163
|
}
|
|
156
164
|
setVerdict('ready');
|
|
157
165
|
} catch (unused) {
|
|
@@ -255,7 +263,8 @@ export function AiThemeProposalCard(props) {
|
|
|
255
263
|
proposeDraft
|
|
256
264
|
]);
|
|
257
265
|
if (verdict !== 'ready') return null;
|
|
258
|
-
|
|
266
|
+
// Only a theme job of the card's own is followed here; a build goes on building its pages elsewhere.
|
|
267
|
+
const running = Boolean(job && job.kind === 'theme' && !isTerminal(job));
|
|
259
268
|
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
260
269
|
contentGutterX: true,
|
|
261
270
|
contentGutterY: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-theme-proposal-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport type { HostThemeSource } from '@aglyn/aglyn/app-utils/site-theme'\nimport type { ConsoleHostThemeZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport type { HostThemeScheme } from '@aglyn/shared-data-types'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { themeEditorControl } from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n CircularProgress,\n Grid,\n Stack,\n Tab,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Tabs,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n AI_JOB_TERMINAL_STATUSES,\n type AiJobSummary,\n} from '../model/ai-jobs.types'\nimport {\n aiThemeColorToken,\n aiThemeProposalAfter,\n aiThemeStaleChanges,\n readAiThemeProposal,\n type AiThemeControlChange,\n type AiThemeCorrection,\n type AiThemeProposal,\n type AiThemeProposalMode,\n} from '../model/ai-theme-proposal'\nimport { readEventFrames } from './assist-jobs-drawer.component'\n\n/**\n * The theme assistant (AGL-2938), on the site's Theme section through the\n * `hostTheme` zone: a brief in, a theme proposal out, previewed before and\n * after with the editor's own preview, and put in the editor below as\n * unsaved changes when the person chooses. It never writes the theme — the\n * editor's Save does, through the same guarded path every edit takes.\n *\n * It renders nothing until the jobs route has answered for this workspace.\n * The shell has already decided the plan and the member's permission before\n * mounting it, but a released-off feature does not exist, and the release\n * flag is the route's to decide: a 404 there is this card staying absent.\n * The same read lists the site's recent proposals, so one made while the\n * person was elsewhere is still here to preview.\n */\n\n/** How many recent jobs the card reads to find this site's proposals. */\nconst RECENT_JOBS_READ = 20\n\n/** How many of this site's proposals the card offers again. */\nconst RECENT_PROPOSALS_SHOWN = 3\n\nconst BRIEF_MAX_CHARS = 4_000\n\nconst SOURCE_COPY: Record<HostThemeSource, string> = {\n installed:\n 'Saved as your changes on top of the installed theme. The publisher’s version stays as it is and can still take an update.',\n preset:\n 'Saved as your changes on top of the built-in theme, which stays as it is and can be restored.',\n custom:\n 'Saved as your changes on top of this theme of yours. Update it to keep them in the theme itself.',\n default:\n 'Only these values are stored. Everything else keeps following the default theme.',\n}\n\ntype Verdict = 'checking' | 'ready' | 'hidden'\n\nconst isTerminal = (job: AiJobSummary) => AI_JOB_TERMINAL_STATUSES.includes(job.status)\n\n/** The proposal a theme job produced, when it produced one. */\nfunction proposalOf(job: AiJobSummary | null): AiThemeProposal | null {\n const output = job?.outputs.find((entry) => entry.resource === 'theme')\n return output ? readAiThemeProposal(output.proposal) : null\n}\n\n/** A control's label, with its scheme when it has one. */\nfunction changeLabel(change: Pick<AiThemeControlChange, 'control' | 'scheme'>): string {\n return themeEditorControl(change.control).label\n}\n\nfunction Swatch({ value }: { value: string | number | null }) {\n if (value === null) {\n return (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Default'}\n </Typography>\n )\n }\n const text = String(value)\n const color = typeof value === 'string' && /^#[0-9a-f]{6}$/i.test(value)\n return (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n {color ? (\n <Box\n aria-hidden\n sx={{\n width: 14,\n height: 14,\n borderRadius: 0.5,\n border: 1,\n borderColor: 'divider',\n backgroundColor: text,\n flexShrink: 0,\n }}\n />\n ) : null}\n <Typography variant=\"body2\" sx={{ fontFamily: 'monospace', wordBreak: 'break-all' }}>\n {text}\n </Typography>\n </Stack>\n )\n}\n\nfunction correctionText(correction: AiThemeCorrection): string {\n const label = `${changeLabel(correction)} (${correction.scheme})`\n if (correction.reason === 'dark-value') {\n return `${label} set to ${correction.to} so the color keeps reading on the dark background.`\n }\n return (\n `${label} moved to ${correction.to} to reach ${correction.required}:1` +\n (correction.against?.length ? ` against ${correction.against.join(' and ')}` : '') +\n (correction.ratio ? ` (it was ${correction.ratio}:1).` : '.')\n )\n}\n\nexport function AiThemeProposalCard(props: ConsoleHostThemeZoneProps) {\n const { hostId, orgId, theme, themeSource, ThemePreview, proposeDraft } = props\n const { data: user } = useUser()\n // Held in a ref so the reads below key on WHO is signed in, never on the\n // identity of the object that says so.\n const userRef = useRef(user)\n userRef.current = user\n const uid = user?.uid ?? null\n\n const [verdict, setVerdict] = useState<Verdict>('checking')\n const [notice, setNotice] = useState<string | null>(null)\n const [recent, setRecent] = useState<AiJobSummary[]>([])\n const [brief, setBrief] = useState('')\n const [mode, setMode] = useState<AiThemeProposalMode>('modify')\n const [busy, setBusy] = useState(false)\n const [job, setJob] = useState<AiJobSummary | null>(null)\n const [scheme, setScheme] = useState<HostThemeScheme>('light')\n const [applied, setApplied] = useState<string | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n useEffect(() => () => watchRef.current?.abort(), [])\n\n useEffect(() => {\n if (!orgId || !hostId || !uid) return\n let active = true\n setVerdict('checking')\n void (async () => {\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs?orgId=${encodeURIComponent(orgId)}&limit=${RECENT_JOBS_READ}`,\n )\n if (!active) return\n // The flag off (404) and the plan without generation (403): the\n // feature is not this workspace's, so the card is not here at all.\n if (response.status === 404 || response.status === 403) {\n setVerdict('hidden')\n return\n }\n const payload = await response.json().catch(() => null)\n if (!active) return\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked\n ? lockdownRefusalText(locked)\n : String(payload?.error ?? 'Recent theme proposals could not be loaded.'),\n )\n } else {\n const jobs = (payload?.jobs ?? []) as AiJobSummary[]\n setRecent(\n jobs\n .filter((entry) => entry.kind === 'theme' && entry.hostId === hostId)\n .filter((entry) => proposalOf(entry) !== null)\n .slice(0, RECENT_PROPOSALS_SHOWN),\n )\n }\n setVerdict('ready')\n } catch {\n if (!active) return\n setNotice('Recent theme proposals could not be loaded.')\n setVerdict('ready')\n }\n })()\n return () => {\n active = false\n }\n }, [orgId, hostId, uid])\n\n /** Follows a job through the events route until it settles. */\n const watch = useCallback(async (jobId: string, org: string) => {\n watchRef.current?.abort()\n const controller = new AbortController()\n watchRef.current = controller\n let again = true\n while (again && !controller.signal.aborted) {\n again = false\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs/${encodeURIComponent(jobId)}/events?orgId=${encodeURIComponent(org)}`,\n { signal: controller.signal },\n )\n if (!response.ok || !response.body) return\n await readEventFrames(response.body, (event) => {\n if (event['type'] === 'state') setJob(event['job'] as AiJobSummary)\n else if (event['type'] === 'reconnect') again = true\n })\n } catch {\n return\n }\n }\n }, [])\n\n const propose = useCallback(async () => {\n if (!orgId || !brief.trim()) return\n setBusy(true)\n setNotice(null)\n setApplied(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({\n orgId,\n hostId,\n kind: 'theme',\n brief: brief.trim(),\n inputs: { mode },\n }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked\n ? lockdownRefusalText(locked)\n : String(payload?.error ?? 'The theme proposal could not be started — try again.'),\n )\n return\n }\n const next = payload?.job as AiJobSummary | undefined\n if (!next) return\n setJob(next)\n if (!isTerminal(next)) await watch(next.id, orgId)\n } catch {\n setNotice('The theme proposal could not be started — try again.')\n } finally {\n setBusy(false)\n }\n }, [orgId, hostId, brief, mode, watch])\n\n const proposal = useMemo(() => proposalOf(job), [job])\n const after = useMemo(\n () => (proposal ? aiThemeProposalAfter(theme, proposal) : null),\n [theme, proposal],\n )\n const stale = useMemo(\n () => (proposal ? aiThemeStaleChanges(theme, proposal).length > 0 : false),\n [theme, proposal],\n )\n\n const apply = useCallback(() => {\n if (!job || !after) return\n proposeDraft(after.theme, job.id)\n setApplied(job.id)\n }, [job, after, proposeDraft])\n\n if (verdict !== 'ready') return null\n\n const running = Boolean(job && !isTerminal(job))\n\n return (\n <CardDisplay\n contentGutterX\n contentGutterY\n header=\"Theme assistant\"\n help={pluginDocsHelp('aiThemes', {\n anchor: '#describe-a-change',\n excerpt:\n 'Describe the change and get a proposal for every control the editor has, previewed before and after. Nothing is saved until you save it in the editor.',\n })}\n >\n <Stack spacing={2}>\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n value={mode}\n onChange={(_, value: AiThemeProposalMode | null) => value && setMode(value)}\n aria-label=\"What to propose\"\n >\n <ToggleButton value=\"modify\">{'Change what I describe'}</ToggleButton>\n <ToggleButton value=\"create\">{'Design a new theme'}</ToggleButton>\n </ToggleButtonGroup>\n <TextField\n multiline\n minRows={2}\n fullWidth\n label=\"Describe the change\"\n placeholder=\"Warmer, with a deep green primary and bigger headings on mobile\"\n helperText=\"A hex color in your description is used exactly. Paste a link to borrow a page’s colors.\"\n value={brief}\n onChange={(event) => setBrief(event.target.value.slice(0, BRIEF_MAX_CHARS))}\n disabled={busy}\n />\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button variant=\"contained\" disabled={busy || !brief.trim()} onClick={() => void propose()}>\n {'Propose changes'}\n </Button>\n {running ? <CircularProgress size={18} aria-label=\"Working on a proposal\" /> : null}\n </Stack>\n {notice ? <Alert severity=\"warning\">{notice}</Alert> : null}\n\n {job && job.error && !proposal ? (\n <Alert severity={job.status === 'failed' ? 'error' : 'warning'}>{job.error}</Alert>\n ) : null}\n\n {!job && recent.length ? (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Recent proposals for this site'}</Typography>\n {recent.map((entry) => (\n <Stack key={entry.id} direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ flexGrow: 1, minWidth: 0 }} noWrap>\n {entry.brief}\n </Typography>\n <Button size=\"small\" onClick={() => setJob(entry)}>\n {'Show'}\n </Button>\n </Stack>\n ))}\n </Stack>\n ) : null}\n\n {proposal && after ? (\n <Stack spacing={2} aria-label=\"Theme proposal\">\n {proposal.summary ? <Typography variant=\"body1\">{proposal.summary}</Typography> : null}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {SOURCE_COPY[proposal.source]}\n </Typography>\n {stale ? (\n <Alert severity=\"info\">\n {'The theme has changed since this proposal was made. The preview and the editor apply it to the theme as it is now.'}\n </Alert>\n ) : null}\n\n {proposal.changes.length || proposal.components.length || proposal.resetComponents ? (\n <Box>\n <ScrollTable size=\"small\" aria-label=\"Proposed changes\">\n <TableHead>\n <TableRow>\n <TableCell>{'Control'}</TableCell>\n <TableCell>{'Now'}</TableCell>\n <TableCell>{'Proposed'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {proposal.changes.map((change) => (\n <TableRow key={`${change.control}:${change.scheme ?? ''}`}>\n <TableCell>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\">{changeLabel(change)}</Typography>\n {aiThemeColorToken(change.control) && change.scheme ? (\n <Chip size=\"small\" variant=\"outlined\" label={change.scheme} />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n <Swatch value={change.before} />\n </TableCell>\n <TableCell>\n <Swatch value={change.value} />\n </TableCell>\n </TableRow>\n ))}\n {proposal.resetComponents ? (\n <TableRow>\n <TableCell>{themeEditorControl('components').label}</TableCell>\n <TableCell>{'This site’s overrides'}</TableCell>\n <TableCell>{'Theme defaults'}</TableCell>\n </TableRow>\n ) : null}\n {proposal.components.map((leaf) => (\n <TableRow key={`${leaf.component}:${leaf.target}:${leaf.slot}:${leaf.media}:${leaf.property}`}>\n <TableCell>\n <Typography variant=\"body2\">\n {[\n leaf.component,\n leaf.target === 'defaultProps' ? 'default' : leaf.slot,\n leaf.property,\n ].join(' · ')}\n </Typography>\n {leaf.media ? <Chip size=\"small\" variant=\"outlined\" label={leaf.media} /> : null}\n </TableCell>\n <TableCell>{'—'}</TableCell>\n <TableCell>\n <Swatch value={typeof leaf.value === 'boolean' ? String(leaf.value) : leaf.value} />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Box>\n ) : (\n <Typography variant=\"body2\">{'No changes proposed.'}</Typography>\n )}\n\n {[...proposal.corrections, ...after.corrections].length ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Adjusted for readability'}</Typography>\n {[...proposal.corrections, ...after.corrections].map((correction) => (\n <Typography\n key={`${correction.control}:${correction.scheme}:${correction.to}`}\n variant=\"body2\"\n >\n {correctionText(correction)}\n </Typography>\n ))}\n </Stack>\n ) : null}\n\n {[...proposal.notes, ...after.notes, ...proposal.dropped].length ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Worth knowing'}</Typography>\n {[...proposal.notes, ...after.notes].map((line) => (\n <Typography key={`note:${line}`} variant=\"body2\" color=\"text.secondary\">\n {line}\n </Typography>\n ))}\n {proposal.dropped.map((line) => (\n <Typography key={`dropped:${line}`} variant=\"body2\" color=\"text.secondary\">\n {`Left out — ${line}`}\n </Typography>\n ))}\n </Stack>\n ) : null}\n\n <Tabs\n value={scheme}\n onChange={(_, value: HostThemeScheme) => setScheme(value)}\n aria-label=\"Preview scheme\"\n >\n <Tab label=\"Light\" value=\"light\" />\n <Tab label=\"Dark\" value=\"dark\" />\n </Tabs>\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, md: 6 }}>\n <Typography variant=\"subtitle2\" gutterBottom>\n {'Before'}\n </Typography>\n <Box data-preview=\"before\">\n <ThemePreview theme={theme ?? {}} scheme={scheme} />\n </Box>\n </Grid>\n <Grid size={{ xs: 12, md: 6 }}>\n <Typography variant=\"subtitle2\" gutterBottom>\n {'After'}\n </Typography>\n <Box data-preview=\"after\">\n <ThemePreview theme={after.theme} scheme={scheme} />\n </Box>\n </Grid>\n </Grid>\n\n {applied === job?.id ? (\n <Alert severity=\"success\">\n {'In the editor below as unsaved changes. Review them there, then Save — or Discard changes to go back.'}\n </Alert>\n ) : null}\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Button variant=\"contained\" onClick={apply} disabled={applied === job?.id}>\n {'Put in the editor'}\n </Button>\n <Button\n onClick={() => {\n setJob(null)\n setApplied(null)\n }}\n >\n {'Dismiss'}\n </Button>\n </Stack>\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nAiThemeProposalCard.displayName = 'AiThemeProposalCard'\n\nexport default AiThemeProposalCard\n"],"names":["lockdownRefusalText","parseLockdownRefusal","pluginDocsHelp","CardDisplay","ScrollTable","themeEditorControl","authorizedFetch","useUser","Alert","Box","Button","Chip","CircularProgress","Grid","Stack","Tab","TableBody","TableCell","TableHead","TableRow","Tabs","TextField","ToggleButton","ToggleButtonGroup","Typography","useCallback","useEffect","useMemo","useRef","useState","AI_JOB_TERMINAL_STATUSES","aiThemeColorToken","aiThemeProposalAfter","aiThemeStaleChanges","readAiThemeProposal","readEventFrames","RECENT_JOBS_READ","RECENT_PROPOSALS_SHOWN","BRIEF_MAX_CHARS","SOURCE_COPY","installed","preset","custom","default","isTerminal","job","includes","status","proposalOf","output","outputs","find","entry","resource","proposal","changeLabel","change","control","label","Swatch","value","variant","color","text","String","test","direction","spacing","sx","alignItems","aria-hidden","width","height","borderRadius","border","borderColor","backgroundColor","flexShrink","fontFamily","wordBreak","correctionText","correction","scheme","reason","to","required","against","length","join","ratio","AiThemeProposalCard","props","hostId","orgId","theme","themeSource","ThemePreview","proposeDraft","data","user","userRef","current","uid","verdict","setVerdict","notice","setNotice","recent","setRecent","brief","setBrief","mode","setMode","busy","setBusy","setJob","setScheme","applied","setApplied","watchRef","abort","active","response","encodeURIComponent","payload","json","catch","ok","locked","error","jobs","filter","kind","slice","watch","jobId","org","controller","AbortController","again","signal","aborted","body","event","propose","trim","method","headers","JSON","stringify","inputs","next","id","after","stale","apply","running","Boolean","contentGutterX","contentGutterY","header","help","anchor","excerpt","exclusive","size","onChange","_","aria-label","multiline","minRows","fullWidth","placeholder","helperText","target","disabled","onClick","severity","map","flexGrow","minWidth","noWrap","summary","source","changes","components","resetComponents","before","leaf","component","slot","property","media","corrections","notes","dropped","line","container","xs","md","gutterBottom","data-preview","useFlexGap","flexWrap","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-theme-proposal-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport type { HostThemeSource } from '@aglyn/aglyn/app-utils/site-theme'\nimport type { ConsoleHostThemeZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport type { HostThemeScheme } from '@aglyn/shared-data-types'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { themeEditorControl } from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Chip,\n CircularProgress,\n Grid,\n Stack,\n Tab,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Tabs,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n AI_JOB_TERMINAL_STATUSES,\n type AiJobSummary,\n} from '../model/ai-jobs.types'\nimport {\n aiThemeColorToken,\n aiThemeProposalAfter,\n aiThemeStaleChanges,\n readAiThemeProposal,\n type AiThemeControlChange,\n type AiThemeCorrection,\n type AiThemeProposal,\n type AiThemeProposalMode,\n} from '../model/ai-theme-proposal'\nimport { readEventFrames } from './assist-jobs-drawer.component'\n\n/**\n * The theme assistant (AGL-2938), on the site's Theme section through the\n * `hostTheme` zone: a brief in, a theme proposal out, previewed before and\n * after with the editor's own preview, and put in the editor below as\n * unsaved changes when the person chooses. It never writes the theme — the\n * editor's Save does, through the same guarded path every edit takes.\n *\n * It renders nothing until the jobs route has answered for this workspace.\n * The shell has already decided the plan and the member's permission before\n * mounting it, but a released-off feature does not exist, and the release\n * flag is the route's to decide: a 404 there is this card staying absent.\n * The same read lists the site's recent proposals, so one made while the\n * person was elsewhere is still here to preview.\n */\n\n/** How many recent jobs the card reads to find this site's proposals. */\nconst RECENT_JOBS_READ = 20\n\n/** How many of this site's proposals the card offers again. */\nconst RECENT_PROPOSALS_SHOWN = 3\n\nconst BRIEF_MAX_CHARS = 4_000\n\nconst SOURCE_COPY: Record<HostThemeSource, string> = {\n installed:\n 'Saved as your changes on top of the installed theme. The publisher’s version stays as it is and can still take an update.',\n preset:\n 'Saved as your changes on top of the built-in theme, which stays as it is and can be restored.',\n custom:\n 'Saved as your changes on top of this theme of yours. Update it to keep them in the theme itself.',\n default:\n 'Only these values are stored. Everything else keeps following the default theme.',\n}\n\ntype Verdict = 'checking' | 'ready' | 'hidden'\n\nconst isTerminal = (job: AiJobSummary) => AI_JOB_TERMINAL_STATUSES.includes(job.status)\n\n/**\n * The jobs whose theme proposals this card offers: a theme job's, and a\n * build's from the chat that planned a theme change (AGL-4077), which the\n * owner applies here like any other.\n */\nconst PROPOSING_KINDS: readonly AiJobSummary['kind'][] = ['theme', 'build']\n\n/** The proposal a theme job, or a build's theme change, produced, when it produced one. */\nfunction proposalOf(job: AiJobSummary | null): AiThemeProposal | null {\n const output = job?.outputs.find((entry) => entry.resource === 'theme')\n return output ? readAiThemeProposal(output.proposal) : null\n}\n\n/** A control's label, with its scheme when it has one. */\nfunction changeLabel(change: Pick<AiThemeControlChange, 'control' | 'scheme'>): string {\n return themeEditorControl(change.control).label\n}\n\nfunction Swatch({ value }: { value: string | number | null }) {\n if (value === null) {\n return (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Default'}\n </Typography>\n )\n }\n const text = String(value)\n const color = typeof value === 'string' && /^#[0-9a-f]{6}$/i.test(value)\n return (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n {color ? (\n <Box\n aria-hidden\n sx={{\n width: 14,\n height: 14,\n borderRadius: 0.5,\n border: 1,\n borderColor: 'divider',\n backgroundColor: text,\n flexShrink: 0,\n }}\n />\n ) : null}\n <Typography variant=\"body2\" sx={{ fontFamily: 'monospace', wordBreak: 'break-all' }}>\n {text}\n </Typography>\n </Stack>\n )\n}\n\nfunction correctionText(correction: AiThemeCorrection): string {\n const label = `${changeLabel(correction)} (${correction.scheme})`\n if (correction.reason === 'dark-value') {\n return `${label} set to ${correction.to} so the color keeps reading on the dark background.`\n }\n return (\n `${label} moved to ${correction.to} to reach ${correction.required}:1` +\n (correction.against?.length ? ` against ${correction.against.join(' and ')}` : '') +\n (correction.ratio ? ` (it was ${correction.ratio}:1).` : '.')\n )\n}\n\nexport function AiThemeProposalCard(props: ConsoleHostThemeZoneProps) {\n const { hostId, orgId, theme, themeSource, ThemePreview, proposeDraft } = props\n const { data: user } = useUser()\n // Held in a ref so the reads below key on WHO is signed in, never on the\n // identity of the object that says so.\n const userRef = useRef(user)\n userRef.current = user\n const uid = user?.uid ?? null\n\n const [verdict, setVerdict] = useState<Verdict>('checking')\n const [notice, setNotice] = useState<string | null>(null)\n const [recent, setRecent] = useState<AiJobSummary[]>([])\n const [brief, setBrief] = useState('')\n const [mode, setMode] = useState<AiThemeProposalMode>('modify')\n const [busy, setBusy] = useState(false)\n const [job, setJob] = useState<AiJobSummary | null>(null)\n const [scheme, setScheme] = useState<HostThemeScheme>('light')\n const [applied, setApplied] = useState<string | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n useEffect(() => () => watchRef.current?.abort(), [])\n\n useEffect(() => {\n if (!orgId || !hostId || !uid) return\n let active = true\n setVerdict('checking')\n void (async () => {\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs?orgId=${encodeURIComponent(orgId)}&limit=${RECENT_JOBS_READ}`,\n )\n if (!active) return\n // The flag off (404) and the plan without generation (403): the\n // feature is not this workspace's, so the card is not here at all.\n if (response.status === 404 || response.status === 403) {\n setVerdict('hidden')\n return\n }\n const payload = await response.json().catch(() => null)\n if (!active) return\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked\n ? lockdownRefusalText(locked)\n : String(payload?.error ?? 'Recent theme proposals could not be loaded.'),\n )\n } else {\n const jobs = (payload?.jobs ?? []) as AiJobSummary[]\n setRecent(\n jobs\n .filter((entry) => PROPOSING_KINDS.includes(entry.kind) && entry.hostId === hostId)\n .filter((entry) => proposalOf(entry) !== null)\n .slice(0, RECENT_PROPOSALS_SHOWN),\n )\n }\n setVerdict('ready')\n } catch {\n if (!active) return\n setNotice('Recent theme proposals could not be loaded.')\n setVerdict('ready')\n }\n })()\n return () => {\n active = false\n }\n }, [orgId, hostId, uid])\n\n /** Follows a job through the events route until it settles. */\n const watch = useCallback(async (jobId: string, org: string) => {\n watchRef.current?.abort()\n const controller = new AbortController()\n watchRef.current = controller\n let again = true\n while (again && !controller.signal.aborted) {\n again = false\n try {\n const response = await authorizedFetch(\n userRef.current,\n `/api/ai/jobs/${encodeURIComponent(jobId)}/events?orgId=${encodeURIComponent(org)}`,\n { signal: controller.signal },\n )\n if (!response.ok || !response.body) return\n await readEventFrames(response.body, (event) => {\n if (event['type'] === 'state') setJob(event['job'] as AiJobSummary)\n else if (event['type'] === 'reconnect') again = true\n })\n } catch {\n return\n }\n }\n }, [])\n\n const propose = useCallback(async () => {\n if (!orgId || !brief.trim()) return\n setBusy(true)\n setNotice(null)\n setApplied(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({\n orgId,\n hostId,\n kind: 'theme',\n brief: brief.trim(),\n inputs: { mode },\n }),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(\n locked\n ? lockdownRefusalText(locked)\n : String(payload?.error ?? 'The theme proposal could not be started — try again.'),\n )\n return\n }\n const next = payload?.job as AiJobSummary | undefined\n if (!next) return\n setJob(next)\n if (!isTerminal(next)) await watch(next.id, orgId)\n } catch {\n setNotice('The theme proposal could not be started — try again.')\n } finally {\n setBusy(false)\n }\n }, [orgId, hostId, brief, mode, watch])\n\n const proposal = useMemo(() => proposalOf(job), [job])\n const after = useMemo(\n () => (proposal ? aiThemeProposalAfter(theme, proposal) : null),\n [theme, proposal],\n )\n const stale = useMemo(\n () => (proposal ? aiThemeStaleChanges(theme, proposal).length > 0 : false),\n [theme, proposal],\n )\n\n const apply = useCallback(() => {\n if (!job || !after) return\n proposeDraft(after.theme, job.id)\n setApplied(job.id)\n }, [job, after, proposeDraft])\n\n if (verdict !== 'ready') return null\n\n // Only a theme job of the card's own is followed here; a build goes on building its pages elsewhere.\n const running = Boolean(job && job.kind === 'theme' && !isTerminal(job))\n\n return (\n <CardDisplay\n contentGutterX\n contentGutterY\n header=\"Theme assistant\"\n help={pluginDocsHelp('aiThemes', {\n anchor: '#describe-a-change',\n excerpt:\n 'Describe the change and get a proposal for every control the editor has, previewed before and after. Nothing is saved until you save it in the editor.',\n })}\n >\n <Stack spacing={2}>\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n value={mode}\n onChange={(_, value: AiThemeProposalMode | null) => value && setMode(value)}\n aria-label=\"What to propose\"\n >\n <ToggleButton value=\"modify\">{'Change what I describe'}</ToggleButton>\n <ToggleButton value=\"create\">{'Design a new theme'}</ToggleButton>\n </ToggleButtonGroup>\n <TextField\n multiline\n minRows={2}\n fullWidth\n label=\"Describe the change\"\n placeholder=\"Warmer, with a deep green primary and bigger headings on mobile\"\n helperText=\"A hex color in your description is used exactly. Paste a link to borrow a page’s colors.\"\n value={brief}\n onChange={(event) => setBrief(event.target.value.slice(0, BRIEF_MAX_CHARS))}\n disabled={busy}\n />\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button variant=\"contained\" disabled={busy || !brief.trim()} onClick={() => void propose()}>\n {'Propose changes'}\n </Button>\n {running ? <CircularProgress size={18} aria-label=\"Working on a proposal\" /> : null}\n </Stack>\n {notice ? <Alert severity=\"warning\">{notice}</Alert> : null}\n\n {job && job.error && !proposal ? (\n <Alert severity={job.status === 'failed' ? 'error' : 'warning'}>{job.error}</Alert>\n ) : null}\n\n {!job && recent.length ? (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Recent proposals for this site'}</Typography>\n {recent.map((entry) => (\n <Stack key={entry.id} direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ flexGrow: 1, minWidth: 0 }} noWrap>\n {entry.brief}\n </Typography>\n <Button size=\"small\" onClick={() => setJob(entry)}>\n {'Show'}\n </Button>\n </Stack>\n ))}\n </Stack>\n ) : null}\n\n {proposal && after ? (\n <Stack spacing={2} aria-label=\"Theme proposal\">\n {proposal.summary ? <Typography variant=\"body1\">{proposal.summary}</Typography> : null}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {SOURCE_COPY[proposal.source]}\n </Typography>\n {stale ? (\n <Alert severity=\"info\">\n {'The theme has changed since this proposal was made. The preview and the editor apply it to the theme as it is now.'}\n </Alert>\n ) : null}\n\n {proposal.changes.length || proposal.components.length || proposal.resetComponents ? (\n <Box>\n <ScrollTable size=\"small\" aria-label=\"Proposed changes\">\n <TableHead>\n <TableRow>\n <TableCell>{'Control'}</TableCell>\n <TableCell>{'Now'}</TableCell>\n <TableCell>{'Proposed'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {proposal.changes.map((change) => (\n <TableRow key={`${change.control}:${change.scheme ?? ''}`}>\n <TableCell>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\">{changeLabel(change)}</Typography>\n {aiThemeColorToken(change.control) && change.scheme ? (\n <Chip size=\"small\" variant=\"outlined\" label={change.scheme} />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n <Swatch value={change.before} />\n </TableCell>\n <TableCell>\n <Swatch value={change.value} />\n </TableCell>\n </TableRow>\n ))}\n {proposal.resetComponents ? (\n <TableRow>\n <TableCell>{themeEditorControl('components').label}</TableCell>\n <TableCell>{'This site’s overrides'}</TableCell>\n <TableCell>{'Theme defaults'}</TableCell>\n </TableRow>\n ) : null}\n {proposal.components.map((leaf) => (\n <TableRow key={`${leaf.component}:${leaf.target}:${leaf.slot}:${leaf.media}:${leaf.property}`}>\n <TableCell>\n <Typography variant=\"body2\">\n {[\n leaf.component,\n leaf.target === 'defaultProps' ? 'default' : leaf.slot,\n leaf.property,\n ].join(' · ')}\n </Typography>\n {leaf.media ? <Chip size=\"small\" variant=\"outlined\" label={leaf.media} /> : null}\n </TableCell>\n <TableCell>{'—'}</TableCell>\n <TableCell>\n <Swatch value={typeof leaf.value === 'boolean' ? String(leaf.value) : leaf.value} />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Box>\n ) : (\n <Typography variant=\"body2\">{'No changes proposed.'}</Typography>\n )}\n\n {[...proposal.corrections, ...after.corrections].length ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Adjusted for readability'}</Typography>\n {[...proposal.corrections, ...after.corrections].map((correction) => (\n <Typography\n key={`${correction.control}:${correction.scheme}:${correction.to}`}\n variant=\"body2\"\n >\n {correctionText(correction)}\n </Typography>\n ))}\n </Stack>\n ) : null}\n\n {[...proposal.notes, ...after.notes, ...proposal.dropped].length ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Worth knowing'}</Typography>\n {[...proposal.notes, ...after.notes].map((line) => (\n <Typography key={`note:${line}`} variant=\"body2\" color=\"text.secondary\">\n {line}\n </Typography>\n ))}\n {proposal.dropped.map((line) => (\n <Typography key={`dropped:${line}`} variant=\"body2\" color=\"text.secondary\">\n {`Left out — ${line}`}\n </Typography>\n ))}\n </Stack>\n ) : null}\n\n <Tabs\n value={scheme}\n onChange={(_, value: HostThemeScheme) => setScheme(value)}\n aria-label=\"Preview scheme\"\n >\n <Tab label=\"Light\" value=\"light\" />\n <Tab label=\"Dark\" value=\"dark\" />\n </Tabs>\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, md: 6 }}>\n <Typography variant=\"subtitle2\" gutterBottom>\n {'Before'}\n </Typography>\n <Box data-preview=\"before\">\n <ThemePreview theme={theme ?? {}} scheme={scheme} />\n </Box>\n </Grid>\n <Grid size={{ xs: 12, md: 6 }}>\n <Typography variant=\"subtitle2\" gutterBottom>\n {'After'}\n </Typography>\n <Box data-preview=\"after\">\n <ThemePreview theme={after.theme} scheme={scheme} />\n </Box>\n </Grid>\n </Grid>\n\n {applied === job?.id ? (\n <Alert severity=\"success\">\n {'In the editor below as unsaved changes. Review them there, then Save — or Discard changes to go back.'}\n </Alert>\n ) : null}\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap' }}\n >\n <Button variant=\"contained\" onClick={apply} disabled={applied === job?.id}>\n {'Put in the editor'}\n </Button>\n <Button\n onClick={() => {\n setJob(null)\n setApplied(null)\n }}\n >\n {'Dismiss'}\n </Button>\n </Stack>\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nAiThemeProposalCard.displayName = 'AiThemeProposalCard'\n\nexport default AiThemeProposalCard\n"],"names":["lockdownRefusalText","parseLockdownRefusal","pluginDocsHelp","CardDisplay","ScrollTable","themeEditorControl","authorizedFetch","useUser","Alert","Box","Button","Chip","CircularProgress","Grid","Stack","Tab","TableBody","TableCell","TableHead","TableRow","Tabs","TextField","ToggleButton","ToggleButtonGroup","Typography","useCallback","useEffect","useMemo","useRef","useState","AI_JOB_TERMINAL_STATUSES","aiThemeColorToken","aiThemeProposalAfter","aiThemeStaleChanges","readAiThemeProposal","readEventFrames","RECENT_JOBS_READ","RECENT_PROPOSALS_SHOWN","BRIEF_MAX_CHARS","SOURCE_COPY","installed","preset","custom","default","isTerminal","job","includes","status","PROPOSING_KINDS","proposalOf","output","outputs","find","entry","resource","proposal","changeLabel","change","control","label","Swatch","value","variant","color","text","String","test","direction","spacing","sx","alignItems","aria-hidden","width","height","borderRadius","border","borderColor","backgroundColor","flexShrink","fontFamily","wordBreak","correctionText","correction","scheme","reason","to","required","against","length","join","ratio","AiThemeProposalCard","props","hostId","orgId","theme","themeSource","ThemePreview","proposeDraft","data","user","userRef","current","uid","verdict","setVerdict","notice","setNotice","recent","setRecent","brief","setBrief","mode","setMode","busy","setBusy","setJob","setScheme","applied","setApplied","watchRef","abort","active","response","encodeURIComponent","payload","json","catch","ok","locked","error","jobs","filter","kind","slice","watch","jobId","org","controller","AbortController","again","signal","aborted","body","event","propose","trim","method","headers","JSON","stringify","inputs","next","id","after","stale","apply","running","Boolean","contentGutterX","contentGutterY","header","help","anchor","excerpt","exclusive","size","onChange","_","aria-label","multiline","minRows","fullWidth","placeholder","helperText","target","disabled","onClick","severity","map","flexGrow","minWidth","noWrap","summary","source","changes","components","resetComponents","before","leaf","component","slot","property","media","corrections","notes","dropped","line","container","xs","md","gutterBottom","data-preview","useFlexGap","flexWrap","displayName"],"mappings":"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"}
|
package/src/lib/declarations.js
CHANGED
|
@@ -20,7 +20,7 @@ import { aiTierQuotaPerUnit, planSellsAiTiers } from "@aglyn/aglyn/app-utils/pla
|
|
|
20
20
|
import { registerPluginConfigSchema } from "@aglyn/aglyn/plugin-manager/plugin-config";
|
|
21
21
|
import { registerPluginEntitlements } from "@aglyn/aglyn/plugin-manager/plugin-entitlements";
|
|
22
22
|
import { AI_PLUGIN_ID } from "./constants.js";
|
|
23
|
-
import { AI_ORG_PERMISSIONS } from "./model/ai-permissions.js";
|
|
23
|
+
import { AI_ORG_PERMISSIONS, AI_WORKSPACE_PERMISSIONS } from "./model/ai-permissions.js";
|
|
24
24
|
import { AI_CONFIG_SCHEMA } from "./plugin-config.js";
|
|
25
25
|
import { registerAiActivityActions } from "./activity/ai-activity-actions.js";
|
|
26
26
|
/**
|
|
@@ -113,7 +113,10 @@ import { registerAiActivityActions } from "./activity/ai-activity-actions.js";
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
],
|
|
116
|
-
orgPermissions:
|
|
116
|
+
orgPermissions: [
|
|
117
|
+
...AI_ORG_PERMISSIONS,
|
|
118
|
+
...AI_WORKSPACE_PERMISSIONS
|
|
119
|
+
]
|
|
117
120
|
};
|
|
118
121
|
let declared = false;
|
|
119
122
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/declarations.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// Named modules rather than the `@aglyn/aglyn` barrel: both apps load this\n// on the server at boot, and the full barrel carries client-only contexts\n// (AGL-405).\nimport {\n aiTierQuotaPerUnit,\n planSellsAiTiers,\n} from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { registerPluginConfigSchema } from '@aglyn/aglyn/plugin-manager/plugin-config'\nimport {\n registerPluginEntitlements,\n type PluginEntitlementRegistration,\n} from '@aglyn/aglyn/plugin-manager/plugin-entitlements'\nimport { AI_PLUGIN_ID } from './constants'\nimport { AI_ORG_PERMISSIONS } from './model/ai-permissions'\nimport { AI_CONFIG_SCHEMA } from './plugin-config'\nimport { registerAiActivityActions } from './activity/ai-activity-actions'\n\n/**\n * The plugin's DECLARATIONS (AGL-2939): what core must know about this\n * plugin before any of its surfaces load — its billing and access keys,\n * the AI permission keys it adds to the org catalog (AGL-2984), its\n * activity codes, its settings schema. Loaded eagerly by both apps\n * through the generated declarations manifest, at boot on the server and\n * with the plugin loader on the client, so a core billing route folds the\n * add-on and the staff lockdown page lists the levers whether or not a\n * request has touched the plugin yet. Light by construction: nothing here\n * reaches a provider, Firestore or React.\n *\n * The plan tables keep the per-plan defaults for `aiGenerative` and\n * `aiAssist`, and `AI_TIERS` keeps each tier's price and credits, so this\n * names keys and restates no figure.\n *\n * `ai-assist` and `ai-generate` are separate levers because the two doors\n * spend at different rates and an incident on one need not stop the other\n * (AGL-2903). Both grant the staff bypass: a provider incident is verified\n * recovered by staff making one real call, not by lifting the lock and\n * watching customers find out.\n */\nexport const AI_PLUGIN_ENTITLEMENTS: PluginEntitlementRegistration = {\n pluginId: AI_PLUGIN_ID,\n seatAddons: [\n {\n key: 'aiAddon',\n label: 'AI add-on',\n maxUnits: 1,\n // The purchased tier's credits on top of the plan's included band\n // (AGL-3965): one pool, widened by what was bought rather than by\n // which plan bought it.\n quota: {\n key: 'assistCreditsPerMonth',\n perUnit: aiTierQuotaPerUnit,\n },\n features: ['aiGenerative', 'aiAssist'],\n soldOn: planSellsAiTiers,\n },\n ],\n features: [\n { key: 'aiAssist', label: 'AI assist' },\n { key: 'aiGenerative', label: 'AI generation' },\n ],\n lockdownFeatures: [\n {\n key: 'ai-assist',\n label: 'AI assist',\n customerName: 'AI assist',\n staffBypass: true,\n notice: {\n title: 'AI assist is temporarily unavailable',\n body: 'AI assist is temporarily unavailable. Your content is unaffected — please try again shortly.',\n },\n // Gated even while the route 501s without a provider key — the switch\n // predates the key on purpose.\n apiPaths: { exact: ['ai/assist', 'assist/chat'] },\n },\n {\n key: 'ai-generate',\n label: 'AI generation',\n customerName: 'AI generation',\n staffBypass: true,\n notice: {\n title: 'AI generation is temporarily unavailable',\n body: 'Generating sections, pages and automations with AI is temporarily unavailable. Everything already built is unaffected — please try again shortly.',\n },\n // `ai/seo` (AGL-2910) applies a finished audit's drafts; it spends\n // nothing, and it is still a generative door the switch stops.\n apiPaths: { prefixes: ['ai/generate', 'ai/jobs', 'ai/seo'] },\n },\n ],\n orgPermissions: AI_ORG_PERMISSIONS,\n}\n\nlet declared = false\n\n/**\n * Registers everything above, and the activity codes. Idempotent: the\n * declarations manifest runs it once per process, and a surface's register\n * fn may call it again. The codes are a call rather than an import made\n * for what the module does as it loads, because the browser's manifest\n * reaches that module through nothing else, and a bundler honoring this\n * package's `sideEffects` deletes such an import (AGL-3025).\n */\nexport function registerAiDeclarations(): void {\n if (declared) return\n declared = true\n registerPluginEntitlements(AI_PLUGIN_ENTITLEMENTS)\n registerPluginConfigSchema(AI_CONFIG_SCHEMA)\n registerAiActivityActions()\n}\n\nregisterAiDeclarations()\n"],"names":["aiTierQuotaPerUnit","planSellsAiTiers","registerPluginConfigSchema","registerPluginEntitlements","AI_PLUGIN_ID","AI_ORG_PERMISSIONS","AI_CONFIG_SCHEMA","registerAiActivityActions","AI_PLUGIN_ENTITLEMENTS","pluginId","seatAddons","key","label","maxUnits","quota","perUnit","features","soldOn","lockdownFeatures","customerName","staffBypass","notice","title","body","apiPaths","exact","prefixes","orgPermissions","declared","registerAiDeclarations"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,2EAA2E;AAC3E,0EAA0E;AAC1E,aAAa;AACb,SACEA,kBAAkB,EAClBC,gBAAgB,QACX,2CAA0C;AACjD,SAASC,0BAA0B,QAAQ,4CAA2C;AACtF,SACEC,0BAA0B,QAErB,kDAAiD;AACxD,SAASC,YAAY,QAAQ,iBAAa;AAC1C,SAASC,kBAAkB,QAAQ,4BAAwB;
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/declarations.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// Named modules rather than the `@aglyn/aglyn` barrel: both apps load this\n// on the server at boot, and the full barrel carries client-only contexts\n// (AGL-405).\nimport {\n aiTierQuotaPerUnit,\n planSellsAiTiers,\n} from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { registerPluginConfigSchema } from '@aglyn/aglyn/plugin-manager/plugin-config'\nimport {\n registerPluginEntitlements,\n type PluginEntitlementRegistration,\n} from '@aglyn/aglyn/plugin-manager/plugin-entitlements'\nimport { AI_PLUGIN_ID } from './constants'\nimport { AI_ORG_PERMISSIONS, AI_WORKSPACE_PERMISSIONS } from './model/ai-permissions'\nimport { AI_CONFIG_SCHEMA } from './plugin-config'\nimport { registerAiActivityActions } from './activity/ai-activity-actions'\n\n/**\n * The plugin's DECLARATIONS (AGL-2939): what core must know about this\n * plugin before any of its surfaces load — its billing and access keys,\n * the AI permission keys it adds to the org catalog (AGL-2984), its\n * activity codes, its settings schema. Loaded eagerly by both apps\n * through the generated declarations manifest, at boot on the server and\n * with the plugin loader on the client, so a core billing route folds the\n * add-on and the staff lockdown page lists the levers whether or not a\n * request has touched the plugin yet. Light by construction: nothing here\n * reaches a provider, Firestore or React.\n *\n * The plan tables keep the per-plan defaults for `aiGenerative` and\n * `aiAssist`, and `AI_TIERS` keeps each tier's price and credits, so this\n * names keys and restates no figure.\n *\n * `ai-assist` and `ai-generate` are separate levers because the two doors\n * spend at different rates and an incident on one need not stop the other\n * (AGL-2903). Both grant the staff bypass: a provider incident is verified\n * recovered by staff making one real call, not by lifting the lock and\n * watching customers find out.\n */\nexport const AI_PLUGIN_ENTITLEMENTS: PluginEntitlementRegistration = {\n pluginId: AI_PLUGIN_ID,\n seatAddons: [\n {\n key: 'aiAddon',\n label: 'AI add-on',\n maxUnits: 1,\n // The purchased tier's credits on top of the plan's included band\n // (AGL-3965): one pool, widened by what was bought rather than by\n // which plan bought it.\n quota: {\n key: 'assistCreditsPerMonth',\n perUnit: aiTierQuotaPerUnit,\n },\n features: ['aiGenerative', 'aiAssist'],\n soldOn: planSellsAiTiers,\n },\n ],\n features: [\n { key: 'aiAssist', label: 'AI assist' },\n { key: 'aiGenerative', label: 'AI generation' },\n ],\n lockdownFeatures: [\n {\n key: 'ai-assist',\n label: 'AI assist',\n customerName: 'AI assist',\n staffBypass: true,\n notice: {\n title: 'AI assist is temporarily unavailable',\n body: 'AI assist is temporarily unavailable. Your content is unaffected — please try again shortly.',\n },\n // Gated even while the route 501s without a provider key — the switch\n // predates the key on purpose.\n apiPaths: { exact: ['ai/assist', 'assist/chat'] },\n },\n {\n key: 'ai-generate',\n label: 'AI generation',\n customerName: 'AI generation',\n staffBypass: true,\n notice: {\n title: 'AI generation is temporarily unavailable',\n body: 'Generating sections, pages and automations with AI is temporarily unavailable. Everything already built is unaffected — please try again shortly.',\n },\n // `ai/seo` (AGL-2910) applies a finished audit's drafts; it spends\n // nothing, and it is still a generative door the switch stops.\n apiPaths: { prefixes: ['ai/generate', 'ai/jobs', 'ai/seo'] },\n },\n ],\n orgPermissions: [...AI_ORG_PERMISSIONS, ...AI_WORKSPACE_PERMISSIONS],\n}\n\nlet declared = false\n\n/**\n * Registers everything above, and the activity codes. Idempotent: the\n * declarations manifest runs it once per process, and a surface's register\n * fn may call it again. The codes are a call rather than an import made\n * for what the module does as it loads, because the browser's manifest\n * reaches that module through nothing else, and a bundler honoring this\n * package's `sideEffects` deletes such an import (AGL-3025).\n */\nexport function registerAiDeclarations(): void {\n if (declared) return\n declared = true\n registerPluginEntitlements(AI_PLUGIN_ENTITLEMENTS)\n registerPluginConfigSchema(AI_CONFIG_SCHEMA)\n registerAiActivityActions()\n}\n\nregisterAiDeclarations()\n"],"names":["aiTierQuotaPerUnit","planSellsAiTiers","registerPluginConfigSchema","registerPluginEntitlements","AI_PLUGIN_ID","AI_ORG_PERMISSIONS","AI_WORKSPACE_PERMISSIONS","AI_CONFIG_SCHEMA","registerAiActivityActions","AI_PLUGIN_ENTITLEMENTS","pluginId","seatAddons","key","label","maxUnits","quota","perUnit","features","soldOn","lockdownFeatures","customerName","staffBypass","notice","title","body","apiPaths","exact","prefixes","orgPermissions","declared","registerAiDeclarations"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,2EAA2E;AAC3E,0EAA0E;AAC1E,aAAa;AACb,SACEA,kBAAkB,EAClBC,gBAAgB,QACX,2CAA0C;AACjD,SAASC,0BAA0B,QAAQ,4CAA2C;AACtF,SACEC,0BAA0B,QAErB,kDAAiD;AACxD,SAASC,YAAY,QAAQ,iBAAa;AAC1C,SAASC,kBAAkB,EAAEC,wBAAwB,QAAQ,4BAAwB;AACrF,SAASC,gBAAgB,QAAQ,qBAAiB;AAClD,SAASC,yBAAyB,QAAQ,oCAAgC;AAE1E;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMC,yBAAwD;IACnEC,UAAUN;IACVO,YAAY;QACV;YACEC,KAAK;YACLC,OAAO;YACPC,UAAU;YACV,kEAAkE;YAClE,kEAAkE;YAClE,wBAAwB;YACxBC,OAAO;gBACLH,KAAK;gBACLI,SAAShB;YACX;YACAiB,UAAU;gBAAC;gBAAgB;aAAW;YACtCC,QAAQjB;QACV;KACD;IACDgB,UAAU;QACR;YAAEL,KAAK;YAAYC,OAAO;QAAY;QACtC;YAAED,KAAK;YAAgBC,OAAO;QAAgB;KAC/C;IACDM,kBAAkB;QAChB;YACEP,KAAK;YACLC,OAAO;YACPO,cAAc;YACdC,aAAa;YACbC,QAAQ;gBACNC,OAAO;gBACPC,MAAM;YACR;YACA,sEAAsE;YACtE,+BAA+B;YAC/BC,UAAU;gBAAEC,OAAO;oBAAC;oBAAa;iBAAc;YAAC;QAClD;QACA;YACEd,KAAK;YACLC,OAAO;YACPO,cAAc;YACdC,aAAa;YACbC,QAAQ;gBACNC,OAAO;gBACPC,MAAM;YACR;YACA,mEAAmE;YACnE,+DAA+D;YAC/DC,UAAU;gBAAEE,UAAU;oBAAC;oBAAe;oBAAW;iBAAS;YAAC;QAC7D;KACD;IACDC,gBAAgB;WAAIvB;WAAuBC;KAAyB;AACtE,EAAC;AAED,IAAIuB,WAAW;AAEf;;;;;;;CAOC,GACD,OAAO,SAASC;IACd,IAAID,UAAU;IACdA,WAAW;IACX1B,2BAA2BM;IAC3BP,2BAA2BK;IAC3BC;AACF;AAEAsB"}
|
|
@@ -70,6 +70,18 @@ import { registerAiDeclarations } from "./declarations.js";
|
|
|
70
70
|
pluginId: AI_PLUGIN_ID
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
// THE PLATFORM'S PER-SITE TOGGLE DOOR (AGL-4047). The collaborators card
|
|
74
|
+
// sets every per-site key through the platform's generic door now, so this
|
|
75
|
+
// plugin keeps its display copy (`aiPermissions` on the site's roster row,
|
|
76
|
+
// which the native apps read) in step from the verdict that door wrote.
|
|
77
|
+
if (!listPluginEventHandlers('host.member.permissions.changed').includes(AI_PLUGIN_ID)) {
|
|
78
|
+
registerPluginEventHandler('host.member.permissions.changed', async ({ hostId, memberId, permissions })=>{
|
|
79
|
+
const { recordAiPermissionsOnRoster } = await import("./server/ai-host-permissions-roster.js");
|
|
80
|
+
await recordAiPermissionsOnRoster(hostId, memberId, permissions);
|
|
81
|
+
}, {
|
|
82
|
+
pluginId: AI_PLUGIN_ID
|
|
83
|
+
});
|
|
84
|
+
}
|
|
73
85
|
// THE PLATFORM'S BILLING EVENTS (AGL-3011). Subscribed at boot for the
|
|
74
86
|
// same reason as the two above: they are raised by the billing webhook,
|
|
75
87
|
// which never loads an AI door, and a workspace's overage standing moving
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/declarations.server.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// Each registry from its own module, not the `@aglyn/aglyn/server` barrel:\n// boot needs three registries rather than the whole server surface, and a\n// suite that stubs that barrel wholesale still loads these declarations\n// against the real registries.\nimport {\n listPluginEventHandlers,\n registerPluginEventHandler,\n} from '@aglyn/aglyn/plugin-manager/plugin-events'\nimport {\n listPluginUserErasers,\n registerPluginUserEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-user-erasure'\nimport {\n listUsageAlertContributors,\n registerUsageAlertContributor,\n} from '@aglyn/aglyn/plugin-manager/usage-alert-contributors'\nimport { registerPluginUsageMeter } from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { registerOperatorAlerts } from '@aglyn/aglyn/plugin-manager/operator-alerts'\nimport { registerStockPhotoPicker } from '@aglyn/aglyn/plugin-manager/stock-photo-provider'\nimport { AI_PLUGIN_ID, AI_USAGE_METER_ID } from './constants'\nimport { AI_BUILD_PARTLY_FAILED, AI_JOB_FAILED, AI_PROVIDER_UNAVAILABLE } from './operator-alerts'\nimport { registerAiDeclarations } from './declarations'\n\n/** The provider-spend staff alerts' contributor id under the plugin. */\nconst AI_USAGE_ALERTS_ID = 'provider-spend'\n\n/**\n * The plugin's SERVER declarations (AGL-2939): what the server must know\n * at boot, before any door of the plugin has been called. The two platform\n * events the plugin writes activity for — the AI add-on bought or dropped,\n * an `ai.*` permission moved — are raised by core billing and membership\n * routes that never load the plugin's API surface, so the subscriptions\n * have to be in place from the first request. So does the eraser: an\n * account erasure is a core route too, and a person's AI usage months sit\n * under each org, keyed by the uid, where no core delete reaches them. So\n * does the usage alert contributor (AGL-2984): the usage-alerts sweep is a\n * core cron, and it evaluates the plugin's staff alerts on provider spend for\n * every org it reads.\n *\n * Light at boot by construction: the writers behind the handlers are\n * imported when the first event arrives, and the alert rules when the sweep\n * first evaluates an org, not when the process starts, so the boot cost is\n * the registration and nothing that touches Firestore.\n */\nexport function registerAiServerDeclarations(): void {\n registerAiDeclarations()\n // Idempotent against each REGISTRY rather than a module flag, so a\n // registry reset (a spec) or a second module instance registers again.\n if (!listPluginEventHandlers('org.seatAddons.changed').includes(AI_PLUGIN_ID)) {\n registerPluginEventHandler(\n 'org.seatAddons.changed',\n async ({ orgId, actor, before, after }) => {\n const { logAiAddonChanged } = await import('./activity/ai-activity')\n await logAiAddonChanged(orgId, actor, { before, after })\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'org.permissions.changed',\n async ({ orgId, actor, subject, permission, granted }) => {\n if (!permission.startsWith('ai.')) return\n const { logAiPermissionChanged } = await import('./activity/ai-activity')\n await logAiPermissionChanged(orgId, actor, { subject, permission, granted })\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // THE PLATFORM'S BILLING EVENTS (AGL-3011). Subscribed at boot for the\n // same reason as the two above: they are raised by the billing webhook,\n // which never loads an AI door, and a workspace's overage standing moving\n // late is a workspace charged or refused against a fact we already had.\n //\n // The handler modules are imported when the first event arrives, so a\n // process that never receives one pays only for the registration.\n if (!listPluginEventHandlers('billing.invoice.paid').includes(AI_PLUGIN_ID)) {\n registerPluginEventHandler(\n 'billing.invoice.paid',\n async (payload) => {\n const { onAiBillingInvoicePaid } = await import('./billing/ai-overage-events')\n await onAiBillingInvoicePaid(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.invoice.failed',\n async (payload) => {\n const { onAiBillingInvoiceFailed } = await import('./billing/ai-overage-events')\n await onAiBillingInvoiceFailed(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.invoice.closed',\n async (payload) => {\n const { onAiBillingInvoiceClosed } = await import('./billing/ai-overage-events')\n await onAiBillingInvoiceClosed(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.dispute.opened',\n async (payload) => {\n const { onAiBillingDisputeOpened } = await import('./billing/ai-overage-events')\n await onAiBillingDisputeOpened(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.paymentMethod.changed',\n async (payload) => {\n const { onAiBillingPaymentMethodChanged } = await import(\n './billing/ai-overage-events'\n )\n await onAiBillingPaymentMethodChanged(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // The plugin's meter in the monthly usage sweep: the month's provider spend\n // onto the rollup, the credits past the band onto the invoice, and — from\n // the cutover month (AGL-3011) — the close-out of what the plugin invoices\n // itself. Registered at boot because the sweep is a core cron, and declared\n // in `usageAxes` so the sweep refuses to bill a month without it. The same\n // registration again replaces itself.\n registerPluginUsageMeter({\n pluginId: AI_PLUGIN_ID,\n id: AI_USAGE_METER_ID,\n measure: async (context) => {\n const { measureAiMonth } = await import('./billing/ai-month-meter')\n return measureAiMonth(context)\n },\n closeMonth: async (context) => {\n const { closeAiMonth } = await import('./billing/ai-month-meter')\n await closeAiMonth(context)\n },\n })\n if (!listPluginUserErasers().includes(AI_PLUGIN_ID)) {\n registerPluginUserEraser(\n async (request) => {\n const { eraseAiUsageForUser } = await import('./usage/ai-usage-eraser')\n return eraseAiUsageForUser(request)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // The platform provider account alert (AGL-3377), and a job failing or a\n // build finishing with parts failed on our side (AGL-3683), listed on\n // Staff → Operator alerts before the first one is raised.\n registerOperatorAlerts([AI_PROVIDER_UNAVAILABLE, AI_JOB_FAILED, AI_BUILD_PARTLY_FAILED], {\n pluginId: AI_PLUGIN_ID,\n })\n // The stock photo picker another plugin's draft writer asks for (AGL-3755):\n // an event's cover, chosen as a page's pictures are. The choosing loads on\n // the first pick; registering again replaces it.\n registerStockPhotoPicker(\n {\n pick: async (request) => {\n const { aiStockPhotoPick } = await import('./jobs/ai-stock-photo-picker')\n return aiStockPhotoPick(request)\n },\n },\n { pluginId: AI_PLUGIN_ID },\n )\n if (\n !listUsageAlertContributors().some(\n (contributor) =>\n contributor.pluginId === AI_PLUGIN_ID &&\n contributor.id === AI_USAGE_ALERTS_ID,\n )\n ) {\n registerUsageAlertContributor({\n pluginId: AI_PLUGIN_ID,\n id: AI_USAGE_ALERTS_ID,\n evaluate: async (context) => {\n const { evaluateAiUsageAlerts } = await import('./usage/ai-usage-alerts')\n await evaluateAiUsageAlerts(context)\n },\n quotaChecks: async (context) => {\n const { aiQuotaChecks } = await import('./usage/ai-quota-checks')\n return aiQuotaChecks(context)\n },\n })\n }\n}\n\nregisterAiServerDeclarations()\n"],"names":["listPluginEventHandlers","registerPluginEventHandler","listPluginUserErasers","registerPluginUserEraser","listUsageAlertContributors","registerUsageAlertContributor","registerPluginUsageMeter","registerOperatorAlerts","registerStockPhotoPicker","AI_PLUGIN_ID","AI_USAGE_METER_ID","AI_BUILD_PARTLY_FAILED","AI_JOB_FAILED","AI_PROVIDER_UNAVAILABLE","registerAiDeclarations","AI_USAGE_ALERTS_ID","registerAiServerDeclarations","includes","orgId","actor","before","after","logAiAddonChanged","pluginId","subject","permission","granted","startsWith","logAiPermissionChanged","payload","onAiBillingInvoicePaid","onAiBillingInvoiceFailed","onAiBillingInvoiceClosed","onAiBillingDisputeOpened","onAiBillingPaymentMethodChanged","id","measure","context","measureAiMonth","closeMonth","closeAiMonth","request","eraseAiUsageForUser","pick","aiStockPhotoPick","some","contributor","evaluate","evaluateAiUsageAlerts","quotaChecks","aiQuotaChecks"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/declarations.server.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// Each registry from its own module, not the `@aglyn/aglyn/server` barrel:\n// boot needs three registries rather than the whole server surface, and a\n// suite that stubs that barrel wholesale still loads these declarations\n// against the real registries.\nimport {\n listPluginEventHandlers,\n registerPluginEventHandler,\n} from '@aglyn/aglyn/plugin-manager/plugin-events'\nimport {\n listPluginUserErasers,\n registerPluginUserEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-user-erasure'\nimport {\n listUsageAlertContributors,\n registerUsageAlertContributor,\n} from '@aglyn/aglyn/plugin-manager/usage-alert-contributors'\nimport { registerPluginUsageMeter } from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { registerOperatorAlerts } from '@aglyn/aglyn/plugin-manager/operator-alerts'\nimport { registerStockPhotoPicker } from '@aglyn/aglyn/plugin-manager/stock-photo-provider'\nimport { AI_PLUGIN_ID, AI_USAGE_METER_ID } from './constants'\nimport { AI_BUILD_PARTLY_FAILED, AI_JOB_FAILED, AI_PROVIDER_UNAVAILABLE } from './operator-alerts'\nimport { registerAiDeclarations } from './declarations'\n\n/** The provider-spend staff alerts' contributor id under the plugin. */\nconst AI_USAGE_ALERTS_ID = 'provider-spend'\n\n/**\n * The plugin's SERVER declarations (AGL-2939): what the server must know\n * at boot, before any door of the plugin has been called. The two platform\n * events the plugin writes activity for — the AI add-on bought or dropped,\n * an `ai.*` permission moved — are raised by core billing and membership\n * routes that never load the plugin's API surface, so the subscriptions\n * have to be in place from the first request. So does the eraser: an\n * account erasure is a core route too, and a person's AI usage months sit\n * under each org, keyed by the uid, where no core delete reaches them. So\n * does the usage alert contributor (AGL-2984): the usage-alerts sweep is a\n * core cron, and it evaluates the plugin's staff alerts on provider spend for\n * every org it reads.\n *\n * Light at boot by construction: the writers behind the handlers are\n * imported when the first event arrives, and the alert rules when the sweep\n * first evaluates an org, not when the process starts, so the boot cost is\n * the registration and nothing that touches Firestore.\n */\nexport function registerAiServerDeclarations(): void {\n registerAiDeclarations()\n // Idempotent against each REGISTRY rather than a module flag, so a\n // registry reset (a spec) or a second module instance registers again.\n if (!listPluginEventHandlers('org.seatAddons.changed').includes(AI_PLUGIN_ID)) {\n registerPluginEventHandler(\n 'org.seatAddons.changed',\n async ({ orgId, actor, before, after }) => {\n const { logAiAddonChanged } = await import('./activity/ai-activity')\n await logAiAddonChanged(orgId, actor, { before, after })\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'org.permissions.changed',\n async ({ orgId, actor, subject, permission, granted }) => {\n if (!permission.startsWith('ai.')) return\n const { logAiPermissionChanged } = await import('./activity/ai-activity')\n await logAiPermissionChanged(orgId, actor, { subject, permission, granted })\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // THE PLATFORM'S PER-SITE TOGGLE DOOR (AGL-4047). The collaborators card\n // sets every per-site key through the platform's generic door now, so this\n // plugin keeps its display copy (`aiPermissions` on the site's roster row,\n // which the native apps read) in step from the verdict that door wrote.\n if (!listPluginEventHandlers('host.member.permissions.changed').includes(AI_PLUGIN_ID)) {\n registerPluginEventHandler(\n 'host.member.permissions.changed',\n async ({ hostId, memberId, permissions }) => {\n const { recordAiPermissionsOnRoster } = await import('./server/ai-host-permissions-roster')\n await recordAiPermissionsOnRoster(hostId, memberId, permissions)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // THE PLATFORM'S BILLING EVENTS (AGL-3011). Subscribed at boot for the\n // same reason as the two above: they are raised by the billing webhook,\n // which never loads an AI door, and a workspace's overage standing moving\n // late is a workspace charged or refused against a fact we already had.\n //\n // The handler modules are imported when the first event arrives, so a\n // process that never receives one pays only for the registration.\n if (!listPluginEventHandlers('billing.invoice.paid').includes(AI_PLUGIN_ID)) {\n registerPluginEventHandler(\n 'billing.invoice.paid',\n async (payload) => {\n const { onAiBillingInvoicePaid } = await import('./billing/ai-overage-events')\n await onAiBillingInvoicePaid(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.invoice.failed',\n async (payload) => {\n const { onAiBillingInvoiceFailed } = await import('./billing/ai-overage-events')\n await onAiBillingInvoiceFailed(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.invoice.closed',\n async (payload) => {\n const { onAiBillingInvoiceClosed } = await import('./billing/ai-overage-events')\n await onAiBillingInvoiceClosed(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.dispute.opened',\n async (payload) => {\n const { onAiBillingDisputeOpened } = await import('./billing/ai-overage-events')\n await onAiBillingDisputeOpened(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.paymentMethod.changed',\n async (payload) => {\n const { onAiBillingPaymentMethodChanged } = await import(\n './billing/ai-overage-events'\n )\n await onAiBillingPaymentMethodChanged(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // The plugin's meter in the monthly usage sweep: the month's provider spend\n // onto the rollup, the credits past the band onto the invoice, and — from\n // the cutover month (AGL-3011) — the close-out of what the plugin invoices\n // itself. Registered at boot because the sweep is a core cron, and declared\n // in `usageAxes` so the sweep refuses to bill a month without it. The same\n // registration again replaces itself.\n registerPluginUsageMeter({\n pluginId: AI_PLUGIN_ID,\n id: AI_USAGE_METER_ID,\n measure: async (context) => {\n const { measureAiMonth } = await import('./billing/ai-month-meter')\n return measureAiMonth(context)\n },\n closeMonth: async (context) => {\n const { closeAiMonth } = await import('./billing/ai-month-meter')\n await closeAiMonth(context)\n },\n })\n if (!listPluginUserErasers().includes(AI_PLUGIN_ID)) {\n registerPluginUserEraser(\n async (request) => {\n const { eraseAiUsageForUser } = await import('./usage/ai-usage-eraser')\n return eraseAiUsageForUser(request)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // The platform provider account alert (AGL-3377), and a job failing or a\n // build finishing with parts failed on our side (AGL-3683), listed on\n // Staff → Operator alerts before the first one is raised.\n registerOperatorAlerts([AI_PROVIDER_UNAVAILABLE, AI_JOB_FAILED, AI_BUILD_PARTLY_FAILED], {\n pluginId: AI_PLUGIN_ID,\n })\n // The stock photo picker another plugin's draft writer asks for (AGL-3755):\n // an event's cover, chosen as a page's pictures are. The choosing loads on\n // the first pick; registering again replaces it.\n registerStockPhotoPicker(\n {\n pick: async (request) => {\n const { aiStockPhotoPick } = await import('./jobs/ai-stock-photo-picker')\n return aiStockPhotoPick(request)\n },\n },\n { pluginId: AI_PLUGIN_ID },\n )\n if (\n !listUsageAlertContributors().some(\n (contributor) =>\n contributor.pluginId === AI_PLUGIN_ID &&\n contributor.id === AI_USAGE_ALERTS_ID,\n )\n ) {\n registerUsageAlertContributor({\n pluginId: AI_PLUGIN_ID,\n id: AI_USAGE_ALERTS_ID,\n evaluate: async (context) => {\n const { evaluateAiUsageAlerts } = await import('./usage/ai-usage-alerts')\n await evaluateAiUsageAlerts(context)\n },\n quotaChecks: async (context) => {\n const { aiQuotaChecks } = await import('./usage/ai-quota-checks')\n return aiQuotaChecks(context)\n },\n })\n }\n}\n\nregisterAiServerDeclarations()\n"],"names":["listPluginEventHandlers","registerPluginEventHandler","listPluginUserErasers","registerPluginUserEraser","listUsageAlertContributors","registerUsageAlertContributor","registerPluginUsageMeter","registerOperatorAlerts","registerStockPhotoPicker","AI_PLUGIN_ID","AI_USAGE_METER_ID","AI_BUILD_PARTLY_FAILED","AI_JOB_FAILED","AI_PROVIDER_UNAVAILABLE","registerAiDeclarations","AI_USAGE_ALERTS_ID","registerAiServerDeclarations","includes","orgId","actor","before","after","logAiAddonChanged","pluginId","subject","permission","granted","startsWith","logAiPermissionChanged","hostId","memberId","permissions","recordAiPermissionsOnRoster","payload","onAiBillingInvoicePaid","onAiBillingInvoiceFailed","onAiBillingInvoiceClosed","onAiBillingDisputeOpened","onAiBillingPaymentMethodChanged","id","measure","context","measureAiMonth","closeMonth","closeAiMonth","request","eraseAiUsageForUser","pick","aiStockPhotoPick","some","contributor","evaluate","evaluateAiUsageAlerts","quotaChecks","aiQuotaChecks"],"mappings":"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"}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types';
|
|
18
18
|
import { type PluginAiCapability, type ResolvedPluginAiCapability } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities';
|
|
19
|
-
import type
|
|
19
|
+
import { type AiBuildOps } from '../model/ai-build-job';
|
|
20
20
|
import { type AiPluginDraftOwnerLookup } from './ai-job-plugin-drafts';
|
|
21
21
|
/** The operations this plugin owns, as it registers them. */
|
|
22
22
|
export declare const AI_OWNED_CAPABILITIES: readonly PluginAiCapability[];
|
|
@@ -66,5 +66,7 @@ export declare function aiBuildOps(context: AiBuildOpsContext, deps?: AiBuildOps
|
|
|
66
66
|
* the job's own turn and never a cached system block.
|
|
67
67
|
*/
|
|
68
68
|
export declare function aiBuildOpLines(ops: AiBuildOps, structural: readonly string[]): string[];
|
|
69
|
+
/** What the plan is told where a build may change the theme (AGL-4077). */
|
|
70
|
+
export declare const AI_BUILD_THEME_OP_LINE = "A change to the theme is planned as the theme item, never as a theme change in create: at most one, and only when the request asks for a different look.";
|
|
69
71
|
/** What a build can make here, in the owner's words, for the chat's instructions. */
|
|
70
72
|
export declare function aiBuildIntents(ops: AiBuildOps): string[];
|
|
@@ -20,6 +20,7 @@ import { pluginAiCapabilities, registerPluginAiCapability } from "@aglyn/aglyn/p
|
|
|
20
20
|
import { filterEnabledPluginsByReleaseFlags } from "@aglyn/tenant-data-admin/server/release-flags";
|
|
21
21
|
import { AI_TEMPLATE_SUBJECTS } from "../model/ai-template-subjects.js";
|
|
22
22
|
import { AI_SITE_PASS_CREDITS } from "../model/ai-site-job.js";
|
|
23
|
+
import { AI_BUILD_THEME_OP } from "../model/ai-build-job.js";
|
|
23
24
|
import { AI_LOGIC_FUNCTION_RESOURCE } from "../model/ai-logic-job.js";
|
|
24
25
|
import { AI_AUTOMATION_RESOURCE } from "../model/ai-workflow-job.js";
|
|
25
26
|
import { AI_EDIT_TARGET_KINDS } from "../model/ai-edit-job.js";
|
|
@@ -46,8 +47,9 @@ import { AI_PRIVACY_ARGS_SCHEMA, AI_PRIVACY_WHERE } from "./ai-job-privacy-step.
|
|
|
46
47
|
* footer menu the `navigation` runner's (AGL-3728), a copy of something
|
|
47
48
|
* it has the `duplicate` runner's (AGL-3740), a change to its settings
|
|
48
49
|
* the `settings` runner's (AGL-3748), organizing its media library the
|
|
49
|
-
* `media` runner's (AGL-3752),
|
|
50
|
-
* advertising tags the `tracking` runner's (AGL-3751)
|
|
50
|
+
* `media` runner's (AGL-3752), a change to its analytics and
|
|
51
|
+
* advertising tags the `tracking` runner's (AGL-3751), and a change to its
|
|
52
|
+
* theme the `theme` runner's proposal (AGL-4077).
|
|
51
53
|
*/ /** This plugin's id, as its registrations name their owner. */ const AI_PLUGIN_ID = 'ai';
|
|
52
54
|
const NO_ARGS = {
|
|
53
55
|
type: 'object',
|
|
@@ -481,6 +483,25 @@ const passes = (count)=>()=>count * AI_SITE_PASS_CREDITS;
|
|
|
481
483
|
// Written by code from the plan's values: no model pass.
|
|
482
484
|
estimateCredits: ()=>0,
|
|
483
485
|
degrade: 'omit'
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
// A change to the site's theme (AGL-4077): the theme runner's proposal —
|
|
489
|
+
// the same one the Theme assistant makes — which the owner previews and
|
|
490
|
+
// applies in Setup → Theme. The build writes nothing to the theme, so it
|
|
491
|
+
// is never live. One a plan, on paid plans only; the Free taste changes
|
|
492
|
+
// its theme in Setup → Theme.
|
|
493
|
+
op: AI_BUILD_THEME_OP,
|
|
494
|
+
noun: 'theme change',
|
|
495
|
+
where: 'Setup → Theme',
|
|
496
|
+
intents: [
|
|
497
|
+
'a change to the site’s theme — its colors, fonts or corners — proposed for the owner to apply in Setup → Theme; the build never changes the live theme'
|
|
498
|
+
],
|
|
499
|
+
argsSchema: NO_ARGS,
|
|
500
|
+
maxPerPlan: 1,
|
|
501
|
+
freeAllowed: false,
|
|
502
|
+
runnerKind: 'theme',
|
|
503
|
+
estimateCredits: passes(1),
|
|
504
|
+
degrade: 'omit'
|
|
484
505
|
}
|
|
485
506
|
];
|
|
486
507
|
/**
|
|
@@ -577,12 +598,16 @@ const passes = (count)=>()=>count * AI_SITE_PASS_CREDITS;
|
|
|
577
598
|
});
|
|
578
599
|
lines.push(`- ${one.op}: ${aiNounWithArticle(one.noun)}, at most ${one.maxPerPlan}; for ${one.intents.join('; ')}.${((_one_pageBlocks = one.pageBlocks) == null ? void 0 : _one_pageBlocks.length) ? ` A page section that uses it places the ${one.pageBlocks.join(' or ')} block: put new:<name> in that section's uses.` : ''} Args: ${args.length ? `{ ${args.join('; ')} }` : '{}'}.`);
|
|
579
600
|
}
|
|
601
|
+
// The plan's own vocabulary has a theme change in create, which a build
|
|
602
|
+
// does not build there (AGL-4077): the item is the one way it is made.
|
|
603
|
+
if (ops.has(AI_BUILD_THEME_OP)) lines.push(AI_BUILD_THEME_OP_LINE);
|
|
580
604
|
// Measured live on the canonical request (AGL-3616): asked for "forms for
|
|
581
605
|
// bookings", the planner made a plain form rather than the operation that
|
|
582
606
|
// takes bookings. An operation does the job; a form that imitates it does not.
|
|
583
607
|
lines.push('When the request asks for what one of these operations makes, plan that operation as an item, and place it on the page that offers it. Never stand in for it with a form, page or component that only imitates it: a form that collects what the operation would handle does not do its job. A form is for what no operation here makes, such as general inquiries.');
|
|
584
608
|
return lines;
|
|
585
609
|
}
|
|
610
|
+
/** What the plan is told where a build may change the theme (AGL-4077). */ export const AI_BUILD_THEME_OP_LINE = 'A change to the theme is planned as the theme item, never as a theme change in create: at most one, and only when the request asks for a different look.';
|
|
586
611
|
function aiNounWithArticle(noun) {
|
|
587
612
|
return `${/^[aeiou]/i.test(noun) ? 'an' : 'a'} ${noun}`;
|
|
588
613
|
}
|