@aglyn/plugins-ai 1.0.0-beta.262 → 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.
Files changed (41) hide show
  1. package/package.json +17 -17
  2. package/src/lib/insights/ai-figure-summaries.d.ts +1 -1
  3. package/src/lib/insights/ai-figure-summaries.js +55 -38
  4. package/src/lib/insights/ai-figure-summaries.js.map +1 -1
  5. package/src/lib/insights/ai-insight-digest.d.ts +2 -0
  6. package/src/lib/insights/ai-insight-digest.js +7 -4
  7. package/src/lib/insights/ai-insight-digest.js.map +1 -1
  8. package/src/lib/jobs/ai-job-insight-step.d.ts +1 -1
  9. package/src/lib/jobs/ai-job-insight-step.js +5 -3
  10. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  11. package/src/lib/jobs/ai-job-page-language.d.ts +2 -0
  12. package/src/lib/jobs/ai-job-page-language.js +23 -2
  13. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  14. package/src/lib/jobs/ai-job-plan-step.d.ts +14 -1
  15. package/src/lib/jobs/ai-job-plan-step.js +44 -9
  16. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  17. package/src/lib/jobs/ai-jobs-notify.d.ts +8 -1
  18. package/src/lib/jobs/ai-jobs-notify.js +44 -2
  19. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  20. package/src/lib/layout-language/ai-layout-compiler.js +18 -7
  21. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  22. package/src/lib/layout-language/ai-layout-promised-copy.d.ts +54 -0
  23. package/src/lib/layout-language/ai-layout-promised-copy.js +104 -0
  24. package/src/lib/layout-language/ai-layout-promised-copy.js.map +1 -0
  25. package/src/lib/model/ai-insight.d.ts +9 -0
  26. package/src/lib/model/ai-insight.js +12 -0
  27. package/src/lib/model/ai-insight.js.map +1 -1
  28. package/src/lib/model/ai-job-notice.d.ts +7 -1
  29. package/src/lib/model/ai-job-notice.js +10 -9
  30. package/src/lib/model/ai-job-notice.js.map +1 -1
  31. package/src/lib/model/ai-plan-said-sections.d.ts +81 -0
  32. package/src/lib/model/ai-plan-said-sections.js +168 -0
  33. package/src/lib/model/ai-plan-said-sections.js.map +1 -0
  34. package/src/lib/model/ai-site-job.d.ts +8 -2
  35. package/src/lib/model/ai-site-job.js +7 -1
  36. package/src/lib/model/ai-site-job.js.map +1 -1
  37. package/src/lib/runtime/ai-doctrine-validators.d.ts +15 -1
  38. package/src/lib/runtime/ai-doctrine-validators.js +80 -1
  39. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  40. package/src/lib/runtime/ai-palette.generated.js +11 -3
  41. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications';
18
18
  import { raiseOperatorAlert } from '@aglyn/tenant-data-admin/server/operator-alerts';
19
+ import { type AiJobNoticeSite } from '../model/ai-job-notice';
19
20
  import { type AiJobTransitionListener } from './ai-jobs';
20
21
  /**
21
22
  * The person who started an AI job is told when it needs them, when it
@@ -33,7 +34,13 @@ import { type AiJobTransitionListener } from './ai-jobs';
33
34
  * `notifyUsers` never throws, and honors the person's per-category,
34
35
  * per-workspace and per-site preferences.
35
36
  */
