@raingor/pi-web-switch 0.2.0 → 0.3.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/index.html CHANGED
@@ -2,9 +2,13 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <title>pi-switch — pi Configuration Manager</title>
7
7
  <link rel="icon" type="image/svg+xml" href="/pi.svg" />
8
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
9
+ <link rel="manifest" href="/manifest.webmanifest" />
10
+ <meta name="theme-color" content="#2563eb" />
11
+ <meta name="description" content="Web UI for pi coding agent — configuration management, session browser, and usage dashboard" />
8
12
  </head>
9
13
  <body class="bg-gray-950 text-gray-100 antialiased">
10
14
  <div id="root"></div>
package/package.json CHANGED
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "@raingor/pi-web-switch",
3
3
  "private": false,
4
- "version": "0.2.0",
4
+ "version": "0.3.1",
5
5
  "type": "module",
6
+ "main": "pi-package/index.ts",
6
7
  "description": "Web UI for pi coding agent — live configuration management, session browser, and memory viewer",
7
- "keywords": ["pi-package", "pi", "pi-coding-agent", "dashboard", "configuration", "web-ui"],
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi",
11
+ "pi-coding-agent",
12
+ "dashboard",
13
+ "configuration",
14
+ "web-ui"
15
+ ],
8
16
  "author": "Raingor",
9
17
  "license": "MIT",
10
18
  "repository": {
@@ -21,8 +29,12 @@
21
29
  "preview": "vite preview"
22
30
  },
23
31
  "pi": {
24
- "extensions": ["./pi-package/index.ts"],
25
- "skills": ["./pi-package/skills"]
32
+ "extensions": [
33
+ "./pi-package/index.ts"
34
+ ],
35
+ "skills": [
36
+ "./pi-package/skills"
37
+ ]
26
38
  },
27
39
  "dependencies": {
28
40
  "lucide-react": "^0.487.0",
@@ -39,6 +51,7 @@
39
51
  "@vitejs/plugin-react": "^4.4.1",
40
52
  "tailwindcss": "^4.1.4",
41
53
  "typescript": "~5.8.3",
42
- "vite": "^6.3.2"
54
+ "vite": "^6.3.2",
55
+ "vite-plugin-pwa": "^1.3.0"
43
56
  }
