@aglyn/plugins-ai 1.0.0-beta.254 → 1.0.0-beta.255

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 (71) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/assist-signals-page.component.js +5 -6
  3. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  4. package/src/lib/components/staff-org-ai-card.component.d.ts +0 -10
  5. package/src/lib/components/staff-org-ai-card.component.js +10 -79
  6. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  7. package/src/lib/components/staff-org-ai-conversations.component.js +8 -41
  8. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -1
  9. package/src/lib/components/staff-user-ai-usage-card.component.js +2 -1
  10. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  11. package/src/lib/jobs/ai-job-page-language.d.ts +14 -1
  12. package/src/lib/jobs/ai-job-page-language.js +18 -3
  13. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  14. package/src/lib/jobs/ai-job-page-step.js +17 -3
  15. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  16. package/src/lib/jobs/ai-job-plan-step.js +4 -0
  17. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  18. package/src/lib/jobs/ai-layout-stock-photos.d.ts +30 -0
  19. package/src/lib/jobs/ai-layout-stock-photos.js +76 -8
  20. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  21. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.d.ts +95 -0
  22. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.js +137 -0
  23. package/src/lib/jobs/fixtures/ai-dental-site-plan-recording.js.map +1 -0
  24. package/src/lib/layout-language/ai-layout-labels.d.ts +2 -0
  25. package/src/lib/layout-language/ai-layout-labels.js +12 -6
  26. package/src/lib/layout-language/ai-layout-labels.js.map +1 -1
  27. package/src/lib/layout-language/ai-layout-pictures.d.ts +12 -3
  28. package/src/lib/layout-language/ai-layout-pictures.js +6 -3
  29. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  30. package/src/lib/model/ai-products-bulk.d.ts +48 -0
  31. package/src/lib/model/ai-products-bulk.js +56 -0
  32. package/src/lib/model/ai-products-bulk.js.map +1 -0
  33. package/src/lib/model/ai-products.d.ts +2 -4
  34. package/src/lib/model/ai-products.js +4 -9
  35. package/src/lib/model/ai-products.js.map +1 -1
  36. package/src/lib/providers/anthropic.js +26 -5
  37. package/src/lib/providers/anthropic.js.map +1 -1
  38. package/src/lib/providers/contract.d.ts +10 -0
  39. package/src/lib/providers/contract.js.map +1 -1
  40. package/src/lib/runtime/ai-doctrine-validators.d.ts +20 -0
  41. package/src/lib/runtime/ai-doctrine-validators.js +50 -0
  42. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  43. package/src/lib/runtime/ai-doctrine.d.ts +11 -0
  44. package/src/lib/runtime/ai-doctrine.js +9 -4
  45. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  46. package/src/lib/runtime/ai-runtime.d.ts +6 -0
  47. package/src/lib/runtime/ai-runtime.js +10 -0
  48. package/src/lib/runtime/ai-runtime.js.map +1 -1
  49. package/src/lib/server/ai-admin-conversations.d.ts +2 -0
  50. package/src/lib/server/ai-admin-conversations.js +40 -7
  51. package/src/lib/server/ai-admin-conversations.js.map +1 -1
  52. package/src/lib/server/ai-admin-org.js +28 -2
  53. package/src/lib/server/ai-admin-org.js.map +1 -1
  54. package/src/lib/server/ai-admin-orgs-spend.js +19 -1
  55. package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
  56. package/src/lib/server/ai-admin-signals.js +11 -3
  57. package/src/lib/server/ai-admin-signals.js.map +1 -1
  58. package/src/lib/server/ai-admin-user.js +11 -1
  59. package/src/lib/server/ai-admin-user.js.map +1 -1
  60. package/src/lib/usage/ai-usage-wire.d.ts +5 -2
  61. package/src/lib/usage/ai-usage-wire.js +5 -2
  62. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  63. package/src/lib/usage/assist-signals-view.d.ts +123 -0
  64. package/src/lib/usage/assist-signals-view.js +151 -0
  65. package/src/lib/usage/assist-signals-view.js.map +1 -0
  66. package/src/lib/usage/staff-org-ai-conversations-view.d.ts +51 -0
  67. package/src/lib/usage/staff-org-ai-conversations-view.js +127 -0
  68. package/src/lib/usage/staff-org-ai-conversations-view.js.map +1 -0
  69. package/src/lib/usage/staff-org-ai-view.d.ts +181 -0
  70. package/src/lib/usage/staff-org-ai-view.js +263 -0
  71. package/src/lib/usage/staff-org-ai-view.js.map +1 -0