36
- export declare function aiJobTransitionNotifier(notify?: typeof notifyUsers): AiJobTransitionListener;
37
+ export declare function aiJobTransitionNotifier(notify?: typeof notifyUsers, readSite?: (hostId: string) => Promise<AiJobNoticeSite | null>): AiJobTransitionListener;
38
+ /**
39
+ * A job's site as its notice names it: the name its managers gave it, else
40
+ * its address, and where it is live — its custom domain once that serves,
41
+ * else its platform subdomain. `null` for a site that is gone.
42
+ */
43
+ export declare function readJobSite(hostId: string): Promise<AiJobNoticeSite | null>;
37
44
  /**
38
45
  * Staff are told when a job fails on our side (`ai.jobFailed`), and when a
39
46
  * build finishes `done` with parts that failed on our side
@@ -18,6 +18,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { notifyUsers } from "@aglyn/tenant-data-admin/server/notifications";
19
19
  import { raiseOperatorAlert } from "@aglyn/tenant-data-admin/server/operator-alerts";
20
20
  import { AI_BUILD_PARTLY_FAILED, AI_JOB_FAILED } from "../operator-alerts.js";
21
+ import { liveCustomDomain } from "@aglyn/aglyn/app-utils/host-naming";
22
+ import { TENANT_APEX } from "@aglyn/aglyn/app-utils/tenant-apex";
21
23
  import { aiJobNotice } from "../model/ai-job-notice.js";
22
24
  import { aiBusinessProfilePrefiller } from "./ai-business-profile-prefill.js";
23
25
  import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
@@ -37,16 +39,56 @@ import { registerAiJobTransitionListener } from "./ai-jobs.js";
37
39
  *
38
40
  * `notifyUsers` never throws, and honors the person's per-category,
39
41
  * per-workspace and per-site preferences.
40
- */ export function aiJobTransitionNotifier(notify = notifyUsers) {
42
+ */ export function aiJobTransitionNotifier(notify = notifyUsers, readSite = readJobSite) {
41
43
  return async ({ job, to })=>{
42
44
  var _job_review;
43
45
  if (!job.createdBy) return;
44
46
  // A guided site start's plan is confirmed for it the moment it is kept
45
47
  // (AGL-3594): there is nothing for the person to confirm.
46
48
  if (to === 'needs-review' && ((_job_review = job.review) == null ? void 0 : _job_review.reason) === 'plan' && aiJobAutoConfirms(job)) return;
49
+ // The site a job worked on, by name and address (AGL-3889): the email is
50
+ // read away from the console, often by someone with several sites.
51
+ const site = job.hostId ? await readSite(job.hostId).catch(()=>null) : null;
52
+ const notice = aiJobNotice(job, to, site);
53
+ if (!site) {
54
+ await notify([
55
+ job.createdBy
56
+ ], notice);
57
+ return;
58
+ }
47
59
  await notify([
48
60
  job.createdBy
49
- ], aiJobNotice(job, to));
61
+ ], notice, {
62
+ emailDetails: [
63
+ {
64
+ label: 'Site',
65
+ value: site.name
66
+ },
67
+ ...site.url ? [
68
+ {
69
+ label: 'Address',
70
+ value: site.url
71
+ }
72
+ ] : []
73
+ ]
74
+ });
75
+ };
76
+ }
77
+ /**
78
+ * A job's site as its notice names it: the name its managers gave it, else
79
+ * its address, and where it is live — its custom domain once that serves,
80
+ * else its platform subdomain. `null` for a site that is gone.
81
+ */ export async function readJobSite(hostId) {
82
+ var _host_data;
83
+ const host = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).get();
84
+ if (!host.exists) return null;
85
+ const data = (_host_data = host.data()) != null ? _host_data : {};
86
+ const text = (value)=>typeof value === 'string' ? value.trim() : '';
87
+ const subdomain = text(data['subdomain']);
88
+ const domain = liveCustomDomain(data);
89
+ return {
90
+ name: text(data['displayName']) || text(data['name']) || subdomain || hostId,
91
+ url: domain ? `https://${domain}` : subdomain ? `https://${subdomain}.${TENANT_APEX}` : null
50
92
  };
51
93
  }
