@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.
@@ -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 { exportConfig, parseImportFile, saveLocalBackup } from "@/lib/config";
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
- const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
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 reader = new FileReader();
164
- reader.onload = async (ev) => {
165
- const result = parseImportFile(ev.target?.result as string);
166
- if (result) {
167
- await importConfigAction(result);
168
- setImportError("");
169
- } else {
170
- setImportError(t("settings.import_error"));
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 handleExport = () => {
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
- exportConfig(cfg);
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={() => fileInputRef.current?.click()}
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={handleImport}
645
+ onChange={handleImportFromInput}
627
646
  />
628
647
  </div>
629
648
  {importError && <p className="mt-3 text-sm text-red-400">{importError}</p>}