@hachej/boring-core 0.1.42 → 0.1.44
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/PostgresMeteringStore-CzNv6xil.d.ts +224 -0
- package/dist/app/front/index.d.ts +216 -3
- package/dist/app/front/index.js +834 -43
- package/dist/app/server/index.d.ts +3 -3
- package/dist/app/server/index.js +33 -8
- package/dist/{authHook-DUqyxueY.d.ts → authHook-CzBsMwwM.d.ts} +2 -2
- package/dist/{chunk-C3YMOITB.js → chunk-I56OTSPB.js} +649 -6
- package/dist/{chunk-H5KU6R6Y.js → chunk-LIBHVT7V.js} +5 -1
- package/dist/{chunk-GZVKZD4P.js → chunk-UM5SHYIS.js} +11 -2
- package/dist/{chunk-MLTJKZL4.js → chunk-VYXEXOCO.js} +21 -10
- package/dist/{connection-AL8KSENV.d.ts → connection-C5SiqoNc.d.ts} +1 -1
- package/dist/front/index.d.ts +15 -2
- package/dist/front/index.js +2 -2
- package/dist/server/db/index.d.ts +4 -4
- package/dist/server/db/index.js +6 -2
- package/dist/server/index.d.ts +594 -7
- package/dist/server/index.js +1467 -4
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/{types-CbMOXLBf.d.ts → types-CWtJ4kgd.d.ts} +3 -0
- package/drizzle/0011_usage_metering.sql +57 -0
- package/drizzle/0012_credit_purchases.sql +9 -0
- package/drizzle/0013_credit_purchase_lifecycle.sql +28 -0
- package/drizzle/0014_reservation_charge_on_expire.sql +7 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +4 -4
- package/dist/migrate-B4dwdtGP.d.ts +0 -8
package/dist/app/front/index.js
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
useSignIn,
|
|
9
9
|
useSignUp,
|
|
10
10
|
useWorkspaceRouteStatus
|
|
11
|
-
} from "../../chunk-
|
|
11
|
+
} from "../../chunk-VYXEXOCO.js";
|
|
12
12
|
import "../../chunk-HYNKZSTF.js";
|
|
13
|
-
import "../../chunk-
|
|
13
|
+
import "../../chunk-LIBHVT7V.js";
|
|
14
14
|
import "../../chunk-MLKGABMK.js";
|
|
15
15
|
|
|
16
16
|
// src/app/front/CoreWorkspaceAgentFront.tsx
|
|
17
|
-
import { useEffect as
|
|
17
|
+
import { useEffect as useEffect3, useMemo, useState as useState3 } from "react";
|
|
18
18
|
import { Navigate, Route, useLocation as useLocation2, useParams } from "react-router-dom";
|
|
19
19
|
import {
|
|
20
20
|
WorkspaceAgentFront as WorkspaceAgentFront2
|
|
@@ -63,14 +63,17 @@ function ChatFirstAuthenticatedShell({
|
|
|
63
63
|
provisionWorkspace: false,
|
|
64
64
|
bootPreloadPaths: [],
|
|
65
65
|
bridgeEndpoint: null,
|
|
66
|
-
excludeDefaults: ["filesystem"],
|
|
67
|
-
plugins: [],
|
|
66
|
+
excludeDefaults: workspaceProps.excludeDefaults ?? ["filesystem"],
|
|
67
|
+
plugins: workspaceProps.plugins ?? [],
|
|
68
68
|
catalogs: [],
|
|
69
69
|
commands: [],
|
|
70
70
|
persistenceEnabled: false,
|
|
71
71
|
navEnabled: false,
|
|
72
72
|
defaultNavOpen: false,
|
|
73
|
-
defaultSurfaceOpen: false,
|
|
73
|
+
defaultSurfaceOpen: workspaceProps.defaultSurfaceOpen ?? false,
|
|
74
|
+
defaultWorkbenchLeftTab: workspaceProps.defaultWorkbenchLeftTab,
|
|
75
|
+
defaultWorkbenchLeftOpen: false,
|
|
76
|
+
surfaceInitialPanels: workspaceProps.surfaceInitialPanels,
|
|
74
77
|
beforeShell: showComposerBlocker ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
75
78
|
workspaceProps.beforeShell,
|
|
76
79
|
/* @__PURE__ */ jsx(ChatFirstComposerBlocker, {})
|
|
@@ -91,8 +94,10 @@ function ChatFirstAuthenticatedShell({
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
// src/app/front/chatFirst/ChatFirstPublicShell.tsx
|
|
94
|
-
import { useState as useState2 } from "react";
|
|
97
|
+
import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
95
98
|
import { useLocation } from "react-router-dom";
|
|
99
|
+
import { builtinCommands } from "@hachej/boring-agent/front";
|
|
100
|
+
import { postUiCommand } from "@hachej/boring-workspace";
|
|
96
101
|
|
|
97
102
|
// src/app/front/chatFirst/AuthCard.tsx
|
|
98
103
|
import { useState } from "react";
|
|
@@ -111,7 +116,6 @@ function AuthCard({
|
|
|
111
116
|
const [password, setPassword] = useState("");
|
|
112
117
|
const [error, setError] = useState(null);
|
|
113
118
|
const [submitting, setSubmitting] = useState(false);
|
|
114
|
-
const forgotPasswordHref = `${routes.forgotPassword}?redirect=${encodeURIComponent(returnTo)}`;
|
|
115
119
|
async function submit(event) {
|
|
116
120
|
event.preventDefault();
|
|
117
121
|
setError(null);
|
|
@@ -130,23 +134,20 @@ function AuthCard({
|
|
|
130
134
|
setSubmitting(false);
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
|
-
return /* @__PURE__ */ jsxs2("div", { className: "w-full max-w-
|
|
137
|
+
return /* @__PURE__ */ jsxs2("div", { className: "w-full max-w-xs rounded-2xl border border-border bg-card p-3 shadow-2xl", children: [
|
|
134
138
|
onClose ? /* @__PURE__ */ jsx2("div", { className: "mb-3 flex justify-end", children: /* @__PURE__ */ jsx2("button", { type: "button", className: "rounded-full px-2 py-1 text-sm text-muted-foreground hover:bg-muted", onClick: onClose, "aria-label": "Close sign in", children: "\xD7" }) }) : null,
|
|
135
|
-
/* @__PURE__ */
|
|
136
|
-
/* @__PURE__ */ jsx2("p", { className: "mt-2 text-center text-sm text-muted-foreground", children: "Keep your draft and unlock the full workspace." }),
|
|
137
|
-
/* @__PURE__ */ jsxs2("div", { className: "mt-4 grid grid-cols-2 rounded-xl bg-muted p-1 text-sm", children: [
|
|
139
|
+
/* @__PURE__ */ jsxs2("div", { className: "grid grid-cols-2 rounded-xl bg-muted p-1 text-sm", children: [
|
|
138
140
|
/* @__PURE__ */ jsx2("button", { type: "button", className: `rounded-lg px-3 py-2 ${mode === "signin" ? "bg-background shadow-sm" : "text-muted-foreground"}`, onClick: () => setMode("signin"), children: "Sign in" }),
|
|
139
141
|
/* @__PURE__ */ jsx2("button", { type: "button", className: `rounded-lg px-3 py-2 ${mode === "signup" ? "bg-background shadow-sm" : "text-muted-foreground"}`, onClick: () => setMode("signup"), children: "Sign up" })
|
|
140
142
|
] }),
|
|
141
|
-
/* @__PURE__ */ jsxs2("form", { className: "mt-
|
|
143
|
+
/* @__PURE__ */ jsxs2("form", { className: "mt-3 space-y-2", onSubmit: submit, children: [
|
|
142
144
|
error ? /* @__PURE__ */ jsx2("div", { className: "rounded-lg border border-destructive/30 bg-destructive/10 p-2 text-sm text-destructive", role: "alert", children: error }) : null,
|
|
143
145
|
mode === "signup" ? /* @__PURE__ */ jsx2("input", { className: "w-full rounded-xl border border-border bg-background px-3 py-2.5 text-sm outline-none focus:border-ring", placeholder: "Name", value: name, onChange: (event) => setName(event.currentTarget.value) }) : null,
|
|
144
146
|
/* @__PURE__ */ jsx2("input", { className: "w-full rounded-xl border border-border bg-background px-3 py-2.5 text-sm outline-none focus:border-ring", type: "email", autoComplete: "email", placeholder: "Email", value: email, onChange: (event) => setEmail(event.currentTarget.value), required: true }),
|
|
145
147
|
/* @__PURE__ */ jsx2("input", { className: "w-full rounded-xl border border-border bg-background px-3 py-2.5 text-sm outline-none focus:border-ring", type: "password", autoComplete: mode === "signin" ? "current-password" : "new-password", placeholder: "Password", value: password, onChange: (event) => setPassword(event.currentTarget.value), required: true }),
|
|
146
|
-
mode === "signin" ? /* @__PURE__ */ jsx2("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx2("a", { href:
|
|
148
|
+
mode === "signin" ? /* @__PURE__ */ jsx2("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx2("a", { href: routes.forgotPassword, className: "text-xs text-muted-foreground hover:underline", children: "Forgot password?" }) }) : null,
|
|
147
149
|
/* @__PURE__ */ jsx2("button", { type: "submit", className: "w-full rounded-xl bg-primary px-3 py-2.5 text-sm font-medium text-primary-foreground disabled:opacity-50", disabled: submitting, children: submitting ? "Please wait\u2026" : mode === "signin" ? "Continue with email" : "Create account" })
|
|
148
|
-
] })
|
|
149
|
-
/* @__PURE__ */ jsx2("p", { className: "mt-4 text-center text-xs text-muted-foreground", children: "By continuing, you agree to continue into your private workspace." })
|
|
150
|
+
] })
|
|
150
151
|
] });
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -233,17 +234,17 @@ function workspaceIdFromPath(pathname, workspaceRoute, workspaceIdParam) {
|
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
// src/app/front/chatFirst/ChatFirstPublicShell.tsx
|
|
236
|
-
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
237
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
237
238
|
var defaultPublicEmptyState = {
|
|
238
|
-
eyebrow: "
|
|
239
|
-
title: "
|
|
240
|
-
description: "
|
|
239
|
+
eyebrow: "Private AI workspace",
|
|
240
|
+
title: "Start in a secure workspace",
|
|
241
|
+
description: "Draft a task, then continue in an isolated workspace where the assistant can inspect files, make changes, and run commands only inside that workspace."
|
|
241
242
|
};
|
|
242
243
|
var defaultPublicSuggestions = [
|
|
243
|
-
{ label: "
|
|
244
|
-
{ label: "
|
|
245
|
-
{ label: "
|
|
246
|
-
{ label: "
|
|
244
|
+
{ label: "Audit a workspace", hint: "Map files, risks, and next steps", prompt: "Inspect this workspace, summarize what is here, and identify the safest next steps." },
|
|
245
|
+
{ label: "Make a safe change", hint: "Edit files and verify the result", prompt: "Make a small, safe change, run the relevant checks, and summarize the diff." },
|
|
246
|
+
{ label: "Explain access boundaries", hint: "Files, commands, and model use", prompt: "Explain what you can access in this workspace, what commands you may run, and how model processing works." },
|
|
247
|
+
{ label: "Plan a project", hint: "Turn an idea into milestones", prompt: "Turn my idea into a concrete workspace plan with milestones, risks, and verification steps." }
|
|
247
248
|
];
|
|
248
249
|
function readComposerDraftFromDom() {
|
|
249
250
|
if (typeof document === "undefined") return "";
|
|
@@ -258,13 +259,132 @@ function ChatFirstPublicShell({
|
|
|
258
259
|
}) {
|
|
259
260
|
const location = useLocation();
|
|
260
261
|
const [modalOpen, setModalOpen] = useState2(false);
|
|
262
|
+
const lastAutoRunCommandRef = useRef("");
|
|
261
263
|
const returnTo = safeReturnTo(location.pathname, location.search, location.hash);
|
|
264
|
+
const promptedDraft = new URLSearchParams(location.search).get("prompt")?.trim() ?? "";
|
|
265
|
+
const pendingReturnTo = promptedDraft ? "/" : returnTo;
|
|
262
266
|
const workspaceId = intendedWorkspaceId || "public";
|
|
263
267
|
const openAuth = (draft = readComposerDraftFromDom()) => {
|
|
264
|
-
writePendingChatEntry({ draft, returnTo, ...intendedWorkspaceId ? { intendedWorkspaceId } : {} });
|
|
268
|
+
writePendingChatEntry({ draft, returnTo: pendingReturnTo, ...intendedWorkspaceId ? { intendedWorkspaceId } : {} });
|
|
265
269
|
setModalOpen(true);
|
|
266
270
|
};
|
|
267
|
-
|
|
271
|
+
const normalizePublicCommand = (draft) => draft.trim().toLowerCase().replace(/[’`]/g, "'");
|
|
272
|
+
const openLandingPage = () => postUiCommand({
|
|
273
|
+
kind: "openPanel",
|
|
274
|
+
params: { id: "public-landing-page", component: "public.launch.landing", title: "Landing page" }
|
|
275
|
+
});
|
|
276
|
+
const openLetsChat = () => postUiCommand({
|
|
277
|
+
kind: "openPanel",
|
|
278
|
+
params: { id: "public-lets-chat", component: "public.launch.lets-chat", title: "Let\u2019s chat" }
|
|
279
|
+
});
|
|
280
|
+
const publicCommands = [
|
|
281
|
+
{
|
|
282
|
+
name: "landing-page",
|
|
283
|
+
description: "Open the landing page workspace tab.",
|
|
284
|
+
kind: "local",
|
|
285
|
+
handler: () => {
|
|
286
|
+
openLandingPage();
|
|
287
|
+
return { message: "Opened Landing page." };
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "reach-out",
|
|
292
|
+
description: "Open the Calendly workspace tab.",
|
|
293
|
+
kind: "local",
|
|
294
|
+
handler: () => {
|
|
295
|
+
openLetsChat();
|
|
296
|
+
return { message: "Opened Let\u2019s chat." };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
];
|
|
300
|
+
const runPublicCommand = (draft) => {
|
|
301
|
+
const command = normalizePublicCommand(draft);
|
|
302
|
+
if (command === "/landing-page") {
|
|
303
|
+
openLandingPage();
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (command === "/reach-out" || command === "/let's-chat" || command === "/lets-chat") {
|
|
307
|
+
openLetsChat();
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
};
|
|
312
|
+
useEffect2(() => {
|
|
313
|
+
const intercept = (event) => {
|
|
314
|
+
const draft = readComposerDraftFromDom();
|
|
315
|
+
if (!runPublicCommand(draft)) return false;
|
|
316
|
+
event.preventDefault();
|
|
317
|
+
event.stopPropagation();
|
|
318
|
+
return true;
|
|
319
|
+
};
|
|
320
|
+
const onClick = (event) => {
|
|
321
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
322
|
+
if (!target?.closest('[data-boring-agent-part="composer-submit"], [aria-label="Submit"]')) return;
|
|
323
|
+
intercept(event);
|
|
324
|
+
};
|
|
325
|
+
const maybeAutoRunDraft = () => {
|
|
326
|
+
const draft = readComposerDraftFromDom();
|
|
327
|
+
const command = normalizePublicCommand(draft);
|
|
328
|
+
const isCommand = command === "/landing-page" || command === "/reach-out" || command === "/let's-chat" || command === "/lets-chat";
|
|
329
|
+
if (!isCommand) {
|
|
330
|
+
lastAutoRunCommandRef.current = "";
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (lastAutoRunCommandRef.current === command) return;
|
|
334
|
+
lastAutoRunCommandRef.current = command;
|
|
335
|
+
runPublicCommand(draft);
|
|
336
|
+
};
|
|
337
|
+
const onKeyDown = (event) => {
|
|
338
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
339
|
+
if (!target?.closest('[data-boring-agent-part="composer-input"]')) return;
|
|
340
|
+
if (event.key === "Enter" && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) intercept(event);
|
|
341
|
+
};
|
|
342
|
+
const onInput = (event) => {
|
|
343
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
344
|
+
if (!target?.closest('[data-boring-agent-part="composer-input"]')) return;
|
|
345
|
+
window.setTimeout(maybeAutoRunDraft, 0);
|
|
346
|
+
};
|
|
347
|
+
document.addEventListener("click", onClick, true);
|
|
348
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
349
|
+
document.addEventListener("input", onInput, true);
|
|
350
|
+
return () => {
|
|
351
|
+
document.removeEventListener("click", onClick, true);
|
|
352
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
353
|
+
document.removeEventListener("input", onInput, true);
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
return /* @__PURE__ */ jsxs3("div", { className: "public-chat-first-shell relative h-screen min-h-0 bg-background", children: [
|
|
357
|
+
publicShell?.showTeachingArrows !== false && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
358
|
+
/* @__PURE__ */ jsxs3("div", { className: "public-arrow public-arrow-computer", "aria-hidden": "true", children: [
|
|
359
|
+
/* @__PURE__ */ jsx4("span", { className: "public-arrow-label", children: "Your remote computer" }),
|
|
360
|
+
/* @__PURE__ */ jsxs3("svg", { className: "public-arrow-svg", viewBox: "0 0 190 140", fill: "none", children: [
|
|
361
|
+
/* @__PURE__ */ jsx4(
|
|
362
|
+
"path",
|
|
363
|
+
{
|
|
364
|
+
className: "paw-stroke",
|
|
365
|
+
d: "M14 122 C 58 134 104 128 134 100 C 150 85 160 64 164 40"
|
|
366
|
+
}
|
|
367
|
+
),
|
|
368
|
+
/* @__PURE__ */ jsx4("path", { className: "paw-stroke", d: "M164 40 C 158 49 149 56 138 60" }),
|
|
369
|
+
/* @__PURE__ */ jsx4("path", { className: "paw-stroke", d: "M164 40 C 168 51 170 63 169 75" })
|
|
370
|
+
] })
|
|
371
|
+
] }),
|
|
372
|
+
/* @__PURE__ */ jsxs3("div", { className: "public-arrow public-arrow-agent", "aria-hidden": "true", children: [
|
|
373
|
+
/* @__PURE__ */ jsxs3("svg", { className: "public-arrow-svg", viewBox: "0 0 120 116", fill: "none", children: [
|
|
374
|
+
/* @__PURE__ */ jsx4(
|
|
375
|
+
"path",
|
|
376
|
+
{
|
|
377
|
+
className: "paw-stroke",
|
|
378
|
+
d: "M60 104 C 56 76 64 50 70 24 C 71 19 73 14 76 10"
|
|
379
|
+
}
|
|
380
|
+
),
|
|
381
|
+
/* @__PURE__ */ jsx4("path", { className: "paw-stroke", d: "M76 10 C 70 16 62 19 53 20" }),
|
|
382
|
+
/* @__PURE__ */ jsx4("path", { className: "paw-stroke", d: "M76 10 C 82 14 87 21 90 29" })
|
|
383
|
+
] }),
|
|
384
|
+
/* @__PURE__ */ jsx4("span", { className: "public-arrow-label", children: "Your agent" })
|
|
385
|
+
] })
|
|
386
|
+
] }),
|
|
387
|
+
/* @__PURE__ */ jsx4("aside", { className: "pointer-events-none fixed bottom-6 left-6 z-20 w-[300px]", children: /* @__PURE__ */ jsx4("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx4(AuthCard, { returnTo }) }) }),
|
|
268
388
|
/* @__PURE__ */ jsx4(
|
|
269
389
|
ChatFirstAuthenticatedShell,
|
|
270
390
|
{
|
|
@@ -279,27 +399,32 @@ function ChatFirstPublicShell({
|
|
|
279
399
|
chatParams: {
|
|
280
400
|
...workspaceProps.chatParams,
|
|
281
401
|
emptyPlacement: "hero",
|
|
282
|
-
composerPlaceholder: publicShell?.composerPlaceholder ?? "
|
|
402
|
+
composerPlaceholder: publicShell?.composerPlaceholder ?? "Type /landing-page or /reach-out",
|
|
403
|
+
hideComposerSettings: true,
|
|
404
|
+
suppressPreSubmitCancelledWarning: true,
|
|
405
|
+
thinkingControl: false,
|
|
406
|
+
initialDraft: promptedDraft || void 0,
|
|
283
407
|
emptyState: {
|
|
284
408
|
...defaultPublicEmptyState,
|
|
285
409
|
...publicShell?.emptyState
|
|
286
410
|
},
|
|
287
411
|
suggestions: publicShell?.suggestions ?? defaultPublicSuggestions,
|
|
412
|
+
commands: publicCommands,
|
|
413
|
+
excludeBuiltinCommands: builtinCommands.map((command) => command.name),
|
|
288
414
|
onBeforeSubmit: (draft) => {
|
|
289
|
-
openAuth(draft);
|
|
415
|
+
if (!runPublicCommand(draft)) openAuth(draft);
|
|
290
416
|
return false;
|
|
291
417
|
}
|
|
292
418
|
}
|
|
293
419
|
}
|
|
294
420
|
}
|
|
295
421
|
),
|
|
296
|
-
/* @__PURE__ */ jsx4("aside", { className: "pointer-events-none fixed bottom-6 right-6 z-20 w-[320px]", children: /* @__PURE__ */ jsx4("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx4(AuthCard, { returnTo }) }) }),
|
|
297
422
|
modalOpen ? /* @__PURE__ */ jsx4(AuthModal, { returnTo, onClose: () => setModalOpen(false) }) : null
|
|
298
423
|
] });
|
|
299
424
|
}
|
|
300
425
|
|
|
301
426
|
// src/app/front/CoreWorkspaceAgentFront.tsx
|
|
302
|
-
import { Fragment as
|
|
427
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
303
428
|
var DEFAULT_WORKSPACE_ROUTE = "/workspace/:id";
|
|
304
429
|
var DEFAULT_WORKSPACE_ID_PARAM = "id";
|
|
305
430
|
function DefaultTopBarRight() {
|
|
@@ -332,11 +457,30 @@ function WorkspaceLoadingPage({
|
|
|
332
457
|
] }) })
|
|
333
458
|
] });
|
|
334
459
|
}
|
|
460
|
+
function mergePublicWorkspaceProps(workspaceProps, publicWorkspaceProps) {
|
|
461
|
+
if (!publicWorkspaceProps) return workspaceProps;
|
|
462
|
+
return {
|
|
463
|
+
...workspaceProps,
|
|
464
|
+
...publicWorkspaceProps,
|
|
465
|
+
requestHeaders: {
|
|
466
|
+
...workspaceProps.requestHeaders,
|
|
467
|
+
...publicWorkspaceProps.requestHeaders
|
|
468
|
+
},
|
|
469
|
+
authHeaders: {
|
|
470
|
+
...workspaceProps.authHeaders,
|
|
471
|
+
...publicWorkspaceProps.authHeaders
|
|
472
|
+
},
|
|
473
|
+
chatParams: {
|
|
474
|
+
...workspaceProps.chatParams,
|
|
475
|
+
...publicWorkspaceProps.chatParams
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
335
479
|
function usePendingChatDraft() {
|
|
336
480
|
const session = useSession();
|
|
337
481
|
const userId = session.data?.user?.id ?? null;
|
|
338
482
|
const [pending, setPending] = useState3(() => userId ? readPendingChatEntry() : null);
|
|
339
|
-
|
|
483
|
+
useEffect3(() => {
|
|
340
484
|
if (!userId) {
|
|
341
485
|
setPending(null);
|
|
342
486
|
return;
|
|
@@ -354,7 +498,8 @@ function HomeRedirect({
|
|
|
354
498
|
chatEntryMode,
|
|
355
499
|
appTitle,
|
|
356
500
|
workspaceProps,
|
|
357
|
-
chatFirstPublicShell
|
|
501
|
+
chatFirstPublicShell,
|
|
502
|
+
chatFirstPublicWorkspaceProps
|
|
358
503
|
}) {
|
|
359
504
|
const location = useLocation2();
|
|
360
505
|
const session = useSession();
|
|
@@ -366,7 +511,16 @@ function HomeRedirect({
|
|
|
366
511
|
location.search,
|
|
367
512
|
location.hash
|
|
368
513
|
);
|
|
369
|
-
if (!session.data?.user && chatEntryMode === "chat-first")
|
|
514
|
+
if (!session.data?.user && chatEntryMode === "chat-first") {
|
|
515
|
+
return /* @__PURE__ */ jsx5(
|
|
516
|
+
ChatFirstPublicShell,
|
|
517
|
+
{
|
|
518
|
+
appTitle,
|
|
519
|
+
publicShell: chatFirstPublicShell,
|
|
520
|
+
workspaceProps: mergePublicWorkspaceProps(workspaceProps, chatFirstPublicWorkspaceProps)
|
|
521
|
+
}
|
|
522
|
+
);
|
|
523
|
+
}
|
|
370
524
|
if (!workspace && chatEntryMode === "chat-first" && session.data?.user && restorePendingDraft) {
|
|
371
525
|
return /* @__PURE__ */ jsx5(
|
|
372
526
|
ChatFirstAuthenticatedShell,
|
|
@@ -378,7 +532,7 @@ function HomeRedirect({
|
|
|
378
532
|
}
|
|
379
533
|
);
|
|
380
534
|
}
|
|
381
|
-
if (!workspace) return /* @__PURE__ */ jsx5(
|
|
535
|
+
if (!workspace) return /* @__PURE__ */ jsx5(Fragment3, { children: loadingFallback });
|
|
382
536
|
return /* @__PURE__ */ jsx5(Navigate, { to: workspaceHref(workspace.id), replace: true });
|
|
383
537
|
}
|
|
384
538
|
function WorkspaceRouteErrorPage({ status, message }) {
|
|
@@ -396,7 +550,8 @@ function WorkspaceRoute({
|
|
|
396
550
|
chatEntryMode,
|
|
397
551
|
appTitle,
|
|
398
552
|
workspaceRoute,
|
|
399
|
-
chatFirstPublicShell
|
|
553
|
+
chatFirstPublicShell,
|
|
554
|
+
chatFirstPublicWorkspaceProps
|
|
400
555
|
}) {
|
|
401
556
|
const params = useParams();
|
|
402
557
|
const location = useLocation2();
|
|
@@ -420,9 +575,17 @@ function WorkspaceRoute({
|
|
|
420
575
|
() => ({ ...workspaceProps.authHeaders, "x-boring-workspace-id": workspaceId }),
|
|
421
576
|
[workspaceId, workspaceProps.authHeaders]
|
|
422
577
|
);
|
|
423
|
-
if (!workspaceId) return /* @__PURE__ */ jsx5(
|
|
578
|
+
if (!workspaceId) return /* @__PURE__ */ jsx5(Fragment3, { children: loadingFallback });
|
|
424
579
|
if (!session.data?.user && chatEntryMode === "chat-first") {
|
|
425
|
-
return /* @__PURE__ */ jsx5(
|
|
580
|
+
return /* @__PURE__ */ jsx5(
|
|
581
|
+
ChatFirstPublicShell,
|
|
582
|
+
{
|
|
583
|
+
appTitle,
|
|
584
|
+
intendedWorkspaceId: workspaceId,
|
|
585
|
+
publicShell: chatFirstPublicShell,
|
|
586
|
+
workspaceProps: mergePublicWorkspaceProps(workspaceProps, chatFirstPublicWorkspaceProps)
|
|
587
|
+
}
|
|
588
|
+
);
|
|
426
589
|
}
|
|
427
590
|
if (routeStatus.status === "not-found" || routeStatus.status === "forbidden" || routeStatus.status === "switch-failed") {
|
|
428
591
|
return /* @__PURE__ */ jsx5(WorkspaceRouteErrorPage, { status: routeStatus.status, message: routeStatus.message });
|
|
@@ -438,7 +601,7 @@ function WorkspaceRoute({
|
|
|
438
601
|
}
|
|
439
602
|
);
|
|
440
603
|
}
|
|
441
|
-
if (routeStatus.status !== "matched" || currentWorkspace?.id !== workspaceId) return /* @__PURE__ */ jsx5(
|
|
604
|
+
if (routeStatus.status !== "matched" || currentWorkspace?.id !== workspaceId) return /* @__PURE__ */ jsx5(Fragment3, { children: loadingFallback });
|
|
442
605
|
const shouldRestorePendingDraft = restorePendingDraft && Boolean(pendingChatEntry?.draft);
|
|
443
606
|
const chatParams = {
|
|
444
607
|
...workspaceProps.chatParams,
|
|
@@ -456,6 +619,7 @@ function WorkspaceRoute({
|
|
|
456
619
|
{
|
|
457
620
|
...workspaceProps,
|
|
458
621
|
workspaceId,
|
|
622
|
+
workspaceLabel: workspaceProps.workspaceLabel ?? currentWorkspace.name,
|
|
459
623
|
requestHeaders,
|
|
460
624
|
authHeaders,
|
|
461
625
|
chatParams,
|
|
@@ -481,11 +645,13 @@ function CoreWorkspaceAgentFront({
|
|
|
481
645
|
bootPreloadPaths,
|
|
482
646
|
topBarLeft = /* @__PURE__ */ jsx5(WorkspaceSwitcher, {}),
|
|
483
647
|
topBarRight = /* @__PURE__ */ jsx5(DefaultTopBarRight, {}),
|
|
484
|
-
appTitle = "
|
|
648
|
+
appTitle = "Sovereign Workspace",
|
|
485
649
|
bridgeEndpoint = "/api/v1/ui",
|
|
486
650
|
hotReload = false,
|
|
487
651
|
chatEntryMode = "auth-first",
|
|
488
652
|
chatFirstPublicShell,
|
|
653
|
+
chatFirstPublicWorkspaceProps,
|
|
654
|
+
publicPaths,
|
|
489
655
|
...workspaceProps
|
|
490
656
|
}) {
|
|
491
657
|
if (hotReload !== false) {
|
|
@@ -515,7 +681,7 @@ function CoreWorkspaceAgentFront({
|
|
|
515
681
|
cspNonce,
|
|
516
682
|
workspaceRoute,
|
|
517
683
|
workspaceIdParam,
|
|
518
|
-
publicPaths: chatEntryMode === "chat-first" ? chatFirstPublicPaths(workspaceRoute) :
|
|
684
|
+
publicPaths: chatEntryMode === "chat-first" ? [...chatFirstPublicPaths(workspaceRoute), ...publicPaths ?? []] : publicPaths,
|
|
519
685
|
children: [
|
|
520
686
|
/* @__PURE__ */ jsx5(
|
|
521
687
|
Route,
|
|
@@ -529,7 +695,8 @@ function CoreWorkspaceAgentFront({
|
|
|
529
695
|
chatEntryMode,
|
|
530
696
|
appTitle,
|
|
531
697
|
workspaceProps: resolvedWorkspaceProps,
|
|
532
|
-
chatFirstPublicShell
|
|
698
|
+
chatFirstPublicShell,
|
|
699
|
+
chatFirstPublicWorkspaceProps
|
|
533
700
|
}
|
|
534
701
|
)
|
|
535
702
|
}
|
|
@@ -548,7 +715,8 @@ function CoreWorkspaceAgentFront({
|
|
|
548
715
|
chatEntryMode,
|
|
549
716
|
appTitle,
|
|
550
717
|
workspaceRoute,
|
|
551
|
-
chatFirstPublicShell
|
|
718
|
+
chatFirstPublicShell,
|
|
719
|
+
chatFirstPublicWorkspaceProps
|
|
552
720
|
}
|
|
553
721
|
)
|
|
554
722
|
}
|
|
@@ -558,6 +726,629 @@ function CoreWorkspaceAgentFront({
|
|
|
558
726
|
}
|
|
559
727
|
);
|
|
560
728
|
}
|
|
729
|
+
|
|
730
|
+
// src/app/front/credits/CreditBalanceBadge.tsx
|
|
731
|
+
import { useState as useState5 } from "react";
|
|
732
|
+
import { Button, Popover, PopoverContent, PopoverTrigger } from "@hachej/boring-ui-kit";
|
|
733
|
+
import { Plus } from "lucide-react";
|
|
734
|
+
|
|
735
|
+
// src/app/front/credits/helpers.ts
|
|
736
|
+
function creditNetMicros(balance) {
|
|
737
|
+
const remaining = Number.isFinite(balance.remainingMicros) ? balance.remainingMicros : 0;
|
|
738
|
+
const debt = Number.isFinite(balance.debtMicros) ? balance.debtMicros : 0;
|
|
739
|
+
return remaining - debt;
|
|
740
|
+
}
|
|
741
|
+
function formatSignedCreditMicros(micros, currency = "EUR", locale) {
|
|
742
|
+
const major = (Number.isFinite(micros) ? micros : 0) / 1e6;
|
|
743
|
+
const sign = major > 0 ? "+" : major < 0 ? "\u2212" : "";
|
|
744
|
+
const abs = new Intl.NumberFormat(locale, { style: "currency", currency }).format(Math.abs(major));
|
|
745
|
+
return `${sign}${abs}`;
|
|
746
|
+
}
|
|
747
|
+
function formatMinorPrice(priceMinor, currency, locale) {
|
|
748
|
+
const major = (Number.isFinite(priceMinor) ? priceMinor : 0) / 100;
|
|
749
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency, maximumFractionDigits: major % 1 === 0 ? 0 : 2 }).format(major);
|
|
750
|
+
}
|
|
751
|
+
function formatCreditMicros(micros, currency = "EUR", locale) {
|
|
752
|
+
const major = (Number.isFinite(micros) ? Math.max(0, micros) : 0) / 1e6;
|
|
753
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(major);
|
|
754
|
+
}
|
|
755
|
+
function isLowBalance(micros, thresholdMicros = 5e5) {
|
|
756
|
+
return Number.isFinite(micros) && micros <= thresholdMicros;
|
|
757
|
+
}
|
|
758
|
+
var PAYMENT_REQUIRED_ERROR_CODE = "PAYMENT_REQUIRED";
|
|
759
|
+
function isPaymentRequiredNotice(notice) {
|
|
760
|
+
return notice.errorCode === PAYMENT_REQUIRED_ERROR_CODE;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/app/front/credits/useCreditBalance.ts
|
|
764
|
+
import { useCallback, useEffect as useEffect4, useRef as useRef2, useState as useState4 } from "react";
|
|
765
|
+
var CREDITS_REFRESH_EVENT = "credits:refresh";
|
|
766
|
+
var CHECKOUT_BASELINE_STORAGE_KEY = "credits:checkout-baseline";
|
|
767
|
+
var CHECKOUT_BASELINE_TTL_MS = 60 * 60 * 1e3;
|
|
768
|
+
var RETRY_BURST_MS = [0, 1e3, 2e3, 4e3, 8e3];
|
|
769
|
+
function useCreditBalance({
|
|
770
|
+
apiBaseUrl = "",
|
|
771
|
+
pollMs = 3e4,
|
|
772
|
+
pack
|
|
773
|
+
} = {}) {
|
|
774
|
+
const [balance, setBalance] = useState4(null);
|
|
775
|
+
const [hidden, setHidden] = useState4(false);
|
|
776
|
+
const [buying, setBuying] = useState4(false);
|
|
777
|
+
const [lastUpdatedAt, setLastUpdatedAt] = useState4(null);
|
|
778
|
+
const [updating, setUpdating] = useState4(false);
|
|
779
|
+
const [error, setError] = useState4(null);
|
|
780
|
+
const buyingRef = useRef2(false);
|
|
781
|
+
const burstRef = useRef2(0);
|
|
782
|
+
const timersRef = useRef2([]);
|
|
783
|
+
const burstActiveRef = useRef2(false);
|
|
784
|
+
const balanceRef = useRef2(null);
|
|
785
|
+
const refresh = useCallback(async () => {
|
|
786
|
+
setUpdating(true);
|
|
787
|
+
try {
|
|
788
|
+
const res = await fetch(`${apiBaseUrl}/api/credits/balance`, { credentials: "include" });
|
|
789
|
+
if (res.status === 401) {
|
|
790
|
+
setHidden(true);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (!res.ok) {
|
|
794
|
+
setError("Could not load your balance.");
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const data = await res.json();
|
|
798
|
+
if (!data.enabled) {
|
|
799
|
+
setHidden(true);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
setBalance(data);
|
|
803
|
+
balanceRef.current = data;
|
|
804
|
+
setHidden(false);
|
|
805
|
+
setError(null);
|
|
806
|
+
setLastUpdatedAt(Date.now());
|
|
807
|
+
} catch {
|
|
808
|
+
setError("Could not load your balance.");
|
|
809
|
+
} finally {
|
|
810
|
+
if (!burstActiveRef.current) setUpdating(false);
|
|
811
|
+
}
|
|
812
|
+
}, [apiBaseUrl]);
|
|
813
|
+
const refreshWithRetry = useCallback(() => {
|
|
814
|
+
const token = burstRef.current += 1;
|
|
815
|
+
for (const t of timersRef.current) clearTimeout(t);
|
|
816
|
+
burstActiveRef.current = true;
|
|
817
|
+
setUpdating(true);
|
|
818
|
+
const lastIndex = RETRY_BURST_MS.length - 1;
|
|
819
|
+
timersRef.current = RETRY_BURST_MS.map(
|
|
820
|
+
(delay, index) => setTimeout(async () => {
|
|
821
|
+
if (burstRef.current !== token) return;
|
|
822
|
+
try {
|
|
823
|
+
await refresh();
|
|
824
|
+
} finally {
|
|
825
|
+
if (index === lastIndex && burstRef.current === token) {
|
|
826
|
+
burstActiveRef.current = false;
|
|
827
|
+
setUpdating(false);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}, delay)
|
|
831
|
+
);
|
|
832
|
+
}, [refresh]);
|
|
833
|
+
useEffect4(() => {
|
|
834
|
+
void refresh();
|
|
835
|
+
const interval = setInterval(() => void refresh(), pollMs);
|
|
836
|
+
const onFocus = () => void refresh();
|
|
837
|
+
const onRefreshEvent = () => refreshWithRetry();
|
|
838
|
+
window.addEventListener("focus", onFocus);
|
|
839
|
+
window.addEventListener(CREDITS_REFRESH_EVENT, onRefreshEvent);
|
|
840
|
+
let channel = null;
|
|
841
|
+
try {
|
|
842
|
+
channel = new BroadcastChannel("credits");
|
|
843
|
+
channel.onmessage = () => refreshWithRetry();
|
|
844
|
+
} catch {
|
|
845
|
+
}
|
|
846
|
+
return () => {
|
|
847
|
+
clearInterval(interval);
|
|
848
|
+
window.removeEventListener("focus", onFocus);
|
|
849
|
+
window.removeEventListener(CREDITS_REFRESH_EVENT, onRefreshEvent);
|
|
850
|
+
for (const t of timersRef.current) clearTimeout(t);
|
|
851
|
+
channel?.close();
|
|
852
|
+
};
|
|
853
|
+
}, [refresh, refreshWithRetry, pollMs]);
|
|
854
|
+
const buy = useCallback(async (overridePack) => {
|
|
855
|
+
if (buyingRef.current) return null;
|
|
856
|
+
buyingRef.current = true;
|
|
857
|
+
setBuying(true);
|
|
858
|
+
let win = null;
|
|
859
|
+
try {
|
|
860
|
+
win = window.open("about:blank", "_blank");
|
|
861
|
+
if (!win) return "Could not open the checkout tab. Please allow pop-ups for this site and try again.";
|
|
862
|
+
try {
|
|
863
|
+
win.opener = null;
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
const chosen = overridePack ?? pack;
|
|
867
|
+
const res = await fetch(`${apiBaseUrl}/api/credits/checkout`, {
|
|
868
|
+
method: "POST",
|
|
869
|
+
credentials: "include",
|
|
870
|
+
headers: { "content-type": "application/json" },
|
|
871
|
+
body: JSON.stringify(chosen ? { pack: chosen } : {})
|
|
872
|
+
});
|
|
873
|
+
if (!res.ok) {
|
|
874
|
+
win.close();
|
|
875
|
+
return "Could not start checkout. Please try again.";
|
|
876
|
+
}
|
|
877
|
+
const { url } = await res.json();
|
|
878
|
+
if (!url) {
|
|
879
|
+
win.close();
|
|
880
|
+
return "Checkout is not available right now.";
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
let current = balanceRef.current;
|
|
884
|
+
try {
|
|
885
|
+
const bres = await fetch(`${apiBaseUrl}/api/credits/balance`, { credentials: "include" });
|
|
886
|
+
if (bres.ok) {
|
|
887
|
+
const fresh = await bres.json();
|
|
888
|
+
if (fresh?.enabled) current = fresh;
|
|
889
|
+
}
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
if (current) {
|
|
893
|
+
window.localStorage.setItem(
|
|
894
|
+
CHECKOUT_BASELINE_STORAGE_KEY,
|
|
895
|
+
// userId so the return handler can reject a baseline left by a DIFFERENT
|
|
896
|
+
// user (shared localStorage) instead of confirming a phantom purchase.
|
|
897
|
+
JSON.stringify({ net: creditNetMicros(current), ts: Date.now(), userId: current.userId })
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
win.location.href = url;
|
|
903
|
+
return null;
|
|
904
|
+
} catch {
|
|
905
|
+
win?.close();
|
|
906
|
+
return "Could not reach the checkout service. Please try again.";
|
|
907
|
+
} finally {
|
|
908
|
+
buyingRef.current = false;
|
|
909
|
+
setBuying(false);
|
|
910
|
+
}
|
|
911
|
+
}, [apiBaseUrl, pack]);
|
|
912
|
+
return { balance, hidden, error, refresh, refreshWithRetry, buy, buying, lastUpdatedAt, updating };
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/app/front/credits/CreditBalanceBadge.tsx
|
|
916
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
917
|
+
function CreditBalanceBadge({
|
|
918
|
+
apiBaseUrl = "",
|
|
919
|
+
buyEnabled = false,
|
|
920
|
+
pollMs = 3e4,
|
|
921
|
+
locale
|
|
922
|
+
}) {
|
|
923
|
+
const { balance, hidden, buy, buying } = useCreditBalance({ apiBaseUrl, pollMs });
|
|
924
|
+
const [open, setOpen] = useState5(false);
|
|
925
|
+
if (hidden || !balance) return null;
|
|
926
|
+
const inDebt = (balance.debtMicros ?? 0) > 0;
|
|
927
|
+
const low = inDebt || isLowBalance(balance.remainingMicros);
|
|
928
|
+
const showBuy = balance.checkoutEnabled ?? buyEnabled;
|
|
929
|
+
const packs = (balance.packs ?? []).filter((p) => !p.custom);
|
|
930
|
+
const currency = balance.packs?.[0]?.currency ?? "EUR";
|
|
931
|
+
const pick = async (packId) => {
|
|
932
|
+
setOpen(false);
|
|
933
|
+
await buy(packId);
|
|
934
|
+
};
|
|
935
|
+
return /* @__PURE__ */ jsxs5("div", { className: "inline-flex items-center gap-1.5", "data-low": low ? "true" : "false", "data-debt": inDebt ? "true" : "false", children: [
|
|
936
|
+
/* @__PURE__ */ jsx6(
|
|
937
|
+
"span",
|
|
938
|
+
{
|
|
939
|
+
title: inDebt ? "Amount owed \u2014 top up to resume" : "Remaining credits",
|
|
940
|
+
className: `text-[11px] tabular-nums ${low ? "text-destructive" : "text-muted-foreground"}`,
|
|
941
|
+
children: inDebt ? `\u2212${formatCreditMicros(balance.debtMicros, currency, locale)}` : formatCreditMicros(balance.remainingMicros, currency, locale)
|
|
942
|
+
}
|
|
943
|
+
),
|
|
944
|
+
showBuy ? /* @__PURE__ */ jsxs5(Popover, { open, onOpenChange: setOpen, children: [
|
|
945
|
+
/* @__PURE__ */ jsx6(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx6(
|
|
946
|
+
Button,
|
|
947
|
+
{
|
|
948
|
+
type: "button",
|
|
949
|
+
variant: "ghost",
|
|
950
|
+
size: "icon-sm",
|
|
951
|
+
"aria-label": "Add credits",
|
|
952
|
+
disabled: buying,
|
|
953
|
+
className: "size-5 rounded-full text-muted-foreground hover:text-foreground",
|
|
954
|
+
children: /* @__PURE__ */ jsx6(Plus, { className: "size-3.5", "aria-hidden": "true" })
|
|
955
|
+
}
|
|
956
|
+
) }),
|
|
957
|
+
/* @__PURE__ */ jsxs5(PopoverContent, { align: "end", className: "w-48 p-1.5", children: [
|
|
958
|
+
/* @__PURE__ */ jsx6("p", { className: "px-2 py-1 text-[11px] font-medium text-muted-foreground", children: "Add credits" }),
|
|
959
|
+
packs.length > 0 ? /* @__PURE__ */ jsx6("div", { className: "flex flex-col", children: packs.map((p) => /* @__PURE__ */ jsxs5(
|
|
960
|
+
"button",
|
|
961
|
+
{
|
|
962
|
+
type: "button",
|
|
963
|
+
onClick: () => void pick(p.id),
|
|
964
|
+
disabled: buying,
|
|
965
|
+
className: "flex items-center justify-between gap-3 rounded-md px-2 py-1.5 text-[13px] hover:bg-muted disabled:opacity-50",
|
|
966
|
+
children: [
|
|
967
|
+
/* @__PURE__ */ jsx6("span", { className: "tabular-nums font-medium", children: formatMinorPrice(p.priceMinor, p.currency, locale) }),
|
|
968
|
+
/* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: formatCreditMicros(p.creditMicros, p.currency, locale) })
|
|
969
|
+
]
|
|
970
|
+
},
|
|
971
|
+
p.id
|
|
972
|
+
)) }) : /* @__PURE__ */ jsx6(
|
|
973
|
+
"button",
|
|
974
|
+
{
|
|
975
|
+
type: "button",
|
|
976
|
+
onClick: () => void pick(),
|
|
977
|
+
disabled: buying,
|
|
978
|
+
className: "w-full rounded-md px-2 py-1.5 text-left text-[13px] hover:bg-muted disabled:opacity-50",
|
|
979
|
+
children: buying ? "Opening\u2026" : "Buy credits"
|
|
980
|
+
}
|
|
981
|
+
)
|
|
982
|
+
] })
|
|
983
|
+
] }) : null
|
|
984
|
+
] });
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/app/front/credits/CreditsSettingsPanel.tsx
|
|
988
|
+
import { useState as useState7 } from "react";
|
|
989
|
+
import {
|
|
990
|
+
Button as Button2,
|
|
991
|
+
DetailList,
|
|
992
|
+
DetailLine,
|
|
993
|
+
Notice,
|
|
994
|
+
SettingsPanel
|
|
995
|
+
} from "@hachej/boring-ui-kit";
|
|
996
|
+
import { CreditCard } from "lucide-react";
|
|
997
|
+
|
|
998
|
+
// src/app/front/credits/useCreditHistory.ts
|
|
999
|
+
import { useCallback as useCallback2, useState as useState6 } from "react";
|
|
1000
|
+
function useCreditHistory(apiBaseUrl = "", limit = 20) {
|
|
1001
|
+
const [entries, setEntries] = useState6(null);
|
|
1002
|
+
const [loading, setLoading] = useState6(false);
|
|
1003
|
+
const [error, setError] = useState6(false);
|
|
1004
|
+
const load = useCallback2(async () => {
|
|
1005
|
+
setLoading(true);
|
|
1006
|
+
setError(false);
|
|
1007
|
+
try {
|
|
1008
|
+
const res = await fetch(`${apiBaseUrl}/api/credits/history?limit=${encodeURIComponent(String(limit))}`, { credentials: "include" });
|
|
1009
|
+
if (!res.ok) {
|
|
1010
|
+
setError(true);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const data = await res.json();
|
|
1014
|
+
setEntries(Array.isArray(data.entries) ? data.entries : []);
|
|
1015
|
+
} catch {
|
|
1016
|
+
setError(true);
|
|
1017
|
+
} finally {
|
|
1018
|
+
setLoading(false);
|
|
1019
|
+
}
|
|
1020
|
+
}, [apiBaseUrl, limit]);
|
|
1021
|
+
return { entries, loading, error, load };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/app/front/credits/CreditsSettingsPanel.tsx
|
|
1025
|
+
import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1026
|
+
function relativeTime(iso, locale) {
|
|
1027
|
+
const then = new Date(iso).getTime();
|
|
1028
|
+
if (!Number.isFinite(then)) return "";
|
|
1029
|
+
return new Date(then).toLocaleString(locale, { dateStyle: "medium", timeStyle: "short" });
|
|
1030
|
+
}
|
|
1031
|
+
function CreditsSettingsPanel({ apiBaseUrl = "", locale }) {
|
|
1032
|
+
const { balance, hidden, error, buy, buying, lastUpdatedAt, updating } = useCreditBalance({ apiBaseUrl });
|
|
1033
|
+
const history = useCreditHistory(apiBaseUrl);
|
|
1034
|
+
const [buyError, setBuyError] = useState7(null);
|
|
1035
|
+
const [selectedPack, setSelectedPack] = useState7(null);
|
|
1036
|
+
if (hidden) return null;
|
|
1037
|
+
if (!balance) {
|
|
1038
|
+
return /* @__PURE__ */ jsx7(
|
|
1039
|
+
SettingsPanel,
|
|
1040
|
+
{
|
|
1041
|
+
id: "billing",
|
|
1042
|
+
icon: /* @__PURE__ */ jsx7(CreditCard, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
|
|
1043
|
+
title: "Billing & credits",
|
|
1044
|
+
description: "Your remaining AI credits, how to top up, and recent activity.",
|
|
1045
|
+
children: error ? /* @__PURE__ */ jsx7(Notice, { role: "alert", tone: "error", description: error }) : /* @__PURE__ */ jsx7("p", { className: "text-[13px] text-muted-foreground", children: "Loading your balance\u2026" })
|
|
1046
|
+
}
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
const inDebt = (balance.debtMicros ?? 0) > 0;
|
|
1050
|
+
const low = inDebt || isLowBalance(balance.remainingMicros);
|
|
1051
|
+
const showBuy = balance.checkoutEnabled ?? false;
|
|
1052
|
+
const packs = balance.packs ?? [];
|
|
1053
|
+
const currency = packs[0]?.currency ?? "EUR";
|
|
1054
|
+
const activePack = selectedPack ?? packs.find((p) => p.isDefault)?.id ?? packs[0]?.id ?? null;
|
|
1055
|
+
const activePackObj = packs.find((p) => p.id === activePack) ?? null;
|
|
1056
|
+
const doBuy = async (pack) => {
|
|
1057
|
+
setBuyError(null);
|
|
1058
|
+
const error2 = await buy(pack);
|
|
1059
|
+
if (error2) setBuyError(error2);
|
|
1060
|
+
};
|
|
1061
|
+
return /* @__PURE__ */ jsx7(
|
|
1062
|
+
SettingsPanel,
|
|
1063
|
+
{
|
|
1064
|
+
id: "billing",
|
|
1065
|
+
icon: /* @__PURE__ */ jsx7(CreditCard, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
|
|
1066
|
+
title: "Billing & credits",
|
|
1067
|
+
description: "Your remaining AI credits, how to top up, and recent activity.",
|
|
1068
|
+
footer: showBuy && packs.length === 0 ? /* @__PURE__ */ jsx7(Button2, { type: "button", size: "sm", onClick: () => void doBuy(), disabled: buying, children: buying ? "Opening checkout\u2026" : "Buy credits" }) : void 0,
|
|
1069
|
+
children: /* @__PURE__ */ jsxs6("div", { className: "space-y-4", children: [
|
|
1070
|
+
buyError && /* @__PURE__ */ jsx7(Notice, { role: "alert", tone: "error", description: buyError }),
|
|
1071
|
+
inDebt && /* @__PURE__ */ jsx7(Notice, { role: "status", tone: "error", description: "Your balance is negative. Top up to resume running the agent." }),
|
|
1072
|
+
!inDebt && low && /* @__PURE__ */ jsx7(Notice, { role: "status", tone: "warning", description: "You're low on credits. Top up to avoid interruptions." }),
|
|
1073
|
+
/* @__PURE__ */ jsxs6(DetailList, { children: [
|
|
1074
|
+
/* @__PURE__ */ jsx7(DetailLine, { label: "Remaining balance", children: /* @__PURE__ */ jsxs6("p", { style: { fontVariantNumeric: "tabular-nums", fontWeight: 600 }, children: [
|
|
1075
|
+
inDebt ? `\u2212${formatCreditMicros(balance.debtMicros, currency, locale)}` : formatCreditMicros(balance.remainingMicros, currency, locale),
|
|
1076
|
+
updating && /* @__PURE__ */ jsx7("span", { className: "ml-2 text-[11px] font-normal text-muted-foreground", "aria-live": "polite", children: "Updating\u2026" })
|
|
1077
|
+
] }) }),
|
|
1078
|
+
/* @__PURE__ */ jsx7(DetailLine, { label: "Used so far", children: /* @__PURE__ */ jsx7("p", { style: { fontVariantNumeric: "tabular-nums" }, children: formatCreditMicros(balance.usedMicros, currency, locale) }) })
|
|
1079
|
+
] }),
|
|
1080
|
+
showBuy && packs.length > 0 && /* @__PURE__ */ jsxs6("fieldset", { className: "space-y-2", children: [
|
|
1081
|
+
/* @__PURE__ */ jsx7("legend", { className: "text-[12px] font-medium text-foreground", children: "Buy credits" }),
|
|
1082
|
+
/* @__PURE__ */ jsx7("div", { role: "radiogroup", "aria-label": "Credit pack", className: "flex flex-wrap gap-2", children: packs.map((p) => {
|
|
1083
|
+
const selected = p.id === activePack;
|
|
1084
|
+
return /* @__PURE__ */ jsxs6(
|
|
1085
|
+
"label",
|
|
1086
|
+
{
|
|
1087
|
+
className: `flex cursor-pointer items-center gap-2 rounded-md border px-3 py-1.5 text-[13px] ${selected ? "border-foreground" : "border-border/60"}`,
|
|
1088
|
+
children: [
|
|
1089
|
+
/* @__PURE__ */ jsx7(
|
|
1090
|
+
"input",
|
|
1091
|
+
{
|
|
1092
|
+
type: "radio",
|
|
1093
|
+
name: "credit-pack",
|
|
1094
|
+
value: p.id,
|
|
1095
|
+
checked: selected,
|
|
1096
|
+
onChange: () => setSelectedPack(p.id)
|
|
1097
|
+
}
|
|
1098
|
+
),
|
|
1099
|
+
p.custom ? /* @__PURE__ */ jsxs6("span", { children: [
|
|
1100
|
+
"Custom",
|
|
1101
|
+
/* @__PURE__ */ jsxs6("span", { className: "text-muted-foreground", children: [
|
|
1102
|
+
" \xB7 from ",
|
|
1103
|
+
formatMinorPrice(p.priceMinor, p.currency, locale)
|
|
1104
|
+
] })
|
|
1105
|
+
] }) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
1106
|
+
/* @__PURE__ */ jsx7("span", { style: { fontVariantNumeric: "tabular-nums" }, children: formatMinorPrice(p.priceMinor, p.currency, locale) }),
|
|
1107
|
+
/* @__PURE__ */ jsxs6("span", { className: "text-muted-foreground", children: [
|
|
1108
|
+
"\xB7 ",
|
|
1109
|
+
formatCreditMicros(p.creditMicros, p.currency, locale)
|
|
1110
|
+
] })
|
|
1111
|
+
] })
|
|
1112
|
+
]
|
|
1113
|
+
},
|
|
1114
|
+
p.id
|
|
1115
|
+
);
|
|
1116
|
+
}) }),
|
|
1117
|
+
/* @__PURE__ */ jsx7(
|
|
1118
|
+
Button2,
|
|
1119
|
+
{
|
|
1120
|
+
type: "button",
|
|
1121
|
+
size: "sm",
|
|
1122
|
+
onClick: () => activePack && void doBuy(activePack),
|
|
1123
|
+
disabled: buying || !activePack,
|
|
1124
|
+
children: buying ? "Opening checkout\u2026" : activePackObj?.custom ? "Choose amount" : activePackObj ? `Buy ${formatMinorPrice(activePackObj.priceMinor, activePackObj.currency, locale)}` : "Buy credits"
|
|
1125
|
+
}
|
|
1126
|
+
)
|
|
1127
|
+
] }),
|
|
1128
|
+
/* @__PURE__ */ jsx7("p", { className: "text-[12px] leading-5 text-muted-foreground", children: showBuy ? "Credits are consumed as the agent runs (priced from model token usage). Checkout opens in a new tab; your balance updates automatically when payment completes." : "Credits are consumed as the agent runs (priced from model token usage). Purchasing more credits is not available in this deployment yet." }),
|
|
1129
|
+
/* @__PURE__ */ jsxs6("details", { onToggle: (e) => {
|
|
1130
|
+
if (e.currentTarget.open && history.entries === null && !history.loading) void history.load();
|
|
1131
|
+
}, children: [
|
|
1132
|
+
/* @__PURE__ */ jsx7("summary", { className: "cursor-pointer text-[12px] font-medium text-foreground", children: "Recent activity" }),
|
|
1133
|
+
/* @__PURE__ */ jsxs6("div", { className: "mt-2", children: [
|
|
1134
|
+
history.loading && /* @__PURE__ */ jsx7("p", { className: "text-[12px] text-muted-foreground", "aria-live": "polite", children: "Loading\u2026" }),
|
|
1135
|
+
history.error && /* @__PURE__ */ jsx7(Notice, { role: "alert", tone: "error", description: "Could not load activity. Try again." }),
|
|
1136
|
+
!history.loading && !history.error && history.entries?.length === 0 && /* @__PURE__ */ jsx7("p", { className: "text-[12px] text-muted-foreground", children: "No credit activity yet." }),
|
|
1137
|
+
!history.loading && !history.error && history.entries && history.entries.length > 0 && /* @__PURE__ */ jsx7("ul", { className: "divide-y divide-border/40", children: history.entries.map((e) => /* @__PURE__ */ jsxs6("li", { className: "flex items-center justify-between gap-3 py-1.5 text-[12px]", children: [
|
|
1138
|
+
/* @__PURE__ */ jsxs6("span", { className: "min-w-0", children: [
|
|
1139
|
+
/* @__PURE__ */ jsx7("span", { className: "text-foreground", children: e.description }),
|
|
1140
|
+
/* @__PURE__ */ jsx7("span", { className: "ml-2 text-muted-foreground", children: relativeTime(e.createdAt, locale) })
|
|
1141
|
+
] }),
|
|
1142
|
+
/* @__PURE__ */ jsx7(
|
|
1143
|
+
"span",
|
|
1144
|
+
{
|
|
1145
|
+
style: { fontVariantNumeric: "tabular-nums" },
|
|
1146
|
+
className: e.amountMicros >= 0 ? "text-foreground" : "text-muted-foreground",
|
|
1147
|
+
children: formatSignedCreditMicros(e.amountMicros, currency, locale)
|
|
1148
|
+
}
|
|
1149
|
+
)
|
|
1150
|
+
] }, e.id)) })
|
|
1151
|
+
] })
|
|
1152
|
+
] }),
|
|
1153
|
+
lastUpdatedAt && /* @__PURE__ */ jsxs6("p", { className: "text-[11px] text-muted-foreground", children: [
|
|
1154
|
+
"Updated ",
|
|
1155
|
+
new Date(lastUpdatedAt).toLocaleTimeString(locale)
|
|
1156
|
+
] })
|
|
1157
|
+
] })
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/app/front/credits/useCheckoutReturnHandler.ts
|
|
1163
|
+
import { useEffect as useEffect5, useState as useState8 } from "react";
|
|
1164
|
+
var CONFIRM_SCHEDULE_MS = [0, 1500, 3e3, 6e3, 1e4, 15e3, 22e3, 3e4, 45e3, 6e4];
|
|
1165
|
+
async function fetchBalance(apiBaseUrl) {
|
|
1166
|
+
try {
|
|
1167
|
+
const res = await fetch(`${apiBaseUrl}/api/credits/balance`, { credentials: "include" });
|
|
1168
|
+
if (!res.ok) return null;
|
|
1169
|
+
return await res.json();
|
|
1170
|
+
} catch {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function clearStoredBaseline() {
|
|
1175
|
+
try {
|
|
1176
|
+
window.localStorage.removeItem(CHECKOUT_BASELINE_STORAGE_KEY);
|
|
1177
|
+
} catch {
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function takeStoredBaseline(now) {
|
|
1181
|
+
try {
|
|
1182
|
+
const raw = window.localStorage.getItem(CHECKOUT_BASELINE_STORAGE_KEY);
|
|
1183
|
+
if (raw === null) return null;
|
|
1184
|
+
window.localStorage.removeItem(CHECKOUT_BASELINE_STORAGE_KEY);
|
|
1185
|
+
const parsed = JSON.parse(raw);
|
|
1186
|
+
const net = Number(parsed?.net);
|
|
1187
|
+
const ts = Number(parsed?.ts);
|
|
1188
|
+
if (!Number.isFinite(net) || !Number.isFinite(ts)) return null;
|
|
1189
|
+
if (now - ts > CHECKOUT_BASELINE_TTL_MS) return null;
|
|
1190
|
+
return { net, userId: typeof parsed?.userId === "string" ? parsed.userId : void 0 };
|
|
1191
|
+
} catch {
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function useCheckoutReturnHandler({ apiBaseUrl = "", param = "checkout" } = {}) {
|
|
1196
|
+
const [status, setStatus] = useState8("idle");
|
|
1197
|
+
useEffect5(() => {
|
|
1198
|
+
if (typeof window === "undefined") return;
|
|
1199
|
+
const url = new URL(window.location.href);
|
|
1200
|
+
const marker = url.searchParams.get(param);
|
|
1201
|
+
if (!marker) return;
|
|
1202
|
+
url.searchParams.delete(param);
|
|
1203
|
+
window.history.replaceState(window.history.state, "", url.toString());
|
|
1204
|
+
if (marker === "cancelled") {
|
|
1205
|
+
clearStoredBaseline();
|
|
1206
|
+
setStatus("cancelled");
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
let cancelled = false;
|
|
1210
|
+
const timers = [];
|
|
1211
|
+
let channel = null;
|
|
1212
|
+
setStatus("checking");
|
|
1213
|
+
const stored = takeStoredBaseline(Date.now());
|
|
1214
|
+
let baseline = null;
|
|
1215
|
+
void (async () => {
|
|
1216
|
+
window.dispatchEvent(new Event(CREDITS_REFRESH_EVENT));
|
|
1217
|
+
try {
|
|
1218
|
+
channel = new BroadcastChannel("credits");
|
|
1219
|
+
channel.postMessage("refresh");
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
for (const delay of CONFIRM_SCHEDULE_MS) {
|
|
1223
|
+
timers.push(setTimeout(async () => {
|
|
1224
|
+
if (cancelled) return;
|
|
1225
|
+
const bal = await fetchBalance(apiBaseUrl);
|
|
1226
|
+
if (cancelled || !bal) return;
|
|
1227
|
+
if (baseline === null) {
|
|
1228
|
+
baseline = stored && stored.userId === bal.userId ? stored.net : creditNetMicros(bal);
|
|
1229
|
+
}
|
|
1230
|
+
if (creditNetMicros(bal) > baseline) {
|
|
1231
|
+
setStatus("confirmed");
|
|
1232
|
+
cancelled = true;
|
|
1233
|
+
window.dispatchEvent(new Event(CREDITS_REFRESH_EVENT));
|
|
1234
|
+
try {
|
|
1235
|
+
channel?.postMessage("refresh");
|
|
1236
|
+
} catch {
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}, delay));
|
|
1240
|
+
}
|
|
1241
|
+
timers.push(setTimeout(() => {
|
|
1242
|
+
if (!cancelled) setStatus((s) => s === "checking" ? "processing" : s);
|
|
1243
|
+
}, CONFIRM_SCHEDULE_MS[CONFIRM_SCHEDULE_MS.length - 1] + 2e3));
|
|
1244
|
+
})();
|
|
1245
|
+
return () => {
|
|
1246
|
+
cancelled = true;
|
|
1247
|
+
for (const t of timers) clearTimeout(t);
|
|
1248
|
+
channel?.close();
|
|
1249
|
+
};
|
|
1250
|
+
}, [apiBaseUrl, param]);
|
|
1251
|
+
return { status, dismiss: () => setStatus("idle") };
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/app/front/credits/CheckoutReturnBanner.tsx
|
|
1255
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1256
|
+
var COPY = {
|
|
1257
|
+
checking: { tone: "info", text: "Checking your payment\u2026" },
|
|
1258
|
+
confirmed: { tone: "success", text: "Credits added \u2014 thank you!" },
|
|
1259
|
+
// Reached on timeout WITHOUT a confirmed balance increase: don't claim the payment
|
|
1260
|
+
// was received (we can't confirm that from the client), just that it's still pending.
|
|
1261
|
+
processing: { tone: "info", text: "Your purchase is still being confirmed \u2014 credits usually appear within a minute. Refresh if it doesn\u2019t update." },
|
|
1262
|
+
cancelled: { tone: "warning", text: "Checkout cancelled \u2014 no charge was made." }
|
|
1263
|
+
};
|
|
1264
|
+
function CheckoutReturnBanner({ apiBaseUrl = "", param }) {
|
|
1265
|
+
const { status, dismiss } = useCheckoutReturnHandler({ apiBaseUrl, ...param ? { param } : {} });
|
|
1266
|
+
if (status === "idle") return null;
|
|
1267
|
+
const copy = COPY[status];
|
|
1268
|
+
if (!copy) return null;
|
|
1269
|
+
return /* @__PURE__ */ jsxs7(
|
|
1270
|
+
"div",
|
|
1271
|
+
{
|
|
1272
|
+
role: "status",
|
|
1273
|
+
"aria-live": "polite",
|
|
1274
|
+
"data-tone": copy.tone,
|
|
1275
|
+
style: {
|
|
1276
|
+
position: "fixed",
|
|
1277
|
+
bottom: 16,
|
|
1278
|
+
right: 16,
|
|
1279
|
+
zIndex: 1e3,
|
|
1280
|
+
maxWidth: 360,
|
|
1281
|
+
display: "flex",
|
|
1282
|
+
alignItems: "center",
|
|
1283
|
+
gap: 12,
|
|
1284
|
+
padding: "10px 12px",
|
|
1285
|
+
borderRadius: 8,
|
|
1286
|
+
fontSize: 13,
|
|
1287
|
+
background: "var(--color-surface, #fff)",
|
|
1288
|
+
color: "var(--color-foreground, inherit)",
|
|
1289
|
+
border: "1px solid var(--color-border, rgba(0,0,0,0.12))",
|
|
1290
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.12)"
|
|
1291
|
+
},
|
|
1292
|
+
children: [
|
|
1293
|
+
/* @__PURE__ */ jsx8("span", { children: copy.text }),
|
|
1294
|
+
/* @__PURE__ */ jsx8(
|
|
1295
|
+
"button",
|
|
1296
|
+
{
|
|
1297
|
+
type: "button",
|
|
1298
|
+
onClick: dismiss,
|
|
1299
|
+
"aria-label": "Dismiss",
|
|
1300
|
+
style: { marginLeft: "auto", background: "none", border: "none", cursor: "pointer", fontSize: 16, lineHeight: 1, color: "inherit" },
|
|
1301
|
+
children: "\xD7"
|
|
1302
|
+
}
|
|
1303
|
+
)
|
|
1304
|
+
]
|
|
1305
|
+
}
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// src/app/front/credits/BuyCreditsNoticeAction.tsx
|
|
1310
|
+
import { useState as useState9 } from "react";
|
|
1311
|
+
import { Button as Button3 } from "@hachej/boring-ui-kit";
|
|
1312
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1313
|
+
function BuyCreditsNoticeAction({ apiBaseUrl = "", label = "Buy credits" }) {
|
|
1314
|
+
const { buy, buying, balance, hidden } = useCreditBalance({ apiBaseUrl, pollMs: 6e4 });
|
|
1315
|
+
const [error, setError] = useState9(null);
|
|
1316
|
+
if (hidden || !balance || balance.checkoutEnabled === false) return null;
|
|
1317
|
+
const onBuy = async () => {
|
|
1318
|
+
setError(null);
|
|
1319
|
+
const message = await buy();
|
|
1320
|
+
if (message) setError(message);
|
|
1321
|
+
};
|
|
1322
|
+
return /* @__PURE__ */ jsxs8("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
|
|
1323
|
+
/* @__PURE__ */ jsx9(
|
|
1324
|
+
Button3,
|
|
1325
|
+
{
|
|
1326
|
+
type: "button",
|
|
1327
|
+
size: "sm",
|
|
1328
|
+
onClick: () => void onBuy(),
|
|
1329
|
+
disabled: buying,
|
|
1330
|
+
className: "bg-foreground text-background hover:bg-foreground/90",
|
|
1331
|
+
children: buying ? "Opening\u2026" : label
|
|
1332
|
+
}
|
|
1333
|
+
),
|
|
1334
|
+
error ? /* @__PURE__ */ jsx9("span", { role: "alert", className: "text-xs text-destructive", children: error }) : null
|
|
1335
|
+
] });
|
|
1336
|
+
}
|
|
561
1337
|
export {
|
|
562
|
-
|
|
1338
|
+
BuyCreditsNoticeAction,
|
|
1339
|
+
CREDITS_REFRESH_EVENT,
|
|
1340
|
+
CheckoutReturnBanner,
|
|
1341
|
+
CoreWorkspaceAgentFront,
|
|
1342
|
+
CreditBalanceBadge,
|
|
1343
|
+
CreditsSettingsPanel,
|
|
1344
|
+
DefaultTopBarRight,
|
|
1345
|
+
PAYMENT_REQUIRED_ERROR_CODE,
|
|
1346
|
+
formatCreditMicros,
|
|
1347
|
+
formatMinorPrice,
|
|
1348
|
+
formatSignedCreditMicros,
|
|
1349
|
+
isLowBalance,
|
|
1350
|
+
isPaymentRequiredNotice,
|
|
1351
|
+
useCheckoutReturnHandler,
|
|
1352
|
+
useCreditBalance,
|
|
1353
|
+
useCreditHistory
|
|
563
1354
|
};
|