@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.
Files changed (41) hide show
  1. package/README.ja.md +137 -0
  2. package/README.md +277 -0
  3. package/README.zh-CN.md +176 -0
  4. package/index.html +13 -0
  5. package/package.json +44 -0
  6. package/pi-package/index.ts +100 -0
  7. package/pi-package/skills/pi-web-switch/SKILL.md +60 -0
  8. package/public/pi.svg +4 -0
  9. package/server/pi-reader.ts +678 -0
  10. package/src/App.tsx +25 -0
  11. package/src/components/dashboard/DashboardPage.tsx +607 -0
  12. package/src/components/layout/AppShell.tsx +18 -0
  13. package/src/components/layout/Sidebar.tsx +116 -0
  14. package/src/components/models/ModelsPage.tsx +570 -0
  15. package/src/components/providers/ProvidersPage.tsx +466 -0
  16. package/src/components/sessions/MemoryPage.tsx +177 -0
  17. package/src/components/sessions/SessionsPage.tsx +347 -0
  18. package/src/components/settings/SettingsPage.tsx +351 -0
  19. package/src/components/ui/Badge.tsx +29 -0
  20. package/src/components/ui/EmptyState.tsx +20 -0
  21. package/src/components/ui/Modal.tsx +41 -0
  22. package/src/components/ui/StatCard.tsx +37 -0
  23. package/src/data/builtin-providers.ts +148 -0
  24. package/src/data/mock-config.ts +261 -0
  25. package/src/data/mock-usage.ts +153 -0
  26. package/src/index.css +217 -0
  27. package/src/lib/config.ts +56 -0
  28. package/src/lib/currency.ts +48 -0
  29. package/src/lib/i18n.tsx +98 -0
  30. package/src/lib/translations/en.ts +168 -0
  31. package/src/lib/translations/index.ts +14 -0
  32. package/src/lib/translations/ja.ts +158 -0
  33. package/src/lib/translations/zh-CN.ts +158 -0
  34. package/src/lib/translations/zh-TW.ts +158 -0
  35. package/src/lib/utils.ts +51 -0
  36. package/src/main.tsx +106 -0
  37. package/src/store/config-store.ts +459 -0
  38. package/src/types/index.ts +187 -0
  39. package/src/vite-env.d.ts +1 -0
  40. package/tsconfig.json +24 -0
  41. package/vite.config.ts +172 -0
