@aglyn/plugins-ai 1.0.0-beta.261 → 1.0.0-beta.262
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-brief-dialog.component.d.ts +21 -58
- package/src/lib/components/ai-brief-dialog.component.js +8 -132
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-follow.component.js +2 -35
- package/src/lib/components/ai-job-follow.component.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.d.ts +2 -8
- package/src/lib/components/ai-job-plan.component.js +13 -74
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-template-collection-field.component.d.ts +2 -12
- package/src/lib/components/ai-template-collection-field.component.js +13 -10
- package/src/lib/components/ai-template-collection-field.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.d.ts +3 -25
- package/src/lib/components/ai-upsell-dialog.component.js +9 -66
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.d.ts +3 -8
- package/src/lib/components/use-ai-job-run.js +4 -15
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/insights/ai-figure-readers.js +15 -5
- package/src/lib/insights/ai-figure-readers.js.map +1 -1
- package/src/lib/insights/ai-figure-summaries.d.ts +27 -0
- package/src/lib/insights/ai-figure-summaries.js +79 -0
- package/src/lib/insights/ai-figure-summaries.js.map +1 -0
- package/src/lib/insights/ai-insight-readers.d.ts +2 -1
- package/src/lib/insights/ai-insight-readers.js +11 -26
- package/src/lib/insights/ai-insight-readers.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.js +11 -1
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/model/ai-brief-copy.d.ts +85 -0
- package/src/lib/model/ai-brief-copy.js +152 -0
- package/src/lib/model/ai-brief-copy.js.map +1 -0
- package/src/lib/model/ai-create-view.d.ts +200 -0
- package/src/lib/model/ai-create-view.js +263 -0
- package/src/lib/model/ai-create-view.js.map +1 -0
- package/src/lib/model/ai-insight.d.ts +1 -1
- package/src/lib/model/ai-insight.js +4 -39
- package/src/lib/model/ai-insight.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +24 -0
- package/src/lib/model/ai-job-activity.js +52 -1
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-job-plan-lines.d.ts +46 -0
- package/src/lib/model/ai-job-plan-lines.js +70 -0
- package/src/lib/model/ai-job-plan-lines.js.map +1 -0
- package/src/lib/model/ai-template-collections.d.ts +28 -0
- package/src/lib/model/ai-template-collections.js +28 -0
- package/src/lib/model/ai-template-collections.js.map +1 -0
- package/src/lib/model/ai-upsell-copy.d.ts +41 -0
- package/src/lib/model/ai-upsell-copy.js +85 -0
- package/src/lib/model/ai-upsell-copy.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.js +16 -2
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-eval-memory-firestore.js +2 -0
- package/src/lib/runtime/ai-eval-memory-firestore.js.map +1 -1
- package/src/lib/server/ai-create-route.d.ts +32 -0
- package/src/lib/server/ai-create-route.js +295 -0
- package/src/lib/server/ai-create-route.js.map +1 -0
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-activity.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 AI_JOB_PLAN_STEP,\n type AiJobKind,\n type AiJobReviewReason,\n type AiJobStatus,\n type AiJobStepStatus,\n} from './ai-jobs.types'\n\n/**\n * WHAT A JOB IS DOING, IN THE WORDS EVERY SURFACE SHOWS (AGL-3593).\n *\n * A job's `status` is the machine's word: `needs_review` covers both a plan\n * waiting to be confirmed and an answer that broke a building rule twice, and\n * `queued`/`running` say nothing about whether the job is still planning or\n * already building. The console's surfaces — the AI jobs drawer's header and\n * rows, the top-bar indicator, the Assist launcher's badge, the dialog that\n * started the job, and the notification the machine raises — each tell a\n * person the same thing, so they read it from here and nowhere else. A drawer\n * that counted a refused plan as \"running\" while the chip said \"needs you\" is\n * the disagreement this exists to rule out.\n *\n * Pure, and over the fields a stored job and its wire form share, so the\n * server's notifier and the console read the same answer.\n */\n\n/** The fields a phase is read from: a stored job and its wire summary both carry them. */\nexport interface AiJobPhaseSource {\n status: AiJobStatus\n review?: { reason: AiJobReviewReason } | null\n steps: ReadonlyArray<{ name: string; status: AiJobStepStatus }>\n /** The wire summary's: the machine confirms this job's plan itself. */\n autoConfirm?: boolean\n /** When the job last moved; the wire summary's ISO string. */\n updatedAt?: unknown\n}\n\n/**\n * How long a self-confirming plan may sit parked before it reads as waiting\n * for a person after all: a confirmation that never came.\n */\nexport const AI_JOB_SELF_CONFIRM_GRACE_MS = 5 * 60_000\n\n/**\n * Whether a job is parked on a plan the machine is about to confirm itself\n * (AGL-3596). A guided site start records its plan step — parking the job on\n * the plan as every planned job does — and confirms it in the next write, so\n * a surface that reads in between sees `needs_review` for an instant. That\n * instant is the build going on, never a stop: read as one, it showed the\n * person a failed site that turned green again a moment later. The job's\n * `updatedAt` there is the clock its step was claimed at, so the grace covers\n * the plan step's own run as well as the confirmation. Past\n * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is\n * what it says.\n */\nexport function aiJobConfirmingOwnPlan(\n job: Pick<AiJobPhaseSource, 'status' | 'review' | 'autoConfirm' | 'updatedAt'>,\n now: number = Date.now(),\n): boolean {\n if (job.status !== 'needs_review' || job.review?.reason !== 'plan' || job.autoConfirm !== true) return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return !Number.isFinite(at) || now - at < AI_JOB_SELF_CONFIRM_GRACE_MS\n}\n\n/**\n * Where one job stands, as a person reads it:\n *\n * - `queued` — created, no step has started.\n * - `planning` — its plan step is the one running or next.\n * - `plan-ready` — its plan waits for the person to confirm it.\n * - `building` — a step after the plan (or a kind with no plan) is running or next.\n * - `attention` — it stopped for something only a person can change: an\n * answer that broke a building rule, a site at an allowance, the\n * workspace's credits or a switch.\n * - `done`, `failed`, `canceled` — settled.\n */\nexport type AiJobPhase =\n | 'queued'\n | 'planning'\n | 'plan-ready'\n | 'building'\n | 'attention'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport function aiJobPhase(job: AiJobPhaseSource): AiJobPhase {\n if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {\n return job.status\n }\n if (job.status === 'needs_review' && !aiJobConfirmingOwnPlan(job)) {\n return job.review?.reason === 'plan' ? 'plan-ready' : 'attention'\n }\n if (job.status === 'needs_input') return 'attention'\n // Moving: the first step that has not finished says which half it is in.\n const next = job.steps.find((step) => step.status === 'running' || step.status === 'pending')\n if (next?.name === AI_JOB_PLAN_STEP) return 'planning'\n if (job.status === 'queued' && job.steps.every((step) => step.status === 'pending')) {\n return 'queued'\n }\n return 'building'\n}\n\n/** Each phase in the words a status line and a chip use. */\nexport const AI_JOB_PHASE_LABELS: Readonly<Record<AiJobPhase, string>> = {\n queued: 'Queued',\n planning: 'Planning',\n 'plan-ready': 'Plan ready',\n building: 'Building',\n attention: 'Needs attention',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\n/**\n * What the top-bar indicator, the launcher badge and the drawer's header\n * count a job as, or `null` for a settled job, which none of them count.\n *\n * - `needs-you` — a plan waits to be confirmed: the one decision that, left\n * alone, leaves the person's site unbuilt.\n * - `attention` — stopped for something a person must change, which is not a\n * plan to confirm.\n * - `running` — queued, planning or building. Never a stopped job.\n */\nexport type AiJobActivityState = 'needs-you' | 'attention' | 'running'\n\n/** Loudest first: the order the indicator picks its one state by. */\nexport const AI_JOB_ACTIVITY_PRECEDENCE: readonly AiJobActivityState[] = [\n 'needs-you',\n 'attention',\n 'running',\n]\n\nexport function aiJobActivityState(job: AiJobPhaseSource): AiJobActivityState | null {\n const phase = aiJobPhase(job)\n if (phase === 'plan-ready') return 'needs-you'\n if (phase === 'attention') return 'attention'\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return 'running'\n return null\n}\n\n/**\n * The statuses a job the indicator counts can have: every one that is not in\n * `AI_JOB_TERMINAL_STATUSES`. The jobs route's `status=active` list asks\n * Firestore for exactly these, under the (status, createdAt) index.\n */\nexport const AI_JOB_ACTIVE_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n]\n\n/** How many of a list's jobs stand in each state. */\nexport type AiJobActivityCounts = Readonly<Record<AiJobActivityState, number>>\n\nexport function aiJobActivityCounts(jobs: readonly AiJobPhaseSource[]): AiJobActivityCounts {\n const counts: Record<AiJobActivityState, number> = { 'needs-you': 0, attention: 0, running: 0 }\n for (const job of jobs) {\n const state = aiJobActivityState(job)\n if (state) counts[state] += 1\n }\n return counts\n}\n\n/**\n * What the indicator and the launcher show for a list of jobs: the loudest\n * state any of them is in, how many are in it, and the job the indicator\n * opens — the oldest of them, which has waited longest. `null` when none is\n * moving or waiting, which is when both show nothing.\n */\nexport interface AiJobsActivity<T> {\n state: AiJobActivityState\n count: number\n /** The job a click targets. */\n job: T\n /** Planning rather than building, for the running chip's word. */\n planning: boolean\n}\n\nexport function aiJobsActivity<T extends AiJobPhaseSource>(\n jobs: readonly T[],\n): AiJobsActivity<T> | null {\n for (const state of AI_JOB_ACTIVITY_PRECEDENCE) {\n const matching = jobs.filter((job) => aiJobActivityState(job) === state)\n if (!matching.length) continue\n return {\n state,\n count: matching.length,\n // Lists arrive newest first.\n job: matching[matching.length - 1],\n planning:\n state === 'running' &&\n matching.every((job) => {\n const phase = aiJobPhase(job)\n return phase === 'planning' || phase === 'queued'\n }),\n }\n }\n return null\n}\n\n/** The chip's words: `AI · plan ready`, `AI · 2 need attention`, `AI · planning`. */\nexport function aiJobsActivityLabel(activity: AiJobsActivity<unknown>): string {\n const { state, count } = activity\n if (state === 'needs-you') {\n return count === 1 ? 'AI · plan ready' : `AI · ${count} plans ready`\n }\n if (state === 'attention') {\n return count === 1 ? 'AI · needs attention' : `AI · ${count} need attention`\n }\n const verb = activity.planning ? 'planning' : 'working'\n return count === 1 ? `AI · ${verb}` : `AI · ${count} ${verb}`\n}\n\n/**\n * The launcher's accessible name: what the button opens, and what waits in it.\n * `Aglyn Assist, 1 AI job needs you`.\n */\nexport function aiJobsLauncherLabel(\n productName: string,\n activity: AiJobsActivity<unknown> | null,\n): string {\n const base = `Open ${productName} Assist`\n if (!activity) return base\n const jobs = activity.count === 1 ? '1 AI job' : `${activity.count} AI jobs`\n const verb = activity.count === 1 ? 'needs' : 'need'\n if (activity.state === 'needs-you') return `${base}, ${jobs} ${verb} you`\n if (activity.state === 'attention') return `${base}, ${jobs} ${verb} attention`\n return `${base}, ${jobs} running`\n}\n\n/** The drawer header's chips, one per state that has a job, loudest first. */\nexport function aiJobsHeaderChips(\n counts: AiJobActivityCounts,\n): Array<{ state: AiJobActivityState; label: string }> {\n return AI_JOB_ACTIVITY_PRECEDENCE.filter((state) => counts[state] > 0).map((state) => ({\n state,\n label:\n state === 'needs-you'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} you`\n : state === 'attention'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} attention`\n : `${counts[state]} running`,\n }))\n}\n\n/** What a job makes, as a sentence's subject: `Your site`, `Your page`. */\nconst KIND_NOUNS: Partial<Record<AiJobKind, string>> = {\n site: 'site',\n page: 'page',\n template: 'page template',\n layout: 'layout',\n form: 'form',\n component: 'component',\n email: 'email',\n campaign: 'campaign',\n workflow: 'automation',\n logic: 'function',\n edit: 'change',\n theme: 'theme',\n products: 'products',\n experiment: 'experiment',\n insight: 'answer',\n build: 'build',\n navigation: 'menu',\n duplicate: 'copy',\n settings: 'settings',\n media: 'media library change',\n tracking: 'tracking',\n}\n\n/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */\nexport function aiJobKindNoun(kind: AiJobKind): string {\n return KIND_NOUNS[kind] ?? 'AI job'\n}\n\n/**\n * The job kinds that build a site's pages (AGL-3765): a page, a guided start\n * and a build from the Assist chat. Two at once on one site each plan against\n * the site as it was, and race for what its plan holds one of.\n */\nexport const AI_PAGE_BUILDING_JOB_KINDS: readonly AiJobKind[] = ['page', 'site', 'build']\n\n/**\n * How long a plan waiting for its confirmation still counts as a build on its\n * way (AGL-3765): one the person is reading now, not one left unconfirmed\n * days ago.\n */\nexport const AI_SITE_BUILD_PLAN_FRESH_MS = 15 * 60_000\n\n/** What a door that starts a page says while another is being built on the site (AGL-3765). */\nexport const AI_SITE_BUILD_IN_FLIGHT_COPY = 'A page is already being built. Wait for it, or start another anyway.'\n\n/**\n * Another job building pages on this site (AGL-3765): queued, planning or\n * building, or a plan that waits for its confirmation and moved in the last\n * {@link AI_SITE_BUILD_PLAN_FRESH_MS}. `null` when there is none. A door warns\n * with it and still starts the job: two builds can share a site, they just\n * cannot both make what its plan holds only one of.\n */\nexport function aiSiteBuildInFlight<\n T extends AiJobPhaseSource & { id: string; kind: AiJobKind | string; hostId?: string | null },\n>(\n jobs: readonly T[],\n input: { hostId: string | null | undefined; exceptJobId?: string | null; now?: number },\n): T | null {\n if (!input.hostId) return null\n const now = input.now ?? Date.now()\n return (\n jobs.find((job) => {\n if (job.id === input.exceptJobId || job.hostId !== input.hostId) return false\n if (!AI_PAGE_BUILDING_JOB_KINDS.includes(job.kind as AiJobKind)) return false\n const phase = aiJobPhase(job)\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return true\n if (phase !== 'plan-ready') return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return Number.isFinite(at) && now - at < AI_SITE_BUILD_PLAN_FRESH_MS\n }) ?? null\n )\n}\n"],"names":["AI_JOB_PLAN_STEP","AI_JOB_SELF_CONFIRM_GRACE_MS","aiJobConfirmingOwnPlan","job","now","Date","status","review","reason","autoConfirm","at","updatedAt","parse","NaN","Number","isFinite","aiJobPhase","next","steps","find","step","name","every","AI_JOB_PHASE_LABELS","queued","planning","building","attention","done","failed","canceled","AI_JOB_ACTIVITY_PRECEDENCE","aiJobActivityState","phase","AI_JOB_ACTIVE_STATUSES","aiJobActivityCounts","jobs","counts","running","state","aiJobsActivity","matching","filter","length","count","aiJobsActivityLabel","activity","verb","aiJobsLauncherLabel","productName","base","aiJobsHeaderChips","map","label","KIND_NOUNS","site","page","template","layout","form","component","email","campaign","workflow","logic","edit","theme","products","experiment","insight","build","navigation","duplicate","settings","media","tracking","aiJobKindNoun","kind","AI_PAGE_BUILDING_JOB_KINDS","AI_SITE_BUILD_PLAN_FRESH_MS","AI_SITE_BUILD_IN_FLIGHT_COPY","aiSiteBuildInFlight","input","hostId","id","exceptJobId","includes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-activity.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 AI_JOB_PLAN_STEP,\n AI_JOB_TERMINAL_STATUSES,\n type AiJobKind,\n type AiJobReviewReason,\n type AiJobStatus,\n type AiJobStepStatus,\n} from './ai-jobs.types'\n\n/**\n * WHAT A JOB IS DOING, IN THE WORDS EVERY SURFACE SHOWS (AGL-3593).\n *\n * A job's `status` is the machine's word: `needs_review` covers both a plan\n * waiting to be confirmed and an answer that broke a building rule twice, and\n * `queued`/`running` say nothing about whether the job is still planning or\n * already building. The console's surfaces — the AI jobs drawer's header and\n * rows, the top-bar indicator, the Assist launcher's badge, the dialog that\n * started the job, and the notification the machine raises — each tell a\n * person the same thing, so they read it from here and nowhere else. A drawer\n * that counted a refused plan as \"running\" while the chip said \"needs you\" is\n * the disagreement this exists to rule out.\n *\n * Pure, and over the fields a stored job and its wire form share, so the\n * server's notifier and the console read the same answer.\n */\n\n/** The fields a phase is read from: a stored job and its wire summary both carry them. */\nexport interface AiJobPhaseSource {\n status: AiJobStatus\n review?: { reason: AiJobReviewReason } | null\n steps: ReadonlyArray<{ name: string; status: AiJobStepStatus }>\n /** The wire summary's: the machine confirms this job's plan itself. */\n autoConfirm?: boolean\n /** When the job last moved; the wire summary's ISO string. */\n updatedAt?: unknown\n}\n\n/**\n * How long a self-confirming plan may sit parked before it reads as waiting\n * for a person after all: a confirmation that never came.\n */\nexport const AI_JOB_SELF_CONFIRM_GRACE_MS = 5 * 60_000\n\n/**\n * Whether a job is parked on a plan the machine is about to confirm itself\n * (AGL-3596). A guided site start records its plan step — parking the job on\n * the plan as every planned job does — and confirms it in the next write, so\n * a surface that reads in between sees `needs_review` for an instant. That\n * instant is the build going on, never a stop: read as one, it showed the\n * person a failed site that turned green again a moment later. The job's\n * `updatedAt` there is the clock its step was claimed at, so the grace covers\n * the plan step's own run as well as the confirmation. Past\n * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is\n * what it says.\n */\nexport function aiJobConfirmingOwnPlan(\n job: Pick<AiJobPhaseSource, 'status' | 'review' | 'autoConfirm' | 'updatedAt'>,\n now: number = Date.now(),\n): boolean {\n if (job.status !== 'needs_review' || job.review?.reason !== 'plan' || job.autoConfirm !== true) return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return !Number.isFinite(at) || now - at < AI_JOB_SELF_CONFIRM_GRACE_MS\n}\n\n/**\n * Where one job stands, as a person reads it:\n *\n * - `queued` — created, no step has started.\n * - `planning` — its plan step is the one running or next.\n * - `plan-ready` — its plan waits for the person to confirm it.\n * - `building` — a step after the plan (or a kind with no plan) is running or next.\n * - `attention` — it stopped for something only a person can change: an\n * answer that broke a building rule, a site at an allowance, the\n * workspace's credits or a switch.\n * - `done`, `failed`, `canceled` — settled.\n */\nexport type AiJobPhase =\n | 'queued'\n | 'planning'\n | 'plan-ready'\n | 'building'\n | 'attention'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport function aiJobPhase(job: AiJobPhaseSource): AiJobPhase {\n if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {\n return job.status\n }\n if (job.status === 'needs_review' && !aiJobConfirmingOwnPlan(job)) {\n return job.review?.reason === 'plan' ? 'plan-ready' : 'attention'\n }\n if (job.status === 'needs_input') return 'attention'\n // Moving: the first step that has not finished says which half it is in.\n const next = job.steps.find((step) => step.status === 'running' || step.status === 'pending')\n if (next?.name === AI_JOB_PLAN_STEP) return 'planning'\n if (job.status === 'queued' && job.steps.every((step) => step.status === 'pending')) {\n return 'queued'\n }\n return 'building'\n}\n\n/** Each phase in the words a status line and a chip use. */\nexport const AI_JOB_PHASE_LABELS: Readonly<Record<AiJobPhase, string>> = {\n queued: 'Queued',\n planning: 'Planning',\n 'plan-ready': 'Plan ready',\n building: 'Building',\n attention: 'Needs attention',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\n/**\n * What the top-bar indicator, the launcher badge and the drawer's header\n * count a job as, or `null` for a settled job, which none of them count.\n *\n * - `needs-you` — a plan waits to be confirmed: the one decision that, left\n * alone, leaves the person's site unbuilt.\n * - `attention` — stopped for something a person must change, which is not a\n * plan to confirm.\n * - `running` — queued, planning or building. Never a stopped job.\n */\nexport type AiJobActivityState = 'needs-you' | 'attention' | 'running'\n\n/** Loudest first: the order the indicator picks its one state by. */\nexport const AI_JOB_ACTIVITY_PRECEDENCE: readonly AiJobActivityState[] = [\n 'needs-you',\n 'attention',\n 'running',\n]\n\nexport function aiJobActivityState(job: AiJobPhaseSource): AiJobActivityState | null {\n const phase = aiJobPhase(job)\n if (phase === 'plan-ready') return 'needs-you'\n if (phase === 'attention') return 'attention'\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return 'running'\n return null\n}\n\n/**\n * The statuses a job the indicator counts can have: every one that is not in\n * `AI_JOB_TERMINAL_STATUSES`. The jobs route's `status=active` list asks\n * Firestore for exactly these, under the (status, createdAt) index.\n */\nexport const AI_JOB_ACTIVE_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n]\n\n/** How many of a list's jobs stand in each state. */\nexport type AiJobActivityCounts = Readonly<Record<AiJobActivityState, number>>\n\nexport function aiJobActivityCounts(jobs: readonly AiJobPhaseSource[]): AiJobActivityCounts {\n const counts: Record<AiJobActivityState, number> = { 'needs-you': 0, attention: 0, running: 0 }\n for (const job of jobs) {\n const state = aiJobActivityState(job)\n if (state) counts[state] += 1\n }\n return counts\n}\n\n/**\n * What the indicator and the launcher show for a list of jobs: the loudest\n * state any of them is in, how many are in it, and the job the indicator\n * opens — the oldest of them, which has waited longest. `null` when none is\n * moving or waiting, which is when both show nothing.\n */\nexport interface AiJobsActivity<T> {\n state: AiJobActivityState\n count: number\n /** The job a click targets. */\n job: T\n /** Planning rather than building, for the running chip's word. */\n planning: boolean\n}\n\nexport function aiJobsActivity<T extends AiJobPhaseSource>(\n jobs: readonly T[],\n): AiJobsActivity<T> | null {\n for (const state of AI_JOB_ACTIVITY_PRECEDENCE) {\n const matching = jobs.filter((job) => aiJobActivityState(job) === state)\n if (!matching.length) continue\n return {\n state,\n count: matching.length,\n // Lists arrive newest first.\n job: matching[matching.length - 1],\n planning:\n state === 'running' &&\n matching.every((job) => {\n const phase = aiJobPhase(job)\n return phase === 'planning' || phase === 'queued'\n }),\n }\n }\n return null\n}\n\n/** The chip's words: `AI · plan ready`, `AI · 2 need attention`, `AI · planning`. */\nexport function aiJobsActivityLabel(activity: AiJobsActivity<unknown>): string {\n const { state, count } = activity\n if (state === 'needs-you') {\n return count === 1 ? 'AI · plan ready' : `AI · ${count} plans ready`\n }\n if (state === 'attention') {\n return count === 1 ? 'AI · needs attention' : `AI · ${count} need attention`\n }\n const verb = activity.planning ? 'planning' : 'working'\n return count === 1 ? `AI · ${verb}` : `AI · ${count} ${verb}`\n}\n\n/**\n * The launcher's accessible name: what the button opens, and what waits in it.\n * `Aglyn Assist, 1 AI job needs you`.\n */\nexport function aiJobsLauncherLabel(\n productName: string,\n activity: AiJobsActivity<unknown> | null,\n): string {\n const base = `Open ${productName} Assist`\n if (!activity) return base\n const jobs = activity.count === 1 ? '1 AI job' : `${activity.count} AI jobs`\n const verb = activity.count === 1 ? 'needs' : 'need'\n if (activity.state === 'needs-you') return `${base}, ${jobs} ${verb} you`\n if (activity.state === 'attention') return `${base}, ${jobs} ${verb} attention`\n return `${base}, ${jobs} running`\n}\n\n/** The drawer header's chips, one per state that has a job, loudest first. */\nexport function aiJobsHeaderChips(\n counts: AiJobActivityCounts,\n): Array<{ state: AiJobActivityState; label: string }> {\n return AI_JOB_ACTIVITY_PRECEDENCE.filter((state) => counts[state] > 0).map((state) => ({\n state,\n label:\n state === 'needs-you'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} you`\n : state === 'attention'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} attention`\n : `${counts[state]} running`,\n }))\n}\n\n/** What a job makes, as a sentence's subject: `Your site`, `Your page`. */\nconst KIND_NOUNS: Partial<Record<AiJobKind, string>> = {\n site: 'site',\n page: 'page',\n template: 'page template',\n layout: 'layout',\n form: 'form',\n component: 'component',\n email: 'email',\n campaign: 'campaign',\n workflow: 'automation',\n logic: 'function',\n edit: 'change',\n theme: 'theme',\n products: 'products',\n experiment: 'experiment',\n insight: 'answer',\n build: 'build',\n navigation: 'menu',\n duplicate: 'copy',\n settings: 'settings',\n media: 'media library change',\n tracking: 'tracking',\n}\n\n/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */\nexport function aiJobKindNoun(kind: AiJobKind): string {\n return KIND_NOUNS[kind] ?? 'AI job'\n}\n\n/**\n * The job kinds that build a site's pages (AGL-3765): a page, a guided start\n * and a build from the Assist chat. Two at once on one site each plan against\n * the site as it was, and race for what its plan holds one of.\n */\nexport const AI_PAGE_BUILDING_JOB_KINDS: readonly AiJobKind[] = ['page', 'site', 'build']\n\n/**\n * How long a plan waiting for its confirmation still counts as a build on its\n * way (AGL-3765): one the person is reading now, not one left unconfirmed\n * days ago.\n */\nexport const AI_SITE_BUILD_PLAN_FRESH_MS = 15 * 60_000\n\n/** What a door that starts a page says while another is being built on the site (AGL-3765). */\nexport const AI_SITE_BUILD_IN_FLIGHT_COPY = 'A page is already being built. Wait for it, or start another anyway.'\n\n/**\n * Another job building pages on this site (AGL-3765): queued, planning or\n * building, or a plan that waits for its confirmation and moved in the last\n * {@link AI_SITE_BUILD_PLAN_FRESH_MS}. `null` when there is none. A door warns\n * with it and still starts the job: two builds can share a site, they just\n * cannot both make what its plan holds only one of.\n */\nexport function aiSiteBuildInFlight<\n T extends AiJobPhaseSource & { id: string; kind: AiJobKind | string; hostId?: string | null },\n>(\n jobs: readonly T[],\n input: { hostId: string | null | undefined; exceptJobId?: string | null; now?: number },\n): T | null {\n if (!input.hostId) return null\n const now = input.now ?? Date.now()\n return (\n jobs.find((job) => {\n if (job.id === input.exceptJobId || job.hostId !== input.hostId) return false\n if (!AI_PAGE_BUILDING_JOB_KINDS.includes(job.kind as AiJobKind)) return false\n const phase = aiJobPhase(job)\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return true\n if (phase !== 'plan-ready') return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return Number.isFinite(at) && now - at < AI_SITE_BUILD_PLAN_FRESH_MS\n }) ?? null\n )\n}\n\n/** The fields of a job {@link aiJobSettled} and {@link aiJobProblem} read. */\nexport interface AiJobSettleSource {\n status: AiJobStatus\n error?: string | null\n review?: { message?: string | null } | null\n}\n\n/** Whether a job has stopped moving: finished, or waiting on something only a person can change. */\nexport function aiJobSettled(job: AiJobSettleSource | null): boolean {\n if (!job) return false\n return (\n AI_JOB_TERMINAL_STATUSES.includes(job.status) ||\n job.status === 'needs_input' ||\n job.status === 'needs_review'\n )\n}\n\n/**\n * Why a settled job has no result to show, in words a person can act on, or\n * `null` for a job that finished.\n */\nexport function aiJobProblem(job: AiJobSettleSource | null, failed: string): string | null {\n if (!job || !aiJobSettled(job) || job.status === 'done') return null\n if (job.status === 'canceled') return 'The job was canceled.'\n if (job.status === 'needs_review') return job.review?.message || job.error || failed\n return job.error || failed\n}\n\nexport type AiFollowTone = 'info' | 'warning' | 'success' | 'error'\n\n/**\n * What the follow card's status line says in each phase, and how loudly\n * (AGL-3593): the dialog that started a job, and the apps' follow view.\n */\nexport const AI_JOB_FOLLOW_PHASE_LINES: Readonly<Record<AiJobPhase, { severity: AiFollowTone; text: string }>> = {\n queued: { severity: 'info', text: 'Queued. It starts in a moment.' },\n planning: { severity: 'info', text: 'Planning. The plan appears here when it is ready.' },\n 'plan-ready': {\n severity: 'warning',\n text: 'Your plan is ready. Read it below and confirm it to build — nothing is built until you do.',\n },\n building: { severity: 'info', text: 'Building. Everything it builds is an unpublished draft.' },\n attention: { severity: 'warning', text: 'The job stopped and needs you.' },\n done: { severity: 'success', text: 'Done. Your drafts are ready to open.' },\n failed: { severity: 'error', text: 'The job stopped before it finished.' },\n canceled: { severity: 'info', text: 'The job was canceled.' },\n}\n"],"names":["AI_JOB_PLAN_STEP","AI_JOB_TERMINAL_STATUSES","AI_JOB_SELF_CONFIRM_GRACE_MS","aiJobConfirmingOwnPlan","job","now","Date","status","review","reason","autoConfirm","at","updatedAt","parse","NaN","Number","isFinite","aiJobPhase","next","steps","find","step","name","every","AI_JOB_PHASE_LABELS","queued","planning","building","attention","done","failed","canceled","AI_JOB_ACTIVITY_PRECEDENCE","aiJobActivityState","phase","AI_JOB_ACTIVE_STATUSES","aiJobActivityCounts","jobs","counts","running","state","aiJobsActivity","matching","filter","length","count","aiJobsActivityLabel","activity","verb","aiJobsLauncherLabel","productName","base","aiJobsHeaderChips","map","label","KIND_NOUNS","site","page","template","layout","form","component","email","campaign","workflow","logic","edit","theme","products","experiment","insight","build","navigation","duplicate","settings","media","tracking","aiJobKindNoun","kind","AI_PAGE_BUILDING_JOB_KINDS","AI_SITE_BUILD_PLAN_FRESH_MS","AI_SITE_BUILD_IN_FLIGHT_COPY","aiSiteBuildInFlight","input","hostId","id","exceptJobId","includes","aiJobSettled","aiJobProblem","message","error","AI_JOB_FOLLOW_PHASE_LINES","severity","text"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
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 AiBuildPlanEmbed } from './ai-build-plan';
|
|
18
|
+
import type { AiJobPlanSummary } from './ai-jobs.types';
|
|
19
|
+
/**
|
|
20
|
+
* A JOB'S PLAN AS THE LINES A PERSON READS BEFORE CONFIRMING IT (AGL-2935):
|
|
21
|
+
* what it reuses from the site, what it creates and why, the pages it builds,
|
|
22
|
+
* the items, the players it embeds and the store pages the platform adds. The
|
|
23
|
+
* console's plan card draws these, and the native apps' follow view sends them
|
|
24
|
+
* (AGL-3854), so both say the same sentences.
|
|
25
|
+
*/
|
|
26
|
+
/** One line of a plan. */
|
|
27
|
+
export interface AiJobPlanLine {
|
|
28
|
+
/** Stable within one plan, for a list's key. */
|
|
29
|
+
key: string;
|
|
30
|
+
text: string;
|
|
31
|
+
/** Added by the platform rather than the model (a store's own pages, AGL-3676): it uses no AI credits. */
|
|
32
|
+
platformAdded?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** A reference as a person reads it: the record's name, or what the plan creates. */
|
|
35
|
+
export declare function aiPlanRefName(plan: Pick<AiJobPlanSummary, 'labels'> | null, ref: string | null): string;
|
|
36
|
+
/**
|
|
37
|
+
* A third-party player the plan lists, with what it costs the page, read
|
|
38
|
+
* before the member confirms it (rule 16, AGL-3433). The cost is the
|
|
39
|
+
* platform's sentence, never the model's: the player is framed lazily, so its
|
|
40
|
+
* host's code loads when a visitor reaches it, whether or not they press play.
|
|
41
|
+
*/
|
|
42
|
+
export declare function aiPlanEmbedLine(embed: AiBuildPlanEmbed, named: (ref: string) => string): string;
|
|
43
|
+
/** The plan's heading: whether it is still a proposal. */
|
|
44
|
+
export declare function aiJobPlanHeading(plan: Pick<AiJobPlanSummary, 'status'>): string;
|
|
45
|
+
/** Every line of a plan, in the order the plan card lists them. */
|
|
46
|
+
export declare function aiJobPlanLines(plan: AiJobPlanSummary): AiJobPlanLine[];
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
*/ import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, isAiPlanNewRef } from "./ai-build-plan.js";
|
|
17
|
+
import { aiBuildOpNoun } from "./ai-build-progress.js";
|
|
18
|
+
import { aiBuildStorePagesAdded } from "./ai-build-store-pages.js";
|
|
19
|
+
/** A reference as a person reads it: the record's name, or what the plan creates. */ export function aiPlanRefName(plan, ref) {
|
|
20
|
+
var _ref;
|
|
21
|
+
if (!ref) return '';
|
|
22
|
+
return isAiPlanNewRef(ref) ? ref.slice('new:'.length) : (_ref = plan == null ? void 0 : plan.labels[ref]) != null ? _ref : ref;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A third-party player the plan lists, with what it costs the page, read
|
|
26
|
+
* before the member confirms it (rule 16, AGL-3433). The cost is the
|
|
27
|
+
* platform's sentence, never the model's: the player is framed lazily, so its
|
|
28
|
+
* host's code loads when a visitor reaches it, whether or not they press play.
|
|
29
|
+
*/ export function aiPlanEmbedLine(embed, named) {
|
|
30
|
+
const host = AI_BUILD_PLAN_EMBED_HOST_NAMES[embed.host];
|
|
31
|
+
const where = isAiPlanNewRef(embed.where) ? `the component ${named(embed.where)}` : embed.where;
|
|
32
|
+
const link = embed.url ? ` playing ${embed.url}` : ', its link left for you to paste';
|
|
33
|
+
return `Embeds a ${host} player on ${where}${link}, as you asked (“${embed.asked}”). It loads ${host}’s own code when a visitor reaches it, whether or not they press play.`;
|
|
34
|
+
}
|
|
35
|
+
/** The plan's heading: whether it is still a proposal. */ export function aiJobPlanHeading(plan) {
|
|
36
|
+
return plan.status === 'confirmed' ? 'Confirmed plan' : 'Proposed plan';
|
|
37
|
+
}
|
|
38
|
+
/** Every line of a plan, in the order the plan card lists them. */ export function aiJobPlanLines(plan) {
|
|
39
|
+
var _plan_items, _plan_embeds;
|
|
40
|
+
const named = (ref)=>aiPlanRefName(plan, ref);
|
|
41
|
+
return [
|
|
42
|
+
...plan.reuse.map((entry, index)=>({
|
|
43
|
+
key: `reuse-${index}`,
|
|
44
|
+
text: `Reuses the ${entry.kind === 'screen' ? 'page' : entry.kind} ${named(entry.id)} — ${entry.purpose}`
|
|
45
|
+
})),
|
|
46
|
+
...plan.create.map((entry, index)=>({
|
|
47
|
+
key: `create-${index}`,
|
|
48
|
+
text: `Creates the ${AI_BUILD_PLAN_CREATION_NOUNS[entry.kind].noun} ${entry.name}` + `${entry.duplicateOf ? `, from a copy of ${named(entry.duplicateOf)}` : ''} — ${entry.why}`
|
|
49
|
+
})),
|
|
50
|
+
...plan.screens.map((screen, index)=>({
|
|
51
|
+
key: `screen-${index}`,
|
|
52
|
+
text: (screen.record ? `Builds the record template ${screen.title}, a page per record of ${named(screen.record.dataset)} at /${screen.record.base}/…` : `Builds the page ${screen.title} at ${screen.slug}`) + (screen.duplicateOf ? `, from a copy of ${named(screen.duplicateOf)}` : '') + (screen.layout ? ` in ${named(screen.layout)}` : '') + (screen.sections.length ? `: ${screen.sections.map((section)=>section.name).join(', ')}` : '')
|
|
53
|
+
})),
|
|
54
|
+
...((_plan_items = plan.items) != null ? _plan_items : []).map((item)=>({
|
|
55
|
+
key: `item-${item.slot}`,
|
|
56
|
+
text: `Makes the ${aiBuildOpNoun(item.op).toLowerCase()} ${item.name}${item.why ? ` — ${item.why}` : ''}`
|
|
57
|
+
})),
|
|
58
|
+
...((_plan_embeds = plan.embeds) != null ? _plan_embeds : []).map((embed, index)=>({
|
|
59
|
+
key: `embed-${index}`,
|
|
60
|
+
text: aiPlanEmbedLine(embed, named)
|
|
61
|
+
})),
|
|
62
|
+
...aiBuildStorePagesAdded(plan).map((page)=>({
|
|
63
|
+
key: `store-${page.key}`,
|
|
64
|
+
text: `Adds the page ${page.title} at ${page.href} — added for your store automatically, uses no AI credits`,
|
|
65
|
+
platformAdded: true
|
|
66
|
+
}))
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=ai-job-plan-lines.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-plan-lines.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {\n AI_BUILD_PLAN_CREATION_NOUNS,\n AI_BUILD_PLAN_EMBED_HOST_NAMES,\n isAiPlanNewRef,\n type AiBuildPlanEmbed,\n} from './ai-build-plan'\nimport { aiBuildOpNoun } from './ai-build-progress'\nimport { aiBuildStorePagesAdded } from './ai-build-store-pages'\nimport type { AiJobPlanSummary } from './ai-jobs.types'\n\n/**\n * A JOB'S PLAN AS THE LINES A PERSON READS BEFORE CONFIRMING IT (AGL-2935):\n * what it reuses from the site, what it creates and why, the pages it builds,\n * the items, the players it embeds and the store pages the platform adds. The\n * console's plan card draws these, and the native apps' follow view sends them\n * (AGL-3854), so both say the same sentences.\n */\n\n/** One line of a plan. */\nexport interface AiJobPlanLine {\n /** Stable within one plan, for a list's key. */\n key: string\n text: string\n /** Added by the platform rather than the model (a store's own pages, AGL-3676): it uses no AI credits. */\n platformAdded?: boolean\n}\n\n/** A reference as a person reads it: the record's name, or what the plan creates. */\nexport function aiPlanRefName(plan: Pick<AiJobPlanSummary, 'labels'> | null, ref: string | null): string {\n if (!ref) return ''\n return isAiPlanNewRef(ref) ? ref.slice('new:'.length) : (plan?.labels[ref] ?? ref)\n}\n\n/**\n * A third-party player the plan lists, with what it costs the page, read\n * before the member confirms it (rule 16, AGL-3433). The cost is the\n * platform's sentence, never the model's: the player is framed lazily, so its\n * host's code loads when a visitor reaches it, whether or not they press play.\n */\nexport function aiPlanEmbedLine(\n embed: AiBuildPlanEmbed,\n named: (ref: string) => string,\n): string {\n const host = AI_BUILD_PLAN_EMBED_HOST_NAMES[embed.host]\n const where = isAiPlanNewRef(embed.where) ? `the component ${named(embed.where)}` : embed.where\n const link = embed.url ? ` playing ${embed.url}` : ', its link left for you to paste'\n return `Embeds a ${host} player on ${where}${link}, as you asked (“${embed.asked}”). It loads ${host}’s own code when a visitor reaches it, whether or not they press play.`\n}\n\n/** The plan's heading: whether it is still a proposal. */\nexport function aiJobPlanHeading(plan: Pick<AiJobPlanSummary, 'status'>): string {\n return plan.status === 'confirmed' ? 'Confirmed plan' : 'Proposed plan'\n}\n\n/** Every line of a plan, in the order the plan card lists them. */\nexport function aiJobPlanLines(plan: AiJobPlanSummary): AiJobPlanLine[] {\n const named = (ref: string | null) => aiPlanRefName(plan, ref)\n return [\n ...plan.reuse.map((entry, index) => ({\n key: `reuse-${index}`,\n text: `Reuses the ${entry.kind === 'screen' ? 'page' : entry.kind} ${named(entry.id)} — ${entry.purpose}`,\n })),\n ...plan.create.map((entry, index) => ({\n key: `create-${index}`,\n text:\n `Creates the ${AI_BUILD_PLAN_CREATION_NOUNS[entry.kind].noun} ${entry.name}` +\n `${entry.duplicateOf ? `, from a copy of ${named(entry.duplicateOf)}` : ''} — ${entry.why}`,\n })),\n ...plan.screens.map((screen, index) => ({\n key: `screen-${index}`,\n text:\n (screen.record\n ? `Builds the record template ${screen.title}, a page per record of ${named(screen.record.dataset)} at /${screen.record.base}/…`\n : `Builds the page ${screen.title} at ${screen.slug}`) +\n (screen.duplicateOf ? `, from a copy of ${named(screen.duplicateOf)}` : '') +\n (screen.layout ? ` in ${named(screen.layout)}` : '') +\n (screen.sections.length ? `: ${screen.sections.map((section) => section.name).join(', ')}` : ''),\n })),\n ...(plan.items ?? []).map((item) => ({\n key: `item-${item.slot}`,\n text: `Makes the ${aiBuildOpNoun(item.op).toLowerCase()} ${item.name}${item.why ? ` — ${item.why}` : ''}`,\n })),\n ...(plan.embeds ?? []).map((embed, index) => ({\n key: `embed-${index}`,\n text: aiPlanEmbedLine(embed, named),\n })),\n ...aiBuildStorePagesAdded(plan).map((page) => ({\n key: `store-${page.key}`,\n text: `Adds the page ${page.title} at ${page.href} — added for your store automatically, uses no AI credits`,\n platformAdded: true,\n })),\n ]\n}\n"],"names":["AI_BUILD_PLAN_CREATION_NOUNS","AI_BUILD_PLAN_EMBED_HOST_NAMES","isAiPlanNewRef","aiBuildOpNoun","aiBuildStorePagesAdded","aiPlanRefName","plan","ref","slice","length","labels","aiPlanEmbedLine","embed","named","host","where","link","url","asked","aiJobPlanHeading","status","aiJobPlanLines","reuse","map","entry","index","key","text","kind","id","purpose","create","noun","name","duplicateOf","why","screens","screen","record","title","dataset","base","slug","layout","sections","section","join","items","item","slot","op","toLowerCase","embeds","page","href","platformAdded"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
/** A content collection as the field offers it. */
|
|
18
|
+
export interface AiTemplateCollectionOption {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The content collections among a site's collection rows, by name, each
|
|
24
|
+
* named as the template step names it: display name, name, slug, then id.
|
|
25
|
+
*/
|
|
26
|
+
export declare function aiTemplateCollectionOptions(rows: ReadonlyArray<Record<string, unknown> & {
|
|
27
|
+
$id: string;
|
|
28
|
+
}>): AiTemplateCollectionOption[];
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
*/ import { hostCollectionKind } from "@aglyn/aglyn/app-utils/collection-kind";
|
|
17
|
+
/**
|
|
18
|
+
* The content collections among a site's collection rows, by name, each
|
|
19
|
+
* named as the template step names it: display name, name, slug, then id.
|
|
20
|
+
*/ export function aiTemplateCollectionOptions(rows) {
|
|
21
|
+
const text = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
22
|
+
return rows.filter((row)=>hostCollectionKind(row) === 'content').map((row)=>({
|
|
23
|
+
id: row.$id,
|
|
24
|
+
name: text(row['displayName']) || text(row['name']) || text(row['slug']) || row.$id
|
|
25
|
+
})).sort((a, b)=>a.name.localeCompare(b.name));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=ai-template-collections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-template-collections.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 { hostCollectionKind } from '@aglyn/aglyn/app-utils/collection-kind'\n\n/** A content collection as the field offers it. */\nexport interface AiTemplateCollectionOption {\n id: string\n name: string\n}\n\n/**\n * The content collections among a site's collection rows, by name, each\n * named as the template step names it: display name, name, slug, then id.\n */\nexport function aiTemplateCollectionOptions(\n rows: ReadonlyArray<Record<string, unknown> & { $id: string }>,\n): AiTemplateCollectionOption[] {\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n return rows\n .filter((row) => hostCollectionKind(row) === 'content')\n .map((row) => ({\n id: row.$id,\n name: text(row['displayName']) || text(row['name']) || text(row['slug']) || row.$id,\n }))\n .sort((a, b) => a.name.localeCompare(b.name))\n}\n"],"names":["hostCollectionKind","aiTemplateCollectionOptions","rows","text","value","trim","filter","row","map","id","$id","name","sort","a","b","localeCompare"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,kBAAkB,QAAQ,yCAAwC;AAQ3E;;;CAGC,GACD,OAAO,SAASC,4BACdC,IAA8D;IAE9D,MAAMC,OAAO,CAACC,QAAoB,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;IAC7E,OAAOH,KACJI,MAAM,CAAC,CAACC,MAAQP,mBAAmBO,SAAS,WAC5CC,GAAG,CAAC,CAACD,MAAS,CAAA;YACbE,IAAIF,IAAIG,GAAG;YACXC,MAAMR,KAAKI,GAAG,CAAC,cAAc,KAAKJ,KAAKI,GAAG,CAAC,OAAO,KAAKJ,KAAKI,GAAG,CAAC,OAAO,KAAKA,IAAIG,GAAG;QACrF,CAAA,GACCE,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEF,IAAI,CAACI,aAAa,CAACD,EAAEH,IAAI;AAC/C"}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 { AiBriefKind } from './ai-brief-copy';
|
|
18
|
+
/**
|
|
19
|
+
* What a "Create with AI" or "Ask AI" entry makes: the brief kinds, an
|
|
20
|
+
* automation, products, a function or variable, an overlay, and an answer
|
|
21
|
+
* about the figures on a page.
|
|
22
|
+
*/
|
|
23
|
+
export type AiUpsellKind = AiBriefKind | 'workflow' | 'product' | 'logic' | 'overlay' | 'funnel' | 'insight';
|
|
24
|
+
/** What one entry's dialog says. */
|
|
25
|
+
export interface AiUpsellCopy {
|
|
26
|
+
title: string;
|
|
27
|
+
does: string;
|
|
28
|
+
/** The entry's own label, which the button reads and the offer names; "Create with AI" when absent. */
|
|
29
|
+
label?: string;
|
|
30
|
+
}
|
|
31
|
+
/** The label every "Create with AI" entry reads. */
|
|
32
|
+
export declare const AI_UPSELL_CREATE_LABEL = "Create with AI";
|
|
33
|
+
/** What the dialog says one entry would do here. */
|
|
34
|
+
export declare const AI_UPSELL_COPY: Readonly<Record<AiUpsellKind, AiUpsellCopy>>;
|
|
35
|
+
/** The label an entry's button reads. */
|
|
36
|
+
export declare function aiUpsellLabel(kind: AiUpsellKind): string;
|
|
37
|
+
/**
|
|
38
|
+
* The sentence that says where the feature comes from, and who can add it.
|
|
39
|
+
* `feature` is what the entry is called: "Create with AI" unless it is an ask.
|
|
40
|
+
*/
|
|
41
|
+
export declare function aiUpsellOffer(canManageBilling: boolean, feature?: string): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
*/ import { aiAddonName } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
17
|
+
/** The label every "Create with AI" entry reads. */ export const AI_UPSELL_CREATE_LABEL = 'Create with AI';
|
|
18
|
+
/** What the dialog says one entry would do here. */ export const AI_UPSELL_COPY = {
|
|
19
|
+
page: {
|
|
20
|
+
title: 'Create a page with AI',
|
|
21
|
+
does: 'Describe the page you need and AI plans it from your brief, using your theme and ' + 'what the site already has. You confirm the plan, and the page is built as an ' + 'unpublished draft.'
|
|
22
|
+
},
|
|
23
|
+
template: {
|
|
24
|
+
title: 'Create a page template with AI',
|
|
25
|
+
does: 'Describe what each page should show and AI plans the template, filling what ' + 'changes from page to page from each record. You confirm the plan, and the template ' + 'is built as a draft in your library.'
|
|
26
|
+
},
|
|
27
|
+
layout: {
|
|
28
|
+
title: 'Create a layout with AI',
|
|
29
|
+
does: 'Describe what the layout should hold and AI plans it, linking your pages and placing ' + 'your navigation or menu component when the site has one. You confirm the plan, and ' + 'the layout is built as a draft no page uses until you assign it.'
|
|
30
|
+
},
|
|
31
|
+
form: {
|
|
32
|
+
title: 'Create a form with AI',
|
|
33
|
+
does: 'Describe what the form is for and AI plans its fields, its marketing consent and ' + 'where each submission goes. You confirm the plan, and the form is built as a draft.'
|
|
34
|
+
},
|
|
35
|
+
component: {
|
|
36
|
+
title: 'Create a reusable component with AI',
|
|
37
|
+
does: 'Describe what the component should show and AI plans it, making what each page can ' + 'change into its properties. You confirm the plan, and the component is built as a ' + 'draft.'
|
|
38
|
+
},
|
|
39
|
+
workflow: {
|
|
40
|
+
title: 'Create an automation with AI',
|
|
41
|
+
does: 'Describe what should happen, and when, and AI drafts the automation from the ' + 'triggers and steps your plan includes. It is saved switched off for you to review.'
|
|
42
|
+
},
|
|
43
|
+
email: {
|
|
44
|
+
title: 'Create an email with AI',
|
|
45
|
+
does: 'Describe the email you need and AI plans a design from the email blocks, with three ' + 'subject lines and three preheaders to choose between. You confirm the plan, and the ' + 'design is built as a draft that is sent to nobody.'
|
|
46
|
+
},
|
|
47
|
+
campaign: {
|
|
48
|
+
title: 'Create a campaign with AI',
|
|
49
|
+
does: 'Describe what the campaign is for and AI writes its email design and a draft campaign ' + 'that would send it. Nothing is sent or scheduled, and the campaign is aimed at nobody ' + 'until you pick its lists.'
|
|
50
|
+
},
|
|
51
|
+
product: {
|
|
52
|
+
title: 'Create products with AI',
|
|
53
|
+
does: 'Describe what the store sells and AI proposes up to twelve products. You review them ' + 'before any is created, and each is created as a draft at a suggested price that is not on ' + 'your storefront until you review it and activate it.'
|
|
54
|
+
},
|
|
55
|
+
logic: {
|
|
56
|
+
title: 'Create a function or variable with AI',
|
|
57
|
+
does: 'Describe what it should work out or hold and AI drafts the function or variable. It ' + 'opens unsaved in the editor for you to check and save.'
|
|
58
|
+
},
|
|
59
|
+
overlay: {
|
|
60
|
+
title: 'Create an overlay with AI',
|
|
61
|
+
does: 'Describe the announcement bar or popup you want and AI writes its copy within the ' + 'overlay’s limits. It is saved switched off, for you to check and turn on.'
|
|
62
|
+
},
|
|
63
|
+
funnel: {
|
|
64
|
+
title: 'Create a funnel with AI',
|
|
65
|
+
does: 'Describe the journey you want to measure and AI proposes its steps from the site’s ' + 'real pages, forms and products. It opens in the funnel editor for you to check and save.'
|
|
66
|
+
},
|
|
67
|
+
insight: {
|
|
68
|
+
title: 'Ask AI about these numbers',
|
|
69
|
+
label: 'Ask a question',
|
|
70
|
+
does: 'Ask a question about the figures on this page and AI answers from them, every answer ' + 'traced to the figures it is built from. It reads; it changes nothing.'
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/** The label an entry's button reads. */ export function aiUpsellLabel(kind) {
|
|
74
|
+
var _AI_UPSELL_COPY_kind_label;
|
|
75
|
+
return (_AI_UPSELL_COPY_kind_label = AI_UPSELL_COPY[kind].label) != null ? _AI_UPSELL_COPY_kind_label : AI_UPSELL_CREATE_LABEL;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The sentence that says where the feature comes from, and who can add it.
|
|
79
|
+
* `feature` is what the entry is called: "Create with AI" unless it is an ask.
|
|
80
|
+
*/ export function aiUpsellOffer(canManageBilling, feature = AI_UPSELL_CREATE_LABEL) {
|
|
81
|
+
const addon = aiAddonName();
|
|
82
|
+
return canManageBilling ? `${feature} comes with the ${addon} add-on, which you can add to this ` + 'workspace from Billing.' : `${feature} comes with the ${addon} add-on. Ask a workspace owner or admin ` + 'to add it from Billing.';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=ai-upsell-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-upsell-copy.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 { aiAddonName } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AiBriefKind } from './ai-brief-copy'\n\n/**\n * What a \"Create with AI\" or \"Ask AI\" entry makes: the brief kinds, an\n * automation, products, a function or variable, an overlay, and an answer\n * about the figures on a page.\n */\nexport type AiUpsellKind =\n | AiBriefKind\n | 'workflow'\n | 'product'\n | 'logic'\n | 'overlay'\n | 'funnel'\n | 'insight'\n\n/** What one entry's dialog says. */\nexport interface AiUpsellCopy {\n title: string\n does: string\n /** The entry's own label, which the button reads and the offer names; \"Create with AI\" when absent. */\n label?: string\n}\n\n/** The label every \"Create with AI\" entry reads. */\nexport const AI_UPSELL_CREATE_LABEL = 'Create with AI'\n\n/** What the dialog says one entry would do here. */\nexport const AI_UPSELL_COPY: Readonly<Record<AiUpsellKind, AiUpsellCopy>> = {\n page: {\n title: 'Create a page with AI',\n does:\n 'Describe the page you need and AI plans it from your brief, using your theme and ' +\n 'what the site already has. You confirm the plan, and the page is built as an ' +\n 'unpublished draft.',\n },\n template: {\n title: 'Create a page template with AI',\n does:\n 'Describe what each page should show and AI plans the template, filling what ' +\n 'changes from page to page from each record. You confirm the plan, and the template ' +\n 'is built as a draft in your library.',\n },\n layout: {\n title: 'Create a layout with AI',\n does:\n 'Describe what the layout should hold and AI plans it, linking your pages and placing ' +\n 'your navigation or menu component when the site has one. You confirm the plan, and ' +\n 'the layout is built as a draft no page uses until you assign it.',\n },\n form: {\n title: 'Create a form with AI',\n does:\n 'Describe what the form is for and AI plans its fields, its marketing consent and ' +\n 'where each submission goes. You confirm the plan, and the form is built as a draft.',\n },\n component: {\n title: 'Create a reusable component with AI',\n does:\n 'Describe what the component should show and AI plans it, making what each page can ' +\n 'change into its properties. You confirm the plan, and the component is built as a ' +\n 'draft.',\n },\n workflow: {\n title: 'Create an automation with AI',\n does:\n 'Describe what should happen, and when, and AI drafts the automation from the ' +\n 'triggers and steps your plan includes. It is saved switched off for you to review.',\n },\n email: {\n title: 'Create an email with AI',\n does:\n 'Describe the email you need and AI plans a design from the email blocks, with three ' +\n 'subject lines and three preheaders to choose between. You confirm the plan, and the ' +\n 'design is built as a draft that is sent to nobody.',\n },\n campaign: {\n title: 'Create a campaign with AI',\n does:\n 'Describe what the campaign is for and AI writes its email design and a draft campaign ' +\n 'that would send it. Nothing is sent or scheduled, and the campaign is aimed at nobody ' +\n 'until you pick its lists.',\n },\n product: {\n title: 'Create products with AI',\n does:\n 'Describe what the store sells and AI proposes up to twelve products. You review them ' +\n 'before any is created, and each is created as a draft at a suggested price that is not on ' +\n 'your storefront until you review it and activate it.',\n },\n logic: {\n title: 'Create a function or variable with AI',\n does:\n 'Describe what it should work out or hold and AI drafts the function or variable. It ' +\n 'opens unsaved in the editor for you to check and save.',\n },\n overlay: {\n title: 'Create an overlay with AI',\n does:\n 'Describe the announcement bar or popup you want and AI writes its copy within the ' +\n 'overlay’s limits. It is saved switched off, for you to check and turn on.',\n },\n funnel: {\n title: 'Create a funnel with AI',\n does:\n 'Describe the journey you want to measure and AI proposes its steps from the site’s ' +\n 'real pages, forms and products. It opens in the funnel editor for you to check and save.',\n },\n insight: {\n title: 'Ask AI about these numbers',\n label: 'Ask a question',\n does:\n 'Ask a question about the figures on this page and AI answers from them, every answer ' +\n 'traced to the figures it is built from. It reads; it changes nothing.',\n },\n}\n\n/** The label an entry's button reads. */\nexport function aiUpsellLabel(kind: AiUpsellKind): string {\n return AI_UPSELL_COPY[kind].label ?? AI_UPSELL_CREATE_LABEL\n}\n\n/**\n * The sentence that says where the feature comes from, and who can add it.\n * `feature` is what the entry is called: \"Create with AI\" unless it is an ask.\n */\nexport function aiUpsellOffer(canManageBilling: boolean, feature = AI_UPSELL_CREATE_LABEL): string {\n const addon = aiAddonName()\n return canManageBilling\n ? `${feature} comes with the ${addon} add-on, which you can add to this ` +\n 'workspace from Billing.'\n : `${feature} comes with the ${addon} add-on. Ask a workspace owner or admin ` +\n 'to add it from Billing.'\n}\n"],"names":["aiAddonName","AI_UPSELL_CREATE_LABEL","AI_UPSELL_COPY","page","title","does","template","layout","form","component","workflow","email","campaign","product","logic","overlay","funnel","insight","label","aiUpsellLabel","kind","aiUpsellOffer","canManageBilling","feature","addon"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,WAAW,QAAQ,2CAA0C;AAyBtE,kDAAkD,GAClD,OAAO,MAAMC,yBAAyB,iBAAgB;AAEtD,kDAAkD,GAClD,OAAO,MAAMC,iBAA+D;IAC1EC,MAAM;QACJC,OAAO;QACPC,MACE,sFACA,kFACA;IACJ;IACAC,UAAU;QACRF,OAAO;QACPC,MACE,iFACA,wFACA;IACJ;IACAE,QAAQ;QACNH,OAAO;QACPC,MACE,0FACA,wFACA;IACJ;IACAG,MAAM;QACJJ,OAAO;QACPC,MACE,sFACA;IACJ;IACAI,WAAW;QACTL,OAAO;QACPC,MACE,wFACA,uFACA;IACJ;IACAK,UAAU;QACRN,OAAO;QACPC,MACE,kFACA;IACJ;IACAM,OAAO;QACLP,OAAO;QACPC,MACE,yFACA,yFACA;IACJ;IACAO,UAAU;QACRR,OAAO;QACPC,MACE,2FACA,2FACA;IACJ;IACAQ,SAAS;QACPT,OAAO;QACPC,MACE,0FACA,+FACA;IACJ;IACAS,OAAO;QACLV,OAAO;QACPC,MACE,yFACA;IACJ;IACAU,SAAS;QACPX,OAAO;QACPC,MACE,uFACA;IACJ;IACAW,QAAQ;QACNZ,OAAO;QACPC,MACE,wFACA;IACJ;IACAY,SAAS;QACPb,OAAO;QACPc,OAAO;QACPb,MACE,0FACA;IACJ;AACF,EAAC;AAED,uCAAuC,GACvC,OAAO,SAASc,cAAcC,IAAkB;QACvClB;IAAP,QAAOA,6BAAAA,cAAc,CAACkB,KAAK,CAACF,KAAK,YAA1BhB,6BAA8BD;AACvC;AAEA;;;CAGC,GACD,OAAO,SAASoB,cAAcC,gBAAyB,EAAEC,UAAUtB,sBAAsB;IACvF,MAAMuB,QAAQxB;IACd,OAAOsB,mBACH,GAAGC,QAAQ,gBAAgB,EAAEC,MAAM,mCAAmC,CAAC,GACrE,4BACF,GAAGD,QAAQ,gBAAgB,EAAEC,MAAM,wCAAwC,CAAC,GAC1E;AACR"}
|
|
@@ -642,6 +642,15 @@ const COLOR_KEYWORDS = new Set([
|
|
|
642
642
|
const LITERAL_COLOR = /#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\s*\(|\b(?:black|white|red|green|blue|yellow|orange|purple|pink|gray|grey|brown|cyan|magenta|navy|teal|maroon|olive|lime|aqua|silver|gold|indigo|violet|beige|coral|crimson|salmon|tomato|turquoise|khaki|lavender|ivory|azure)\b/i;
|
|
643
643
|
const LENGTH_WITH_UNIT = /-?\d*\.?\d+\s*(?:px|rem|em|pt|vh|vw|ch)\b/i;
|
|
644
644
|
const paletteTokens = new Set(AI_SX_TOKENS.palette);
|
|
645
|
+
/**
|
|
646
|
+
* A color written as a value — a hex code or a color function — and never a
|
|
647
|
+
* color WORD. Plan prose names things in words: a photographer's "Blue Ridge
|
|
648
|
+
* elopements" or "Black and white portraits" is a place and a style, not a
|
|
649
|
+
* color, and the live 2026-10-16 guided start was refused for it (AGL-3864).
|
|
650
|
+
*/ const LITERAL_COLOR_VALUE = /#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\s*\(/i;
|
|
651
|
+
/** Whether plan prose writes a color by value: a hex code or a color function. */ function proseHasLiteralColor(value) {
|
|
652
|
+
return typeof value === 'string' && LITERAL_COLOR_VALUE.test(value);
|
|
653
|
+
}
|
|
645
654
|
/** Whether a value names a color by value rather than by theme token. */ function hasLiteralColor(value) {
|
|
646
655
|
if (typeof value !== 'string') return false;
|
|
647
656
|
const parts = value.trim().split(/\s+/).filter((part)=>!paletteTokens.has(part) && !COLOR_KEYWORDS.has(part.toLowerCase()));
|
|
@@ -4513,12 +4522,12 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
4513
4522
|
const paths = [];
|
|
4514
4523
|
plan.create.forEach((entry, index)=>{
|
|
4515
4524
|
if (entry.kind === 'theme-change') return;
|
|
4516
|
-
if (
|
|
4525
|
+
if (proseHasLiteralColor(entry.why) || entry.fields.some(proseHasLiteralColor)) {
|
|
4517
4526
|
paths.push(`create[${index}]`);
|
|
4518
4527
|
}
|
|
4519
4528
|
});
|
|
4520
4529
|
for (const { section, path } of sectionPaths(plan)){
|
|
4521
|
-
if (
|
|
4530
|
+
if (proseHasLiteralColor(section.name)) paths.push(path);
|
|
4522
4531
|
}
|
|
4523
4532
|
return paths.length ? [
|
|
4524
4533
|
{
|
|
@@ -4766,6 +4775,11 @@ const NEWSLETTER_NAME = /\bnewsletter\b|\bsubscribe\b|\bsign[- ]?up\b/i;
|
|
|
4766
4775
|
].includes(String(refKind(ref, plan, kinds))));
|
|
4767
4776
|
if (bindsData) continue;
|
|
4768
4777
|
if (section.items >= AI_TYPED_LIST_MIN_ITEMS) typed.push(path);
|
|
4778
|
+
// A form section COLLECTS what its dataset holds; it never copies it
|
|
4779
|
+
// (AGL-3864). A workspace whose other site made a "Consultation
|
|
4780
|
+
// requests" dataset refused a new law firm's "Consultation requests" form.
|
|
4781
|
+
const collects = FORM_SECTION_NAME.test(section.name) || section.uses.some((ref)=>refKind(ref, plan, kinds) === 'form');
|
|
4782
|
+
if (collects) continue;
|
|
4769
4783
|
const match = data.find((row)=>aiNamesMatch(row.name, section.name));
|
|
4770
4784
|
if (match) unbound.push({
|
|
4771
4785
|
path,
|