@aglyn/plugins-ai 1.0.0-beta.260 → 1.0.0-beta.262

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-brief-dialog.component.d.ts +21 -58
  3. package/src/lib/components/ai-brief-dialog.component.js +8 -132
  4. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  5. package/src/lib/components/ai-job-follow.component.js +2 -35
  6. package/src/lib/components/ai-job-follow.component.js.map +1 -1
  7. package/src/lib/components/ai-job-plan.component.d.ts +2 -8
  8. package/src/lib/components/ai-job-plan.component.js +13 -74
  9. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  10. package/src/lib/components/ai-site-start-card.component.js +43 -34
  11. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  12. package/src/lib/components/ai-template-collection-field.component.d.ts +2 -12
  13. package/src/lib/components/ai-template-collection-field.component.js +13 -10
  14. package/src/lib/components/ai-template-collection-field.component.js.map +1 -1
  15. package/src/lib/components/ai-upsell-dialog.component.d.ts +3 -25
  16. package/src/lib/components/ai-upsell-dialog.component.js +9 -66
  17. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  18. package/src/lib/components/use-ai-job-run.d.ts +3 -8
  19. package/src/lib/components/use-ai-job-run.js +4 -15
  20. package/src/lib/components/use-ai-job-run.js.map +1 -1
  21. package/src/lib/insights/ai-figure-readers.js +15 -5
  22. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  23. package/src/lib/insights/ai-figure-summaries.d.ts +27 -0
  24. package/src/lib/insights/ai-figure-summaries.js +79 -0
  25. package/src/lib/insights/ai-figure-summaries.js.map +1 -0
  26. package/src/lib/insights/ai-insight-readers.d.ts +2 -1
  27. package/src/lib/insights/ai-insight-readers.js +11 -26
  28. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  29. package/src/lib/jobs/ai-job-admission.d.ts +6 -0
  30. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  31. package/src/lib/jobs/ai-job-site-step.js +10 -6
  32. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  33. package/src/lib/jobs/ai-layout-stock-photos.d.ts +43 -11
  34. package/src/lib/jobs/ai-layout-stock-photos.js +329 -32
  35. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  36. package/src/lib/jobs/ai-stock-photo-picker.js +13 -7
  37. package/src/lib/jobs/ai-stock-photo-picker.js.map +1 -1
  38. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.d.ts +35 -0
  39. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.js +404 -0
  40. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.js.map +1 -0
  41. package/src/lib/layout-language/ai-layout-motion.d.ts +9 -3
  42. package/src/lib/layout-language/ai-layout-motion.js +9 -3
  43. package/src/lib/layout-language/ai-layout-motion.js.map +1 -1
  44. package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
  45. package/src/lib/layout-language/ai-layout-pictures.js +12 -6
  46. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  47. package/src/lib/model/ai-brief-copy.d.ts +85 -0
  48. package/src/lib/model/ai-brief-copy.js +152 -0
  49. package/src/lib/model/ai-brief-copy.js.map +1 -0
  50. package/src/lib/model/ai-create-view.d.ts +200 -0
  51. package/src/lib/model/ai-create-view.js +263 -0
  52. package/src/lib/model/ai-create-view.js.map +1 -0
  53. package/src/lib/model/ai-insight.d.ts +1 -1
  54. package/src/lib/model/ai-insight.js +4 -39
  55. package/src/lib/model/ai-insight.js.map +1 -1
  56. package/src/lib/model/ai-job-activity.d.ts +24 -0
  57. package/src/lib/model/ai-job-activity.js +52 -1
  58. package/src/lib/model/ai-job-activity.js.map +1 -1
  59. package/src/lib/model/ai-job-plan-lines.d.ts +46 -0
  60. package/src/lib/model/ai-job-plan-lines.js +70 -0
  61. package/src/lib/model/ai-job-plan-lines.js.map +1 -0
  62. package/src/lib/model/ai-template-collections.d.ts +28 -0
  63. package/src/lib/model/ai-template-collections.js +28 -0
  64. package/src/lib/model/ai-template-collections.js.map +1 -0
  65. package/src/lib/model/ai-upsell-copy.d.ts +41 -0
  66. package/src/lib/model/ai-upsell-copy.js +85 -0
  67. package/src/lib/model/ai-upsell-copy.js.map +1 -0
  68. package/src/lib/runtime/ai-doctrine-validators.d.ts +40 -0
  69. package/src/lib/runtime/ai-doctrine-validators.js +148 -3
  70. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  71. package/src/lib/runtime/ai-doctrine.js +6 -2
  72. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  73. package/src/lib/runtime/ai-eval-memory-firestore.js +2 -0
  74. package/src/lib/runtime/ai-eval-memory-firestore.js.map +1 -1
  75. package/src/lib/server/ai-create-route.d.ts +32 -0
  76. package/src/lib/server/ai-create-route.js +295 -0
  77. package/src/lib/server/ai-create-route.js.map +1 -0
  78. package/src/lib/server/ai-jobs-route.js +19 -0
  79. package/src/lib/server/ai-jobs-route.js.map +1 -1
  80. package/src/lib/server.js +14 -0
  81. package/src/lib/server.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-upsell-dialog.component.tsx"],"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 { aiAddonName } from '@aglyn/aglyn'\nimport { trackEvent } from '@aglyn/aglyn/app-utils/analytics-events'\nimport type { ConsoleWidgetUpgrade } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { mdiCreation } from '@aglyn/shared-data-mdi'\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport { HelpTip, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState, type ReactNode } from 'react'\nimport type { AiBriefKind } from './ai-brief-dialog.component'\n\n/**\n * \"Create with AI\" on a plan that could buy the AI add-on and has not\n * (AGL-3601). The shell mounts the entry with `entitled={false}` only where\n * the add-on is sold to this workspace, and the jobs route has said the plan\n * is the one thing missing; the entry then keeps its place on the page and\n * opens this instead of the brief, so the reader sees where the add-on is used.\n */\n\n/**\n * What a \"Create with AI\" or \"Ask AI\" entry makes: the brief kinds, an\n * automation, products, a function or variable, an overlay, and an answer\n * about the figures on a page.\n */\nexport type AiUpsellKind =\n | AiBriefKind\n | 'workflow'\n | 'product'\n | 'logic'\n | 'overlay'\n | 'funnel'\n | 'insight'\n\n/** What one entry's dialog says. */\nexport interface AiUpsellCopy {\n title: string\n does: string\n /** The entry's own label, which the button reads and the offer names; \"Create with AI\" when absent. */\n label?: string\n}\n\n/** The label every \"Create with AI\" entry reads. */\nexport const AI_UPSELL_CREATE_LABEL = 'Create with AI'\n\n/** What the dialog says one entry would do here. */\nexport const AI_UPSELL_COPY: Readonly<Record<AiUpsellKind, AiUpsellCopy>> = {\n page: {\n title: 'Create a page with AI',\n does:\n 'Describe the page you need and AI plans it from your brief, using your theme and ' +\n 'what the site already has. You confirm the plan, and the page is built as an ' +\n 'unpublished draft.',\n },\n template: {\n title: 'Create a page template with AI',\n does:\n 'Describe what each page should show and AI plans the template, filling what ' +\n 'changes from page to page from each record. You confirm the plan, and the template ' +\n 'is built as a draft in your library.',\n },\n layout: {\n title: 'Create a layout with AI',\n does:\n 'Describe what the layout should hold and AI plans it, linking your pages and placing ' +\n 'your navigation or menu component when the site has one. You confirm the plan, and ' +\n 'the layout is built as a draft no page uses until you assign it.',\n },\n form: {\n title: 'Create a form with AI',\n does:\n 'Describe what the form is for and AI plans its fields, its marketing consent and ' +\n 'where each submission goes. You confirm the plan, and the form is built as a draft.',\n },\n component: {\n title: 'Create a reusable component with AI',\n does:\n 'Describe what the component should show and AI plans it, making what each page can ' +\n 'change into its properties. You confirm the plan, and the component is built as a ' +\n 'draft.',\n },\n workflow: {\n title: 'Create an automation with AI',\n does:\n 'Describe what should happen, and when, and AI drafts the automation from the ' +\n 'triggers and steps your plan includes. It is saved switched off for you to review.',\n },\n email: {\n title: 'Create an email with AI',\n does:\n 'Describe the email you need and AI plans a design from the email blocks, with three ' +\n 'subject lines and three preheaders to choose between. You confirm the plan, and the ' +\n 'design is built as a draft that is sent to nobody.',\n },\n campaign: {\n title: 'Create a campaign with AI',\n does:\n 'Describe what the campaign is for and AI writes its email design and a draft campaign ' +\n 'that would send it. Nothing is sent or scheduled, and the campaign is aimed at nobody ' +\n 'until you pick its lists.',\n },\n product: {\n title: 'Create products with AI',\n does:\n 'Describe what the store sells and AI proposes up to twelve products. You review them ' +\n 'before any is created, and each is created as a draft at a suggested price that is not on ' +\n 'your storefront until you review it and activate it.',\n },\n logic: {\n title: 'Create a function or variable with AI',\n does:\n 'Describe what it should work out or hold and AI drafts the function or variable. It ' +\n 'opens unsaved in the editor for you to check and save.',\n },\n overlay: {\n title: 'Create an overlay with AI',\n does:\n 'Describe the announcement bar or popup you want and AI writes its copy within the ' +\n 'overlay’s limits. It is saved switched off, for you to check and turn on.',\n },\n funnel: {\n title: 'Create a funnel with AI',\n does:\n 'Describe the journey you want to measure and AI proposes its steps from the site’s ' +\n 'real pages, forms and products. It opens in the funnel editor for you to check and save.',\n },\n insight: {\n title: 'Ask AI about these numbers',\n label: 'Ask a question',\n does:\n 'Ask a question about the figures on this page and AI answers from them, every answer ' +\n 'traced to the figures it is built from. It reads; it changes nothing.',\n },\n}\n\n/** The label an entry's button reads. */\nexport function aiUpsellLabel(kind: AiUpsellKind): string {\n return AI_UPSELL_COPY[kind].label ?? AI_UPSELL_CREATE_LABEL\n}\n\n/**\n * The sentence that says where the feature comes from, and who can add it.\n * `feature` is what the entry is called: \"Create with AI\" unless it is an ask.\n */\nexport function aiUpsellOffer(canManageBilling: boolean, feature = AI_UPSELL_CREATE_LABEL): string {\n const addon = aiAddonName()\n return canManageBilling\n ? `${feature} comes with the ${addon} add-on, which you can add to this ` +\n 'workspace from Billing.'\n : `${feature} comes with the ${addon} add-on. Ask a workspace owner or admin ` +\n 'to add it from Billing.'\n}\n\nexport interface AiUpsellDialogProps {\n kind: AiUpsellKind\n open: boolean\n onClose: () => void\n upgrade: ConsoleWidgetUpgrade\n}\n\n/** The add-on's own section of the Create with AI guide (AGL-3660). */\nconst aiUpsellHelp = () =>\n pluginDocsHelp('aiCreate', {\n anchor: '#without-the-add-on',\n excerpt: `Create with AI needs the ${aiAddonName()} add-on on a paid plan. An owner or admin adds it from Billing; on Free, the monthly AI credits cover it.`,\n })\n\nexport function AiUpsellDialog({ kind, open, onClose, upgrade }: AiUpsellDialogProps) {\n const copy = AI_UPSELL_COPY[kind]\n const { canManageBilling, billingHref } = upgrade\n\n // Counted once per opening: the denominator the click is a rate against.\n useEffect(() => {\n if (open) trackEvent('ai_upsell_shown', { kind, can_manage: canManageBilling })\n }, [open, kind, canManageBilling])\n\n return (\n <Dialog open={open} onClose={onClose} fullWidth maxWidth=\"xs\">\n <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>\n {copy.title}\n <HelpTip {...aiUpsellHelp()} />\n </DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <Typography variant=\"body2\">{copy.does}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {aiUpsellOffer(canManageBilling, kind === 'insight' ? copy.title : AI_UPSELL_CREATE_LABEL)}\n </Typography>\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{canManageBilling ? 'Not now' : 'Close'}</Button>\n {canManageBilling ? (\n <Button\n variant=\"contained\"\n href={billingHref}\n onClick={() => trackEvent('ai_upsell_clicked', { kind })}\n >\n {`Add ${aiAddonName()}`}\n </Button>\n ) : null}\n </DialogActions>\n </Dialog>\n )\n}\n\n/**\n * The entry itself on a plan without the add-on: the same button, label and\n * icon as the entitled entry, opening {@link AiUpsellDialog}. Every entry that\n * draws it renders from the shell's client-side gates alone and asks the\n * server nothing until it is used (AGL-3601).\n */\nexport function AiUpsellButton({\n kind,\n upgrade,\n label,\n}: {\n kind: AiUpsellKind\n upgrade: ConsoleWidgetUpgrade\n /** The entitled entry's own label, when it is not the kind's. */\n label?: string\n}): ReactNode {\n const [open, setOpen] = useState(false)\n return (\n <>\n <Button\n size=\"small\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiCreation.path} />}\n onClick={() => setOpen(true)}\n >\n {label ?? aiUpsellLabel(kind)}\n </Button>\n <AiUpsellDialog kind={kind} open={open} onClose={() => setOpen(false)} upgrade={upgrade} />\n </>\n )\n}\n"],"names":["aiAddonName","trackEvent","mdiCreation","pluginDocsHelp","HelpTip","MdiIcon","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","Typography","useEffect","useState","AI_UPSELL_CREATE_LABEL","AI_UPSELL_COPY","page","title","does","template","layout","form","component","workflow","email","campaign","product","logic","overlay","funnel","insight","label","aiUpsellLabel","kind","aiUpsellOffer","canManageBilling","feature","addon","aiUpsellHelp","anchor","excerpt","AiUpsellDialog","open","onClose","upgrade","copy","billingHref","can_manage","fullWidth","maxWidth","sx","display","alignItems","gap","spacing","variant","color","onClick","href","AiUpsellButton","setOpen","size","startIcon","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-upsell-dialog.component.tsx"],"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 { aiAddonName } from '@aglyn/aglyn'\nimport { trackEvent } from '@aglyn/aglyn/app-utils/analytics-events'\nimport type { ConsoleWidgetUpgrade } from '@aglyn/aglyn/plugin-manager/feature-plugins'\nimport { mdiCreation } from '@aglyn/shared-data-mdi'\nimport { pluginDocsHelp } from '@aglyn/aglyn/app-utils/docs-help'\nimport { HelpTip, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState, type ReactNode } from 'react'\nimport {\n AI_UPSELL_COPY,\n AI_UPSELL_CREATE_LABEL,\n aiUpsellLabel,\n aiUpsellOffer,\n type AiUpsellKind,\n} from '../model/ai-upsell-copy'\n\n/**\n * \"Create with AI\" on a plan that could buy the AI add-on and has not\n * (AGL-3601). The shell mounts the entry with `entitled={false}` only where\n * the add-on is sold to this workspace, and the jobs route has said the plan\n * is the one thing missing; the entry then keeps its place on the page and\n * opens this instead of the brief, so the reader sees where the add-on is used.\n */\n\n// The entries' words are the model's, which the native apps' door reads too\n// (AGL-3854); re-exported for this dialog's existing readers.\nexport { AI_UPSELL_COPY, AI_UPSELL_CREATE_LABEL, aiUpsellLabel, aiUpsellOffer }\nexport type { AiUpsellCopy, AiUpsellKind } from '../model/ai-upsell-copy'\n\nexport interface AiUpsellDialogProps {\n kind: AiUpsellKind\n open: boolean\n onClose: () => void\n upgrade: ConsoleWidgetUpgrade\n}\n\n/** The add-on's own section of the Create with AI guide (AGL-3660). */\nconst aiUpsellHelp = () =>\n pluginDocsHelp('aiCreate', {\n anchor: '#without-the-add-on',\n excerpt: `Create with AI needs the ${aiAddonName()} add-on on a paid plan. An owner or admin adds it from Billing; on Free, the monthly AI credits cover it.`,\n })\n\nexport function AiUpsellDialog({ kind, open, onClose, upgrade }: AiUpsellDialogProps) {\n const copy = AI_UPSELL_COPY[kind]\n const { canManageBilling, billingHref } = upgrade\n\n // Counted once per opening: the denominator the click is a rate against.\n useEffect(() => {\n if (open) trackEvent('ai_upsell_shown', { kind, can_manage: canManageBilling })\n }, [open, kind, canManageBilling])\n\n return (\n <Dialog open={open} onClose={onClose} fullWidth maxWidth=\"xs\">\n <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>\n {copy.title}\n <HelpTip {...aiUpsellHelp()} />\n </DialogTitle>\n <DialogContent>\n <Stack spacing={2}>\n <Typography variant=\"body2\">{copy.does}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {aiUpsellOffer(canManageBilling, kind === 'insight' ? copy.title : AI_UPSELL_CREATE_LABEL)}\n </Typography>\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{canManageBilling ? 'Not now' : 'Close'}</Button>\n {canManageBilling ? (\n <Button\n variant=\"contained\"\n href={billingHref}\n onClick={() => trackEvent('ai_upsell_clicked', { kind })}\n >\n {`Add ${aiAddonName()}`}\n </Button>\n ) : null}\n </DialogActions>\n </Dialog>\n )\n}\n\n/**\n * The entry itself on a plan without the add-on: the same button, label and\n * icon as the entitled entry, opening {@link AiUpsellDialog}. Every entry that\n * draws it renders from the shell's client-side gates alone and asks the\n * server nothing until it is used (AGL-3601).\n */\nexport function AiUpsellButton({\n kind,\n upgrade,\n label,\n}: {\n kind: AiUpsellKind\n upgrade: ConsoleWidgetUpgrade\n /** The entitled entry's own label, when it is not the kind's. */\n label?: string\n}): ReactNode {\n const [open, setOpen] = useState(false)\n return (\n <>\n <Button\n size=\"small\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiCreation.path} />}\n onClick={() => setOpen(true)}\n >\n {label ?? aiUpsellLabel(kind)}\n </Button>\n <AiUpsellDialog kind={kind} open={open} onClose={() => setOpen(false)} upgrade={upgrade} />\n </>\n )\n}\n"],"names":["aiAddonName","trackEvent","mdiCreation","pluginDocsHelp","HelpTip","MdiIcon","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","Typography","useEffect","useState","AI_UPSELL_COPY","AI_UPSELL_CREATE_LABEL","aiUpsellLabel","aiUpsellOffer","aiUpsellHelp","anchor","excerpt","AiUpsellDialog","kind","open","onClose","upgrade","copy","canManageBilling","billingHref","can_manage","fullWidth","maxWidth","sx","display","alignItems","gap","title","spacing","variant","does","color","onClick","href","AiUpsellButton","label","setOpen","size","startIcon","path"],"mappings":"AAAA;;;AAEA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,WAAW,QAAQ,eAAc;AAC1C,SAASC,UAAU,QAAQ,0CAAyC;AAEpE,SAASC,WAAW,QAAQ,yBAAwB;AACpD,SAASC,cAAc,QAAQ,mCAAkC;AACjE,SAASC,OAAO,EAAEC,OAAO,QAAQ,uBAAsB;AACvD,SACEC,MAAM,EACNC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,WAAW,EACXC,KAAK,EACLC,UAAU,QACL,gBAAe;AACtB,SAASC,SAAS,EAAEC,QAAQ,QAAwB,QAAO;AAC3D,SACEC,cAAc,EACdC,sBAAsB,EACtBC,aAAa,EACbC,aAAa,QAER,6BAAyB;AAEhC;;;;;;CAMC,GAED,4EAA4E;AAC5E,8DAA8D;AAC9D,SAASH,cAAc,EAAEC,sBAAsB,EAAEC,aAAa,EAAEC,aAAa,GAAE;AAU/E,qEAAqE,GACrE,MAAMC,eAAe,IACnBhB,eAAe,YAAY;QACzBiB,QAAQ;QACRC,SAAS,CAAC,yBAAyB,EAAErB,cAAc,yGAAyG,CAAC;IAC/J;AAEF,OAAO,SAASsB,eAAe,EAAEC,IAAI,EAAEC,IAAI,EAAEC,OAAO,EAAEC,OAAO,EAAuB;IAClF,MAAMC,OAAOZ,cAAc,CAACQ,KAAK;IACjC,MAAM,EAAEK,gBAAgB,EAAEC,WAAW,EAAE,GAAGH;IAE1C,yEAAyE;IACzEb,UAAU;QACR,IAAIW,MAAMvB,WAAW,mBAAmB;YAAEsB;YAAMO,YAAYF;QAAiB;IAC/E,GAAG;QAACJ;QAAMD;QAAMK;KAAiB;IAEjC,qBACE,MAACrB;QAAOiB,MAAMA;QAAMC,SAASA;QAASM,SAAS;QAACC,UAAS;;0BACvD,MAACtB;gBAAYuB,IAAI;oBAAEC,SAAS;oBAAQC,YAAY;oBAAUC,KAAK;gBAAI;;oBAChET,KAAKU,KAAK;kCACX,KAACjC,sBAAYe;;;0BAEf,KAACV;0BACC,cAAA,MAACE;oBAAM2B,SAAS;;sCACd,KAAC1B;4BAAW2B,SAAQ;sCAASZ,KAAKa,IAAI;;sCACtC,KAAC5B;4BAAW2B,SAAQ;4BAAQE,OAAM;sCAC/BvB,cAAcU,kBAAkBL,SAAS,YAAYI,KAAKU,KAAK,GAAGrB;;;;;0BAIzE,MAACR;;kCACC,KAACF;wBAAOoC,SAASjB;kCAAUG,mBAAmB,YAAY;;oBACzDA,iCACC,KAACtB;wBACCiC,SAAQ;wBACRI,MAAMd;wBACNa,SAAS,IAAMzC,WAAW,qBAAqB;gCAAEsB;4BAAK;kCAErD,CAAC,IAAI,EAAEvB,eAAe;yBAEvB;;;;;AAIZ;AAEA;;;;;CAKC,GACD,OAAO,SAAS4C,eAAe,EAC7BrB,IAAI,EACJG,OAAO,EACPmB,KAAK,EAMN;IACC,MAAM,CAACrB,MAAMsB,QAAQ,GAAGhC,SAAS;IACjC,qBACE;;0BACE,KAACR;gBACCyC,MAAK;gBACLR,SAAQ;gBACRS,yBAAW,KAAC3C;oBAAQ4C,MAAM/C,YAAY+C,IAAI;;gBAC1CP,SAAS,IAAMI,QAAQ;0BAEtBD,gBAAAA,QAAS5B,cAAcM;;0BAE1B,KAACD;gBAAeC,MAAMA;gBAAMC,MAAMA;gBAAMC,SAAS,IAAMqB,QAAQ;gBAAQpB,SAASA;;;;AAGtF"}
@@ -1,5 +1,6 @@
1
1
  import { type MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token';
2
- import { type AiJobKind, type AiJobSummary } from '../model/ai-jobs.types';
2
+ import type { AiJobKind, AiJobSummary } from '../model/ai-jobs.types';
3
+ import { aiJobProblem, aiJobSettled } from '../model/ai-job-activity';
3
4
  /**
4
5
  * Starting one AI job from a console control, and following it until it
5
6
  * settles (AGL-2919): the jobs route's create door, then its events door, the
@@ -16,13 +17,7 @@ export interface AiJobRequest {
16
17
  brief: string;
17
18
  inputs: Record<string, string>;
18
19
  }
19
- /** Whether a job has stopped moving: finished, or waiting on something only a person can change. */
20
- export declare function aiJobSettled(job: AiJobSummary | null): boolean;
21
- /**
22
- * Why a settled job has no result to show, in words a person can act on, or
23
- * `null` for a job that finished.
24
- */
25
- export declare function aiJobProblem(job: AiJobSummary | null, failed: string): string | null;
20
+ export { aiJobProblem, aiJobSettled };
26
21
  export interface AiJobRun {
27
22
  /** The job as it last arrived; `null` before one is started. */
28
23
  job: AiJobSummary | null;
@@ -17,23 +17,12 @@
17
17
  */ import { lockdownRefusalText, parseLockdownRefusal } from "@aglyn/aglyn";
18
18
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
19
19
  import { useCallback, useEffect, useRef, useState } from "react";
20
- import { AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
20
+ import { aiJobProblem, aiJobSettled } from "../model/ai-job-activity.js";
21
21
  import { followAiJobEvents } from "./ai-job-events.js";
22
22
  import { publishAiJob } from "./ai-jobs-store.js";
23
- /** Whether a job has stopped moving: finished, or waiting on something only a person can change. */ export function aiJobSettled(job) {
24
- if (!job) return false;
25
- return AI_JOB_TERMINAL_STATUSES.includes(job.status) || job.status === 'needs_input' || job.status === 'needs_review';
26
- }
27
- /**
28
- * Why a settled job has no result to show, in words a person can act on, or
29
- * `null` for a job that finished.
30
- */ export function aiJobProblem(job, failed) {
31
- var _job_review;
32
- if (!job || !aiJobSettled(job) || job.status === 'done') return null;
33
- if (job.status === 'canceled') return 'The job was canceled.';
34
- if (job.status === 'needs_review') return ((_job_review = job.review) == null ? void 0 : _job_review.message) || job.error || failed;
35
- return job.error || failed;
36
- }
23
+ // Whether a job settled, and why it has nothing to show, are the model's
24
+ // (AGL-3854), which the native apps' follow view reads too.
25
+ export { aiJobProblem, aiJobSettled };
37
26
  export function useAiJobRun(user, failed) {
38
27
  const userRef = useRef(user);
39
28
  userRef.current = user;
@@ -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 {\n AI_JOB_TERMINAL_STATUSES,\n type AiJobKind,\n type AiJobSummary,\n} from '../model/ai-jobs.types'\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 has stopped moving: finished, or waiting on something only a person can change. */\nexport function aiJobSettled(job: AiJobSummary | null): boolean {\n if (!job) return false\n return (\n AI_JOB_TERMINAL_STATUSES.includes(job.status) ||\n job.status === 'needs_input' ||\n job.status === 'needs_review'\n )\n}\n\n/**\n * Why a settled job has no result to show, in words a person can act on, or\n * `null` for a job that finished.\n */\nexport function aiJobProblem(job: AiJobSummary | null, failed: string): string | null {\n if (!job || !aiJobSettled(job) || job.status === 'done') return null\n if (job.status === 'canceled') return 'The job was canceled.'\n if (job.status === 'needs_review') return job.review?.message || job.error || failed\n return job.error || failed\n}\n\nexport 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","AI_JOB_TERMINAL_STATUSES","followAiJobEvents","publishAiJob","aiJobSettled","job","includes","status","aiJobProblem","failed","review","message","error","useAiJobRun","user","userRef","current","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","String","id","reset","running","Boolean"],"mappings":"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"}
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
- const referrers = ranked(sumMaps(days, (day)=>day.referrers));
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 `[]` 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,79 @@
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 { figureCount, figureHasColumn, figureRanked, figureRow, figureWithChange, weeklySummaryDay } from "@aglyn/aglyn/app-utils/weekly-summary";
17
+ /**
18
+ * How the traffic and forms readers read in the weekly summary (AGL-3867).
19
+ * Each answers `[]` for a week it has nothing to say about, and for a table
20
+ * that is not the shape it was written for, so a changed reader drops out of
21
+ * the email rather than misreporting.
22
+ */ export const TRAFFIC_WEEKLY_SUMMARY = {
23
+ order: 10,
24
+ lines: (table)=>{
25
+ const visitors = figureRow(table, 'figure', 'Visitors');
26
+ const views = figureRow(table, 'figure', 'Page views');
27
+ if (!figureCount(visitors == null ? void 0 : visitors['current']) && !figureCount(views == null ? void 0 : views['current'])) return [];
28
+ return [
29
+ [
30
+ visitors ? `Visitors: ${figureWithChange(table, visitors, 'current', 'change')}` : null,
31
+ views ? `Page views: ${figureWithChange(table, views, 'current', 'change')}` : null
32
+ ].filter(Boolean).join(' · ')
33
+ ];
34
+ },
35
+ headline: (table)=>{
36
+ var _figureRow;
37
+ const count = figureCount((_figureRow = figureRow(table, 'figure', 'Visitors')) == null ? void 0 : _figureRow['current']);
38
+ return count ? {
39
+ count,
40
+ one: 'visitor',
41
+ many: 'visitors'
42
+ } : null;
43
+ }
44
+ };
45
+ export const TOP_PAGES_WEEKLY_SUMMARY = {
46
+ order: 20,
47
+ lines: (table)=>{
48
+ const top = figureHasColumn(table, 'views') ? figureRanked(table, 'page', 'views', 'views') : null;
49
+ return top ? [
50
+ `Top pages: ${top}`
51
+ ] : [];
52
+ }
53
+ };
54
+ export const SOURCES_WEEKLY_SUMMARY = {
55
+ order: 30,
56
+ lines: (table)=>{
57
+ const top = figureHasColumn(table, 'views') ? figureRanked(table, 'source', 'views', 'views') : null;
58
+ return top ? [
59
+ `Top sources: ${top}`
60
+ ] : [];
61
+ }
62
+ };
63
+ export const FORMS_WEEKLY_SUMMARY = {
64
+ order: 40,
65
+ lines: (table)=>{
66
+ if (!figureHasColumn(table, 'submissions')) return [];
67
+ const total = table.rows.reduce((sum, row)=>sum + figureCount(row['submissions']), 0);
68
+ if (!total) return [];
69
+ // Form figures are kept by calendar month, so the line says so rather
70
+ // than calling a month-to-date count "last week".
71
+ const since = table.period ? ` since ${weeklySummaryDay(table.period.from)}` : '';
72
+ const top = figureRanked(table, 'form', 'submissions');
73
+ return [
74
+ `Form submissions${since}: ${total.toLocaleString('en-US')}${top ? ` (${top})` : ''}`
75
+ ];
76
+ }
77
+ };
78
+
79
+ //# 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 figureWithChange,\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 `[]` 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 lines: (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 []\n return [\n [\n visitors ? `Visitors: ${figureWithChange(table, visitors, 'current', 'change')}` : null,\n views ? `Page views: ${figureWithChange(table, views, 'current', 'change')}` : null,\n ]\n .filter(Boolean)\n .join(' · '),\n ]\n },\n headline: (table) => {\n const count = figureCount(figureRow(table, 'figure', 'Visitors')?.['current'])\n return count ? { count, one: 'visitor', many: 'visitors' } : null\n },\n}\n\nexport const TOP_PAGES_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 20,\n lines: (table) => {\n const top = figureHasColumn(table, 'views') ? figureRanked(table, 'page', 'views', 'views') : null\n return top ? [`Top pages: ${top}`] : []\n },\n}\n\nexport const SOURCES_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 30,\n lines: (table) => {\n const top = figureHasColumn(table, 'views') ? figureRanked(table, 'source', 'views', 'views') : null\n return top ? [`Top sources: ${top}`] : []\n },\n}\n\nexport const FORMS_WEEKLY_SUMMARY: PluginFigureWeeklySummary = {\n order: 40,\n lines: (table) => {\n if (!figureHasColumn(table, 'submissions')) return []\n const total = table.rows.reduce((sum, row) => sum + figureCount(row['submissions']), 0)\n if (!total) return []\n // Form figures are kept by calendar month, so the line says so rather\n // than calling a month-to-date count \"last week\".\n const since = table.period ? ` since ${weeklySummaryDay(table.period.from)}` : ''\n const top = figureRanked(table, 'form', 'submissions')\n return [`Form submissions${since}: ${total.toLocaleString('en-US')}${top ? ` (${top})` : ''}`]\n },\n}\n"],"names":["figureCount","figureHasColumn","figureRanked","figureRow","figureWithChange","weeklySummaryDay","TRAFFIC_WEEKLY_SUMMARY","order","lines","table","visitors","views","filter","Boolean","join","headline","count","one","many","TOP_PAGES_WEEKLY_SUMMARY","top","SOURCES_WEEKLY_SUMMARY","FORMS_WEEKLY_SUMMARY","total","rows","reduce","sum","row","since","period","from","toLocaleString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SACEA,WAAW,EACXC,eAAe,EACfC,YAAY,EACZC,SAAS,EACTC,gBAAgB,EAChBC,gBAAgB,QACX,wCAAuC;AAE9C;;;;;CAKC,GAED,OAAO,MAAMC,yBAAoD;IAC/DC,OAAO;IACPC,OAAO,CAACC;QACN,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,EAAE;QACtF,OAAO;YACL;gBACED,WAAW,CAAC,UAAU,EAAEN,iBAAiBK,OAAOC,UAAU,WAAW,WAAW,GAAG;gBACnFC,QAAQ,CAAC,YAAY,EAAEP,iBAAiBK,OAAOE,OAAO,WAAW,WAAW,GAAG;aAChF,CACEC,MAAM,CAACC,SACPC,IAAI,CAAC;SACT;IACH;IACAC,UAAU,CAACN;YACiBN;QAA1B,MAAMa,QAAQhB,aAAYG,aAAAA,UAAUM,OAAO,UAAU,gCAA3BN,UAAwC,CAAC,UAAU;QAC7E,OAAOa,QAAQ;YAAEA;YAAOC,KAAK;YAAWC,MAAM;QAAW,IAAI;IAC/D;AACF,EAAC;AAED,OAAO,MAAMC,2BAAsD;IACjEZ,OAAO;IACPC,OAAO,CAACC;QACN,MAAMW,MAAMnB,gBAAgBQ,OAAO,WAAWP,aAAaO,OAAO,QAAQ,SAAS,WAAW;QAC9F,OAAOW,MAAM;YAAC,CAAC,WAAW,EAAEA,KAAK;SAAC,GAAG,EAAE;IACzC;AACF,EAAC;AAED,OAAO,MAAMC,yBAAoD;IAC/Dd,OAAO;IACPC,OAAO,CAACC;QACN,MAAMW,MAAMnB,gBAAgBQ,OAAO,WAAWP,aAAaO,OAAO,UAAU,SAAS,WAAW;QAChG,OAAOW,MAAM;YAAC,CAAC,aAAa,EAAEA,KAAK;SAAC,GAAG,EAAE;IAC3C;AACF,EAAC;AAED,OAAO,MAAME,uBAAkD;IAC7Df,OAAO;IACPC,OAAO,CAACC;QACN,IAAI,CAACR,gBAAgBQ,OAAO,gBAAgB,OAAO,EAAE;QACrD,MAAMc,QAAQd,MAAMe,IAAI,CAACC,MAAM,CAAC,CAACC,KAAKC,MAAQD,MAAM1B,YAAY2B,GAAG,CAAC,cAAc,GAAG;QACrF,IAAI,CAACJ,OAAO,OAAO,EAAE;QACrB,sEAAsE;QACtE,kDAAkD;QAClD,MAAMK,QAAQnB,MAAMoB,MAAM,GAAG,CAAC,OAAO,EAAExB,iBAAiBI,MAAMoB,MAAM,CAACC,IAAI,GAAG,GAAG;QAC/E,MAAMV,MAAMlB,aAAaO,OAAO,QAAQ;QACxC,OAAO;YAAC,CAAC,gBAAgB,EAAEmB,MAAM,EAAE,EAAEL,MAAMQ,cAAc,CAAC,WAAWX,MAAM,CAAC,EAAE,EAAEA,IAAI,CAAC,CAAC,GAAG,IAAI;SAAC;IAChG;AACF,EAAC"}
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { type ResolvedPluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures';
18
18
  import { type AiInsightSurface, type AiInsightTable } from '../model/ai-insight';
19
+ import { type ReleasedFigurePlugins } from '@aglyn/tenant-data-admin/server/figure-readers';
19
20
  import type { AiInsightReadRequest } from '../tools/ai-insight-tool';
20
21
  import type { AiDatasetCatalogEntry } from './ai-dataset-catalog';
21
22
  /**
@@ -30,7 +31,7 @@ import type { AiDatasetCatalogEntry } from './ai-dataset-catalog';
30
31
  * to the job's workspace, and the member's reach, are the job's own checks.
31
32
  */
32
33
  /** Which of a set of plugins are past their release flags for an org; the platform's flags by default. */
33
- export type AiReleasedPlugins = (pluginIds: string[], orgId: string) => Promise<string[]>;
34
+ export type AiReleasedPlugins = ReleasedFigurePlugins;
34
35
  export interface AiInsightReaderContext {
35
36
  orgId: string;
36
37
  /** The site the figures are about; `null` for a workspace-level question. */
@@ -14,35 +14,20 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
- import { isHostPluginEnabled, isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
19
- import { listPluginFigureReaders, normalizePluginFigureTable, pluginFigureParams, pluginFigureReaderPlugin } from "@aglyn/aglyn/plugin-manager/plugin-figures";
17
+ */ import { normalizePluginFigureTable, pluginFigureParams } from "@aglyn/aglyn/plugin-manager/plugin-figures";
20
18
  import { AI_INSIGHT_MAX_READS, AI_INSIGHT_SURFACE_READERS, aiInsightCellText } from "../model/ai-insight.js";
21
- import { filterEnabledPluginsByReleaseFlags } from "@aglyn/tenant-data-admin/server/release-flags";
22
- const releasedByFlags = (pluginIds, orgId)=>filterEnabledPluginsByReleaseFlags(pluginIds, {
23
- orgId,
24
- authorization: null
25
- });
19
+ import { eligibleFigureReaders } from "@aglyn/tenant-data-admin/server/figure-readers";
26
20
  /** The readers a job on this surface may read, ordered by id so a prompt lists them in the same order every time. */ export async function aiInsightReaders(context) {
27
- var _context_released;
28
21
  const prefixes = AI_INSIGHT_SURFACE_READERS[context.surface];
29
- const org = context.org;
30
- const candidates = listPluginFigureReaders().filter(({ reader })=>{
31
- if (!prefixes.includes(reader.id.split('.')[0])) return false;
32
- // A site reader needs a site; a workspace reader reads with or without one.
33
- if (reader.scope === 'site' && !context.hostId) return false;
34
- return !reader.feature || checkEntitlement(org, reader.feature);
35
- });
36
- const plugins = [
37
- ...new Set(candidates.map(pluginFigureReaderPlugin).filter((id)=>Boolean(id)))
38
- ];
39
- const released = new Set(plugins.length ? await ((_context_released = context.released) != null ? _context_released : releasedByFlags)(plugins, context.orgId) : []);
40
- return candidates.filter((resolved)=>{
41
- const plugin = pluginFigureReaderPlugin(resolved);
42
- if (!plugin) return true;
43
- if (!released.has(plugin)) return false;
44
- return context.hostId ? isHostPluginEnabled(org, context.host, plugin) : isPluginEnabled(org, plugin);
45
- });
22
+ return eligibleFigureReaders(_extends({
23
+ orgId: context.orgId,
24
+ hostId: context.hostId,
25
+ org: context.org,
26
+ host: context.host,
27
+ accept: (reader)=>prefixes.includes(reader.id.split('.')[0])
28
+ }, context.released ? {
29
+ released: context.released
30
+ } : {}));
46
31
  }
47
32
  /** The window a reader is read over: the one asked for when it covers it, else the nearest it does. */ export function aiInsightReaderDays(windows, asked) {
48
33
  if (!windows.length) return 0;