@raingor/pi-web-switch 0.3.2 → 0.4.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 +17 -1
- package/README.md +17 -1
- package/README.zh-CN.md +17 -1
- package/package.json +46 -3
- package/public/apple-touch-icon.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/pi.svg +47 -4
- package/server/pi-reader.ts +750 -4
- package/src/App.tsx +2 -0
- package/src/components/layout/Sidebar.tsx +3 -5
- package/src/components/providers/ProvidersModelsPage.tsx +1171 -124
- package/src/components/settings/SettingsPage.tsx +74 -55
- package/src/components/subagents/SubagentsPage.tsx +502 -0
- package/src/data/builtin-providers.ts +15 -7
- package/src/data/model-catalog.ts +967 -0
- package/src/index.css +5 -1
- package/src/lib/config.ts +61 -0
- package/src/lib/translations/en.ts +91 -1
- package/src/lib/translations/ja.ts +91 -1
- package/src/lib/translations/zh-CN.ts +91 -1
- package/src/lib/translations/zh-TW.ts +91 -1
- package/src/main.tsx +9 -0
- package/src/store/config-store.ts +75 -28
- package/src/types/index.ts +52 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +58 -2
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -2,7 +2,13 @@ import { useState, useRef, useEffect } from "react";
|
|
|
2
2
|
import { useConfigStore } from "@/store/config-store";
|
|
3
3
|
import { useTranslation } from "@/lib/i18n";
|
|
4
4
|
import { Modal } from "@/components/ui/Modal";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
exportConfig,
|
|
7
|
+
exportConfigToDirectory,
|
|
8
|
+
importConfigFromFile,
|
|
9
|
+
parseImportFile,
|
|
10
|
+
saveLocalBackup,
|
|
11
|
+
} from "@/lib/config";
|
|
6
12
|
import type { PiConfig, UpdateCheckResult } from "@/types";
|
|
7
13
|
import { cn } from "@/lib/utils";
|
|
8
14
|
import {
|
|
@@ -18,9 +24,9 @@ import {
|
|
|
18
24
|
LayoutGrid,
|
|
19
25
|
Wrench,
|
|
20
26
|
Settings as SettingsIcon,
|
|
21
|
-
Zap,
|
|
22
27
|
CloudDownload,
|
|
23
28
|
RefreshCw,
|
|
29
|
+
ZoomIn,
|
|
24
30
|
} from "lucide-react";
|
|
25
31
|
|
|
26
32
|
type SettingsTab = "appearance" | "models" | "advanced";
|
|
@@ -38,6 +44,7 @@ const THEME_SWATCHES: {
|
|
|
38
44
|
];
|
|
39
45
|
|
|
40
46
|
const FONT_SIZE_KEY = "pi-font-size";
|
|
47
|
+
const UI_ZOOM_KEY = "pi-ui-zoom";
|
|
41
48
|
|
|
42
49
|
function Card({
|
|
43
50
|
icon: Icon,
|
|
@@ -85,8 +92,6 @@ export function SettingsPage() {
|
|
|
85
92
|
allModels,
|
|
86
93
|
updateSettings,
|
|
87
94
|
setTheme,
|
|
88
|
-
addEnabledModel,
|
|
89
|
-
removeEnabledModel,
|
|
90
95
|
addPackage,
|
|
91
96
|
removePackage,
|
|
92
97
|
importConfig: importConfigAction,
|
|
@@ -101,6 +106,10 @@ export function SettingsPage() {
|
|
|
101
106
|
const saved = Number(localStorage.getItem(FONT_SIZE_KEY));
|
|
102
107
|
return saved >= 12 && saved <= 24 ? saved : 16;
|
|
103
108
|
});
|
|
109
|
+
const [uiZoom, setUiZoom] = useState(() => {
|
|
110
|
+
const saved = Number(localStorage.getItem(UI_ZOOM_KEY));
|
|
111
|
+
return saved >= 50 && saved <= 200 ? saved : 100;
|
|
112
|
+
});
|
|
104
113
|
// pi core / extensions update check (Advanced tab).
|
|
105
114
|
const [updateResult, setUpdateResult] = useState<UpdateCheckResult | null>(null);
|
|
106
115
|
const [checkingUpdates, setCheckingUpdates] = useState(false);
|
|
@@ -157,30 +166,52 @@ export function SettingsPage() {
|
|
|
157
166
|
localStorage.setItem(FONT_SIZE_KEY, String(fontSize));
|
|
158
167
|
}, [fontSize]);
|
|
159
168
|
|
|
160
|
-
|
|
169
|
+
// Apply + persist UI zoom (whole-interface percentage scaling).
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
document.documentElement.style.zoom = `${uiZoom}%`;
|
|
172
|
+
localStorage.setItem(UI_ZOOM_KEY, String(uiZoom));
|
|
173
|
+
}, [uiZoom]);
|
|
174
|
+
|
|
175
|
+
const handleImportFromInput = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
161
176
|
const file = e.target.files?.[0];
|
|
162
177
|
if (!file) return;
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
reader.readAsText(file);
|
|
178
|
+
const text = await file.text();
|
|
179
|
+
const result = parseImportFile(text);
|
|
180
|
+
if (result) {
|
|
181
|
+
await importConfigAction(result);
|
|
182
|
+
setImportError("");
|
|
183
|
+
} else {
|
|
184
|
+
setImportError(t("settings.import_error"));
|
|
185
|
+
}
|
|
186
|
+
// Reset input so the same file can be selected again
|
|
187
|
+
e.target.value = "";
|
|
174
188
|
};
|
|
175
189
|
|
|
176
|
-
const
|
|
190
|
+
const handleImportClick = async () => {
|
|
191
|
+
const { config, cancelled } = await importConfigFromFile();
|
|
192
|
+
if (config) {
|
|
193
|
+
await importConfigAction(config);
|
|
194
|
+
setImportError("");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!cancelled) {
|
|
198
|
+
// API unavailable or parse failed → fallback to hidden file input
|
|
199
|
+
fileInputRef.current?.click();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handleExport = async () => {
|
|
177
204
|
const cfg: PiConfig = {
|
|
178
205
|
settings: settings ?? { theme: "dark", packages: [], enabledModels: [] },
|
|
179
206
|
auth: auth ?? {},
|
|
180
207
|
modelsJson: modelsJson ?? { providers: {} },
|
|
181
208
|
};
|
|
182
209
|
saveLocalBackup(cfg);
|
|
183
|
-
|
|
210
|
+
const { ok, cancelled } = await exportConfigToDirectory(cfg);
|
|
211
|
+
if (!ok && !cancelled) {
|
|
212
|
+
// API unavailable or write failed → fallback to download
|
|
213
|
+
exportConfig(cfg);
|
|
214
|
+
}
|
|
184
215
|
};
|
|
185
216
|
|
|
186
217
|
// ── Default model select: composite `providerId/modelId` values, but the
|
|
@@ -227,18 +258,6 @@ export function SettingsPage() {
|
|
|
227
258
|
updateSettings(clearModel ? { defaultProvider: v, defaultModel: undefined } : { defaultProvider: v || undefined });
|
|
228
259
|
};
|
|
229
260
|
|
|
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
261
|
const enabledModels = settings?.enabledModels ?? [];
|
|
243
262
|
const themeLabelKey: Record<string, string> = {
|
|
244
263
|
light: "settings.light",
|
|
@@ -367,6 +386,29 @@ export function SettingsPage() {
|
|
|
367
386
|
</span>
|
|
368
387
|
</div>
|
|
369
388
|
</Card>
|
|
389
|
+
|
|
390
|
+
<Card icon={ZoomIn} title={t("settings.ui_zoom")} desc={t("settings.ui_zoom_desc")}>
|
|
391
|
+
<div className="flex max-w-xl items-center gap-4">
|
|
392
|
+
<input
|
|
393
|
+
type="range"
|
|
394
|
+
min={50}
|
|
395
|
+
max={200}
|
|
396
|
+
step={5}
|
|
397
|
+
value={uiZoom}
|
|
398
|
+
onChange={(e) => setUiZoom(Number(e.target.value))}
|
|
399
|
+
className="flex-1 accent-blue-500"
|
|
400
|
+
/>
|
|
401
|
+
<span className="w-14 rounded-lg border border-gray-700 bg-gray-800 px-2 py-1 text-center text-xs text-gray-300">
|
|
402
|
+
{uiZoom}%
|
|
403
|
+
</span>
|
|
404
|
+
<button
|
|
405
|
+
onClick={() => setUiZoom(100)}
|
|
406
|
+
className="rounded-lg border border-gray-700 px-3 py-1 text-xs text-gray-300 transition-colors hover:bg-gray-800"
|
|
407
|
+
>
|
|
408
|
+
{t("settings.ui_zoom_reset")}
|
|
409
|
+
</button>
|
|
410
|
+
</div>
|
|
411
|
+
</Card>
|
|
370
412
|
</div>
|
|
371
413
|
)}
|
|
372
414
|
|
|
@@ -427,29 +469,6 @@ export function SettingsPage() {
|
|
|
427
469
|
</select>
|
|
428
470
|
</SettingRow>
|
|
429
471
|
</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
|
-
})}
|
|
451
|
-
</div>
|
|
452
|
-
</Card>
|
|
453
472
|
</div>
|
|
454
473
|
)}
|
|
455
474
|
|
|
@@ -605,7 +624,7 @@ export function SettingsPage() {
|
|
|
605
624
|
{t("settings.export")}
|
|
606
625
|
</button>
|
|
607
626
|
<button
|
|
608
|
-
onClick={
|
|
627
|
+
onClick={handleImportClick}
|
|
609
628
|
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
629
|
>
|
|
611
630
|
<Upload className="h-4 w-4" />
|
|
@@ -623,7 +642,7 @@ export function SettingsPage() {
|
|
|
623
642
|
type="file"
|
|
624
643
|
accept=".json"
|
|
625
644
|
className="hidden"
|
|
626
|
-
onChange={
|
|
645
|
+
onChange={handleImportFromInput}
|
|
627
646
|
/>
|
|
628
647
|
</div>
|
|
629
648
|
{importError && <p className="mt-3 text-sm text-red-400">{importError}</p>}
|