@burdenoff/microfe-billing 2026.1028.1 → 2026.1030.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing/modules/usage/components/WorkspaceAiSpendPanel.js +152 -0
- package/dist/billing/modules/usage/components/WorkspaceAiSpendPanel.js.map +1 -0
- package/dist/billing/modules/usage/hooks/useWorkspaceAiSpend.js +44 -0
- package/dist/billing/modules/usage/hooks/useWorkspaceAiSpend.js.map +1 -0
- package/dist/billing/modules/usage/pages/UsagePage.js +399 -395
- package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
- package/dist/billing/modules/usage/utils/aiSpend.js +78 -0
- package/dist/billing/modules/usage/utils/aiSpend.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { statusTokens as e } from "../../../shared/utils/tokens.js";
|
|
2
|
+
import { formatNumber as t } from "../../../shared/utils/format.js";
|
|
3
|
+
import { AI_SPEND_WINDOWS as n, formatMicroUsd as r, formatProductKey as i } from "../utils/aiSpend.js";
|
|
4
|
+
import { useWorkspaceAiSpend as a } from "../hooks/useWorkspaceAiSpend.js";
|
|
5
|
+
import { useState as o } from "react";
|
|
6
|
+
import { AlertTriangle as s, RefreshCw as c, Sparkles as l } from "lucide-react";
|
|
7
|
+
import { Fragment as u, jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
8
|
+
//#region src/billing/modules/usage/components/WorkspaceAiSpendPanel.tsx
|
|
9
|
+
var p = ({ workspaceId: p }) => {
|
|
10
|
+
let [m, h] = o("month"), { summary: g, truncated: _, isLoading: v, isForbidden: y, error: b, refetch: x } = a(p, m);
|
|
11
|
+
if (y) return null;
|
|
12
|
+
let S = n.find((e) => e.value === m)?.label ?? "";
|
|
13
|
+
return /* @__PURE__ */ f("section", {
|
|
14
|
+
"aria-labelledby": "workspace-ai-spend-heading",
|
|
15
|
+
"data-testid": "workspace-ai-spend",
|
|
16
|
+
className: "rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 p-4 space-y-3",
|
|
17
|
+
children: [
|
|
18
|
+
/* @__PURE__ */ f("div", {
|
|
19
|
+
className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between",
|
|
20
|
+
children: [/* @__PURE__ */ f("div", {
|
|
21
|
+
className: "flex items-center gap-2",
|
|
22
|
+
children: [/* @__PURE__ */ d("span", {
|
|
23
|
+
className: `flex size-7 items-center justify-center rounded-lg ${e.info.bg}`,
|
|
24
|
+
children: /* @__PURE__ */ d(l, { className: `size-4 ${e.info.icon}` })
|
|
25
|
+
}), /* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("h3", {
|
|
26
|
+
id: "workspace-ai-spend-heading",
|
|
27
|
+
className: "text-sm font-semibold text-text-primary",
|
|
28
|
+
children: "AI spend"
|
|
29
|
+
}), /* @__PURE__ */ d("p", {
|
|
30
|
+
className: "text-xs text-text-secondary",
|
|
31
|
+
children: "What AI calls cost this workspace, in US dollars."
|
|
32
|
+
})] })]
|
|
33
|
+
}), /* @__PURE__ */ f("div", {
|
|
34
|
+
className: "flex items-center gap-2",
|
|
35
|
+
children: [
|
|
36
|
+
/* @__PURE__ */ d("label", {
|
|
37
|
+
htmlFor: "workspace-ai-spend-window",
|
|
38
|
+
className: "sr-only",
|
|
39
|
+
children: "AI spend period"
|
|
40
|
+
}),
|
|
41
|
+
/* @__PURE__ */ d("select", {
|
|
42
|
+
id: "workspace-ai-spend-window",
|
|
43
|
+
value: m,
|
|
44
|
+
onChange: (e) => h(e.target.value),
|
|
45
|
+
className: "px-2 py-1.5 text-sm border border-border-subtle rounded-button bg-bg-surface text-text-primary",
|
|
46
|
+
children: n.map((e) => /* @__PURE__ */ d("option", {
|
|
47
|
+
value: e.value,
|
|
48
|
+
children: e.label
|
|
49
|
+
}, e.value))
|
|
50
|
+
}),
|
|
51
|
+
/* @__PURE__ */ d("button", {
|
|
52
|
+
type: "button",
|
|
53
|
+
onClick: x,
|
|
54
|
+
className: "p-2 border border-border-subtle rounded-button text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors",
|
|
55
|
+
"aria-label": "Refresh AI spend",
|
|
56
|
+
title: "Refresh AI spend",
|
|
57
|
+
children: /* @__PURE__ */ d(c, { className: "size-4" })
|
|
58
|
+
})
|
|
59
|
+
]
|
|
60
|
+
})]
|
|
61
|
+
}),
|
|
62
|
+
v && !g && /* @__PURE__ */ d("div", {
|
|
63
|
+
className: "h-16 bg-bg-sunken animate-pulse rounded-card",
|
|
64
|
+
"aria-busy": "true"
|
|
65
|
+
}),
|
|
66
|
+
b && !g && /* @__PURE__ */ f("div", {
|
|
67
|
+
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ d(s, { className: `size-4 ${e.error.icon}` }),
|
|
70
|
+
/* @__PURE__ */ d("span", { children: "AI spend could not be loaded." }),
|
|
71
|
+
/* @__PURE__ */ d("button", {
|
|
72
|
+
type: "button",
|
|
73
|
+
onClick: x,
|
|
74
|
+
className: "underline text-text-primary hover:text-text-secondary",
|
|
75
|
+
children: "Retry"
|
|
76
|
+
})
|
|
77
|
+
]
|
|
78
|
+
}),
|
|
79
|
+
g && /* @__PURE__ */ f(u, { children: [
|
|
80
|
+
/* @__PURE__ */ f("div", { children: [/* @__PURE__ */ d("p", {
|
|
81
|
+
className: "text-2xl font-semibold tabular-nums text-text-primary",
|
|
82
|
+
"data-testid": "workspace-ai-spend-total",
|
|
83
|
+
children: r(g.totalMicroUsd)
|
|
84
|
+
}), /* @__PURE__ */ f("p", {
|
|
85
|
+
className: "text-xs text-text-secondary",
|
|
86
|
+
children: [
|
|
87
|
+
S,
|
|
88
|
+
" (UTC) · ",
|
|
89
|
+
t(g.attempts),
|
|
90
|
+
" AI",
|
|
91
|
+
" ",
|
|
92
|
+
g.attempts === 1 ? "call" : "calls",
|
|
93
|
+
g.unpricedAttempts > 0 && ` · ${t(g.unpricedAttempts)} without a recorded cost`
|
|
94
|
+
]
|
|
95
|
+
})] }),
|
|
96
|
+
g.products.length === 0 ? /* @__PURE__ */ d("p", {
|
|
97
|
+
className: "text-sm text-text-secondary",
|
|
98
|
+
children: "No AI usage in this period."
|
|
99
|
+
}) : /* @__PURE__ */ d("div", {
|
|
100
|
+
className: "overflow-x-auto",
|
|
101
|
+
children: /* @__PURE__ */ f("table", {
|
|
102
|
+
className: "w-full text-sm",
|
|
103
|
+
children: [/* @__PURE__ */ d("thead", { children: /* @__PURE__ */ f("tr", {
|
|
104
|
+
className: "text-left text-xs text-text-secondary border-b border-border-seam",
|
|
105
|
+
children: [
|
|
106
|
+
/* @__PURE__ */ d("th", {
|
|
107
|
+
scope: "col",
|
|
108
|
+
className: "py-2 pr-4 font-medium",
|
|
109
|
+
children: "Product"
|
|
110
|
+
}),
|
|
111
|
+
/* @__PURE__ */ d("th", {
|
|
112
|
+
scope: "col",
|
|
113
|
+
className: "py-2 pr-4 font-medium text-right",
|
|
114
|
+
children: "AI calls"
|
|
115
|
+
}),
|
|
116
|
+
/* @__PURE__ */ d("th", {
|
|
117
|
+
scope: "col",
|
|
118
|
+
className: "py-2 font-medium text-right",
|
|
119
|
+
children: "Cost (USD)"
|
|
120
|
+
})
|
|
121
|
+
]
|
|
122
|
+
}) }), /* @__PURE__ */ d("tbody", { children: g.products.map((e) => /* @__PURE__ */ f("tr", {
|
|
123
|
+
className: "border-b border-border-seam last:border-0",
|
|
124
|
+
children: [
|
|
125
|
+
/* @__PURE__ */ d("td", {
|
|
126
|
+
className: "py-2 pr-4 text-text-primary",
|
|
127
|
+
children: i(e.productKey)
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ d("td", {
|
|
130
|
+
className: "py-2 pr-4 text-right tabular-nums text-text-secondary",
|
|
131
|
+
children: t(e.attempts)
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ d("td", {
|
|
134
|
+
className: "py-2 text-right tabular-nums text-text-primary",
|
|
135
|
+
children: r(e.costMicroUsd)
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
}, e.productKey)) })]
|
|
139
|
+
})
|
|
140
|
+
}),
|
|
141
|
+
/* @__PURE__ */ f("p", {
|
|
142
|
+
className: "text-xs text-text-secondary",
|
|
143
|
+
children: ["Costs come from the workspace's AI ledger at the provider prices recorded for each call. AI credits are charged at USD 0.000003 per credit.", _ && " Only the products with the highest cost are listed."]
|
|
144
|
+
})
|
|
145
|
+
] })
|
|
146
|
+
]
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
//#endregion
|
|
150
|
+
export { p as WorkspaceAiSpendPanel };
|
|
151
|
+
|
|
152
|
+
//# sourceMappingURL=WorkspaceAiSpendPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkspaceAiSpendPanel.js","names":[],"sources":["../../../../../src/billing/modules/usage/components/WorkspaceAiSpendPanel.tsx"],"sourcesContent":["/**\n * Workspace AI spend in US dollars (BOFF-10273).\n *\n * Sits in the workspace view of the usage page, above the workspace quotas, so\n * a workspace admin sees what AI cost the workspace in dollars next to the AI\n * credit quota that limits it. Members who may not read the AI ledger see\n * nothing: the gateway refuses the query and the panel stays hidden.\n */\n\nimport { useState, type FC } from 'react';\nimport { AlertTriangle, RefreshCw, Sparkles } from 'lucide-react';\nimport { useWorkspaceAiSpend } from '../hooks/useWorkspaceAiSpend';\nimport {\n AI_SPEND_WINDOWS,\n formatMicroUsd,\n formatProductKey,\n type AiSpendWindow,\n} from '../utils/aiSpend';\nimport { formatNumber } from '../../../shared/utils/format';\nimport { statusTokens } from '../../../shared/utils/tokens';\n\ninterface WorkspaceAiSpendPanelProps {\n workspaceId: string;\n}\n\nexport const WorkspaceAiSpendPanel: FC<WorkspaceAiSpendPanelProps> = ({ workspaceId }) => {\n const [spendWindow, setSpendWindow] = useState<AiSpendWindow>('month');\n const { summary, truncated, isLoading, isForbidden, error, refetch } = useWorkspaceAiSpend(\n workspaceId,\n spendWindow\n );\n\n if (isForbidden) return null;\n\n const windowLabel = AI_SPEND_WINDOWS.find((option) => option.value === spendWindow)?.label ?? '';\n\n return (\n <section\n aria-labelledby=\"workspace-ai-spend-heading\"\n data-testid=\"workspace-ai-spend\"\n className=\"rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 p-4 space-y-3\"\n >\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-2\">\n <span\n className={`flex size-7 items-center justify-center rounded-lg ${statusTokens.info.bg}`}\n >\n <Sparkles className={`size-4 ${statusTokens.info.icon}`} />\n </span>\n <div>\n <h3 id=\"workspace-ai-spend-heading\" className=\"text-sm font-semibold text-text-primary\">\n AI spend\n </h3>\n <p className=\"text-xs text-text-secondary\">\n What AI calls cost this workspace, in US dollars.\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <label htmlFor=\"workspace-ai-spend-window\" className=\"sr-only\">\n AI spend period\n </label>\n <select\n id=\"workspace-ai-spend-window\"\n value={spendWindow}\n onChange={(event) => setSpendWindow(event.target.value as AiSpendWindow)}\n className=\"px-2 py-1.5 text-sm border border-border-subtle rounded-button bg-bg-surface text-text-primary\"\n >\n {AI_SPEND_WINDOWS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n <button\n type=\"button\"\n onClick={refetch}\n className=\"p-2 border border-border-subtle rounded-button text-text-secondary hover:text-text-primary hover:bg-bg-sunken transition-colors\"\n aria-label=\"Refresh AI spend\"\n title=\"Refresh AI spend\"\n >\n <RefreshCw className=\"size-4\" />\n </button>\n </div>\n </div>\n\n {isLoading && !summary && (\n <div className=\"h-16 bg-bg-sunken animate-pulse rounded-card\" aria-busy=\"true\" />\n )}\n\n {error && !summary && (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <AlertTriangle className={`size-4 ${statusTokens.error.icon}`} />\n <span>AI spend could not be loaded.</span>\n <button\n type=\"button\"\n onClick={refetch}\n className=\"underline text-text-primary hover:text-text-secondary\"\n >\n Retry\n </button>\n </div>\n )}\n\n {summary && (\n <>\n <div>\n <p\n className=\"text-2xl font-semibold tabular-nums text-text-primary\"\n data-testid=\"workspace-ai-spend-total\"\n >\n {formatMicroUsd(summary.totalMicroUsd)}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {windowLabel} (UTC) · {formatNumber(summary.attempts)} AI{' '}\n {summary.attempts === 1 ? 'call' : 'calls'}\n {summary.unpricedAttempts > 0 &&\n ` · ${formatNumber(summary.unpricedAttempts)} without a recorded cost`}\n </p>\n </div>\n\n {summary.products.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">No AI usage in this period.</p>\n ) : (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-text-secondary border-b border-border-seam\">\n <th scope=\"col\" className=\"py-2 pr-4 font-medium\">\n Product\n </th>\n <th scope=\"col\" className=\"py-2 pr-4 font-medium text-right\">\n AI calls\n </th>\n <th scope=\"col\" className=\"py-2 font-medium text-right\">\n Cost (USD)\n </th>\n </tr>\n </thead>\n <tbody>\n {summary.products.map((row) => (\n <tr key={row.productKey} className=\"border-b border-border-seam last:border-0\">\n <td className=\"py-2 pr-4 text-text-primary\">\n {formatProductKey(row.productKey)}\n </td>\n <td className=\"py-2 pr-4 text-right tabular-nums text-text-secondary\">\n {formatNumber(row.attempts)}\n </td>\n <td className=\"py-2 text-right tabular-nums text-text-primary\">\n {formatMicroUsd(row.costMicroUsd)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n\n <p className=\"text-xs text-text-secondary\">\n Costs come from the workspace's AI ledger at the provider prices recorded for each\n call. AI credits are charged at USD 0.000003 per credit.\n {truncated && ' Only the products with the highest cost are listed.'}\n </p>\n </>\n )}\n </section>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { aiSpendRange as e, isAiSpendPermissionError as t, summarizeAiSpend as n } from "../utils/aiSpend.js";
|
|
2
|
+
import { useMemo as r } from "react";
|
|
3
|
+
import { gql as i, useGatewayClient as a, useQuery as o } from "@burdenoff/fe-libs/shared/graphql";
|
|
4
|
+
//#region src/billing/modules/usage/hooks/useWorkspaceAiSpend.ts
|
|
5
|
+
var s = i`
|
|
6
|
+
query WorkspaceAiSpendByProduct($from: DateTime!, $to: DateTime!) {
|
|
7
|
+
promptUsageBreakdownV2(from: $from, to: $to, groupBy: [PRODUCT]) {
|
|
8
|
+
workspaceId
|
|
9
|
+
currency
|
|
10
|
+
truncated
|
|
11
|
+
rows {
|
|
12
|
+
productKey
|
|
13
|
+
attempts
|
|
14
|
+
costMicroUsd {
|
|
15
|
+
knownSubtotal
|
|
16
|
+
knownAttempts
|
|
17
|
+
missingAttempts
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
function c(i, c) {
|
|
24
|
+
let { data: l, loading: u, error: d, refetch: f } = o(s, {
|
|
25
|
+
client: a("workspace"),
|
|
26
|
+
variables: r(() => e(c), [c]),
|
|
27
|
+
skip: !i,
|
|
28
|
+
fetchPolicy: "cache-and-network"
|
|
29
|
+
}), p = l?.promptUsageBreakdownV2, m = r(() => p ? n(p.rows) : null, [p]), h = t(d);
|
|
30
|
+
return {
|
|
31
|
+
summary: m,
|
|
32
|
+
truncated: p?.truncated ?? !1,
|
|
33
|
+
isLoading: u,
|
|
34
|
+
isForbidden: h,
|
|
35
|
+
error: d && !h ? d : null,
|
|
36
|
+
refetch: () => {
|
|
37
|
+
f();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { c as useWorkspaceAiSpend };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=useWorkspaceAiSpend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useWorkspaceAiSpend.js","names":[],"sources":["../../../../../src/billing/modules/usage/hooks/useWorkspaceAiSpend.ts"],"sourcesContent":["/**\n * Workspace AI spend in US dollars (BOFF-10273).\n *\n * Reads the workspace's AI ledger through `promptUsageBreakdownV2` on the\n * workspace gateway (wspace-prompts-svc), grouped by product. The workspace is\n * the one in the request context; the gateway enforces\n * `read prompt-invocation` on it, so only members allowed to see AI usage get\n * a result.\n */\n\nimport { useMemo } from 'react';\nimport { useGatewayClient, useQuery, gql } from '@burdenoff/fe-libs/shared/graphql';\nimport {\n aiSpendRange,\n isAiSpendPermissionError,\n summarizeAiSpend,\n type AiSpendBreakdownRowInput,\n type AiSpendSummary,\n type AiSpendWindow,\n} from '../utils/aiSpend';\n\nexport const WORKSPACE_AI_SPEND_QUERY = gql`\n query WorkspaceAiSpendByProduct($from: DateTime!, $to: DateTime!) {\n promptUsageBreakdownV2(from: $from, to: $to, groupBy: [PRODUCT]) {\n workspaceId\n currency\n truncated\n rows {\n productKey\n attempts\n costMicroUsd {\n knownSubtotal\n knownAttempts\n missingAttempts\n }\n }\n }\n }\n`;\n\ninterface WorkspaceAiSpendQueryResult {\n promptUsageBreakdownV2: {\n workspaceId: string;\n currency: string;\n truncated: boolean;\n rows: AiSpendBreakdownRowInput[];\n };\n}\n\nexport interface UseWorkspaceAiSpendResult {\n summary: AiSpendSummary | null;\n truncated: boolean;\n isLoading: boolean;\n /** The caller may not read this workspace's AI usage; render nothing. */\n isForbidden: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\nexport function useWorkspaceAiSpend(\n workspaceId: string | undefined,\n window: AiSpendWindow\n): UseWorkspaceAiSpendResult {\n const wsClient = useGatewayClient('workspace');\n // The range is fixed when the window is chosen, so a re-render does not\n // move it and refetch; choosing a window again recomputes it.\n const range = useMemo(() => aiSpendRange(window), [window]);\n const { data, loading, error, refetch } = useQuery<WorkspaceAiSpendQueryResult>(\n WORKSPACE_AI_SPEND_QUERY,\n {\n client: wsClient,\n variables: range,\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n }\n );\n\n const breakdown = data?.promptUsageBreakdownV2;\n const summary = useMemo(() => (breakdown ? summarizeAiSpend(breakdown.rows) : null), [breakdown]);\n const isForbidden = isAiSpendPermissionError(error);\n\n return {\n summary,\n truncated: breakdown?.truncated ?? false,\n isLoading: loading,\n isForbidden,\n error: error && !isForbidden ? error : null,\n refetch: () => {\n void refetch();\n },\n };\n}\n"],"mappings":";;;;AAqBA,IAAa,IAA2B,CAAG;;;;;;;;;;;;;;;;;;AAsC3C,SAAgB,EACd,GACA,GAC2B;CAK3B,IAAM,EAAE,SAAM,YAAS,UAAO,eAAY,EACxC,GACA;EACE,QAPa,EAAiB,WAOtB;EACR,WALU,QAAc,EAAa,CAAM,GAAG,CAAC,CAAM,CAK1C;EACX,MAAM,CAAC;EACP,aAAa;CACf,CACF,GAEM,IAAY,GAAM,wBAClB,IAAU,QAAe,IAAY,EAAiB,EAAU,IAAI,IAAI,MAAO,CAAC,CAAS,CAAC,GAC1F,IAAc,EAAyB,CAAK;CAElD,OAAO;EACL;EACA,WAAW,GAAW,aAAa;EACnC,WAAW;EACX;EACA,OAAO,KAAS,CAAC,IAAc,IAAQ;EACvC,eAAe;GACb,EAAa;EACf;CACF;AACF"}
|