@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.
- package/package.json +17 -17
- package/src/lib/insights/ai-figure-summaries.d.ts +1 -1
- package/src/lib/insights/ai-figure-summaries.js +55 -38
- package/src/lib/insights/ai-figure-summaries.js.map +1 -1
- 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/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/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-insight.d.ts +9 -0
- package/src/lib/model/ai-insight.js +12 -0
- package/src/lib/model/ai-insight.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-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/runtime/ai-doctrine-validators.d.ts +15 -1
- package/src/lib/runtime/ai-doctrine-validators.js +80 -1
- package/src/lib/runtime/ai-doctrine-validators.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
|
@@ -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
|
-
],
|
|
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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,WAAW,QAAQ,gDAA+C;AAC3E,SAASC,kBAAkB,QAAQ,kDAAiD;AACpF,SAASC,sBAAsB,EAAEC,aAAa,QAAQ,wBAAoB;AAC1E,SAASC,gBAAgB,QAAQ,qCAAoC;AACrE,SAASC,WAAW,QAAQ,qCAAoC;AAChE,SAASC,WAAW,QAA8B,4BAAwB;AAC1E,SAASC,0BAA0B,QAAQ,mCAA+B;AAC1E,SAASC,iBAAiB,QAAQ,2BAAuB;AACzD,SAASC,+BAA+B,QAAsC,eAAW;AAEzF;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,wBACdC,SAA6BX,WAAW,EACxCY,WAAgEC,WAAW;IAE3E,OAAO,OAAO,EAAEC,GAAG,EAAEC,EAAE,EAAE;YAIMD;QAH7B,IAAI,CAACA,IAAIE,SAAS,EAAE;QACpB,uEAAuE;QACvE,0DAA0D;QAC1D,IAAID,OAAO,kBAAkBD,EAAAA,cAAAA,IAAIG,MAAM,qBAAVH,YAAYI,MAAM,MAAK,UAAUV,kBAAkBM,MAAM;QACtF,yEAAyE;QACzE,mEAAmE;QACnE,MAAMK,OAAOL,IAAIM,MAAM,GAAG,MAAMR,SAASE,IAAIM,MAAM,EAAEC,KAAK,CAAC,IAAM,QAAQ;QACzE,MAAMC,SAAShB,YAAYQ,KAAKC,IAAII;QACpC,IAAI,CAACA,MAAM;YACT,MAAMR,OAAO;gBAACG,IAAIE,SAAS;aAAC,EAAEM;YAC9B;QACF;QACA,MAAMX,OAAO;YAACG,IAAIE,SAAS;SAAC,EAAEM,QAAQ;YACpCC,cAAc;gBACZ;oBAAEC,OAAO;oBAAQC,OAAON,KAAKO,IAAI;gBAAC;mBAC9BP,KAAKQ,GAAG,GAAG;oBAAC;wBAAEH,OAAO;wBAAWC,OAAON,KAAKQ,GAAG;oBAAC;iBAAE,GAAG,EAAE;aAC5D;QACH;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAed,YAAYO,MAAc;QAGhCQ;IAFd,MAAMA,OAAO,MAAM7B,cAAc8B,GAAG,GAAGC,SAAS,GAAGC,UAAU,CAAC,SAASC,GAAG,CAACZ,QAAQa,GAAG;IACtF,IAAI,CAACL,KAAKM,MAAM,EAAE,OAAO;IACzB,MAAMC,QAAQP,aAAAA,KAAKO,IAAI,cAATP,aAAe,CAAC;IAC9B,MAAMQ,OAAO,CAACX,QAAoB,OAAOA,UAAU,WAAWA,MAAMY,IAAI,KAAK;IAC7E,MAAMC,YAAYF,KAAKD,IAAI,CAAC,YAAY;IACxC,MAAMI,SAASnC,iBAAiB+B;IAChC,OAAO;QACLT,MAAMU,KAAKD,IAAI,CAAC,cAAc,KAAKC,KAAKD,IAAI,CAAC,OAAO,KAAKG,aAAalB;QACtEO,KAAKY,SAAS,CAAC,QAAQ,EAAEA,QAAQ,GAAGD,YAAY,CAAC,QAAQ,EAAEA,UAAU,CAAC,EAAEjC,aAAa,GAAG;IAC1F;AACF;AAEA,oEAAoE,GACpE,MAAMmC,kBAAkB;AAExB,uEAAuE,GACvE,MAAMC,kBAAkB;AAExB,MAAMC,SAAS,CAACN,OACdA,KAAKO,MAAM,GAAGH,kBAAkB,GAAGJ,KAAKQ,KAAK,CAAC,GAAGJ,iBAAiB,CAAC,CAAC,GAAGJ;AAEzE;;;;;;;CAOC,GACD,OAAO,SAASS,oBACdC,QAAmC7C,kBAAkB;IAErD,OAAO,OAAO,EAAEa,GAAG,EAAEC,EAAE,EAAEgC,OAAO,EAAE;YAyBlBA,MAAAA;YASiCjC;QAjC/C,IAAIC,OAAO,QAAQ;gBACHD;YAAd,MAAMkC,SAAQlC,aAAAA,IAAIkC,KAAK,YAATlC,aAAa,EAAE;YAC7B,MAAMmC,OAAOD,MAAME,MAAM,CAAC,CAACC;oBAAmCA;uBAA3BA,IAAIC,MAAM,KAAK,cAAYD,eAAAA,IAAIJ,OAAO,qBAAXI,aAAaF,IAAI;;YAC/E,IAAI,CAACA,KAAKN,MAAM,EAAE;YAClB,MAAMU,QAAQJ,KACXL,KAAK,CAAC,GAAGH,iBACTa,GAAG,CAAC,CAACH;oBAAyBA,cAAsBA;uBAAvC,GAAGA,IAAI3B,KAAK,CAAC,EAAE,GAAE2B,eAAAA,IAAIJ,OAAO,qBAAXI,aAAajC,MAAM,GAAGiC,EAAAA,gBAAAA,IAAIJ,OAAO,qBAAXI,cAAaI,MAAM,IAAG,CAAC,EAAE,EAAEJ,IAAIJ,OAAO,CAACQ,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;eAC3GC,IAAI,CAAC;YACR,MAAMV,MAAM5C,wBAAwB;gBAClCuD,WAAW3C,IAAI4C,IAAI;gBACnBC,OAAO7C,IAAI6C,KAAK;eACZ7C,IAAIM,MAAM,GAAG;gBAAEA,QAAQN,IAAIM,MAAM;YAAC,IAAI,CAAC;gBAC3CwC,SAAS;oBACPF,MAAM5C,IAAI4C,IAAI;oBACdG,OAAO/C,IAAIgD,GAAG;oBACdH,OAAO7C,IAAI6C,KAAK;oBAChBI,QAAQd,KAAKN,MAAM;oBACnBqB,OAAOhB,MAAML,MAAM;oBACnBK,OAAON,OAAOO,KAAKN,MAAM,GAAGF,kBAAkB,GAAGY,MAAM,MAAM,EAAEJ,KAAKN,MAAM,GAAGF,gBAAgB,KAAK,CAAC,GAAGY;gBACxG;;YAEF;QACF;QACA,IAAItC,OAAO,YAAY,EAACgC,2BAAAA,QAASE,IAAI,GAAE;QACvC,MAAMgB,SAAQlB,QAAAA,iBAAAA,QAAQkB,KAAK,YAAblB,iBAAiBjC,IAAImD,KAAK,YAA1BlB,OAA8B;QAC5C,MAAMD,MAAM3C,eAAe;YACzBsD,WAAW3C,IAAI4C,IAAI;YACnBC,OAAO7C,IAAI6C,KAAK;WACZ7C,IAAIM,MAAM,GAAG;YAAEA,QAAQN,IAAIM,MAAM;QAAC,IAAI,CAAC;YAC3CwC,SAAS;gBACPF,MAAM5C,IAAI4C,IAAI;gBACdG,OAAO/C,IAAIgD,GAAG;gBACdH,OAAO7C,IAAI6C,KAAK;gBAChBO,MAAMnB,QAAQoB,SAAS,KAAK,OAAO,kBAAQrD,+BAAAA,IAAIsD,KAAK,CAACrB,QAAQoB,SAAS,CAAC,qBAA5BrD,6BAA8BY,IAAI,oBAAIqB,QAAQoB,SAAS;gBAClGF,OAAOvB,OAAOuB;YAChB;;IAEJ;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASI;IACd,MAAM1D,SAASD;IACf,MAAM4D,QAAQzB;IACd,MAAM0B,UAAUhE,2BAA2B,IAAMR,cAAc8B,GAAG,GAAGC,SAAS;IAC9ErB,gCAAgC,OAAO+D;QACrC,MAAM7D,OAAO6D;QACb,MAAMF,MAAME;QACZ,MAAMD,QAAQC;IAChB;AACF"}
|
|
@@ -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 (
|
|
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
|