@@ -0,0 +1,151 @@
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 { aiAddonName } from "@aglyn/aglyn/app-utils/plan-entitlements";
17
+ import { costSplitRows, freeTasteRefusals, kindTokenRows } from "./assist-signal-mining.js";
18
+ /*
19
+ * THE ASSIST SIGNAL BOARD, AS THE STAFF PAGE PRINTS IT (AGL-3667).
20
+ *
21
+ * `/admin/assist-signals` draws the mining report in the browser: money to the
22
+ * cent (four places under a dollar), shares as whole percents, the cost split
23
+ * by tier, model and kind. The staff apps read the same report through
24
+ * `/api/ai/admin/signals?view=prepared`, which sends each table as the lines
25
+ * the page prints, so a figure or a sentence on the board is decided once.
26
+ */ export const money = (value)=>`$${(Number(value) || 0).toFixed(Math.abs(Number(value) || 0) < 1 ? 4 : 2)}`;
27
+ export const percent = (value)=>value == null ? '\u2014' : `${Math.round(value * 100)}%`;
28
+ /** The line under a ranking, which is a cut and not a page. */ export function rankingFootnote(shown, total, noun) {
29
+ if (!total) return '';
30
+ const counted = `${total.toLocaleString('en-US')} ${total === 1 ? noun.one : noun.many}`;
31
+ return shown >= total ? `Showing all ${counted}.` : `Showing the top ${shown.toLocaleString('en-US')} of ${counted}. This is a ranking, not a page \u2014 what is past the cut is what it ranked lowest.`;
32
+ }
33
+ const count = (value)=>(Number(value) || 0).toLocaleString('en-US');
34
+ export function assistSignalsView(report) {
35
+ var _report_spend, _totals_deflected, _ref, _ref1, _ref2, _ref3, _ref4, _report_docsGaps, _ref5, _ref6, _ref7, _ref8, _ref9, _report_prose, _ref10, _ref11, _report_orgs;
36
+ var _spend_rows, _report_docsGaps1, _report_ranked, _report_ungrounded, _report_ungrounded1, _report_ungrounded_routes, _report_ungrounded2, _report_ranked1, _report_ungrounded3, _report_orgs1, _report_ranked2;
37
+ const totals = report.totals;
38
+ const spend = (_report_spend = report.spend) != null ? _report_spend : null;
39
+ const shareOf = (cost, total)=>percent(total > 0 ? cost / total : null);
40
+ const splitRows = (split)=>costSplitRows(split).map((row)=>{
41
+ var _ref;
42
+ return {
43
+ key: row.key,
44
+ detail: `${count(row.messages)} turns \u00b7 ${money(row.providerCostUsd)} \u00b7 ${shareOf(row.providerCostUsd, (_ref = totals == null ? void 0 : totals.providerCostUsd) != null ? _ref : 0)}`
45
+ };
46
+ });
47
+ const free = report.freeSpend;
48
+ const chips = totals ? [
49
+ `${count(totals.messages)} turns`,
50
+ `${money(totals.providerCostUsd)} estimated`,
51
+ `answered free ${percent(totals.deflectionRate)} (${count((_totals_deflected = totals.deflected) != null ? _totals_deflected : 0)})`,
52
+ `${count(totals.inputTokens)} in / ${count(totals.outputTokens)} out`,
53
+ `cache reads ${percent(totals.cacheReadRate)}`,
54
+ `cache writes ${count(totals.cacheWriteTokens)}`,
55
+ `up ${totals.feedback.up} \u00b7 down ${totals.feedback.down} \u00b7 unrated ${totals.feedback.none}`,
56
+ ...Object.entries(totals.stopReasons).map(([reason, total])=>`${reason}: ${total}`)
57
+ ] : [];
58
+ return {
59
+ truncatedNote: report.truncated ? `Ranked the first ${count(report.scanned)} signals only; there are more. Every number below describes that sample, not the fleet.` : '',
60
+ fleet: {
61
+ chips,
62
+ empty: totals ? '' : 'No assist turns recorded yet.'
63
+ },
64
+ freeSpend: free ? {
65
+ line: `free tier today ${money(free.estCostUsd)} of ${money(free.ceilingUsd)} ceiling \u00b7 ${count(free.requests)} requests` + (free.paused ? ' \u00b7 PAUSED until tomorrow' : ''),
66
+ tone: free.paused ? 'error' : free.alerted ? 'warning' : 'neutral'
67
+ } : null,
68
+ spend: {
69
+ heading: `AI spend this month, by workspace${spend ? ` \u00b7 ${spend.month}` : ''}`,
70
+ note: (spend == null ? void 0 : spend.failed) ? 'Could not read this month\u2019s spend documents: a failed read, not a month in which nobody spent.' : (spend == null ? void 0 : spend.truncated) ? 'Ranked over a sample of the month documents; there are more. This is not the whole fleet.' : '',
71
+ addonName: aiAddonName(),
72
+ rows: ((_ref = spend == null ? void 0 : spend.rows) != null ? _ref : []).map((row)=>{
73
+ var _row_orgLabel;
74
+ return {
75
+ orgId: row.orgId,
76
+ workspace: (_row_orgLabel = row.orgLabel) != null ? _row_orgLabel : row.orgId,
77
+ plan: row.plan,
78
+ addon: row.aiAddon ? 'on' : 'off',
79
+ credits: count(row.credits),
80
+ cost: money(row.providerCostUsd),
81
+ refusals: count(row.refusals.total),
82
+ refusalDetail: row.refusals.total > 0 ? `band ${row.refusals.band} \u00b7 ceiling ${row.refusals.cap} \u00b7 messages ${row.refusals.messages} \u00b7 backstop ${row.refusals.budget}` + (row.refusals.allotment > 0 ? ` \u00b7 allotments ${row.refusals.allotment}` : '') + (freeTasteRefusals(row.refusals) > 0 ? ` \u00b7 free taste ${freeTasteRefusals(row.refusals)}` : '') : ''
83
+ };
84
+ }),
85
+ footnote: rankingFootnote((_ref1 = spend == null ? void 0 : (_spend_rows = spend.rows) == null ? void 0 : _spend_rows.length) != null ? _ref1 : 0, (_ref2 = spend == null ? void 0 : spend.ranked) != null ? _ref2 : 0, {
86
+ one: 'workspace',
87
+ many: 'workspaces'
88
+ })
89
+ },
90
+ split: {
91
+ note: 'The same spend split two ways. By tier answers whether the free cap or the paid price is the lever; by model answers whether a cheaper model on the common path would do, the one lever that never reaches the customer.',
92
+ empty: !totals ? 'No assist turns recorded yet.' : totals.providerCostUsd === 0 ? totals.deflected >= totals.messages ? `Nothing spent on these ${count(totals.messages)} turns: every one was answered from the docs index or the answer cache, with no model call. There is no split to show until a turn reaches a provider.` : `${count(totals.messages - totals.deflected)} of ${count(totals.messages)} turns reached a model and every one priced at $0. That is a gap in the rate table, not a saving: the serving model has no entry in the cost model, so its spend is landing nowhere.` : '',
93
+ tier: totals && totals.providerCostUsd !== 0 ? splitRows(totals.byTier) : [],
94
+ model: totals && totals.providerCostUsd !== 0 ? splitRows(totals.byModel) : []
95
+ },
96
+ kinds: ((totals == null ? void 0 : totals.byKind) ? kindTokenRows(totals.byKind) : []).map((row)=>({
97
+ kind: row.kind,
98
+ headline: `${count(row.messages)} requests \u00b7 ${money(row.messages ? row.providerCostUsd / row.messages : 0)} per request \u00b7 cache hit ${percent(row.cacheHitRate)}`,
99
+ detail: `in ${count(row.inputTokens)} \u00b7 cache reads ${count(row.cacheReadTokens)} \u00b7 cache writes ${count(row.cacheWriteTokens)} \u00b7 out ${count(row.outputTokens)} \u00b7 p95 out ${count(row.outputP95)}`
100
+ })),
101
+ docsGaps: {
102
+ footnote: rankingFootnote((_ref3 = (_report_docsGaps1 = report.docsGaps) == null ? void 0 : _report_docsGaps1.length) != null ? _ref3 : 0, (_ref4 = (_report_ranked = report.ranked) == null ? void 0 : _report_ranked.docsGaps) != null ? _ref4 : 0, {
103
+ one: 'cited page',
104
+ many: 'cited pages'
105
+ }),
106
+ rows: ((_report_docsGaps = report.docsGaps) != null ? _report_docsGaps : []).map((row)=>({
107
+ path: row.path,
108
+ headline: `${row.questions} questions \u00b7 down rate ${percent(row.downRate)} \u00b7 ${row.orgs} workspaces`,
109
+ detail: `up ${row.up} \u00b7 down ${row.down} \u00b7 cost ${money(row.providerCostUsd)}`
110
+ }))
111
+ },
112
+ ungrounded: {
113
+ note: `${count((_ref5 = (_report_ungrounded = report.ungrounded) == null ? void 0 : _report_ungrounded.questions) != null ? _ref5 : 0)} turns matched no documentation at all, ${(_ref6 = (_report_ungrounded1 = report.ungrounded) == null ? void 0 : _report_ungrounded1.down) != null ? _ref6 : 0} of them rated down. These cite no page, so they cannot appear in the ranking above: a missing page is invisible to anything keyed on paths.`,
114
+ footnote: rankingFootnote((_ref7 = (_report_ungrounded2 = report.ungrounded) == null ? void 0 : (_report_ungrounded_routes = _report_ungrounded2.routes) == null ? void 0 : _report_ungrounded_routes.length) != null ? _ref7 : 0, (_ref8 = (_report_ranked1 = report.ranked) == null ? void 0 : _report_ranked1.ungroundedRoutes) != null ? _ref8 : 0, {
115
+ one: 'console page',
116
+ many: 'console pages'
117
+ }),
118
+ rows: ((_ref9 = (_report_ungrounded3 = report.ungrounded) == null ? void 0 : _report_ungrounded3.routes) != null ? _ref9 : []).map((row)=>({
119
+ route: row.route,
120
+ detail: `${row.questions} questions \u00b7 ${row.down} rated down`
121
+ }))
122
+ },
123
+ prose: ((_report_prose = report.prose) != null ? _report_prose : []).map((row)=>{
124
+ var _row_orgLabel;
125
+ return {
126
+ flag: row.feedback === 'down' ? 'rated down' : 'no documentation matched',
127
+ flagTone: row.feedback === 'down' ? 'error' : 'neutral',
128
+ where: `${row.route} \u00b7 ${(_row_orgLabel = row.orgLabel) != null ? _row_orgLabel : row.orgId}`,
129
+ text: row.expired ? 'The words are past their 180-day retention and have been deleted. The counts still include this turn.' : row.question || '(no question recorded)',
130
+ answer: !row.expired && row.answer ? `We said: ${row.answer}${row.answerTruncated ? '\u2026' : ''}` : ''
131
+ };
132
+ }),
133
+ orgs: {
134
+ footnote: rankingFootnote((_ref10 = (_report_orgs1 = report.orgs) == null ? void 0 : _report_orgs1.length) != null ? _ref10 : 0, (_ref11 = (_report_ranked2 = report.ranked) == null ? void 0 : _report_ranked2.orgs) != null ? _ref11 : 0, {
135
+ one: 'workspace',
136
+ many: 'workspaces'
137
+ }),
138
+ rows: ((_report_orgs = report.orgs) != null ? _report_orgs : []).map((row)=>{
139
+ var _row_orgLabel;
140
+ return {
141
+ orgId: row.orgId,
142
+ workspace: (_row_orgLabel = row.orgLabel) != null ? _row_orgLabel : row.orgId,
143
+ headline: `${count(row.messages)} turns \u00b7 cost ${money(row.providerCostUsd)} \u00b7 down ${row.down}`,
144
+ detail: `in ${count(row.inputTokens)} \u00b7 out ${count(row.outputTokens)} \u00b7 cache reads ${count(row.cacheReadTokens)}`
145
+ };
146
+ })
147
+ }
148
+ };
149
+ }
150
+
151
+ //# sourceMappingURL=assist-signals-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/assist-signals-view.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 { aiAddonName } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n costSplitRows,\n freeTasteRefusals,\n kindTokenRows,\n type AssistMiningReport,\n type AssistSpendRow,\n} from './assist-signal-mining'\n\n/*\n * THE ASSIST SIGNAL BOARD, AS THE STAFF PAGE PRINTS IT (AGL-3667).\n *\n * `/admin/assist-signals` draws the mining report in the browser: money to the\n * cent (four places under a dollar), shares as whole percents, the cost split\n * by tier, model and kind. The staff apps read the same report through\n * `/api/ai/admin/signals?view=prepared`, which sends each table as the lines\n * the page prints, so a figure or a sentence on the board is decided once.\n */\n\nexport const money = (value: number): string =>\n `$${(Number(value) || 0).toFixed(Math.abs(Number(value) || 0) < 1 ? 4 : 2)}`\n\nexport const percent = (value: number | null): string => (value == null ? '\\u2014' : `${Math.round(value * 100)}%`)\n\n/** The line under a ranking, which is a cut and not a page. */\nexport function rankingFootnote(shown: number, total: number, noun: { one: string; many: string }): string {\n if (!total) return ''\n const counted = `${total.toLocaleString('en-US')} ${total === 1 ? noun.one : noun.many}`\n return shown >= total\n ? `Showing all ${counted}.`\n : `Showing the top ${shown.toLocaleString('en-US')} of ${counted}. This is a ranking, not a page \\u2014 what is past the cut is what it ranked lowest.`\n}\n\ntype SpendBoard = {\n month: string\n rows: AssistSpendRow[]\n ranked: number\n truncated: boolean\n failed: boolean\n}\n\ntype ProseRow = {\n orgId: string\n orgLabel?: string | null\n exchangeId: string\n route: string\n feedback: 'up' | 'down' | null\n expired?: boolean\n question?: string | null\n answer?: string | null\n answerTruncated?: boolean\n}\n\nexport interface AssistSignalsView {\n truncatedNote: string\n fleet: { chips: string[]; empty: string }\n freeSpend: { line: string; tone: 'error' | 'warning' | 'neutral' } | null\n spend: {\n heading: string\n note: string\n addonName: string\n rows: Array<{ orgId: string; workspace: string; plan: string; addon: string; credits: string; cost: string; refusals: string; refusalDetail: string }>\n footnote: string\n }\n split: {\n note: string\n empty: string\n tier: Array<{ key: string; detail: string }>\n model: Array<{ key: string; detail: string }>\n }\n kinds: Array<{ kind: string; headline: string; detail: string }>\n docsGaps: { footnote: string; rows: Array<{ path: string; headline: string; detail: string }> }\n ungrounded: { note: string; footnote: string; rows: Array<{ route: string; detail: string }> }\n prose: Array<{ flag: string; flagTone: string; where: string; text: string; answer: string }>\n orgs: { footnote: string; rows: Array<{ orgId: string; workspace: string; headline: string; detail: string }> }\n}\n\nconst count = (value: number) => (Number(value) || 0).toLocaleString('en-US')\n\nexport function assistSignalsView(report: AssistMiningReport & { spend?: SpendBoard | null; prose?: ProseRow[] }): AssistSignalsView {\n const totals = report.totals\n const spend = report.spend ?? null\n const shareOf = (cost: number, total: number) => percent(total > 0 ? cost / total : null)\n const splitRows = (split: Record<string, { messages: number; providerCostUsd: number }>) =>\n costSplitRows(split as never).map((row) => ({\n key: row.key,\n detail: `${count(row.messages)} turns \\u00b7 ${money(row.providerCostUsd)} \\u00b7 ${shareOf(row.providerCostUsd, totals?.providerCostUsd ?? 0)}`,\n }))\n const free = report.freeSpend\n const chips = totals\n ? [\n `${count(totals.messages)} turns`,\n `${money(totals.providerCostUsd)} estimated`,\n `answered free ${percent(totals.deflectionRate)} (${count(totals.deflected ?? 0)})`,\n `${count(totals.inputTokens)} in / ${count(totals.outputTokens)} out`,\n `cache reads ${percent(totals.cacheReadRate)}`,\n `cache writes ${count(totals.cacheWriteTokens)}`,\n `up ${totals.feedback.up} \\u00b7 down ${totals.feedback.down} \\u00b7 unrated ${totals.feedback.none}`,\n ...Object.entries(totals.stopReasons).map(([reason, total]) => `${reason}: ${total}`),\n ]\n : []\n return {\n truncatedNote: report.truncated\n ? `Ranked the first ${count(report.scanned)} signals only; there are more. Every number below describes that sample, not the fleet.`\n : '',\n fleet: { chips, empty: totals ? '' : 'No assist turns recorded yet.' },\n freeSpend: free\n ? {\n line:\n `free tier today ${money(free.estCostUsd)} of ${money(free.ceilingUsd)} ceiling \\u00b7 ${count(free.requests)} requests` +\n (free.paused ? ' \\u00b7 PAUSED until tomorrow' : ''),\n tone: free.paused ? 'error' : free.alerted ? 'warning' : 'neutral',\n }\n : null,\n spend: {\n heading: `AI spend this month, by workspace${spend ? ` \\u00b7 ${spend.month}` : ''}`,\n note: spend?.failed\n ? 'Could not read this month\\u2019s spend documents: a failed read, not a month in which nobody spent.'\n : spend?.truncated\n ? 'Ranked over a sample of the month documents; there are more. This is not the whole fleet.'\n : '',\n addonName: aiAddonName(),\n rows: (spend?.rows ?? []).map((row) => ({\n orgId: row.orgId,\n workspace: row.orgLabel ?? row.orgId,\n plan: row.plan,\n addon: row.aiAddon ? 'on' : 'off',\n credits: count(row.credits),\n cost: money(row.providerCostUsd),\n refusals: count(row.refusals.total),\n refusalDetail:\n row.refusals.total > 0\n ? `band ${row.refusals.band} \\u00b7 ceiling ${row.refusals.cap} \\u00b7 messages ${row.refusals.messages} \\u00b7 backstop ${row.refusals.budget}` +\n (row.refusals.allotment > 0 ? ` \\u00b7 allotments ${row.refusals.allotment}` : '') +\n (freeTasteRefusals(row.refusals) > 0 ? ` \\u00b7 free taste ${freeTasteRefusals(row.refusals)}` : '')\n : '',\n })),\n footnote: rankingFootnote(spend?.rows?.length ?? 0, spend?.ranked ?? 0, { one: 'workspace', many: 'workspaces' }),\n },\n split: {\n note: 'The same spend split two ways. By tier answers whether the free cap or the paid price is the lever; by model answers whether a cheaper model on the common path would do, the one lever that never reaches the customer.',\n empty: !totals\n ? 'No assist turns recorded yet.'\n : totals.providerCostUsd === 0\n ? totals.deflected >= totals.messages\n ? `Nothing spent on these ${count(totals.messages)} turns: every one was answered from the docs index or the answer cache, with no model call. There is no split to show until a turn reaches a provider.`\n : `${count(totals.messages - totals.deflected)} of ${count(totals.messages)} turns reached a model and every one priced at $0. That is a gap in the rate table, not a saving: the serving model has no entry in the cost model, so its spend is landing nowhere.`\n : '',\n tier: totals && totals.providerCostUsd !== 0 ? splitRows(totals.byTier) : [],\n model: totals && totals.providerCostUsd !== 0 ? splitRows(totals.byModel) : [],\n },\n kinds: (totals?.byKind ? kindTokenRows(totals.byKind) : []).map((row) => ({\n kind: row.kind,\n headline: `${count(row.messages)} requests \\u00b7 ${money(row.messages ? row.providerCostUsd / row.messages : 0)} per request \\u00b7 cache hit ${percent(row.cacheHitRate)}`,\n detail: `in ${count(row.inputTokens)} \\u00b7 cache reads ${count(row.cacheReadTokens)} \\u00b7 cache writes ${count(row.cacheWriteTokens)} \\u00b7 out ${count(row.outputTokens)} \\u00b7 p95 out ${count(row.outputP95)}`,\n })),\n docsGaps: {\n footnote: rankingFootnote(report.docsGaps?.length ?? 0, report.ranked?.docsGaps ?? 0, { one: 'cited page', many: 'cited pages' }),\n rows: (report.docsGaps ?? []).map((row) => ({\n path: row.path,\n headline: `${row.questions} questions \\u00b7 down rate ${percent(row.downRate)} \\u00b7 ${row.orgs} workspaces`,\n detail: `up ${row.up} \\u00b7 down ${row.down} \\u00b7 cost ${money(row.providerCostUsd)}`,\n })),\n },\n ungrounded: {\n note: `${count(report.ungrounded?.questions ?? 0)} turns matched no documentation at all, ${report.ungrounded?.down ?? 0} of them rated down. These cite no page, so they cannot appear in the ranking above: a missing page is invisible to anything keyed on paths.`,\n footnote: rankingFootnote(report.ungrounded?.routes?.length ?? 0, report.ranked?.ungroundedRoutes ?? 0, { one: 'console page', many: 'console pages' }),\n rows: (report.ungrounded?.routes ?? []).map((row) => ({ route: row.route, detail: `${row.questions} questions \\u00b7 ${row.down} rated down` })),\n },\n prose: (report.prose ?? []).map((row) => ({\n flag: row.feedback === 'down' ? 'rated down' : 'no documentation matched',\n flagTone: row.feedback === 'down' ? 'error' : 'neutral',\n where: `${row.route} \\u00b7 ${row.orgLabel ?? row.orgId}`,\n text: row.expired\n ? 'The words are past their 180-day retention and have been deleted. The counts still include this turn.'\n : row.question || '(no question recorded)',\n answer: !row.expired && row.answer ? `We said: ${row.answer}${row.answerTruncated ? '\\u2026' : ''}` : '',\n })),\n orgs: {\n footnote: rankingFootnote(report.orgs?.length ?? 0, report.ranked?.orgs ?? 0, { one: 'workspace', many: 'workspaces' }),\n rows: (report.orgs ?? []).map((row) => ({\n orgId: row.orgId,\n workspace: row.orgLabel ?? row.orgId,\n headline: `${count(row.messages)} turns \\u00b7 cost ${money(row.providerCostUsd)} \\u00b7 down ${row.down}`,\n detail: `in ${count(row.inputTokens)} \\u00b7 out ${count(row.outputTokens)} \\u00b7 cache reads ${count(row.cacheReadTokens)}`,\n })),\n },\n }\n}\n"],"names":["aiAddonName","costSplitRows","freeTasteRefusals","kindTokenRows","money","value","Number","toFixed","Math","abs","percent","round","rankingFootnote","shown","total","noun","counted","toLocaleString","one","many","count","assistSignalsView","report","totals","spend","shareOf","cost","splitRows","split","map","row","key","detail","messages","providerCostUsd","free","freeSpend","chips","deflectionRate","deflected","inputTokens","outputTokens","cacheReadRate","cacheWriteTokens","feedback","up","down","none","Object","entries","stopReasons","reason","truncatedNote","truncated","scanned","fleet","empty","line","estCostUsd","ceilingUsd","requests","paused","tone","alerted","heading","month","note","failed","addonName","rows","orgId","workspace","orgLabel","plan","addon","aiAddon","credits","refusals","refusalDetail","band","cap","budget","allotment","footnote","length","ranked","tier","byTier","model","byModel","kinds","byKind","kind","headline","cacheHitRate","cacheReadTokens","outputP95","docsGaps","path","questions","downRate","orgs","ungrounded","routes","ungroundedRoutes","route","prose","flag","flagTone","where","text","expired","question","answer","answerTruncated"],"mappings":"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"}
@@ -0,0 +1,51 @@
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 { StaffAiAssistRow, StaffAiConversationPerson, StaffAiJobResult, StaffAiJobRow } from './staff-org-ai-conversations';
18
+ export declare const who: (person: StaffAiConversationPerson) => string;
19
+ export declare const money: (value: number | null) => string | null;
20
+ /** What a job kept beside it, as the label above it and the words under it. */
21
+ export declare function jobResultText(result: StaffAiJobResult): {
22
+ label: string;
23
+ text: string;
24
+ };
25
+ /** The chips under an Assist turn: where it was asked, the model, the cost, the thumbs. */
26
+ export declare function assistChips(row: StaffAiAssistRow): string[];
27
+ /** The chips under a job: its kind, its status, what it spent. */
28
+ export declare function jobChips(row: StaffAiJobRow): string[];
29
+ /** What a job made, one line each. */
30
+ export declare function jobMadeText(row: StaffAiJobRow): string;
31
+ /** The fields filled in beside a brief, one line each. */
32
+ export declare function jobFilledInText(row: StaffAiJobRow): string;
33
+ export interface StaffAiRowView {
34
+ who: string;
35
+ /** The chips joined into the one line under a list row. */
36
+ meta: string;
37
+ chips: string[];
38
+ /** Empty for an Assist turn and for a job with nothing filled in. */
39
+ filledIn: string;
40
+ resultLabel: string;
41
+ /** Empty when the kind keeps nothing beside the job, or the result has expired. */
42
+ resultText: string;
43
+ made: string;
44
+ }
45
+ export declare function staffAiAssistRowView(row: StaffAiAssistRow): StaffAiRowView;
46
+ export declare function staffAiJobRowView(row: StaffAiJobRow): StaffAiRowView;
47
+ /** A page with each row's `view` beside it. */
48
+ export declare function withRowViews<T extends {
49
+ kind: 'assist' | 'jobs';
50
+ rows: StaffAiAssistRow[] | StaffAiJobRow[];
51
+ }>(page: T): T;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ /*
18
+ * THE AI REQUESTS CARD, AS IT PRINTS (AGL-3667).
19
+ *
20
+ * The staff card lists what people typed to Aglyn AI and what came back. The
21
+ * words themselves are the rows' own; what the card adds is who asked (an
22
+ * email, else a name, else the uid), the chips under each row (the route and
23
+ * model and cost of an Assist turn, the kind and status and credits of a job),
24
+ * the fields filled in beside a brief, the result a job kept, and what it made.
25
+ * The staff apps read the same rows through
26
+ * `/api/ai/admin/conversations?view=prepared`, which sends each row with that
27
+ * `view`, so those lines are decided once, and the card imports them too.
28
+ */ export const who = (person)=>{
29
+ var _ref, _person_email;
30
+ return (_ref = (_person_email = person.email) != null ? _person_email : person.name) != null ? _ref : person.uid ? `uid ${person.uid}` : 'Unknown member';
31
+ };
32
+ export const money = (value)=>value === null ? null : `$${value.toFixed(value < 1 ? 4 : 2)}`;
33
+ /** What a job kept beside it, as the label above it and the words under it. */ export function jobResultText(result) {
34
+ switch(result.kind){
35
+ case 'insight':
36
+ return {
37
+ label: 'Answer',
38
+ text: [
39
+ ...result.insights,
40
+ ...result.gap ? [
41
+ `Not answered: ${result.gap}`
42
+ ] : []
43
+ ].join('\n\n')
44
+ };
45
+ case 'crm-record':
46
+ return {
47
+ label: 'Summary',
48
+ text: [
49
+ result.summary,
50
+ result.standing,
51
+ result.nextStep ? `Next step: ${result.nextStep}` : null
52
+ ].filter(Boolean).join('\n\n')
53
+ };
54
+ case 'crm-email':
55
+ return {
56
+ label: 'Email draft',
57
+ text: `Subject: ${result.subject}\n\n${result.body}`
58
+ };
59
+ case 'crm-mapping':
60
+ return {
61
+ label: 'Column matches',
62
+ text: result.matches.join('\n')
63
+ };
64
+ }
65
+ }
66
+ /** The chips under an Assist turn: where it was asked, the model, the cost, the thumbs. */ export function assistChips(row) {
67
+ return [
68
+ row.route,
69
+ row.model,
70
+ money(row.estCostUsd),
71
+ row.feedback ? `Rated ${row.feedback}` : null
72
+ ].filter((chip)=>Boolean(chip));
73
+ }
74
+ /** The chips under a job: its kind, its status, what it spent. */ export function jobChips(row) {
75
+ return [
76
+ row.kind,
77
+ row.status,
78
+ `${row.credits.toLocaleString('en-US')} credits`
79
+ ];
80
+ }
81
+ /** What a job made, one line each. */ export function jobMadeText(row) {
82
+ return row.outputs.map((output)=>[
83
+ output.label,
84
+ output.note
85
+ ].filter(Boolean).join(' — ')).join('\n');
86
+ }
87
+ /** The fields filled in beside a brief, one line each. */ export function jobFilledInText(row) {
88
+ var _row_inputs;
89
+ return ((_row_inputs = row.inputs) != null ? _row_inputs : []).map((input)=>`${input.label}: ${input.value}`).join('\n');
90
+ }
91
+ export function staffAiAssistRowView(row) {
92
+ const chips = assistChips(row);
93
+ return {
94
+ who: who(row.by),
95
+ meta: chips.join(' · '),
96
+ chips,
97
+ filledIn: '',
98
+ resultLabel: '',
99
+ resultText: '',
100
+ made: ''
101
+ };
102
+ }
103
+ export function staffAiJobRowView(row) {
104
+ var _ref, _ref1;
105
+ const chips = jobChips(row);
106
+ const result = row.result ? jobResultText(row.result) : null;
107
+ return {
108
+ who: who(row.by),
109
+ meta: chips.join(' · '),
110
+ chips,
111
+ filledIn: jobFilledInText(row),
112
+ resultLabel: (_ref = result == null ? void 0 : result.label) != null ? _ref : '',
113
+ resultText: (_ref1 = result == null ? void 0 : result.text) != null ? _ref1 : '',
114
+ made: jobMadeText(row)
115
+ };
116
+ }
117
+ /** A page with each row's `view` beside it. */ export function withRowViews(page) {
118
+ return _extends({}, page, {
119
+ rows: page.kind === 'assist' ? page.rows.map((row)=>_extends({}, row, {
120
+ view: staffAiAssistRowView(row)
121
+ })) : page.rows.map((row)=>_extends({}, row, {
122
+ view: staffAiJobRowView(row)
123
+ }))
124
+ });
125
+ }
126
+
127
+ //# sourceMappingURL=staff-org-ai-conversations-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/staff-org-ai-conversations-view.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 {\n StaffAiAssistRow,\n StaffAiConversationPerson,\n StaffAiJobResult,\n StaffAiJobRow,\n} from './staff-org-ai-conversations'\n\n/*\n * THE AI REQUESTS CARD, AS IT PRINTS (AGL-3667).\n *\n * The staff card lists what people typed to Aglyn AI and what came back. The\n * words themselves are the rows' own; what the card adds is who asked (an\n * email, else a name, else the uid), the chips under each row (the route and\n * model and cost of an Assist turn, the kind and status and credits of a job),\n * the fields filled in beside a brief, the result a job kept, and what it made.\n * The staff apps read the same rows through\n * `/api/ai/admin/conversations?view=prepared`, which sends each row with that\n * `view`, so those lines are decided once, and the card imports them too.\n */\n\nexport const who = (person: StaffAiConversationPerson): string =>\n person.email ?? person.name ?? (person.uid ? `uid ${person.uid}` : 'Unknown member')\n\nexport const money = (value: number | null): string | null =>\n value === null ? null : `$${value.toFixed(value < 1 ? 4 : 2)}`\n\n/** What a job kept beside it, as the label above it and the words under it. */\nexport function jobResultText(result: StaffAiJobResult): { label: string; text: string } {\n switch (result.kind) {\n case 'insight':\n return {\n label: 'Answer',\n text: [...result.insights, ...(result.gap ? [`Not answered: ${result.gap}`] : [])].join('\\n\\n'),\n }\n case 'crm-record':\n return {\n label: 'Summary',\n text: [result.summary, result.standing, result.nextStep ? `Next step: ${result.nextStep}` : null]\n .filter(Boolean)\n .join('\\n\\n'),\n }\n case 'crm-email':\n return { label: 'Email draft', text: `Subject: ${result.subject}\\n\\n${result.body}` }\n case 'crm-mapping':\n return { label: 'Column matches', text: result.matches.join('\\n') }\n }\n}\n\n/** The chips under an Assist turn: where it was asked, the model, the cost, the thumbs. */\nexport function assistChips(row: StaffAiAssistRow): string[] {\n return [row.route, row.model, money(row.estCostUsd), row.feedback ? `Rated ${row.feedback}` : null].filter(\n (chip): chip is string => Boolean(chip),\n )\n}\n\n/** The chips under a job: its kind, its status, what it spent. */\nexport function jobChips(row: StaffAiJobRow): string[] {\n return [row.kind, row.status, `${row.credits.toLocaleString('en-US')} credits`]\n}\n\n/** What a job made, one line each. */\nexport function jobMadeText(row: StaffAiJobRow): string {\n return row.outputs.map((output) => [output.label, output.note].filter(Boolean).join(' — ')).join('\\n')\n}\n\n/** The fields filled in beside a brief, one line each. */\nexport function jobFilledInText(row: StaffAiJobRow): string {\n return (row.inputs ?? []).map((input) => `${input.label}: ${input.value}`).join('\\n')\n}\n\nexport interface StaffAiRowView {\n who: string\n /** The chips joined into the one line under a list row. */\n meta: string\n chips: string[]\n /** Empty for an Assist turn and for a job with nothing filled in. */\n filledIn: string\n resultLabel: string\n /** Empty when the kind keeps nothing beside the job, or the result has expired. */\n resultText: string\n made: string\n}\n\nexport function staffAiAssistRowView(row: StaffAiAssistRow): StaffAiRowView {\n const chips = assistChips(row)\n return { who: who(row.by), meta: chips.join(' · '), chips, filledIn: '', resultLabel: '', resultText: '', made: '' }\n}\n\nexport function staffAiJobRowView(row: StaffAiJobRow): StaffAiRowView {\n const chips = jobChips(row)\n const result = row.result ? jobResultText(row.result) : null\n return {\n who: who(row.by),\n meta: chips.join(' · '),\n chips,\n filledIn: jobFilledInText(row),\n resultLabel: result?.label ?? '',\n resultText: result?.text ?? '',\n made: jobMadeText(row),\n }\n}\n\n/** A page with each row's `view` beside it. */\nexport function withRowViews<T extends { kind: 'assist' | 'jobs'; rows: StaffAiAssistRow[] | StaffAiJobRow[] }>(\n page: T,\n): T {\n return {\n ...page,\n rows:\n page.kind === 'assist'\n ? (page.rows as StaffAiAssistRow[]).map((row) => ({ ...row, view: staffAiAssistRowView(row) }))\n : (page.rows as StaffAiJobRow[]).map((row) => ({ ...row, view: staffAiJobRowView(row) })),\n }\n}\n"],"names":["who","person","email","name","uid","money","value","toFixed","jobResultText","result","kind","label","text","insights","gap","join","summary","standing","nextStep","filter","Boolean","subject","body","matches","assistChips","row","route","model","estCostUsd","feedback","chip","jobChips","status","credits","toLocaleString","jobMadeText","outputs","map","output","note","jobFilledInText","inputs","input","staffAiAssistRowView","chips","by","meta","filledIn","resultLabel","resultText","made","staffAiJobRowView","withRowViews","page","rows","view"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AASD;;;;;;;;;;;CAWC,GAED,OAAO,MAAMA,MAAM,CAACC;QAClBA,MAAAA;YAAAA,QAAAA,gBAAAA,OAAOC,KAAK,YAAZD,gBAAgBA,OAAOE,IAAI,YAA3BF,OAAgCA,OAAOG,GAAG,GAAG,CAAC,IAAI,EAAEH,OAAOG,GAAG,EAAE,GAAG;EAAiB;AAEtF,OAAO,MAAMC,QAAQ,CAACC,QACpBA,UAAU,OAAO,OAAO,CAAC,CAAC,EAAEA,MAAMC,OAAO,CAACD,QAAQ,IAAI,IAAI,IAAI,CAAA;AAEhE,6EAA6E,GAC7E,OAAO,SAASE,cAAcC,MAAwB;IACpD,OAAQA,OAAOC,IAAI;QACjB,KAAK;YACH,OAAO;gBACLC,OAAO;gBACPC,MAAM;uBAAIH,OAAOI,QAAQ;uBAAMJ,OAAOK,GAAG,GAAG;wBAAC,CAAC,cAAc,EAAEL,OAAOK,GAAG,EAAE;qBAAC,GAAG,EAAE;iBAAE,CAACC,IAAI,CAAC;YAC1F;QACF,KAAK;YACH,OAAO;gBACLJ,OAAO;gBACPC,MAAM;oBAACH,OAAOO,OAAO;oBAAEP,OAAOQ,QAAQ;oBAAER,OAAOS,QAAQ,GAAG,CAAC,WAAW,EAAET,OAAOS,QAAQ,EAAE,GAAG;iBAAK,CAC9FC,MAAM,CAACC,SACPL,IAAI,CAAC;YACV;QACF,KAAK;YACH,OAAO;gBAAEJ,OAAO;gBAAeC,MAAM,CAAC,SAAS,EAAEH,OAAOY,OAAO,CAAC,IAAI,EAAEZ,OAAOa,IAAI,EAAE;YAAC;QACtF,KAAK;YACH,OAAO;gBAAEX,OAAO;gBAAkBC,MAAMH,OAAOc,OAAO,CAACR,IAAI,CAAC;YAAM;IACtE;AACF;AAEA,yFAAyF,GACzF,OAAO,SAASS,YAAYC,GAAqB;IAC/C,OAAO;QAACA,IAAIC,KAAK;QAAED,IAAIE,KAAK;QAAEtB,MAAMoB,IAAIG,UAAU;QAAGH,IAAII,QAAQ,GAAG,CAAC,MAAM,EAAEJ,IAAII,QAAQ,EAAE,GAAG;KAAK,CAACV,MAAM,CACxG,CAACW,OAAyBV,QAAQU;AAEtC;AAEA,gEAAgE,GAChE,OAAO,SAASC,SAASN,GAAkB;IACzC,OAAO;QAACA,IAAIf,IAAI;QAAEe,IAAIO,MAAM;QAAE,GAAGP,IAAIQ,OAAO,CAACC,cAAc,CAAC,SAAS,QAAQ,CAAC;KAAC;AACjF;AAEA,oCAAoC,GACpC,OAAO,SAASC,YAAYV,GAAkB;IAC5C,OAAOA,IAAIW,OAAO,CAACC,GAAG,CAAC,CAACC,SAAW;YAACA,OAAO3B,KAAK;YAAE2B,OAAOC,IAAI;SAAC,CAACpB,MAAM,CAACC,SAASL,IAAI,CAAC,QAAQA,IAAI,CAAC;AACnG;AAEA,wDAAwD,GACxD,OAAO,SAASyB,gBAAgBf,GAAkB;QACxCA;IAAR,OAAO,EAACA,cAAAA,IAAIgB,MAAM,YAAVhB,cAAc,EAAE,EAAEY,GAAG,CAAC,CAACK,QAAU,GAAGA,MAAM/B,KAAK,CAAC,EAAE,EAAE+B,MAAMpC,KAAK,EAAE,EAAES,IAAI,CAAC;AAClF;AAeA,OAAO,SAAS4B,qBAAqBlB,GAAqB;IACxD,MAAMmB,QAAQpB,YAAYC;IAC1B,OAAO;QAAEzB,KAAKA,IAAIyB,IAAIoB,EAAE;QAAGC,MAAMF,MAAM7B,IAAI,CAAC;QAAQ6B;QAAOG,UAAU;QAAIC,aAAa;QAAIC,YAAY;QAAIC,MAAM;IAAG;AACrH;AAEA,OAAO,SAASC,kBAAkB1B,GAAkB;;IAClD,MAAMmB,QAAQb,SAASN;IACvB,MAAMhB,SAASgB,IAAIhB,MAAM,GAAGD,cAAciB,IAAIhB,MAAM,IAAI;IACxD,OAAO;QACLT,KAAKA,IAAIyB,IAAIoB,EAAE;QACfC,MAAMF,MAAM7B,IAAI,CAAC;QACjB6B;QACAG,UAAUP,gBAAgBf;QAC1BuB,WAAW,UAAEvC,0BAAAA,OAAQE,KAAK,mBAAI;QAC9BsC,UAAU,WAAExC,0BAAAA,OAAQG,IAAI,oBAAI;QAC5BsC,MAAMf,YAAYV;IACpB;AACF;AAEA,6CAA6C,GAC7C,OAAO,SAAS2B,aACdC,IAAO;IAEP,OAAO,aACFA;QACHC,MACED,KAAK3C,IAAI,KAAK,WACV,AAAC2C,KAAKC,IAAI,CAAwBjB,GAAG,CAAC,CAACZ,MAAS,aAAKA;gBAAK8B,MAAMZ,qBAAqBlB;kBACrF,AAAC4B,KAAKC,IAAI,CAAqBjB,GAAG,CAAC,CAACZ,MAAS,aAAKA;gBAAK8B,MAAMJ,kBAAkB1B;;;AAEzF"}
@@ -0,0 +1,181 @@
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 StaffAssistPool, type StaffOrgUsageAiMonth } from './staff-org-usage-ai';
18
+ import type { StaffAiAccountAllowance, StaffOrgAiMargin, StaffOrgAiOverage, StaffOrgAiPool, StaffOrgAiRefusals, StaffOrgAiResponse } from './staff-org-ai';
19
+ export declare const credits: (value: number | null | undefined) => string;
20
+ /**
21
+ * Four decimals under a dollar, two above (the assist-signals page's rule):
22
+ * a month that really cost eight cents must not read as `$0.00`.
23
+ */
24
+ export declare const usd: (value: number | null | undefined) => string;
25
+ export declare const tokenCount: (value: number) => string;
26
+ export declare const rate: (value: number | null) => string;
27
+ /** The pool as one sentence naming every part that is non-zero. */
28
+ export declare function poolPartsSentence(pool: StaffOrgAiPool, addonName: string): string;
29
+ /**
30
+ * The overage's state as one line: sold, walled, or capped — or, for an
31
+ * uncapped staff comp (AGL-3049), nothing to sell past at all.
32
+ */
33
+ export declare function overageStateSentence(overage: StaffOrgAiOverage, uncapped?: boolean): string;
34
+ /** Whether the margin section renders red. */
35
+ export declare function marginTone(margin: StaffOrgAiMargin): 'error' | 'warning' | 'success';
36
+ /** The words for each reason the gate refused, in the order the card lists them. */
37
+ export declare const REFUSAL_LABELS: Record<keyof Omit<StaffOrgAiRefusals, 'total'>, string>;
38
+ /**
39
+ * Each reason as the chip the card draws: `band 3`. A reason the answering
40
+ * server does not yet count reads as zero.
41
+ */
42
+ export declare function refusalChips(refusals: StaffOrgAiRefusals): Array<{
43
+ label: string;
44
+ count: number;
45
+ }>;
46
+ /** The statuses the jobs row names, and the ones that turn it amber or red. */
47
+ export declare const JOB_CHIP_STATUSES: readonly ["queued", "running", "needs_input", "needs_review", "failed"];
48
+ /** What the add-on row says about when it was bought. */
49
+ export declare function addonSinceNote(addon: StaffOrgAiResponse['addon']): string;
50
+ /** Which meters a give-back may go to, as the dialog offers them, and the one it starts on. */
51
+ export declare function creditReturnMeters(hasWorkspace: boolean, hasAccount: boolean): Array<{
52
+ value: 'both' | 'workspace' | 'account';
53
+ label: string;
54
+ }>;
55
+ /** The most one give-back may return to the meter(s) chosen: the lower of the two for both. */
56
+ export declare function creditReturnable(meter: 'both' | 'workspace' | 'account', workspaceUsed: number | null, accountUsed: number | null): number;
57
+ /** The sentence under the give-back form: what each meter used, which is all it can return. */
58
+ export declare function creditReturnSummary(workspaceUsed: number | null, accountUsed: number | null): string;
59
+ /** One line of the allowance, as the org card and the user card print it. */
60
+ export declare function allowanceSentence(account: StaffAiAccountAllowance, scope: 'org' | 'user'): string;
61
+ export interface StaffOrgAiView {
62
+ month: string;
63
+ addon: {
64
+ status: string;
65
+ price: string;
66
+ since: string | null;
67
+ sinceNote: string;
68
+ on: boolean;
69
+ };
70
+ pool: {
71
+ band: string;
72
+ used: string;
73
+ returned: string;
74
+ allowance: string;
75
+ projected: string;
76
+ };
77
+ overage: {
78
+ line: string;
79
+ state: string;
80
+ };
81
+ refusals: {
82
+ heading: string;
83
+ chips: string[];
84
+ };
85
+ jobs: {
86
+ failedRead: boolean;
87
+ empty: boolean;
88
+ chips: string[];
89
+ floor: string;
90
+ recent: Array<{
91
+ id: string;
92
+ kind: string;
93
+ status: string;
94
+ credits: string;
95
+ startedAt: string | null;
96
+ startedBy: string | null;
97
+ giveBack: number;
98
+ }>;
99
+ };
100
+ tokens: {
101
+ totals: string;
102
+ cacheHit: string;
103
+ empty: boolean;
104
+ kinds: Array<{
105
+ kind: string;
106
+ headline: string;
107
+ detail: string;
108
+ }>;
109
+ } | null;
110
+ users: Array<{
111
+ uid: string;
112
+ name: string;
113
+ headline: string;
114
+ detail: string;
115
+ }>;
116
+ margin: {
117
+ tone: 'error' | 'warning' | 'success';
118
+ text: string;
119
+ contribution: string;
120
+ };
121
+ giveBack: {
122
+ meters: Array<{
123
+ value: string;
124
+ label: string;
125
+ }>;
126
+ defaultMeter: string;
127
+ summary: string;
128
+ returnable: number;
129
+ };
130
+ /**
131
+ * The AI columns of the org page's monthly usage table, and the pool line it draws above them
132
+ * (`staffOrgUsageColumn`); null where the reader did not hand the rollups over.
133
+ */
134
+ usage: {
135
+ pool: string;
136
+ months: Array<{
137
+ month: string;
138
+ headline: string;
139
+ detail: string;
140
+ }>;
141
+ } | null;
142
+ }
143
+ /** The AI fields of one monthly rollup, with the month they belong to. */
144
+ export type StaffOrgAiUsageRollup = StaffOrgUsageAiMonth & {
145
+ month: string;
146
+ };
147
+ /** The card's figures as the sentences it prints. */
148
+ export declare function staffOrgAiView(data: StaffOrgAiResponse, addonName: string, usage?: {
149
+ pool: StaffAssistPool;
150
+ months: readonly StaffOrgAiUsageRollup[];
151
+ }): StaffOrgAiView;
152
+ /** One row of `/api/ai/admin/user`: a workspace's month for the account. */
153
+ export interface StaffUserAiRow {
154
+ orgId: string;
155
+ orgName: string | null;
156
+ slug: string | null;
157
+ month: string;
158
+ credits: number;
159
+ requests: number;
160
+ refusals: number;
161
+ }
162
+ export interface StaffUserAiView {
163
+ allowance: string;
164
+ giveBack: {
165
+ summary: string;
166
+ returnable: number;
167
+ };
168
+ empty: string;
169
+ rows: Array<{
170
+ orgId: string;
171
+ workspace: string;
172
+ month: string;
173
+ headline: string;
174
+ detail: string;
175
+ }>;
176
+ }
177
+ /** The account card as it prints: the Free allowance, and a row per workspace per month kept. */
178
+ export declare function staffUserAiView(data: {
179
+ rows: StaffUserAiRow[];
180
+ allowance: StaffAiAccountAllowance | null;
181
+ }): StaffUserAiView;