52
94
  /** The longest runner error an alert quotes; the log has the rest. */ const ALERT_ERROR_MAX = 300;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-notify.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 { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport { raiseOperatorAlert } from '@aglyn/tenant-data-admin/server/operator-alerts'\nimport { AI_BUILD_PARTLY_FAILED, AI_JOB_FAILED } from '../operator-alerts'\nimport { aiJobNotice } from '../model/ai-job-notice'\nimport { aiBusinessProfilePrefiller } from './ai-business-profile-prefill'\nimport { aiJobAutoConfirms } from './ai-job-auto-confirm'\nimport { registerAiJobTransitionListener, type AiJobTransitionListener } from './ai-jobs'\n\n/**\n * The person who started an AI job is told when it needs them, when it\n * finishes and when it stops (AGL-3593), through the console's notifications\n * — the bell, and the generic notification email for a person who switched\n * that channel on for the category. No other channel: there is no push.\n *\n * The machine calls this where the job's state changes, once per change\n * (`registerAiJobTransitionListener`): the request that ran a step inline,\n * or the beat that ran it with nobody watching — which is exactly when a\n * person needs telling. The recipient is the job's creator, whose job it is\n * and who alone confirms its plan; a teammate is not told about someone\n * else's draft.\n *\n * `notifyUsers` never throws, and honors the person's per-category,\n * per-workspace and per-site preferences.\n */\nexport function aiJobTransitionNotifier(\n notify: typeof notifyUsers = notifyUsers,\n): AiJobTransitionListener {\n return async ({ job, to }) => {\n if (!job.createdBy) return\n // A guided site start's plan is confirmed for it the moment it is kept\n // (AGL-3594): there is nothing for the person to confirm.\n if (to === 'needs-review' && job.review?.reason === 'plan' && aiJobAutoConfirms(job)) return\n await notify([job.createdBy], aiJobNotice(job, to))\n }\n}\n\n/** The longest runner error an alert quotes; the log has the rest. */\nconst ALERT_ERROR_MAX = 300\n\n/** At most this many failed parts are named in a partial-build alert. */\nconst ALERT_ITEMS_MAX = 5\n\nconst capped = (text: string): string =>\n text.length > ALERT_ERROR_MAX ? `${text.slice(0, ALERT_ERROR_MAX)}…` : text\n\n/**\n * Staff are told when a job fails on our side (`ai.jobFailed`), and when a\n * build finishes `done` with parts that failed on our side\n * (`ai.buildPartlyFailed`) — each deduped per job kind, so a broken provider\n * or a broken step is heard about the hour it starts. A failure that is the\n * customer's — the model declined the brief or a part of it, the site\n * switched AI off — raises nothing. `raiseOperatorAlert` never throws.\n */\nexport function aiJobFailureAlerter(\n raise: typeof raiseOperatorAlert = raiseOperatorAlert,\n): AiJobTransitionListener {\n return async ({ job, to, failure }) => {\n if (to === 'done') {\n const items = job.items ?? []\n const ours = items.filter((row) => row.status === 'failed' && row.failure?.ours)\n if (!ours.length) return\n const named = ours\n .slice(0, ALERT_ITEMS_MAX)\n .map((row) => `${row.label} (${row.failure?.reason}${row.failure?.detail ? `: ${row.failure.detail}` : ''})`)\n .join('; ')\n await raise(AI_BUILD_PARTLY_FAILED, {\n dedupeKey: job.kind,\n orgId: job.orgId,\n ...(job.hostId ? { hostId: job.hostId } : {}),\n context: {\n kind: job.kind,\n jobId: job.$id,\n orgId: job.orgId,\n failed: ours.length,\n total: items.length,\n items: capped(ours.length > ALERT_ITEMS_MAX ? `${named}; and ${ours.length - ALERT_ITEMS_MAX} more` : named),\n },\n })\n return\n }\n if (to !== 'failed' || !failure?.ours) return\n const error = failure.error ?? job.error ?? 'no error recorded'\n await raise(AI_JOB_FAILED, {\n dedupeKey: job.kind,\n orgId: job.orgId,\n ...(job.hostId ? { hostId: job.hostId } : {}),\n context: {\n kind: job.kind,\n jobId: job.$id,\n orgId: job.orgId,\n step: failure.stepIndex === null ? 'n/a' : job.steps[failure.stepIndex]?.name ?? failure.stepIndex,\n error: capped(error),\n },\n })\n }\n}\n\n/**\n * Registers what the jobs machine tells about each change: the notifier, the\n * staff alert for a failure on our side, and the business profile's prefill\n * from a site job (AGL-3661). One listener, each in turn; the alert and the\n * prefill swallow their own failures, so neither costs the person their notice.\n */\nexport function registerAiJobsNotify(): void {\n const notify = aiJobTransitionNotifier()\n const alert = aiJobFailureAlerter()\n const prefill = aiBusinessProfilePrefiller(() => firebaseAdmin.app().firestore())\n registerAiJobTransitionListener(async (change) => {\n await notify(change)\n await alert(change)\n await prefill(change)\n })\n}\n"],"names":["firebaseAdmin","notifyUsers","raiseOperatorAlert","AI_BUILD_PARTLY_FAILED","AI_JOB_FAILED","aiJobNotice","aiBusinessProfilePrefiller","aiJobAutoConfirms","registerAiJobTransitionListener","aiJobTransitionNotifier","notify","job","to","createdBy","review","reason","ALERT_ERROR_MAX","ALERT_ITEMS_MAX","capped","text","length","slice","aiJobFailureAlerter","raise","failure","items","ours","filter","row","status","named","map","label","detail","join","dedupeKey","kind","orgId","hostId","context","jobId","$id","failed","total","error","step","stepIndex","steps","name","registerAiJobsNotify","alert","prefill","app","firestore","change"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-notify.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 { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport { raiseOperatorAlert } from '@aglyn/tenant-data-admin/server/operator-alerts'\nimport { AI_BUILD_PARTLY_FAILED, AI_JOB_FAILED } from '../operator-alerts'\nimport { liveCustomDomain } from '@aglyn/aglyn/app-utils/host-naming'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport { aiJobNotice, type AiJobNoticeSite } from '../model/ai-job-notice'\nimport { aiBusinessProfilePrefiller } from './ai-business-profile-prefill'\nimport { aiJobAutoConfirms } from './ai-job-auto-confirm'\nimport { registerAiJobTransitionListener, type AiJobTransitionListener } from './ai-jobs'\n\n/**\n * The person who started an AI job is told when it needs them, when it\n * finishes and when it stops (AGL-3593), through the console's notifications\n * — the bell, and the generic notification email for a person who switched\n * that channel on for the category. No other channel: there is no push.\n *\n * The machine calls this where the job's state changes, once per change\n * (`registerAiJobTransitionListener`): the request that ran a step inline,\n * or the beat that ran it with nobody watching — which is exactly when a\n * person needs telling. The recipient is the job's creator, whose job it is\n * and who alone confirms its plan; a teammate is not told about someone\n * else's draft.\n *\n * `notifyUsers` never throws, and honors the person's per-category,\n * per-workspace and per-site preferences.\n */\nexport function aiJobTransitionNotifier(\n notify: typeof notifyUsers = notifyUsers,\n readSite: (hostId: string) => Promise<AiJobNoticeSite | null> = readJobSite,\n): AiJobTransitionListener {\n return async ({ job, to }) => {\n if (!job.createdBy) return\n // A guided site start's plan is confirmed for it the moment it is kept\n // (AGL-3594): there is nothing for the person to confirm.\n if (to === 'needs-review' && job.review?.reason === 'plan' && aiJobAutoConfirms(job)) return\n // The site a job worked on, by name and address (AGL-3889): the email is\n // read away from the console, often by someone with several sites.\n const site = job.hostId ? await readSite(job.hostId).catch(() => null) : null\n const notice = aiJobNotice(job, to, site)\n if (!site) {\n await notify([job.createdBy], notice)\n return\n }\n await notify([job.createdBy], notice, {\n emailDetails: [\n { label: 'Site', value: site.name },\n ...(site.url ? [{ label: 'Address', value: site.url }] : []),\n ],\n })\n }\n}\n\n/**\n * A job's site as its notice names it: the name its managers gave it, else\n * its address, and where it is live — its custom domain once that serves,\n * else its platform subdomain. `null` for a site that is gone.\n */\nexport async function readJobSite(hostId: string): Promise<AiJobNoticeSite | null> {\n const host = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).get()\n if (!host.exists) return null\n const data = (host.data() ?? {}) as Record<string, unknown>\n const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')\n const subdomain = text(data['subdomain'])\n const domain = liveCustomDomain(data as never)\n return {\n name: text(data['displayName']) || text(data['name']) || subdomain || hostId,\n url: domain ? `https://${domain}` : subdomain ? `https://${subdomain}.${TENANT_APEX}` : null,\n }\n}\n\n/** The longest runner error an alert quotes; the log has the rest. */\nconst ALERT_ERROR_MAX = 300\n\n/** At most this many failed parts are named in a partial-build alert. */\nconst ALERT_ITEMS_MAX = 5\n\nconst capped = (text: string): string =>\n text.length > ALERT_ERROR_MAX ? `${text.slice(0, ALERT_ERROR_MAX)}…` : text\n\n/**\n * Staff are told when a job fails on our side (`ai.jobFailed`), and when a\n * build finishes `done` with parts that failed on our side\n * (`ai.buildPartlyFailed`) — each deduped per job kind, so a broken provider\n * or a broken step is heard about the hour it starts. A failure that is the\n * customer's — the model declined the brief or a part of it, the site\n * switched AI off — raises nothing. `raiseOperatorAlert` never throws.\n */\nexport function aiJobFailureAlerter(\n raise: typeof raiseOperatorAlert = raiseOperatorAlert,\n): AiJobTransitionListener {\n return async ({ job, to, failure }) => {\n if (to === 'done') {\n const items = job.items ?? []\n const ours = items.filter((row) => row.status === 'failed' && row.failure?.ours)\n if (!ours.length) return\n const named = ours\n .slice(0, ALERT_ITEMS_MAX)\n .map((row) => `${row.label} (${row.failure?.reason}${row.failure?.detail ? `: ${row.failure.detail}` : ''})`)\n .join('; ')\n await raise(AI_BUILD_PARTLY_FAILED, {\n dedupeKey: job.kind,\n orgId: job.orgId,\n ...(job.hostId ? { hostId: job.hostId } : {}),\n context: {\n kind: job.kind,\n jobId: job.$id,\n orgId: job.orgId,\n failed: ours.length,\n total: items.length,\n items: capped(ours.length > ALERT_ITEMS_MAX ? `${named}; and ${ours.length - ALERT_ITEMS_MAX} more` : named),\n },\n })\n return\n }\n if (to !== 'failed' || !failure?.ours) return\n const error = failure.error ?? job.error ?? 'no error recorded'\n await raise(AI_JOB_FAILED, {\n dedupeKey: job.kind,\n orgId: job.orgId,\n ...(job.hostId ? { hostId: job.hostId } : {}),\n context: {\n kind: job.kind,\n jobId: job.$id,\n orgId: job.orgId,\n step: failure.stepIndex === null ? 'n/a' : job.steps[failure.stepIndex]?.name ?? failure.stepIndex,\n error: capped(error),\n },\n })\n }\n}\n\n/**\n * Registers what the jobs machine tells about each change: the notifier, the\n * staff alert for a failure on our side, and the business profile's prefill\n * from a site job (AGL-3661). One listener, each in turn; the alert and the\n * prefill swallow their own failures, so neither costs the person their notice.\n */\nexport function registerAiJobsNotify(): void {\n const notify = aiJobTransitionNotifier()\n const alert = aiJobFailureAlerter()\n const prefill = aiBusinessProfilePrefiller(() => firebaseAdmin.app().firestore())\n registerAiJobTransitionListener(async (change) => {\n await notify(change)\n await alert(change)\n await prefill(change)\n })\n}\n"],"names":["firebaseAdmin","notifyUsers","raiseOperatorAlert","AI_BUILD_PARTLY_FAILED","AI_JOB_FAILED","liveCustomDomain","TENANT_APEX","aiJobNotice","aiBusinessProfilePrefiller","aiJobAutoConfirms","registerAiJobTransitionListener","aiJobTransitionNotifier","notify","readSite","readJobSite","job","to","createdBy","review","reason","site","hostId","catch","notice","emailDetails","label","value","name","url","host","app","firestore","collection","doc","get","exists","data","text","trim","subdomain","domain","ALERT_ERROR_MAX","ALERT_ITEMS_MAX","capped","length","slice","aiJobFailureAlerter","raise","failure","items","ours","filter","row","status","named","map","detail","join","dedupeKey","kind","orgId","context","jobId","$id","failed","total","error","step","stepIndex","steps","registerAiJobsNotify","alert","prefill","change"],"mappings":"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"}
@@ -689,6 +689,15 @@ function compileBlock(scope, block, room) {
689
689
  color: 'text.secondary'
690
690
  };
691
691
  }
692
+ /**
693
+ * A List Item Text's secondary words: MUI draws them in the muted text color
694
+ * whatever they sit on, so on a brand band, or over a photo, they take the
695
+ * band's own color as the primary words do (AGL-3886).
696
+ */ function mutedSecondary(scope) {
697
+ return scope.band === 'brand' || scope.overPhoto ? {
698
+ secondaryColor: 'inherit'
699
+ } : {};
700
+ }
692
701
  /** An accent's color: the brand's, or the band's own on a brand band. */ function accent(scope) {
693
702
  return scope.band === 'brand' || scope.overPhoto ? null : {
694
703
  color: 'primary.main'
@@ -1109,9 +1118,9 @@ function instances(scope, placed, items, perRow) {
1109
1118
  const text = block.kind === 'quotes' ? '[Customer name]' : aiLayoutWords(item.text, block.kind === 'stats' ? 'statLabel' : 'itemText', facts, scope.page.options.language);
1110
1119
  return tree.add('muiListItemText', _extends({
1111
1120
  primary: title
1112
- }, text ? {
1121
+ }, text ? _extends({
1113
1122
  secondary: text
1114
- } : {}), null, null, 'pair');
1123
+ }, mutedSecondary(scope)) : {}), null, null, 'pair');
1115
1124
  });