@@ -0,0 +1,158 @@
1
+ // ─── Traditional Chinese Translations ───────────────────
2
+
3
+ const zhTW: Record<string, string> = {
4
+ "nav.dashboard": "儀表板",
5
+ "nav.models": "模型",
6
+ "nav.providers": "提供商",
7
+ "nav.sessions": "會話",
8
+ "nav.memory": "記憶",
9
+ "nav.settings": "設定",
10
+ "app.subtitle": "設定管理器",
11
+ "app.version": "pi-switch v0.1.0",
12
+
13
+ "dashboard.title": "使用統計",
14
+ "dashboard.requests_count": "{0} 次請求 · 總成本 {1}",
15
+ "dashboard.range.today": "當天",
16
+ "dashboard.range.7d": "7天",
17
+ "dashboard.range.30d": "30天",
18
+ "dashboard.range.custom": "自訂",
19
+ "dashboard.to": "至",
20
+ "dashboard.total_tokens": "消耗 Tokens",
21
+ "dashboard.total_requests": "總請求數",
22
+ "dashboard.total_cost": "總成本",
23
+ "dashboard.cache_hit_rate": "快取命中率",
24
+ "dashboard.input": "新增輸入",
25
+ "dashboard.output": "Output",
26
+ "dashboard.cache_create": "建立",
27
+ "dashboard.cache_hit": "命中",
28
+ "dashboard.api_calls": "API 呼叫次數",
29
+ "dashboard.usd": "USD",
30
+ "dashboard.usage_trend": "使用趨勢",
31
+ "dashboard.tokens_k": "Tokens (K)",
32
+ "dashboard.cost_label": "成本 ($)",
33
+ "dashboard.request_log": "請求日誌",
34
+ "dashboard.provider_stats": "Provider 統計",
35
+ "dashboard.model_stats": "Model 統計",
36
+ "dashboard.time": "時間",
37
+ "dashboard.provider": "供應商",
38
+ "dashboard.model": "計費模型",
39
+ "dashboard.cost": "總成本",
40
+ "dashboard.requests": "請求數",
41
+ "dashboard.status": "狀態",
42
+ "dashboard.no_data": "該時間範圍內無請求資料",
43
+ "dashboard.provider_stats_desc": "按供應商聚合的統計資料",
44
+ "dashboard.model_stats_desc": "按模型聚合的統計資料",
45
+
46
+ "models.title": "模型管理",
47
+ "models.search_placeholder": "搜尋模型...",
48
+ "models.filter_all": "全部提供商",
49
+ "models.add_model": "新增模型",
50
+ "models.edit_model": "編輯模型",
51
+ "models.delete_model": "刪除模型",
52
+ "models.no_models": "未找到模型",
53
+ "models.enabled": "已啟用",
54
+ "models.disabled": "已停用",
55
+ "models.enable": "啟用",
56
+ "models.disable": "停用",
57
+ "models.capabilities": "能力",
58
+ "models.reasoning": "推理",
59
+ "models.image_input": "圖片輸入",
60
+ "models.context_window": "上下文視窗",
61
+ "models.max_tokens": "最大 Token",
62
+ "models.cost_input": "輸入",
63
+ "models.cost_output": "輸出",
64
+ "models.cost_cache_read": "快取讀取",
65
+ "models.cost_cache_write": "快取寫入",
66
+ "models.save": "儲存",
67
+ "models.cancel": "取消",
68
+ "models.delete_confirm": "確定要刪除此模型嗎?",
69
+
70
+ "providers.title": "提供商管理",
71
+ "providers.add_provider": "新增提供商",
72
+ "providers.edit_provider": "編輯提供商",
73
+ "providers.delete_provider": "刪除提供商",
74
+ "providers.custom": "自訂",
75
+ "providers.builtin": "內建",
76
+ "providers.no_auth": "未設定",
77
+ "providers.configured": "已設定",
78
+ "providers.api_key": "API Key",
79
+ "providers.base_url": "基礎位址",
80
+ "providers.api_type": "API 類型",
81
+ "providers.headers": "請求頭",
82
+ "providers.auth_method": "認證方式",
83
+ "providers.save": "儲存",
84
+ "providers.cancel": "取消",
85
+ "providers.delete_confirm": "確定要刪除此提供商嗎?",
86
+ "providers.set_api_key": "設定 API Key",
87
+ "providers.remove_api_key": "移除 API Key",
88
+ "providers.models_count": "{0} 個模型",
89
+
90
+ "sessions.title": "會話列表",
91
+ "sessions.summary": "共 {0} 個會話,分佈在 {1} 個專案",
92
+ "sessions.filter_placeholder": "按專案名稱搜尋...",
93
+ "sessions.no_sessions": "未找到會話",
94
+ "sessions.last_active": "最近活躍 {0}",
95
+ "sessions.sessions_count": "{0} 個會話",
96
+ "sessions.messages": "{0} 條訊息",
97
+ "sessions.delete_title": "刪除會話",
98
+ "sessions.delete_confirm": "確定要刪除此會話嗎?",
99
+ "sessions.delete_permanent": "這將永久刪除磁碟上的會話檔案。",
100
+ "sessions.cancel": "取消",
101
+ "sessions.delete": "刪除",
102
+ "sessions.deleting": "刪除中...",
103
+ "sessions.protected": "近 3 天有更新,無法刪除",
104
+ "sessions.delete_failed": "刪除會話檔案失敗",
105
+ "sessions.delete_error": "刪除時發生錯誤",
106
+
107
+ "memory.title": "記憶檢視",
108
+ "memory.summary": "pi-hermes-memory · 共 {0} 行,{1} 個檔案",
109
+ "memory.no_memory": "未找到記憶檔案",
110
+ "memory.no_memory_desc": "未安裝 pi-hermes-memory 擴充功能",
111
+ "memory.project_memories": "專案記憶",
112
+ "memory.user_profile": "使用者輪廓",
113
+ "memory.failure_records": "故障記錄",
114
+ "memory.lines": "{0} 行",
115
+ "memory.updated": "更新於 {0}",
116
+
117
+ "settings.title": "設定",
118
+ "settings.defaults": "預設值",
119
+ "settings.default_provider": "預設提供商",
120
+ "settings.default_model": "預設模型",
121
+ "settings.default_thinking": "預設思考層級",
122
+ "settings.project_trust": "專案信任",
123
+ "settings.none": "— 無 —",
124
+ "settings.appearance": "外觀",
125
+ "settings.light": "淺色",
126
+ "settings.dark": "深色",
127
+ "settings.system": "跟隨系統",
128
+ "settings.hide_thinking": "隱藏思考過程",
129
+ "settings.enabled_models": "已啟用模型",
130
+ "settings.no_models_enabled": "暫無已啟用模型。請在「模型」頁面啟用。",
131
+ "settings.packages": "擴充功能與套件",
132
+ "settings.package_placeholder": "npm:package-name",
133
+ "settings.add": "新增",
134
+ "settings.no_packages": "未安裝任何套件",
135
+ "settings.import_export": "匯入與匯出",
136
+ "settings.export": "匯出設定",
137
+ "settings.import": "匯入設定",
138
+ "settings.import_error": "無效的設定檔案格式",
139
+ "settings.danger_zone": "危險區域",
140
+ "settings.reset": "重設為預設",
141
+ "settings.reset_title": "重設設定?",
142
+ "settings.reset_confirm": "此操作將重設所有設定、認證金鑰和自訂提供商為預設值。變更將直接寫入你的 pi 設定檔 (~/.pi/agent/)。此操作不可撤銷。",
143
+ "settings.cancel": "取消",
144
+ "settings.reset_action": "重設",
145
+
146
+ "loading.config": "正在載入 pi 設定...",
147
+ "loading.initializing": "正在初始化...",
148
+ "loading.error_title": "載入設定失敗",
149
+ "loading.retry": "重試",
150
+
151
+ "language.label": "語言",
152
+ "language.en": "English",
153
+ "language.zh-CN": "简体中文",
154
+ "language.zh-TW": "繁體中文",
155
+ "language.ja": "日本語",
156
+ };
157
+
158
+ export default zhTW;
@@ -0,0 +1,51 @@
1
+ export function formatTokens(tokens: number): string {
2
+ if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(2)}M`;
3
+ if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`;
4
+ return tokens.toString();
5
+ }
6
+
7
+ export const USD_TO_CNY = 7.2;
8
+
9
+ export function formatCost(cost: number, currency: "USD" | "CNY" = "USD"): string {
10
+ if (currency === "CNY") {
11
+ const cny = cost * USD_TO_CNY;
12
+ if (cny >= 100) return `¥${cny.toFixed(2)}`;
13
+ if (cny >= 1) return `¥${cny.toFixed(3)}`;
14
+ return `¥${cny.toFixed(4)}`;
15
+ }
16
+ if (cost >= 100) return `$${cost.toFixed(2)}`;
17
+ if (cost >= 1) return `$${cost.toFixed(3)}`;
18
+ if (cost >= 0.01) return `¢${(cost * 100).toFixed(1)}`;
19
+ return `$${cost.toFixed(4)}`;
20
+ }
21
+
22
+ export function formatDate(dateStr: string): string {
23
+ const d = new Date(dateStr + "T00:00:00");
24
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
25
+ }
26
+
27
+ export function formatDateFull(dateStr: string): string {
28
+ const d = new Date(dateStr + "T00:00:00");
29
+ return d.toLocaleDateString("en-US", {
30
+ weekday: "short",
31
+ month: "short",
32
+ day: "numeric",
33
+ year: "numeric",
34
+ });
35
+ }
36
+
37
+ export function formatNumber(n: number): string {
38
+ return n.toLocaleString("en-US");
39
+ }
40
+
41
+ export function clamp(value: number, min: number, max: number): number {
42
+ return Math.max(min, Math.min(max, value));
43
+ }
44
+
45
+ export function cn(...classes: (string | boolean | undefined | null)[]): string {
46
+ return classes.filter(Boolean).join(" ");
47
+ }
48
+
49
+ export function generateId(): string {
50
+ return Math.random().toString(36).substring(2, 10);
51
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,106 @@
1
+ import { StrictMode, useEffect } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { useConfigStore } from "@/store/config-store";
4
+ import { I18nProvider } from "@/lib/i18n";
5
+ import { useTranslation } from "@/lib/i18n";
6
+ import translations from "@/lib/translations";
7
+ import App from "@/App";
8
+ import "@/index.css";
9
+
10
+ // ─── Theme Sync ─────────────────────────────────────────
11
+ function useThemeSync(initialized: boolean) {
12
+ const theme = useConfigStore((s) => s.settings?.theme);
13
+
14
+ useEffect(() => {
15
+ if (!initialized) return;
16
+ const root = document.documentElement;
17
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
18
+
19
+ function applyTheme() {
20
+ root.classList.remove("dark", "light");
21
+ if (theme === "dark") {
22
+ root.classList.add("dark");
23
+ } else if (theme === "light") {
24
+ root.classList.add("light");
25
+ } else {
26
+ root.classList.add(mediaQuery.matches ? "dark" : "light");
27
+ }
28
+ }
29
+ applyTheme();
30
+
31
+ if (theme !== "light" && theme !== "dark") {
32
+ const handler = () => applyTheme();
33
+ mediaQuery.addEventListener("change", handler);
34
+ return () => mediaQuery.removeEventListener("change", handler);
35
+ }
36
+ }, [initialized, theme]);
37
+ }
38
+
39
+ function LoadingScreen() {
40
+ const { t } = useTranslation();
41
+ return (
42
+ <div className="flex h-screen items-center justify-center bg-gray-950">
43
+ <div className="flex flex-col items-center gap-4">
44
+ <div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-600 border-t-blue-500" />
45
+ <p className="text-sm text-gray-500">{t("loading.config")}</p>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ function ErrorScreen({ error, onRetry }: { error: string; onRetry: () => void }) {
52
+ const { t } = useTranslation();
53
+ return (
54
+ <div className="flex h-screen items-center justify-center bg-gray-950">
55
+ <div className="flex flex-col items-center gap-4 max-w-md text-center">
56
+ <p className="text-lg font-semibold text-red-400">{t("loading.error_title")}</p>
57
+ <p className="text-sm text-gray-400">{error}</p>
58
+ <button
59
+ onClick={onRetry}
60
+ className="mt-2 rounded-lg bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-500"
61
+ >
62
+ {t("loading.retry")}
63
+ </button>
64
+ </div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function InitGate({ children }: { children: React.ReactNode }) {
70
+ const initialized = useConfigStore((s) => s.initialized);
71
+ const loading = useConfigStore((s) => s.loading);
72
+ const error = useConfigStore((s) => s.error);
73
+ const init = useConfigStore((s) => s.init);
74
+
75
+ useThemeSync(initialized);
76
+
77
+ useEffect(() => {
78
+ init();
79
+ }, [init]);
80
+
81
+ if (!initialized || loading) {
82
+ return <LoadingScreen />;
83
+ }
84
+
85
+ if (error) {
86
+ return <ErrorScreen error={error} onRetry={init} />;
87
+ }
88
+
89
+ return <>{children}</>;
90
+ }
91
+
92
+ function Root() {
93
+ return (
94
+ <I18nProvider translations={translations}>
95
+ <InitGate>
96
+ <App />
97
+ </InitGate>
98
+ </I18nProvider>
99
+ );
100
+ }
101
+
102
+ createRoot(document.getElementById("root")!).render(
103
+ <StrictMode>
104
+ <Root />
105
+ </StrictMode>
106
+ );