@opengeni/react 7.4.0-canary.36855269964001 → 7.4.0
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/README.md +3 -48
- package/dist/artifacts-document.js +2 -3
- package/dist/artifacts-presentation.js +2 -3
- package/dist/artifacts-spreadsheet.js +2 -2
- package/dist/artifacts.d.ts +0 -1
- package/dist/artifacts.js +1402 -505
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-R6KK5VKR.js → browser-viewer-2E6R2Y4W.js} +4 -4
- package/dist/{chunk-WUISZ3WU.js → chunk-3E2AITTZ.js} +28 -32
- package/dist/{chunk-WUISZ3WU.js.map → chunk-3E2AITTZ.js.map} +1 -1
- package/dist/{chunk-PWUILT2U.js → chunk-5AZSK6MV.js} +2 -2
- package/dist/{chunk-Y2PRMYLC.js → chunk-DUEBTL3N.js} +3 -4
- package/dist/{chunk-Y2PRMYLC.js.map → chunk-DUEBTL3N.js.map} +1 -1
- package/dist/{chunk-7UAMYXJY.js → chunk-KNDIXZN6.js} +18 -25
- package/dist/{chunk-7UAMYXJY.js.map → chunk-KNDIXZN6.js.map} +1 -1
- package/dist/{chunk-P4G6K2E2.js → chunk-L4OSXQ6H.js} +884 -1119
- package/dist/chunk-L4OSXQ6H.js.map +1 -0
- package/dist/{chunk-LLIH7Q4B.js → chunk-S5JQ4R22.js} +19 -47
- package/dist/chunk-S5JQ4R22.js.map +1 -0
- package/dist/{chunk-A4UJB2MM.js → chunk-SEEVCT36.js} +2 -2
- package/dist/{chunk-UXJCQ5GH.js → chunk-UDD6DLLT.js} +1 -6
- package/dist/chunk-UDD6DLLT.js.map +1 -0
- package/dist/chunk-UGMKPUEM.js +299 -0
- package/dist/chunk-UGMKPUEM.js.map +1 -0
- package/dist/{chunk-ZCZHKJKZ.js → chunk-USXVQACU.js} +3 -3
- package/dist/{chunk-VABBF52G.js → chunk-V2ABIVYJ.js} +6 -70
- package/dist/chunk-V2ABIVYJ.js.map +1 -0
- package/dist/{chunk-TVO6GXR6.js → chunk-X4HIN6TV.js} +162 -71
- package/dist/chunk-X4HIN6TV.js.map +1 -0
- package/dist/{chunk-IIDEIBMC.js → chunk-XEJEOAOV.js} +38 -49
- package/dist/chunk-XEJEOAOV.js.map +1 -0
- package/dist/{chunk-ZKLCPBWN.js → chunk-Y3QTZN2F.js} +5 -5
- package/dist/{chunk-73BUNGLF.js → chunk-YZ3YYMOM.js} +1 -1
- package/dist/components/artifacts/editable-artifact-ui.d.ts +1 -2
- package/dist/components/artifacts/index.d.ts +0 -7
- package/dist/components/browser-select-control.d.ts +1 -3
- package/dist/components/message-timeline.d.ts +1 -18
- package/dist/components/open-geni-links.d.ts +0 -16
- package/dist/components/session-conversation.d.ts +1 -20
- package/dist/composer.js +3 -3
- package/dist/{computer-viewer-DJHPSP2J.js → computer-viewer-WMJE6OA3.js} +3 -3
- package/dist/connect.js +6 -7
- package/dist/connect.js.map +1 -1
- package/dist/hooks/latest-question.d.ts +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.js +28 -39
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +4 -4
- package/dist/{latest-question-KGCDK3XX.js → latest-question-QB4EUCW5.js} +3 -3
- package/dist/lib/format.d.ts +0 -3
- package/dist/machines.js +2 -2
- package/dist/{markdown-table-layout-QXSLK4BY.js → markdown-table-layout-PFUEZ2U4.js} +5 -37
- package/dist/markdown-table-layout-PFUEZ2U4.js.map +1 -0
- package/dist/{sandbox-files-4PMXDSDV.js → sandbox-files-PXPIZAFG.js} +2 -2
- package/dist/session-ui.d.ts +2 -5
- package/dist/session-ui.js +13 -23
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +3 -3
- package/dist/sites.js +289 -4
- package/dist/sites.js.map +1 -1
- package/dist/timeline/types.d.ts +1 -7
- package/dist/{timeline-annotation-selection-DMLMM2TX.js → timeline-annotation-selection-ODLVF4OI.js} +1 -6
- package/dist/timeline-annotation-selection-ODLVF4OI.js.map +1 -0
- package/package.json +3 -7
- package/src/artifacts.ts +0 -37
- package/src/components/artifacts/editable-artifact-ui.tsx +1 -3
- package/src/components/artifacts/editable-artifact-workbench.tsx +2 -5
- package/src/components/artifacts/editable-document.tsx +36 -34
- package/src/components/artifacts/editable-presentation.tsx +16 -21
- package/src/components/artifacts/index.ts +0 -41
- package/src/components/browser-select-control.tsx +1 -4
- package/src/components/browser-viewer.tsx +22 -81
- package/src/components/computer-viewer.tsx +23 -51
- package/src/components/markdown-table-layout.ts +5 -49
- package/src/components/markdown.tsx +2 -5
- package/src/components/message-timeline.tsx +434 -515
- package/src/components/open-geni-links.tsx +0 -24
- package/src/components/session-conversation.tsx +20 -131
- package/src/components/timeline-annotation-selection.tsx +0 -7
- package/src/hooks/use-available-models.ts +1 -1
- package/src/index.ts +0 -9
- package/src/lib/format.ts +0 -13
- package/src/session-ui.ts +0 -9
- package/src/timeline/projection.ts +4 -78
- package/src/timeline/types.ts +0 -7
- package/styles/compiled.css +0 -325
- package/dist/chat-interactive-block-VAGABN7J.js +0 -11
- package/dist/chunk-5CVXZA3Y.js +0 -292
- package/dist/chunk-5CVXZA3Y.js.map +0 -1
- package/dist/chunk-B3V6TG77.js +0 -41
- package/dist/chunk-B3V6TG77.js.map +0 -1
- package/dist/chunk-IIDEIBMC.js.map +0 -1
- package/dist/chunk-LLIH7Q4B.js.map +0 -1
- package/dist/chunk-P4G6K2E2.js.map +0 -1
- package/dist/chunk-RAIQYITC.js +0 -1772
- package/dist/chunk-RAIQYITC.js.map +0 -1
- package/dist/chunk-SCOTPE2I.js +0 -589
- package/dist/chunk-SCOTPE2I.js.map +0 -1
- package/dist/chunk-TVO6GXR6.js.map +0 -1
- package/dist/chunk-UXJCQ5GH.js.map +0 -1
- package/dist/chunk-VABBF52G.js.map +0 -1
- package/dist/chunk-WEHMPOXK.js +0 -39
- package/dist/chunk-WEHMPOXK.js.map +0 -1
- package/dist/chunk-XCHWDJ22.js +0 -107
- package/dist/chunk-XCHWDJ22.js.map +0 -1
- package/dist/components/artifacts/artifact-chrome.d.ts +0 -120
- package/dist/components/artifacts/artifact-sandbox.d.ts +0 -27
- package/dist/components/artifacts/chat-interactive-block.d.ts +0 -29
- package/dist/components/artifacts/deferred-chat-media.d.ts +0 -15
- package/dist/components/artifacts/editable-artifact-view.d.ts +0 -46
- package/dist/components/artifacts/latest-task-runner.d.ts +0 -15
- package/dist/components/artifacts/session-artifact-viewer.d.ts +0 -39
- package/dist/components/artifacts/site-view.d.ts +0 -23
- package/dist/components/timeline-pill-geometry.d.ts +0 -6
- package/dist/components/timeline-pill-placement.d.ts +0 -18
- package/dist/components/usage-limit-notice.d.ts +0 -49
- package/dist/components/usage-member-list.d.ts +0 -91
- package/dist/components/usage-meter.d.ts +0 -73
- package/dist/hooks/use-usage.d.ts +0 -40
- package/dist/markdown-table-layout-QXSLK4BY.js.map +0 -1
- package/dist/sandbox-files-4PMXDSDV.js.map +0 -1
- package/dist/timeline/allowance-exhausted-row.d.ts +0 -24
- package/dist/timeline-annotation-selection-DMLMM2TX.js.map +0 -1
- package/dist/timeline-pill-geometry-2G6EK47V.js +0 -73
- package/dist/timeline-pill-geometry-2G6EK47V.js.map +0 -1
- package/dist/usage/allowance-copy.d.ts +0 -25
- package/dist/usage/summary.d.ts +0 -35
- package/dist/usage.d.ts +0 -13
- package/dist/usage.js +0 -1073
- package/dist/usage.js.map +0 -1
- package/src/components/artifacts/artifact-chrome.tsx +0 -413
- package/src/components/artifacts/artifact-sandbox.tsx +0 -185
- package/src/components/artifacts/chat-interactive-block.tsx +0 -260
- package/src/components/artifacts/deferred-chat-media.tsx +0 -67
- package/src/components/artifacts/editable-artifact-view.tsx +0 -187
- package/src/components/artifacts/latest-task-runner.ts +0 -49
- package/src/components/artifacts/session-artifact-viewer.tsx +0 -376
- package/src/components/artifacts/site-view.tsx +0 -116
- package/src/components/timeline-pill-geometry.ts +0 -112
- package/src/components/timeline-pill-placement.ts +0 -81
- package/src/components/usage-limit-notice.tsx +0 -179
- package/src/components/usage-member-list.tsx +0 -792
- package/src/components/usage-meter.tsx +0 -382
- package/src/hooks/use-usage.ts +0 -100
- package/src/timeline/allowance-exhausted-row.tsx +0 -86
- package/src/usage/allowance-copy.ts +0 -64
- package/src/usage/summary.ts +0 -93
- package/src/usage.ts +0 -50
- /package/dist/{browser-viewer-R6KK5VKR.js.map → browser-viewer-2E6R2Y4W.js.map} +0 -0
- /package/dist/{chunk-PWUILT2U.js.map → chunk-5AZSK6MV.js.map} +0 -0
- /package/dist/{chunk-A4UJB2MM.js.map → chunk-SEEVCT36.js.map} +0 -0
- /package/dist/{chunk-ZCZHKJKZ.js.map → chunk-USXVQACU.js.map} +0 -0
- /package/dist/{chunk-ZKLCPBWN.js.map → chunk-Y3QTZN2F.js.map} +0 -0
- /package/dist/{chunk-73BUNGLF.js.map → chunk-YZ3YYMOM.js.map} +0 -0
- /package/dist/{chat-interactive-block-VAGABN7J.js.map → computer-viewer-WMJE6OA3.js.map} +0 -0
- /package/dist/{latest-question-KGCDK3XX.js.map → latest-question-QB4EUCW5.js.map} +0 -0
- /package/dist/{computer-viewer-DJHPSP2J.js.map → sandbox-files-PXPIZAFG.js.map} +0 -0
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
import type { UsageAllowancePeriod, WorkspaceUsageResponse } from "@opengeni/sdk/usage-allowances";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
-
|
|
4
|
-
import { useUsage, type UsageClientLike } from "../hooks/use-usage";
|
|
5
|
-
import { cn } from "../lib/cn";
|
|
6
|
-
import {
|
|
7
|
-
allowanceLabels,
|
|
8
|
-
allowanceResetSentence,
|
|
9
|
-
formatAllowanceInstant,
|
|
10
|
-
type AllowanceLabels,
|
|
11
|
-
} from "../usage/allowance-copy";
|
|
12
|
-
import {
|
|
13
|
-
summarizeUsage,
|
|
14
|
-
usagePercentLabel,
|
|
15
|
-
type UsageReading,
|
|
16
|
-
type UsageSummary,
|
|
17
|
-
} from "../usage/summary";
|
|
18
|
-
|
|
19
|
-
export type UsageMeterLabels = AllowanceLabels & {
|
|
20
|
-
/** Heading for the member's own ceiling. */
|
|
21
|
-
title: string;
|
|
22
|
-
/** Heading when only the shared workspace pool applies. */
|
|
23
|
-
workspaceTitle: string;
|
|
24
|
-
/** "38% left" — `percent` is preformatted ("38", "<1"). */
|
|
25
|
-
left: (percent: string) => string;
|
|
26
|
-
/** "62% used". */
|
|
27
|
-
used: (percent: string) => string;
|
|
28
|
-
limitReached: string;
|
|
29
|
-
/** Status chip beside a hero meter that is close to its limit. */
|
|
30
|
-
nearLimit: string;
|
|
31
|
-
/** "$13.00 left" for a hero meter with amounts. */
|
|
32
|
-
amountLeft: (amount: string) => string;
|
|
33
|
-
/** No ceiling applies to this member. */
|
|
34
|
-
unlimited: string;
|
|
35
|
-
loading: string;
|
|
36
|
-
error: string;
|
|
37
|
-
retry: string;
|
|
38
|
-
/** "$31.00 of $50.00 used" when the host passes `formatAmount`. */
|
|
39
|
-
amounts: (used: string, limit: string) => string;
|
|
40
|
-
/** The shared pool, under a member's own meter: "Workspace: 9% left". */
|
|
41
|
-
workspaceLine: (value: string) => string;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const DEFAULT_USAGE_METER_LABELS: UsageMeterLabels = {
|
|
45
|
-
...allowanceLabels(),
|
|
46
|
-
title: "Your usage",
|
|
47
|
-
workspaceTitle: "Workspace usage",
|
|
48
|
-
left: (percent) => `${percent}% left`,
|
|
49
|
-
used: (percent) => `${percent}% used`,
|
|
50
|
-
limitReached: "Limit reached",
|
|
51
|
-
nearLimit: "Near limit",
|
|
52
|
-
amountLeft: (amount) => `${amount} left`,
|
|
53
|
-
unlimited: "No usage limit",
|
|
54
|
-
loading: "Checking usage…",
|
|
55
|
-
error: "Couldn't load usage",
|
|
56
|
-
retry: "Try again",
|
|
57
|
-
amounts: (used, limit) => `${used} of ${limit} used`,
|
|
58
|
-
workspaceLine: (value) => `Workspace: ${value}`,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export type UsageMeterBaseProps = {
|
|
62
|
-
/** "left" (default) reads like a battery; "used" reads like spend. */
|
|
63
|
-
measure?: "left" | "used" | undefined;
|
|
64
|
-
/**
|
|
65
|
-
* Opt in to amounts. Receives USD micros; return your own unit ("$31.00",
|
|
66
|
-
* "3.1 credits", "1.5× plan"). Without it the meter shows only shares.
|
|
67
|
-
*/
|
|
68
|
-
formatAmount?: ((micros: number) => string) | undefined;
|
|
69
|
-
labels?: Partial<UsageMeterLabels> | undefined;
|
|
70
|
-
/**
|
|
71
|
-
* `compact` is one line for menus and headers; `hero` leads a page with the
|
|
72
|
-
* amount (or share) left in large type. An empty `labels.title` hides the
|
|
73
|
-
* heading when the page already names it.
|
|
74
|
-
*/
|
|
75
|
-
density?: "default" | "compact" | "hero" | undefined;
|
|
76
|
-
/** Show the shared pool under the member's own meter when it is the tighter one. Default true. */
|
|
77
|
-
showWorkspace?: boolean | undefined;
|
|
78
|
-
className?: string | undefined;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export type UsageMeterProps = UsageMeterBaseProps & {
|
|
82
|
-
/**
|
|
83
|
-
* A usage response you already loaded (for example from `useUsage`).
|
|
84
|
-
* Omit it to let the meter read `/usage/me` itself.
|
|
85
|
-
*/
|
|
86
|
-
usage?: WorkspaceUsageResponse | null | undefined;
|
|
87
|
-
client?: UsageClientLike | undefined;
|
|
88
|
-
workspaceId?: string | undefined;
|
|
89
|
-
period?: UsageAllowancePeriod | undefined;
|
|
90
|
-
/** Re-read when this changes (see `useUsage`). */
|
|
91
|
-
refreshKey?: unknown;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* The signed-in member's own usage: how much of their limit is left and when
|
|
96
|
-
* it resets. Shares only by default; hosts opt in to amounts.
|
|
97
|
-
*/
|
|
98
|
-
export function UsageMeter({
|
|
99
|
-
usage,
|
|
100
|
-
client,
|
|
101
|
-
workspaceId,
|
|
102
|
-
period,
|
|
103
|
-
refreshKey,
|
|
104
|
-
...view
|
|
105
|
-
}: UsageMeterProps) {
|
|
106
|
-
if (usage !== undefined) {
|
|
107
|
-
return (
|
|
108
|
-
<UsageMeterView
|
|
109
|
-
{...view}
|
|
110
|
-
summary={usage ? summarizeUsage(usage) : null}
|
|
111
|
-
loading={usage === null}
|
|
112
|
-
/>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
return (
|
|
116
|
-
<LoadingUsageMeter
|
|
117
|
-
{...view}
|
|
118
|
-
client={client}
|
|
119
|
-
workspaceId={workspaceId}
|
|
120
|
-
period={period}
|
|
121
|
-
refreshKey={refreshKey}
|
|
122
|
-
/>
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function LoadingUsageMeter({
|
|
127
|
-
client,
|
|
128
|
-
workspaceId,
|
|
129
|
-
period,
|
|
130
|
-
refreshKey,
|
|
131
|
-
...view
|
|
132
|
-
}: UsageMeterBaseProps &
|
|
133
|
-
Pick<UsageMeterProps, "client" | "workspaceId" | "period" | "refreshKey">) {
|
|
134
|
-
const state = useUsage({ client, workspaceId, period, refreshKey });
|
|
135
|
-
return (
|
|
136
|
-
<UsageMeterView
|
|
137
|
-
{...view}
|
|
138
|
-
summary={state.summary}
|
|
139
|
-
loading={state.loading && !state.summary}
|
|
140
|
-
error={state.summary ? null : state.error}
|
|
141
|
-
onRetry={() => void state.refresh()}
|
|
142
|
-
/>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export type UsageMeterViewProps = UsageMeterBaseProps & {
|
|
147
|
-
summary: UsageSummary | null;
|
|
148
|
-
loading?: boolean | undefined;
|
|
149
|
-
error?: Error | null | undefined;
|
|
150
|
-
onRetry?: (() => void) | undefined;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const FILL: Record<UsageReading["status"], string> = {
|
|
154
|
-
ok: "bg-og-accent",
|
|
155
|
-
warning: "bg-og-status-waiting",
|
|
156
|
-
exhausted: "bg-og-danger",
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
function valueText(
|
|
160
|
-
reading: UsageReading,
|
|
161
|
-
measure: "left" | "used",
|
|
162
|
-
labels: UsageMeterLabels,
|
|
163
|
-
): string {
|
|
164
|
-
if (reading.status === "exhausted") return labels.limitReached;
|
|
165
|
-
return measure === "left"
|
|
166
|
-
? labels.left(usagePercentLabel(Math.max(0, 1 - reading.fraction)))
|
|
167
|
-
: labels.used(usagePercentLabel(reading.fraction));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Presentational meter for a summary you computed (`summarizeUsage`). */
|
|
171
|
-
export function UsageMeterView({
|
|
172
|
-
summary,
|
|
173
|
-
loading = false,
|
|
174
|
-
error = null,
|
|
175
|
-
onRetry,
|
|
176
|
-
measure = "left",
|
|
177
|
-
formatAmount,
|
|
178
|
-
labels: overrides,
|
|
179
|
-
density = "default",
|
|
180
|
-
showWorkspace = true,
|
|
181
|
-
className,
|
|
182
|
-
}: UsageMeterViewProps) {
|
|
183
|
-
const labels = { ...DEFAULT_USAGE_METER_LABELS, ...overrides };
|
|
184
|
-
const compact = density === "compact";
|
|
185
|
-
if (loading || error || !summary) {
|
|
186
|
-
return (
|
|
187
|
-
<div
|
|
188
|
-
className={cn("og-root min-w-0 text-og-sm", className)}
|
|
189
|
-
data-og-usage-meter={error ? "error" : "loading"}
|
|
190
|
-
aria-busy={loading || undefined}
|
|
191
|
-
>
|
|
192
|
-
{error ? (
|
|
193
|
-
<p className="flex flex-wrap items-center gap-x-2 text-og-fg-muted" role="alert">
|
|
194
|
-
{labels.error}
|
|
195
|
-
{onRetry ? (
|
|
196
|
-
<button
|
|
197
|
-
type="button"
|
|
198
|
-
onClick={onRetry}
|
|
199
|
-
className="rounded-og-sm text-og-fg underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-og-accent"
|
|
200
|
-
>
|
|
201
|
-
{labels.retry}
|
|
202
|
-
</button>
|
|
203
|
-
) : null}
|
|
204
|
-
</p>
|
|
205
|
-
) : (
|
|
206
|
-
<span className="text-og-fg-subtle">{labels.loading}</span>
|
|
207
|
-
)}
|
|
208
|
-
</div>
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
const primary = summary.member ?? summary.workspace;
|
|
212
|
-
if (!primary) {
|
|
213
|
-
return (
|
|
214
|
-
<div
|
|
215
|
-
className={cn("og-root min-w-0 text-og-sm text-og-fg-muted", className)}
|
|
216
|
-
data-og-usage-meter="unlimited"
|
|
217
|
-
>
|
|
218
|
-
{compact ? null : <span className="font-medium text-og-fg">{labels.title} · </span>}
|
|
219
|
-
{labels.unlimited}
|
|
220
|
-
</div>
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
const title = summary.member ? labels.title : labels.workspaceTitle;
|
|
224
|
-
const value = valueText(primary, measure, labels);
|
|
225
|
-
// The bar always fills with what's used, so "near the limit" reads as a
|
|
226
|
-
// nearly full bar whichever way the words count.
|
|
227
|
-
const fill = Math.min(1, Math.max(0, primary.fraction));
|
|
228
|
-
const reset = allowanceResetSentence(labels, primary.resetsAt);
|
|
229
|
-
const pool =
|
|
230
|
-
showWorkspace &&
|
|
231
|
-
summary.member &&
|
|
232
|
-
summary.workspace &&
|
|
233
|
-
(summary.binding?.scope === "workspace" || summary.workspace.status !== "ok")
|
|
234
|
-
? summary.workspace
|
|
235
|
-
: null;
|
|
236
|
-
const meterProps = {
|
|
237
|
-
role: "meter" as const,
|
|
238
|
-
"aria-label": title || labels.title,
|
|
239
|
-
"aria-valuemin": 0,
|
|
240
|
-
"aria-valuemax": 100,
|
|
241
|
-
"aria-valuenow": Math.round(fill * 100),
|
|
242
|
-
"aria-valuetext": `${value}. ${reset}${
|
|
243
|
-
pool ? ` ${labels.workspaceLine(valueText(pool, measure, labels))}.` : ""
|
|
244
|
-
}`,
|
|
245
|
-
};
|
|
246
|
-
const bar = (
|
|
247
|
-
<span
|
|
248
|
-
aria-hidden
|
|
249
|
-
className={cn(
|
|
250
|
-
"relative block overflow-hidden rounded-full",
|
|
251
|
-
density === "hero" ? "h-1.5" : "h-1",
|
|
252
|
-
primary.status === "exhausted" ? "bg-og-danger/15" : "bg-og-surface-3",
|
|
253
|
-
compact ? "w-16 shrink-0" : "w-full",
|
|
254
|
-
)}
|
|
255
|
-
>
|
|
256
|
-
<span
|
|
257
|
-
className={cn("absolute inset-y-0 left-0 rounded-full", FILL[primary.status])}
|
|
258
|
-
style={{ width: `${Math.round(fill * 1000) / 10}%` }}
|
|
259
|
-
/>
|
|
260
|
-
</span>
|
|
261
|
-
);
|
|
262
|
-
const valueClass = cn(
|
|
263
|
-
"shrink-0 whitespace-nowrap tabular-nums",
|
|
264
|
-
primary.status === "ok" ? "text-og-fg-muted" : "font-medium",
|
|
265
|
-
primary.status === "warning" && "text-og-status-waiting",
|
|
266
|
-
primary.status === "exhausted" && "text-og-danger",
|
|
267
|
-
);
|
|
268
|
-
if (compact) {
|
|
269
|
-
return (
|
|
270
|
-
<span
|
|
271
|
-
{...meterProps}
|
|
272
|
-
data-og-usage-meter={primary.status}
|
|
273
|
-
title={primary.resetsAt ? `${reset} ${formatAllowanceInstant(primary.resetsAt)}` : reset}
|
|
274
|
-
className={cn("og-root inline-flex min-w-0 items-center gap-2 text-og-xs", className)}
|
|
275
|
-
>
|
|
276
|
-
<span className={valueClass}>{value}</span>
|
|
277
|
-
{bar}
|
|
278
|
-
</span>
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
const amounts = formatAmount
|
|
282
|
-
? labels.amounts(formatAmount(primary.used), formatAmount(primary.limit))
|
|
283
|
-
: null;
|
|
284
|
-
if (density === "hero") {
|
|
285
|
-
// Lead with what's left, even at zero; the chip names the state.
|
|
286
|
-
const headline = formatAmount
|
|
287
|
-
? labels.amountLeft(formatAmount(primary.remaining))
|
|
288
|
-
: labels.left(usagePercentLabel(Math.max(0, 1 - primary.fraction)));
|
|
289
|
-
return (
|
|
290
|
-
<div
|
|
291
|
-
{...meterProps}
|
|
292
|
-
data-og-usage-meter={primary.status}
|
|
293
|
-
className={cn("og-root flex min-w-0 flex-col gap-3", className)}
|
|
294
|
-
>
|
|
295
|
-
{title ? <span className="text-og-sm font-medium text-og-fg-muted">{title}</span> : null}
|
|
296
|
-
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">
|
|
297
|
-
<span className="text-2xl leading-8 font-semibold tracking-[-0.5px] text-og-fg tabular-nums">
|
|
298
|
-
{headline}
|
|
299
|
-
</span>
|
|
300
|
-
{primary.status !== "ok" ? (
|
|
301
|
-
<span
|
|
302
|
-
className={cn(
|
|
303
|
-
"inline-flex h-[22px] items-center gap-1.5 rounded-full border px-2 text-og-xs font-medium",
|
|
304
|
-
primary.status === "warning"
|
|
305
|
-
? "border-og-status-waiting/30 text-og-status-waiting"
|
|
306
|
-
: "border-og-danger/30 text-og-danger",
|
|
307
|
-
)}
|
|
308
|
-
>
|
|
309
|
-
<span
|
|
310
|
-
aria-hidden
|
|
311
|
-
className={cn(
|
|
312
|
-
"size-1.5 rounded-full",
|
|
313
|
-
primary.status === "warning" ? "bg-og-status-waiting" : "bg-og-danger",
|
|
314
|
-
)}
|
|
315
|
-
/>
|
|
316
|
-
{primary.status === "warning" ? labels.nearLimit : labels.limitReached}
|
|
317
|
-
</span>
|
|
318
|
-
) : null}
|
|
319
|
-
</div>
|
|
320
|
-
{bar}
|
|
321
|
-
<p className="text-og-xs text-og-fg-muted tabular-nums">
|
|
322
|
-
{[
|
|
323
|
-
amounts ?? labels.used(usagePercentLabel(primary.fraction)),
|
|
324
|
-
amounts ? `${usagePercentLabel(primary.fraction)}%` : null,
|
|
325
|
-
]
|
|
326
|
-
.filter(Boolean)
|
|
327
|
-
.join(" · ")}{" "}
|
|
328
|
-
·{" "}
|
|
329
|
-
<span title={primary.resetsAt ? formatAllowanceInstant(primary.resetsAt) : undefined}>
|
|
330
|
-
{reset}
|
|
331
|
-
</span>
|
|
332
|
-
</p>
|
|
333
|
-
{pool ? <PoolLine reading={pool} measure={measure} labels={labels} /> : null}
|
|
334
|
-
</div>
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
return (
|
|
338
|
-
<div
|
|
339
|
-
{...meterProps}
|
|
340
|
-
data-og-usage-meter={primary.status}
|
|
341
|
-
className={cn("og-root flex min-w-0 flex-col gap-2 text-og-sm", className)}
|
|
342
|
-
>
|
|
343
|
-
<div className="flex min-w-0 items-baseline justify-between gap-3">
|
|
344
|
-
<span className="min-w-0 truncate font-medium text-og-fg">{title}</span>
|
|
345
|
-
<span className={valueClass}>{value}</span>
|
|
346
|
-
</div>
|
|
347
|
-
{bar}
|
|
348
|
-
<div className="flex min-w-0 flex-wrap items-baseline justify-between gap-x-3 gap-y-1 text-og-xs text-og-fg-subtle">
|
|
349
|
-
<span title={primary.resetsAt ? formatAllowanceInstant(primary.resetsAt) : undefined}>
|
|
350
|
-
{reset}
|
|
351
|
-
</span>
|
|
352
|
-
{amounts ? <span className="tabular-nums">{amounts}</span> : null}
|
|
353
|
-
</div>
|
|
354
|
-
{pool ? <PoolLine reading={pool} measure={measure} labels={labels} /> : null}
|
|
355
|
-
</div>
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function PoolLine({
|
|
360
|
-
reading,
|
|
361
|
-
measure,
|
|
362
|
-
labels,
|
|
363
|
-
}: {
|
|
364
|
-
reading: UsageReading;
|
|
365
|
-
measure: "left" | "used";
|
|
366
|
-
labels: UsageMeterLabels;
|
|
367
|
-
}): ReactNode {
|
|
368
|
-
return (
|
|
369
|
-
<p
|
|
370
|
-
className={cn(
|
|
371
|
-
"text-og-xs",
|
|
372
|
-
reading.status === "exhausted"
|
|
373
|
-
? "font-medium text-og-danger"
|
|
374
|
-
: reading.status === "warning"
|
|
375
|
-
? "text-og-status-waiting"
|
|
376
|
-
: "text-og-fg-muted",
|
|
377
|
-
)}
|
|
378
|
-
>
|
|
379
|
-
{labels.workspaceLine(valueText(reading, measure, labels))}
|
|
380
|
-
</p>
|
|
381
|
-
);
|
|
382
|
-
}
|
package/src/hooks/use-usage.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type { OpenGeniClient } from "@opengeni/sdk";
|
|
2
|
-
import {
|
|
3
|
-
getMyUsage,
|
|
4
|
-
type UsageAllowancePeriod,
|
|
5
|
-
type WorkspaceUsageResponse,
|
|
6
|
-
} from "@opengeni/sdk/usage-allowances";
|
|
7
|
-
import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
|
8
|
-
|
|
9
|
-
import { OpenGeniContext } from "../session-context";
|
|
10
|
-
import { summarizeUsage, type UsageSummary } from "../usage/summary";
|
|
11
|
-
import { usePolledValue } from "./internal";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Anything that can read the signed-in member's own usage: the root SDK client
|
|
15
|
-
* pointed at your session proxy (`getMyUsage`), or any client exposing
|
|
16
|
-
* `requestJson` (for example the narrower browser client).
|
|
17
|
-
*/
|
|
18
|
-
export type UsageClientLike =
|
|
19
|
-
| Pick<OpenGeniClient, "getMyUsage">
|
|
20
|
-
| Pick<OpenGeniClient, "requestJson">;
|
|
21
|
-
|
|
22
|
-
export type UseUsageOptions = {
|
|
23
|
-
/** Defaults to the `<OpenGeniProvider>` client. */
|
|
24
|
-
client?: UsageClientLike | undefined;
|
|
25
|
-
/** Defaults to the `<OpenGeniProvider>` workspace. */
|
|
26
|
-
workspaceId?: string | undefined;
|
|
27
|
-
/** `"current"` (default) or a `YYYY-MM` allowance period. */
|
|
28
|
-
period?: UsageAllowancePeriod | undefined;
|
|
29
|
-
/** Re-read on an interval. Off by default; usage changes only when work settles. */
|
|
30
|
-
pollIntervalMs?: number | undefined;
|
|
31
|
-
enabled?: boolean | undefined;
|
|
32
|
-
/**
|
|
33
|
-
* Re-read whenever this value changes, keeping the current reading while it
|
|
34
|
-
* loads. Pass something that changes when work settles (a turn count, the
|
|
35
|
-
* session status) so the meter follows real usage without polling.
|
|
36
|
-
*/
|
|
37
|
-
refreshKey?: unknown;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export type UseUsageResult = {
|
|
41
|
-
/** The raw `/usage/me` response: the workspace pool and your own row. */
|
|
42
|
-
usage: WorkspaceUsageResponse | null;
|
|
43
|
-
/** What to tell this member; null until loaded. */
|
|
44
|
-
summary: UsageSummary | null;
|
|
45
|
-
loading: boolean;
|
|
46
|
-
error: Error | null;
|
|
47
|
-
refresh: () => Promise<void>;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
function readOwnUsage(
|
|
51
|
-
client: UsageClientLike,
|
|
52
|
-
workspaceId: string,
|
|
53
|
-
period: UsageAllowancePeriod | undefined,
|
|
54
|
-
signal: AbortSignal | undefined,
|
|
55
|
-
): Promise<WorkspaceUsageResponse> {
|
|
56
|
-
const query = period === undefined ? {} : { period };
|
|
57
|
-
const options = signal ? { signal } : {};
|
|
58
|
-
if ("getMyUsage" in client && typeof client.getMyUsage === "function") {
|
|
59
|
-
return client.getMyUsage(workspaceId, query, options);
|
|
60
|
-
}
|
|
61
|
-
if ("requestJson" in client && typeof client.requestJson === "function") {
|
|
62
|
-
return getMyUsage(client, workspaceId, query, options);
|
|
63
|
-
}
|
|
64
|
-
throw new Error("@opengeni/react: the usage client needs getMyUsage or requestJson.");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The signed-in member's own usage in one workspace, read through `/usage/me`
|
|
69
|
-
* (the session proxy serves exactly this route). Never reads another member.
|
|
70
|
-
*/
|
|
71
|
-
export function useUsage(options: UseUsageOptions = {}): UseUsageResult {
|
|
72
|
-
const context = useContext(OpenGeniContext);
|
|
73
|
-
const client = (options.client ?? context?.client) as UsageClientLike | undefined;
|
|
74
|
-
const workspaceId = options.workspaceId ?? context?.workspaceId;
|
|
75
|
-
if (!client || !workspaceId) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
"@opengeni/react: useUsage needs a client and workspace. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId }.",
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
const period = options.period;
|
|
81
|
-
const load = useCallback(
|
|
82
|
-
(signal?: AbortSignal) => readOwnUsage(client, workspaceId, period, signal),
|
|
83
|
-
[client, workspaceId, period],
|
|
84
|
-
);
|
|
85
|
-
const state = usePolledValue(load, {
|
|
86
|
-
pollIntervalMs: options.pollIntervalMs,
|
|
87
|
-
enabled: options.enabled,
|
|
88
|
-
});
|
|
89
|
-
const refreshKey = options.refreshKey;
|
|
90
|
-
const seenKey = useRef(refreshKey);
|
|
91
|
-
const refresh = state.refresh;
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
if (Object.is(seenKey.current, refreshKey)) return;
|
|
94
|
-
seenKey.current = refreshKey;
|
|
95
|
-
void refresh();
|
|
96
|
-
}, [refreshKey, refresh]);
|
|
97
|
-
const usage = state.data;
|
|
98
|
-
const summary = useMemo(() => (usage ? summarizeUsage(usage) : null), [usage]);
|
|
99
|
-
return { usage, summary, loading: state.loading, error: state.error, refresh };
|
|
100
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import type { AllowanceExhaustedRefusal } from "@opengeni/sdk";
|
|
2
|
-
import { GaugeIcon } from "lucide-react";
|
|
3
|
-
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
4
|
-
|
|
5
|
-
import { cn } from "../lib/cn";
|
|
6
|
-
import {
|
|
7
|
-
allowanceLabels,
|
|
8
|
-
allowanceResetSentence,
|
|
9
|
-
formatAllowanceInstant,
|
|
10
|
-
type AllowanceLabels,
|
|
11
|
-
} from "../usage/allowance-copy";
|
|
12
|
-
import { useEntranceAnimation } from "./entrance";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Replace the built-in "usage limit reached" row. Return `undefined` to keep
|
|
16
|
-
* the default for this refusal (for example, only customize member limits).
|
|
17
|
-
*/
|
|
18
|
-
export type RenderAllowanceExhausted = (
|
|
19
|
-
refusal: AllowanceExhaustedRefusal,
|
|
20
|
-
context: { labels: AllowanceLabels; defaultRow: ReactNode },
|
|
21
|
-
) => ReactNode | undefined;
|
|
22
|
-
|
|
23
|
-
type AllowancePresentation = {
|
|
24
|
-
render: RenderAllowanceExhausted | undefined;
|
|
25
|
-
labels: AllowanceLabels;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const AllowancePresentationContext = createContext<AllowancePresentation>({
|
|
29
|
-
render: undefined,
|
|
30
|
-
labels: allowanceLabels(),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
export function AllowancePresentationProvider({
|
|
34
|
-
render,
|
|
35
|
-
labels,
|
|
36
|
-
children,
|
|
37
|
-
}: {
|
|
38
|
-
render: RenderAllowanceExhausted | undefined;
|
|
39
|
-
labels: Partial<AllowanceLabels> | undefined;
|
|
40
|
-
children: ReactNode;
|
|
41
|
-
}) {
|
|
42
|
-
const value = useMemo(() => ({ render, labels: allowanceLabels(labels) }), [render, labels]);
|
|
43
|
-
return (
|
|
44
|
-
<AllowancePresentationContext.Provider value={value}>
|
|
45
|
-
{children}
|
|
46
|
-
</AllowancePresentationContext.Provider>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* A usage ceiling stopped further work. Calm by design: nothing failed, the
|
|
52
|
-
* conversation is kept, and the row says who can raise the limit and when it
|
|
53
|
-
* resets. Never the server's prose or a member identifier.
|
|
54
|
-
*/
|
|
55
|
-
export function AllowanceExhaustedRow({ refusal }: { refusal: AllowanceExhaustedRefusal }) {
|
|
56
|
-
const { render, labels } = useContext(AllowancePresentationContext);
|
|
57
|
-
const enter = useEntranceAnimation();
|
|
58
|
-
const workspace = refusal.scope === "workspace";
|
|
59
|
-
const defaultRow = (
|
|
60
|
-
<div
|
|
61
|
-
className={cn(
|
|
62
|
-
enter && "animate-og-enter",
|
|
63
|
-
"flex items-start gap-2.5 rounded-og-md border border-og-status-waiting/35 bg-og-status-waiting/8 px-3.5 py-2.5 text-og-menu text-og-fg",
|
|
64
|
-
)}
|
|
65
|
-
role="status"
|
|
66
|
-
data-og-allowance-exhausted={refusal.scope}
|
|
67
|
-
>
|
|
68
|
-
<GaugeIcon aria-hidden className="mt-0.5 size-4 shrink-0 text-og-status-waiting" />
|
|
69
|
-
<div className="min-w-0 flex-1">
|
|
70
|
-
<p className="font-medium">
|
|
71
|
-
{workspace ? labels.workspaceLimitReachedTitle : labels.memberLimitReachedTitle}
|
|
72
|
-
</p>
|
|
73
|
-
<p className="mt-0.5 text-og-fg-muted">
|
|
74
|
-
{workspace ? labels.workspaceRemedy : labels.memberRemedy}{" "}
|
|
75
|
-
<span
|
|
76
|
-
title={refusal.resetsAt ? formatAllowanceInstant(refusal.resetsAt) : undefined}
|
|
77
|
-
className="whitespace-nowrap"
|
|
78
|
-
>
|
|
79
|
-
{allowanceResetSentence(labels, refusal.resetsAt)}
|
|
80
|
-
</span>
|
|
81
|
-
</p>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
);
|
|
85
|
-
return <>{render?.(refusal, { labels, defaultRow }) ?? defaultRow}</>;
|
|
86
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// Shared, replaceable words for usage allowances. The timeline refusal row
|
|
2
|
-
// (session graph) and the opt-in `@opengeni/react/usage` surfaces both read
|
|
3
|
-
// these, so a host that renames "workspace" or "admin" does it once.
|
|
4
|
-
//
|
|
5
|
-
// Product rule: never show raw credit amounts here. Hosts that want money or
|
|
6
|
-
// plan multiples opt in with `formatAmount` on the meter components.
|
|
7
|
-
|
|
8
|
-
/** Which ceiling ran out: the shared workspace pool, or one member's share of it. */
|
|
9
|
-
export type AllowanceScope = "workspace" | "member";
|
|
10
|
-
|
|
11
|
-
export type AllowanceLabels = {
|
|
12
|
-
/** "Usage limit reached" for a member ceiling. */
|
|
13
|
-
memberLimitReachedTitle: string;
|
|
14
|
-
/** "Workspace usage limit reached" for the shared pool. */
|
|
15
|
-
workspaceLimitReachedTitle: string;
|
|
16
|
-
/** Who can fix a member ceiling. */
|
|
17
|
-
memberRemedy: string;
|
|
18
|
-
/** Who can fix the workspace pool. */
|
|
19
|
-
workspaceRemedy: string;
|
|
20
|
-
/** "Resets Nov 1". `date` is already formatted by `formatDate`. */
|
|
21
|
-
resets: (date: string) => string;
|
|
22
|
-
/** Shown when a ceiling has no automatic reset. */
|
|
23
|
-
noReset: string;
|
|
24
|
-
/** Formats a reset instant for `resets`. Defaults to a short local date. */
|
|
25
|
-
formatDate: (iso: string) => string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export function formatAllowanceDate(iso: string, now: Date = new Date()): string {
|
|
29
|
-
const date = new Date(iso);
|
|
30
|
-
if (Number.isNaN(date.getTime())) return iso;
|
|
31
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
32
|
-
month: "short",
|
|
33
|
-
day: "numeric",
|
|
34
|
-
...(date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
|
|
35
|
-
}).format(date);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Full local date and time, for a title/tooltip next to the short date. */
|
|
39
|
-
export function formatAllowanceInstant(iso: string): string {
|
|
40
|
-
const date = new Date(iso);
|
|
41
|
-
if (Number.isNaN(date.getTime())) return iso;
|
|
42
|
-
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(
|
|
43
|
-
date,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const DEFAULT_ALLOWANCE_LABELS: AllowanceLabels = {
|
|
48
|
-
memberLimitReachedTitle: "Usage limit reached",
|
|
49
|
-
workspaceLimitReachedTitle: "Workspace usage limit reached",
|
|
50
|
-
memberRemedy: "A workspace admin can raise this limit.",
|
|
51
|
-
workspaceRemedy: "An organization admin can raise the workspace budget.",
|
|
52
|
-
resets: (date) => `Resets ${date}.`,
|
|
53
|
-
noReset: "It doesn't reset automatically.",
|
|
54
|
-
formatDate: (iso) => formatAllowanceDate(iso),
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export function allowanceLabels(overrides?: Partial<AllowanceLabels>): AllowanceLabels {
|
|
58
|
-
return overrides ? { ...DEFAULT_ALLOWANCE_LABELS, ...overrides } : DEFAULT_ALLOWANCE_LABELS;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** "Resets Nov 1." or the no-reset sentence. */
|
|
62
|
-
export function allowanceResetSentence(labels: AllowanceLabels, resetsAt: string | null): string {
|
|
63
|
-
return resetsAt === null ? labels.noReset : labels.resets(labels.formatDate(resetsAt));
|
|
64
|
-
}
|