44
- }
57
+ }
Binary file
Binary file
Binary file
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "pi-web-switch",
3
+ "short_name": "pi-switch",
4
+ "description": "Web UI for pi coding agent — configuration management, session browser, and usage dashboard",
5
+ "start_url": "/",
6
+ "scope": "/",
7
+ "display": "standalone",
8
+ "orientation": "any",
9
+ "background_color": "#0a0a0a",
10
+ "theme_color": "#2563eb",
11
+ "icons": [
12
+ {
13
+ "src": "/icon-192.png",
14
+ "sizes": "192x192",
15
+ "type": "image/png",
16
+ "purpose": "any"
17
+ },
18
+ {
19
+ "src": "/icon-512.png",
20
+ "sizes": "512x512",
21
+ "type": "image/png",
22
+ "purpose": "any"
23
+ },
24
+ {
25
+ "src": "/icon-512.png",
26
+ "sizes": "512x512",
27
+ "type": "image/png",
28
+ "purpose": "maskable"
29
+ }
30
+ ]
31
+ }
package/public/sw.js ADDED
@@ -0,0 +1,81 @@
1
+ // ─── pi-web-switch Service Worker ───────────────────────
2
+ // Cache-first for static assets, network-first for API calls.
3
+ // Bump CACHE_VERSION to invalidate old caches on deploy.
4
+
5
+ const CACHE_VERSION = "pi-web-switch-v1";
6
+ const STATIC_CACHE = `${CACHE_VERSION}-static`;
7
+ const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
8
+
9
+ const PRECACHE_URLS = [
10
+ "/",
11
+ "/index.html",
12
+ "/manifest.webmanifest",
13
+ "/icon-192.png",
14
+ "/icon-512.png",
15
+ "/apple-touch-icon.png",
16
+ ];
17
+
18
+ // ─── Install: precache core assets ─────────────────────
19
+ self.addEventListener("install", (event) => {
20
+ event.waitUntil(
21
+ caches
22
+ .open(STATIC_CACHE)
23
+ .then((cache) => cache.addAll(PRECACHE_URLS))
24
+ .then(() => self.skipWaiting())
25
+ );
26
+ });
27
+
28
+ // ─── Activate: clean old caches ────────────────────────
29
+ self.addEventListener("activate", (event) => {
30
+ event.waitUntil(
31
+ caches
32
+ .keys()
33
+ .then((keys) =>
34
+ Promise.all(
35
+ keys
36
+ .filter((k) => !k.startsWith(CACHE_VERSION))
37
+ .map((k) => caches.delete(k))
38
+ )
39
+ )
40
+ .then(() => self.clients.claim())
41
+ );
42
+ });
43
+
44
+ // ─── Fetch: strategy by request type ───────────────────
45
+ self.addEventListener("fetch", (event) => {
46
+ const { request } = event;
47
+
48
+ // Only handle GET; ignore cross-origin and chrome-extension.
49
+ if (request.method !== "GET") return;
50
+ const url = new URL(request.url);
51
+ if (url.origin !== self.location.origin) return;
52
+
53
+ // API calls: network-first, fallback to cache.
54
+ if (url.pathname.startsWith("/api/")) {
55
+ event.respondWith(
56
+ fetch(request)
57
+ .then((response) => {
58
+ const copy = response.clone();
59
+ caches.open(RUNTIME_CACHE).then((cache) => cache.put(request, copy));
60
+ return response;
61
+ })
62
+ .catch(() => caches.match(request))
63
+ );
64
+ return;
65
+ }
66
+
67
+ // Static assets: cache-first, then network (and cache the result).
68
+ event.respondWith(
69
+ caches.match(request).then((cached) => {
70
+ if (cached) return cached;
71
+ return fetch(request).then((response) => {
72
+ if (!response || response.status !== 200 || response.type !== "basic") {
73
+ return response;
74
+ }
75
+ const copy = response.clone();
76
+ caches.open(RUNTIME_CACHE).then((cache) => cache.put(request, copy));
77
+ return response;
78
+ });
79
+ })
80
+ );
81
+ });
package/src/App.tsx CHANGED
@@ -1,8 +1,6 @@
1
1
  import { BrowserRouter, Routes, Route } from "react-router-dom";
2
2
  import { AppShell } from "@/components/layout/AppShell";
3
3
  import { DashboardPage } from "@/components/dashboard/DashboardPage";
4
- import { ModelsPage } from "@/components/models/ModelsPage";
5
- import { ProvidersPage } from "@/components/providers/ProvidersPage";
6
4
  import { SessionsPage } from "@/components/sessions/SessionsPage";
7
5
  import { MemoryPage } from "@/components/sessions/MemoryPage";
8
6
  import { SettingsPage } from "@/components/settings/SettingsPage";
