@raingor/pi-web-switch 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +137 -0
- package/README.md +277 -0
- package/README.zh-CN.md +176 -0
- package/index.html +13 -0
- package/package.json +44 -0
- package/pi-package/index.ts +100 -0
- package/pi-package/skills/pi-web-switch/SKILL.md +60 -0
- package/public/pi.svg +4 -0
- package/server/pi-reader.ts +678 -0
- package/src/App.tsx +25 -0
- package/src/components/dashboard/DashboardPage.tsx +607 -0
- package/src/components/layout/AppShell.tsx +18 -0
- package/src/components/layout/Sidebar.tsx +116 -0
- package/src/components/models/ModelsPage.tsx +570 -0
- package/src/components/providers/ProvidersPage.tsx +466 -0
- package/src/components/sessions/MemoryPage.tsx +177 -0
- package/src/components/sessions/SessionsPage.tsx +347 -0
- package/src/components/settings/SettingsPage.tsx +351 -0
- package/src/components/ui/Badge.tsx +29 -0
- package/src/components/ui/EmptyState.tsx +20 -0
- package/src/components/ui/Modal.tsx +41 -0
- package/src/components/ui/StatCard.tsx +37 -0
- package/src/data/builtin-providers.ts +148 -0
- package/src/data/mock-config.ts +261 -0
- package/src/data/mock-usage.ts +153 -0
- package/src/index.css +217 -0
- package/src/lib/config.ts +56 -0
- package/src/lib/currency.ts +48 -0
- package/src/lib/i18n.tsx +98 -0
- package/src/lib/translations/en.ts +168 -0
- package/src/lib/translations/index.ts +14 -0
- package/src/lib/translations/ja.ts +158 -0
- package/src/lib/translations/zh-CN.ts +158 -0
- package/src/lib/translations/zh-TW.ts +158 -0
- package/src/lib/utils.ts +51 -0
- package/src/main.tsx +106 -0
- package/src/store/config-store.ts +459 -0
- package/src/types/index.ts +187 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +172 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { useState, useRef } from "react";
|
|
2
|
+
import { useConfigStore } from "@/store/config-store";
|
|
3
|
+
import { useTranslation } from "@/lib/i18n";
|
|
4
|
+
import { Badge } from "@/components/ui/Badge";
|
|
5
|
+
import { Modal } from "@/components/ui/Modal";
|
|
6
|
+
import { exportConfig, parseImportFile, saveLocalBackup } from "@/lib/config";
|
|
7
|
+
import type { PiConfig } from "@/types";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
import {
|
|
10
|
+
Settings,
|
|
11
|
+
Download,
|
|
12
|
+
Upload,
|
|
13
|
+
RotateCcw,
|
|
14
|
+
Sun,
|
|
15
|
+
Moon,
|
|
16
|
+
Monitor,
|
|
17
|
+
Package,
|
|
18
|
+
Plus,
|
|
19
|
+
Trash2,
|
|
20
|
+
CheckCircle2,
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
|
|
23
|
+
export function SettingsPage() {
|
|
24
|
+
const { t } = useTranslation();
|
|
25
|
+
const { settings, auth, modelsJson, allProviders, allModels, updateSettings, setTheme, addPackage, removePackage, importConfig: importConfigAction, resetToDefaults } = useConfigStore();
|
|
26
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
27
|
+
const [newPackage, setNewPackage] = useState("");
|
|
28
|
+
const [importError, setImportError] = useState("");
|
|
29
|
+
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
|
30
|
+
|
|
31
|
+
const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
32
|
+
const file = e.target.files?.[0];
|
|
33
|
+
if (!file) return;
|
|
34
|
+
const reader = new FileReader();
|
|
35
|
+
reader.onload = async (ev) => {
|
|
36
|
+
const result = parseImportFile(ev.target?.result as string);
|
|
37
|
+
if (result) {
|
|
38
|
+
await importConfigAction(result);
|
|
39
|
+
setImportError("");
|
|
40
|
+
} else {
|
|
41
|
+
setImportError("Invalid config file format");
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
reader.readAsText(file);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const buildConfigForExport = (): PiConfig => ({
|
|
48
|
+
settings: settings ?? { theme: "dark", packages: [], enabledModels: [] },
|
|
49
|
+
auth: auth ?? {},
|
|
50
|
+
modelsJson: modelsJson ?? { providers: {} },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const handleExport = () => {
|
|
54
|
+
const cfg = buildConfigForExport();
|
|
55
|
+
saveLocalBackup(cfg);
|
|
56
|
+
exportConfig(cfg);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const providerOptions = allProviders.map((p) => ({
|
|
60
|
+
value: p.id,
|
|
61
|
+
label: p.name,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const modelOptions = allModels.map((m) => ({
|
|
65
|
+
value: `${m.providerId}/${m.id}`,
|
|
66
|
+
label: `${m.providerName} / ${m.name || m.id}`,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="space-y-6">
|
|
71
|
+
<div>
|
|
72
|
+
<h1 className="text-2xl font-bold text-white">Settings</h1>
|
|
73
|
+
<p className="mt-1 text-sm text-gray-400">Configure pi agent preferences</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Default Provider/Model */}
|
|
77
|
+
<section className="rounded-xl border border-gray-800 bg-gray-900/50">
|
|
78
|
+
<div className="border-b border-gray-800 px-6 py-4">
|
|
79
|
+
<h2 className="text-sm font-semibold text-gray-300">Defaults</h2>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="space-y-4 p-6">
|
|
82
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
83
|
+
<div>
|
|
84
|
+
<label className="block text-xs font-medium text-gray-400">Default Provider</label>
|
|
85
|
+
<select
|
|
86
|
+
value={settings?.defaultProvider ?? ""}
|
|
87
|
+
onChange={(e) => updateSettings({ defaultProvider: e.target.value })}
|
|
88
|
+
className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white"
|
|
89
|
+
>
|
|
90
|
+
<option value="">— None —</option>
|
|
91
|
+
{providerOptions.map((o) => (
|
|
92
|
+
<option key={o.value} value={o.value}>{o.label}</option>
|
|
93
|
+
))}
|
|
94
|
+
</select>
|
|
95
|
+
</div>
|
|
96
|
+
<div>
|
|
97
|
+
<label className="block text-xs font-medium text-gray-400">Default Model</label>
|
|
98
|
+
<select
|
|
99
|
+
value={settings?.defaultModel ?? ""}
|
|
100
|
+
onChange={(e) => updateSettings({ defaultModel: e.target.value })}
|
|
101
|
+
className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white"
|
|
102
|
+
>
|
|
103
|
+
<option value="">— None —</option>
|
|
104
|
+
{modelOptions.map((o) => (
|
|
105
|
+
<option key={o.value} value={o.value}>{o.label}</option>
|
|
106
|
+
))}
|
|
107
|
+
</select>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
111
|
+
<div>
|
|
112
|
+
<label className="block text-xs font-medium text-gray-400">Default Thinking Level</label>
|
|
113
|
+
<select
|
|
114
|
+
value={settings?.defaultThinkingLevel ?? "medium"}
|
|
115
|
+
onChange={(e) => updateSettings({ defaultThinkingLevel: e.target.value })}
|
|
116
|
+
className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white"
|
|
117
|
+
>
|
|
118
|
+
{["off", "minimal", "low", "medium", "high", "xhigh"].map((l) => (
|
|
119
|
+
<option key={l} value={l}>{l.charAt(0).toUpperCase() + l.slice(1)}</option>
|
|
120
|
+
))}
|
|
121
|
+
</select>
|
|
122
|
+
</div>
|
|
123
|
+
<div>
|
|
124
|
+
<label className="block text-xs font-medium text-gray-400">Project Trust</label>
|
|
125
|
+
<select
|
|
126
|
+
value={settings?.defaultProjectTrust ?? "prompt"}
|
|
127
|
+
onChange={(e) => updateSettings({ defaultProjectTrust: e.target.value })}
|
|
128
|
+
className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white"
|
|
129
|
+
>
|
|
130
|
+
{["prompt", "always", "never"].map((t) => (
|
|
131
|
+
<option key={t} value={t}>{t.charAt(0).toUpperCase() + t.slice(1)}</option>
|
|
132
|
+
))}
|
|
133
|
+
</select>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</section>
|
|
138
|
+
|
|
139
|
+
{/* Theme */}
|
|
140
|
+
<section className="rounded-xl border border-gray-800 bg-gray-900/50">
|
|
141
|
+
<div className="border-b border-gray-800 px-6 py-4">
|
|
142
|
+
<h2 className="text-sm font-semibold text-gray-300">Appearance</h2>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="p-6">
|
|
145
|
+
<div className="flex gap-3">
|
|
146
|
+
{[
|
|
147
|
+
{ value: "light" as const, icon: Sun, label: "Light" },
|
|
148
|
+
{ value: "dark" as const, icon: Moon, label: "Dark" },
|
|
149
|
+
{ value: "light/dark" as const, icon: Monitor, label: "System" },
|
|
150
|
+
].map(({ value, icon: Icon, label }) => {
|
|
151
|
+
const active = (settings?.theme ?? "light/dark") === value;
|
|
152
|
+
return (
|
|
153
|
+
<button
|
|
154
|
+
key={value}
|
|
155
|
+
onClick={() => setTheme(value)}
|
|
156
|
+
className={cn(
|
|
157
|
+
"flex items-center gap-2 rounded-lg border px-4 py-3 text-sm transition-all",
|
|
158
|
+
active
|
|
159
|
+
? "border-blue-500 bg-blue-500/10 text-blue-400"
|
|
160
|
+
: "border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600"
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
163
|
+
<Icon className="h-4 w-4" />
|
|
164
|
+
{label}
|
|
165
|
+
{active && <CheckCircle2 className="h-3.5 w-3.5 ml-1" />}
|
|
166
|
+
</button>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
169
|
+
</div>
|
|
170
|
+
<div className="mt-4 flex items-center gap-3">
|
|
171
|
+
<label className="flex items-center gap-2 text-sm text-gray-400">
|
|
172
|
+
<input
|
|
173
|
+
type="checkbox"
|
|
174
|
+
checked={settings?.hideThinkingBlock ?? false}
|
|
175
|
+
onChange={(e) => updateSettings({ hideThinkingBlock: e.target.checked })}
|
|
176
|
+
className="rounded border-gray-600 bg-gray-800 text-blue-500"
|
|
177
|
+
/>
|
|
178
|
+
Hide thinking blocks
|
|
179
|
+
</label>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
|
|
184
|
+
{/* Enabled Models */}
|
|
185
|
+
<section className="rounded-xl border border-gray-800 bg-gray-900/50">
|
|
186
|
+
<div className="border-b border-gray-800 px-6 py-4">
|
|
187
|
+
<h2 className="text-sm font-semibold text-gray-300">Enabled Models</h2>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="p-6">
|
|
190
|
+
<div className="space-y-2">
|
|
191
|
+
{(settings?.enabledModels ?? []).length === 0 ? (
|
|
192
|
+
<p className="text-sm text-gray-500">No models enabled. Enable models from the Models page.</p>
|
|
193
|
+
) : (
|
|
194
|
+
(settings?.enabledModels ?? []).map((ref) => (
|
|
195
|
+
<div key={ref} className="flex items-center justify-between rounded-lg border border-gray-800 bg-gray-800/30 px-4 py-2.5">
|
|
196
|
+
<div className="flex items-center gap-2">
|
|
197
|
+
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
|
198
|
+
<span className="text-sm text-gray-200">{ref}</span>
|
|
199
|
+
</div>
|
|
200
|
+
<button
|
|
201
|
+
onClick={() => useConfigStore.getState().removeEnabledModel(ref)}
|
|
202
|
+
className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-red-400"
|
|
203
|
+
>
|
|
204
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
))
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</section>
|
|
212
|
+
|
|
213
|
+
{/* Packages */}
|
|
214
|
+
<section className="rounded-xl border border-gray-800 bg-gray-900/50">
|
|
215
|
+
<div className="border-b border-gray-800 px-6 py-4">
|
|
216
|
+
<h2 className="text-sm font-semibold text-gray-300">Extensions & Packages</h2>
|
|
217
|
+
</div>
|
|
218
|
+
<div className="p-6 space-y-4">
|
|
219
|
+
<div className="flex gap-2">
|
|
220
|
+
<input
|
|
221
|
+
type="text"
|
|
222
|
+
value={newPackage}
|
|
223
|
+
onChange={(e) => setNewPackage(e.target.value)}
|
|
224
|
+
placeholder="npm:package-name"
|
|
225
|
+
className="flex-1 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-white placeholder-gray-500"
|
|
226
|
+
onKeyDown={(e) => {
|
|
227
|
+
if (e.key === "Enter" && newPackage) {
|
|
228
|
+
addPackage(newPackage);
|
|
229
|
+
setNewPackage("");
|
|
230
|
+
}
|
|
231
|
+
}}
|
|
232
|
+
/>
|
|
233
|
+
<button
|
|
234
|
+
onClick={() => {
|
|
235
|
+
if (newPackage) {
|
|
236
|
+
addPackage(newPackage);
|
|
237
|
+
setNewPackage("");
|
|
238
|
+
}
|
|
239
|
+
}}
|
|
240
|
+
className="flex items-center gap-1 rounded-lg bg-blue-600 px-3 py-2 text-sm text-white hover:bg-blue-500"
|
|
241
|
+
>
|
|
242
|
+
<Plus className="h-4 w-4" />
|
|
243
|
+
Add
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="space-y-2">
|
|
247
|
+
{(settings?.packages ?? []).length === 0 ? (
|
|
248
|
+
<p className="text-sm text-gray-500">No packages installed</p>
|
|
249
|
+
) : (
|
|
250
|
+
(settings?.packages ?? []).map((pkg) => (
|
|
251
|
+
<div key={pkg} className="flex items-center justify-between rounded-lg border border-gray-800 bg-gray-800/30 px-4 py-2.5">
|
|
252
|
+
<div className="flex items-center gap-2">
|
|
253
|
+
<Package className="h-4 w-4 text-blue-400" />
|
|
254
|
+
<span className="text-sm text-gray-200">{pkg}</span>
|
|
255
|
+
</div>
|
|
256
|
+
<button
|
|
257
|
+
onClick={() => removePackage(pkg)}
|
|
258
|
+
className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-red-400"
|
|
259
|
+
>
|
|
260
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
261
|
+
</button>
|
|
262
|
+
</div>
|
|
263
|
+
))
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</section>
|
|
268
|
+
|
|
269
|
+
{/* Import/Export */}
|
|
270
|
+
<section className="rounded-xl border border-gray-800 bg-gray-900/50">
|
|
271
|
+
<div className="border-b border-gray-800 px-6 py-4">
|
|
272
|
+
<h2 className="text-sm font-semibold text-gray-300">Import & Export</h2>
|
|
273
|
+
</div>
|
|
274
|
+
<div className="p-6 space-y-4">
|
|
275
|
+
<div className="flex flex-wrap gap-3">
|
|
276
|
+
<button
|
|
277
|
+
onClick={handleExport}
|
|
278
|
+
className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm text-gray-300 hover:bg-gray-700"
|
|
279
|
+
>
|
|
280
|
+
<Download className="h-4 w-4" />
|
|
281
|
+
Export Config
|
|
282
|
+
</button>
|
|
283
|
+
<button
|
|
284
|
+
onClick={() => fileInputRef.current?.click()}
|
|
285
|
+
className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm text-gray-300 hover:bg-gray-700"
|
|
286
|
+
>
|
|
287
|
+
<Upload className="h-4 w-4" />
|
|
288
|
+
Import Config
|
|
289
|
+
</button>
|
|
290
|
+
<input
|
|
291
|
+
ref={fileInputRef}
|
|
292
|
+
type="file"
|
|
293
|
+
accept=".json"
|
|
294
|
+
className="hidden"
|
|
295
|
+
onChange={handleImport}
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
{importError && <p className="text-sm text-red-400">{importError}</p>}
|
|
299
|
+
</div>
|
|
300
|
+
</section>
|
|
301
|
+
|
|
302
|
+
{/* Danger Zone */}
|
|
303
|
+
<section className="rounded-xl border border-red-900/30 bg-red-950/20">
|
|
304
|
+
<div className="border-b border-red-900/30 px-6 py-4">
|
|
305
|
+
<h2 className="text-sm font-semibold text-red-400">Danger Zone</h2>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="p-6">
|
|
308
|
+
<button
|
|
309
|
+
onClick={() => setShowResetConfirm(true)}
|
|
310
|
+
className="flex items-center gap-2 rounded-lg border border-red-700 bg-red-900/30 px-4 py-2 text-sm text-red-400 hover:bg-red-900/50"
|
|
311
|
+
>
|
|
312
|
+
<RotateCcw className="h-4 w-4" />
|
|
313
|
+
Reset to Defaults
|
|
314
|
+
</button>
|
|
315
|
+
</div>
|
|
316
|
+
</section>
|
|
317
|
+
|
|
318
|
+
{/* Reset Confirm */}
|
|
319
|
+
<Modal
|
|
320
|
+
open={showResetConfirm}
|
|
321
|
+
onClose={() => setShowResetConfirm(false)}
|
|
322
|
+
title="Reset Configuration?"
|
|
323
|
+
>
|
|
324
|
+
<div className="space-y-4">
|
|
325
|
+
<p className="text-sm text-gray-400">
|
|
326
|
+
This will reset all settings, auth keys, and custom providers to defaults.
|
|
327
|
+
Changes are written directly to your pi config files (~/.pi/agent/).
|
|
328
|
+
This action cannot be undone.
|
|
329
|
+
</p>
|
|
330
|
+
<div className="flex justify-end gap-3">
|
|
331
|
+
<button
|
|
332
|
+
onClick={() => setShowResetConfirm(false)}
|
|
333
|
+
className="rounded-lg px-4 py-2 text-sm text-gray-400 hover:bg-gray-800"
|
|
334
|
+
>
|
|
335
|
+
Cancel
|
|
336
|
+
</button>
|
|
337
|
+
<button
|
|
338
|
+
onClick={() => {
|
|
339
|
+
resetToDefaults();
|
|
340
|
+
setShowResetConfirm(false);
|
|
341
|
+
}}
|
|
342
|
+
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-500"
|
|
343
|
+
>
|
|
344
|
+
Reset
|
|
345
|
+
</button>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</Modal>
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
interface BadgeProps {
|
|
4
|
+
children: string;
|
|
5
|
+
variant?: "default" | "success" | "warning" | "error" | "info";
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const variants = {
|
|
10
|
+
default: "bg-gray-800 text-gray-300",
|
|
11
|
+
success: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
|
|
12
|
+
warning: "bg-amber-500/10 text-amber-400 border-amber-500/20",
|
|
13
|
+
error: "bg-red-500/10 text-red-400 border-red-500/20",
|
|
14
|
+
info: "bg-blue-500/10 text-blue-400 border-blue-500/20",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function Badge({ children, variant = "default", className }: BadgeProps) {
|
|
18
|
+
return (
|
|
19
|
+
<span
|
|
20
|
+
className={cn(
|
|
21
|
+
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border",
|
|
22
|
+
variants[variant],
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</span>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
interface EmptyStateProps {
|
|
4
|
+
icon: React.ReactNode;
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
action?: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className={cn("flex flex-col items-center justify-center rounded-xl border border-dashed border-gray-700 p-12 text-center", className)}>
|
|
14
|
+
<div className="mb-4 text-gray-500">{icon}</div>
|
|
15
|
+
<h3 className="text-lg font-medium text-gray-300">{title}</h3>
|
|
16
|
+
{description && <p className="mt-1 text-sm text-gray-500">{description}</p>}
|
|
17
|
+
{action && <div className="mt-4">{action}</div>}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
interface ModalProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
title: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
size?: "sm" | "md" | "lg";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Modal({ open, onClose, title, children, size = "md" }: ModalProps) {
|
|
12
|
+
if (!open) return null;
|
|
13
|
+
|
|
14
|
+
const sizeClasses = {
|
|
15
|
+
sm: "max-w-md",
|
|
16
|
+
md: "max-w-lg",
|
|
17
|
+
lg: "max-w-2xl",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
22
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
|
23
|
+
<div
|
|
24
|
+
className={`relative w-full ${sizeClasses[size]} mx-4 rounded-xl border border-gray-700 bg-gray-900 shadow-2xl`}
|
|
25
|
+
>
|
|
26
|
+
<div className="flex items-center justify-between border-b border-gray-800 px-6 py-4">
|
|
27
|
+
<h2 className="text-lg font-semibold text-white">{title}</h2>
|
|
28
|
+
<button
|
|
29
|
+
onClick={onClose}
|
|
30
|
+
className="rounded-lg p-1 text-gray-400 hover:bg-gray-800 hover:text-white"
|
|
31
|
+
>
|
|
32
|
+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
33
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
34
|
+
</svg>
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="px-6 py-4">{children}</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
interface StatCardProps {
|
|
5
|
+
title: string;
|
|
6
|
+
value: string;
|
|
7
|
+
subtitle?: string;
|
|
8
|
+
icon: ReactNode;
|
|
9
|
+
trend?: { value: string; positive: boolean };
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function StatCard({ title, value, subtitle, icon, trend, className }: StatCardProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className={cn("rounded-xl border border-gray-800 bg-gray-900/50 p-5 backdrop-blur-sm", className)}>
|
|
16
|
+
<div className="flex items-start justify-between">
|
|
17
|
+
<div className="space-y-1">
|
|
18
|
+
<p className="text-sm font-medium text-gray-400">{title}</p>
|
|
19
|
+
<p className="text-2xl font-bold tracking-tight text-white">{value}</p>
|
|
20
|
+
{subtitle && <p className="text-xs text-gray-500">{subtitle}</p>}
|
|
21
|
+
{trend && (
|
|
22
|
+
<span
|
|
23
|
+
className={cn(
|
|
24
|
+
"inline-flex items-center gap-1 text-xs font-medium",
|
|
25
|
+
trend.positive ? "text-emerald-400" : "text-red-400"
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
<span>{trend.positive ? "↑" : "↓"}</span>
|
|
29
|
+
{trend.value}
|
|
30
|
+
</span>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
<div className="rounded-lg bg-blue-500/10 p-2.5 text-blue-400">{icon}</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// ─── Built-in Providers ───────────────────────────────────
|
|
2
|
+
// These are hardcoded in pi's Rust source and not exposed as config files.
|
|
3
|
+
// We maintain this list to display known providers in the UI.
|
|
4
|
+
|
|
5
|
+
import type { Provider } from "@/types";
|
|
6
|
+
|
|
7
|
+
export const BUILTIN_PROVIDERS: Provider[] = [
|
|
8
|
+
{
|
|
9
|
+
id: "anthropic",
|
|
10
|
+
name: "Anthropic",
|
|
11
|
+
type: "builtin",
|
|
12
|
+
api: "anthropic-messages",
|
|
13
|
+
hasAuth: true,
|
|
14
|
+
authMethod: "env",
|
|
15
|
+
models: [
|
|
16
|
+
{ id: "claude-sonnet-4", name: "Claude 4 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: true },
|
|
17
|
+
{ id: "claude-sonnet-4-5", name: "Claude 4.5 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: true },
|
|
18
|
+
{ id: "claude-opus-4", name: "Claude 4 Opus", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, enabled: false },
|
|
19
|
+
{ id: "claude-haiku-3-5", name: "Claude 3.5 Haiku", reasoning: false, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 }, enabled: true },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "openai",
|
|
24
|
+
name: "OpenAI",
|
|
25
|
+
type: "builtin",
|
|
26
|
+
api: "openai-completions",
|
|
27
|
+
hasAuth: true,
|
|
28
|
+
authMethod: "env",
|
|
29
|
+
models: [
|
|
30
|
+
{ id: "gpt-4o", name: "GPT-4o", reasoning: false, input: ["text", "image"], contextWindow: 128000, maxTokens: 16384, cost: { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 3.75 }, enabled: true },
|
|
31
|
+
{ id: "gpt-4o-mini", name: "GPT-4o Mini", reasoning: false, input: ["text", "image"], contextWindow: 128000, maxTokens: 16384, cost: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.225 }, enabled: true },
|
|
32
|
+
{ id: "gpt-5.1", name: "GPT-5.1", reasoning: true, input: ["text", "image"], contextWindow: 256000, maxTokens: 65536, cost: { input: 10, output: 40, cacheRead: 5, cacheWrite: 10 }, enabled: false },
|
|
33
|
+
{ id: "o3-mini", name: "o3-mini", reasoning: true, input: ["text"], contextWindow: 200000, maxTokens: 100000, cost: { input: 1.1, output: 4.4, cacheRead: 0.55, cacheWrite: 1.65 }, enabled: false },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "deepseek",
|
|
38
|
+
name: "DeepSeek",
|
|
39
|
+
type: "builtin",
|
|
40
|
+
api: "openai-completions",
|
|
41
|
+
hasAuth: true,
|
|
42
|
+
authMethod: "env",
|
|
43
|
+
models: [
|
|
44
|
+
{ id: "deepseek-chat", name: "DeepSeek V3", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.27, output: 1.1, cacheRead: 0.07, cacheWrite: 0.27 }, enabled: true },
|
|
45
|
+
{ id: "deepseek-reasoner", name: "DeepSeek R1", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 }, enabled: true },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "opencode",
|
|
50
|
+
name: "OpenCode",
|
|
51
|
+
type: "builtin",
|
|
52
|
+
api: "openai-completions",
|
|
53
|
+
hasAuth: true,
|
|
54
|
+
authMethod: "file",
|
|
55
|
+
models: [
|
|
56
|
+
{ id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash (Free)", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, enabled: true },
|
|
57
|
+
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.3, output: 0.6, cacheRead: 0.15, cacheWrite: 0.3 }, enabled: true },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "opencode-go",
|
|
62
|
+
name: "OpenCode Go",
|
|
63
|
+
type: "builtin",
|
|
64
|
+
api: "openai-completions",
|
|
65
|
+
hasAuth: true,
|
|
66
|
+
authMethod: "file",
|
|
67
|
+
models: [
|
|
68
|
+
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.3, output: 0.6, cacheRead: 0.15, cacheWrite: 0.3 }, enabled: true },
|
|
69
|
+
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 2, output: 8, cacheRead: 1, cacheWrite: 2 }, enabled: true },
|
|
70
|
+
{ id: "glm-5.1", name: "GLM 5.1", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.5, output: 2, cacheRead: 0.25, cacheWrite: 0.5 }, enabled: true },
|
|
71
|
+
{ id: "qwen3.7-max", name: "Qwen 3.7 Max", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 1.5, output: 6, cacheRead: 0.75, cacheWrite: 1.5 }, enabled: true },
|
|
72
|
+
{ id: "mimo-v2.5", name: "MiMo V2.5", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 1.2, output: 4.8, cacheRead: 0.6, cacheWrite: 1.2 }, enabled: true },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "sensenova",
|
|
77
|
+
name: "SenseNova",
|
|
78
|
+
type: "builtin",
|
|
79
|
+
api: "openai-completions",
|
|
80
|
+
hasAuth: true,
|
|
81
|
+
authMethod: "env",
|
|
82
|
+
models: [
|
|
83
|
+
{ id: "glm-5.2", name: "GLM 5.2", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.8, output: 2.4, cacheRead: 0.4, cacheWrite: 0.8 }, enabled: true },
|
|
84
|
+
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.3, output: 0.6, cacheRead: 0.15, cacheWrite: 0.3 }, enabled: true },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "google",
|
|
89
|
+
name: "Google Gemini",
|
|
90
|
+
type: "builtin",
|
|
91
|
+
api: "google-generative-ai",
|
|
92
|
+
hasAuth: true,
|
|
93
|
+
authMethod: "env",
|
|
94
|
+
models: [
|
|
95
|
+
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", reasoning: false, input: ["text", "image"], contextWindow: 1048576, maxTokens: 8192, cost: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.15 }, enabled: true },
|
|
96
|
+
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 8192, cost: { input: 1.25, output: 10, cacheRead: 0.625, cacheWrite: 1.25 }, enabled: false },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "openrouter",
|
|
101
|
+
name: "OpenRouter",
|
|
102
|
+
type: "builtin",
|
|
103
|
+
api: "openai-completions",
|
|
104
|
+
hasAuth: false,
|
|
105
|
+
authMethod: "none",
|
|
106
|
+
models: [
|
|
107
|
+
{ id: "openrouter/anthropic/claude-sonnet-4", name: "Claude 4 Sonnet (OpenRouter)", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: false },
|
|
108
|
+
{ id: "openrouter/deepseek/deepseek-r1", name: "DeepSeek R1 (OpenRouter)", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 }, enabled: false },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "mistral",
|
|
113
|
+
name: "Mistral",
|
|
114
|
+
type: "builtin",
|
|
115
|
+
api: "mistral-conversations",
|
|
116
|
+
hasAuth: false,
|
|
117
|
+
authMethod: "none",
|
|
118
|
+
models: [
|
|
119
|
+
{ id: "mistral-large", name: "Mistral Large", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 2, output: 6, cacheRead: 1, cacheWrite: 2 }, enabled: false },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "github-copilot",
|
|
124
|
+
name: "GitHub Copilot",
|
|
125
|
+
type: "builtin",
|
|
126
|
+
api: "openai-completions",
|
|
127
|
+
hasAuth: false,
|
|
128
|
+
authMethod: "none",
|
|
129
|
+
models: [
|
|
130
|
+
{ id: "copilot-gpt-4o", name: "Copilot GPT-4o", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 4096, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, enabled: false },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "groq",
|
|
135
|
+
name: "Groq",
|
|
136
|
+
type: "builtin",
|
|
137
|
+
api: "openai-completions",
|
|
138
|
+
hasAuth: false,
|
|
139
|
+
authMethod: "none",
|
|
140
|
+
models: [
|
|
141
|
+
{ id: "llama-3.3-70b", name: "Llama 3.3 70B", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.59, output: 0.79, cacheRead: 0, cacheWrite: 0 }, enabled: false },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
export function getBuiltinProviders(): Provider[] {
|
|
147
|
+
return BUILTIN_PROVIDERS;
|
|
148
|
+
}
|