aigetwey 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/assets/logo.svg +8 -0
- package/assets/screenshot.png +0 -0
- package/assets/wordmark.svg +9 -0
- package/config.example.yaml +56 -0
- package/dashboard/.env.example +12 -0
- package/dashboard/next-env.d.ts +6 -0
- package/dashboard/next.config.ts +12 -0
- package/dashboard/package-lock.json +1771 -0
- package/dashboard/package.json +29 -0
- package/dashboard/postcss.config.mjs +5 -0
- package/dashboard/src/app/(console)/combos/page.tsx +10 -0
- package/dashboard/src/app/(console)/config/page.tsx +5 -0
- package/dashboard/src/app/(console)/console/page.tsx +92 -0
- package/dashboard/src/app/(console)/endpoint/page.tsx +5 -0
- package/dashboard/src/app/(console)/layout.tsx +17 -0
- package/dashboard/src/app/(console)/page.tsx +8 -0
- package/dashboard/src/app/(console)/providers/[id]/page.tsx +6 -0
- package/dashboard/src/app/(console)/providers/page.tsx +5 -0
- package/dashboard/src/app/(console)/quota/page.tsx +5 -0
- package/dashboard/src/app/(console)/tools/[id]/page.tsx +6 -0
- package/dashboard/src/app/(console)/tools/page.tsx +5 -0
- package/dashboard/src/app/(console)/usage/page.tsx +24 -0
- package/dashboard/src/app/api/cli-detect/[tool]/route.ts +253 -0
- package/dashboard/src/app/api/gw/[...path]/route.ts +89 -0
- package/dashboard/src/app/api/login/route.ts +30 -0
- package/dashboard/src/app/api/logout/route.ts +9 -0
- package/dashboard/src/app/api/password/route.ts +34 -0
- package/dashboard/src/app/globals.css +340 -0
- package/dashboard/src/app/icon.svg +8 -0
- package/dashboard/src/app/layout.tsx +28 -0
- package/dashboard/src/app/login/page.tsx +60 -0
- package/dashboard/src/components/AreaChart.tsx +115 -0
- package/dashboard/src/components/Badge.tsx +32 -0
- package/dashboard/src/components/Button.tsx +60 -0
- package/dashboard/src/components/CapacityBadges.tsx +40 -0
- package/dashboard/src/components/Checkbox.tsx +40 -0
- package/dashboard/src/components/CliToolConfig.tsx +63 -0
- package/dashboard/src/components/ConfigEditor.tsx +199 -0
- package/dashboard/src/components/ConfirmModal.tsx +36 -0
- package/dashboard/src/components/CooldownTimer.tsx +42 -0
- package/dashboard/src/components/EndpointView.tsx +439 -0
- package/dashboard/src/components/Icon.tsx +25 -0
- package/dashboard/src/components/KeyReveal.tsx +78 -0
- package/dashboard/src/components/Lamp.tsx +8 -0
- package/dashboard/src/components/LogTable.tsx +223 -0
- package/dashboard/src/components/LogoutButton.tsx +20 -0
- package/dashboard/src/components/ModelPicker.tsx +121 -0
- package/dashboard/src/components/ModelSelectModal.tsx +126 -0
- package/dashboard/src/components/PasswordEditor.tsx +86 -0
- package/dashboard/src/components/PricingEditor.tsx +171 -0
- package/dashboard/src/components/ProviderDetail.tsx +566 -0
- package/dashboard/src/components/ProviderManager.tsx +311 -0
- package/dashboard/src/components/QuotaView.tsx +78 -0
- package/dashboard/src/components/Rail.tsx +82 -0
- package/dashboard/src/components/RichCard.tsx +46 -0
- package/dashboard/src/components/RoutingView.tsx +329 -0
- package/dashboard/src/components/ThemeProvider.tsx +36 -0
- package/dashboard/src/components/ToastProvider.tsx +58 -0
- package/dashboard/src/components/ToolDetail.tsx +475 -0
- package/dashboard/src/components/TopBar.tsx +128 -0
- package/dashboard/src/components/UsageView.tsx +151 -0
- package/dashboard/src/components/ui.tsx +54 -0
- package/dashboard/src/lib/capabilities.ts +318 -0
- package/dashboard/src/lib/cliTools.ts +120 -0
- package/dashboard/src/lib/client.ts +190 -0
- package/dashboard/src/lib/gateway.ts +269 -0
- package/dashboard/src/lib/session.ts +71 -0
- package/dashboard/src/middleware.ts +37 -0
- package/dashboard/tsconfig.json +21 -0
- package/dist/adapters/anthropic.js +289 -0
- package/dist/adapters/anthropic.js.map +1 -0
- package/dist/adapters/gemini.js +268 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openai.js +13 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/cli/tray/autostart.js +152 -0
- package/dist/cli/tray/autostart.js.map +1 -0
- package/dist/cli/tray/icon.js +4 -0
- package/dist/cli/tray/icon.js.map +1 -0
- package/dist/cli/tray/tray.js +141 -0
- package/dist/cli/tray/tray.js.map +1 -0
- package/dist/cli/tray/trayRuntime.js +91 -0
- package/dist/cli/tray/trayRuntime.js.map +1 -0
- package/dist/cli.js +361 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +728 -0
- package/dist/config.js.map +1 -0
- package/dist/core/authStore.js +78 -0
- package/dist/core/authStore.js.map +1 -0
- package/dist/core/canonical.js +9 -0
- package/dist/core/canonical.js.map +1 -0
- package/dist/core/console-buffer.js +25 -0
- package/dist/core/console-buffer.js.map +1 -0
- package/dist/core/fallback.js +62 -0
- package/dist/core/fallback.js.map +1 -0
- package/dist/core/handler.js +174 -0
- package/dist/core/handler.js.map +1 -0
- package/dist/core/keypool.js +105 -0
- package/dist/core/keypool.js.map +1 -0
- package/dist/core/quota.js +165 -0
- package/dist/core/quota.js.map +1 -0
- package/dist/core/state.js +52 -0
- package/dist/core/state.js.map +1 -0
- package/dist/db.js +193 -0
- package/dist/db.js.map +1 -0
- package/dist/headroom/compress.js +44 -0
- package/dist/headroom/compress.js.map +1 -0
- package/dist/headroom/detect.js +108 -0
- package/dist/headroom/detect.js.map +1 -0
- package/dist/headroom/process.js +158 -0
- package/dist/headroom/process.js.map +1 -0
- package/dist/inject/caveman.js +30 -0
- package/dist/inject/caveman.js.map +1 -0
- package/dist/inject/index.js +24 -0
- package/dist/inject/index.js.map +1 -0
- package/dist/inject/ponytail.js +19 -0
- package/dist/inject/ponytail.js.map +1 -0
- package/dist/middleware/auth.js +66 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/providers/capabilities.js +246 -0
- package/dist/providers/capabilities.js.map +1 -0
- package/dist/providers/free.js +43 -0
- package/dist/providers/free.js.map +1 -0
- package/dist/providers/pricing.js +224 -0
- package/dist/providers/pricing.js.map +1 -0
- package/dist/providers/vertex.js +97 -0
- package/dist/providers/vertex.js.map +1 -0
- package/dist/routes/admin.js +622 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/health.js +4 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/v1.js +75 -0
- package/dist/routes/v1.js.map +1 -0
- package/dist/rtk/detect.js +50 -0
- package/dist/rtk/detect.js.map +1 -0
- package/dist/rtk/filters.js +85 -0
- package/dist/rtk/filters.js.map +1 -0
- package/dist/rtk/index.js +39 -0
- package/dist/rtk/index.js.map +1 -0
- package/dist/server.js +100 -0
- package/dist/server.js.map +1 -0
- package/dist/stream/anthropic-stream.js +239 -0
- package/dist/stream/anthropic-stream.js.map +1 -0
- package/dist/stream/chunk.js +7 -0
- package/dist/stream/chunk.js.map +1 -0
- package/dist/stream/gemini-stream.js +135 -0
- package/dist/stream/gemini-stream.js.map +1 -0
- package/dist/stream/index.js +12 -0
- package/dist/stream/index.js.map +1 -0
- package/dist/stream/openai-stream.js +34 -0
- package/dist/stream/openai-stream.js.map +1 -0
- package/dist/stream/sse.js +64 -0
- package/dist/stream/sse.js.map +1 -0
- package/dist/translator/thinking.js +70 -0
- package/dist/translator/thinking.js.map +1 -0
- package/dist/translator/thinkingUnified.js +322 -0
- package/dist/translator/thinkingUnified.js.map +1 -0
- package/dist/upstream/client.js +120 -0
- package/dist/upstream/client.js.map +1 -0
- package/package.json +76 -0
- package/run.sh +27 -0
- package/src/adapters/anthropic.ts +377 -0
- package/src/adapters/gemini.ts +341 -0
- package/src/adapters/index.ts +17 -0
- package/src/adapters/openai.ts +22 -0
- package/src/cli/tray/autostart.ts +133 -0
- package/src/cli/tray/icon.ts +4 -0
- package/src/cli/tray/tray.ts +156 -0
- package/src/cli/tray/trayRuntime.ts +90 -0
- package/src/cli.ts +379 -0
- package/src/config.ts +777 -0
- package/src/core/authStore.ts +86 -0
- package/src/core/canonical.ts +93 -0
- package/src/core/console-buffer.ts +39 -0
- package/src/core/fallback.ts +116 -0
- package/src/core/handler.ts +236 -0
- package/src/core/keypool.ts +152 -0
- package/src/core/quota.ts +214 -0
- package/src/core/state.ts +65 -0
- package/src/db.ts +280 -0
- package/src/headroom/compress.ts +78 -0
- package/src/headroom/detect.ts +119 -0
- package/src/headroom/process.ts +166 -0
- package/src/inject/caveman.ts +35 -0
- package/src/inject/index.ts +46 -0
- package/src/inject/ponytail.ts +31 -0
- package/src/middleware/auth.ts +76 -0
- package/src/providers/capabilities.ts +297 -0
- package/src/providers/free.ts +53 -0
- package/src/providers/pricing.ts +261 -0
- package/src/providers/vertex.ts +117 -0
- package/src/routes/admin.ts +716 -0
- package/src/routes/health.ts +5 -0
- package/src/routes/index.ts +24 -0
- package/src/routes/v1.ts +87 -0
- package/src/rtk/detect.ts +55 -0
- package/src/rtk/filters.ts +94 -0
- package/src/rtk/index.ts +58 -0
- package/src/server.ts +108 -0
- package/src/stream/anthropic-stream.ts +310 -0
- package/src/stream/chunk.ts +46 -0
- package/src/stream/gemini-stream.ts +158 -0
- package/src/stream/index.ts +23 -0
- package/src/stream/openai-stream.ts +41 -0
- package/src/stream/sse.ts +72 -0
- package/src/translator/thinking.ts +64 -0
- package/src/translator/thinkingUnified.ts +319 -0
- package/src/upstream/client.ts +155 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Icon } from "./Icon";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Themed checkbox. Native checkboxes look out of place on the dark Haulix
|
|
5
|
+
* surface, so this is a button styled to match: filled with the accent + a
|
|
6
|
+
* check glyph when on, a bordered box when off. `indeterminate` renders a dash
|
|
7
|
+
* for "some selected" (drives the select-all row).
|
|
8
|
+
*/
|
|
9
|
+
export function Checkbox({
|
|
10
|
+
checked,
|
|
11
|
+
indeterminate,
|
|
12
|
+
onChange,
|
|
13
|
+
className,
|
|
14
|
+
ariaLabel,
|
|
15
|
+
}: {
|
|
16
|
+
checked: boolean;
|
|
17
|
+
indeterminate?: boolean;
|
|
18
|
+
onChange: () => void;
|
|
19
|
+
className?: string;
|
|
20
|
+
ariaLabel?: string;
|
|
21
|
+
}) {
|
|
22
|
+
const on = checked || indeterminate;
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
role="checkbox"
|
|
27
|
+
aria-checked={indeterminate ? "mixed" : checked}
|
|
28
|
+
aria-label={ariaLabel}
|
|
29
|
+
onClick={(e) => {
|
|
30
|
+
e.stopPropagation();
|
|
31
|
+
onChange();
|
|
32
|
+
}}
|
|
33
|
+
className={`flex h-[18px] w-[18px] flex-none items-center justify-center rounded-[5px] border transition-colors ${
|
|
34
|
+
on ? "border-accent bg-accent text-accent-ink" : "border-border bg-bg text-transparent hover:border-text-subtle"
|
|
35
|
+
}${className ? ` ${className}` : ""}`}
|
|
36
|
+
>
|
|
37
|
+
<Icon name={indeterminate ? "remove" : "check"} size={13} />
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { Badge } from "@/components/Badge";
|
|
6
|
+
import { Icon } from "@/components/Icon";
|
|
7
|
+
import { Empty } from "@/components/ui";
|
|
8
|
+
import { CLI_TOOLS } from "@/lib/cliTools";
|
|
9
|
+
import type { EndpointPayload } from "@/lib/gateway";
|
|
10
|
+
|
|
11
|
+
/** Grid of CLI tool setup cards. Each links to a step-by-step detail page. */
|
|
12
|
+
export function CliToolConfig() {
|
|
13
|
+
const [port, setPort] = useState<number | null>(null);
|
|
14
|
+
const [error, setError] = useState("");
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
void (async () => {
|
|
18
|
+
const res = await fetch("/api/gw/admin/endpoint");
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
setError("could not reach the gateway");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
setPort(((await res.json()) as EndpointPayload).port);
|
|
24
|
+
})();
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
if (error) return <Empty>{error}</Empty>;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<div className="mb-6">
|
|
32
|
+
<h1 className="text-[22px] font-semibold tracking-tight text-text">CLI Tools</h1>
|
|
33
|
+
<p className="mt-1 text-[13px] text-text-muted">
|
|
34
|
+
Point your coding tools at the gateway. {port ? `Listening on port ${port}.` : ""}
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
39
|
+
{CLI_TOOLS.map((t) => (
|
|
40
|
+
<Link
|
|
41
|
+
key={t.id}
|
|
42
|
+
href={`/tools/${t.id}`}
|
|
43
|
+
className="group rounded-brand-lg border border-border bg-surface p-4 shadow-soft transition-colors hover:border-text-subtle"
|
|
44
|
+
>
|
|
45
|
+
<div className="flex items-center justify-between gap-2">
|
|
46
|
+
<span className="flex items-center gap-2.5">
|
|
47
|
+
<span className="flex h-8 w-8 items-center justify-center rounded-brand bg-surface-2 text-text-muted group-hover:text-text">
|
|
48
|
+
<Icon name={t.icon} size={18} />
|
|
49
|
+
</span>
|
|
50
|
+
<span className="text-[14px] font-semibold text-text">{t.name}</span>
|
|
51
|
+
</span>
|
|
52
|
+
<Badge tone="info">{t.format}</Badge>
|
|
53
|
+
</div>
|
|
54
|
+
<p className="mt-2 text-[12.5px] text-text-muted">{t.blurb}</p>
|
|
55
|
+
<span className="mt-3 inline-flex items-center gap-1 text-[12px] text-text-subtle group-hover:text-text">
|
|
56
|
+
Setup <Icon name="arrow_forward" size={14} />
|
|
57
|
+
</span>
|
|
58
|
+
</Link>
|
|
59
|
+
))}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback, useRef } from "react";
|
|
4
|
+
import { Button } from "@/components/Button";
|
|
5
|
+
import { Badge } from "@/components/Badge";
|
|
6
|
+
import { RichCard, CardTitle } from "@/components/RichCard";
|
|
7
|
+
import { Icon } from "@/components/Icon";
|
|
8
|
+
import { Empty } from "@/components/ui";
|
|
9
|
+
import { PricingEditor } from "@/components/PricingEditor";
|
|
10
|
+
import { PasswordEditor } from "@/components/PasswordEditor";
|
|
11
|
+
import { stringify } from "yaml";
|
|
12
|
+
import type { MaskedConfig } from "@/lib/gateway";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Settings — structured cards (instance summary, per-model pricing, backup) with
|
|
16
|
+
* the raw config editor tucked into an Advanced disclosure. Saving the YAML
|
|
17
|
+
* re-validates (zod) and hot-reloads on the gateway; an invalid edit is rejected
|
|
18
|
+
* with the message and the live config keeps serving. Masked keys (sk-…1234) left
|
|
19
|
+
* unchanged are restored server-side.
|
|
20
|
+
*/
|
|
21
|
+
export function ConfigEditor() {
|
|
22
|
+
const [text, setText] = useState("");
|
|
23
|
+
const [original, setOriginal] = useState("");
|
|
24
|
+
const [info, setInfo] = useState<MaskedConfig["server"] | null>(null);
|
|
25
|
+
const [error, setError] = useState("");
|
|
26
|
+
const [saved, setSaved] = useState(false);
|
|
27
|
+
const [busy, setBusy] = useState(false);
|
|
28
|
+
const [loading, setLoading] = useState(true);
|
|
29
|
+
|
|
30
|
+
const reload = useCallback(async () => {
|
|
31
|
+
setLoading(true);
|
|
32
|
+
const res = await fetch("/api/gw/admin/config");
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
setError("could not reach the gateway");
|
|
35
|
+
setLoading(false);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const cfg = (await res.json()) as MaskedConfig;
|
|
39
|
+
const yaml = stringify(cfg);
|
|
40
|
+
setText(yaml);
|
|
41
|
+
setOriginal(yaml);
|
|
42
|
+
setInfo(cfg.server);
|
|
43
|
+
setError("");
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
void reload();
|
|
49
|
+
}, [reload]);
|
|
50
|
+
|
|
51
|
+
async function save() {
|
|
52
|
+
setBusy(true);
|
|
53
|
+
setError("");
|
|
54
|
+
setSaved(false);
|
|
55
|
+
const res = await fetch("/api/gw/admin/config", {
|
|
56
|
+
method: "PUT",
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
body: JSON.stringify({ text }),
|
|
59
|
+
});
|
|
60
|
+
setBusy(false);
|
|
61
|
+
if (res.ok) {
|
|
62
|
+
setSaved(true);
|
|
63
|
+
setTimeout(() => setSaved(false), 2000);
|
|
64
|
+
await reload();
|
|
65
|
+
} else {
|
|
66
|
+
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
67
|
+
setError(body.error ?? "validation failed");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const fileInput = useRef<HTMLInputElement>(null);
|
|
72
|
+
|
|
73
|
+
// Export the UNMASKED backup straight through the proxy. The download attribute
|
|
74
|
+
// forces a save with our filename even though the proxy labels it as JSON.
|
|
75
|
+
function exportConfig() {
|
|
76
|
+
const a = document.createElement("a");
|
|
77
|
+
a.href = "/api/gw/admin/config/export";
|
|
78
|
+
a.download = "aigetwey-config.yaml";
|
|
79
|
+
document.body.appendChild(a);
|
|
80
|
+
a.click();
|
|
81
|
+
a.remove();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Load a backup file INTO the editor (not a blind apply) so it goes through the
|
|
85
|
+
// same validate + hot-reload path on Save, and the operator reviews it first.
|
|
86
|
+
async function importFile(e: React.ChangeEvent<HTMLInputElement>) {
|
|
87
|
+
const file = e.target.files?.[0];
|
|
88
|
+
e.target.value = "";
|
|
89
|
+
if (!file) return;
|
|
90
|
+
setText(await file.text());
|
|
91
|
+
setError("");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const dirty = text !== original;
|
|
95
|
+
const keyCount = info?.api_keys.length ?? 0;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div>
|
|
99
|
+
<div className="mb-6">
|
|
100
|
+
<h1 className="text-[22px] font-semibold tracking-tight text-text">Settings</h1>
|
|
101
|
+
<p className="mt-1 text-[13px] text-text-muted">Instance details, model pricing, and backup.</p>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="grid gap-4">
|
|
105
|
+
<RichCard header={<CardTitle title="Instance" sub="read-only" />}>
|
|
106
|
+
{info ? (
|
|
107
|
+
<div className="space-y-2.5 text-[13px]">
|
|
108
|
+
<Row label="Listen address">
|
|
109
|
+
<span className="tnum text-text">{info.host}:{info.port}</span>
|
|
110
|
+
</Row>
|
|
111
|
+
<Row label="Gateway auth">
|
|
112
|
+
<Badge tone={keyCount > 0 ? "live" : "warn"}>
|
|
113
|
+
{keyCount > 0 ? `${keyCount} key${keyCount > 1 ? "s" : ""}` : "disabled (localhost only)"}
|
|
114
|
+
</Badge>
|
|
115
|
+
</Row>
|
|
116
|
+
<Row label="Admin password">
|
|
117
|
+
<span className="text-text-subtle">seeded from AIGETWEY_ADMIN_PASSWORD — change it below</span>
|
|
118
|
+
</Row>
|
|
119
|
+
</div>
|
|
120
|
+
) : (
|
|
121
|
+
<Empty>Loading…</Empty>
|
|
122
|
+
)}
|
|
123
|
+
</RichCard>
|
|
124
|
+
|
|
125
|
+
<PasswordEditor />
|
|
126
|
+
|
|
127
|
+
<PricingEditor />
|
|
128
|
+
|
|
129
|
+
<RichCard
|
|
130
|
+
header={
|
|
131
|
+
<>
|
|
132
|
+
<CardTitle title="Backup" sub="full config including real keys" />
|
|
133
|
+
<div className="flex items-center gap-2">
|
|
134
|
+
<input ref={fileInput} type="file" accept=".yaml,.yml,.json,text/*" className="hidden" onChange={importFile} />
|
|
135
|
+
<Button variant="ghost" disabled={busy} onClick={exportConfig} title="Download the full config (includes real keys)">
|
|
136
|
+
<Icon name="download" size={14} /> Export
|
|
137
|
+
</Button>
|
|
138
|
+
<Button variant="ghost" disabled={busy} onClick={() => fileInput.current?.click()} title="Load a backup file into the Advanced editor">
|
|
139
|
+
<Icon name="upload" size={14} /> Import
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
</>
|
|
143
|
+
}
|
|
144
|
+
>
|
|
145
|
+
<p className="text-[12.5px] text-text-muted">
|
|
146
|
+
Export downloads the live config as YAML with unmasked keys — keep it safe. Import loads a file into
|
|
147
|
+
the raw editor below for review; it only applies when you Save there.
|
|
148
|
+
</p>
|
|
149
|
+
</RichCard>
|
|
150
|
+
|
|
151
|
+
<details className="group overflow-hidden rounded-brand-lg border border-border bg-surface shadow-soft">
|
|
152
|
+
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 [&::-webkit-details-marker]:hidden">
|
|
153
|
+
<CardTitle title="Advanced — raw config" sub="YAML, validated + hot-reloaded on save" />
|
|
154
|
+
<span className="flex items-center gap-2">
|
|
155
|
+
{dirty && <span className="text-[12px] text-warning">unsaved changes</span>}
|
|
156
|
+
{saved && (
|
|
157
|
+
<span className="flex items-center gap-1 text-[12px] text-success">
|
|
158
|
+
<Icon name="check" size={14} /> saved
|
|
159
|
+
</span>
|
|
160
|
+
)}
|
|
161
|
+
<Icon name="expand_more" size={18} className="text-text-subtle transition-transform group-open:rotate-180" />
|
|
162
|
+
</span>
|
|
163
|
+
</summary>
|
|
164
|
+
|
|
165
|
+
<div className="border-t border-border-subtle p-4">
|
|
166
|
+
<div className="mb-3 flex items-center justify-end gap-2">
|
|
167
|
+
<Button variant="ghost" disabled={!dirty || busy} onClick={() => setText(original)}>Revert</Button>
|
|
168
|
+
<Button disabled={!dirty || busy} onClick={save}>{busy ? "Saving…" : "Save & reload"}</Button>
|
|
169
|
+
</div>
|
|
170
|
+
{error && (
|
|
171
|
+
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap rounded-brand border border-danger/40 bg-danger/8 px-3 py-2 text-[12px] text-danger">
|
|
172
|
+
{error}
|
|
173
|
+
</pre>
|
|
174
|
+
)}
|
|
175
|
+
{loading ? (
|
|
176
|
+
<Empty>Loading…</Empty>
|
|
177
|
+
) : (
|
|
178
|
+
<textarea
|
|
179
|
+
value={text}
|
|
180
|
+
onChange={(e) => setText(e.target.value)}
|
|
181
|
+
spellCheck={false}
|
|
182
|
+
className="h-[55vh] w-full resize-none rounded-brand border border-border bg-bg p-4 font-mono text-[12.5px] leading-relaxed text-text focus:border-accent focus:outline-none"
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</details>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function Row({ label, children }: { label: string; children: React.ReactNode }) {
|
|
193
|
+
return (
|
|
194
|
+
<div className="flex items-center justify-between gap-3">
|
|
195
|
+
<span className="text-text-subtle">{label}</span>
|
|
196
|
+
{children}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/Button";
|
|
4
|
+
import { Icon } from "@/components/Icon";
|
|
5
|
+
|
|
6
|
+
interface ConfirmModalProps {
|
|
7
|
+
title: string;
|
|
8
|
+
message: string;
|
|
9
|
+
confirmLabel?: string;
|
|
10
|
+
onConfirm: () => void;
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
busy?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ConfirmModal({ title, message, confirmLabel = "Delete", onConfirm, onCancel, busy }: ConfirmModalProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-6" onClick={onCancel}>
|
|
18
|
+
<div
|
|
19
|
+
className="w-full max-w-sm rounded-brand-lg border border-border bg-surface p-5 shadow-elevated"
|
|
20
|
+
onClick={(e) => e.stopPropagation()}
|
|
21
|
+
>
|
|
22
|
+
<div className="mb-2 flex items-center gap-2">
|
|
23
|
+
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-danger/10 text-danger">
|
|
24
|
+
<Icon name="warning" size={16} />
|
|
25
|
+
</span>
|
|
26
|
+
<h2 className="text-[15px] font-semibold text-text">{title}</h2>
|
|
27
|
+
</div>
|
|
28
|
+
<p className="mb-4 text-[12.5px] text-text-muted">{message}</p>
|
|
29
|
+
<div className="flex justify-end gap-2">
|
|
30
|
+
<Button variant="ghost" onClick={onCancel}>Cancel</Button>
|
|
31
|
+
<Button variant="danger" disabled={busy} onClick={onConfirm}>{confirmLabel}</Button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Icon } from "./Icon";
|
|
5
|
+
import { fmt } from "./ui";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Live countdown from a gateway-snapshot remaining-ms. Counts down locally each
|
|
9
|
+
* second from render. Used for both key cooldowns (danger tone) and quota window
|
|
10
|
+
* resets (muted tone). Renders nothing once it hits zero unless `keepZero`.
|
|
11
|
+
*/
|
|
12
|
+
export function CooldownTimer({
|
|
13
|
+
ms,
|
|
14
|
+
tone = "danger",
|
|
15
|
+
icon = "timer",
|
|
16
|
+
keepZero = false,
|
|
17
|
+
}: {
|
|
18
|
+
ms: number;
|
|
19
|
+
tone?: "danger" | "muted";
|
|
20
|
+
icon?: string;
|
|
21
|
+
keepZero?: boolean;
|
|
22
|
+
}) {
|
|
23
|
+
const [until] = useState(() => Date.now() + ms);
|
|
24
|
+
const [remaining, setRemaining] = useState(() => Math.max(0, ms));
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const tick = () => setRemaining(Math.max(0, until - Date.now()));
|
|
28
|
+
tick();
|
|
29
|
+
const id = setInterval(tick, 1000);
|
|
30
|
+
return () => clearInterval(id);
|
|
31
|
+
}, [until]);
|
|
32
|
+
|
|
33
|
+
if (remaining <= 0 && !keepZero) return null;
|
|
34
|
+
|
|
35
|
+
const color = tone === "danger" ? "text-danger" : "text-text-muted";
|
|
36
|
+
return (
|
|
37
|
+
<span className={`inline-flex items-center gap-1 tnum text-[12px] ${color}`}>
|
|
38
|
+
<Icon name={icon} size={13} />
|
|
39
|
+
{remaining <= 0 ? "now" : fmt.duration(remaining)}
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
}
|