@@ -13,8 +11,6 @@ export default function App() {
13
11
  <Routes>
14
12
  <Route element={<AppShell />}>
15
13
  <Route path="/" element={<DashboardPage />} />
16
- <Route path="/models" element={<ModelsPage />} />
17
- <Route path="/providers" element={<ProvidersPage />} />
18
14
  <Route path="/sessions" element={<SessionsPage />} />
19
15
  <Route path="/memory" element={<MemoryPage />} />
20
16
  <Route path="/settings" element={<SettingsPage />} />
@@ -99,7 +99,20 @@ const CHART_LABELS: Record<string, string> = {
99
99
 
100
100
  // ─── Helpers ────────────────────────────────────────────
101
101
 
102
- function formatTokensShort(n: number): string {
102
+ function formatTokensShort(n: number, lang: string = "en"): string {
103
+ // 中文:亿 / 万
104
+ if (lang.startsWith("zh")) {
105
+ if (n >= 100_000_000) return `${(n / 100_000_000).toFixed(2)}亿`;
106
+ if (n >= 10_000) return `${(n / 10_000).toFixed(1)}万`;
107
+ return n.toLocaleString("zh-CN");
108
+ }
109
+ // 日文:億 / 万
110
+ if (lang.startsWith("ja")) {
111
+ if (n >= 100_000_000) return `${(n / 100_000_000).toFixed(2)}億`;
112
+ if (n >= 10_000) return `${(n / 10_000).toFixed(1)}万`;
113
+ return n.toLocaleString("ja-JP");
114
+ }
115
+ // 英文:M / K
103
116
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
104
117
  if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
105
118
  return n.toString();
@@ -183,7 +196,7 @@ function StatCard({
183
196
 
184
197
  // ─── Breakdown Row ──────────────────────────────────────
185
198
 
186
- function BreakdownRow({ label, value, total, color }: { label: string; value: number; total: number; color: string }) {
199
+ function BreakdownRow({ label, value, total, color, lang = "en" }: { label: string; value: number; total: number; color: string; lang?: string }) {
187
200
  const pct = total > 0 ? (value / total) * 100 : 0;
188
201
  return (
189
202
  <div className="flex items-center justify-between py-1.5">
@@ -192,7 +205,7 @@ function BreakdownRow({ label, value, total, color }: { label: string; value: nu
192
205
  <span className="text-xs" style={{ color: "var(--muted-text)" }}>{label}</span>
193
206
  </div>
194
207
  <div className="flex items-center gap-2">
195
- <span className="text-xs font-medium" style={{ color: "var(--page-text)" }}>{formatTokensShort(value)}</span>
208
+ <span className="text-xs font-medium" style={{ color: "var(--page-text)" }}>{formatTokensShort(value, lang)}</span>
196
209
  <span className="text-xs" style={{ color: "var(--subtle-text)" }}>({pct.toFixed(1)}%)</span>
197
210
  </div>
198
211
  </div>
@@ -202,7 +215,7 @@ function BreakdownRow({ label, value, total, color }: { label: string; value: nu
202
215
  // ─── Main Component ─────────────────────────────────────
203
216
 
204
217
  export function DashboardPage() {
205
- const { t } = useTranslation();
218
+ const { t, lang } = useTranslation();
206
219
  const { currency, toggle: toggleCurrency } = useCurrency();
207
220
  const { initialized } = useConfigStore();
208
221
  const [range, setRange] = useState<RangeKey>("today");
@@ -389,13 +402,13 @@ export function DashboardPage() {
389
402
  title={t("dashboard.total_tokens")}
390
403
  value={data.totalTokens.toLocaleString("en-US")}
391
404
  icon={<Activity className="h-4 w-4" style={{ color: "#3b82f6" }} />}
392
- subtitle={`≈ ${formatTokensShort(data.totalTokens)}`}
405
+ subtitle={`≈ ${formatTokensShort(data.totalTokens, lang)}`}
393
406
  >
394
407
  <div className="mt-3 space-y-0.5 border-t pt-3" style={{ borderColor: "var(--card-border)" }}>
395
- <BreakdownRow label={t("dashboard.input")} value={data.totalInput} total={data.totalTokens} color="#3b82f6" />
396
- <BreakdownRow label={t("dashboard.output")} value={data.totalOutput} total={data.totalTokens} color="#10b981" />
397
- <BreakdownRow label={t("dashboard.cache_create")} value={data.totalCacheWrite} total={data.totalTokens} color="#f59e0b" />
398
- <BreakdownRow label={t("dashboard.cache_hit")} value={data.totalCacheRead} total={data.totalTokens} color="#8b5cf6" />
408
+ <BreakdownRow label={t("dashboard.input")} value={data.totalInput} total={data.totalTokens} color="#3b82f6" lang={lang} />
409
+ <BreakdownRow label={t("dashboard.output")} value={data.totalOutput} total={data.totalTokens} color="#10b981" lang={lang} />
410
+ <BreakdownRow label={t("dashboard.cache_create")} value={data.totalCacheWrite} total={data.totalTokens} color="#f59e0b" lang={lang} />
411
+ <BreakdownRow label={t("dashboard.cache_hit")} value={data.totalCacheRead} total={data.totalTokens} color="#8b5cf6" lang={lang} />
399
412
  </div>
400
413
  </StatCard>
401
414
 
@@ -509,8 +522,8 @@ export function DashboardPage() {
509
522
  <td className="px-4 py-2.5 whitespace-nowrap" style={{ color: "var(--page-text)" }}>{r.timestamp}</td>
510
523
  <td className="px-4 py-2.5" style={{ color: "var(--page-text)" }}>{r.providerId}</td>
511
524
  <td className="px-4 py-2.5" style={{ color: "var(--page-text)" }}>{r.modelId}</td>
512
- <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(r.input)}</td>
513
- <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(r.output)}</td>
525
+ <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(r.input, lang)}</td>
526
+ <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(r.output, lang)}</td>
514
527
  <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{currency === "CNY" ? `¥${(r.cost * USD_TO_CNY).toFixed(4)}` : formatCostShort(r.cost)}</td>
515
528
  <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{r.requests}</td>
516
529
  <td className="px-4 py-2.5 text-center">
@@ -545,9 +558,9 @@ export function DashboardPage() {
545
558
  data.providerStats.map((p, i) => (
546
559
  <tr key={i} className="border-b" style={{ borderColor: "var(--card-border)" }}>
547
560
  <td className="px-4 py-2.5 font-medium" style={{ color: "var(--page-text)" }}>{p.providerId}</td>
548
- <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(p.totalTokens)}</td>
549
- <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(p.totalInput)}</td>
550
- <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(p.totalOutput)}</td>
561
+ <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(p.totalTokens, lang)}</td>
562
+ <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(p.totalInput, lang)}</td>
563
+ <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(p.totalOutput, lang)}</td>
551
564
  <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{currency === "CNY" ? `¥${(p.totalCost * USD_TO_CNY).toFixed(4)}` : formatCostShort(p.totalCost)}</td>
552
565
  <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{p.totalRequests}</td>
553
566
  <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{p.modelCount}</td>
@@ -579,9 +592,9 @@ export function DashboardPage() {
579
592
  <tr key={i} className="border-b" style={{ borderColor: "var(--card-border)" }}>
580
593
  <td className="px-4 py-2.5 font-medium" style={{ color: "var(--page-text)" }}>{m.modelId}</td>
581
594
  <td className="px-4 py-2.5" style={{ color: "var(--page-text)" }}>{m.providerId}</td>
582
- <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(m.totalTokens)}</td>
583
- <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(m.totalInput)}</td>
584
- <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(m.totalOutput)}</td>
595
+ <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(m.totalTokens, lang)}</td>
596
+ <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(m.totalInput, lang)}</td>
597
+ <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{formatTokensShort(m.totalOutput, lang)}</td>
585
598
  <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{currency === "CNY" ? `¥${(m.totalCost * USD_TO_CNY).toFixed(4)}` : formatCostShort(m.totalCost)}</td>
586
599
  <td className="px-4 py-2.5 text-right font-mono" style={{ color: "var(--page-text)" }}>{m.totalRequests}</td>
587
600
  </tr>
@@ -1,8 +1,6 @@
1
1
  import { NavLink } from "react-router-dom";
2
2
  import {
3
3
  LayoutDashboard,
4
- Box,
5
- Plug,
6
4
  Settings,
7
5
  Pi,
8
6
  History,
@@ -15,8 +13,6 @@ import { useState } from "react";
15
13
 
16
14
  const navItems = [
17
15
  { to: "/", icon: LayoutDashboard, key: "nav.dashboard" },
18
- { to: "/models", icon: Box, key: "nav.models" },
19
- { to: "/providers", icon: Plug, key: "nav.providers" },
20
16
  { to: "/sessions", icon: History, key: "nav.sessions" },
21
17
  { to: "/memory", icon: Brain, key: "nav.memory" },
22
18
  { to: "/settings", icon: Settings, key: "nav.settings" },
package/src/main.tsx CHANGED
@@ -103,4 +103,13 @@ createRoot(document.getElementById("root")!).render(
103
103
  <StrictMode>
104
104
  <Root />
105
105
  </StrictMode>
106
- );
106
+ );
107
+
108
+ // ─── PWA: register service worker ──────────────────────
109
+ if ("serviceWorker" in navigator) {
110
+ window.addEventListener("load", () => {
111
+ navigator.serviceWorker.register("/sw.js").catch((err) => {
112
+ console.warn("SW registration failed:", err);
113
+ });
114
+ });
115
+ }
@@ -421,7 +421,11 @@ export const useConfigStore = create<ConfigState>((set, get) => ({
421
421
  if (ok) {
422
422
  set({ modelsJson: updated });
423
423
  const { auth } = get();
424
- set({ allProviders: mergeProviders(auth ?? {}, updated) });
424
+ const newAllProviders = mergeProviders(auth ?? {}, updated);
425
+ const newAllModels = newAllProviders.flatMap((p) =>
426
+ p.models.map((m) => ({ ...m, providerId: p.id, providerName: p.name }))
427
+ );
428
+ set({ allProviders: newAllProviders, allModels: newAllModels });
425
429
  }
426
430
  },
427
431