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