@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.
Files changed (157) hide show
  1. package/README.md +3 -48
  2. package/dist/artifacts-document.js +2 -3
  3. package/dist/artifacts-presentation.js +2 -3
  4. package/dist/artifacts-spreadsheet.js +2 -2
  5. package/dist/artifacts.d.ts +0 -1
  6. package/dist/artifacts.js +1402 -505
  7. package/dist/artifacts.js.map +1 -1
  8. package/dist/{browser-viewer-R6KK5VKR.js → browser-viewer-2E6R2Y4W.js} +4 -4
  9. package/dist/{chunk-WUISZ3WU.js → chunk-3E2AITTZ.js} +28 -32
  10. package/dist/{chunk-WUISZ3WU.js.map → chunk-3E2AITTZ.js.map} +1 -1
  11. package/dist/{chunk-PWUILT2U.js → chunk-5AZSK6MV.js} +2 -2
  12. package/dist/{chunk-Y2PRMYLC.js → chunk-DUEBTL3N.js} +3 -4
  13. package/dist/{chunk-Y2PRMYLC.js.map → chunk-DUEBTL3N.js.map} +1 -1
  14. package/dist/{chunk-7UAMYXJY.js → chunk-KNDIXZN6.js} +18 -25
  15. package/dist/{chunk-7UAMYXJY.js.map → chunk-KNDIXZN6.js.map} +1 -1
  16. package/dist/{chunk-P4G6K2E2.js → chunk-L4OSXQ6H.js} +884 -1119
  17. package/dist/chunk-L4OSXQ6H.js.map +1 -0
  18. package/dist/{chunk-LLIH7Q4B.js → chunk-S5JQ4R22.js} +19 -47
  19. package/dist/chunk-S5JQ4R22.js.map +1 -0
  20. package/dist/{chunk-A4UJB2MM.js → chunk-SEEVCT36.js} +2 -2
  21. package/dist/{chunk-UXJCQ5GH.js → chunk-UDD6DLLT.js} +1 -6
  22. package/dist/chunk-UDD6DLLT.js.map +1 -0
  23. package/dist/chunk-UGMKPUEM.js +299 -0
  24. package/dist/chunk-UGMKPUEM.js.map +1 -0
  25. package/dist/{chunk-ZCZHKJKZ.js → chunk-USXVQACU.js} +3 -3
  26. package/dist/{chunk-VABBF52G.js → chunk-V2ABIVYJ.js} +6 -70
  27. package/dist/chunk-V2ABIVYJ.js.map +1 -0
  28. package/dist/{chunk-TVO6GXR6.js → chunk-X4HIN6TV.js} +162 -71
  29. package/dist/chunk-X4HIN6TV.js.map +1 -0
  30. package/dist/{chunk-IIDEIBMC.js → chunk-XEJEOAOV.js} +38 -49
  31. package/dist/chunk-XEJEOAOV.js.map +1 -0
  32. package/dist/{chunk-ZKLCPBWN.js → chunk-Y3QTZN2F.js} +5 -5
  33. package/dist/{chunk-73BUNGLF.js → chunk-YZ3YYMOM.js} +1 -1
  34. package/dist/components/artifacts/editable-artifact-ui.d.ts +1 -2
  35. package/dist/components/artifacts/index.d.ts +0 -7
  36. package/dist/components/browser-select-control.d.ts +1 -3
  37. package/dist/components/message-timeline.d.ts +1 -18
  38. package/dist/components/open-geni-links.d.ts +0 -16
  39. package/dist/components/session-conversation.d.ts +1 -20
  40. package/dist/composer.js +3 -3
  41. package/dist/{computer-viewer-DJHPSP2J.js → computer-viewer-WMJE6OA3.js} +3 -3
  42. package/dist/connect.js +6 -7
  43. package/dist/connect.js.map +1 -1
  44. package/dist/hooks/latest-question.d.ts +1 -1
  45. package/dist/index.d.ts +2 -5
  46. package/dist/index.js +28 -39
  47. package/dist/index.js.map +1 -1
  48. package/dist/interaction.js +4 -4
  49. package/dist/{latest-question-KGCDK3XX.js → latest-question-QB4EUCW5.js} +3 -3
  50. package/dist/lib/format.d.ts +0 -3
  51. package/dist/machines.js +2 -2
  52. package/dist/{markdown-table-layout-QXSLK4BY.js → markdown-table-layout-PFUEZ2U4.js} +5 -37
  53. package/dist/markdown-table-layout-PFUEZ2U4.js.map +1 -0
  54. package/dist/{sandbox-files-4PMXDSDV.js → sandbox-files-PXPIZAFG.js} +2 -2
  55. package/dist/session-ui.d.ts +2 -5
  56. package/dist/session-ui.js +13 -23
  57. package/dist/session-ui.js.map +1 -1
  58. package/dist/session.js +3 -3
  59. package/dist/sites.js +289 -4
  60. package/dist/sites.js.map +1 -1
  61. package/dist/timeline/types.d.ts +1 -7
  62. package/dist/{timeline-annotation-selection-DMLMM2TX.js → timeline-annotation-selection-ODLVF4OI.js} +1 -6
  63. package/dist/timeline-annotation-selection-ODLVF4OI.js.map +1 -0
  64. package/package.json +3 -7
  65. package/src/artifacts.ts +0 -37
  66. package/src/components/artifacts/editable-artifact-ui.tsx +1 -3
  67. package/src/components/artifacts/editable-artifact-workbench.tsx +2 -5
  68. package/src/components/artifacts/editable-document.tsx +36 -34
  69. package/src/components/artifacts/editable-presentation.tsx +16 -21
  70. package/src/components/artifacts/index.ts +0 -41
  71. package/src/components/browser-select-control.tsx +1 -4
  72. package/src/components/browser-viewer.tsx +22 -81
  73. package/src/components/computer-viewer.tsx +23 -51
  74. package/src/components/markdown-table-layout.ts +5 -49
  75. package/src/components/markdown.tsx +2 -5
  76. package/src/components/message-timeline.tsx +434 -515
  77. package/src/components/open-geni-links.tsx +0 -24
  78. package/src/components/session-conversation.tsx +20 -131
  79. package/src/components/timeline-annotation-selection.tsx +0 -7
  80. package/src/hooks/use-available-models.ts +1 -1
  81. package/src/index.ts +0 -9
  82. package/src/lib/format.ts +0 -13
  83. package/src/session-ui.ts +0 -9
  84. package/src/timeline/projection.ts +4 -78
  85. package/src/timeline/types.ts +0 -7
  86. package/styles/compiled.css +0 -325
  87. package/dist/chat-interactive-block-VAGABN7J.js +0 -11
  88. package/dist/chunk-5CVXZA3Y.js +0 -292
  89. package/dist/chunk-5CVXZA3Y.js.map +0 -1
  90. package/dist/chunk-B3V6TG77.js +0 -41
  91. package/dist/chunk-B3V6TG77.js.map +0 -1
  92. package/dist/chunk-IIDEIBMC.js.map +0 -1
  93. package/dist/chunk-LLIH7Q4B.js.map +0 -1
  94. package/dist/chunk-P4G6K2E2.js.map +0 -1
  95. package/dist/chunk-RAIQYITC.js +0 -1772
  96. package/dist/chunk-RAIQYITC.js.map +0 -1
  97. package/dist/chunk-SCOTPE2I.js +0 -589
  98. package/dist/chunk-SCOTPE2I.js.map +0 -1
  99. package/dist/chunk-TVO6GXR6.js.map +0 -1
  100. package/dist/chunk-UXJCQ5GH.js.map +0 -1
  101. package/dist/chunk-VABBF52G.js.map +0 -1
  102. package/dist/chunk-WEHMPOXK.js +0 -39
  103. package/dist/chunk-WEHMPOXK.js.map +0 -1
  104. package/dist/chunk-XCHWDJ22.js +0 -107
  105. package/dist/chunk-XCHWDJ22.js.map +0 -1
  106. package/dist/components/artifacts/artifact-chrome.d.ts +0 -120
  107. package/dist/components/artifacts/artifact-sandbox.d.ts +0 -27
  108. package/dist/components/artifacts/chat-interactive-block.d.ts +0 -29
  109. package/dist/components/artifacts/deferred-chat-media.d.ts +0 -15
  110. package/dist/components/artifacts/editable-artifact-view.d.ts +0 -46
  111. package/dist/components/artifacts/latest-task-runner.d.ts +0 -15
  112. package/dist/components/artifacts/session-artifact-viewer.d.ts +0 -39
  113. package/dist/components/artifacts/site-view.d.ts +0 -23
  114. package/dist/components/timeline-pill-geometry.d.ts +0 -6
  115. package/dist/components/timeline-pill-placement.d.ts +0 -18
  116. package/dist/components/usage-limit-notice.d.ts +0 -49
  117. package/dist/components/usage-member-list.d.ts +0 -91
  118. package/dist/components/usage-meter.d.ts +0 -73
  119. package/dist/hooks/use-usage.d.ts +0 -40
  120. package/dist/markdown-table-layout-QXSLK4BY.js.map +0 -1
  121. package/dist/sandbox-files-4PMXDSDV.js.map +0 -1
  122. package/dist/timeline/allowance-exhausted-row.d.ts +0 -24
  123. package/dist/timeline-annotation-selection-DMLMM2TX.js.map +0 -1
  124. package/dist/timeline-pill-geometry-2G6EK47V.js +0 -73
  125. package/dist/timeline-pill-geometry-2G6EK47V.js.map +0 -1
  126. package/dist/usage/allowance-copy.d.ts +0 -25
  127. package/dist/usage/summary.d.ts +0 -35
  128. package/dist/usage.d.ts +0 -13
  129. package/dist/usage.js +0 -1073
  130. package/dist/usage.js.map +0 -1
  131. package/src/components/artifacts/artifact-chrome.tsx +0 -413
  132. package/src/components/artifacts/artifact-sandbox.tsx +0 -185
  133. package/src/components/artifacts/chat-interactive-block.tsx +0 -260
  134. package/src/components/artifacts/deferred-chat-media.tsx +0 -67
  135. package/src/components/artifacts/editable-artifact-view.tsx +0 -187
  136. package/src/components/artifacts/latest-task-runner.ts +0 -49
  137. package/src/components/artifacts/session-artifact-viewer.tsx +0 -376
  138. package/src/components/artifacts/site-view.tsx +0 -116
  139. package/src/components/timeline-pill-geometry.ts +0 -112
  140. package/src/components/timeline-pill-placement.ts +0 -81
  141. package/src/components/usage-limit-notice.tsx +0 -179
  142. package/src/components/usage-member-list.tsx +0 -792
  143. package/src/components/usage-meter.tsx +0 -382
  144. package/src/hooks/use-usage.ts +0 -100
  145. package/src/timeline/allowance-exhausted-row.tsx +0 -86
  146. package/src/usage/allowance-copy.ts +0 -64
  147. package/src/usage/summary.ts +0 -93
  148. package/src/usage.ts +0 -50
  149. /package/dist/{browser-viewer-R6KK5VKR.js.map → browser-viewer-2E6R2Y4W.js.map} +0 -0
  150. /package/dist/{chunk-PWUILT2U.js.map → chunk-5AZSK6MV.js.map} +0 -0
  151. /package/dist/{chunk-A4UJB2MM.js.map → chunk-SEEVCT36.js.map} +0 -0
  152. /package/dist/{chunk-ZCZHKJKZ.js.map → chunk-USXVQACU.js.map} +0 -0
  153. /package/dist/{chunk-ZKLCPBWN.js.map → chunk-Y3QTZN2F.js.map} +0 -0
  154. /package/dist/{chunk-73BUNGLF.js.map → chunk-YZ3YYMOM.js.map} +0 -0
  155. /package/dist/{chat-interactive-block-VAGABN7J.js.map → computer-viewer-WMJE6OA3.js.map} +0 -0
  156. /package/dist/{latest-question-KGCDK3XX.js.map → latest-question-QB4EUCW5.js.map} +0 -0
  157. /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
- }
@@ -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
- }