@aglyn/plugins-ai 1.0.0-beta.261 → 1.0.0-beta.263
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 +96 -0
- package/src/lib/insights/ai-figure-summaries.js.map +1 -0
- package/src/lib/insights/ai-insight-digest.d.ts +2 -0
- package/src/lib/insights/ai-insight-digest.js +7 -4
- package/src/lib/insights/ai-insight-digest.js.map +1 -1
- 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-job-insight-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +5 -3
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +2 -0
- package/src/lib/jobs/ai-job-page-language.js +23 -2
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +14 -1
- package/src/lib/jobs/ai-job-plan-step.js +44 -9
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +8 -1
- package/src/lib/jobs/ai-jobs-notify.js +44 -2
- package/src/lib/jobs/ai-jobs-notify.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/layout-language/ai-layout-compiler.js +18 -7
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-promised-copy.d.ts +54 -0
- package/src/lib/layout-language/ai-layout-promised-copy.js +104 -0
- package/src/lib/layout-language/ai-layout-promised-copy.js.map +1 -0
- 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 +10 -1
- package/src/lib/model/ai-insight.js +16 -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-notice.d.ts +7 -1
- package/src/lib/model/ai-job-notice.js +10 -9
- package/src/lib/model/ai-job-notice.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-plan-said-sections.d.ts +81 -0
- package/src/lib/model/ai-plan-said-sections.js +168 -0
- package/src/lib/model/ai-plan-said-sections.js.map +1 -0
- package/src/lib/model/ai-site-job.d.ts +8 -2
- package/src/lib/model/ai-site-job.js +7 -1
- package/src/lib/model/ai-site-job.js.map +1 -1
- 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.d.ts +15 -1
- package/src/lib/runtime/ai-doctrine-validators.js +96 -3
- 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/runtime/ai-palette.generated.js +11 -3
- package/src/lib/runtime/ai-palette.generated.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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"}
|
|
@@ -73,4 +73,10 @@ export declare function aiJobNoticeLink(job: AiJobNoticeSource, to: AiJobNoticeT
|
|
|
73
73
|
* person who created it. Customer-safe throughout: a stopped job's sentence is
|
|
74
74
|
* the job's own `error`, which the machine writes as a fixed sentence.
|
|
75
75
|
*/
|
|
76
|
-
|
|
76
|
+
/** The site a job worked on, as its notice names it: its name, and where it is live. */
|
|
77
|
+
export interface AiJobNoticeSite {
|
|
78
|
+
name: string;
|
|
79
|
+
/** The site's live address, absolute; `null` when it has none yet. */
|
|
80
|
+
url: string | null;
|
|
81
|
+
}
|
|
82
|
+
export declare function aiJobNotice(job: AiJobNoticeSource, to: AiJobNoticeTransition, site?: AiJobNoticeSite | null): AiJobNotice;
|
|
@@ -76,11 +76,7 @@ import { aiCustomerSafeCopy, aiJobRefundCopy } from "./ai-job-failure-copy.js";
|
|
|
76
76
|
}
|
|
77
77
|
return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`;
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
* The in-app notification for one change of one job (AGL-3593), for the
|
|
81
|
-
* person who created it. Customer-safe throughout: a stopped job's sentence is
|
|
82
|
-
* the job's own `error`, which the machine writes as a fixed sentence.
|
|
83
|
-
*/ export function aiJobNotice(job, to) {
|
|
79
|
+
export function aiJobNotice(job, to, site = null) {
|
|
84
80
|
const noun = aiJobKindNoun(job.kind);
|
|
85
81
|
const scope = _extends({
|
|
86
82
|
orgId: job.orgId
|
|
@@ -142,17 +138,22 @@ import { aiCustomerSafeCopy, aiJobRefundCopy } from "./ai-job-failure-copy.js";
|
|
|
142
138
|
// A guided site start publishes its pages when it finishes (AGL-3596):
|
|
143
139
|
// with one live, the site is, and the build page lists any that stayed
|
|
144
140
|
// drafts with the reason for each.
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
141
|
+
const published = job.kind === 'site' ? (_ref = (_job_sitePublish = job.sitePublish) == null ? void 0 : _job_sitePublish.published.length) != null ? _ref : 0 : 0;
|
|
142
|
+
if (published > 0) {
|
|
147
143
|
var _ref1;
|
|
148
144
|
var _job_sitePublish1;
|
|
149
145
|
const drafts = (_ref1 = (_job_sitePublish1 = job.sitePublish) == null ? void 0 : _job_sitePublish1.drafts.length) != null ? _ref1 : 0;
|
|
150
146
|
const stayed = drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`;
|
|
147
|
+
// Named, with its address (AGL-3889): someone with several sites, or
|
|
148
|
+
// reading the email days later, cannot tell which site went live from
|
|
149
|
+
// "Your site is live", nor where to see it.
|
|
150
|
+
const live = site ? `${site.name} is live` : 'Your site is live';
|
|
151
|
+
const at = (site == null ? void 0 : site.url) ? ` at ${site.url}` : '';
|
|
151
152
|
return _extends({
|
|
152
153
|
type: 'content.aiJobDone',
|
|
153
154
|
level,
|
|
154
|
-
title: partial ?
|
|
155
|
-
body: partial ? `${partial} ${drafts > 0 ? `What was built is published, except ${stayed}.` :
|
|
155
|
+
title: partial ? `${live}, but part of it wasn’t built` : live,
|
|
156
|
+
body: partial ? `${partial} ${drafts > 0 ? `What was built is published${at}, except ${stayed}.` : `What was built is published${at}.`}` : drafts > 0 ? `Your pages are published${at}, except ${stayed}. Open the build page to see why.` : at ? `Your pages are published${at}. Visit your site there, or open it in the console to edit your pages.` : 'Your pages are published. Open it to view your site or edit your pages.',
|
|
156
157
|
link
|
|
157
158
|
}, scope);
|
|
158
159
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-notice.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 { AglynNotification } from '@aglyn/aglyn/app-utils/notifications'\nimport { aiBuildOutcomeLine, aiSitePartialCopy } from './ai-build-progress'\nimport { aiJobKindNoun } from './ai-job-activity'\nimport { aiCustomerSafeCopy, aiJobRefundCopy } from './ai-job-failure-copy'\nimport type { AiJobItemLedger, AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types'\n\n/** The besigner segment each versioned resource lives under. */\nexport const AI_JOB_BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>> = {\n screen: 'screens',\n reusableComponent: 'components',\n layout: 'layouts',\n template: 'templates',\n // An email design is a screen: it opens in the screen besigner, as the\n // Emails page's own Edit design does.\n emailScreen: 'screens',\n}\n\n/**\n * The query parameter a console address carries to open AI jobs on one job\n * (AGL-3593): the Assist panel reads it on arrival.\n */\nexport const AI_JOB_LINK_PARAM = 'aiJob'\n\n/** Where a site's \"Building your site\" page lives, under the site (AGL-3594). */\nexport const AI_SITE_BUILD_HREF = '/ai-jobs'\n\n/**\n * What a job the meter paused says comes next (AGL-3660), on its notice and\n * its page alike: nothing it built is lost, and it carries on from where it\n * stopped — by Resume once there are credits, or by itself when they return.\n */\nexport const AI_JOB_PAUSED_NEXT_COPY =\n 'Nothing is lost: what it built so far is saved. Once you have AI credits again — from Billing, ' +\n 'or when your monthly credits reset — choose Resume, or it carries on by itself within about an hour.'\n\n/** A paused job's heading, by what it builds. */\nexport function aiJobPausedTitle(kind: AiJobKind): string {\n const noun = aiJobKindNoun(kind)\n return noun === 'AI job' ? 'Your AI job is paused' : `Your ${noun} ${noun.endsWith('s') ? 'are' : 'is'} paused`\n}\n\n/** A change the person who started a job is told about. */\nexport type AiJobNoticeTransition = 'needs-review' | 'done' | 'failed' | 'paused'\n\n/** The fields of a stored job a notice is written from. */\nexport interface AiJobNoticeSource {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n review?: { reason: AiJobReviewReason; message?: string | null } | null\n outputs?: readonly AiJobOutput[] | null\n error?: string | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: Pick<AiJobSitePublish, 'published' | 'drafts'> | null\n /** What the job spent, and gave back for a failure on our side (AGL-3596). */\n creditsSpent?: number\n refundedCredits?: number\n /** A build's or a guided start's items (AGL-3616), so a finished one says what was not built. */\n items?: AiJobItemLedger[] | null\n}\n\n/** A stopped job's sentence with what became of its credits, from the job's recorded give-back (AGL-3596). */\nfunction withRefund(job: AiJobNoticeSource, text: string, status: 'failed' | 'needs_review'): string {\n const refund = aiJobRefundCopy({ status, refundedCredits: job.refundedCredits, creditsSpent: job.creditsSpent })\n return refund ? `${text} ${refund}` : text\n}\n\nexport type AiJobNotice = Pick<\n AglynNotification,\n 'type' | 'title' | 'body' | 'link' | 'orgId' | 'hostId' | 'level'\n>\n\n/**\n * Where a notice's link goes, in the stored form the console rewrites when it\n * is followed (`normalizeNotificationLink`): `/{hostId}/…` for the job's site,\n * `/org…` for a job of the workspace's.\n *\n * A job that is done leads to what it built (AGL-3593): several pages of the\n * job's site open that site's Pages list, one versioned output opens in the\n * editor on the version the job wrote. Everything else — a plan to confirm, a\n * job that stopped, a done job with nothing of its own to open — opens AI\n * jobs on the job.\n */\nexport function aiJobNoticeLink(job: AiJobNoticeSource, to: AiJobNoticeTransition): string {\n const base = job.hostId ? `/${job.hostId}` : '/org'\n // A site job's every notice opens its \"Building your site\" page (AGL-3594),\n // which leads with the next step whatever the job's state.\n if (job.kind === 'site' && job.hostId) return `${base}${AI_SITE_BUILD_HREF}/${encodeURIComponent(job.$id)}`\n if (to === 'done' && job.hostId) {\n const own = (job.outputs ?? []).filter((output) => output.hostId === job.hostId)\n const screens = own.filter((output) => output.resource === 'screen')\n if (screens.length > 1) return `${base}/screens`\n const versioned = own.find(\n (output) => AI_JOB_BESIGNER_SEGMENT[output.resource] && output.versionId,\n )\n if (own.length === 1 && versioned) {\n const segment = AI_JOB_BESIGNER_SEGMENT[versioned.resource]\n return `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`\n }\n }\n return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`\n}\n\n/**\n * The in-app notification for one change of one job (AGL-3593), for the\n * person who created it. Customer-safe throughout: a stopped job's sentence is\n * the job's own `error`, which the machine writes as a fixed sentence.\n */\nexport function aiJobNotice(job: AiJobNoticeSource, to: AiJobNoticeTransition): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, to)\n if (to === 'needs-review') {\n if (job.review?.reason === 'plan') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title:\n noun === 'AI job'\n ? 'Your plan is ready: confirm it to build'\n : `Your ${noun} plan is ready: confirm it to build`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs.',\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} needs you`,\n // The doctrine's own words never reach a customer (AGL-3596).\n body: withRefund(\n job,\n aiCustomerSafeCopy(job.review?.message || job.error || 'It stopped for your decision. It waits in AI jobs.', { page: job.kind === 'page' }),\n 'needs_review',\n ),\n link,\n ...scope,\n }\n }\n if (to === 'paused') {\n // Paused by the meter (AGL-3660): out of credits or at a cap. Nothing is\n // lost and the job is not over, so it is a warning that names the way on.\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: aiJobPausedTitle(job.kind),\n body: [job.error, AI_JOB_PAUSED_NEXT_COPY].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n if (to === 'done') {\n // A job that finished with part of it unbuilt (AGL-3596) says so in the\n // words its page uses, as a warning: \"done\" alone hid a page that failed.\n const items = job.items ?? []\n const unbuilt = items.some((row) => row.status === 'failed' || row.status === 'skipped')\n const level = unbuilt ? 'warning' : 'success'\n const partial =\n job.kind === 'site'\n ? aiSitePartialCopy({ items, status: 'done' })\n : unbuilt\n ? aiBuildOutcomeLine({ items, status: 'done' })\n : null\n // A guided site start publishes its pages when it finishes (AGL-3596):\n // with one live, the site is, and the build page lists any that stayed\n // drafts with the reason for each.\n const live = job.kind === 'site' ? (job.sitePublish?.published.length ?? 0) : 0\n if (live > 0) {\n const drafts = job.sitePublish?.drafts.length ?? 0\n const stayed = drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`\n return {\n type: 'content.aiJobDone',\n level,\n title: partial ? 'Your site is live, but part of it wasn’t built' : 'Your site is live',\n body: partial\n ? `${partial} ${drafts > 0 ? `What was built is published, except ${stayed}.` : 'What was built is published.'}`\n : drafts > 0\n ? `Your pages are published, except ${stayed}. Open it to see why.`\n : 'Your pages are published. Open it to view your site or edit your pages.',\n link,\n ...scope,\n }\n }\n const pages = (job.outputs ?? []).filter((output) => output.resource === 'screen').length\n const title =\n job.kind === 'site' || pages > 1\n ? 'Your site’s draft pages are ready'\n : job.kind === 'page'\n ? 'Your draft page is ready'\n : noun === 'AI job'\n ? 'Your AI job finished'\n : `Your ${noun} is ready`\n return {\n type: 'content.aiJobDone',\n level,\n title,\n body:\n job.kind === 'insight' || job.kind === 'text'\n ? 'Open AI jobs to read it.'\n : [partial, 'Everything it built is an unpublished draft until you publish it.'].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobFailed',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} stopped`,\n body: withRefund(job, aiCustomerSafeCopy(job.error || 'It stopped before it finished.', { page: job.kind === 'page' }), 'failed'),\n link,\n ...scope,\n }\n}\n"],"names":["aiBuildOutcomeLine","aiSitePartialCopy","aiJobKindNoun","aiCustomerSafeCopy","aiJobRefundCopy","AI_JOB_BESIGNER_SEGMENT","screen","reusableComponent","layout","template","emailScreen","AI_JOB_LINK_PARAM","AI_SITE_BUILD_HREF","AI_JOB_PAUSED_NEXT_COPY","aiJobPausedTitle","kind","noun","endsWith","withRefund","job","text","status","refund","refundedCredits","creditsSpent","aiJobNoticeLink","to","base","hostId","encodeURIComponent","$id","own","outputs","filter","output","screens","resource","length","versioned","find","versionId","segment","id","aiJobNotice","scope","orgId","link","review","reason","type","level","title","body","message","error","page","Boolean","join","items","unbuilt","some","row","partial","live","sitePublish","published","drafts","stayed","pages"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-notice.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 { AglynNotification } from '@aglyn/aglyn/app-utils/notifications'\nimport { aiBuildOutcomeLine, aiSitePartialCopy } from './ai-build-progress'\nimport { aiJobKindNoun } from './ai-job-activity'\nimport { aiCustomerSafeCopy, aiJobRefundCopy } from './ai-job-failure-copy'\nimport type { AiJobItemLedger, AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types'\n\n/** The besigner segment each versioned resource lives under. */\nexport const AI_JOB_BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>> = {\n screen: 'screens',\n reusableComponent: 'components',\n layout: 'layouts',\n template: 'templates',\n // An email design is a screen: it opens in the screen besigner, as the\n // Emails page's own Edit design does.\n emailScreen: 'screens',\n}\n\n/**\n * The query parameter a console address carries to open AI jobs on one job\n * (AGL-3593): the Assist panel reads it on arrival.\n */\nexport const AI_JOB_LINK_PARAM = 'aiJob'\n\n/** Where a site's \"Building your site\" page lives, under the site (AGL-3594). */\nexport const AI_SITE_BUILD_HREF = '/ai-jobs'\n\n/**\n * What a job the meter paused says comes next (AGL-3660), on its notice and\n * its page alike: nothing it built is lost, and it carries on from where it\n * stopped — by Resume once there are credits, or by itself when they return.\n */\nexport const AI_JOB_PAUSED_NEXT_COPY =\n 'Nothing is lost: what it built so far is saved. Once you have AI credits again — from Billing, ' +\n 'or when your monthly credits reset — choose Resume, or it carries on by itself within about an hour.'\n\n/** A paused job's heading, by what it builds. */\nexport function aiJobPausedTitle(kind: AiJobKind): string {\n const noun = aiJobKindNoun(kind)\n return noun === 'AI job' ? 'Your AI job is paused' : `Your ${noun} ${noun.endsWith('s') ? 'are' : 'is'} paused`\n}\n\n/** A change the person who started a job is told about. */\nexport type AiJobNoticeTransition = 'needs-review' | 'done' | 'failed' | 'paused'\n\n/** The fields of a stored job a notice is written from. */\nexport interface AiJobNoticeSource {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n review?: { reason: AiJobReviewReason; message?: string | null } | null\n outputs?: readonly AiJobOutput[] | null\n error?: string | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: Pick<AiJobSitePublish, 'published' | 'drafts'> | null\n /** What the job spent, and gave back for a failure on our side (AGL-3596). */\n creditsSpent?: number\n refundedCredits?: number\n /** A build's or a guided start's items (AGL-3616), so a finished one says what was not built. */\n items?: AiJobItemLedger[] | null\n}\n\n/** A stopped job's sentence with what became of its credits, from the job's recorded give-back (AGL-3596). */\nfunction withRefund(job: AiJobNoticeSource, text: string, status: 'failed' | 'needs_review'): string {\n const refund = aiJobRefundCopy({ status, refundedCredits: job.refundedCredits, creditsSpent: job.creditsSpent })\n return refund ? `${text} ${refund}` : text\n}\n\nexport type AiJobNotice = Pick<\n AglynNotification,\n 'type' | 'title' | 'body' | 'link' | 'orgId' | 'hostId' | 'level'\n>\n\n/**\n * Where a notice's link goes, in the stored form the console rewrites when it\n * is followed (`normalizeNotificationLink`): `/{hostId}/…` for the job's site,\n * `/org…` for a job of the workspace's.\n *\n * A job that is done leads to what it built (AGL-3593): several pages of the\n * job's site open that site's Pages list, one versioned output opens in the\n * editor on the version the job wrote. Everything else — a plan to confirm, a\n * job that stopped, a done job with nothing of its own to open — opens AI\n * jobs on the job.\n */\nexport function aiJobNoticeLink(job: AiJobNoticeSource, to: AiJobNoticeTransition): string {\n const base = job.hostId ? `/${job.hostId}` : '/org'\n // A site job's every notice opens its \"Building your site\" page (AGL-3594),\n // which leads with the next step whatever the job's state.\n if (job.kind === 'site' && job.hostId) return `${base}${AI_SITE_BUILD_HREF}/${encodeURIComponent(job.$id)}`\n if (to === 'done' && job.hostId) {\n const own = (job.outputs ?? []).filter((output) => output.hostId === job.hostId)\n const screens = own.filter((output) => output.resource === 'screen')\n if (screens.length > 1) return `${base}/screens`\n const versioned = own.find(\n (output) => AI_JOB_BESIGNER_SEGMENT[output.resource] && output.versionId,\n )\n if (own.length === 1 && versioned) {\n const segment = AI_JOB_BESIGNER_SEGMENT[versioned.resource]\n return `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`\n }\n }\n return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`\n}\n\n/**\n * The in-app notification for one change of one job (AGL-3593), for the\n * person who created it. Customer-safe throughout: a stopped job's sentence is\n * the job's own `error`, which the machine writes as a fixed sentence.\n */\n/** The site a job worked on, as its notice names it: its name, and where it is live. */\nexport interface AiJobNoticeSite {\n name: string\n /** The site's live address, absolute; `null` when it has none yet. */\n url: string | null\n}\n\nexport function aiJobNotice(\n job: AiJobNoticeSource,\n to: AiJobNoticeTransition,\n site: AiJobNoticeSite | null = null,\n): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, to)\n if (to === 'needs-review') {\n if (job.review?.reason === 'plan') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title:\n noun === 'AI job'\n ? 'Your plan is ready: confirm it to build'\n : `Your ${noun} plan is ready: confirm it to build`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs.',\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} needs you`,\n // The doctrine's own words never reach a customer (AGL-3596).\n body: withRefund(\n job,\n aiCustomerSafeCopy(job.review?.message || job.error || 'It stopped for your decision. It waits in AI jobs.', { page: job.kind === 'page' }),\n 'needs_review',\n ),\n link,\n ...scope,\n }\n }\n if (to === 'paused') {\n // Paused by the meter (AGL-3660): out of credits or at a cap. Nothing is\n // lost and the job is not over, so it is a warning that names the way on.\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: aiJobPausedTitle(job.kind),\n body: [job.error, AI_JOB_PAUSED_NEXT_COPY].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n if (to === 'done') {\n // A job that finished with part of it unbuilt (AGL-3596) says so in the\n // words its page uses, as a warning: \"done\" alone hid a page that failed.\n const items = job.items ?? []\n const unbuilt = items.some((row) => row.status === 'failed' || row.status === 'skipped')\n const level = unbuilt ? 'warning' : 'success'\n const partial =\n job.kind === 'site'\n ? aiSitePartialCopy({ items, status: 'done' })\n : unbuilt\n ? aiBuildOutcomeLine({ items, status: 'done' })\n : null\n // A guided site start publishes its pages when it finishes (AGL-3596):\n // with one live, the site is, and the build page lists any that stayed\n // drafts with the reason for each.\n const published = job.kind === 'site' ? (job.sitePublish?.published.length ?? 0) : 0\n if (published > 0) {\n const drafts = job.sitePublish?.drafts.length ?? 0\n const stayed = drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`\n // Named, with its address (AGL-3889): someone with several sites, or\n // reading the email days later, cannot tell which site went live from\n // \"Your site is live\", nor where to see it.\n const live = site ? `${site.name} is live` : 'Your site is live'\n const at = site?.url ? ` at ${site.url}` : ''\n return {\n type: 'content.aiJobDone',\n level,\n title: partial ? `${live}, but part of it wasn’t built` : live,\n body: partial\n ? `${partial} ${drafts > 0 ? `What was built is published${at}, except ${stayed}.` : `What was built is published${at}.`}`\n : drafts > 0\n ? `Your pages are published${at}, except ${stayed}. Open the build page to see why.`\n : at\n ? `Your pages are published${at}. Visit your site there, or open it in the console to edit your pages.`\n : 'Your pages are published. Open it to view your site or edit your pages.',\n link,\n ...scope,\n }\n }\n const pages = (job.outputs ?? []).filter((output) => output.resource === 'screen').length\n const title =\n job.kind === 'site' || pages > 1\n ? 'Your site’s draft pages are ready'\n : job.kind === 'page'\n ? 'Your draft page is ready'\n : noun === 'AI job'\n ? 'Your AI job finished'\n : `Your ${noun} is ready`\n return {\n type: 'content.aiJobDone',\n level,\n title,\n body:\n job.kind === 'insight' || job.kind === 'text'\n ? 'Open AI jobs to read it.'\n : [partial, 'Everything it built is an unpublished draft until you publish it.'].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobFailed',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} stopped`,\n body: withRefund(job, aiCustomerSafeCopy(job.error || 'It stopped before it finished.', { page: job.kind === 'page' }), 'failed'),\n link,\n ...scope,\n }\n}\n"],"names":["aiBuildOutcomeLine","aiSitePartialCopy","aiJobKindNoun","aiCustomerSafeCopy","aiJobRefundCopy","AI_JOB_BESIGNER_SEGMENT","screen","reusableComponent","layout","template","emailScreen","AI_JOB_LINK_PARAM","AI_SITE_BUILD_HREF","AI_JOB_PAUSED_NEXT_COPY","aiJobPausedTitle","kind","noun","endsWith","withRefund","job","text","status","refund","refundedCredits","creditsSpent","aiJobNoticeLink","to","base","hostId","encodeURIComponent","$id","own","outputs","filter","output","screens","resource","length","versioned","find","versionId","segment","id","aiJobNotice","site","scope","orgId","link","review","reason","type","level","title","body","message","error","page","Boolean","join","items","unbuilt","some","row","partial","published","sitePublish","drafts","stayed","live","name","at","url","pages"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SAASA,kBAAkB,EAAEC,iBAAiB,QAAQ,yBAAqB;AAC3E,SAASC,aAAa,QAAQ,uBAAmB;AACjD,SAASC,kBAAkB,EAAEC,eAAe,QAAQ,2BAAuB;AAG3E,8DAA8D,GAC9D,OAAO,MAAMC,0BAA4E;IACvFC,QAAQ;IACRC,mBAAmB;IACnBC,QAAQ;IACRC,UAAU;IACV,uEAAuE;IACvE,sCAAsC;IACtCC,aAAa;AACf,EAAC;AAED;;;CAGC,GACD,OAAO,MAAMC,oBAAoB,QAAO;AAExC,+EAA+E,GAC/E,OAAO,MAAMC,qBAAqB,WAAU;AAE5C;;;;CAIC,GACD,OAAO,MAAMC,0BACX,oGACA,uGAAsG;AAExG,+CAA+C,GAC/C,OAAO,SAASC,iBAAiBC,IAAe;IAC9C,MAAMC,OAAOd,cAAca;IAC3B,OAAOC,SAAS,WAAW,0BAA0B,CAAC,KAAK,EAAEA,KAAK,CAAC,EAAEA,KAAKC,QAAQ,CAAC,OAAO,QAAQ,KAAK,OAAO,CAAC;AACjH;AAuBA,4GAA4G,GAC5G,SAASC,WAAWC,GAAsB,EAAEC,IAAY,EAAEC,MAAiC;IACzF,MAAMC,SAASlB,gBAAgB;QAAEiB;QAAQE,iBAAiBJ,IAAII,eAAe;QAAEC,cAAcL,IAAIK,YAAY;IAAC;IAC9G,OAAOF,SAAS,GAAGF,KAAK,CAAC,EAAEE,QAAQ,GAAGF;AACxC;AAOA;;;;;;;;;;CAUC,GACD,OAAO,SAASK,gBAAgBN,GAAsB,EAAEO,EAAyB;IAC/E,MAAMC,OAAOR,IAAIS,MAAM,GAAG,CAAC,CAAC,EAAET,IAAIS,MAAM,EAAE,GAAG;IAC7C,4EAA4E;IAC5E,2DAA2D;IAC3D,IAAIT,IAAIJ,IAAI,KAAK,UAAUI,IAAIS,MAAM,EAAE,OAAO,GAAGD,OAAOf,mBAAmB,CAAC,EAAEiB,mBAAmBV,IAAIW,GAAG,GAAG;IAC3G,IAAIJ,OAAO,UAAUP,IAAIS,MAAM,EAAE;YAClBT;QAAb,MAAMY,MAAM,EAACZ,eAAAA,IAAIa,OAAO,YAAXb,eAAe,EAAE,EAAEc,MAAM,CAAC,CAACC,SAAWA,OAAON,MAAM,KAAKT,IAAIS,MAAM;QAC/E,MAAMO,UAAUJ,IAAIE,MAAM,CAAC,CAACC,SAAWA,OAAOE,QAAQ,KAAK;QAC3D,IAAID,QAAQE,MAAM,GAAG,GAAG,OAAO,GAAGV,KAAK,QAAQ,CAAC;QAChD,MAAMW,YAAYP,IAAIQ,IAAI,CACxB,CAACL,SAAW7B,uBAAuB,CAAC6B,OAAOE,QAAQ,CAAC,IAAIF,OAAOM,SAAS;QAE1E,IAAIT,IAAIM,MAAM,KAAK,KAAKC,WAAW;YACjC,MAAMG,UAAUpC,uBAAuB,CAACiC,UAAUF,QAAQ,CAAC;YAC3D,OAAO,GAAGT,KAAK,CAAC,EAAEc,QAAQ,CAAC,EAAEH,UAAUI,EAAE,CAAC,UAAU,EAAEJ,UAAUE,SAAS,CAAC,SAAS,CAAC;QACtF;IACF;IACA,OAAO,GAAGb,KAAK,CAAC,EAAEhB,kBAAkB,CAAC,EAAEkB,mBAAmBV,IAAIW,GAAG,GAAG;AACtE;AAcA,OAAO,SAASa,YACdxB,GAAsB,EACtBO,EAAyB,EACzBkB,OAA+B,IAAI;IAEnC,MAAM5B,OAAOd,cAAciB,IAAIJ,IAAI;IACnC,MAAM8B,QAAQ;QAAEC,OAAO3B,IAAI2B,KAAK;OAAM3B,IAAIS,MAAM,GAAG;QAAEA,QAAQT,IAAIS,MAAM;IAAC,IAAI,CAAC;IAC7E,MAAMmB,OAAOtB,gBAAgBN,KAAKO;IAClC,IAAIA,OAAO,gBAAgB;YACrBP,aAoBmBA;QApBvB,IAAIA,EAAAA,cAAAA,IAAI6B,MAAM,qBAAV7B,YAAY8B,MAAM,MAAK,QAAQ;YACjC,OAAO;gBACLC,MAAM;gBACNC,OAAO;gBACPC,OACEpC,SAAS,WACL,4CACA,CAAC,KAAK,EAAEA,KAAK,mCAAmC,CAAC;gBACvDqC,MAAM;gBACNN;eACGF;QAEP;QACA,OAAO;YACLK,MAAM;YACNC,OAAO;YACPC,OAAO,CAAC,KAAK,EAAEpC,SAAS,WAAW,WAAW,GAAGA,KAAK,IAAI,CAAC,CAAC,UAAU,CAAC;YACvE,8DAA8D;YAC9DqC,MAAMnC,WACJC,KACAhB,mBAAmBgB,EAAAA,eAAAA,IAAI6B,MAAM,qBAAV7B,aAAYmC,OAAO,KAAInC,IAAIoC,KAAK,IAAI,sDAAsD;gBAAEC,MAAMrC,IAAIJ,IAAI,KAAK;YAAO,IACzI;YAEFgC;WACGF;IAEP;IACA,IAAInB,OAAO,UAAU;QACnB,yEAAyE;QACzE,0EAA0E;QAC1E,OAAO;YACLwB,MAAM;YACNC,OAAO;YACPC,OAAOtC,iBAAiBK,IAAIJ,IAAI;YAChCsC,MAAM;gBAAClC,IAAIoC,KAAK;gBAAE1C;aAAwB,CAACoB,MAAM,CAACwB,SAASC,IAAI,CAAC;YAChEX;WACGF;IAEP;IACA,IAAInB,OAAO,QAAQ;YAGHP,kBAoCCA;YAxB0BA;QAdzC,wEAAwE;QACxE,0EAA0E;QAC1E,MAAMwC,SAAQxC,aAAAA,IAAIwC,KAAK,YAATxC,aAAa,EAAE;QAC7B,MAAMyC,UAAUD,MAAME,IAAI,CAAC,CAACC,MAAQA,IAAIzC,MAAM,KAAK,YAAYyC,IAAIzC,MAAM,KAAK;QAC9E,MAAM8B,QAAQS,UAAU,YAAY;QACpC,MAAMG,UACJ5C,IAAIJ,IAAI,KAAK,SACTd,kBAAkB;YAAE0D;YAAOtC,QAAQ;QAAO,KAC1CuC,UACE5D,mBAAmB;YAAE2D;YAAOtC,QAAQ;QAAO,KAC3C;QACR,uEAAuE;QACvE,uEAAuE;QACvE,mCAAmC;QACnC,MAAM2C,YAAY7C,IAAIJ,IAAI,KAAK,kBAAUI,mBAAAA,IAAI8C,WAAW,qBAAf9C,iBAAiB6C,SAAS,CAAC3B,MAAM,mBAAI,IAAK;QACnF,IAAI2B,YAAY,GAAG;;gBACF7C;YAAf,MAAM+C,mBAAS/C,oBAAAA,IAAI8C,WAAW,qBAAf9C,kBAAiB+C,MAAM,CAAC7B,MAAM,oBAAI;YACjD,MAAM8B,SAASD,WAAW,IAAI,4BAA4B,GAAGA,OAAO,mBAAmB,CAAC;YACxF,qEAAqE;YACrE,sEAAsE;YACtE,4CAA4C;YAC5C,MAAME,OAAOxB,OAAO,GAAGA,KAAKyB,IAAI,CAAC,QAAQ,CAAC,GAAG;YAC7C,MAAMC,KAAK1B,CAAAA,wBAAAA,KAAM2B,GAAG,IAAG,CAAC,IAAI,EAAE3B,KAAK2B,GAAG,EAAE,GAAG;YAC3C,OAAO;gBACLrB,MAAM;gBACNC;gBACAC,OAAOW,UAAU,GAAGK,KAAK,6BAA6B,CAAC,GAAGA;gBAC1Df,MAAMU,UACF,GAAGA,QAAQ,CAAC,EAAEG,SAAS,IAAI,CAAC,2BAA2B,EAAEI,GAAG,SAAS,EAAEH,OAAO,CAAC,CAAC,GAAG,CAAC,2BAA2B,EAAEG,GAAG,CAAC,CAAC,EAAE,GACxHJ,SAAS,IACP,CAAC,wBAAwB,EAAEI,GAAG,SAAS,EAAEH,OAAO,iCAAiC,CAAC,GAClFG,KACE,CAAC,wBAAwB,EAAEA,GAAG,sEAAsE,CAAC,GACrG;gBACRvB;eACGF;QAEP;QACA,MAAM2B,QAAQ,EAACrD,eAAAA,IAAIa,OAAO,YAAXb,eAAe,EAAE,EAAEc,MAAM,CAAC,CAACC,SAAWA,OAAOE,QAAQ,KAAK,UAAUC,MAAM;QACzF,MAAMe,QACJjC,IAAIJ,IAAI,KAAK,UAAUyD,QAAQ,IAC3B,sCACArD,IAAIJ,IAAI,KAAK,SACX,6BACAC,SAAS,WACP,yBACA,CAAC,KAAK,EAAEA,KAAK,SAAS,CAAC;QACjC,OAAO;YACLkC,MAAM;YACNC;YACAC;YACAC,MACElC,IAAIJ,IAAI,KAAK,aAAaI,IAAIJ,IAAI,KAAK,SACnC,6BACA;gBAACgD;gBAAS;aAAoE,CAAC9B,MAAM,CAACwB,SAASC,IAAI,CAAC;YAC1GX;WACGF;IAEP;IACA,OAAO;QACLK,MAAM;QACNC,OAAO;QACPC,OAAO,CAAC,KAAK,EAAEpC,SAAS,WAAW,WAAW,GAAGA,KAAK,IAAI,CAAC,CAAC,QAAQ,CAAC;QACrEqC,MAAMnC,WAAWC,KAAKhB,mBAAmBgB,IAAIoC,KAAK,IAAI,kCAAkC;YAAEC,MAAMrC,IAAIJ,IAAI,KAAK;QAAO,IAAI;QACxHgC;OACGF;AAEP"}
|
|
@@ -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,81 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* A plan's sections of what customers say (AGL-3887).
|
|
19
|
+
*
|
|
20
|
+
* A job never writes a review, a testimonial or a quote for people who did
|
|
21
|
+
* not say it (rule 14, `AI_SITE_CONTEXT_RULE`). So a section planned to show
|
|
22
|
+
* them, on a site that has none, has nothing true to hold, and the build
|
|
23
|
+
* fills it with the only honest thing left: a promise of words to come. The
|
|
24
|
+
* live Whitmore Lane Family Law start (beta.262, 2026-10-16) planned "Client
|
|
25
|
+
* feedback placeholder" on its home page and shipped "We are building this
|
|
26
|
+
* space for the words of families we have worked with" over a stock photo.
|
|
27
|
+
*
|
|
28
|
+
* A site start's plan is settled in code before it is kept, never re-asked
|
|
29
|
+
* (a re-ask costs the person credits for a plan that has one answer): such a
|
|
30
|
+
* section becomes a band about what working together is like, as a
|
|
31
|
+
* photographer's start of the same day planned its "Kind words" as "What
|
|
32
|
+
* working together is like" (Relaxed / Prepared / Personal). Where the page
|
|
33
|
+
* already has that band, the section goes, if the page keeps its fewest
|
|
34
|
+
* sections without it.
|
|
35
|
+
*
|
|
36
|
+
* Kept as planned where the customer words are real: a store, whose
|
|
37
|
+
* platform shows its shoppers' reviews; a site with a dataset of them; a
|
|
38
|
+
* brief that asks for them or quotes them.
|
|
39
|
+
*
|
|
40
|
+
* Every plan, whatever its kind, names no section "placeholder": a section's
|
|
41
|
+
* name is what its build is asked to write, and a name that says it is a
|
|
42
|
+
* stand-in is written as one.
|
|
43
|
+
*
|
|
44
|
+
* Pure: plans in, plans out.
|
|
45
|
+
*/
|
|
46
|
+
import type { AiBuildPlan } from './ai-build-plan';
|
|
47
|
+
import type { AiSiteInventory } from './ai-site-inventory';
|
|
48
|
+
/** What a section of customer words on a site that has none is planned as instead. */
|
|
49
|
+
export declare const AI_PLAN_SAID_SECTION_NAME = "What working with us is like";
|
|
50
|
+
/** The points that band makes (the photographer's three: Relaxed, Prepared, Personal). */
|
|
51
|
+
export declare const AI_PLAN_SAID_SECTION_ITEMS = 3;
|
|
52
|
+
/** Whether a planned section's name says it shows what customers say. */
|
|
53
|
+
export declare function aiPlanSectionSaysCustomerWords(name: string): boolean;
|
|
54
|
+
/** A section's name with the word "placeholder" taken out; the name unchanged where nothing is left. */
|
|
55
|
+
export declare function aiPlanSectionNameWithoutPlaceholder(name: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the site, or the person asking, has customer words that are real
|
|
58
|
+
* (AGL-3887): a store's shoppers' reviews, which its platform shows; a
|
|
59
|
+
* dataset of them the site keeps; a brief that names them or quotes one with
|
|
60
|
+
* its speaker.
|
|
61
|
+
*/
|
|
62
|
+
export declare function aiPlanHasCustomerWords(input: {
|
|
63
|
+
brief?: string | null;
|
|
64
|
+
inventory?: Pick<AiSiteInventory, 'datasets'> | null;
|
|
65
|
+
siteKind?: string | null;
|
|
66
|
+
}): boolean;
|
|
67
|
+
/** Every section's name with the word "placeholder" taken out (AGL-3887), the plan unchanged where none had it. */
|
|
68
|
+
export declare function aiSettlePlanPlaceholderNames<P extends Pick<AiBuildPlan, 'screens'>>(plan: P): P;
|
|
69
|
+
/**
|
|
70
|
+
* A site start's plan with no section of customer words it cannot fill
|
|
71
|
+
* (AGL-3887). Where `hasCustomerWords` is false, each such section past a
|
|
72
|
+
* page's first is planned as {@link AI_PLAN_SAID_SECTION_NAME} with its
|
|
73
|
+
* points; where the page already has that band, it is taken out instead
|
|
74
|
+
* while the page keeps its fewest sections (`homeMin` on the home page, two
|
|
75
|
+
* on any other), and planned as the band all the same where it cannot be.
|
|
76
|
+
* The plan is returned unchanged where nothing moved.
|
|
77
|
+
*/
|
|
78
|
+
export declare function aiSettlePlanSaidSections<P extends Pick<AiBuildPlan, 'screens'>>(plan: P, options: {
|
|
79
|
+
hasCustomerWords: boolean;
|
|
80
|
+
homeMin: number;
|
|
81
|
+
}): P;
|