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,223 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
|
+
import { Lamp } from "@/components/Lamp";
|
|
5
|
+
import { Badge } from "@/components/Badge";
|
|
6
|
+
import { Icon } from "@/components/Icon";
|
|
7
|
+
import { fmt } from "@/components/ui";
|
|
8
|
+
import type { UsageLog } from "@/lib/gateway";
|
|
9
|
+
|
|
10
|
+
type StatusFilter = "all" | "ok" | "error";
|
|
11
|
+
|
|
12
|
+
const FILTERS: { key: StatusFilter; label: string }[] = [
|
|
13
|
+
{ key: "all", label: "All" },
|
|
14
|
+
{ key: "ok", label: "Success" },
|
|
15
|
+
{ key: "error", label: "Errors" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function LogTable({ logs: initial }: { logs: UsageLog[] }) {
|
|
19
|
+
const [logs, setLogs] = useState(initial);
|
|
20
|
+
const [filter, setFilter] = useState<StatusFilter>("all");
|
|
21
|
+
const [provFilter, setProvFilter] = useState<string>("all");
|
|
22
|
+
const [live, setLive] = useState(true);
|
|
23
|
+
const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
|
|
24
|
+
const timer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
25
|
+
|
|
26
|
+
const refresh = useCallback(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch("/api/gw/admin/logs?limit=200");
|
|
29
|
+
if (res.ok) {
|
|
30
|
+
const data = (await res.json()) as { logs: UsageLog[] };
|
|
31
|
+
setLogs(data.logs);
|
|
32
|
+
}
|
|
33
|
+
} catch { /* ignore */ }
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (live) {
|
|
38
|
+
timer.current = setInterval(refresh, 5000);
|
|
39
|
+
return () => { if (timer.current) clearInterval(timer.current); };
|
|
40
|
+
}
|
|
41
|
+
if (timer.current) clearInterval(timer.current);
|
|
42
|
+
}, [live, refresh]);
|
|
43
|
+
|
|
44
|
+
const providers = [...new Set(logs.map((l) => l.provider))].sort();
|
|
45
|
+
|
|
46
|
+
const okCount = logs.filter((l) => l.status >= 200 && l.status < 300).length;
|
|
47
|
+
const errCount = logs.length - okCount;
|
|
48
|
+
|
|
49
|
+
const shown = logs.filter((l) => {
|
|
50
|
+
if (filter === "ok" && !(l.status >= 200 && l.status < 300)) return false;
|
|
51
|
+
if (filter === "error" && l.status >= 200 && l.status < 300) return false;
|
|
52
|
+
if (provFilter !== "all" && l.provider !== provFilter) return false;
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const count = (k: StatusFilter) => (k === "all" ? logs.length : k === "ok" ? okCount : errCount);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="overflow-hidden rounded-brand-lg border border-border bg-surface shadow-soft">
|
|
60
|
+
<header className="flex flex-wrap items-center justify-between gap-3 border-b border-border-subtle px-4 py-3">
|
|
61
|
+
<div className="flex items-center gap-1">
|
|
62
|
+
{FILTERS.map((f) => (
|
|
63
|
+
<button
|
|
64
|
+
key={f.key}
|
|
65
|
+
onClick={() => setFilter(f.key)}
|
|
66
|
+
className={`flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[12px] font-medium transition-colors ${
|
|
67
|
+
filter === f.key ? "bg-surface-2 text-text" : "text-text-muted hover:text-text"
|
|
68
|
+
}`}
|
|
69
|
+
>
|
|
70
|
+
{f.label}
|
|
71
|
+
<span className="tnum text-text-subtle">{count(f.key)}</span>
|
|
72
|
+
</button>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
<div className="flex items-center gap-2">
|
|
76
|
+
{providers.length > 1 && (
|
|
77
|
+
<select
|
|
78
|
+
value={provFilter}
|
|
79
|
+
onChange={(e) => setProvFilter(e.target.value)}
|
|
80
|
+
className="rounded border border-border-subtle bg-transparent px-2 py-1 text-[11px] text-text-muted focus:border-accent focus:outline-none"
|
|
81
|
+
>
|
|
82
|
+
<option value="all">All providers</option>
|
|
83
|
+
{providers.map((p) => (
|
|
84
|
+
<option key={p} value={p}>{p}</option>
|
|
85
|
+
))}
|
|
86
|
+
</select>
|
|
87
|
+
)}
|
|
88
|
+
<button
|
|
89
|
+
onClick={() => setLive((v) => !v)}
|
|
90
|
+
className={`flex items-center gap-1 rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors ${
|
|
91
|
+
live ? "bg-success/15 text-success" : "text-text-muted hover:text-text"
|
|
92
|
+
}`}
|
|
93
|
+
title={live ? "Auto-refresh ON (5s)" : "Auto-refresh OFF"}
|
|
94
|
+
>
|
|
95
|
+
<Icon name={live ? "radio_button_checked" : "radio_button_unchecked"} size={12} />
|
|
96
|
+
Live
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
</header>
|
|
100
|
+
|
|
101
|
+
{shown.length === 0 ? (
|
|
102
|
+
<div className="px-4 py-8 text-center text-[13px] text-text-muted">
|
|
103
|
+
{logs.length === 0 ? "No requests recorded yet." : "No requests match this filter."}
|
|
104
|
+
</div>
|
|
105
|
+
) : (
|
|
106
|
+
<>
|
|
107
|
+
<div className="table-wrap">
|
|
108
|
+
<table className="w-full min-w-[720px] border-collapse">
|
|
109
|
+
<thead>
|
|
110
|
+
<tr className="text-text-subtle">
|
|
111
|
+
{["Status", "Time", "Alias", "Provider", "Model", "In", "Out", "ms", "Mode"].map((h, i) => (
|
|
112
|
+
<th
|
|
113
|
+
key={h + i}
|
|
114
|
+
className={`whitespace-nowrap px-4 pb-2.5 pt-3 text-[10px] font-medium uppercase tracking-wider ${
|
|
115
|
+
i >= 5 && i <= 7 ? "text-right" : "text-left"
|
|
116
|
+
}`}
|
|
117
|
+
>
|
|
118
|
+
{h}
|
|
119
|
+
</th>
|
|
120
|
+
))}
|
|
121
|
+
</tr>
|
|
122
|
+
</thead>
|
|
123
|
+
<tbody>
|
|
124
|
+
{shown.map((l, i) => {
|
|
125
|
+
const ok = l.status >= 200 && l.status < 300;
|
|
126
|
+
const isExpanded = expandedIdx === i;
|
|
127
|
+
return (
|
|
128
|
+
<tr key={i} onClick={() => setExpandedIdx(isExpanded ? null : i)} className={`border-t border-border-subtle cursor-pointer transition-colors ${isExpanded ? "bg-surface-2/60" : "hover:bg-surface-2/40"}`}>
|
|
129
|
+
<Td>
|
|
130
|
+
<Badge tone={ok ? "live" : "down"}>
|
|
131
|
+
<Lamp state={ok ? "live" : "down"} />
|
|
132
|
+
{l.status}
|
|
133
|
+
</Badge>
|
|
134
|
+
</Td>
|
|
135
|
+
<Td muted title={fmt.time(l.ts)}>
|
|
136
|
+
{fmt.ago(l.ts)} ago
|
|
137
|
+
</Td>
|
|
138
|
+
<Td className="text-text">{l.alias}</Td>
|
|
139
|
+
<Td muted>{l.provider}</Td>
|
|
140
|
+
<Td muted>{l.model}</Td>
|
|
141
|
+
<Td right>{fmt.int(l.tokens_in)}</Td>
|
|
142
|
+
<Td right>{fmt.int(l.tokens_out)}</Td>
|
|
143
|
+
<Td right>{fmt.int(l.latency_ms)}</Td>
|
|
144
|
+
<Td muted>{l.stream ? "stream" : "unary"}</Td>
|
|
145
|
+
</tr>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</tbody>
|
|
149
|
+
</table>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{expandedIdx !== null && shown[expandedIdx] && (
|
|
153
|
+
<RequestDetail log={shown[expandedIdx]} onClose={() => setExpandedIdx(null)} />
|
|
154
|
+
)}
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function Td({
|
|
162
|
+
children,
|
|
163
|
+
muted,
|
|
164
|
+
right,
|
|
165
|
+
title,
|
|
166
|
+
className,
|
|
167
|
+
}: {
|
|
168
|
+
children: React.ReactNode;
|
|
169
|
+
muted?: boolean;
|
|
170
|
+
right?: boolean;
|
|
171
|
+
title?: string;
|
|
172
|
+
className?: string;
|
|
173
|
+
}) {
|
|
174
|
+
return (
|
|
175
|
+
<td
|
|
176
|
+
title={title}
|
|
177
|
+
className={`whitespace-nowrap px-4 py-2.5 tnum text-[12.5px] ${right ? "text-right" : "text-left"} ${
|
|
178
|
+
muted ? "text-text-muted" : "text-text"
|
|
179
|
+
}${className ? ` ${className}` : ""}`}
|
|
180
|
+
>
|
|
181
|
+
{children}
|
|
182
|
+
</td>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function RequestDetail({ log, onClose }: { log: UsageLog; onClose: () => void }) {
|
|
187
|
+
const ok = log.status >= 200 && log.status < 300;
|
|
188
|
+
const totalTokens = log.tokens_in + log.tokens_out;
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div className="border-t border-border-subtle bg-surface-2/40 px-5 py-4">
|
|
192
|
+
<div className="mb-3 flex items-center justify-between">
|
|
193
|
+
<span className="text-[12px] font-semibold text-text">Request Details</span>
|
|
194
|
+
<button onClick={onClose} className="text-text-subtle hover:text-text">
|
|
195
|
+
<Icon name="close" size={16} />
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-[12px] sm:grid-cols-4">
|
|
199
|
+
<DetailCell label="Status" value={`${log.status} ${ok ? "OK" : "Error"}`} />
|
|
200
|
+
<DetailCell label="Timestamp" value={new Date(log.ts).toLocaleString()} />
|
|
201
|
+
<DetailCell label="Alias" value={log.alias} />
|
|
202
|
+
<DetailCell label="Provider" value={log.provider} />
|
|
203
|
+
<DetailCell label="Model" value={log.model} />
|
|
204
|
+
<DetailCell label="Mode" value={log.stream ? "Streaming" : "Unary"} />
|
|
205
|
+
<DetailCell label="Latency" value={`${log.latency_ms}ms`} />
|
|
206
|
+
<DetailCell label="Cost" value={fmt.cost(log.cost)} />
|
|
207
|
+
<DetailCell label="Input tokens" value={fmt.int(log.tokens_in)} />
|
|
208
|
+
<DetailCell label="Output tokens" value={fmt.int(log.tokens_out)} />
|
|
209
|
+
<DetailCell label="Cached tokens" value={fmt.int(log.cached_tokens)} />
|
|
210
|
+
<DetailCell label="Total tokens" value={fmt.int(totalTokens)} />
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function DetailCell({ label, value }: { label: string; value: string }) {
|
|
217
|
+
return (
|
|
218
|
+
<div>
|
|
219
|
+
<span className="text-[10px] uppercase tracking-wider text-text-subtle">{label}</span>
|
|
220
|
+
<div className="tnum text-text">{value}</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { Button } from "./Button";
|
|
5
|
+
import { Icon } from "./Icon";
|
|
6
|
+
|
|
7
|
+
export function LogoutButton() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
async function logout() {
|
|
10
|
+
await fetch("/api/logout", { method: "POST" });
|
|
11
|
+
router.replace("/login");
|
|
12
|
+
router.refresh();
|
|
13
|
+
}
|
|
14
|
+
return (
|
|
15
|
+
<Button variant="ghost" onClick={logout} className="w-full">
|
|
16
|
+
<Icon name="logout" size={17} />
|
|
17
|
+
Disconnect
|
|
18
|
+
</Button>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Icon } from "@/components/Icon";
|
|
5
|
+
import { CapacityBadges } from "@/components/CapacityBadges";
|
|
6
|
+
|
|
7
|
+
export interface ModelGroup {
|
|
8
|
+
label: string;
|
|
9
|
+
items: { value: string; label: string; tag?: string }[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* aigetwey-style model picker: a search box + provider-grouped chips you click to
|
|
14
|
+
* toggle in/out of a selection. Used by the combo form and the CLI-tool model
|
|
15
|
+
* selection so both add models the same way (click to add, click again to drop).
|
|
16
|
+
*/
|
|
17
|
+
export function ModelPicker({
|
|
18
|
+
title = "Add models",
|
|
19
|
+
note = "Click to add, click again to remove.",
|
|
20
|
+
groups,
|
|
21
|
+
selected,
|
|
22
|
+
onToggle,
|
|
23
|
+
onClose,
|
|
24
|
+
}: {
|
|
25
|
+
title?: string;
|
|
26
|
+
note?: string;
|
|
27
|
+
groups: ModelGroup[];
|
|
28
|
+
selected: string[];
|
|
29
|
+
onToggle: (value: string) => void;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
}) {
|
|
32
|
+
const [q, setQ] = useState("");
|
|
33
|
+
const needle = q.trim().toLowerCase();
|
|
34
|
+
const filtered = groups
|
|
35
|
+
.map((g) => ({
|
|
36
|
+
...g,
|
|
37
|
+
items: needle ? g.items.filter((i) => i.value.toLowerCase().includes(needle)) : g.items,
|
|
38
|
+
}))
|
|
39
|
+
.filter((g) => g.items.length > 0);
|
|
40
|
+
const sel = new Set(selected);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/60 p-6 sm:p-10" onClick={onClose}>
|
|
44
|
+
<div
|
|
45
|
+
className="flex max-h-[80vh] w-full max-w-lg flex-col rounded-brand-lg border border-border bg-surface shadow-elevated"
|
|
46
|
+
onClick={(e) => e.stopPropagation()}
|
|
47
|
+
>
|
|
48
|
+
<div className="flex items-center justify-between border-b border-border-subtle px-4 py-3">
|
|
49
|
+
<h2 className="text-[14px] font-semibold text-text">{title}</h2>
|
|
50
|
+
<button onClick={onClose} className="text-text-subtle hover:text-text" aria-label="Close">
|
|
51
|
+
<Icon name="close" size={18} />
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="border-b border-border-subtle px-4 py-3">
|
|
56
|
+
<p className="mb-2 text-[12px] text-text-muted">{note}</p>
|
|
57
|
+
<div className="relative">
|
|
58
|
+
<Icon name="search" size={15} className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-text-subtle" />
|
|
59
|
+
<input
|
|
60
|
+
autoFocus
|
|
61
|
+
value={q}
|
|
62
|
+
onChange={(e) => setQ(e.target.value)}
|
|
63
|
+
placeholder="Search models…"
|
|
64
|
+
className="w-full rounded-brand border border-border bg-bg py-2 pl-8 pr-3 text-[13px] text-text placeholder:text-text-subtle focus:border-accent focus:outline-none"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="flex-1 overflow-y-auto px-4 py-3">
|
|
70
|
+
{filtered.length === 0 ? (
|
|
71
|
+
<p className="py-6 text-center text-[12.5px] text-text-subtle">No models match “{q}”.</p>
|
|
72
|
+
) : (
|
|
73
|
+
<div className="space-y-4">
|
|
74
|
+
{filtered.map((g) => (
|
|
75
|
+
<div key={g.label}>
|
|
76
|
+
<div className="mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-text-subtle">
|
|
77
|
+
{g.label} <span className="tnum text-text-subtle/70">({g.items.length})</span>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="flex flex-wrap gap-1.5">
|
|
80
|
+
{g.items.map((it) => {
|
|
81
|
+
const on = sel.has(it.value);
|
|
82
|
+
return (
|
|
83
|
+
<button
|
|
84
|
+
key={it.value}
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={() => onToggle(it.value)}
|
|
87
|
+
className={`inline-flex items-center gap-1 rounded-brand border px-2 py-1 text-[12px] transition-colors ${
|
|
88
|
+
on ? "border-accent bg-accent-soft text-accent" : "border-border bg-bg text-text-muted hover:border-text-subtle hover:text-text"
|
|
89
|
+
}`}
|
|
90
|
+
>
|
|
91
|
+
{on && <Icon name="check" size={12} />}
|
|
92
|
+
<span className="tnum">{it.label}</span>
|
|
93
|
+
<CapacityBadges model={it.value} size={13} />
|
|
94
|
+
{it.tag && <span className="rounded bg-surface-2 px-1 text-[10px] text-text-subtle">{it.tag}</span>}
|
|
95
|
+
</button>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div className="border-t border-border-subtle px-4 py-2 text-[11px] text-text-subtle">
|
|
106
|
+
<Icon name="neurology" size={12} className="mr-1 inline align-text-bottom text-warning" />
|
|
107
|
+
Reasoning models accept a thinking suffix — call{" "}
|
|
108
|
+
<code className="rounded bg-surface-2 px-1 text-text-muted">model(high)</code> or{" "}
|
|
109
|
+
<code className="rounded bg-surface-2 px-1 text-text-muted">model(none)</code> (high·low·medium·minimal·auto·none·or a token budget).
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className="flex items-center justify-between border-t border-border-subtle px-4 py-3">
|
|
113
|
+
<span className="tnum text-[12px] text-text-subtle">{selected.length} selected</span>
|
|
114
|
+
<button onClick={onClose} className="rounded-brand bg-accent px-3.5 py-1.5 text-[13px] font-semibold text-accent-ink hover:bg-accent-hover">
|
|
115
|
+
Done
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import { Button, Input } from "@/components/Button";
|
|
5
|
+
import { Checkbox } from "@/components/Checkbox";
|
|
6
|
+
import { Icon } from "@/components/Icon";
|
|
7
|
+
|
|
8
|
+
export interface DiscoveredModel {
|
|
9
|
+
id: string;
|
|
10
|
+
added: boolean; // already in the provider catalog
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pick which discovered models to add. Free providers can return hundreds of
|
|
15
|
+
* ids, so this is a filterable checklist (defaulting to only the not-yet-added
|
|
16
|
+
* ones selected) rather than dumping everything into the catalog.
|
|
17
|
+
*/
|
|
18
|
+
export function ModelSelectModal({
|
|
19
|
+
models,
|
|
20
|
+
busy,
|
|
21
|
+
onAdd,
|
|
22
|
+
onClose,
|
|
23
|
+
}: {
|
|
24
|
+
models: DiscoveredModel[];
|
|
25
|
+
busy: boolean;
|
|
26
|
+
onAdd: (ids: string[]) => void;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
}) {
|
|
29
|
+
const [filter, setFilter] = useState("");
|
|
30
|
+
const [picked, setPicked] = useState<Set<string>>(() => new Set(models.filter((m) => !m.added).map((m) => m.id)));
|
|
31
|
+
|
|
32
|
+
const shown = useMemo(() => {
|
|
33
|
+
const q = filter.trim().toLowerCase();
|
|
34
|
+
return q ? models.filter((m) => m.id.toLowerCase().includes(q)) : models;
|
|
35
|
+
}, [models, filter]);
|
|
36
|
+
|
|
37
|
+
function toggle(id: string) {
|
|
38
|
+
setPicked((s) => {
|
|
39
|
+
const next = new Set(s);
|
|
40
|
+
if (next.has(id)) next.delete(id);
|
|
41
|
+
else next.add(id);
|
|
42
|
+
return next;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const shownPickedCount = shown.filter((m) => picked.has(m.id)).length;
|
|
47
|
+
const allShownPicked = shown.length > 0 && shownPickedCount === shown.length;
|
|
48
|
+
const someShownPicked = shownPickedCount > 0 && !allShownPicked;
|
|
49
|
+
function toggleAllShown() {
|
|
50
|
+
setPicked((s) => {
|
|
51
|
+
const next = new Set(s);
|
|
52
|
+
if (allShownPicked) shown.forEach((m) => next.delete(m.id));
|
|
53
|
+
else shown.forEach((m) => next.add(m.id));
|
|
54
|
+
return next;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="fixed inset-0 z-50 grid place-items-center bg-black/50 p-6" onClick={onClose}>
|
|
60
|
+
<div
|
|
61
|
+
className="flex max-h-[80vh] w-full max-w-[480px] flex-col overflow-hidden rounded-brand-lg border border-border bg-surface shadow-elevated"
|
|
62
|
+
onClick={(e) => e.stopPropagation()}
|
|
63
|
+
>
|
|
64
|
+
<header className="flex items-center justify-between border-b border-border-subtle px-4 py-3">
|
|
65
|
+
<span className="text-[14px] font-semibold text-text">{models.length} models found</span>
|
|
66
|
+
<button onClick={onClose} className="text-text-subtle hover:text-text" aria-label="Close">
|
|
67
|
+
<Icon name="close" size={18} />
|
|
68
|
+
</button>
|
|
69
|
+
</header>
|
|
70
|
+
|
|
71
|
+
<div className="border-b border-border-subtle px-4 py-2.5">
|
|
72
|
+
<Input value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="filter…" autoFocus />
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* select-all row: an obvious clickable control, not an ambiguous button */}
|
|
76
|
+
{shown.length > 0 && (
|
|
77
|
+
<button
|
|
78
|
+
onClick={toggleAllShown}
|
|
79
|
+
className="flex items-center gap-2.5 border-b border-border-subtle px-4 py-2 text-left hover:bg-surface-2"
|
|
80
|
+
>
|
|
81
|
+
<Checkbox checked={allShownPicked} indeterminate={someShownPicked} onChange={toggleAllShown} ariaLabel="Select all" />
|
|
82
|
+
<span className="text-[12.5px] font-medium text-text">Select all{filter ? " (filtered)" : ""}</span>
|
|
83
|
+
<span className="ml-auto tnum text-[11px] text-text-subtle">
|
|
84
|
+
{shownPickedCount}/{shown.length}
|
|
85
|
+
</span>
|
|
86
|
+
</button>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
<div className="flex-1 overflow-y-auto px-2 py-2">
|
|
90
|
+
{shown.length === 0 ? (
|
|
91
|
+
<div className="px-2 py-6 text-center text-[13px] text-text-muted">No matches.</div>
|
|
92
|
+
) : (
|
|
93
|
+
shown.map((m) => {
|
|
94
|
+
const sel = picked.has(m.id);
|
|
95
|
+
return (
|
|
96
|
+
<button
|
|
97
|
+
key={m.id}
|
|
98
|
+
onClick={() => toggle(m.id)}
|
|
99
|
+
className={`flex w-full items-center gap-2.5 rounded-brand border px-2.5 py-2 text-left transition-colors ${
|
|
100
|
+
sel ? "border-accent/40 bg-accent-soft" : "border-transparent hover:bg-surface-2"
|
|
101
|
+
}`}
|
|
102
|
+
>
|
|
103
|
+
<Checkbox checked={sel} onChange={() => toggle(m.id)} ariaLabel={m.id} />
|
|
104
|
+
<span className="flex-1 truncate text-[12.5px] text-text">{m.id}</span>
|
|
105
|
+
{m.added && <span className="text-[11px] text-text-subtle">in catalog</span>}
|
|
106
|
+
</button>
|
|
107
|
+
);
|
|
108
|
+
})
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<footer className="flex items-center justify-between gap-3 border-t border-border-subtle bg-bg-alt px-4 py-2.5">
|
|
113
|
+
<span className="tnum text-[12px] text-text-muted">{picked.size} selected</span>
|
|
114
|
+
<div className="flex gap-2">
|
|
115
|
+
<Button variant="ghost" onClick={onClose}>
|
|
116
|
+
Cancel
|
|
117
|
+
</Button>
|
|
118
|
+
<Button disabled={picked.size === 0 || busy} onClick={() => onAdd([...picked])}>
|
|
119
|
+
{busy ? "Adding…" : `Add ${picked.size}`}
|
|
120
|
+
</Button>
|
|
121
|
+
</div>
|
|
122
|
+
</footer>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { RichCard, CardTitle } from "@/components/RichCard";
|
|
5
|
+
import { Button } from "@/components/Button";
|
|
6
|
+
import { Icon } from "@/components/Icon";
|
|
7
|
+
import { account } from "@/lib/client";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Change the admin password (used for the dashboard login and the gateway's
|
|
11
|
+
* /admin API). The gateway verifies the current password and stores the new one
|
|
12
|
+
* hashed; this browser's session is refreshed so you stay logged in.
|
|
13
|
+
*/
|
|
14
|
+
export function PasswordEditor() {
|
|
15
|
+
const [current, setCurrent] = useState("");
|
|
16
|
+
const [next, setNext] = useState("");
|
|
17
|
+
const [confirm, setConfirm] = useState("");
|
|
18
|
+
const [busy, setBusy] = useState(false);
|
|
19
|
+
const [msg, setMsg] = useState("");
|
|
20
|
+
const [err, setErr] = useState("");
|
|
21
|
+
|
|
22
|
+
async function save() {
|
|
23
|
+
setErr("");
|
|
24
|
+
setMsg("");
|
|
25
|
+
if (next !== confirm) {
|
|
26
|
+
setErr("new password and confirmation don't match");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (next.length < 4) {
|
|
30
|
+
setErr("new password must be at least 4 characters");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
setBusy(true);
|
|
34
|
+
const r = await account.changePassword(current, next);
|
|
35
|
+
setBusy(false);
|
|
36
|
+
if (!r.ok) {
|
|
37
|
+
setErr(r.error ?? "could not change password");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setCurrent("");
|
|
41
|
+
setNext("");
|
|
42
|
+
setConfirm("");
|
|
43
|
+
setMsg("Password changed ✓ — it's active now.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const field = "w-full max-w-[280px] rounded-brand border border-border bg-bg px-2.5 py-1.5 text-[13px] text-text focus:border-accent focus:outline-none";
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<RichCard header={<CardTitle title="Admin password" sub="for the dashboard login + the gateway admin API" />}>
|
|
50
|
+
<div className="space-y-2.5">
|
|
51
|
+
<Row label="Current">
|
|
52
|
+
<input type="password" value={current} onChange={(e) => setCurrent(e.target.value)} className={field} autoComplete="current-password" />
|
|
53
|
+
</Row>
|
|
54
|
+
<Row label="New">
|
|
55
|
+
<input type="password" value={next} onChange={(e) => setNext(e.target.value)} className={field} autoComplete="new-password" />
|
|
56
|
+
</Row>
|
|
57
|
+
<Row label="Confirm">
|
|
58
|
+
<input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} className={field} autoComplete="new-password" />
|
|
59
|
+
</Row>
|
|
60
|
+
<div className="flex items-center gap-2 pt-1">
|
|
61
|
+
<Button disabled={busy || !current || !next} onClick={save}>
|
|
62
|
+
{busy ? "Saving…" : "Change password"}
|
|
63
|
+
</Button>
|
|
64
|
+
{msg && (
|
|
65
|
+
<span className="flex items-center gap-1 text-[12px] text-success">
|
|
66
|
+
<Icon name="check" size={14} /> {msg}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
{err && <span className="text-[12px] text-danger">{err}</span>}
|
|
70
|
+
</div>
|
|
71
|
+
<p className="text-[11.5px] text-text-subtle">
|
|
72
|
+
The default is the one from <span className="tnum">AIGETWEY_ADMIN_PASSWORD</span> (or <span className="tnum">123456</span>). Changing it here is optional and takes effect immediately.
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
</RichCard>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function Row({ label, children }: { label: string; children: React.ReactNode }) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex items-center justify-between gap-3">
|
|
82
|
+
<span className="text-[12px] font-medium text-text-subtle">{label}</span>
|
|
83
|
+
{children}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|