@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/components/use-ai-job-run.ts"],"sourcesContent":["'use client'\n\n/**\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 { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport {\n authorizedFetch,\n type MaybeTokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/use-ai-job-run.ts"],"sourcesContent":["'use client'\n\n/**\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 { lockdownRefusalText, parseLockdownRefusal } from '@aglyn/aglyn'\nimport {\n authorizedFetch,\n type MaybeTokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { AiJobKind, AiJobSummary } from '../model/ai-jobs.types'\nimport { aiJobProblem, aiJobSettled } from '../model/ai-job-activity'\nimport { followAiJobEvents } from './ai-job-events'\nimport { publishAiJob } from './ai-jobs-store'\n\n/**\n * Starting one AI job from a console control, and following it until it\n * settles (AGL-2919): the jobs route's create door, then its events door, the\n * way the SEO card follows its jobs. A control that starts a job the beat\n * runs shows its progress here and its result when it arrives; closing the\n * control stops following, and the job carries on in AI jobs.\n */\n\n/** What a control asks the create door for. */\nexport interface AiJobRequest {\n orgId: string\n /** `null` for a job about the workspace rather than one site. */\n hostId: string | null\n kind: AiJobKind\n brief: string\n inputs: Record<string, string>\n}\n\n// Whether a job settled, and why it has nothing to show, are the model's\n// (AGL-3854), which the native apps' follow view reads too.\nexport { aiJobProblem, aiJobSettled }\n\nexport interface AiJobRun {\n /** The job as it last arrived; `null` before one is started. */\n job: AiJobSummary | null\n /** The create door is being asked. */\n starting: boolean\n /** The job exists and has not settled. */\n running: boolean\n /** Why the create door refused, or could not be reached. */\n notice: string | null\n /** Starts the job, `true` once it exists, and follows it from there; `false` when the door refused. */\n start: (request: AiJobRequest) => Promise<boolean>\n /** Stops following and forgets the job, for a control asked again. */\n reset: () => void\n}\n\nexport function useAiJobRun(user: MaybeTokenSource, failed: string): AiJobRun {\n const userRef = useRef(user)\n userRef.current = user\n const [job, setJob] = useState<AiJobSummary | null>(null)\n const [starting, setStarting] = useState(false)\n const [notice, setNotice] = useState<string | null>(null)\n const watchRef = useRef<AbortController | null>(null)\n\n useEffect(() => () => watchRef.current?.abort(), [])\n\n /** Follows a job through the events route until it settles or the control goes. */\n const watch = useCallback(async (jobId: string, orgId: string) => {\n watchRef.current?.abort()\n const controller = new AbortController()\n watchRef.current = controller\n await followAiJobEvents(() => userRef.current, orgId, jobId, controller.signal, setJob)\n }, [])\n\n const start = useCallback(\n async (request: AiJobRequest) => {\n watchRef.current?.abort()\n setStarting(true)\n setNotice(null)\n setJob(null)\n let next: AiJobSummary | undefined\n try {\n const response = await authorizedFetch(userRef.current, '/api/ai/jobs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(request),\n })\n const payload = await response.json().catch(() => null)\n if (!response.ok) {\n const locked = parseLockdownRefusal(response.status, payload)\n setNotice(locked ? lockdownRefusalText(locked) : String(payload?.error ?? failed))\n return false\n }\n next = payload?.job as AiJobSummary | undefined\n if (!next) {\n setNotice(failed)\n return false\n }\n setJob(next)\n // The indicator counts it from the moment it exists (AGL-3593).\n publishAiJob(next)\n } catch {\n setNotice(failed)\n return false\n } finally {\n setStarting(false)\n }\n if (!aiJobSettled(next)) void watch(next.id, request.orgId)\n return true\n },\n [failed, watch],\n )\n\n const reset = useCallback(() => {\n watchRef.current?.abort()\n setJob(null)\n setNotice(null)\n }, [])\n\n return { job, starting, running: Boolean(job && !aiJobSettled(job)), notice, start, reset }\n}\n"],"names":["lockdownRefusalText","parseLockdownRefusal","authorizedFetch","useCallback","useEffect","useRef","useState","aiJobProblem","aiJobSettled","followAiJobEvents","publishAiJob","useAiJobRun","user","failed","userRef","current","job","setJob","starting","setStarting","notice","setNotice","watchRef","abort","watch","jobId","orgId","controller","AbortController","signal","start","request","next","response","method","headers","body","JSON","stringify","payload","json","catch","ok","locked","status","String","error","id","reset","running","Boolean"],"mappings":"AAAA;AAEA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,EAAEC,oBAAoB,QAAQ,eAAc;AACxE,SACEC,eAAe,QAEV,2CAA0C;AACjD,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAEhE,SAASC,YAAY,EAAEC,YAAY,QAAQ,8BAA0B;AACrE,SAASC,iBAAiB,QAAQ,qBAAiB;AACnD,SAASC,YAAY,QAAQ,qBAAiB;AAoB9C,yEAAyE;AACzE,4DAA4D;AAC5D,SAASH,YAAY,EAAEC,YAAY,GAAE;AAiBrC,OAAO,SAASG,YAAYC,IAAsB,EAAEC,MAAc;IAChE,MAAMC,UAAUT,OAAOO;IACvBE,QAAQC,OAAO,GAAGH;IAClB,MAAM,CAACI,KAAKC,OAAO,GAAGX,SAA8B;IACpD,MAAM,CAACY,UAAUC,YAAY,GAAGb,SAAS;IACzC,MAAM,CAACc,QAAQC,UAAU,GAAGf,SAAwB;IACpD,MAAMgB,WAAWjB,OAA+B;IAEhDD,UAAU,IAAM;gBAAMkB;oBAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;WAAI,EAAE;IAEnD,iFAAiF,GACjF,MAAMC,QAAQrB,YAAY,OAAOsB,OAAeC;YAC9CJ;SAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;QACvB,MAAMI,aAAa,IAAIC;QACvBN,SAASP,OAAO,GAAGY;QACnB,MAAMlB,kBAAkB,IAAMK,QAAQC,OAAO,EAAEW,OAAOD,OAAOE,WAAWE,MAAM,EAAEZ;IAClF,GAAG,EAAE;IAEL,MAAMa,QAAQ3B,YACZ,OAAO4B;YACLT;SAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;QACvBJ,YAAY;QACZE,UAAU;QACVJ,OAAO;QACP,IAAIe;QACJ,IAAI;YACF,MAAMC,WAAW,MAAM/B,gBAAgBY,QAAQC,OAAO,EAAE,gBAAgB;gBACtEmB,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CC,MAAMC,KAAKC,SAAS,CAACP;YACvB;YACA,MAAMQ,UAAU,MAAMN,SAASO,IAAI,GAAGC,KAAK,CAAC,IAAM;YAClD,IAAI,CAACR,SAASS,EAAE,EAAE;;gBAChB,MAAMC,SAAS1C,qBAAqBgC,SAASW,MAAM,EAAEL;gBACrDlB,UAAUsB,SAAS3C,oBAAoB2C,UAAUE,eAAON,2BAAAA,QAASO,KAAK,mBAAIjC;gBAC1E,OAAO;YACT;YACAmB,OAAOO,2BAAAA,QAASvB,GAAG;YACnB,IAAI,CAACgB,MAAM;gBACTX,UAAUR;gBACV,OAAO;YACT;YACAI,OAAOe;YACP,gEAAgE;YAChEtB,aAAasB;QACf,EAAE,eAAM;YACNX,UAAUR;YACV,OAAO;QACT,SAAU;YACRM,YAAY;QACd;QACA,IAAI,CAACX,aAAawB,OAAO,KAAKR,MAAMQ,KAAKe,EAAE,EAAEhB,QAAQL,KAAK;QAC1D,OAAO;IACT,GACA;QAACb;QAAQW;KAAM;IAGjB,MAAMwB,QAAQ7C,YAAY;YACxBmB;SAAAA,oBAAAA,SAASP,OAAO,qBAAhBO,kBAAkBC,KAAK;QACvBN,OAAO;QACPI,UAAU;IACZ,GAAG,EAAE;IAEL,OAAO;QAAEL;QAAKE;QAAU+B,SAASC,QAAQlC,OAAO,CAACR,aAAaQ;QAAOI;QAAQU;QAAOkB;IAAM;AAC5F"}
|
|
@@ -17,6 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ import { formPeriodKey, formStatsTotals } from "@aglyn/aglyn/app-utils/forms";
|
|
18
18
|
import { PLUGIN_FIGURE_MAX_ROWS, pluginFigureChange, pluginFigureDay, pluginFigureWindows, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
|
|
19
19
|
import { AI_PLUGIN_ID } from "../constants.js";
|
|
20
|
+
import { FORMS_WEEKLY_SUMMARY, SOURCES_WEEKLY_SUMMARY, TOP_PAGES_WEEKLY_SUMMARY, TRAFFIC_WEEKLY_SUMMARY } from "./ai-figure-summaries.js";
|
|
20
21
|
/** The traffic windows: the Traffic card's own ranges. */ export const AI_TRAFFIC_WINDOWS = [
|
|
21
22
|
7,
|
|
22
23
|
14,
|
|
@@ -129,7 +130,8 @@ export function trafficFigureReaders(firestore) {
|
|
|
129
130
|
id: 'traffic.summary',
|
|
130
131
|
label: 'Traffic',
|
|
131
132
|
description: 'Page views, visitors and views a day on the site over the window, each beside the window before it and the change.',
|
|
132
|
-
windows: AI_TRAFFIC_WINDOWS
|
|
133
|
+
windows: AI_TRAFFIC_WINDOWS,
|
|
134
|
+
weeklySummary: TRAFFIC_WEEKLY_SUMMARY
|
|
133
135
|
}, async (db, request)=>{
|
|
134
136
|
const ids = dayIds(request.now, request.days);
|
|
135
137
|
const [current, previous] = await Promise.all([
|
|
@@ -205,7 +207,8 @@ export function trafficFigureReaders(firestore) {
|
|
|
205
207
|
id: 'traffic.pages',
|
|
206
208
|
label: 'Top pages',
|
|
207
209
|
description: `The site's ${AI_TRAFFIC_TOP_PAGES} most viewed pages over the window, with each page's share of all views and its change from the window before.`,
|
|
208
|
-
windows: AI_TRAFFIC_WINDOWS
|
|
210
|
+
windows: AI_TRAFFIC_WINDOWS,
|
|
211
|
+
weeklySummary: TOP_PAGES_WEEKLY_SUMMARY
|
|
209
212
|
}, async (db, request)=>{
|
|
210
213
|
const ids = dayIds(request.now, request.days);
|
|
211
214
|
const [current, previous] = await Promise.all([
|
|
@@ -262,11 +265,17 @@ export function trafficFigureReaders(firestore) {
|
|
|
262
265
|
id: 'traffic.sources',
|
|
263
266
|
label: 'Traffic sources',
|
|
264
267
|
description: 'Where visits came from over the window: the referring sites, and the campaign sources and campaigns links were tagged with, with each one’s views.',
|
|
265
|
-
windows: AI_TRAFFIC_WINDOWS
|
|
268
|
+
windows: AI_TRAFFIC_WINDOWS,
|
|
269
|
+
weeklySummary: SOURCES_WEEKLY_SUMMARY
|
|
266
270
|
}, async (db, request)=>{
|
|
267
271
|
const days = await readTrafficDays(db, request.hostId, dayIds(request.now, request.days).current);
|
|
268
272
|
const all = days.reduce((sum, day)=>sum + day.total, 0);
|
|
269
|
-
|
|
273
|
+
// The collector stores a referring host with its dots as `_`, because a
|
|
274
|
+
// Firestore field path cannot hold one; a host is read back as written.
|
|
275
|
+
const referrers = ranked(sumMaps(days, (day)=>day.referrers)).map(([host, views])=>[
|
|
276
|
+
host.replace(/_/g, '.'),
|
|
277
|
+
views
|
|
278
|
+
]);
|
|
270
279
|
const sources = ranked(sumMaps(days, (day)=>{
|
|
271
280
|
var _day_utm_source;
|
|
272
281
|
return (_day_utm_source = day.utm['source']) != null ? _day_utm_source : {};
|
|
@@ -375,7 +384,8 @@ export function formsFigureReader(firestore) {
|
|
|
375
384
|
id: 'forms.performance',
|
|
376
385
|
label: 'Forms',
|
|
377
386
|
description: 'Each form’s views, submissions, the share of views that became a submission, and leads, over the calendar months the window falls in.',
|
|
378
|
-
windows: AI_TRAFFIC_WINDOWS
|
|
387
|
+
windows: AI_TRAFFIC_WINDOWS,
|
|
388
|
+
weeklySummary: FORMS_WEEKLY_SUMMARY
|
|
379
389
|
}, async (db, request)=>{
|
|
380
390
|
const { current } = pluginFigureWindows(request.now, request.days);
|
|
381
391
|
const from = formPeriodKey(current.startMs);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-figure-readers.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 { formPeriodKey, formStatsTotals, type FormStats } from '@aglyn/aglyn/app-utils/forms'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureChange,\n pluginFigureDay,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRead,\n type PluginFigureRequest,\n type PluginFigureRow,\n type PluginFigureTable,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { AI_PLUGIN_ID } from '../constants'\n\n/**\n * The figure readers this plugin registers for records the PLATFORM keeps\n * (AGL-2915): a site's page-view counters and its forms' counters. Every\n * other reader an insight reads is registered by the plugin that owns its\n * records — commerce, bookings, marketing, the workspace's datasets — on the\n * `plugin-figures` seam, and nothing here reads their documents.\n *\n * Each reader answers with one compact table of aggregates, read-only and\n * about nobody: counts, sums and rates, labeled by a page path, a form's name\n * or a referring site — never a visitor or a submission.\n *\n * The readers use the arithmetic their console cards use: the Traffic card's\n * windows and change (`pluginFigureWindows`, `pluginFigureChange`, which are\n * that card's split and delta), and the forms plugin's month counters through\n * `formStatsTotals`, the one function every forms surface windows by.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** How a reader reaches Firestore: resolved when it reads, never when it registers. */\nexport type AiFigureFirestore = () => Firestore\n\n/** The traffic windows: the Traffic card's own ranges. */\nexport const AI_TRAFFIC_WINDOWS: readonly number[] = [7, 14, 30, 90]\n\n/** A day-by-day table fits a table's row bound only over a short window. */\nexport const AI_TRAFFIC_DAILY_WINDOWS: readonly number[] = [7, 14]\n\n/** Pages a pages table lists; the rest are counted in `omitted`. */\nexport const AI_TRAFFIC_TOP_PAGES = 10\n\n/** Referring sites, and campaign sources and campaigns, a sources table lists. */\nexport const AI_TRAFFIC_TOP_REFERRERS = 8\nexport const AI_TRAFFIC_TOP_CAMPAIGNS = 5\n\n/** Forms a forms table reads. */\nexport const AI_FORMS_READ_LIMIT = 100\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst num = (value: unknown): number => {\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nconst round1 = (value: number): number => Math.round(value * 10) / 10\n\nconst share = (part: number, whole: number): number | null => (whole > 0 ? round1((part / whole) * 100) : null)\n\nfunction refused(status: 400 | 403 | 404, error: string): PluginFigureRead {\n return { ok: false, status, error }\n}\n\n/** A window's days as the table's period, oldest to newest. */\nfunction periodOf(request: PluginFigureRequest): PluginFigureTable['period'] {\n const { current } = pluginFigureWindows(request.now, request.days)\n return { from: current.from, to: current.to, days: request.days }\n}\n\n/** Every day id of the window and the one before it, newest first. */\nfunction dayIds(now: Date, days: number): { current: string[]; previous: string[] } {\n const { current, previous } = pluginFigureWindows(now, days)\n const walk = (startMs: number, endMs: number): string[] => {\n const ids: string[] = []\n for (let at = endMs - 86_400_000; at >= startMs; at -= 86_400_000) ids.push(pluginFigureDay(new Date(at)))\n return ids\n }\n return { current: walk(current.startMs, current.endMs), previous: walk(previous.startMs, previous.endMs) }\n}\n\n// ── Traffic ───────────────────────────────────────────────────────────────\n\ninterface TrafficDay {\n total: number\n visitors: number\n paths: Record<string, number>\n referrers: Record<string, number>\n utm: Record<string, Record<string, number>>\n}\n\nconst counts = (value: unknown): Record<string, number> => {\n const out: Record<string, number> = {}\n if (!value || typeof value !== 'object') return out\n for (const [key, count] of Object.entries(value as Record<string, unknown>)) {\n const n = num(count)\n if (n > 0) out[key] = n\n }\n return out\n}\n\nasync function readTrafficDays(\n firestore: Firestore,\n hostId: string,\n ids: readonly string[],\n): Promise<TrafficDay[]> {\n if (!ids.length) return []\n const collection = firestore.collection('hosts').doc(hostId).collection('analytics')\n const snapshots = await firestore.getAll(...ids.map((id) => collection.doc(id)))\n return snapshots.map((snapshot) => {\n const data = (snapshot.exists ? snapshot.data() : null) ?? {}\n const utm: Record<string, Record<string, number>> = {}\n for (const [param, values] of Object.entries((data['utm'] ?? {}) as Record<string, unknown>)) {\n utm[param] = counts(values)\n }\n return {\n total: num(data['total']),\n visitors: num(data['visitors']),\n paths: counts(data['paths']),\n referrers: counts(data['referrers']),\n utm,\n }\n })\n}\n\nfunction sumMaps(days: readonly TrafficDay[], pick: (day: TrafficDay) => Record<string, number>): Map<string, number> {\n const totals = new Map<string, number>()\n for (const day of days) {\n for (const [key, count] of Object.entries(pick(day))) totals.set(key, (totals.get(key) ?? 0) + count)\n }\n return totals\n}\n\nconst ranked = (totals: Map<string, number>): Array<[string, number]> =>\n [...totals.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))\n\nfunction siteReader(\n base: Omit<PluginFigureReader, 'read' | 'scope' | 'plugin'>,\n read: (firestore: Firestore, request: PluginFigureRequest & { hostId: string }) => Promise<PluginFigureRead>,\n firestore: AiFigureFirestore,\n plugin: string | null,\n): PluginFigureReader {\n return {\n ...base,\n scope: 'site',\n plugin,\n read: async (request) => {\n if (!request.hostId) return refused(400, 'Open a site to read its figures')\n if (!base.windows.includes(request.days)) return refused(400, 'That window is not one this reader covers')\n return read(firestore(), { ...request, hostId: request.hostId })\n },\n }\n}\n\nexport function trafficFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n const source = { label: 'Analytics', path: 'analytics' }\n const visitorsNote =\n 'Visitors are counted once per browser tab per day, so one person on two days counts twice.'\n return [\n siteReader(\n {\n id: 'traffic.summary',\n label: 'Traffic',\n description:\n 'Page views, visitors and views a day on the site over the window, each beside the window before it and the change.',\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const total = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.total, 0)\n const visitors = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.visitors, 0)\n const views = total(current)\n const viewsBefore = total(previous)\n const perDay = Math.round(views / request.days)\n const perDayBefore = Math.round(viewsBefore / request.days)\n const table: PluginFigureTable = {\n title: 'Traffic',\n source,\n period: periodOf(request),\n columns: [\n { key: 'figure', label: 'Figure', kind: 'text' },\n { key: 'current', label: 'This window', kind: 'count' },\n { key: 'previous', label: 'Window before', kind: 'count' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows: [\n { figure: 'Page views', current: views, previous: viewsBefore, change: pluginFigureChange(views, viewsBefore) },\n {\n figure: 'Visitors',\n current: visitors(current),\n previous: visitors(previous),\n change: pluginFigureChange(visitors(current), visitors(previous)),\n },\n {\n figure: 'Page views a day',\n current: perDay,\n previous: perDayBefore,\n change: pluginFigureChange(perDay, perDayBefore),\n },\n ],\n omitted: 0,\n notes: [\n visitorsNote,\n ...(viewsBefore ? [] : ['The window before this one recorded no page views, so there is no change to compare.']),\n ],\n }\n return { ok: true, table }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.pages',\n label: 'Top pages',\n description: `The site's ${AI_TRAFFIC_TOP_PAGES} most viewed pages over the window, with each page's share of all views and its change from the window before.`,\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const now = ranked(sumMaps(current, (day) => day.paths))\n const before = sumMaps(previous, (day) => day.paths)\n const all = now.reduce((sum, [, count]) => sum + count, 0)\n const rows: PluginFigureRow[] = now.slice(0, AI_TRAFFIC_TOP_PAGES).map(([path, views]) => ({\n page: path,\n views,\n share: share(views, all),\n change: pluginFigureChange(views, before.get(path) ?? null),\n }))\n return {\n ok: true,\n table: {\n title: 'Top pages',\n source,\n period: periodOf(request),\n columns: [\n { key: 'page', label: 'Page', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of views', kind: 'percent' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows,\n omitted: Math.max(0, now.length - rows.length),\n notes: [],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.sources',\n label: 'Traffic sources',\n description:\n 'Where visits came from over the window: the referring sites, and the campaign sources and campaigns links were tagged with, with each one’s views.',\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const days = await readTrafficDays(db, request.hostId, dayIds(request.now, request.days).current)\n const all = days.reduce((sum, day) => sum + day.total, 0)\n const referrers = ranked(sumMaps(days, (day) => day.referrers))\n const sources = ranked(sumMaps(days, (day) => day.utm['source'] ?? {}))\n const campaigns = ranked(sumMaps(days, (day) => day.utm['campaign'] ?? {}))\n const rowsOf = (entries: Array<[string, number]>, limit: number, type: string) =>\n entries.slice(0, limit).map(([name, views]) => ({ source: name, type, views, share: share(views, all) }))\n const rows = [\n ...rowsOf(referrers, AI_TRAFFIC_TOP_REFERRERS, 'Referring site'),\n ...rowsOf(sources, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign source'),\n ...rowsOf(campaigns, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign'),\n ]\n return {\n ok: true,\n table: {\n title: 'Traffic sources',\n source,\n period: periodOf(request),\n columns: [\n { key: 'source', label: 'Source', kind: 'text' },\n { key: 'type', label: 'Kind', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of all views', kind: 'percent' },\n ],\n rows,\n omitted:\n Math.max(0, referrers.length - AI_TRAFFIC_TOP_REFERRERS) +\n Math.max(0, sources.length - AI_TRAFFIC_TOP_CAMPAIGNS) +\n Math.max(0, campaigns.length - AI_TRAFFIC_TOP_CAMPAIGNS),\n notes: ['A visit with no referring site, such as a typed address, is in no row.'],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.daily',\n label: 'Traffic by day',\n description: 'Page views and visitors on each day of the window, oldest first, for spotting a day that stood out.',\n windows: AI_TRAFFIC_DAILY_WINDOWS,\n },\n async (db, request) => {\n const ids = [...dayIds(request.now, request.days).current].reverse()\n const days = await readTrafficDays(db, request.hostId, ids)\n return {\n ok: true,\n table: {\n title: 'Traffic by day',\n source,\n period: periodOf(request),\n columns: [\n { key: 'day', label: 'Day', kind: 'date' },\n { key: 'views', label: 'Page views', kind: 'count' },\n { key: 'visitors', label: 'Visitors', kind: 'count' },\n ],\n rows: days.map((day, index) => ({ day: ids[index], views: day.total, visitors: day.visitors })),\n omitted: 0,\n notes: [visitorsNote],\n },\n }\n },\n firestore,\n null,\n ),\n ]\n}\n\n// ── Forms ─────────────────────────────────────────────────────────────────\n\nexport function formsFigureReader(firestore: AiFigureFirestore): PluginFigureReader {\n return siteReader(\n {\n id: 'forms.performance',\n label: 'Forms',\n description:\n 'Each form’s views, submissions, the share of views that became a submission, and leads, over the calendar months the window falls in.',\n windows: AI_TRAFFIC_WINDOWS,\n },\n async (db, request) => {\n const { current } = pluginFigureWindows(request.now, request.days)\n const from = formPeriodKey(current.startMs)\n const to = formPeriodKey(current.endMs - 1)\n const snapshot = await db\n .collection('hosts')\n .doc(request.hostId)\n .collection('forms')\n .limit(AI_FORMS_READ_LIMIT)\n .get()\n const forms = snapshot.docs\n .map((doc) => ({ doc, data: (doc.data() ?? {}) as Record<string, unknown> }))\n .filter(({ data }) => !data['deletedAt'])\n .map(({ doc, data }) => {\n const totals = formStatsTotals(data['stats'] as FormStats | undefined, { from, to })\n // Every submission to a form is counted on it, so a form with none\n // recorded in the window had none: 0, not a blank the answer has to\n // hedge over (AGL-3834). Leads are counted only on a form that\n // routes them, so one that does reads 0 the same way, and a form\n // that never has keeps a blank, which the table's note explains.\n const submissions = totals.submissions ?? 0\n const routesLeads = (data['routing'] as { lead?: unknown } | undefined)?.lead === true\n return {\n form: str(data['displayName']) || doc.id,\n views: totals.views,\n submissions,\n completion: totals.views ? share(submissions, totals.views) : null,\n leads: totals.leads ?? (routesLeads ? 0 : null),\n }\n })\n .sort((a, b) => b.submissions - a.submissions || a.form.localeCompare(b.form))\n const monthStart = from ? `${from}-01` : current.from\n return {\n ok: true,\n table: {\n title: 'Forms',\n source: { label: 'Forms', path: 'forms' },\n period: {\n from: monthStart,\n to: current.to,\n days: Math.round((Date.parse(current.to) - Date.parse(monthStart)) / 86_400_000) + 1,\n },\n columns: [\n { key: 'form', label: 'Form', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'submissions', label: 'Submissions', kind: 'count' },\n { key: 'completion', label: 'Views that became a submission', kind: 'percent' },\n { key: 'leads', label: 'Leads', kind: 'count' },\n ],\n rows: forms.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, forms.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n 'Form figures are kept by calendar month, so they cover whole months from the first day of the month the window starts in.',\n 'Views are counted in the visitor’s browser and submissions on the server, so a rate can pass 100%.',\n 'A blank Leads figure means the form does not add its submissions to Leads.',\n ],\n },\n }\n },\n firestore,\n 'forms',\n )\n}\n\n/** Every reader this plugin registers, for the platform's own records. */\nexport function aiFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n return [...trafficFigureReaders(firestore), formsFigureReader(firestore)]\n}\n\n/**\n * Registers them from the console surface, the one surface that runs insight\n * jobs. By a call, never by an import (AGL-3025).\n */\nexport function registerAiFigureReaders(firestore: AiFigureFirestore): void {\n for (const reader of aiFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: AI_PLUGIN_ID })\n }\n}\n"],"names":["formPeriodKey","formStatsTotals","PLUGIN_FIGURE_MAX_ROWS","pluginFigureChange","pluginFigureDay","pluginFigureWindows","registerPluginFigureReader","AI_PLUGIN_ID","AI_TRAFFIC_WINDOWS","AI_TRAFFIC_DAILY_WINDOWS","AI_TRAFFIC_TOP_PAGES","AI_TRAFFIC_TOP_REFERRERS","AI_TRAFFIC_TOP_CAMPAIGNS","AI_FORMS_READ_LIMIT","str","value","trim","num","parsed","Number","isFinite","round1","Math","round","share","part","whole","refused","status","error","ok","periodOf","request","current","now","days","from","to","dayIds","previous","walk","startMs","endMs","ids","at","push","Date","counts","out","key","count","Object","entries","n","readTrafficDays","firestore","hostId","length","collection","doc","snapshots","getAll","map","id","snapshot","data","exists","utm","param","values","total","visitors","paths","referrers","sumMaps","pick","totals","Map","day","set","get","ranked","sort","a","b","localeCompare","siteReader","base","read","plugin","scope","windows","includes","trafficFigureReaders","source","label","path","visitorsNote","description","db","Promise","all","reduce","sum","views","viewsBefore","perDay","perDayBefore","table","title","period","columns","kind","rows","figure","change","omitted","notes","before","slice","page","max","sources","campaigns","rowsOf","limit","type","name","reverse","index","formsFigureReader","forms","docs","filter","submissions","routesLeads","lead","form","completion","leads","monthStart","parse","aiFigureReaders","registerAiFigureReaders","reader","pluginId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-figure-readers.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 { formPeriodKey, formStatsTotals, type FormStats } from '@aglyn/aglyn/app-utils/forms'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureChange,\n pluginFigureDay,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRead,\n type PluginFigureRequest,\n type PluginFigureRow,\n type PluginFigureTable,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { AI_PLUGIN_ID } from '../constants'\nimport {\n FORMS_WEEKLY_SUMMARY,\n SOURCES_WEEKLY_SUMMARY,\n TOP_PAGES_WEEKLY_SUMMARY,\n TRAFFIC_WEEKLY_SUMMARY,\n} from './ai-figure-summaries'\n\n/**\n * The figure readers this plugin registers for records the PLATFORM keeps\n * (AGL-2915): a site's page-view counters and its forms' counters. Every\n * other reader an insight reads is registered by the plugin that owns its\n * records — commerce, bookings, marketing, the workspace's datasets — on the\n * `plugin-figures` seam, and nothing here reads their documents.\n *\n * Each reader answers with one compact table of aggregates, read-only and\n * about nobody: counts, sums and rates, labeled by a page path, a form's name\n * or a referring site — never a visitor or a submission.\n *\n * The readers use the arithmetic their console cards use: the Traffic card's\n * windows and change (`pluginFigureWindows`, `pluginFigureChange`, which are\n * that card's split and delta), and the forms plugin's month counters through\n * `formStatsTotals`, the one function every forms surface windows by.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** How a reader reaches Firestore: resolved when it reads, never when it registers. */\nexport type AiFigureFirestore = () => Firestore\n\n/** The traffic windows: the Traffic card's own ranges. */\nexport const AI_TRAFFIC_WINDOWS: readonly number[] = [7, 14, 30, 90]\n\n/** A day-by-day table fits a table's row bound only over a short window. */\nexport const AI_TRAFFIC_DAILY_WINDOWS: readonly number[] = [7, 14]\n\n/** Pages a pages table lists; the rest are counted in `omitted`. */\nexport const AI_TRAFFIC_TOP_PAGES = 10\n\n/** Referring sites, and campaign sources and campaigns, a sources table lists. */\nexport const AI_TRAFFIC_TOP_REFERRERS = 8\nexport const AI_TRAFFIC_TOP_CAMPAIGNS = 5\n\n/** Forms a forms table reads. */\nexport const AI_FORMS_READ_LIMIT = 100\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nconst num = (value: unknown): number => {\n const parsed = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nconst round1 = (value: number): number => Math.round(value * 10) / 10\n\nconst share = (part: number, whole: number): number | null => (whole > 0 ? round1((part / whole) * 100) : null)\n\nfunction refused(status: 400 | 403 | 404, error: string): PluginFigureRead {\n return { ok: false, status, error }\n}\n\n/** A window's days as the table's period, oldest to newest. */\nfunction periodOf(request: PluginFigureRequest): PluginFigureTable['period'] {\n const { current } = pluginFigureWindows(request.now, request.days)\n return { from: current.from, to: current.to, days: request.days }\n}\n\n/** Every day id of the window and the one before it, newest first. */\nfunction dayIds(now: Date, days: number): { current: string[]; previous: string[] } {\n const { current, previous } = pluginFigureWindows(now, days)\n const walk = (startMs: number, endMs: number): string[] => {\n const ids: string[] = []\n for (let at = endMs - 86_400_000; at >= startMs; at -= 86_400_000) ids.push(pluginFigureDay(new Date(at)))\n return ids\n }\n return { current: walk(current.startMs, current.endMs), previous: walk(previous.startMs, previous.endMs) }\n}\n\n// ── Traffic ───────────────────────────────────────────────────────────────\n\ninterface TrafficDay {\n total: number\n visitors: number\n paths: Record<string, number>\n referrers: Record<string, number>\n utm: Record<string, Record<string, number>>\n}\n\nconst counts = (value: unknown): Record<string, number> => {\n const out: Record<string, number> = {}\n if (!value || typeof value !== 'object') return out\n for (const [key, count] of Object.entries(value as Record<string, unknown>)) {\n const n = num(count)\n if (n > 0) out[key] = n\n }\n return out\n}\n\nasync function readTrafficDays(\n firestore: Firestore,\n hostId: string,\n ids: readonly string[],\n): Promise<TrafficDay[]> {\n if (!ids.length) return []\n const collection = firestore.collection('hosts').doc(hostId).collection('analytics')\n const snapshots = await firestore.getAll(...ids.map((id) => collection.doc(id)))\n return snapshots.map((snapshot) => {\n const data = (snapshot.exists ? snapshot.data() : null) ?? {}\n const utm: Record<string, Record<string, number>> = {}\n for (const [param, values] of Object.entries((data['utm'] ?? {}) as Record<string, unknown>)) {\n utm[param] = counts(values)\n }\n return {\n total: num(data['total']),\n visitors: num(data['visitors']),\n paths: counts(data['paths']),\n referrers: counts(data['referrers']),\n utm,\n }\n })\n}\n\nfunction sumMaps(days: readonly TrafficDay[], pick: (day: TrafficDay) => Record<string, number>): Map<string, number> {\n const totals = new Map<string, number>()\n for (const day of days) {\n for (const [key, count] of Object.entries(pick(day))) totals.set(key, (totals.get(key) ?? 0) + count)\n }\n return totals\n}\n\nconst ranked = (totals: Map<string, number>): Array<[string, number]> =>\n [...totals.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))\n\nfunction siteReader(\n base: Omit<PluginFigureReader, 'read' | 'scope' | 'plugin'>,\n read: (firestore: Firestore, request: PluginFigureRequest & { hostId: string }) => Promise<PluginFigureRead>,\n firestore: AiFigureFirestore,\n plugin: string | null,\n): PluginFigureReader {\n return {\n ...base,\n scope: 'site',\n plugin,\n read: async (request) => {\n if (!request.hostId) return refused(400, 'Open a site to read its figures')\n if (!base.windows.includes(request.days)) return refused(400, 'That window is not one this reader covers')\n return read(firestore(), { ...request, hostId: request.hostId })\n },\n }\n}\n\nexport function trafficFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n const source = { label: 'Analytics', path: 'analytics' }\n const visitorsNote =\n 'Visitors are counted once per browser tab per day, so one person on two days counts twice.'\n return [\n siteReader(\n {\n id: 'traffic.summary',\n label: 'Traffic',\n description:\n 'Page views, visitors and views a day on the site over the window, each beside the window before it and the change.',\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: TRAFFIC_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const total = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.total, 0)\n const visitors = (days: TrafficDay[]) => days.reduce((sum, day) => sum + day.visitors, 0)\n const views = total(current)\n const viewsBefore = total(previous)\n const perDay = Math.round(views / request.days)\n const perDayBefore = Math.round(viewsBefore / request.days)\n const table: PluginFigureTable = {\n title: 'Traffic',\n source,\n period: periodOf(request),\n columns: [\n { key: 'figure', label: 'Figure', kind: 'text' },\n { key: 'current', label: 'This window', kind: 'count' },\n { key: 'previous', label: 'Window before', kind: 'count' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows: [\n { figure: 'Page views', current: views, previous: viewsBefore, change: pluginFigureChange(views, viewsBefore) },\n {\n figure: 'Visitors',\n current: visitors(current),\n previous: visitors(previous),\n change: pluginFigureChange(visitors(current), visitors(previous)),\n },\n {\n figure: 'Page views a day',\n current: perDay,\n previous: perDayBefore,\n change: pluginFigureChange(perDay, perDayBefore),\n },\n ],\n omitted: 0,\n notes: [\n visitorsNote,\n ...(viewsBefore ? [] : ['The window before this one recorded no page views, so there is no change to compare.']),\n ],\n }\n return { ok: true, table }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.pages',\n label: 'Top pages',\n description: `The site's ${AI_TRAFFIC_TOP_PAGES} most viewed pages over the window, with each page's share of all views and its change from the window before.`,\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: TOP_PAGES_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const ids = dayIds(request.now, request.days)\n const [current, previous] = await Promise.all([\n readTrafficDays(db, request.hostId, ids.current),\n readTrafficDays(db, request.hostId, ids.previous),\n ])\n const now = ranked(sumMaps(current, (day) => day.paths))\n const before = sumMaps(previous, (day) => day.paths)\n const all = now.reduce((sum, [, count]) => sum + count, 0)\n const rows: PluginFigureRow[] = now.slice(0, AI_TRAFFIC_TOP_PAGES).map(([path, views]) => ({\n page: path,\n views,\n share: share(views, all),\n change: pluginFigureChange(views, before.get(path) ?? null),\n }))\n return {\n ok: true,\n table: {\n title: 'Top pages',\n source,\n period: periodOf(request),\n columns: [\n { key: 'page', label: 'Page', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of views', kind: 'percent' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows,\n omitted: Math.max(0, now.length - rows.length),\n notes: [],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.sources',\n label: 'Traffic sources',\n description:\n 'Where visits came from over the window: the referring sites, and the campaign sources and campaigns links were tagged with, with each one’s views.',\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: SOURCES_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const days = await readTrafficDays(db, request.hostId, dayIds(request.now, request.days).current)\n const all = days.reduce((sum, day) => sum + day.total, 0)\n // The collector stores a referring host with its dots as `_`, because a\n // Firestore field path cannot hold one; a host is read back as written.\n const referrers = ranked(sumMaps(days, (day) => day.referrers)).map(\n ([host, views]): [string, number] => [host.replace(/_/g, '.'), views],\n )\n const sources = ranked(sumMaps(days, (day) => day.utm['source'] ?? {}))\n const campaigns = ranked(sumMaps(days, (day) => day.utm['campaign'] ?? {}))\n const rowsOf = (entries: Array<[string, number]>, limit: number, type: string) =>\n entries.slice(0, limit).map(([name, views]) => ({ source: name, type, views, share: share(views, all) }))\n const rows = [\n ...rowsOf(referrers, AI_TRAFFIC_TOP_REFERRERS, 'Referring site'),\n ...rowsOf(sources, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign source'),\n ...rowsOf(campaigns, AI_TRAFFIC_TOP_CAMPAIGNS, 'Campaign'),\n ]\n return {\n ok: true,\n table: {\n title: 'Traffic sources',\n source,\n period: periodOf(request),\n columns: [\n { key: 'source', label: 'Source', kind: 'text' },\n { key: 'type', label: 'Kind', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'share', label: 'Share of all views', kind: 'percent' },\n ],\n rows,\n omitted:\n Math.max(0, referrers.length - AI_TRAFFIC_TOP_REFERRERS) +\n Math.max(0, sources.length - AI_TRAFFIC_TOP_CAMPAIGNS) +\n Math.max(0, campaigns.length - AI_TRAFFIC_TOP_CAMPAIGNS),\n notes: ['A visit with no referring site, such as a typed address, is in no row.'],\n },\n }\n },\n firestore,\n null,\n ),\n siteReader(\n {\n id: 'traffic.daily',\n label: 'Traffic by day',\n description: 'Page views and visitors on each day of the window, oldest first, for spotting a day that stood out.',\n windows: AI_TRAFFIC_DAILY_WINDOWS,\n },\n async (db, request) => {\n const ids = [...dayIds(request.now, request.days).current].reverse()\n const days = await readTrafficDays(db, request.hostId, ids)\n return {\n ok: true,\n table: {\n title: 'Traffic by day',\n source,\n period: periodOf(request),\n columns: [\n { key: 'day', label: 'Day', kind: 'date' },\n { key: 'views', label: 'Page views', kind: 'count' },\n { key: 'visitors', label: 'Visitors', kind: 'count' },\n ],\n rows: days.map((day, index) => ({ day: ids[index], views: day.total, visitors: day.visitors })),\n omitted: 0,\n notes: [visitorsNote],\n },\n }\n },\n firestore,\n null,\n ),\n ]\n}\n\n// ── Forms ─────────────────────────────────────────────────────────────────\n\nexport function formsFigureReader(firestore: AiFigureFirestore): PluginFigureReader {\n return siteReader(\n {\n id: 'forms.performance',\n label: 'Forms',\n description:\n 'Each form’s views, submissions, the share of views that became a submission, and leads, over the calendar months the window falls in.',\n windows: AI_TRAFFIC_WINDOWS,\n weeklySummary: FORMS_WEEKLY_SUMMARY,\n },\n async (db, request) => {\n const { current } = pluginFigureWindows(request.now, request.days)\n const from = formPeriodKey(current.startMs)\n const to = formPeriodKey(current.endMs - 1)\n const snapshot = await db\n .collection('hosts')\n .doc(request.hostId)\n .collection('forms')\n .limit(AI_FORMS_READ_LIMIT)\n .get()\n const forms = snapshot.docs\n .map((doc) => ({ doc, data: (doc.data() ?? {}) as Record<string, unknown> }))\n .filter(({ data }) => !data['deletedAt'])\n .map(({ doc, data }) => {\n const totals = formStatsTotals(data['stats'] as FormStats | undefined, { from, to })\n // Every submission to a form is counted on it, so a form with none\n // recorded in the window had none: 0, not a blank the answer has to\n // hedge over (AGL-3834). Leads are counted only on a form that\n // routes them, so one that does reads 0 the same way, and a form\n // that never has keeps a blank, which the table's note explains.\n const submissions = totals.submissions ?? 0\n const routesLeads = (data['routing'] as { lead?: unknown } | undefined)?.lead === true\n return {\n form: str(data['displayName']) || doc.id,\n views: totals.views,\n submissions,\n completion: totals.views ? share(submissions, totals.views) : null,\n leads: totals.leads ?? (routesLeads ? 0 : null),\n }\n })\n .sort((a, b) => b.submissions - a.submissions || a.form.localeCompare(b.form))\n const monthStart = from ? `${from}-01` : current.from\n return {\n ok: true,\n table: {\n title: 'Forms',\n source: { label: 'Forms', path: 'forms' },\n period: {\n from: monthStart,\n to: current.to,\n days: Math.round((Date.parse(current.to) - Date.parse(monthStart)) / 86_400_000) + 1,\n },\n columns: [\n { key: 'form', label: 'Form', kind: 'text' },\n { key: 'views', label: 'Views', kind: 'count' },\n { key: 'submissions', label: 'Submissions', kind: 'count' },\n { key: 'completion', label: 'Views that became a submission', kind: 'percent' },\n { key: 'leads', label: 'Leads', kind: 'count' },\n ],\n rows: forms.slice(0, PLUGIN_FIGURE_MAX_ROWS),\n omitted: Math.max(0, forms.length - PLUGIN_FIGURE_MAX_ROWS),\n notes: [\n 'Form figures are kept by calendar month, so they cover whole months from the first day of the month the window starts in.',\n 'Views are counted in the visitor’s browser and submissions on the server, so a rate can pass 100%.',\n 'A blank Leads figure means the form does not add its submissions to Leads.',\n ],\n },\n }\n },\n firestore,\n 'forms',\n )\n}\n\n/** Every reader this plugin registers, for the platform's own records. */\nexport function aiFigureReaders(firestore: AiFigureFirestore): PluginFigureReader[] {\n return [...trafficFigureReaders(firestore), formsFigureReader(firestore)]\n}\n\n/**\n * Registers them from the console surface, the one surface that runs insight\n * jobs. By a call, never by an import (AGL-3025).\n */\nexport function registerAiFigureReaders(firestore: AiFigureFirestore): void {\n for (const reader of aiFigureReaders(firestore)) {\n registerPluginFigureReader(reader, { pluginId: AI_PLUGIN_ID })\n }\n}\n"],"names":["formPeriodKey","formStatsTotals","PLUGIN_FIGURE_MAX_ROWS","pluginFigureChange","pluginFigureDay","pluginFigureWindows","registerPluginFigureReader","AI_PLUGIN_ID","FORMS_WEEKLY_SUMMARY","SOURCES_WEEKLY_SUMMARY","TOP_PAGES_WEEKLY_SUMMARY","TRAFFIC_WEEKLY_SUMMARY","AI_TRAFFIC_WINDOWS","AI_TRAFFIC_DAILY_WINDOWS","AI_TRAFFIC_TOP_PAGES","AI_TRAFFIC_TOP_REFERRERS","AI_TRAFFIC_TOP_CAMPAIGNS","AI_FORMS_READ_LIMIT","str","value","trim","num","parsed","Number","isFinite","round1","Math","round","share","part","whole","refused","status","error","ok","periodOf","request","current","now","days","from","to","dayIds","previous","walk","startMs","endMs","ids","at","push","Date","counts","out","key","count","Object","entries","n","readTrafficDays","firestore","hostId","length","collection","doc","snapshots","getAll","map","id","snapshot","data","exists","utm","param","values","total","visitors","paths","referrers","sumMaps","pick","totals","Map","day","set","get","ranked","sort","a","b","localeCompare","siteReader","base","read","plugin","scope","windows","includes","trafficFigureReaders","source","label","path","visitorsNote","description","weeklySummary","db","Promise","all","reduce","sum","views","viewsBefore","perDay","perDayBefore","table","title","period","columns","kind","rows","figure","change","omitted","notes","before","slice","page","max","host","replace","sources","campaigns","rowsOf","limit","type","name","reverse","index","formsFigureReader","forms","docs","filter","submissions","routesLeads","lead","form","completion","leads","monthStart","parse","aiFigureReaders","registerAiFigureReaders","reader","pluginId"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { PluginFigureWeeklySummary } from '@aglyn/aglyn/plugin-manager/plugin-figures';
|
|
18
|
+
/**
|
|
19
|
+
* How the traffic and forms readers read in the weekly summary (AGL-3867).
|
|
20
|
+
* Each answers `null` for a week it has nothing to say about, and for a table
|
|
21
|
+
* that is not the shape it was written for, so a changed reader drops out of
|
|
22
|
+
* the email rather than misreporting.
|
|
23
|
+
*/
|
|
24
|
+
export declare const TRAFFIC_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
|
|
25
|
+
export declare const TOP_PAGES_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
|
|
26
|
+
export declare const SOURCES_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
|
|
27
|
+
export declare const FORMS_WEEKLY_SUMMARY: PluginFigureWeeklySummary;
|
|
@@ -0,0 +1,96 @@
|
|
|
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 { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { figureCount, figureHasColumn, figureRanked, figureRow, figureStat, weeklySummaryDay } from "@aglyn/aglyn/app-utils/weekly-summary";
|
|
18
|
+
/**
|
|
19
|
+
* How the traffic and forms readers read in the weekly summary (AGL-3867).
|
|
20
|
+
* Each answers `null` for a week it has nothing to say about, and for a table
|
|
21
|
+
* that is not the shape it was written for, so a changed reader drops out of
|
|
22
|
+
* the email rather than misreporting.
|
|
23
|
+
*/ export const TRAFFIC_WEEKLY_SUMMARY = {
|
|
24
|
+
order: 10,
|
|
25
|
+
summarize: (table)=>{
|
|
26
|
+
const visitors = figureRow(table, 'figure', 'Visitors');
|
|
27
|
+
const views = figureRow(table, 'figure', 'Page views');
|
|
28
|
+
if (!figureCount(visitors == null ? void 0 : visitors['current']) && !figureCount(views == null ? void 0 : views['current'])) return null;
|
|
29
|
+
return {
|
|
30
|
+
title: 'Traffic',
|
|
31
|
+
stats: [
|
|
32
|
+
...visitors ? [
|
|
33
|
+
figureStat(table, visitors, 'current', 'Visitors', {
|
|
34
|
+
change: 'change',
|
|
35
|
+
total: {
|
|
36
|
+
one: 'visitor',
|
|
37
|
+
many: 'visitors'
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
] : [],
|
|
41
|
+
...views ? [
|
|
42
|
+
figureStat(table, views, 'current', 'Page views', {
|
|
43
|
+
change: 'change'
|
|
44
|
+
})
|
|
45
|
+
] : []
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export const TOP_PAGES_WEEKLY_SUMMARY = {
|
|
51
|
+
order: 20,
|
|
52
|
+
summarize: (table)=>{
|
|
53
|
+
const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'page', 'views') : [];
|
|
54
|
+
return rows.length ? {
|
|
55
|
+
title: 'Top pages',
|
|
56
|
+
rows: rows.map((row)=>_extends({}, row, {
|
|
57
|
+
value: `${row.value} views`
|
|
58
|
+
}))
|
|
59
|
+
} : null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
export const SOURCES_WEEKLY_SUMMARY = {
|
|
63
|
+
order: 30,
|
|
64
|
+
summarize: (table)=>{
|
|
65
|
+
const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'source', 'views') : [];
|
|
66
|
+
return rows.length ? {
|
|
67
|
+
title: 'Where visits came from',
|
|
68
|
+
rows: rows.map((row)=>_extends({}, row, {
|
|
69
|
+
value: `${row.value} views`
|
|
70
|
+
}))
|
|
71
|
+
} : null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
export const FORMS_WEEKLY_SUMMARY = {
|
|
75
|
+
order: 40,
|
|
76
|
+
summarize: (table)=>{
|
|
77
|
+
if (!figureHasColumn(table, 'submissions')) return null;
|
|
78
|
+
const total = table.rows.reduce((sum, row)=>sum + figureCount(row['submissions']), 0);
|
|
79
|
+
if (!total) return null;
|
|
80
|
+
// Form figures are kept by calendar month, so the block says so rather
|
|
81
|
+
// than calling a month-to-date count "last week".
|
|
82
|
+
const since = table.period ? weeklySummaryDay(table.period.from) : null;
|
|
83
|
+
return {
|
|
84
|
+
title: 'Forms',
|
|
85
|
+
stats: [
|
|
86
|
+
{
|
|
87
|
+
label: since ? `Submissions since ${since}` : 'Submissions',
|
|
88
|
+
value: total.toLocaleString('en-US')
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
rows: figureRanked(table, 'form', 'submissions')
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=ai-figure-summaries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-figure-summaries.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 { PluginFigureWeeklySummary } from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n figureCount,\n figureHasColumn,\n figureRanked,\n figureRow,\n figureStat,\n weeklySummaryDay,\n} from '@aglyn/aglyn/app-utils/weekly-summary'\n\n/**\n * How the traffic and forms readers read in the weekly summary (AGL-3867).\n * Each answers `null` for a week it has nothing to say about, and for a table\n * that is not the shape it was written for, so a changed reader drops out of\n * the email rather than misreporting.\n */\n\nexport const TRAFFIC_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 10,\n summarize: (table) => {\n const visitors = figureRow(table, 'figure', 'Visitors')\n const views = figureRow(table, 'figure', 'Page views')\n if (!figureCount(visitors?.['current']) && !figureCount(views?.['current'])) return null\n return {\n title: 'Traffic',\n stats: [\n ...(visitors\n ? [figureStat(table, visitors, 'current', 'Visitors', { change: 'change', total: { one: 'visitor', many: 'visitors' } })]\n : []),\n ...(views ? [figureStat(table, views, 'current', 'Page views', { change: 'change' })] : []),\n ],\n }\n },\n}\n\nexport const TOP_PAGES_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 20,\n summarize: (table) => {\n const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'page', 'views') : []\n return rows.length ? { title: 'Top pages', rows: rows.map((row) => ({ ...row, value: `${row.value} views` })) } : null\n },\n}\n\nexport const SOURCES_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 30,\n summarize: (table) => {\n const rows = figureHasColumn(table, 'views') ? figureRanked(table, 'source', 'views') : []\n return rows.length ? { title: 'Where visits came from', rows: rows.map((row) => ({ ...row, value: `${row.value} views` })) } : null\n },\n}\n\nexport const FORMS_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 40,\n summarize: (table) => {\n if (!figureHasColumn(table, 'submissions')) return null\n const total = table.rows.reduce((sum, row) => sum + figureCount(row['submissions']), 0)\n if (!total) return null\n // Form figures are kept by calendar month, so the block says so rather\n // than calling a month-to-date count \"last week\".\n const since = table.period ? weeklySummaryDay(table.period.from) : null\n return {\n title: 'Forms',\n stats: [\n {\n label: since ? `Submissions since ${since}` : 'Submissions',\n value: total.toLocaleString('en-US'),\n },\n ],\n rows: figureRanked(table, 'form', 'submissions'),\n }\n },\n}\n"],"names":["figureCount","figureHasColumn","figureRanked","figureRow","figureStat","weeklySummaryDay","TRAFFIC_WEEKLY_SUMMARY","order","summarize","table","visitors","views","title","stats","change","total","one","many","TOP_PAGES_WEEKLY_SUMMARY","rows","length","map","row","value","SOURCES_WEEKLY_SUMMARY","FORMS_WEEKLY_SUMMARY","reduce","sum","since","period","from","label","toLocaleString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,WAAW,EACXC,eAAe,EACfC,YAAY,EACZC,SAAS,EACTC,UAAU,EACVC,gBAAgB,QACX,wCAAuC;AAE9C;;;;;CAKC,GAED,OAAO,MAAMC,yBAAoD;IAC/DC,OAAO;IACPC,WAAW,CAACC;QACV,MAAMC,WAAWP,UAAUM,OAAO,UAAU;QAC5C,MAAME,QAAQR,UAAUM,OAAO,UAAU;QACzC,IAAI,CAACT,YAAYU,4BAAAA,QAAU,CAAC,UAAU,KAAK,CAACV,YAAYW,yBAAAA,KAAO,CAAC,UAAU,GAAG,OAAO;QACpF,OAAO;YACLC,OAAO;YACPC,OAAO;mBACDH,WACA;oBAACN,WAAWK,OAAOC,UAAU,WAAW,YAAY;wBAAEI,QAAQ;wBAAUC,OAAO;4BAAEC,KAAK;4BAAWC,MAAM;wBAAW;oBAAE;iBAAG,GACvH,EAAE;mBACFN,QAAQ;oBAACP,WAAWK,OAAOE,OAAO,WAAW,cAAc;wBAAEG,QAAQ;oBAAS;iBAAG,GAAG,EAAE;aAC3F;QACH;IACF;AACF,EAAC;AAED,OAAO,MAAMI,2BAAsD;IACjEX,OAAO;IACPC,WAAW,CAACC;QACV,MAAMU,OAAOlB,gBAAgBQ,OAAO,WAAWP,aAAaO,OAAO,QAAQ,WAAW,EAAE;QACxF,OAAOU,KAAKC,MAAM,GAAG;YAAER,OAAO;YAAaO,MAAMA,KAAKE,GAAG,CAAC,CAACC,MAAS,aAAKA;oBAAKC,OAAO,GAAGD,IAAIC,KAAK,CAAC,MAAM,CAAC;;QAAK,IAAI;IACpH;AACF,EAAC;AAED,OAAO,MAAMC,yBAAoD;IAC/DjB,OAAO;IACPC,WAAW,CAACC;QACV,MAAMU,OAAOlB,gBAAgBQ,OAAO,WAAWP,aAAaO,OAAO,UAAU,WAAW,EAAE;QAC1F,OAAOU,KAAKC,MAAM,GAAG;YAAER,OAAO;YAA0BO,MAAMA,KAAKE,GAAG,CAAC,CAACC,MAAS,aAAKA;oBAAKC,OAAO,GAAGD,IAAIC,KAAK,CAAC,MAAM,CAAC;;QAAK,IAAI;IACjI;AACF,EAAC;AAED,OAAO,MAAME,uBAAkD;IAC7DlB,OAAO;IACPC,WAAW,CAACC;QACV,IAAI,CAACR,gBAAgBQ,OAAO,gBAAgB,OAAO;QACnD,MAAMM,QAAQN,MAAMU,IAAI,CAACO,MAAM,CAAC,CAACC,KAAKL,MAAQK,MAAM3B,YAAYsB,GAAG,CAAC,cAAc,GAAG;QACrF,IAAI,CAACP,OAAO,OAAO;QACnB,uEAAuE;QACvE,kDAAkD;QAClD,MAAMa,QAAQnB,MAAMoB,MAAM,GAAGxB,iBAAiBI,MAAMoB,MAAM,CAACC,IAAI,IAAI;QACnE,OAAO;YACLlB,OAAO;YACPC,OAAO;gBACL;oBACEkB,OAAOH,QAAQ,CAAC,kBAAkB,EAAEA,OAAO,GAAG;oBAC9CL,OAAOR,MAAMiB,cAAc,CAAC;gBAC9B;aACD;YACDb,MAAMjB,aAAaO,OAAO,QAAQ;QACpC;IACF;AACF,EAAC"}
|
|
@@ -164,6 +164,8 @@ export declare function aiInsightDigestEmailText(input: {
|
|
|
164
164
|
workspaceName?: string;
|
|
165
165
|
/** The AI credits the digest's job drew; 0 or absent says nothing. */
|
|
166
166
|
creditsSpent?: number;
|
|
167
|
+
/** The week the figures cover, "Oct 5 – Oct 11"; absent says "this week". */
|
|
168
|
+
periodLabel?: string;
|
|
167
169
|
}): string;
|
|
168
170
|
/**
|
|
169
171
|
* One call of the sweep: delivery for this week and the last on the first
|
|
@@ -18,7 +18,8 @@ import { INSIGHT_DIGESTS_FIELD, insightDigestSubscribed, notificationMuted } fro
|
|
|
18
18
|
import { hostRoleFor } from "@aglyn/aglyn/app-utils/organizations";
|
|
19
19
|
import { checkEntitlement, resolveBrandingProfile, resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
20
20
|
import { isReleaseFlagOnForOrg, parseOrgReleaseFlagOverrides } from "@aglyn/aglyn/app-utils/release-flags";
|
|
21
|
-
import {
|
|
21
|
+
import { weeklySummaryPeriod } from "@aglyn/aglyn/app-utils/weekly-summary";
|
|
22
|
+
import { aiInsightDigestPeriod, AI_INSIGHTS_COLLECTION, aiInsightIsoWeek } from "../model/ai-insight.js";
|
|
22
23
|
import { isAiOnForSite } from "../model/ai-site-switch.js";
|
|
23
24
|
import { cancelAiJob, createAiJob, getAiJob } from "../jobs/ai-jobs.js";
|
|
24
25
|
export const AI_INSIGHTS_DIGEST_CRON_ID = 'ai-insights-digest';
|
|
@@ -56,7 +57,7 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
|
56
57
|
const drew = credits ? ` Writing it used ${credits.toLocaleString('en-US')} of ` + `${workspace ? `${workspace}’s` : 'the workspace’s'} AI credits.` : '';
|
|
57
58
|
const forWhom = workspace || 'this workspace';
|
|
58
59
|
return [
|
|
59
|
-
`Here is what ${input.siteName}'s figures showed this week.${drew}`,
|
|
60
|
+
input.periodLabel ? `Here is what ${input.siteName}'s figures showed last week, ${input.periodLabel}.${drew}` : `Here is what ${input.siteName}'s figures showed this week.${drew}`,
|
|
60
61
|
'',
|
|
61
62
|
...input.insights.map((insight, index)=>`${index + 1}. ${insight.text}`),
|
|
62
63
|
'',
|
|
@@ -124,7 +125,8 @@ async function readUsers(firestore, uids) {
|
|
|
124
125
|
brief: `Weekly insights for ${site.name}`,
|
|
125
126
|
inputs: {
|
|
126
127
|
surface: 'digest',
|
|
127
|
-
week
|
|
128
|
+
week,
|
|
129
|
+
through: weeklySummaryPeriod(now).to
|
|
128
130
|
},
|
|
129
131
|
createdBy: site.creator
|
|
130
132
|
}, now);
|
|
@@ -240,7 +242,8 @@ async function readUsers(firestore, uids) {
|
|
|
240
242
|
analyticsUrl,
|
|
241
243
|
settingsUrl: origin ? `${origin}/manage/notifications` : null,
|
|
242
244
|
workspaceName: str(org['name']),
|
|
243
|
-
creditsSpent: job.creditsSpent
|
|
245
|
+
creditsSpent: job.creditsSpent,
|
|
246
|
+
periodLabel: aiInsightDigestPeriod(job.inputs, new Date(Number(marker.createdAtMs) || now.getTime())).label
|
|
244
247
|
}),
|
|
245
248
|
fromName: branding.fromName,
|
|
246
249
|
siteName: site.name,
|