@raingor/pi-web-switch 0.3.1 → 0.3.2
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.md +0 -1
- package/README.zh-CN.md +0 -1
- package/package.json +1 -1
- package/server/pi-reader.ts +403 -4
- package/src/App.tsx +3 -0
- package/src/components/dashboard/DashboardPage.tsx +275 -117
- package/src/components/layout/Sidebar.tsx +2 -0
- package/src/components/providers/ProvidersModelsPage.tsx +1387 -0
- package/src/components/sessions/MemoryPage.tsx +480 -85
- package/src/components/sessions/SessionsPage.tsx +473 -89
- package/src/components/settings/SettingsPage.tsx +555 -243
- package/src/data/builtin-providers.ts +0 -12
- package/src/data/mock-config.ts +1 -15
- package/src/data/mock-usage.ts +0 -2
- package/src/lib/translations/en.ts +114 -23
- package/src/lib/translations/ja.ts +114 -23
- package/src/lib/translations/zh-CN.ts +114 -23
- package/src/lib/translations/zh-TW.ts +114 -23
- package/src/main.tsx +9 -0
- package/src/store/config-store.ts +18 -12
- package/src/types/index.ts +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +115 -3
- package/src/components/models/ModelsPage.tsx +0 -570
- package/src/components/providers/ProvidersPage.tsx +0 -466
|
@@ -1,32 +1,161 @@
|
|
|
1
|
-
import { useState, useRef } from "react";
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
2
|
import { useConfigStore } from "@/store/config-store";
|
|
3
3
|
import { useTranslation } from "@/lib/i18n";
|
|
4
|
-
import { Badge } from "@/components/ui/Badge";
|
|
5
4
|
import { Modal } from "@/components/ui/Modal";
|
|
6
5
|
import { exportConfig, parseImportFile, saveLocalBackup } from "@/lib/config";
|
|
7
|
-
import type { PiConfig } from "@/types";
|
|
6
|
+
import type { PiConfig, UpdateCheckResult } from "@/types";
|
|
8
7
|
import { cn } from "@/lib/utils";
|
|
9
8
|
import {
|
|
10
|
-
Settings,
|
|
11
9
|
Download,
|
|
12
10
|
Upload,
|
|
13
11
|
RotateCcw,
|
|
14
|
-
Sun,
|
|
15
|
-
Moon,
|
|
16
|
-
Monitor,
|
|
17
12
|
Package,
|
|
18
13
|
Plus,
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
X,
|
|
15
|
+
Check,
|
|
16
|
+
Palette,
|
|
17
|
+
Type,
|
|
18
|
+
LayoutGrid,
|
|
19
|
+
Wrench,
|
|
20
|
+
Settings as SettingsIcon,
|
|
21
|
+
Zap,
|
|
22
|
+
CloudDownload,
|
|
23
|
+
RefreshCw,
|
|
21
24
|
} from "lucide-react";
|
|
22
25
|
|
|
26
|
+
type SettingsTab = "appearance" | "models" | "advanced";
|
|
27
|
+
|
|
28
|
+
// Visual palette previews for the theme swatch picker (mirrors pi-desktop).
|
|
29
|
+
const THEME_SWATCHES: {
|
|
30
|
+
value: "dark" | "light" | "light/dark";
|
|
31
|
+
labelKey: string;
|
|
32
|
+
bg: string;
|
|
33
|
+
dots: string[];
|
|
34
|
+
}[] = [
|
|
35
|
+
{ value: "dark", labelKey: "settings.dark", bg: "linear-gradient(145deg, #14141c, #0a0a0f)", dots: ["#00d4aa", "#7c5cfc", "#2a2a35"] },
|
|
36
|
+
{ value: "light", labelKey: "settings.light", bg: "linear-gradient(145deg, #ffffff, #e9ecf2)", dots: ["#00b894", "#7c5cfc", "#c9ced8"] },
|
|
37
|
+
{ value: "light/dark", labelKey: "settings.system", bg: "linear-gradient(105deg, #0a0a0f 49.5%, #f1f2f6 50.5%)", dots: ["#00d4aa", "#7c5cfc", "#9aa0ab"] },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const FONT_SIZE_KEY = "pi-font-size";
|
|
41
|
+
|
|
42
|
+
function Card({
|
|
43
|
+
icon: Icon,
|
|
44
|
+
title,
|
|
45
|
+
desc,
|
|
46
|
+
children,
|
|
47
|
+
}: {
|
|
48
|
+
icon: typeof Palette;
|
|
49
|
+
title: string;
|
|
50
|
+
desc?: string;
|
|
51
|
+
children: React.ReactNode;
|
|
52
|
+
}) {
|
|
53
|
+
return (
|
|
54
|
+
<section className="rounded-xl border border-gray-800 bg-gray-900/50">
|
|
55
|
+
<div className="flex items-center gap-3 border-b border-gray-800 px-6 py-4">
|
|
56
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg border border-gray-700 bg-gray-800">
|
|
57
|
+
<Icon className="h-4 w-4 text-blue-400" />
|
|
58
|
+
</div>
|
|
59
|
+
<div>
|
|
60
|
+
<h2 className="text-sm font-semibold text-gray-300">{title}</h2>
|
|
61
|
+
{desc && <p className="text-xs text-gray-500">{desc}</p>}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="p-6">{children}</div>
|
|
65
|
+
</section>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function SettingRow({ label, children }: { label: string; children: React.ReactNode }) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex items-center justify-between gap-4 border-b border-gray-800/60 py-3 last:border-b-0 last:pb-0 first:pt-0">
|
|
72
|
+
<span className="text-sm text-gray-400">{label}</span>
|
|
73
|
+
{children}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
23
78
|
export function SettingsPage() {
|
|
24
79
|
const { t } = useTranslation();
|
|
25
|
-
const {
|
|
80
|
+
const {
|
|
81
|
+
settings,
|
|
82
|
+
auth,
|
|
83
|
+
modelsJson,
|
|
84
|
+
allProviders,
|
|
85
|
+
allModels,
|
|
86
|
+
updateSettings,
|
|
87
|
+
setTheme,
|
|
88
|
+
addEnabledModel,
|
|
89
|
+
removeEnabledModel,
|
|
90
|
+
addPackage,
|
|
91
|
+
removePackage,
|
|
92
|
+
importConfig: importConfigAction,
|
|
93
|
+
resetToDefaults,
|
|
94
|
+
} = useConfigStore();
|
|
26
95
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
96
|
+
const [activeTab, setActiveTab] = useState<SettingsTab>("appearance");
|
|
27
97
|
const [newPackage, setNewPackage] = useState("");
|
|
28
98
|
const [importError, setImportError] = useState("");
|
|
29
99
|
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
|
100
|
+
const [fontSize, setFontSize] = useState(() => {
|
|
101
|
+
const saved = Number(localStorage.getItem(FONT_SIZE_KEY));
|
|
102
|
+
return saved >= 12 && saved <= 24 ? saved : 16;
|
|
103
|
+
});
|
|
104
|
+
// pi core / extensions update check (Advanced tab).
|
|
105
|
+
const [updateResult, setUpdateResult] = useState<UpdateCheckResult | null>(null);
|
|
106
|
+
const [checkingUpdates, setCheckingUpdates] = useState(false);
|
|
107
|
+
const [updateError, setUpdateError] = useState(false);
|
|
108
|
+
const [applying, setApplying] = useState(false);
|
|
109
|
+
const [applyMessage, setApplyMessage] = useState<{ ok: number; failNames: string[] } | null>(null);
|
|
110
|
+
|
|
111
|
+
const handleCheckUpdates = async () => {
|
|
112
|
+
setCheckingUpdates(true);
|
|
113
|
+
setUpdateError(false);
|
|
114
|
+
setApplyMessage(null);
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch("/api/pi/check-updates");
|
|
117
|
+
if (!res.ok) throw new Error("check failed");
|
|
118
|
+
setUpdateResult(await res.json());
|
|
119
|
+
} catch {
|
|
120
|
+
setUpdateError(true);
|
|
121
|
+
} finally {
|
|
122
|
+
setCheckingUpdates(false);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// One-click update: npm install <name>@latest for every updatable extension,
|
|
127
|
+
// then re-check so the list reflects the new installed versions.
|
|
128
|
+
const handleApplyUpdates = async () => {
|
|
129
|
+
const names = (updateResult?.extensions ?? []).filter((e) => e.hasUpdate).map((e) => e.name);
|
|
130
|
+
if (names.length === 0) return;
|
|
131
|
+
setApplying(true);
|
|
132
|
+
setApplyMessage(null);
|
|
133
|
+
try {
|
|
134
|
+
const res = await fetch("/api/pi/apply-updates", {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: { "Content-Type": "application/json" },
|
|
137
|
+
body: JSON.stringify({ names }),
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok) throw new Error("apply failed");
|
|
140
|
+
const { results } = (await res.json()) as {
|
|
141
|
+
results: { name: string; success: boolean; message?: string }[];
|
|
142
|
+
};
|
|
143
|
+
const failNames = results.filter((r) => !r.success).map((r) => r.name);
|
|
144
|
+
setApplyMessage({ ok: results.length - failNames.length, failNames });
|
|
145
|
+
const check = await fetch("/api/pi/check-updates");
|
|
146
|
+
if (check.ok) setUpdateResult(await check.json());
|
|
147
|
+
} catch {
|
|
148
|
+
setUpdateError(true);
|
|
149
|
+
} finally {
|
|
150
|
+
setApplying(false);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Apply + persist UI font size (root rem scaling).
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
document.documentElement.style.fontSize = `${fontSize}px`;
|
|
157
|
+
localStorage.setItem(FONT_SIZE_KEY, String(fontSize));
|
|
158
|
+
}, [fontSize]);
|
|
30
159
|
|
|
31
160
|
const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
32
161
|
const file = e.target.files?.[0];
|
|
@@ -38,301 +167,484 @@ export function SettingsPage() {
|
|
|
38
167
|
await importConfigAction(result);
|
|
39
168
|
setImportError("");
|
|
40
169
|
} else {
|
|
41
|
-
setImportError("
|
|
170
|
+
setImportError(t("settings.import_error"));
|
|
42
171
|
}
|
|
43
172
|
};
|
|
44
173
|
reader.readAsText(file);
|
|
45
174
|
};
|
|
46
175
|
|
|
47
|
-
const buildConfigForExport = (): PiConfig => ({
|
|
48
|
-
settings: settings ?? { theme: "dark", packages: [], enabledModels: [] },
|
|
49
|
-
auth: auth ?? {},
|
|
50
|
-
modelsJson: modelsJson ?? { providers: {} },
|
|
51
|
-
});
|
|
52
|
-
|
|
53
176
|
const handleExport = () => {
|
|
54
|
-
const cfg =
|
|
177
|
+
const cfg: PiConfig = {
|
|
178
|
+
settings: settings ?? { theme: "dark", packages: [], enabledModels: [] },
|
|
179
|
+
auth: auth ?? {},
|
|
180
|
+
modelsJson: modelsJson ?? { providers: {} },
|
|
181
|
+
};
|
|
55
182
|
saveLocalBackup(cfg);
|
|
56
183
|
exportConfig(cfg);
|
|
57
184
|
};
|
|
58
185
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
186
|
+
// ── Default model select: composite `providerId/modelId` values, but the
|
|
187
|
+
// settings file stores the bare model id (that's what pi expects on disk).
|
|
188
|
+
const modelValue = (providerId: string, modelId: string) => `${providerId}/${modelId}`;
|
|
189
|
+
const modelProviders = allProviders.filter((p) => p.models.length > 0);
|
|
190
|
+
const scopedId = settings?.defaultProvider;
|
|
191
|
+
const groupedModelOptions = scopedId
|
|
192
|
+
? [...modelProviders.filter((p) => p.id === scopedId), ...modelProviders.filter((p) => p.id !== scopedId)]
|
|
193
|
+
: modelProviders;
|
|
194
|
+
|
|
195
|
+
const selectedModelValue = (() => {
|
|
196
|
+
const id = settings?.defaultModel;
|
|
197
|
+
if (!id) return "";
|
|
198
|
+
const dp = settings?.defaultProvider;
|
|
199
|
+
if (dp && allProviders.find((p) => p.id === dp)?.models.some((m) => m.id === id)) {
|
|
200
|
+
return modelValue(dp, id);
|
|
201
|
+
}
|
|
202
|
+
const owner = allProviders.find((p) => p.models.some((m) => m.id === id));
|
|
203
|
+
return owner ? modelValue(owner.id, id) : id; // stale ids display as-is
|
|
204
|
+
})();
|
|
205
|
+
|
|
206
|
+
const handleDefaultModelChange = (v: string) => {
|
|
207
|
+
if (!v) {
|
|
208
|
+
updateSettings({ defaultModel: undefined });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const idx = v.indexOf("/"); // model ids may themselves contain '/'
|
|
212
|
+
const providerId = idx >= 0 ? v.slice(0, idx) : "";
|
|
213
|
+
const modelId = idx >= 0 ? v.slice(idx + 1) : v;
|
|
214
|
+
// Keep the provider in sync so the (provider, model) pair stays consistent.
|
|
215
|
+
updateSettings(providerId ? { defaultProvider: providerId, defaultModel: modelId } : { defaultModel: modelId });
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleDefaultProviderChange = (v: string) => {
|
|
219
|
+
// Drop a default model that doesn't belong to the newly picked provider.
|
|
220
|
+
let clearModel = false;
|
|
221
|
+
if (v && settings?.defaultModel) {
|
|
222
|
+
const provider = allProviders.find((p) => p.id === v);
|
|
223
|
+
if (provider && !provider.models.some((m) => m.id === settings.defaultModel)) {
|
|
224
|
+
clearModel = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
updateSettings(clearModel ? { defaultProvider: v, defaultModel: undefined } : { defaultProvider: v || undefined });
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Display names that appear under several providers need the provider name
|
|
231
|
+
// appended to stay distinguishable.
|
|
232
|
+
const modelNameCounts = allModels.reduce<Record<string, number>>((acc, m) => {
|
|
233
|
+
const name = m.name || m.id;
|
|
234
|
+
acc[name] = (acc[name] ?? 0) + 1;
|
|
235
|
+
return acc;
|
|
236
|
+
}, {});
|
|
237
|
+
const modelTagLabel = (m: (typeof allModels)[number]) => {
|
|
238
|
+
const name = m.name || m.id;
|
|
239
|
+
return (modelNameCounts[name] ?? 0) > 1 ? `${name} · ${m.providerName}` : name;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const enabledModels = settings?.enabledModels ?? [];
|
|
243
|
+
const themeLabelKey: Record<string, string> = {
|
|
244
|
+
light: "settings.light",
|
|
245
|
+
dark: "settings.dark",
|
|
246
|
+
"light/dark": "settings.system",
|
|
247
|
+
};
|
|
63
248
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
249
|
+
const tabs: { key: SettingsTab; icon: typeof Palette; label: string }[] = [
|
|
250
|
+
{ key: "appearance", icon: Palette, label: t("settings.appearance") },
|
|
251
|
+
{ key: "models", icon: LayoutGrid, label: t("settings.tab_models") },
|
|
252
|
+
{ key: "advanced", icon: Wrench, label: t("settings.tab_advanced") },
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
const selectCls =
|
|
256
|
+
"rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-white";
|
|
68
257
|
|
|
69
258
|
return (
|
|
70
259
|
<div className="space-y-6">
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
|
|
260
|
+
{/* ── Hero ─────────────────────────────────────────── */}
|
|
261
|
+
<header>
|
|
262
|
+
<div className="text-[11px] font-semibold uppercase tracking-widest text-blue-400">
|
|
263
|
+
pi · workspace
|
|
264
|
+
</div>
|
|
265
|
+
<h1 className="mt-1 text-2xl font-bold text-white">{t("settings.title")}</h1>
|
|
266
|
+
<p className="mt-1 text-sm text-gray-400">{t("settings.subtitle")}</p>
|
|
267
|
+
<div className="mt-4 grid max-w-lg grid-cols-3 gap-3">
|
|
268
|
+
<div className="rounded-xl border border-gray-800 bg-gray-900/50 px-4 py-3">
|
|
269
|
+
<div className="text-xl font-bold text-white">{allProviders.length}</div>
|
|
270
|
+
<div className="text-xs text-gray-500">{t("settings.stat_providers")}</div>
|
|
271
|
+
</div>
|
|
272
|
+
<div className="rounded-xl border border-gray-800 bg-gray-900/50 px-4 py-3">
|
|
273
|
+
<div className="text-xl font-bold text-white">
|
|
274
|
+
{enabledModels.length}
|
|
275
|
+
<small className="ml-0.5 text-xs font-normal text-gray-500">/ {allModels.length}</small>
|
|
276
|
+
</div>
|
|
277
|
+
<div className="text-xs text-gray-500">{t("settings.stat_enabled")}</div>
|
|
278
|
+
</div>
|
|
279
|
+
<div className="rounded-xl border border-gray-800 bg-gray-900/50 px-4 py-3">
|
|
280
|
+
<div className="pt-1 text-sm font-bold text-white">
|
|
281
|
+
{t(themeLabelKey[settings?.theme ?? "light/dark"] ?? "settings.system")}
|
|
282
|
+
</div>
|
|
283
|
+
<div className="mt-0.5 text-xs text-gray-500">{t("settings.stat_theme")}</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</header>
|
|
287
|
+
|
|
288
|
+
{/* ── Tab nav ──────────────────────────────────────── */}
|
|
289
|
+
<nav className="flex gap-2 border-b border-gray-800 pb-3">
|
|
290
|
+
{tabs.map(({ key, icon: Icon, label }) => (
|
|
291
|
+
<button
|
|
292
|
+
key={key}
|
|
293
|
+
onClick={() => setActiveTab(key)}
|
|
294
|
+
className={cn(
|
|
295
|
+
"flex items-center gap-2 rounded-full border px-4 py-1.5 text-sm transition-all",
|
|
296
|
+
activeTab === key
|
|
297
|
+
? "border-blue-500 bg-blue-500/10 text-blue-400"
|
|
298
|
+
: "border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600"
|
|
299
|
+
)}
|
|
300
|
+
>
|
|
301
|
+
<Icon className="h-3.5 w-3.5" />
|
|
302
|
+
{label}
|
|
303
|
+
</button>
|
|
304
|
+
))}
|
|
305
|
+
</nav>
|
|
75
306
|
|
|
76
|
-
{/*
|
|
77
|
-
|
|
78
|
-
<div className="
|
|
79
|
-
<
|
|
307
|
+
{/* ── Appearance ───────────────────────────────────── */}
|
|
308
|
+
{activeTab === "appearance" && (
|
|
309
|
+
<div className="space-y-6">
|
|
310
|
+
<Card icon={Palette} title={t("settings.theme")} desc={t("settings.theme_desc")}>
|
|
311
|
+
<div className="grid grid-cols-3 gap-3 max-w-xl">
|
|
312
|
+
{THEME_SWATCHES.map((s) => {
|
|
313
|
+
const active = (settings?.theme ?? "light/dark") === s.value;
|
|
314
|
+
return (
|
|
315
|
+
<button
|
|
316
|
+
key={s.value}
|
|
317
|
+
onClick={() => setTheme(s.value)}
|
|
318
|
+
className={cn(
|
|
319
|
+
"overflow-hidden rounded-xl border text-left transition-all",
|
|
320
|
+
active ? "border-blue-500 ring-1 ring-blue-500/40" : "border-gray-700 hover:border-gray-600"
|
|
321
|
+
)}
|
|
322
|
+
>
|
|
323
|
+
<div
|
|
324
|
+
className="flex h-16 items-end gap-1.5 p-2.5"
|
|
325
|
+
style={{ background: s.bg }}
|
|
326
|
+
>
|
|
327
|
+
{s.dots.map((d) => (
|
|
328
|
+
<span key={d} className="h-2.5 w-2.5 rounded-full" style={{ background: d }} />
|
|
329
|
+
))}
|
|
330
|
+
</div>
|
|
331
|
+
<div className="flex items-center justify-between bg-gray-800 px-3 py-2">
|
|
332
|
+
<span className={cn("text-xs font-medium", active ? "text-blue-400" : "text-gray-400")}>
|
|
333
|
+
{t(s.labelKey)}
|
|
334
|
+
</span>
|
|
335
|
+
{active && <Check className="h-3.5 w-3.5 text-blue-400" />}
|
|
336
|
+
</div>
|
|
337
|
+
</button>
|
|
338
|
+
);
|
|
339
|
+
})}
|
|
340
|
+
</div>
|
|
341
|
+
<div className="mt-5 flex items-center gap-3">
|
|
342
|
+
<label className="flex items-center gap-2 text-sm text-gray-400">
|
|
343
|
+
<input
|
|
344
|
+
type="checkbox"
|
|
345
|
+
checked={settings?.hideThinkingBlock ?? false}
|
|
346
|
+
onChange={(e) => updateSettings({ hideThinkingBlock: e.target.checked })}
|
|
347
|
+
className="rounded border-gray-600 bg-gray-800 text-blue-500"
|
|
348
|
+
/>
|
|
349
|
+
{t("settings.hide_thinking")}
|
|
350
|
+
</label>
|
|
351
|
+
</div>
|
|
352
|
+
</Card>
|
|
353
|
+
|
|
354
|
+
<Card icon={Type} title={t("settings.font_size")} desc={t("settings.font_size_desc")}>
|
|
355
|
+
<div className="flex max-w-xl items-center gap-4">
|
|
356
|
+
<input
|
|
357
|
+
type="range"
|
|
358
|
+
min={12}
|
|
359
|
+
max={24}
|
|
360
|
+
step={1}
|
|
361
|
+
value={fontSize}
|
|
362
|
+
onChange={(e) => setFontSize(Number(e.target.value))}
|
|
363
|
+
className="flex-1 accent-blue-500"
|
|
364
|
+
/>
|
|
365
|
+
<span className="w-12 rounded-lg border border-gray-700 bg-gray-800 px-2 py-1 text-center text-xs text-gray-300">
|
|
366
|
+
{fontSize}px
|
|
367
|
+
</span>
|
|
368
|
+
</div>
|
|
369
|
+
</Card>
|
|
80
370
|
</div>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
371
|
+
)}
|
|
372
|
+
|
|
373
|
+
{/* ── Models ───────────────────────────────────────── */}
|
|
374
|
+
{activeTab === "models" && (
|
|
375
|
+
<div className="space-y-6">
|
|
376
|
+
<Card icon={SettingsIcon} title={t("settings.defaults")}>
|
|
377
|
+
<SettingRow label={t("settings.default_provider")}>
|
|
85
378
|
<select
|
|
86
379
|
value={settings?.defaultProvider ?? ""}
|
|
87
|
-
onChange={(e) =>
|
|
88
|
-
className="
|
|
380
|
+
onChange={(e) => handleDefaultProviderChange(e.target.value)}
|
|
381
|
+
className={cn(selectCls, "w-56")}
|
|
89
382
|
>
|
|
90
|
-
<option value=""
|
|
91
|
-
{
|
|
92
|
-
<option key={
|
|
383
|
+
<option value="">{t("settings.none")}</option>
|
|
384
|
+
{allProviders.map((p) => (
|
|
385
|
+
<option key={p.id} value={p.id}>{p.name}</option>
|
|
93
386
|
))}
|
|
94
387
|
</select>
|
|
95
|
-
</
|
|
96
|
-
<
|
|
97
|
-
<label className="block text-xs font-medium text-gray-400">Default Model</label>
|
|
388
|
+
</SettingRow>
|
|
389
|
+
<SettingRow label={t("settings.default_model")}>
|
|
98
390
|
<select
|
|
99
|
-
value={
|
|
100
|
-
onChange={(e) =>
|
|
101
|
-
className="
|
|
391
|
+
value={selectedModelValue}
|
|
392
|
+
onChange={(e) => handleDefaultModelChange(e.target.value)}
|
|
393
|
+
className={cn(selectCls, "w-56")}
|
|
102
394
|
>
|
|
103
|
-
<option value=""
|
|
104
|
-
{
|
|
105
|
-
<
|
|
395
|
+
<option value="">{t("settings.none")}</option>
|
|
396
|
+
{groupedModelOptions.map((p) => (
|
|
397
|
+
<optgroup key={p.id} label={p.name}>
|
|
398
|
+
{p.models.map((m) => (
|
|
399
|
+
<option key={modelValue(p.id, m.id)} value={modelValue(p.id, m.id)}>
|
|
400
|
+
{(m.name || m.id) + " · " + p.name}
|
|
401
|
+
</option>
|
|
402
|
+
))}
|
|
403
|
+
</optgroup>
|
|
106
404
|
))}
|
|
107
405
|
</select>
|
|
108
|
-
</
|
|
109
|
-
|
|
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>
|
|
406
|
+
</SettingRow>
|
|
407
|
+
<SettingRow label={t("settings.default_thinking")}>
|
|
113
408
|
<select
|
|
114
409
|
value={settings?.defaultThinkingLevel ?? "medium"}
|
|
115
410
|
onChange={(e) => updateSettings({ defaultThinkingLevel: e.target.value })}
|
|
116
|
-
className="
|
|
411
|
+
className={cn(selectCls, "w-56")}
|
|
117
412
|
>
|
|
118
413
|
{["off", "minimal", "low", "medium", "high", "xhigh"].map((l) => (
|
|
119
414
|
<option key={l} value={l}>{l.charAt(0).toUpperCase() + l.slice(1)}</option>
|
|
120
415
|
))}
|
|
121
416
|
</select>
|
|
122
|
-
</
|
|
123
|
-
<
|
|
124
|
-
<label className="block text-xs font-medium text-gray-400">Project Trust</label>
|
|
417
|
+
</SettingRow>
|
|
418
|
+
<SettingRow label={t("settings.project_trust")}>
|
|
125
419
|
<select
|
|
126
420
|
value={settings?.defaultProjectTrust ?? "prompt"}
|
|
127
421
|
onChange={(e) => updateSettings({ defaultProjectTrust: e.target.value })}
|
|
128
|
-
className="
|
|
422
|
+
className={cn(selectCls, "w-56")}
|
|
129
423
|
>
|
|
130
|
-
{["prompt", "always", "never"].map((
|
|
131
|
-
<option key={
|
|
424
|
+
{["prompt", "always", "never"].map((v) => (
|
|
425
|
+
<option key={v} value={v}>{v.charAt(0).toUpperCase() + v.slice(1)}</option>
|
|
132
426
|
))}
|
|
133
427
|
</select>
|
|
428
|
+
</SettingRow>
|
|
429
|
+
</Card>
|
|
430
|
+
|
|
431
|
+
<Card icon={Zap} title={t("settings.enabled_models")} desc={t("settings.enabled_models_desc")}>
|
|
432
|
+
<div className="flex flex-wrap gap-1.5">
|
|
433
|
+
{allModels.map((m) => {
|
|
434
|
+
const ref = `${m.providerId}/${m.id}`;
|
|
435
|
+
const on = enabledModels.includes(ref);
|
|
436
|
+
return (
|
|
437
|
+
<button
|
|
438
|
+
key={ref}
|
|
439
|
+
onClick={() => (on ? removeEnabledModel(ref) : addEnabledModel(ref))}
|
|
440
|
+
className={cn(
|
|
441
|
+
"rounded-md border px-2.5 py-1 text-xs transition-colors",
|
|
442
|
+
on
|
|
443
|
+
? "border-blue-500/50 bg-blue-500/10 text-blue-400"
|
|
444
|
+
: "border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600"
|
|
445
|
+
)}
|
|
446
|
+
>
|
|
447
|
+
{modelTagLabel(m)}
|
|
448
|
+
</button>
|
|
449
|
+
);
|
|
450
|
+
})}
|
|
134
451
|
</div>
|
|
135
|
-
</
|
|
452
|
+
</Card>
|
|
136
453
|
</div>
|
|
137
|
-
|
|
454
|
+
)}
|
|
138
455
|
|
|
139
|
-
{/*
|
|
140
|
-
|
|
141
|
-
<div className="
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
456
|
+
{/* ── Advanced ─────────────────────────────────────── */}
|
|
457
|
+
{activeTab === "advanced" && (
|
|
458
|
+
<div className="space-y-6">
|
|
459
|
+
<Card icon={CloudDownload} title={t("settings.updates_title")} desc={t("settings.updates_desc")}>
|
|
460
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
461
|
+
<button
|
|
462
|
+
onClick={handleCheckUpdates}
|
|
463
|
+
disabled={checkingUpdates || applying}
|
|
464
|
+
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 disabled:opacity-60"
|
|
465
|
+
>
|
|
466
|
+
<RefreshCw className={cn("h-4 w-4", checkingUpdates && "animate-spin")} />
|
|
467
|
+
{t("settings.check_updates")}
|
|
468
|
+
</button>
|
|
469
|
+
{(updateResult?.extensions ?? []).some((e) => e.hasUpdate) && (
|
|
153
470
|
<button
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
className=
|
|
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
|
-
)}
|
|
471
|
+
onClick={handleApplyUpdates}
|
|
472
|
+
disabled={applying || checkingUpdates}
|
|
473
|
+
className="flex items-center gap-2 rounded-lg border border-amber-500 bg-amber-500/10 px-4 py-2 text-sm font-medium text-amber-400 hover:bg-amber-500/20 disabled:opacity-60"
|
|
162
474
|
>
|
|
163
|
-
<
|
|
164
|
-
{
|
|
165
|
-
{active && <CheckCircle2 className="h-3.5 w-3.5 ml-1" />}
|
|
475
|
+
<CloudDownload className={cn("h-4 w-4", applying && "animate-pulse")} />
|
|
476
|
+
{applying ? t("settings.updating") : t("settings.update_all")}
|
|
166
477
|
</button>
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
{updateError && <p className="mt-3 text-sm text-red-400">{t("settings.updates_failed")}</p>}
|
|
481
|
+
{applyMessage && (
|
|
482
|
+
<div className="mt-3 space-y-1">
|
|
483
|
+
{applyMessage.ok > 0 && (
|
|
484
|
+
<p className="text-sm text-emerald-400">{t("settings.update_success", String(applyMessage.ok))}</p>
|
|
485
|
+
)}
|
|
486
|
+
{applyMessage.failNames.length > 0 && (
|
|
487
|
+
<p className="text-sm text-red-400">
|
|
488
|
+
{t("settings.update_failed_names", String(applyMessage.failNames.length), applyMessage.failNames.join(", "))}
|
|
489
|
+
</p>
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
{updateResult && (() => {
|
|
494
|
+
const rows = [...(updateResult.pi ? [updateResult.pi] : []), ...updateResult.extensions];
|
|
495
|
+
const updatable = rows.filter((r) => r.hasUpdate).length;
|
|
496
|
+
return (
|
|
497
|
+
<div className="mt-4 space-y-1.5">
|
|
498
|
+
<p className={cn("text-xs font-semibold", updatable > 0 ? "text-amber-400" : "text-emerald-400")}>
|
|
499
|
+
{updatable > 0
|
|
500
|
+
? t("settings.updates_summary", String(updatable))
|
|
501
|
+
: t("settings.updates_all_latest")}
|
|
502
|
+
</p>
|
|
503
|
+
{rows.map((r) => (
|
|
504
|
+
<div
|
|
505
|
+
key={r.name}
|
|
506
|
+
className={cn(
|
|
507
|
+
"flex items-center justify-between gap-3 rounded-lg border bg-gray-800/30 px-3 py-2",
|
|
508
|
+
r.hasUpdate ? "border-amber-500/60" : "border-gray-800"
|
|
509
|
+
)}
|
|
510
|
+
>
|
|
511
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
512
|
+
<span className="truncate font-mono text-xs text-gray-200">{r.name}</span>
|
|
513
|
+
{updateResult.pi && r.name === updateResult.pi.name && (
|
|
514
|
+
<span className="rounded border border-gray-700 bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-500">
|
|
515
|
+
core
|
|
516
|
+
</span>
|
|
517
|
+
)}
|
|
518
|
+
</div>
|
|
519
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
520
|
+
<span className="font-mono text-[11px] text-gray-500">
|
|
521
|
+
{r.latest === null ? `${r.installed} → ?` : r.hasUpdate ? `${r.installed} → ${r.latest}` : r.installed}
|
|
522
|
+
</span>
|
|
523
|
+
{r.latest === null ? (
|
|
524
|
+
<span className="rounded border border-gray-700 px-1.5 py-0.5 text-[10px] text-gray-500">
|
|
525
|
+
{t("settings.updates_lookup_failed")}
|
|
526
|
+
</span>
|
|
527
|
+
) : r.hasUpdate ? (
|
|
528
|
+
<span className="rounded border border-amber-500 bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium text-amber-400">
|
|
529
|
+
{t("settings.update_available")}
|
|
530
|
+
</span>
|
|
531
|
+
) : (
|
|
532
|
+
<span className="rounded border border-emerald-600/60 px-1.5 py-0.5 text-[10px] text-emerald-400">
|
|
533
|
+
{t("settings.updates_up_to_date")}
|
|
534
|
+
</span>
|
|
535
|
+
)}
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
))}
|
|
539
|
+
{updatable > 0 && (
|
|
540
|
+
<p className="pt-1 text-[11px] text-gray-500">{t("settings.updates_hint")}</p>
|
|
541
|
+
)}
|
|
542
|
+
</div>
|
|
167
543
|
);
|
|
168
|
-
})}
|
|
169
|
-
</
|
|
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>
|
|
544
|
+
})()}
|
|
545
|
+
</Card>
|
|
183
546
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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"
|
|
547
|
+
<Card icon={Package} title={t("settings.packages")}>
|
|
548
|
+
{(settings?.packages ?? []).length > 0 && (
|
|
549
|
+
<div className="mb-4 flex flex-wrap gap-1.5">
|
|
550
|
+
{(settings?.packages ?? []).map((pkg) => (
|
|
551
|
+
<span
|
|
552
|
+
key={pkg}
|
|
553
|
+
className="flex items-center gap-1.5 rounded-md border border-gray-700 bg-gray-800 px-2.5 py-1 text-xs text-gray-300"
|
|
203
554
|
>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
555
|
+
{pkg}
|
|
556
|
+
<button
|
|
557
|
+
onClick={() => removePackage(pkg)}
|
|
558
|
+
className="text-gray-500 hover:text-red-400"
|
|
559
|
+
>
|
|
560
|
+
<X className="h-3 w-3" />
|
|
561
|
+
</button>
|
|
562
|
+
</span>
|
|
563
|
+
))}
|
|
564
|
+
</div>
|
|
208
565
|
)}
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
))
|
|
566
|
+
{(settings?.packages ?? []).length === 0 && (
|
|
567
|
+
<p className="mb-4 text-sm text-gray-500">{t("settings.no_packages")}</p>
|
|
264
568
|
)}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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>
|
|
569
|
+
<div className="flex max-w-md gap-2">
|
|
570
|
+
<input
|
|
571
|
+
type="text"
|
|
572
|
+
value={newPackage}
|
|
573
|
+
onChange={(e) => setNewPackage(e.target.value)}
|
|
574
|
+
placeholder={t("settings.package_placeholder")}
|
|
575
|
+
className="flex-1 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-white placeholder-gray-500"
|
|
576
|
+
onKeyDown={(e) => {
|
|
577
|
+
if (e.key === "Enter" && newPackage.trim()) {
|
|
578
|
+
addPackage(newPackage.trim());
|
|
579
|
+
setNewPackage("");
|
|
580
|
+
}
|
|
581
|
+
}}
|
|
582
|
+
/>
|
|
583
|
+
<button
|
|
584
|
+
onClick={() => {
|
|
585
|
+
if (newPackage.trim()) {
|
|
586
|
+
addPackage(newPackage.trim());
|
|
587
|
+
setNewPackage("");
|
|
588
|
+
}
|
|
589
|
+
}}
|
|
590
|
+
className="flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-300 hover:bg-gray-700"
|
|
591
|
+
>
|
|
592
|
+
<Plus className="h-3.5 w-3.5" />
|
|
593
|
+
{t("settings.add")}
|
|
594
|
+
</button>
|
|
595
|
+
</div>
|
|
596
|
+
</Card>
|
|
301
597
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
598
|
+
<Card icon={Download} title={t("settings.import_export")}>
|
|
599
|
+
<div className="flex flex-wrap gap-3">
|
|
600
|
+
<button
|
|
601
|
+
onClick={handleExport}
|
|
602
|
+
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"
|
|
603
|
+
>
|
|
604
|
+
<Download className="h-4 w-4" />
|
|
605
|
+
{t("settings.export")}
|
|
606
|
+
</button>
|
|
607
|
+
<button
|
|
608
|
+
onClick={() => fileInputRef.current?.click()}
|
|
609
|
+
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"
|
|
610
|
+
>
|
|
611
|
+
<Upload className="h-4 w-4" />
|
|
612
|
+
{t("settings.import")}
|
|
613
|
+
</button>
|
|
614
|
+
<button
|
|
615
|
+
onClick={() => setShowResetConfirm(true)}
|
|
616
|
+
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"
|
|
617
|
+
>
|
|
618
|
+
<RotateCcw className="h-4 w-4" />
|
|
619
|
+
{t("settings.reset")}
|
|
620
|
+
</button>
|
|
621
|
+
<input
|
|
622
|
+
ref={fileInputRef}
|
|
623
|
+
type="file"
|
|
624
|
+
accept=".json"
|
|
625
|
+
className="hidden"
|
|
626
|
+
onChange={handleImport}
|
|
627
|
+
/>
|
|
628
|
+
</div>
|
|
629
|
+
{importError && <p className="mt-3 text-sm text-red-400">{importError}</p>}
|
|
630
|
+
</Card>
|
|
315
631
|
</div>
|
|
316
|
-
|
|
632
|
+
)}
|
|
317
633
|
|
|
318
634
|
{/* Reset Confirm */}
|
|
319
635
|
<Modal
|
|
320
636
|
open={showResetConfirm}
|
|
321
637
|
onClose={() => setShowResetConfirm(false)}
|
|
322
|
-
title="
|
|
638
|
+
title={t("settings.reset_title")}
|
|
323
639
|
>
|
|
324
640
|
<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>
|
|
641
|
+
<p className="text-sm text-gray-400">{t("settings.reset_confirm")}</p>
|
|
330
642
|
<div className="flex justify-end gap-3">
|
|
331
643
|
<button
|
|
332
644
|
onClick={() => setShowResetConfirm(false)}
|
|
333
645
|
className="rounded-lg px-4 py-2 text-sm text-gray-400 hover:bg-gray-800"
|
|
334
646
|
>
|
|
335
|
-
|
|
647
|
+
{t("settings.cancel")}
|
|
336
648
|
</button>
|
|
337
649
|
<button
|
|
338
650
|
onClick={() => {
|
|
@@ -341,11 +653,11 @@ export function SettingsPage() {
|
|
|
341
653
|
}}
|
|
342
654
|
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-500"
|
|
343
655
|
>
|
|
344
|
-
|
|
656
|
+
{t("settings.reset_action")}
|
|
345
657
|
</button>
|
|
346
658
|
</div>
|
|
347
659
|
</div>
|
|
348
660
|
</Modal>
|
|
349
661
|
</div>
|
|
350
662
|
);
|
|
351
|
-
}
|
|
663
|
+
}
|