1116
1125
  return layOut(scope, noted(scope, pairs), perRow, '3', 'compact');
1117
1126
  }
@@ -1120,11 +1129,13 @@ function instances(scope, placed, items, perRow) {
1120
1129
  const tree = scope.page.tree;
1121
1130
  const facts = scope.page.targets.facts;
1122
1131
  const icon = (_aiIconOfWord = aiIconOfWord(item.icon)) != null ? _aiIconOfWord : aiIconOfWord(block.icon);
1132
+ // A quiet card draws no surface of its own, so it is colored as the band it sits on.
1133
+ const quiet = block.style === 'quiet';
1123
1134
  const children = [
1124
1135
  icon ? tree.add(AI_ICON_COMPONENT_ID, {
1125
1136
  iconId: icon.id,
1126
1137
  size: '32'
1127
- }, {
1138
+ }, quiet ? accent(scope) : {
1128
1139
  color: 'primary.main'
1129
1140
  }, null, 'icon') : null,
1130
1141
  item.title.trim() ? tree.add('muiTypography', {
@@ -1135,14 +1146,14 @@ function instances(scope, placed, items, perRow) {
1135
1146
  item.text.trim() ? tree.add('muiTypography', {
1136
1147
  children: aiLayoutWords(item.text, 'itemText', facts, scope.page.options.language),
1137
1148
  variant: 'body1'
1138
- }, {
1149
+ }, quiet ? muted(scope) : {
1139
1150
  color: 'text.secondary'
1140
1151
  }, null, 'text') : null
1141
1152
  ];
1142
1153
  const stack = tree.add('muiStack', {
1143
1154
  spacing: '1.5'
1144
1155
  }, null, children, 'cardStack');
1145
- if (block.style === 'quiet') return stack;
1156
+ if (quiet) return stack;
1146
1157
  const content = tree.add('muiCardContent', null, {
1147
1158
  p: 3
1148
1159
  }, [
@@ -1307,9 +1318,9 @@ function instances(scope, placed, items, perRow) {
1307
1318
  }, null, [
1308
1319
  tree.add('muiListItemText', _extends({
1309
1320
  primary: aiLayoutWords(item.title, 'question', facts, scope.page.options.language)
1310
- }, item.text.trim() ? {
1321
+ }, item.text.trim() ? _extends({
1311
1322
  secondary: aiLayoutWords(item.text, 'answer', facts, scope.page.options.language)
1312
- } : {}), null, null, 'qa')
1323
+ }, mutedSecondary(scope)) : {}), null, null, 'qa')
1313
1324
  ], 'row'));
1314
1325
  return tree.add('muiList', {
1315
1326
  disablePadding: true