@aglyn/plugins-ai 1.0.0-beta.218 → 1.0.0-beta.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +17 -17
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/activity/ai-activity-actions.js +8 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-assist-provider.component.js +2 -2
- package/src/lib/components/ai-assist-provider.component.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.js +3 -3
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-experiment-cards.component.js +1 -1
- package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.d.ts +8 -0
- package/src/lib/components/ai-job-plan.component.js +21 -4
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
- package/src/lib/components/ai-permissions-on-host.component.js +12 -3
- package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
- package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
- package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
- package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
- package/src/lib/components/ai-product-import-option.component.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +124 -0
- package/src/lib/components/ai-product-zones.js +32 -0
- package/src/lib/components/ai-product-zones.js.map +1 -0
- package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-seo-audit-card.component.js +35 -92
- package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
- package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +21 -15
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +2 -2
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/assist-signals-page.component.js +2 -2
- package/src/lib/components/assist-signals-page.component.js.map +1 -1
- package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
- package/src/lib/jobs/ai-email-bindings.js +75 -42
- package/src/lib/jobs/ai-email-bindings.js.map +1 -1
- package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
- package/src/lib/jobs/ai-job-campaign-step.js +5 -10
- package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
- package/src/lib/jobs/ai-job-component-step.js +9 -3
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.js +1 -1
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +0 -2
- package/src/lib/jobs/ai-job-email-step.js +1 -3
- package/src/lib/jobs/ai-job-email-step.js.map +1 -1
- package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
- package/src/lib/jobs/ai-job-generation.d.ts +1 -1
- package/src/lib/jobs/ai-job-generation.js +10 -2
- package/src/lib/jobs/ai-job-generation.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +1 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
- package/src/lib/jobs/ai-job-page-sections.js +7 -4
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +7 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-conformance.js +33 -0
- package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +3 -0
- package/src/lib/jobs/ai-job-plan-step.js +29 -17
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
- package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
- package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-products-step.js +54 -32
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
- package/src/lib/jobs/ai-job-seo-step.js +35 -108
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +1 -0
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-theme-step.js +1 -1
- package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
- package/src/lib/jobs/ai-job-workflow-step.js +17 -9
- package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
- package/src/lib/jobs/ai-workflow-records.d.ts +19 -11
- package/src/lib/jobs/ai-workflow-records.js +60 -27
- package/src/lib/jobs/ai-workflow-records.js.map +1 -1
- package/src/lib/model/ai-allotments.d.ts +11 -3
- package/src/lib/model/ai-allotments.js +21 -4
- package/src/lib/model/ai-allotments.js.map +1 -1
- package/src/lib/model/ai-automation-outline.d.ts +21 -2
- package/src/lib/model/ai-automation-outline.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +57 -0
- package/src/lib/model/ai-build-plan.js +116 -7
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-seo.d.ts +11 -51
- package/src/lib/model/ai-seo.js +0 -17
- package/src/lib/model/ai-seo.js.map +1 -1
- package/src/lib/model/ai-site-inventory.js +1 -1
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-template-subjects.js +2 -2
- package/src/lib/model/ai-template-subjects.js.map +1 -1
- package/src/lib/model/ai-theme-proposal.d.ts +1 -1
- package/src/lib/model/ai-theme-proposal.js +1 -1
- package/src/lib/model/ai-theme-proposal.js.map +1 -1
- package/src/lib/model/ai-workflow-job.d.ts +1 -3
- package/src/lib/model/ai-workflow-job.js +1 -2
- package/src/lib/model/ai-workflow-job.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +149 -0
- package/src/lib/plan-entitlements.js +168 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +1 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
- package/src/lib/runtime/ai-doctrine-validators.js +178 -21
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +5 -5
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.js +29 -14
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-eval.d.ts +8 -1
- package/src/lib/runtime/ai-eval.js +29 -2
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.js +3 -3
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +6 -0
- package/src/lib/runtime/ai-palette.generated.js +15 -12
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +7 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/seo-audit.d.ts +13 -63
- package/src/lib/runtime/seo-audit.js +22 -207
- package/src/lib/runtime/seo-audit.js.map +1 -1
- package/src/lib/runtime/site-inventory.js +1 -1
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-assist-prompts.js +3 -3
- package/src/lib/server/ai-assist-prompts.js.map +1 -1
- package/src/lib/server/assist-chat.js +11 -10
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-view-context.js +23 -23
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.d.ts +1 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
- package/src/lib/testing/stand-in-automation-index.js +102 -0
- package/src/lib/testing/stand-in-automation-index.js.map +1 -0
- package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
- package/src/lib/testing/stand-in-list-send-time.js +77 -0
- package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
- package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
- package/src/lib/testing/stand-in-pipeline-index.js +59 -0
- package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
- package/src/lib/testing/stand-in-product-index.d.ts +20 -0
- package/src/lib/testing/stand-in-product-index.js +105 -0
- package/src/lib/testing/stand-in-product-index.js.map +1 -0
- package/src/lib/tools/ai-component-tool.js +2 -2
- package/src/lib/tools/ai-component-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/tools/ai-seo-tool.d.ts +6 -18
- package/src/lib/tools/ai-seo-tool.js +7 -38
- package/src/lib/tools/ai-seo-tool.js.map +1 -1
- package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
- package/src/lib/tools/ai-workflow-tool.js +1 -1
- package/src/lib/tools/ai-workflow-tool.js.map +1 -1
- package/src/lib/usage/ai-allotment-alerts.js +8 -1
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
- package/src/lib/usage/assist-free-taste.js +2 -1
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage-axes.d.ts +39 -0
- package/src/lib/usage-axes.js +70 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/runtime/seo-page-facts.d.ts +0 -61
- package/src/lib/runtime/seo-page-facts.js +0 -150
- package/src/lib/runtime/seo-page-facts.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-theme-proposal.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CONTRAST_AA,\n CONTRAST_AA_LARGE,\n contrastRatio,\n type HostThemeSource,\n} from '@aglyn/aglyn/app-utils/marketplace-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport { accessibleShade } from '@aglyn/shared-ui-theme/util/accessible-shade'\nimport { inheritedThemeColor } from '@aglyn/shared-ui-theme/util/theme-editor-defaults'\nimport {\n readDarkScheme,\n readFontFamily,\n readThemeColor,\n readToolbarHeight,\n resetComponentOverrides,\n SYSTEM_FONT_VALUE,\n THEME_COLOR_FIELDS,\n THEME_EDITOR_CONTROLS,\n THEME_EDITOR_MEDIA_QUERIES,\n THEME_EDITOR_SCHEMES,\n writeBorderRadius,\n writeComponentOverride,\n writeDarkScheme,\n writeFontFamily,\n writeSpacing,\n writeThemeColor,\n writeToolbarHeight,\n type ThemeColorToken,\n type ThemeComponentOverrideLeaf,\n type ThemeEditorControlId,\n} from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport { prefersDarkInk } from '@aglyn/shared-util-tools/contrast'\n\n/**\n * A theme proposal (AGL-2938): what a theme job hands a person to apply.\n *\n * Not a theme. A proposal is a set of theme editor controls set to values,\n * applied to whatever the site's theme is when the person applies it, through\n * the same writes the editor performs. A proposal that names three colors\n * changes three colors, and every control it does not name is untouched by\n * construction rather than by the model's restraint — which is what makes\n * \"make it feel warmer\" a targeted diff instead of a regenerated theme.\n *\n * Pure and free of anything server-side: the job step builds a proposal, and\n * the Theme section's widget previews and applies one, from this module.\n */\n\nexport const AI_THEME_PROPOSAL_VERSION = 1\n\n/**\n * `modify` changes what the brief is about and nothing else; `create` is a\n * whole design over every control the editor has.\n */\nexport type AiThemeProposalMode = 'modify' | 'create'\n\nexport const AI_THEME_PROPOSAL_MODES: readonly AiThemeProposalMode[] = ['modify', 'create']\n\n/** The parts of a theme a brief can be about. */\nexport type AiThemeChangeGroup =\n | 'color'\n | 'darkScheme'\n | 'typography'\n | 'shape'\n | 'spacing'\n | 'navigation'\n | 'components'\n\nexport const AI_THEME_CHANGE_GROUPS: readonly AiThemeChangeGroup[] = [\n 'color',\n 'darkScheme',\n 'typography',\n 'shape',\n 'spacing',\n 'navigation',\n 'components',\n]\n\n/** The controls a proposal sets one value on; component overrides are leaves of their own. */\nexport type AiThemeValueControl = Exclude<ThemeEditorControlId, 'components'>\n\n/** Every value control, as the catalog lists them. */\nexport const AI_THEME_VALUE_CONTROLS: readonly AiThemeValueControl[] = THEME_EDITOR_CONTROLS.filter(\n (control) => control.id !== 'components',\n).map((control) => control.id as AiThemeValueControl)\n\n/** One editor control set to a value, or back to its default with `null`. */\nexport interface AiThemeControlChange {\n control: AiThemeValueControl\n /** The scheme a color is set for; `null` for every other control. */\n scheme: HostThemeScheme | null\n value: string | number | null\n /** What the site had set when the proposal was made; `null` when it inherited. */\n before: string | number | null\n}\n\n/** A value the proposal carries that the model did not write, and why. */\nexport interface AiThemeCorrection {\n control: `color.${ThemeColorToken}`\n scheme: HostThemeScheme\n /** The value before the correction; `null` when the slot inherited. */\n from: string | null\n to: string\n /**\n * `contrast` — the pair fell below its bar and this is the nearest shade\n * of the foreground that clears it. `dark-value` — the proposal changed an\n * accent for light and gave no dark value, and this keeps the same hue\n * readable on the dark background.\n */\n reason: 'contrast' | 'dark-value'\n /** The colors a contrast correction was measured against. */\n against?: string[]\n /** The worst ratio before the correction. */\n ratio?: number\n /** The bar the pair has to clear. */\n required?: number\n}\n\nexport interface AiThemeProposal {\n version: typeof AI_THEME_PROPOSAL_VERSION\n mode: AiThemeProposalMode\n /** One sentence naming what the proposal changes, in the brief's language. */\n summary: string\n /**\n * Where the site's theme came from when the proposal was made, which is\n * how the editor's Save stores it: an installed theme's edits become its\n * override patch, a default theme stores only the changed values, and a\n * site's own theme changes in place.\n */\n source: HostThemeSource\n changes: AiThemeControlChange[]\n components: ThemeComponentOverrideLeaf[]\n /** The site's own component overrides are dropped before `components` apply. */\n resetComponents: boolean\n corrections: AiThemeCorrection[]\n /** What the model proposed that the proposal leaves out, in words. */\n dropped: string[]\n /** Anything else a person should read before applying, in words. */\n notes: string[]\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading and applying\n * ------------------------------------------------------------------------ */\n\n/** The color token a control writes, or `null` for a control that is not a color. */\nexport function aiThemeColorToken(control: string): ThemeColorToken | null {\n return control.startsWith('color.')\n ? (control.slice('color.'.length) as ThemeColorToken)\n : null\n}\n\n/** What a control holds on a theme, as a proposal's `before` names it. */\nexport function readAiThemeControl(\n theme: HostTheme | undefined,\n control: AiThemeValueControl,\n scheme: HostThemeScheme | null,\n): string | number | null {\n const token = aiThemeColorToken(control)\n if (token) return readThemeColor(theme, scheme ?? 'light', token) ?? null\n switch (control) {\n case 'darkScheme':\n return readDarkScheme(theme) === 'off' ? 'off' : null\n case 'fontFamily': {\n const family = readFontFamily(theme)\n return family === SYSTEM_FONT_VALUE ? null : family\n }\n case 'borderRadius':\n return typeof theme?.shape?.borderRadius === 'number' ? theme.shape.borderRadius : null\n case 'spacing':\n return typeof theme?.spacing === 'number' ? theme.spacing : null\n case 'navHeight.xs':\n return (theme && readToolbarHeight(theme, 'xs')) ?? null\n case 'navHeight.sm':\n return (theme && readToolbarHeight(theme, 'sm')) ?? null\n default:\n return null\n }\n}\n\nfunction applyControlChange(\n theme: HostTheme,\n change: Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>,\n): HostTheme {\n const { control, scheme, value } = change\n const token = aiThemeColorToken(control)\n if (token) {\n return writeThemeColor(\n theme,\n scheme ?? 'light',\n token,\n typeof value === 'string' ? value : undefined,\n )\n }\n const number = typeof value === 'number' ? value : undefined\n switch (control) {\n case 'darkScheme':\n return writeDarkScheme(theme, value === 'off' ? 'off' : 'auto')\n case 'fontFamily':\n return writeFontFamily(theme, typeof value === 'string' ? value : SYSTEM_FONT_VALUE)\n case 'borderRadius':\n return writeBorderRadius(theme, number)\n case 'spacing':\n return writeSpacing(theme, number)\n case 'navHeight.xs':\n return writeToolbarHeight(theme, 'xs', number)\n case 'navHeight.sm':\n return writeToolbarHeight(theme, 'sm', number)\n default:\n return theme\n }\n}\n\n/**\n * The theme a set of changes produces on top of `theme`: each change written\n * the way the editor writes it, then the component overrides. Returns a new\n * object; `theme` is not mutated.\n */\nexport function applyAiThemeChanges(\n theme: HostTheme | undefined,\n proposal: {\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n },\n): HostTheme {\n let next: HostTheme = { ...(theme ?? {}) }\n for (const change of proposal.changes) next = applyControlChange(next, change)\n if (proposal.resetComponents) next = resetComponentOverrides(next)\n for (const leaf of proposal.components) next = writeComponentOverride(next, leaf)\n return next\n}\n\n/* ------------------------------------------------------------------------ *\n * Scope: which parts of the theme a brief is about\n * ------------------------------------------------------------------------ */\n\n/** The part of the theme a value control belongs to. */\nexport function aiThemeControlGroup(control: AiThemeValueControl): AiThemeChangeGroup {\n if (aiThemeColorToken(control)) return 'color'\n switch (control) {\n case 'darkScheme':\n return 'darkScheme'\n case 'fontFamily':\n return 'typography'\n case 'borderRadius':\n return 'shape'\n case 'spacing':\n return 'spacing'\n default:\n return 'navigation'\n }\n}\n\n/**\n * The part of the theme a component override belongs to: the type ramp's\n * overrides are typography, the app bar's and the toolbar's are navigation,\n * and the rest are components.\n */\nexport function aiThemeComponentGroup(\n component: ThemeComponentOverrideLeaf['component'],\n): AiThemeChangeGroup {\n if (component === 'MuiTypography') return 'typography'\n if (component === 'MuiAppBar' || component === 'MuiToolbar') return 'navigation'\n return 'components'\n}\n\n/**\n * The words that tie a brief to a part of the theme, matched case-insensitively\n * on whole words. A brief can name several parts.\n */\nconst BRIEF_VOCABULARY: Record<AiThemeChangeGroup, RegExp> = {\n color:\n /(#[0-9a-f]{3,8}\\b)|\\b(colou?rs?|palettes?|hues?|warm(er|th|ly)?|cool(er)?|cold(er)?|contrast\\w*|bright(er|ness)?|dark(er|en)?|light(er|en)?|saturat\\w*|desaturat\\w*|muted|vibrant|pastels?|tones?|tints?|shades?|accents?|primary|secondary|tertiary|backgrounds?|surfaces?|brand\\w*|earthy|neon|moody|red|orange|yellow|green|blue|teal|purple|violet|pink|magenta|brown|beige|cream|gold|navy|black|white|gr[ae]y|hex)\\b/i,\n darkScheme: /\\b(dark (mode|scheme|theme)|light (mode|scheme)|always light|night mode)\\b/i,\n typography:\n /\\b(fonts?|typefaces?|typography|serif|sans|monospace|headings?|headlines?|titles?|type ?scale|text size|font size|lettering|weights?|bold(er)?|editorial)\\b/i,\n shape: /\\b(radius|radii|round(ed|er|ness)?|corners?|sharp(er)?|square(d|r)?|pills?|curv\\w*)\\b/i,\n spacing:\n /\\b(spac\\w*|padding|margins?|roomy|airy|tight(er)?|compact|dense|breathing room|white ?space|gaps?)\\b/i,\n navigation: /\\b(nav\\w*|header height|toolbar|app ?bar|menu bar|top bar)\\b/i,\n components:\n /\\b(buttons?|cards?|chips?|inputs?|text ?fields?|links?|tabs?|switch(es)?|sliders?|avatars?|badges?|tooltips?|dividers?|menus?|components?|uppercase|capitali[sz]\\w*|shadows?|elevation|outlined|underlin\\w*)\\b/i,\n}\n\n/**\n * The parts of the theme a job may change. A `create` job is a whole design.\n * A `modify` job may change only what its brief is about, so \"make it feel\n * warmer\" reaches the colors and nothing else; a brief that names no part at\n * all — in another language, or in words this does not know — is taken to\n * be about the whole design rather than refused.\n */\nexport function aiThemeBriefScope(\n brief: string,\n mode: AiThemeProposalMode,\n): ReadonlySet<AiThemeChangeGroup> {\n if (mode === 'create') return new Set(AI_THEME_CHANGE_GROUPS)\n const named = AI_THEME_CHANGE_GROUPS.filter((group) => BRIEF_VOCABULARY[group].test(brief))\n return new Set(named.length ? named : AI_THEME_CHANGE_GROUPS)\n}\n\nconst GROUP_WORDS: Record<AiThemeChangeGroup, string> = {\n color: 'colors',\n darkScheme: 'the dark scheme',\n typography: 'typography',\n shape: 'corners',\n spacing: 'spacing',\n navigation: 'navigation',\n components: 'component styles',\n}\n\n/* ------------------------------------------------------------------------ *\n * Colors as the site renders them, and dark values\n * ------------------------------------------------------------------------ */\n\nconst colorKey = (scheme: HostThemeScheme, token: ThemeColorToken) => `${scheme}:${token}`\n\n/** A color as the site renders it in one scheme: its own value, else the brand's. */\nexport function effectiveThemeColor(\n theme: HostTheme | undefined,\n scheme: HostThemeScheme,\n token: ThemeColorToken,\n): string | undefined {\n return readThemeColor(theme, scheme, token) ?? inheritedThemeColor(scheme, token)\n}\n\n/** The accents: the colors whose dark value can follow their light one. */\nconst ACCENT_TOKENS: ReadonlySet<ThemeColorToken> = new Set(\n THEME_COLOR_FIELDS.filter((field) => field.group === 'palette').map((field) => field.token),\n)\n\n/**\n * The colors a set of changes gives a light value and no dark one, while the\n * site's dark scheme is on. Each is a visitor in dark mode seeing half a\n * change.\n */\nexport function aiThemeMissingDarkValues(\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n themeAfter: HostTheme,\n): ThemeColorToken[] {\n if (readDarkScheme(themeAfter) === 'off') return []\n const set = new Set(\n changes\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n return THEME_COLOR_FIELDS.map(({ token }) => token).filter(\n (token) => set.has(colorKey('light', token)) && !set.has(colorKey('dark', token)),\n )\n}\n\n/**\n * The dark value an accent keeps when a proposal gave none: the same hue,\n * lightened only as far as it takes to read on the dark background as a fill\n * or an indicator. Surfaces, text and tints have no such value — a pale light\n * surface lightened is still a light surface — so they go on inheriting the\n * platform's dark palette, which is what a site with no dark design renders.\n */\nexport function deriveAiThemeDarkAccent(light: string, themeAfter: HostTheme): string | null {\n const background = effectiveThemeColor(themeAfter, 'dark', 'background.default')\n if (!background) return null\n try {\n return accessibleShade(light, [background], 'lighten', { minContrast: CONTRAST_AA_LARGE })\n } catch {\n return null\n }\n}\n\n/* ------------------------------------------------------------------------ *\n * Contrast\n * ------------------------------------------------------------------------ */\n\n/** A foreground that has to read on each of its backgrounds. */\nexport interface AiThemeContrastPair {\n foreground: ThemeColorToken\n backgrounds: readonly ThemeColorToken[]\n required: number\n}\n\n/**\n * The pairs a proposal is held to. Body text on the page and on paper, and\n * secondary text on the page, are the pairs the theme publish check\n * (`validateThemeForPublish`) measures, at its bars; the primary color on\n * the page is held to the bar a fill or an indicator owes.\n */\nexport const AI_THEME_CONTRAST_PAIRS: readonly AiThemeContrastPair[] = [\n {\n foreground: 'text.primary',\n backgrounds: ['background.default', 'background.paper'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'text.secondary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'primary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA_LARGE,\n },\n]\n\nconst round2 = (ratio: number) => Math.round(ratio * 100) / 100\n\n/**\n * Whether dark ink reads better than light ink on a background. A hex takes\n * the shared helper; anything else a theme can hold (`rgba()`) is measured\n * against black and white directly, which is the same comparison.\n */\nfunction darkInkReads(background: string): boolean {\n const verdict = prefersDarkInk(background)\n if (verdict !== null) return verdict\n return (contrastRatio('#000000', background) ?? 0) >= (contrastRatio('#ffffff', background) ?? 0)\n}\n\nexport interface AiThemeContrastResult {\n theme: HostTheme\n corrections: AiThemeCorrection[]\n notes: string[]\n}\n\n/**\n * Every pair a set of changes takes below its bar, corrected to the nearest\n * shade of its foreground that clears it, and named.\n *\n * Only pairs the changes touched are corrected. A pair that already fell\n * short with neither of its colors changed is named in `notes` and left\n * alone, because correcting it would change a color the brief did not ask\n * about. A dark scheme the site has switched off is not measured: no visitor\n * sees it.\n */\nexport function correctAiThemeContrast(\n themeAfter: HostTheme,\n touchedChanges: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n): AiThemeContrastResult {\n const touched = new Set(\n touchedChanges\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n const schemes = THEME_EDITOR_SCHEMES.filter(\n (scheme) => scheme === 'light' || readDarkScheme(themeAfter) !== 'off',\n )\n let theme = themeAfter\n const corrections: AiThemeCorrection[] = []\n const notes: string[] = []\n for (const scheme of schemes) {\n for (const pair of AI_THEME_CONTRAST_PAIRS) {\n const foreground = effectiveThemeColor(theme, scheme, pair.foreground)\n const backgrounds = pair.backgrounds\n .map((token) => effectiveThemeColor(theme, scheme, token))\n .filter((value): value is string => typeof value === 'string')\n if (!foreground || backgrounds.length !== pair.backgrounds.length) continue\n const ratios = backgrounds.map((background) => contrastRatio(foreground, background))\n if (ratios.some((ratio) => ratio === null)) continue\n const worst = Math.min(...(ratios as number[]))\n if (worst >= pair.required) continue\n const involved = [pair.foreground, ...pair.backgrounds].some((token) =>\n touched.has(colorKey(scheme, token)),\n )\n if (!involved) {\n notes.push(\n `${pair.foreground} on ${pair.backgrounds.join(' and ')} in ${scheme} was already ` +\n `${round2(worst)}:1 before this proposal, below ${pair.required}:1.`,\n )\n continue\n }\n let corrected: string\n try {\n corrected = accessibleShade(\n foreground,\n backgrounds,\n darkInkReads(backgrounds[0]) ? 'darken' : 'lighten',\n { minContrast: pair.required },\n )\n } catch {\n continue\n }\n const clears = backgrounds.every(\n (background) => (contrastRatio(corrected, background) ?? 0) >= pair.required,\n )\n if (!clears) {\n notes.push(\n `${pair.foreground} cannot reach ${pair.required}:1 on the ${scheme} background by ` +\n 'changing its shade alone; the background needs to change too.',\n )\n continue\n }\n corrections.push({\n control: `color.${pair.foreground}`,\n scheme,\n from: readThemeColor(theme, scheme, pair.foreground) ?? null,\n to: corrected,\n reason: 'contrast',\n against: backgrounds,\n ratio: round2(worst),\n required: pair.required,\n })\n theme = writeThemeColor(theme, scheme, pair.foreground, corrected)\n }\n }\n return { theme, corrections, notes }\n}\n\n/** Folds a correction into a change list: the change it corrects, or a new one. */\nfunction withCorrection(\n changes: AiThemeControlChange[],\n correction: AiThemeCorrection,\n base: HostTheme | undefined,\n): AiThemeControlChange[] {\n const index = changes.findIndex(\n (change) => change.control === correction.control && change.scheme === correction.scheme,\n )\n if (index >= 0) {\n return changes.map((change, at) => (at === index ? { ...change, value: correction.to } : change))\n }\n return [\n ...changes,\n {\n control: correction.control,\n scheme: correction.scheme,\n value: correction.to,\n before: readAiThemeControl(base, correction.control, correction.scheme),\n },\n ]\n}\n\n/* ------------------------------------------------------------------------ *\n * Building a proposal\n * ------------------------------------------------------------------------ */\n\nexport interface BuildAiThemeProposalInput {\n /** The site's theme as the editor shows it, overrides resolved. */\n base: HostTheme | undefined\n source: HostThemeSource\n mode: AiThemeProposalMode\n brief: string\n summary: string\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n /** What reading the model's answer already left out, in words. */\n dropped?: readonly string[]\n notes?: readonly string[]\n}\n\n/**\n * A proposal from what the model proposed, held to the editor and to the\n * doctrine: changes that change nothing are gone, a modify brief reaches\n * only the parts of the theme it names, accents keep a dark value, and every\n * pair the proposal touches clears its contrast bar.\n */\nexport function buildAiThemeProposal(input: BuildAiThemeProposalInput): AiThemeProposal {\n const dropped = [...(input.dropped ?? [])]\n const notes = [...(input.notes ?? [])]\n const scope = aiThemeBriefScope(input.brief, input.mode)\n const outOfScope = (group: AiThemeChangeGroup, what: string) => {\n dropped.push(`${what}: the brief did not ask about ${GROUP_WORDS[group]}`)\n }\n\n let changes: AiThemeControlChange[] = []\n for (const change of input.changes) {\n const before = readAiThemeControl(input.base, change.control, change.scheme)\n if (before === change.value) continue\n const group = aiThemeControlGroup(change.control)\n if (!scope.has(group)) {\n outOfScope(group, change.scheme ? `${change.control} (${change.scheme})` : change.control)\n continue\n }\n changes.push({ ...change, before })\n }\n const components = input.components.filter((leaf) => {\n const group = aiThemeComponentGroup(leaf.component)\n if (scope.has(group)) return true\n outOfScope(group, `${leaf.component}.${leaf.property}`)\n return false\n })\n let resetComponents = input.resetComponents\n if (resetComponents && !scope.has('components')) {\n outOfScope('components', 'Resetting the component overrides')\n resetComponents = false\n }\n\n const corrections: AiThemeCorrection[] = []\n let after = applyAiThemeChanges(input.base, { changes, components, resetComponents })\n for (const token of aiThemeMissingDarkValues(changes, after)) {\n const light = changes.find(\n (change) => change.scheme === 'light' && aiThemeColorToken(change.control) === token,\n )\n if (!ACCENT_TOKENS.has(token) || typeof light?.value !== 'string') {\n notes.push(`${token} keeps its current dark value; only its light value changes.`)\n continue\n }\n const dark = deriveAiThemeDarkAccent(light.value, after)\n if (!dark) continue\n const correction: AiThemeCorrection = {\n control: `color.${token}`,\n scheme: 'dark',\n from: readThemeColor(input.base, 'dark', token) ?? null,\n to: dark,\n reason: 'dark-value',\n }\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n after = writeThemeColor(after, 'dark', token, dark)\n }\n\n const contrast = correctAiThemeContrast(after, changes)\n for (const correction of contrast.corrections) {\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n }\n notes.push(...contrast.notes)\n\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode: input.mode,\n summary: input.summary,\n source: input.source,\n changes,\n components,\n resetComponents,\n corrections,\n dropped,\n notes,\n }\n}\n\n/**\n * What applying a proposal to the site's CURRENT theme produces, held to the\n * contrast bars again: the theme may have changed since the proposal was\n * made, and a proposal that cleared its bars against the old background can\n * fall short against the new one. The corrections this makes are named\n * beside the proposal's own.\n */\nexport function aiThemeProposalAfter(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes' | 'components' | 'resetComponents'>,\n): AiThemeContrastResult {\n const applied = applyAiThemeChanges(theme, proposal)\n return correctAiThemeContrast(applied, proposal.changes)\n}\n\n/** The changes whose `before` no longer matches the theme a proposal is applied to. */\nexport function aiThemeStaleChanges(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes'>,\n): AiThemeControlChange[] {\n return proposal.changes.filter(\n (change) => readAiThemeControl(theme, change.control, change.scheme) !== change.before,\n )\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading a stored proposal back\n * ------------------------------------------------------------------------ */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst strings = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n\nconst scalar = (value: unknown): string | number | null =>\n typeof value === 'string' || typeof value === 'number' ? value : null\n\nconst VALUE_CONTROLS: ReadonlySet<string> = new Set(AI_THEME_VALUE_CONTROLS)\n\n/**\n * A proposal as a job output stores it, or `null` for anything that is not\n * one. The job writes these through the Admin SDK and nothing else can, but\n * a reader that applies what it reads to a theme checks the shape first.\n */\nexport function readAiThemeProposal(value: unknown): AiThemeProposal | null {\n if (!isRecord(value) || value['version'] !== AI_THEME_PROPOSAL_VERSION) return null\n const mode = value['mode'] === 'create' ? 'create' : 'modify'\n const source =\n value['source'] === 'installed' || value['source'] === 'custom' ? value['source'] : 'default'\n const changes = (Array.isArray(value['changes']) ? value['changes'] : [])\n .filter(isRecord)\n .filter((change) => VALUE_CONTROLS.has(String(change['control'])))\n .map(\n (change): AiThemeControlChange => ({\n control: change['control'] as AiThemeValueControl,\n scheme: change['scheme'] === 'light' || change['scheme'] === 'dark' ? change['scheme'] : null,\n value: scalar(change['value']),\n before: scalar(change['before']),\n }),\n )\n const components = (Array.isArray(value['components']) ? value['components'] : [])\n .filter(isRecord)\n .filter(\n (leaf) =>\n (THEME_EDITOR_CONTROLS.find((control) => control.id === 'components')?.options ?? []).includes(\n String(leaf['component']),\n ) &&\n (leaf['target'] === 'styleOverrides' ||\n leaf['target'] === 'sx' ||\n leaf['target'] === 'defaultProps') &&\n typeof leaf['property'] === 'string' &&\n ['string', 'number', 'boolean'].includes(typeof leaf['value']) &&\n (leaf['media'] === null || String(leaf['media']) in THEME_EDITOR_MEDIA_QUERIES),\n )\n .map(\n (leaf): ThemeComponentOverrideLeaf => ({\n component: leaf['component'] as ThemeComponentOverrideLeaf['component'],\n target: leaf['target'] as ThemeComponentOverrideLeaf['target'],\n slot: typeof leaf['slot'] === 'string' ? leaf['slot'] : null,\n property: leaf['property'] as string,\n media: (leaf['media'] as ThemeComponentOverrideLeaf['media']) ?? null,\n value: leaf['value'] as string | number | boolean,\n }),\n )\n const corrections = (Array.isArray(value['corrections']) ? value['corrections'] : [])\n .filter(isRecord)\n .filter(\n (correction) =>\n aiThemeColorToken(String(correction['control'])) !== null &&\n (correction['scheme'] === 'light' || correction['scheme'] === 'dark') &&\n typeof correction['to'] === 'string',\n )\n .map(\n (correction): AiThemeCorrection => ({\n control: correction['control'] as AiThemeCorrection['control'],\n scheme: correction['scheme'] as HostThemeScheme,\n from: typeof correction['from'] === 'string' ? correction['from'] : null,\n to: correction['to'] as string,\n reason: correction['reason'] === 'dark-value' ? 'dark-value' : 'contrast',\n ...(Array.isArray(correction['against']) ? { against: strings(correction['against']) } : {}),\n ...(typeof correction['ratio'] === 'number' ? { ratio: correction['ratio'] } : {}),\n ...(typeof correction['required'] === 'number' ? { required: correction['required'] } : {}),\n }),\n )\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode,\n summary: typeof value['summary'] === 'string' ? value['summary'] : '',\n source,\n changes,\n components,\n resetComponents: value['resetComponents'] === true,\n corrections,\n dropped: strings(value['dropped']),\n notes: strings(value['notes']),\n }\n}\n"],"names":["CONTRAST_AA","CONTRAST_AA_LARGE","contrastRatio","accessibleShade","inheritedThemeColor","readDarkScheme","readFontFamily","readThemeColor","readToolbarHeight","resetComponentOverrides","SYSTEM_FONT_VALUE","THEME_COLOR_FIELDS","THEME_EDITOR_CONTROLS","THEME_EDITOR_MEDIA_QUERIES","THEME_EDITOR_SCHEMES","writeBorderRadius","writeComponentOverride","writeDarkScheme","writeFontFamily","writeSpacing","writeThemeColor","writeToolbarHeight","prefersDarkInk","AI_THEME_PROPOSAL_VERSION","AI_THEME_PROPOSAL_MODES","AI_THEME_CHANGE_GROUPS","AI_THEME_VALUE_CONTROLS","filter","control","id","map","aiThemeColorToken","startsWith","slice","length","readAiThemeControl","theme","scheme","token","family","shape","borderRadius","spacing","applyControlChange","change","value","undefined","number","applyAiThemeChanges","proposal","next","changes","resetComponents","leaf","components","aiThemeControlGroup","aiThemeComponentGroup","component","BRIEF_VOCABULARY","color","darkScheme","typography","navigation","aiThemeBriefScope","brief","mode","Set","named","group","test","GROUP_WORDS","colorKey","effectiveThemeColor","ACCENT_TOKENS","field","aiThemeMissingDarkValues","themeAfter","set","has","deriveAiThemeDarkAccent","light","background","minContrast","AI_THEME_CONTRAST_PAIRS","foreground","backgrounds","required","round2","ratio","Math","round","darkInkReads","verdict","correctAiThemeContrast","touchedChanges","touched","schemes","corrections","notes","pair","ratios","some","worst","min","involved","push","join","corrected","clears","every","from","to","reason","against","withCorrection","correction","base","index","findIndex","at","before","buildAiThemeProposal","input","dropped","scope","outOfScope","what","property","after","find","dark","contrast","version","summary","source","aiThemeProposalAfter","applied","aiThemeStaleChanges","isRecord","Array","isArray","strings","entry","scalar","VALUE_CONTROLS","readAiThemeProposal","String","options","includes","target","slot","media"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-theme-proposal.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CONTRAST_AA,\n CONTRAST_AA_LARGE,\n contrastRatio,\n type HostThemeSource,\n} from '@aglyn/aglyn/app-utils/site-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport { accessibleShade } from '@aglyn/shared-ui-theme/util/accessible-shade'\nimport { inheritedThemeColor } from '@aglyn/shared-ui-theme/util/theme-editor-defaults'\nimport {\n readDarkScheme,\n readFontFamily,\n readThemeColor,\n readToolbarHeight,\n resetComponentOverrides,\n SYSTEM_FONT_VALUE,\n THEME_COLOR_FIELDS,\n THEME_EDITOR_CONTROLS,\n THEME_EDITOR_MEDIA_QUERIES,\n THEME_EDITOR_SCHEMES,\n writeBorderRadius,\n writeComponentOverride,\n writeDarkScheme,\n writeFontFamily,\n writeSpacing,\n writeThemeColor,\n writeToolbarHeight,\n type ThemeColorToken,\n type ThemeComponentOverrideLeaf,\n type ThemeEditorControlId,\n} from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport { prefersDarkInk } from '@aglyn/shared-util-tools/contrast'\n\n/**\n * A theme proposal (AGL-2938): what a theme job hands a person to apply.\n *\n * Not a theme. A proposal is a set of theme editor controls set to values,\n * applied to whatever the site's theme is when the person applies it, through\n * the same writes the editor performs. A proposal that names three colors\n * changes three colors, and every control it does not name is untouched by\n * construction rather than by the model's restraint — which is what makes\n * \"make it feel warmer\" a targeted diff instead of a regenerated theme.\n *\n * Pure and free of anything server-side: the job step builds a proposal, and\n * the Theme section's widget previews and applies one, from this module.\n */\n\nexport const AI_THEME_PROPOSAL_VERSION = 1\n\n/**\n * `modify` changes what the brief is about and nothing else; `create` is a\n * whole design over every control the editor has.\n */\nexport type AiThemeProposalMode = 'modify' | 'create'\n\nexport const AI_THEME_PROPOSAL_MODES: readonly AiThemeProposalMode[] = ['modify', 'create']\n\n/** The parts of a theme a brief can be about. */\nexport type AiThemeChangeGroup =\n | 'color'\n | 'darkScheme'\n | 'typography'\n | 'shape'\n | 'spacing'\n | 'navigation'\n | 'components'\n\nexport const AI_THEME_CHANGE_GROUPS: readonly AiThemeChangeGroup[] = [\n 'color',\n 'darkScheme',\n 'typography',\n 'shape',\n 'spacing',\n 'navigation',\n 'components',\n]\n\n/** The controls a proposal sets one value on; component overrides are leaves of their own. */\nexport type AiThemeValueControl = Exclude<ThemeEditorControlId, 'components'>\n\n/** Every value control, as the catalog lists them. */\nexport const AI_THEME_VALUE_CONTROLS: readonly AiThemeValueControl[] = THEME_EDITOR_CONTROLS.filter(\n (control) => control.id !== 'components',\n).map((control) => control.id as AiThemeValueControl)\n\n/** One editor control set to a value, or back to its default with `null`. */\nexport interface AiThemeControlChange {\n control: AiThemeValueControl\n /** The scheme a color is set for; `null` for every other control. */\n scheme: HostThemeScheme | null\n value: string | number | null\n /** What the site had set when the proposal was made; `null` when it inherited. */\n before: string | number | null\n}\n\n/** A value the proposal carries that the model did not write, and why. */\nexport interface AiThemeCorrection {\n control: `color.${ThemeColorToken}`\n scheme: HostThemeScheme\n /** The value before the correction; `null` when the slot inherited. */\n from: string | null\n to: string\n /**\n * `contrast` — the pair fell below its bar and this is the nearest shade\n * of the foreground that clears it. `dark-value` — the proposal changed an\n * accent for light and gave no dark value, and this keeps the same hue\n * readable on the dark background.\n */\n reason: 'contrast' | 'dark-value'\n /** The colors a contrast correction was measured against. */\n against?: string[]\n /** The worst ratio before the correction. */\n ratio?: number\n /** The bar the pair has to clear. */\n required?: number\n}\n\nexport interface AiThemeProposal {\n version: typeof AI_THEME_PROPOSAL_VERSION\n mode: AiThemeProposalMode\n /** One sentence naming what the proposal changes, in the brief's language. */\n summary: string\n /**\n * Where the site's theme came from when the proposal was made, which is\n * how the editor's Save stores it: an installed theme's edits become its\n * override patch, a default theme stores only the changed values, and a\n * site's own theme changes in place.\n */\n source: HostThemeSource\n changes: AiThemeControlChange[]\n components: ThemeComponentOverrideLeaf[]\n /** The site's own component overrides are dropped before `components` apply. */\n resetComponents: boolean\n corrections: AiThemeCorrection[]\n /** What the model proposed that the proposal leaves out, in words. */\n dropped: string[]\n /** Anything else a person should read before applying, in words. */\n notes: string[]\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading and applying\n * ------------------------------------------------------------------------ */\n\n/** The color token a control writes, or `null` for a control that is not a color. */\nexport function aiThemeColorToken(control: string): ThemeColorToken | null {\n return control.startsWith('color.')\n ? (control.slice('color.'.length) as ThemeColorToken)\n : null\n}\n\n/** What a control holds on a theme, as a proposal's `before` names it. */\nexport function readAiThemeControl(\n theme: HostTheme | undefined,\n control: AiThemeValueControl,\n scheme: HostThemeScheme | null,\n): string | number | null {\n const token = aiThemeColorToken(control)\n if (token) return readThemeColor(theme, scheme ?? 'light', token) ?? null\n switch (control) {\n case 'darkScheme':\n return readDarkScheme(theme) === 'off' ? 'off' : null\n case 'fontFamily': {\n const family = readFontFamily(theme)\n return family === SYSTEM_FONT_VALUE ? null : family\n }\n case 'borderRadius':\n return typeof theme?.shape?.borderRadius === 'number' ? theme.shape.borderRadius : null\n case 'spacing':\n return typeof theme?.spacing === 'number' ? theme.spacing : null\n case 'navHeight.xs':\n return (theme && readToolbarHeight(theme, 'xs')) ?? null\n case 'navHeight.sm':\n return (theme && readToolbarHeight(theme, 'sm')) ?? null\n default:\n return null\n }\n}\n\nfunction applyControlChange(\n theme: HostTheme,\n change: Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>,\n): HostTheme {\n const { control, scheme, value } = change\n const token = aiThemeColorToken(control)\n if (token) {\n return writeThemeColor(\n theme,\n scheme ?? 'light',\n token,\n typeof value === 'string' ? value : undefined,\n )\n }\n const number = typeof value === 'number' ? value : undefined\n switch (control) {\n case 'darkScheme':\n return writeDarkScheme(theme, value === 'off' ? 'off' : 'auto')\n case 'fontFamily':\n return writeFontFamily(theme, typeof value === 'string' ? value : SYSTEM_FONT_VALUE)\n case 'borderRadius':\n return writeBorderRadius(theme, number)\n case 'spacing':\n return writeSpacing(theme, number)\n case 'navHeight.xs':\n return writeToolbarHeight(theme, 'xs', number)\n case 'navHeight.sm':\n return writeToolbarHeight(theme, 'sm', number)\n default:\n return theme\n }\n}\n\n/**\n * The theme a set of changes produces on top of `theme`: each change written\n * the way the editor writes it, then the component overrides. Returns a new\n * object; `theme` is not mutated.\n */\nexport function applyAiThemeChanges(\n theme: HostTheme | undefined,\n proposal: {\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n },\n): HostTheme {\n let next: HostTheme = { ...(theme ?? {}) }\n for (const change of proposal.changes) next = applyControlChange(next, change)\n if (proposal.resetComponents) next = resetComponentOverrides(next)\n for (const leaf of proposal.components) next = writeComponentOverride(next, leaf)\n return next\n}\n\n/* ------------------------------------------------------------------------ *\n * Scope: which parts of the theme a brief is about\n * ------------------------------------------------------------------------ */\n\n/** The part of the theme a value control belongs to. */\nexport function aiThemeControlGroup(control: AiThemeValueControl): AiThemeChangeGroup {\n if (aiThemeColorToken(control)) return 'color'\n switch (control) {\n case 'darkScheme':\n return 'darkScheme'\n case 'fontFamily':\n return 'typography'\n case 'borderRadius':\n return 'shape'\n case 'spacing':\n return 'spacing'\n default:\n return 'navigation'\n }\n}\n\n/**\n * The part of the theme a component override belongs to: the type ramp's\n * overrides are typography, the app bar's and the toolbar's are navigation,\n * and the rest are components.\n */\nexport function aiThemeComponentGroup(\n component: ThemeComponentOverrideLeaf['component'],\n): AiThemeChangeGroup {\n if (component === 'MuiTypography') return 'typography'\n if (component === 'MuiAppBar' || component === 'MuiToolbar') return 'navigation'\n return 'components'\n}\n\n/**\n * The words that tie a brief to a part of the theme, matched case-insensitively\n * on whole words. A brief can name several parts.\n */\nconst BRIEF_VOCABULARY: Record<AiThemeChangeGroup, RegExp> = {\n color:\n /(#[0-9a-f]{3,8}\\b)|\\b(colou?rs?|palettes?|hues?|warm(er|th|ly)?|cool(er)?|cold(er)?|contrast\\w*|bright(er|ness)?|dark(er|en)?|light(er|en)?|saturat\\w*|desaturat\\w*|muted|vibrant|pastels?|tones?|tints?|shades?|accents?|primary|secondary|tertiary|backgrounds?|surfaces?|brand\\w*|earthy|neon|moody|red|orange|yellow|green|blue|teal|purple|violet|pink|magenta|brown|beige|cream|gold|navy|black|white|gr[ae]y|hex)\\b/i,\n darkScheme: /\\b(dark (mode|scheme|theme)|light (mode|scheme)|always light|night mode)\\b/i,\n typography:\n /\\b(fonts?|typefaces?|typography|serif|sans|monospace|headings?|headlines?|titles?|type ?scale|text size|font size|lettering|weights?|bold(er)?|editorial)\\b/i,\n shape: /\\b(radius|radii|round(ed|er|ness)?|corners?|sharp(er)?|square(d|r)?|pills?|curv\\w*)\\b/i,\n spacing:\n /\\b(spac\\w*|padding|margins?|roomy|airy|tight(er)?|compact|dense|breathing room|white ?space|gaps?)\\b/i,\n navigation: /\\b(nav\\w*|header height|toolbar|app ?bar|menu bar|top bar)\\b/i,\n components:\n /\\b(buttons?|cards?|chips?|inputs?|text ?fields?|links?|tabs?|switch(es)?|sliders?|avatars?|badges?|tooltips?|dividers?|menus?|components?|uppercase|capitali[sz]\\w*|shadows?|elevation|outlined|underlin\\w*)\\b/i,\n}\n\n/**\n * The parts of the theme a job may change. A `create` job is a whole design.\n * A `modify` job may change only what its brief is about, so \"make it feel\n * warmer\" reaches the colors and nothing else; a brief that names no part at\n * all — in another language, or in words this does not know — is taken to\n * be about the whole design rather than refused.\n */\nexport function aiThemeBriefScope(\n brief: string,\n mode: AiThemeProposalMode,\n): ReadonlySet<AiThemeChangeGroup> {\n if (mode === 'create') return new Set(AI_THEME_CHANGE_GROUPS)\n const named = AI_THEME_CHANGE_GROUPS.filter((group) => BRIEF_VOCABULARY[group].test(brief))\n return new Set(named.length ? named : AI_THEME_CHANGE_GROUPS)\n}\n\nconst GROUP_WORDS: Record<AiThemeChangeGroup, string> = {\n color: 'colors',\n darkScheme: 'the dark scheme',\n typography: 'typography',\n shape: 'corners',\n spacing: 'spacing',\n navigation: 'navigation',\n components: 'component styles',\n}\n\n/* ------------------------------------------------------------------------ *\n * Colors as the site renders them, and dark values\n * ------------------------------------------------------------------------ */\n\nconst colorKey = (scheme: HostThemeScheme, token: ThemeColorToken) => `${scheme}:${token}`\n\n/** A color as the site renders it in one scheme: its own value, else the brand's. */\nexport function effectiveThemeColor(\n theme: HostTheme | undefined,\n scheme: HostThemeScheme,\n token: ThemeColorToken,\n): string | undefined {\n return readThemeColor(theme, scheme, token) ?? inheritedThemeColor(scheme, token)\n}\n\n/** The accents: the colors whose dark value can follow their light one. */\nconst ACCENT_TOKENS: ReadonlySet<ThemeColorToken> = new Set(\n THEME_COLOR_FIELDS.filter((field) => field.group === 'palette').map((field) => field.token),\n)\n\n/**\n * The colors a set of changes gives a light value and no dark one, while the\n * site's dark scheme is on. Each is a visitor in dark mode seeing half a\n * change.\n */\nexport function aiThemeMissingDarkValues(\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n themeAfter: HostTheme,\n): ThemeColorToken[] {\n if (readDarkScheme(themeAfter) === 'off') return []\n const set = new Set(\n changes\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n return THEME_COLOR_FIELDS.map(({ token }) => token).filter(\n (token) => set.has(colorKey('light', token)) && !set.has(colorKey('dark', token)),\n )\n}\n\n/**\n * The dark value an accent keeps when a proposal gave none: the same hue,\n * lightened only as far as it takes to read on the dark background as a fill\n * or an indicator. Surfaces, text and tints have no such value — a pale light\n * surface lightened is still a light surface — so they go on inheriting the\n * platform's dark palette, which is what a site with no dark design renders.\n */\nexport function deriveAiThemeDarkAccent(light: string, themeAfter: HostTheme): string | null {\n const background = effectiveThemeColor(themeAfter, 'dark', 'background.default')\n if (!background) return null\n try {\n return accessibleShade(light, [background], 'lighten', { minContrast: CONTRAST_AA_LARGE })\n } catch {\n return null\n }\n}\n\n/* ------------------------------------------------------------------------ *\n * Contrast\n * ------------------------------------------------------------------------ */\n\n/** A foreground that has to read on each of its backgrounds. */\nexport interface AiThemeContrastPair {\n foreground: ThemeColorToken\n backgrounds: readonly ThemeColorToken[]\n required: number\n}\n\n/**\n * The pairs a proposal is held to. Body text on the page and on paper, and\n * secondary text on the page, are the pairs the theme publish check\n * (`validateThemeForPublish`) measures, at its bars; the primary color on\n * the page is held to the bar a fill or an indicator owes.\n */\nexport const AI_THEME_CONTRAST_PAIRS: readonly AiThemeContrastPair[] = [\n {\n foreground: 'text.primary',\n backgrounds: ['background.default', 'background.paper'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'text.secondary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'primary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA_LARGE,\n },\n]\n\nconst round2 = (ratio: number) => Math.round(ratio * 100) / 100\n\n/**\n * Whether dark ink reads better than light ink on a background. A hex takes\n * the shared helper; anything else a theme can hold (`rgba()`) is measured\n * against black and white directly, which is the same comparison.\n */\nfunction darkInkReads(background: string): boolean {\n const verdict = prefersDarkInk(background)\n if (verdict !== null) return verdict\n return (contrastRatio('#000000', background) ?? 0) >= (contrastRatio('#ffffff', background) ?? 0)\n}\n\nexport interface AiThemeContrastResult {\n theme: HostTheme\n corrections: AiThemeCorrection[]\n notes: string[]\n}\n\n/**\n * Every pair a set of changes takes below its bar, corrected to the nearest\n * shade of its foreground that clears it, and named.\n *\n * Only pairs the changes touched are corrected. A pair that already fell\n * short with neither of its colors changed is named in `notes` and left\n * alone, because correcting it would change a color the brief did not ask\n * about. A dark scheme the site has switched off is not measured: no visitor\n * sees it.\n */\nexport function correctAiThemeContrast(\n themeAfter: HostTheme,\n touchedChanges: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n): AiThemeContrastResult {\n const touched = new Set(\n touchedChanges\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n const schemes = THEME_EDITOR_SCHEMES.filter(\n (scheme) => scheme === 'light' || readDarkScheme(themeAfter) !== 'off',\n )\n let theme = themeAfter\n const corrections: AiThemeCorrection[] = []\n const notes: string[] = []\n for (const scheme of schemes) {\n for (const pair of AI_THEME_CONTRAST_PAIRS) {\n const foreground = effectiveThemeColor(theme, scheme, pair.foreground)\n const backgrounds = pair.backgrounds\n .map((token) => effectiveThemeColor(theme, scheme, token))\n .filter((value): value is string => typeof value === 'string')\n if (!foreground || backgrounds.length !== pair.backgrounds.length) continue\n const ratios = backgrounds.map((background) => contrastRatio(foreground, background))\n if (ratios.some((ratio) => ratio === null)) continue\n const worst = Math.min(...(ratios as number[]))\n if (worst >= pair.required) continue\n const involved = [pair.foreground, ...pair.backgrounds].some((token) =>\n touched.has(colorKey(scheme, token)),\n )\n if (!involved) {\n notes.push(\n `${pair.foreground} on ${pair.backgrounds.join(' and ')} in ${scheme} was already ` +\n `${round2(worst)}:1 before this proposal, below ${pair.required}:1.`,\n )\n continue\n }\n let corrected: string\n try {\n corrected = accessibleShade(\n foreground,\n backgrounds,\n darkInkReads(backgrounds[0]) ? 'darken' : 'lighten',\n { minContrast: pair.required },\n )\n } catch {\n continue\n }\n const clears = backgrounds.every(\n (background) => (contrastRatio(corrected, background) ?? 0) >= pair.required,\n )\n if (!clears) {\n notes.push(\n `${pair.foreground} cannot reach ${pair.required}:1 on the ${scheme} background by ` +\n 'changing its shade alone; the background needs to change too.',\n )\n continue\n }\n corrections.push({\n control: `color.${pair.foreground}`,\n scheme,\n from: readThemeColor(theme, scheme, pair.foreground) ?? null,\n to: corrected,\n reason: 'contrast',\n against: backgrounds,\n ratio: round2(worst),\n required: pair.required,\n })\n theme = writeThemeColor(theme, scheme, pair.foreground, corrected)\n }\n }\n return { theme, corrections, notes }\n}\n\n/** Folds a correction into a change list: the change it corrects, or a new one. */\nfunction withCorrection(\n changes: AiThemeControlChange[],\n correction: AiThemeCorrection,\n base: HostTheme | undefined,\n): AiThemeControlChange[] {\n const index = changes.findIndex(\n (change) => change.control === correction.control && change.scheme === correction.scheme,\n )\n if (index >= 0) {\n return changes.map((change, at) => (at === index ? { ...change, value: correction.to } : change))\n }\n return [\n ...changes,\n {\n control: correction.control,\n scheme: correction.scheme,\n value: correction.to,\n before: readAiThemeControl(base, correction.control, correction.scheme),\n },\n ]\n}\n\n/* ------------------------------------------------------------------------ *\n * Building a proposal\n * ------------------------------------------------------------------------ */\n\nexport interface BuildAiThemeProposalInput {\n /** The site's theme as the editor shows it, overrides resolved. */\n base: HostTheme | undefined\n source: HostThemeSource\n mode: AiThemeProposalMode\n brief: string\n summary: string\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n /** What reading the model's answer already left out, in words. */\n dropped?: readonly string[]\n notes?: readonly string[]\n}\n\n/**\n * A proposal from what the model proposed, held to the editor and to the\n * doctrine: changes that change nothing are gone, a modify brief reaches\n * only the parts of the theme it names, accents keep a dark value, and every\n * pair the proposal touches clears its contrast bar.\n */\nexport function buildAiThemeProposal(input: BuildAiThemeProposalInput): AiThemeProposal {\n const dropped = [...(input.dropped ?? [])]\n const notes = [...(input.notes ?? [])]\n const scope = aiThemeBriefScope(input.brief, input.mode)\n const outOfScope = (group: AiThemeChangeGroup, what: string) => {\n dropped.push(`${what}: the brief did not ask about ${GROUP_WORDS[group]}`)\n }\n\n let changes: AiThemeControlChange[] = []\n for (const change of input.changes) {\n const before = readAiThemeControl(input.base, change.control, change.scheme)\n if (before === change.value) continue\n const group = aiThemeControlGroup(change.control)\n if (!scope.has(group)) {\n outOfScope(group, change.scheme ? `${change.control} (${change.scheme})` : change.control)\n continue\n }\n changes.push({ ...change, before })\n }\n const components = input.components.filter((leaf) => {\n const group = aiThemeComponentGroup(leaf.component)\n if (scope.has(group)) return true\n outOfScope(group, `${leaf.component}.${leaf.property}`)\n return false\n })\n let resetComponents = input.resetComponents\n if (resetComponents && !scope.has('components')) {\n outOfScope('components', 'Resetting the component overrides')\n resetComponents = false\n }\n\n const corrections: AiThemeCorrection[] = []\n let after = applyAiThemeChanges(input.base, { changes, components, resetComponents })\n for (const token of aiThemeMissingDarkValues(changes, after)) {\n const light = changes.find(\n (change) => change.scheme === 'light' && aiThemeColorToken(change.control) === token,\n )\n if (!ACCENT_TOKENS.has(token) || typeof light?.value !== 'string') {\n notes.push(`${token} keeps its current dark value; only its light value changes.`)\n continue\n }\n const dark = deriveAiThemeDarkAccent(light.value, after)\n if (!dark) continue\n const correction: AiThemeCorrection = {\n control: `color.${token}`,\n scheme: 'dark',\n from: readThemeColor(input.base, 'dark', token) ?? null,\n to: dark,\n reason: 'dark-value',\n }\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n after = writeThemeColor(after, 'dark', token, dark)\n }\n\n const contrast = correctAiThemeContrast(after, changes)\n for (const correction of contrast.corrections) {\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n }\n notes.push(...contrast.notes)\n\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode: input.mode,\n summary: input.summary,\n source: input.source,\n changes,\n components,\n resetComponents,\n corrections,\n dropped,\n notes,\n }\n}\n\n/**\n * What applying a proposal to the site's CURRENT theme produces, held to the\n * contrast bars again: the theme may have changed since the proposal was\n * made, and a proposal that cleared its bars against the old background can\n * fall short against the new one. The corrections this makes are named\n * beside the proposal's own.\n */\nexport function aiThemeProposalAfter(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes' | 'components' | 'resetComponents'>,\n): AiThemeContrastResult {\n const applied = applyAiThemeChanges(theme, proposal)\n return correctAiThemeContrast(applied, proposal.changes)\n}\n\n/** The changes whose `before` no longer matches the theme a proposal is applied to. */\nexport function aiThemeStaleChanges(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes'>,\n): AiThemeControlChange[] {\n return proposal.changes.filter(\n (change) => readAiThemeControl(theme, change.control, change.scheme) !== change.before,\n )\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading a stored proposal back\n * ------------------------------------------------------------------------ */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst strings = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n\nconst scalar = (value: unknown): string | number | null =>\n typeof value === 'string' || typeof value === 'number' ? value : null\n\nconst VALUE_CONTROLS: ReadonlySet<string> = new Set(AI_THEME_VALUE_CONTROLS)\n\n/**\n * A proposal as a job output stores it, or `null` for anything that is not\n * one. The job writes these through the Admin SDK and nothing else can, but\n * a reader that applies what it reads to a theme checks the shape first.\n */\nexport function readAiThemeProposal(value: unknown): AiThemeProposal | null {\n if (!isRecord(value) || value['version'] !== AI_THEME_PROPOSAL_VERSION) return null\n const mode = value['mode'] === 'create' ? 'create' : 'modify'\n const source =\n value['source'] === 'installed' || value['source'] === 'custom' ? value['source'] : 'default'\n const changes = (Array.isArray(value['changes']) ? value['changes'] : [])\n .filter(isRecord)\n .filter((change) => VALUE_CONTROLS.has(String(change['control'])))\n .map(\n (change): AiThemeControlChange => ({\n control: change['control'] as AiThemeValueControl,\n scheme: change['scheme'] === 'light' || change['scheme'] === 'dark' ? change['scheme'] : null,\n value: scalar(change['value']),\n before: scalar(change['before']),\n }),\n )\n const components = (Array.isArray(value['components']) ? value['components'] : [])\n .filter(isRecord)\n .filter(\n (leaf) =>\n (THEME_EDITOR_CONTROLS.find((control) => control.id === 'components')?.options ?? []).includes(\n String(leaf['component']),\n ) &&\n (leaf['target'] === 'styleOverrides' ||\n leaf['target'] === 'sx' ||\n leaf['target'] === 'defaultProps') &&\n typeof leaf['property'] === 'string' &&\n ['string', 'number', 'boolean'].includes(typeof leaf['value']) &&\n (leaf['media'] === null || String(leaf['media']) in THEME_EDITOR_MEDIA_QUERIES),\n )\n .map(\n (leaf): ThemeComponentOverrideLeaf => ({\n component: leaf['component'] as ThemeComponentOverrideLeaf['component'],\n target: leaf['target'] as ThemeComponentOverrideLeaf['target'],\n slot: typeof leaf['slot'] === 'string' ? leaf['slot'] : null,\n property: leaf['property'] as string,\n media: (leaf['media'] as ThemeComponentOverrideLeaf['media']) ?? null,\n value: leaf['value'] as string | number | boolean,\n }),\n )\n const corrections = (Array.isArray(value['corrections']) ? value['corrections'] : [])\n .filter(isRecord)\n .filter(\n (correction) =>\n aiThemeColorToken(String(correction['control'])) !== null &&\n (correction['scheme'] === 'light' || correction['scheme'] === 'dark') &&\n typeof correction['to'] === 'string',\n )\n .map(\n (correction): AiThemeCorrection => ({\n control: correction['control'] as AiThemeCorrection['control'],\n scheme: correction['scheme'] as HostThemeScheme,\n from: typeof correction['from'] === 'string' ? correction['from'] : null,\n to: correction['to'] as string,\n reason: correction['reason'] === 'dark-value' ? 'dark-value' : 'contrast',\n ...(Array.isArray(correction['against']) ? { against: strings(correction['against']) } : {}),\n ...(typeof correction['ratio'] === 'number' ? { ratio: correction['ratio'] } : {}),\n ...(typeof correction['required'] === 'number' ? { required: correction['required'] } : {}),\n }),\n )\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode,\n summary: typeof value['summary'] === 'string' ? value['summary'] : '',\n source,\n changes,\n components,\n resetComponents: value['resetComponents'] === true,\n corrections,\n dropped: strings(value['dropped']),\n notes: strings(value['notes']),\n }\n}\n"],"names":["CONTRAST_AA","CONTRAST_AA_LARGE","contrastRatio","accessibleShade","inheritedThemeColor","readDarkScheme","readFontFamily","readThemeColor","readToolbarHeight","resetComponentOverrides","SYSTEM_FONT_VALUE","THEME_COLOR_FIELDS","THEME_EDITOR_CONTROLS","THEME_EDITOR_MEDIA_QUERIES","THEME_EDITOR_SCHEMES","writeBorderRadius","writeComponentOverride","writeDarkScheme","writeFontFamily","writeSpacing","writeThemeColor","writeToolbarHeight","prefersDarkInk","AI_THEME_PROPOSAL_VERSION","AI_THEME_PROPOSAL_MODES","AI_THEME_CHANGE_GROUPS","AI_THEME_VALUE_CONTROLS","filter","control","id","map","aiThemeColorToken","startsWith","slice","length","readAiThemeControl","theme","scheme","token","family","shape","borderRadius","spacing","applyControlChange","change","value","undefined","number","applyAiThemeChanges","proposal","next","changes","resetComponents","leaf","components","aiThemeControlGroup","aiThemeComponentGroup","component","BRIEF_VOCABULARY","color","darkScheme","typography","navigation","aiThemeBriefScope","brief","mode","Set","named","group","test","GROUP_WORDS","colorKey","effectiveThemeColor","ACCENT_TOKENS","field","aiThemeMissingDarkValues","themeAfter","set","has","deriveAiThemeDarkAccent","light","background","minContrast","AI_THEME_CONTRAST_PAIRS","foreground","backgrounds","required","round2","ratio","Math","round","darkInkReads","verdict","correctAiThemeContrast","touchedChanges","touched","schemes","corrections","notes","pair","ratios","some","worst","min","involved","push","join","corrected","clears","every","from","to","reason","against","withCorrection","correction","base","index","findIndex","at","before","buildAiThemeProposal","input","dropped","scope","outOfScope","what","property","after","find","dark","contrast","version","summary","source","aiThemeProposalAfter","applied","aiThemeStaleChanges","isRecord","Array","isArray","strings","entry","scalar","VALUE_CONTROLS","readAiThemeProposal","String","options","includes","target","slot","media"],"mappings":"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"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type HostEventType } from '@aglyn/aglyn/app-utils/
|
|
17
|
+
import { type HostEventType } from '@aglyn/aglyn/app-utils/host-events';
|
|
18
18
|
import type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types';
|
|
19
19
|
/**
|
|
20
20
|
* A `workflow` job (AGL-2919): an automation drafted from a description, or an
|
|
@@ -97,8 +97,6 @@ export type AiAutomationUnsupported = (typeof AI_AUTOMATION_UNSUPPORTED)[number]
|
|
|
97
97
|
/** The need an unsupported reason names, for the reasons that name one. */
|
|
98
98
|
export declare const AI_AUTOMATION_UNSUPPORTED_NEED: Readonly<Partial<Record<AiAutomationUnsupported, AiAutomationNeed>>>;
|
|
99
99
|
export declare const AI_AUTOMATION_UNSUPPORTED_COPY: Readonly<Record<AiAutomationUnsupported, string>>;
|
|
100
|
-
/** The id the workflows plugin is registered under. */
|
|
101
|
-
export declare const AI_WORKFLOWS_PLUGIN_ID = "workflows";
|
|
102
100
|
/** The resource the workflows plugin writes a drafted automation under. */
|
|
103
101
|
export declare const AI_AUTOMATION_RESOURCE = "automation";
|
|
104
102
|
export declare const AI_WORKFLOW_NO_SITE_COPY = "This automation job names a site this workspace does not have.";
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
17
|
import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
|
-
import { HOST_EVENT_LABELS, HOST_EVENT_TYPES } from "@aglyn/aglyn/app-utils/
|
|
18
|
+
import { HOST_EVENT_LABELS, HOST_EVENT_TYPES } from "@aglyn/aglyn/app-utils/host-events";
|
|
19
19
|
/**
|
|
20
20
|
* A `workflow` job (AGL-2919): an automation drafted from a description, or an
|
|
21
21
|
* existing automation explained — what it does, or why one of its runs failed.
|
|
@@ -169,7 +169,6 @@ export const AI_AUTOMATION_UNSUPPORTED_COPY = {
|
|
|
169
169
|
'needs-bookings': 'This automation needs bookings, which this workspace’s plan does not include, so nothing was drafted.'
|
|
170
170
|
};
|
|
171
171
|
// ── What a job tells a person ─────────────────────────────────────────────
|
|
172
|
-
/** The id the workflows plugin is registered under. */ export const AI_WORKFLOWS_PLUGIN_ID = 'workflows';
|
|
173
172
|
/** The resource the workflows plugin writes a drafted automation under. */ export const AI_AUTOMATION_RESOURCE = 'automation';
|
|
174
173
|
export const AI_WORKFLOW_NO_SITE_COPY = 'This automation job names a site this workspace does not have.';
|
|
175
174
|
export const AI_WORKFLOW_UNAVAILABLE_COPY = 'Automations are not available on this site.';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-workflow-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostActionStepType } from '@aglyn/aglyn/app-utils/actions'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n HOST_EVENT_LABELS,\n HOST_EVENT_TYPES,\n type HostEventType,\n} from '@aglyn/aglyn/app-utils/workflows'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\n\n/**\n * A `workflow` job (AGL-2919): an automation drafted from a description, or an\n * existing automation explained — what it does, or why one of its runs failed.\n *\n * One kind, three modes, because the three share everything a job carries:\n * the site, the gates, the meter and the automation vocabulary. `inputs.mode`\n * picks the mode, and a job created without one drafts.\n *\n * - `draft` writes a new automation to the site's Actions through the\n * workflows plugin's draft writer, OFF, built only from the triggers and\n * steps below that this workspace can run. What the description names that\n * the site does not have is left as a placeholder, never guessed.\n * - `explain` reads a saved automation — an action or a workflow — and\n * answers in plain words what it does.\n * - `diagnose` reads a saved automation and one of its FAILED runs, as the\n * run history recorded it, and answers why it failed and what to change.\n *\n * Nothing in any mode switches an automation on, runs one, or changes one.\n */\n\nexport const AI_WORKFLOW_JOB_MODES = ['draft', 'explain', 'diagnose'] as const\nexport type AiWorkflowJobMode = (typeof AI_WORKFLOW_JOB_MODES)[number]\n\n/** What an explained automation is: an action, or a workflow of function calls. */\nexport const AI_WORKFLOW_TARGET_TYPES = ['action', 'workflow'] as const\nexport type AiWorkflowTargetType = (typeof AI_WORKFLOW_TARGET_TYPES)[number]\n\nexport type AiWorkflowJobInputs =\n | { mode: 'draft' }\n | { mode: 'explain'; targetType: AiWorkflowTargetType; targetId: string }\n | { mode: 'diagnose'; targetType: AiWorkflowTargetType; targetId: string; runId: string }\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,128}$/\n\n/** A job's inputs read into its mode, or the sentence that says what is wrong with them. */\nexport function parseAiWorkflowJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiWorkflowJobInputs | string {\n const mode = inputs?.['mode'] ?? 'draft'\n if (!(AI_WORKFLOW_JOB_MODES as readonly unknown[]).includes(mode)) {\n return 'inputs.mode must be draft, explain or diagnose'\n }\n if (mode === 'draft') return { mode }\n const targetType = inputs?.['targetType']\n const targetId = inputs?.['targetId']\n if (\n !(AI_WORKFLOW_TARGET_TYPES as readonly unknown[]).includes(targetType) ||\n typeof targetId !== 'string' ||\n !DOCUMENT_ID.test(targetId)\n ) {\n return 'Pick the automation to explain'\n }\n const target = { targetType: targetType as AiWorkflowTargetType, targetId }\n if (mode === 'explain') return { mode, ...target }\n const runId = inputs?.['runId']\n if (typeof runId !== 'string' || !DOCUMENT_ID.test(runId)) return 'Pick the run to explain'\n return { mode: 'diagnose', ...target, runId }\n}\n\n// ── What a workspace can run ──────────────────────────────────────────────\n\n/** A part of the platform some triggers and steps need beyond the actions builder itself. */\nexport type AiAutomationNeed = 'crm' | 'webhooks' | 'bookings'\n\nexport type AiAutomationCapabilities = Readonly<Record<AiAutomationNeed, boolean>>\n\n/** Each need as a sentence names it. */\nexport const AI_AUTOMATION_NEED_LABELS: Readonly<Record<AiAutomationNeed, string>> = {\n crm: 'the CRM',\n webhooks: 'webhooks',\n bookings: 'bookings',\n}\n\n/**\n * What this workspace can run, from the entitlements the executor itself\n * reads before it runs a step: `crm` for the CRM steps, `webhooks` for a\n * webhook post, and `bookings` for the booking trigger's door.\n */\nexport function aiAutomationCapabilities(\n org: Partial<AglynOrgBilling> | Readonly<Record<string, unknown>> | null | undefined,\n): AiAutomationCapabilities {\n const billing = org as Partial<AglynOrgBilling> | null | undefined\n return {\n crm: checkEntitlement(billing, 'crm'),\n webhooks: checkEntitlement(billing, 'webhooks'),\n bookings: checkEntitlement(billing, 'bookings'),\n }\n}\n\n// ── The vocabulary ────────────────────────────────────────────────────────\n\n/**\n * The triggers a drafted automation may start on: every host event, which is\n * every event a server door emits. The on-page events (a click, a scroll) are\n * left out: they watch one element of one page, which a description cannot\n * name, and they belong to the page's own interactions.\n */\nexport const AI_AUTOMATION_TRIGGERS: readonly HostEventType[] = HOST_EVENT_TYPES\n\n/** The triggers only a door of a part of the platform emits. */\nexport const AI_AUTOMATION_TRIGGER_NEEDS: Readonly<Partial<Record<HostEventType, AiAutomationNeed>>> = {\n booking: 'bookings',\n taskCompleted: 'crm',\n contactStageChanged: 'crm',\n dealStageChanged: 'crm',\n dealWon: 'crm',\n dealLost: 'crm',\n}\n\n/**\n * The steps a drafted automation may take: the server steps and the flow\n * steps, which act on the person an event names. The on-page steps — show an\n * element, open a menu, run a script — are left out for the reason the\n * on-page triggers are: they need a selector on a page a description cannot\n * name.\n */\nexport const AI_AUTOMATION_STEP_TYPES = [\n 'sendEmail',\n 'notifyAdmins',\n 'enrollList',\n 'assignCampaign',\n 'runWorkflow',\n 'datasetAppend',\n 'updateDataset',\n 'webhookPost',\n 'siteAlert',\n 'wait',\n 'waitForEvent',\n 'exitFlow',\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n] as const satisfies readonly HostActionStepType[]\n\nexport type AiAutomationStepType = (typeof AI_AUTOMATION_STEP_TYPES)[number]\n\n/** The steps the executor refuses without a part of the platform. */\nexport const AI_AUTOMATION_STEP_NEEDS: Readonly<Partial<Record<AiAutomationStepType, AiAutomationNeed>>> = {\n webhookPost: 'webhooks',\n setContactStage: 'crm',\n addContactTag: 'crm',\n assignContactOwner: 'crm',\n createCrmTask: 'crm',\n logCrmActivity: 'crm',\n}\n\n/** The trigger's name as the Actions editor labels it. */\nexport function aiAutomationTriggerLabel(event: string): string {\n return HOST_EVENT_LABELS[event as HostEventType] ?? event\n}\n\n// ── A description the vocabulary cannot build ─────────────────────────────\n\n/**\n * Why a description was answered with no automation. Each is a fixed reason\n * with a fixed sentence, so what a person reads is never the model's own\n * account of itself.\n */\nexport const AI_AUTOMATION_UNSUPPORTED = [\n 'not-an-automation',\n 'no-trigger',\n 'no-step',\n 'needs-crm',\n 'needs-webhooks',\n 'needs-bookings',\n] as const\n\nexport type AiAutomationUnsupported = (typeof AI_AUTOMATION_UNSUPPORTED)[number]\n\n/** The need an unsupported reason names, for the reasons that name one. */\nexport const AI_AUTOMATION_UNSUPPORTED_NEED: Readonly<\n Partial<Record<AiAutomationUnsupported, AiAutomationNeed>>\n> = {\n 'needs-crm': 'crm',\n 'needs-webhooks': 'webhooks',\n 'needs-bookings': 'bookings',\n}\n\nexport const AI_AUTOMATION_UNSUPPORTED_COPY: Readonly<Record<AiAutomationUnsupported, string>> = {\n 'not-an-automation':\n 'This description is not an automation — something that happens, and what to do then — so nothing was drafted.',\n 'no-trigger':\n 'Nothing on the platform can start an automation like this one, so nothing was drafted. Describe it as something that happens on your site, such as a form being submitted.',\n 'no-step': 'An automation cannot do what this description asks yet, so nothing was drafted.',\n 'needs-crm':\n 'This automation needs the CRM, which this workspace’s plan does not include, so nothing was drafted.',\n 'needs-webhooks':\n 'This automation needs webhooks, which this workspace’s plan does not include, so nothing was drafted.',\n 'needs-bookings':\n 'This automation needs bookings, which this workspace’s plan does not include, so nothing was drafted.',\n}\n\n// ── What a job tells a person ─────────────────────────────────────────────\n\n/** The id the workflows plugin is registered under. */\nexport const AI_WORKFLOWS_PLUGIN_ID = 'workflows'\n\n/** The resource the workflows plugin writes a drafted automation under. */\nexport const AI_AUTOMATION_RESOURCE = 'automation'\n\nexport const AI_WORKFLOW_NO_SITE_COPY = 'This automation job names a site this workspace does not have.'\nexport const AI_WORKFLOW_UNAVAILABLE_COPY = 'Automations are not available on this site.'\nexport const AI_WORKFLOW_GONE_COPY = 'That automation no longer exists.'\nexport const AI_WORKFLOW_RUN_GONE_COPY = 'That run is no longer in the automation’s history.'\nexport const AI_WORKFLOW_RUN_NOT_FAILED_COPY = 'That run did not fail, so there is nothing to explain.'\nexport const AI_WORKFLOW_NO_DRAFT_COPY =\n 'The AI could not draft an automation from this description. Try describing what happens, and what should happen then.'\nexport const AI_WORKFLOW_NO_EXPLANATION_COPY =\n 'The AI could not explain this automation. Try again.'\nexport const AI_WORKFLOW_SAVE_FAILURE_COPY =\n 'The automation was drafted but could not be saved. Try the job again.'\n"],"names":["checkEntitlement","HOST_EVENT_LABELS","HOST_EVENT_TYPES","AI_WORKFLOW_JOB_MODES","AI_WORKFLOW_TARGET_TYPES","DOCUMENT_ID","parseAiWorkflowJobInputs","inputs","mode","includes","targetType","targetId","test","target","runId","AI_AUTOMATION_NEED_LABELS","crm","webhooks","bookings","aiAutomationCapabilities","org","billing","AI_AUTOMATION_TRIGGERS","AI_AUTOMATION_TRIGGER_NEEDS","booking","taskCompleted","contactStageChanged","dealStageChanged","dealWon","dealLost","AI_AUTOMATION_STEP_TYPES","AI_AUTOMATION_STEP_NEEDS","webhookPost","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","aiAutomationTriggerLabel","event","AI_AUTOMATION_UNSUPPORTED","AI_AUTOMATION_UNSUPPORTED_NEED","AI_AUTOMATION_UNSUPPORTED_COPY","AI_WORKFLOWS_PLUGIN_ID","AI_AUTOMATION_RESOURCE","AI_WORKFLOW_NO_SITE_COPY","AI_WORKFLOW_UNAVAILABLE_COPY","AI_WORKFLOW_GONE_COPY","AI_WORKFLOW_RUN_GONE_COPY","AI_WORKFLOW_RUN_NOT_FAILED_COPY","AI_WORKFLOW_NO_DRAFT_COPY","AI_WORKFLOW_NO_EXPLANATION_COPY","AI_WORKFLOW_SAVE_FAILURE_COPY"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-workflow-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostActionStepType } from '@aglyn/aglyn/app-utils/actions'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n HOST_EVENT_LABELS,\n HOST_EVENT_TYPES,\n type HostEventType,\n} from '@aglyn/aglyn/app-utils/host-events'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\n\n/**\n * A `workflow` job (AGL-2919): an automation drafted from a description, or an\n * existing automation explained — what it does, or why one of its runs failed.\n *\n * One kind, three modes, because the three share everything a job carries:\n * the site, the gates, the meter and the automation vocabulary. `inputs.mode`\n * picks the mode, and a job created without one drafts.\n *\n * - `draft` writes a new automation to the site's Actions through the\n * workflows plugin's draft writer, OFF, built only from the triggers and\n * steps below that this workspace can run. What the description names that\n * the site does not have is left as a placeholder, never guessed.\n * - `explain` reads a saved automation — an action or a workflow — and\n * answers in plain words what it does.\n * - `diagnose` reads a saved automation and one of its FAILED runs, as the\n * run history recorded it, and answers why it failed and what to change.\n *\n * Nothing in any mode switches an automation on, runs one, or changes one.\n */\n\nexport const AI_WORKFLOW_JOB_MODES = ['draft', 'explain', 'diagnose'] as const\nexport type AiWorkflowJobMode = (typeof AI_WORKFLOW_JOB_MODES)[number]\n\n/** What an explained automation is: an action, or a workflow of function calls. */\nexport const AI_WORKFLOW_TARGET_TYPES = ['action', 'workflow'] as const\nexport type AiWorkflowTargetType = (typeof AI_WORKFLOW_TARGET_TYPES)[number]\n\nexport type AiWorkflowJobInputs =\n | { mode: 'draft' }\n | { mode: 'explain'; targetType: AiWorkflowTargetType; targetId: string }\n | { mode: 'diagnose'; targetType: AiWorkflowTargetType; targetId: string; runId: string }\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,128}$/\n\n/** A job's inputs read into its mode, or the sentence that says what is wrong with them. */\nexport function parseAiWorkflowJobInputs(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n): AiWorkflowJobInputs | string {\n const mode = inputs?.['mode'] ?? 'draft'\n if (!(AI_WORKFLOW_JOB_MODES as readonly unknown[]).includes(mode)) {\n return 'inputs.mode must be draft, explain or diagnose'\n }\n if (mode === 'draft') return { mode }\n const targetType = inputs?.['targetType']\n const targetId = inputs?.['targetId']\n if (\n !(AI_WORKFLOW_TARGET_TYPES as readonly unknown[]).includes(targetType) ||\n typeof targetId !== 'string' ||\n !DOCUMENT_ID.test(targetId)\n ) {\n return 'Pick the automation to explain'\n }\n const target = { targetType: targetType as AiWorkflowTargetType, targetId }\n if (mode === 'explain') return { mode, ...target }\n const runId = inputs?.['runId']\n if (typeof runId !== 'string' || !DOCUMENT_ID.test(runId)) return 'Pick the run to explain'\n return { mode: 'diagnose', ...target, runId }\n}\n\n// ── What a workspace can run ──────────────────────────────────────────────\n\n/** A part of the platform some triggers and steps need beyond the actions builder itself. */\nexport type AiAutomationNeed = 'crm' | 'webhooks' | 'bookings'\n\nexport type AiAutomationCapabilities = Readonly<Record<AiAutomationNeed, boolean>>\n\n/** Each need as a sentence names it. */\nexport const AI_AUTOMATION_NEED_LABELS: Readonly<Record<AiAutomationNeed, string>> = {\n crm: 'the CRM',\n webhooks: 'webhooks',\n bookings: 'bookings',\n}\n\n/**\n * What this workspace can run, from the entitlements the executor itself\n * reads before it runs a step: `crm` for the CRM steps, `webhooks` for a\n * webhook post, and `bookings` for the booking trigger's door.\n */\nexport function aiAutomationCapabilities(\n org: Partial<AglynOrgBilling> | Readonly<Record<string, unknown>> | null | undefined,\n): AiAutomationCapabilities {\n const billing = org as Partial<AglynOrgBilling> | null | undefined\n return {\n crm: checkEntitlement(billing, 'crm'),\n webhooks: checkEntitlement(billing, 'webhooks'),\n bookings: checkEntitlement(billing, 'bookings'),\n }\n}\n\n// ── The vocabulary ────────────────────────────────────────────────────────\n\n/**\n * The triggers a drafted automation may start on: every host event, which is\n * every event a server door emits. The on-page events (a click, a scroll) are\n * left out: they watch one element of one page, which a description cannot\n * name, and they belong to the page's own interactions.\n */\nexport const AI_AUTOMATION_TRIGGERS: readonly HostEventType[] = HOST_EVENT_TYPES\n\n/** The triggers only a door of a part of the platform emits. */\nexport const AI_AUTOMATION_TRIGGER_NEEDS: Readonly<Partial<Record<HostEventType, AiAutomationNeed>>> = {\n booking: 'bookings',\n taskCompleted: 'crm',\n contactStageChanged: 'crm',\n dealStageChanged: 'crm',\n dealWon: 'crm',\n dealLost: 'crm',\n}\n\n/**\n * The steps a drafted automation may take: the server steps and the flow\n * steps, which act on the person an event names. The on-page steps — show an\n * element, open a menu, run a script — are left out for the reason the\n * on-page triggers are: they need a selector on a page a description cannot\n * name.\n */\nexport const AI_AUTOMATION_STEP_TYPES = [\n 'sendEmail',\n 'notifyAdmins',\n 'enrollList',\n 'assignCampaign',\n 'runWorkflow',\n 'datasetAppend',\n 'updateDataset',\n 'webhookPost',\n 'siteAlert',\n 'wait',\n 'waitForEvent',\n 'exitFlow',\n 'setContactStage',\n 'addContactTag',\n 'assignContactOwner',\n 'createCrmTask',\n 'logCrmActivity',\n] as const satisfies readonly HostActionStepType[]\n\nexport type AiAutomationStepType = (typeof AI_AUTOMATION_STEP_TYPES)[number]\n\n/** The steps the executor refuses without a part of the platform. */\nexport const AI_AUTOMATION_STEP_NEEDS: Readonly<Partial<Record<AiAutomationStepType, AiAutomationNeed>>> = {\n webhookPost: 'webhooks',\n setContactStage: 'crm',\n addContactTag: 'crm',\n assignContactOwner: 'crm',\n createCrmTask: 'crm',\n logCrmActivity: 'crm',\n}\n\n/** The trigger's name as the Actions editor labels it. */\nexport function aiAutomationTriggerLabel(event: string): string {\n return HOST_EVENT_LABELS[event as HostEventType] ?? event\n}\n\n// ── A description the vocabulary cannot build ─────────────────────────────\n\n/**\n * Why a description was answered with no automation. Each is a fixed reason\n * with a fixed sentence, so what a person reads is never the model's own\n * account of itself.\n */\nexport const AI_AUTOMATION_UNSUPPORTED = [\n 'not-an-automation',\n 'no-trigger',\n 'no-step',\n 'needs-crm',\n 'needs-webhooks',\n 'needs-bookings',\n] as const\n\nexport type AiAutomationUnsupported = (typeof AI_AUTOMATION_UNSUPPORTED)[number]\n\n/** The need an unsupported reason names, for the reasons that name one. */\nexport const AI_AUTOMATION_UNSUPPORTED_NEED: Readonly<\n Partial<Record<AiAutomationUnsupported, AiAutomationNeed>>\n> = {\n 'needs-crm': 'crm',\n 'needs-webhooks': 'webhooks',\n 'needs-bookings': 'bookings',\n}\n\nexport const AI_AUTOMATION_UNSUPPORTED_COPY: Readonly<Record<AiAutomationUnsupported, string>> = {\n 'not-an-automation':\n 'This description is not an automation — something that happens, and what to do then — so nothing was drafted.',\n 'no-trigger':\n 'Nothing on the platform can start an automation like this one, so nothing was drafted. Describe it as something that happens on your site, such as a form being submitted.',\n 'no-step': 'An automation cannot do what this description asks yet, so nothing was drafted.',\n 'needs-crm':\n 'This automation needs the CRM, which this workspace’s plan does not include, so nothing was drafted.',\n 'needs-webhooks':\n 'This automation needs webhooks, which this workspace’s plan does not include, so nothing was drafted.',\n 'needs-bookings':\n 'This automation needs bookings, which this workspace’s plan does not include, so nothing was drafted.',\n}\n\n// ── What a job tells a person ─────────────────────────────────────────────\n\n/** The resource the workflows plugin writes a drafted automation under. */\nexport const AI_AUTOMATION_RESOURCE = 'automation'\n\nexport const AI_WORKFLOW_NO_SITE_COPY = 'This automation job names a site this workspace does not have.'\nexport const AI_WORKFLOW_UNAVAILABLE_COPY = 'Automations are not available on this site.'\nexport const AI_WORKFLOW_GONE_COPY = 'That automation no longer exists.'\nexport const AI_WORKFLOW_RUN_GONE_COPY = 'That run is no longer in the automation’s history.'\nexport const AI_WORKFLOW_RUN_NOT_FAILED_COPY = 'That run did not fail, so there is nothing to explain.'\nexport const AI_WORKFLOW_NO_DRAFT_COPY =\n 'The AI could not draft an automation from this description. Try describing what happens, and what should happen then.'\nexport const AI_WORKFLOW_NO_EXPLANATION_COPY =\n 'The AI could not explain this automation. Try again.'\nexport const AI_WORKFLOW_SAVE_FAILURE_COPY =\n 'The automation was drafted but could not be saved. Try the job again.'\n"],"names":["checkEntitlement","HOST_EVENT_LABELS","HOST_EVENT_TYPES","AI_WORKFLOW_JOB_MODES","AI_WORKFLOW_TARGET_TYPES","DOCUMENT_ID","parseAiWorkflowJobInputs","inputs","mode","includes","targetType","targetId","test","target","runId","AI_AUTOMATION_NEED_LABELS","crm","webhooks","bookings","aiAutomationCapabilities","org","billing","AI_AUTOMATION_TRIGGERS","AI_AUTOMATION_TRIGGER_NEEDS","booking","taskCompleted","contactStageChanged","dealStageChanged","dealWon","dealLost","AI_AUTOMATION_STEP_TYPES","AI_AUTOMATION_STEP_NEEDS","webhookPost","setContactStage","addContactTag","assignContactOwner","createCrmTask","logCrmActivity","aiAutomationTriggerLabel","event","AI_AUTOMATION_UNSUPPORTED","AI_AUTOMATION_UNSUPPORTED_NEED","AI_AUTOMATION_UNSUPPORTED_COPY","AI_AUTOMATION_RESOURCE","AI_WORKFLOW_NO_SITE_COPY","AI_WORKFLOW_UNAVAILABLE_COPY","AI_WORKFLOW_GONE_COPY","AI_WORKFLOW_RUN_GONE_COPY","AI_WORKFLOW_RUN_NOT_FAILED_COPY","AI_WORKFLOW_NO_DRAFT_COPY","AI_WORKFLOW_NO_EXPLANATION_COPY","AI_WORKFLOW_SAVE_FAILURE_COPY"],"mappings":"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"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types';
|
|
18
|
+
import type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements';
|
|
19
|
+
/**
|
|
20
|
+
* THE AI PLUGIN'S PLAN BAND (AGL-3080): how many AI credits each plan
|
|
21
|
+
* includes a month. Compiled into core's `PLAN_ENTITLEMENTS` by the manifest
|
|
22
|
+
* generator (`register.planEntitlements`), so `/pricing`, the plan comparison
|
|
23
|
+
* and every gate read the same figure they always have; this is where it is
|
|
24
|
+
* written, beside the reasoning that sized it.
|
|
25
|
+
*/
|
|
26
|
+
declare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {
|
|
27
|
+
interface PluginEntitlementQuotas {
|
|
28
|
+
/**
|
|
29
|
+
* AI credits per calendar month — the band assist spends against.
|
|
30
|
+
*
|
|
31
|
+
* A CREDIT IS A UNIT OF MODEL COST, not a message. One assist action can
|
|
32
|
+
* cost two orders of magnitude more than another: a question is a few
|
|
33
|
+
* thousand tokens, while generating a screen carries the node tree, the
|
|
34
|
+
* component catalog and the theme tokens in, structured markup out, and
|
|
35
|
+
* iterates. A message allowance would price those the same, so one
|
|
36
|
+
* workspace's ten screen builds would outspend another's thousand
|
|
37
|
+
* questions and both would read as "within allowance".
|
|
38
|
+
*
|
|
39
|
+
* The unit is defined by `ASSIST_CREDIT_COST_USD` and the conversion
|
|
40
|
+
* lives in `assist-credits.ts`. Only that module turns credits into
|
|
41
|
+
* dollars; everything a customer sees counts credits, because the dollar
|
|
42
|
+
* figure behind them is our provider bill and not a price.
|
|
43
|
+
*
|
|
44
|
+
* ## HOW THE BAND IS SIZED, and why it is not a share of the price
|
|
45
|
+
*
|
|
46
|
+
* A credit is a dollar of provider spend at `ASSIST_CREDIT_COST_USD`, so
|
|
47
|
+
* the band IS a liability figure: the plan's whole assist give is
|
|
48
|
+
* `assistCreditsPerMonth / 1000` dollars. Credits past it are not a give
|
|
49
|
+
* — since AGL-2653 they are SOLD at `extraAssistCreditsUsdPer1k`, unless
|
|
50
|
+
* the org's `assistOverage.hardCap` asks `reserveAssistMessage` to refuse
|
|
51
|
+
* at the band instead. Sizing it as a share of the subscription price
|
|
52
|
+
* says nothing about whether the tier can afford it, because the price is
|
|
53
|
+
* also carrying storage, bandwidth, form submissions, dataset storage,
|
|
54
|
+
* API requests, contacts and email.
|
|
55
|
+
*
|
|
56
|
+
* So each band is sized against what those SEVEN other terms leave.
|
|
57
|
+
* Every paid band takes between a quarter and a third of that remainder,
|
|
58
|
+
* which keeps the tier's worst case positive and keeps the ladder's
|
|
59
|
+
* shape: the margin each tier holds at 100% of every band stays in
|
|
60
|
+
* proportion to what it held before assist was sold at all.
|
|
61
|
+
* `tier-margin-floor.spec.ts` is the model, and it pins both the rule and
|
|
62
|
+
* the resulting figures.
|
|
63
|
+
*
|
|
64
|
+
* The Aglyn AI add-on (AGL-2896) adds `AI_ADDON_CREDITS_PER_MONTH[plan]`
|
|
65
|
+
* to this band in `resolveOrgEntitlements` — one pool, widened, rather
|
|
66
|
+
* than a second meter.
|
|
67
|
+
*/
|
|
68
|
+
assistCreditsPerMonth?: number;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* AI credits an Enterprise agreement includes per month, before the contract
|
|
73
|
+
* says otherwise.
|
|
74
|
+
*
|
|
75
|
+
* ## Never `UNLIMITED`, and the reason is not stylistic
|
|
76
|
+
*
|
|
77
|
+
* `UNLIMITED` is `Number.POSITIVE_INFINITY`, `JSON.stringify(Infinity)` is
|
|
78
|
+
* `null`, and `Number(null)` is `0`. An unbounded assist band therefore
|
|
79
|
+
* serialises to a band of ZERO through every route that does not also send
|
|
80
|
+
* the explicit flag `restoreQuotaLimit` rebuilds from — which would hand the
|
|
81
|
+
* only customers with a signed contract the one budget that refuses
|
|
82
|
+
* everything. A finite number crosses the wire as itself.
|
|
83
|
+
*
|
|
84
|
+
* The band is also a real liability rather than a capacity we already own.
|
|
85
|
+
* Storage and page views are infrastructure metered into the deal; assist is
|
|
86
|
+
* a per-token charge from a third party, and generative building spends it in
|
|
87
|
+
* units two orders of magnitude apart.
|
|
88
|
+
*
|
|
89
|
+
* ## Why this number
|
|
90
|
+
*
|
|
91
|
+
* Enterprise carries no list price, so the band cannot be sized the way every
|
|
92
|
+
* self-serve tier's is — against what that tier's other cost terms leave out
|
|
93
|
+
* of a known price. It is anchored to the top of the ladder instead: 116,000
|
|
94
|
+
* credits is twice Agency's band, the rule every Enterprise fallback follows
|
|
95
|
+
* since the 2026-09-07 pricing decision (see the `enterprise` row of
|
|
96
|
+
* `PLAN_ENTITLEMENTS`). In cost that is $116 of provider spend a month, or
|
|
97
|
+
* under 9% of any deal priced at or above the self-serve top. A deal below
|
|
98
|
+
* that is sold as Agency, not written as an Enterprise agreement.
|
|
99
|
+
*
|
|
100
|
+
* ## It is a DEFAULT, and a contract raises it
|
|
101
|
+
*
|
|
102
|
+
* `resolveOrgEntitlements` applies a per-org
|
|
103
|
+
* `entitlements.assistCreditsPerMonth` override ahead of this, so a deal that
|
|
104
|
+
* buys more assist buys it on that org without moving the figure every other
|
|
105
|
+
* agreement is measured against — the same mechanism the other contracted
|
|
106
|
+
* bands use.
|
|
107
|
+
*/
|
|
108
|
+
export declare const ENTERPRISE_ASSIST_CREDITS_PER_MONTH = 116000;
|
|
109
|
+
/**
|
|
110
|
+
* The Free AI taste (AGL-2925): the credits a Free workspace draws on each
|
|
111
|
+
* month, behind a hard wall.
|
|
112
|
+
*
|
|
113
|
+
* ## Why it exists
|
|
114
|
+
*
|
|
115
|
+
* So the AI landing pages can say "generate your first page free" and mean
|
|
116
|
+
* it. Three hundred credits is one or two generated sections or a handful of
|
|
117
|
+
* copy rewrites — enough to see what the assistant does, not enough to build
|
|
118
|
+
* a site on. The band is a WALL: `PLAN_PRICING.free
|
|
119
|
+
* .extraAssistCreditsUsdPer1k` stays `null`, so `assistBandRefuses` answers
|
|
120
|
+
* true and the reservation refuses at 100% with no overage, no switch and no
|
|
121
|
+
* invoice. Nothing about a Free workspace can produce a charge.
|
|
122
|
+
*
|
|
123
|
+
* ## What it costs, and why that is the number
|
|
124
|
+
*
|
|
125
|
+
* A credit is `ASSIST_CREDIT_COST_USD` of provider spend, so the band costs
|
|
126
|
+
* at most **$0.30 per Free workspace per month** — asserted by
|
|
127
|
+
* `apps/console/specs/tier-margin-floor.spec.ts`. It is the one AI band with
|
|
128
|
+
* no invoice behind it, which is why the same issue closes every multiplier
|
|
129
|
+
* a script could use before the flag flips:
|
|
130
|
+
*
|
|
131
|
+
* - it is metered per ACCOUNT as well as per workspace, so the three free
|
|
132
|
+
* workspaces an account may hold (AGL-2265) share one 300-credit
|
|
133
|
+
* allowance rather than tripling it (`reserveAssistMessage`);
|
|
134
|
+
* - a fresh account waits `AI_FREE_MIN_ACCOUNT_AGE_HOURS` before it can
|
|
135
|
+
* spend, and every free request is bounded per IP, per uid and per
|
|
136
|
+
* account per day;
|
|
137
|
+
* - a platform-wide daily ceiling on free spend pauses the taste for
|
|
138
|
+
* everyone until the UTC day rolls, paid workspaces untouched.
|
|
139
|
+
*
|
|
140
|
+
* The account allowance reads THIS constant rather than the workspace's
|
|
141
|
+
* resolved band, because a staff override that widens one workspace's band
|
|
142
|
+
* is a decision about that workspace, not about how much one person may draw
|
|
143
|
+
* across all of theirs.
|
|
144
|
+
*/
|
|
145
|
+
export declare const FREE_AI_TASTE_CREDITS_PER_MONTH = 300;
|
|
146
|
+
/** The AI credits each plan includes a month — see the key's own docblock. */
|
|
147
|
+
export declare const AI_CREDITS_BY_PLAN: Readonly<Record<OrgPlan, number>>;
|
|
148
|
+
/** The AI plugin's plan figures, for the manifest generator. */
|
|
149
|
+
export declare function aiPlanEntitlements(): PluginPlanEntitlementsDeclaration;
|