@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,261 @@
1
+ import type { PiConfig, Provider, Model, PiSettings, PiAuth, PiModelsJson } from "@/types";
2
+
3
+ // ─── Built-in Providers ───────────────────────────────────
4
+
5
+ const BUILTIN_PROVIDERS: Provider[] = [
6
+ {
7
+ id: "anthropic",
8
+ name: "Anthropic",
9
+ type: "builtin",
10
+ api: "anthropic-messages",
11
+ hasAuth: true,
12
+ authMethod: "env",
13
+ models: [
14
+ { id: "claude-sonnet-4", name: "Claude 4 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: true },
15
+ { id: "claude-sonnet-4-5", name: "Claude 4.5 Sonnet", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: true },
16
+ { id: "claude-opus-4", name: "Claude 4 Opus", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 }, enabled: false },
17
+ { id: "claude-haiku-3-5", name: "Claude 3.5 Haiku", reasoning: false, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 }, enabled: true },
18
+ ],
19
+ },
20
+ {
21
+ id: "openai",
22
+ name: "OpenAI",
23
+ type: "builtin",
24
+ api: "openai-completions",
25
+ hasAuth: true,
26
+ authMethod: "env",
27
+ models: [
28
+ { id: "gpt-4o", name: "GPT-4o", reasoning: false, input: ["text", "image"], contextWindow: 128000, maxTokens: 16384, cost: { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 3.75 }, enabled: true },
29
+ { id: "gpt-4o-mini", name: "GPT-4o Mini", reasoning: false, input: ["text", "image"], contextWindow: 128000, maxTokens: 16384, cost: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.225 }, enabled: true },
30
+ { id: "gpt-5.1", name: "GPT-5.1", reasoning: true, input: ["text", "image"], contextWindow: 256000, maxTokens: 65536, cost: { input: 10, output: 40, cacheRead: 5, cacheWrite: 10 }, enabled: false },
31
+ { id: "o3-mini", name: "o3-mini", reasoning: true, input: ["text"], contextWindow: 200000, maxTokens: 100000, cost: { input: 1.1, output: 4.4, cacheRead: 0.55, cacheWrite: 1.65 }, enabled: false },
32
+ ],
33
+ },
34
+ {
35
+ id: "deepseek",
36
+ name: "DeepSeek",
37
+ type: "builtin",
38
+ api: "openai-completions",
39
+ hasAuth: true,
40
+ authMethod: "env",
41
+ models: [
42
+ { id: "deepseek-chat", name: "DeepSeek V3", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.27, output: 1.1, cacheRead: 0.07, cacheWrite: 0.27 }, enabled: true },
43
+ { id: "deepseek-reasoner", name: "DeepSeek R1", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 }, enabled: true },
44
+ ],
45
+ },
46
+ {
47
+ id: "opencode",
48
+ name: "OpenCode",
49
+ type: "builtin",
50
+ api: "openai-completions",
51
+ hasAuth: true,
52
+ authMethod: "file",
53
+ models: [
54
+ { id: "deepseek-v4-flash-free", name: "DeepSeek V4 Flash (Free)", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, enabled: true },
55
+ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.3, output: 0.6, cacheRead: 0.15, cacheWrite: 0.3 }, enabled: true },
56
+ ],
57
+ },
58
+ {
59
+ id: "opencode-go",
60
+ name: "OpenCode Go",
61
+ type: "builtin",
62
+ api: "openai-completions",
63
+ hasAuth: true,
64
+ authMethod: "file",
65
+ models: [
66
+ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.3, output: 0.6, cacheRead: 0.15, cacheWrite: 0.3 }, enabled: true },
67
+ { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 2, output: 8, cacheRead: 1, cacheWrite: 2 }, enabled: true },
68
+ { id: "glm-5.1", name: "GLM 5.1", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.5, output: 2, cacheRead: 0.25, cacheWrite: 0.5 }, enabled: true },
69
+ { id: "qwen3.7-max", name: "Qwen 3.7 Max", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 1.5, output: 6, cacheRead: 0.75, cacheWrite: 1.5 }, enabled: true },
70
+ { id: "mimo-v2.5", name: "MiMo V2.5", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 1.2, output: 4.8, cacheRead: 0.6, cacheWrite: 1.2 }, enabled: true },
71
+ ],
72
+ },
73
+ {
74
+ id: "sensenova",
75
+ name: "SenseNova",
76
+ type: "builtin",
77
+ api: "openai-completions",
78
+ hasAuth: true,
79
+ authMethod: "env",
80
+ models: [
81
+ { id: "glm-5.2", name: "GLM 5.2", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.8, output: 2.4, cacheRead: 0.4, cacheWrite: 0.8 }, enabled: true },
82
+ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.3, output: 0.6, cacheRead: 0.15, cacheWrite: 0.3 }, enabled: true },
83
+ ],
84
+ },
85
+ {
86
+ id: "google",
87
+ name: "Google Gemini",
88
+ type: "builtin",
89
+ api: "google-generative-ai",
90
+ hasAuth: true,
91
+ authMethod: "env",
92
+ models: [
93
+ { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", reasoning: false, input: ["text", "image"], contextWindow: 1048576, maxTokens: 8192, cost: { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0.15 }, enabled: true },
94
+ { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", reasoning: true, input: ["text", "image"], contextWindow: 1048576, maxTokens: 8192, cost: { input: 1.25, output: 10, cacheRead: 0.625, cacheWrite: 1.25 }, enabled: false },
95
+ ],
96
+ },
97
+ {
98
+ id: "openrouter",
99
+ name: "OpenRouter",
100
+ type: "builtin",
101
+ api: "openai-completions",
102
+ hasAuth: false,
103
+ authMethod: "none",
104
+ models: [
105
+ { id: "openrouter/anthropic/claude-sonnet-4", name: "Claude 4 Sonnet (OpenRouter)", reasoning: true, input: ["text", "image"], contextWindow: 200000, maxTokens: 8192, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, enabled: false },
106
+ { id: "openrouter/deepseek/deepseek-r1", name: "DeepSeek R1 (OpenRouter)", reasoning: true, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.55, output: 2.19, cacheRead: 0.14, cacheWrite: 0.55 }, enabled: false },
107
+ ],
108
+ },
109
+ {
110
+ id: "mistral",
111
+ name: "Mistral",
112
+ type: "builtin",
113
+ api: "mistral-conversations",
114
+ hasAuth: false,
115
+ authMethod: "none",
116
+ models: [
117
+ { id: "mistral-large", name: "Mistral Large", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 2, output: 6, cacheRead: 1, cacheWrite: 2 }, enabled: false },
118
+ ],
119
+ },
120
+ {
121
+ id: "github-copilot",
122
+ name: "GitHub Copilot",
123
+ type: "builtin",
124
+ api: "openai-completions",
125
+ hasAuth: false,
126
+ authMethod: "none",
127
+ models: [
128
+ { id: "copilot-gpt-4o", name: "Copilot GPT-4o", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 4096, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, enabled: false },
129
+ ],
130
+ },
131
+ {
132
+ id: "groq",
133
+ name: "Groq",
134
+ type: "builtin",
135
+ api: "openai-completions",
136
+ hasAuth: false,
137
+ authMethod: "none",
138
+ models: [
139
+ { id: "llama-3.3-70b", name: "Llama 3.3 70B", reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 8192, cost: { input: 0.59, output: 0.79, cacheRead: 0, cacheWrite: 0 }, enabled: false },
140
+ ],
141
+ },
142
+ ];
143
+
144
+ // ─── Default Settings ─────────────────────────────────────
145
+
146
+ export const DEFAULT_SETTINGS: PiSettings = {
147
+ lastChangelogVersion: "0.80.3",
148
+ defaultProvider: "sensenova",
149
+ defaultModel: "deepseek-v4-flash",
150
+ defaultThinkingLevel: "high",
151
+ defaultProjectTrust: "always",
152
+ theme: "light/dark",
153
+ hideThinkingBlock: true,
154
+ retry: { enabled: true },
155
+ packages: [
156
+ "npm:pi-hermes-memory",
157
+ "npm:@pi-unipi/notify",
158
+ "npm:context-mode",
159
+ "npm:pi-subagents",
160
+ "npm:pi-web-access",
161
+ "npm:pi-rtk-optimizer",
162
+ "npm:pi-puppeteer",
163
+ "npm:pi-intercom",
164
+ "npm:pi-prompt-template-model",
165
+ ],
166
+ terminal: { showTerminalProgress: true },
167
+ warnings: { anthropicExtraUsage: true },
168
+ treeFilterMode: "default",
169
+ doubleEscapeAction: "tree",
170
+ enabledModels: [
171
+ "opencode-go/deepseek-v4-flash",
172
+ "opencode-go/deepseek-v4-pro",
173
+ "opencode/deepseek-v4-flash-free",
174
+ "opencode-go/glm-5.1",
175
+ "opencode-go/qwen3.7-max",
176
+ "opencode-go/mimo-v2.5",
177
+ "sensenova/glm-5.2",
178
+ "sensenova/deepseek-v4-flash",
179
+ ],
180
+ };
181
+
182
+ // ─── Default Auth ─────────────────────────────────────────
183
+
184
+ export const DEFAULT_AUTH: PiAuth = {
185
+ opencode: {
186
+ type: "api_key",
187
+ key: "sk-hqW3wtminBBkiTbjR57V15MClbOoZutz1StHXtv64HU0tiICeRDiA3DfC9vb0NGW",
188
+ },
189
+ "opencode-go": {
190
+ type: "api_key",
191
+ key: "sk-hqW3wtminBBkiTbjR57V15MClbOoZutz1StHXtv64HU0tiICeRDiA3DfC9vb0NGW",
192
+ },
193
+ };
194
+
195
+ // ─── Default models.json (empty — no custom providers yet) ─
196
+
197
+ export const DEFAULT_MODELS_JSON: PiModelsJson = {
198
+ providers: {},
199
+ };
200
+
201
+ // ─── Full Config ──────────────────────────────────────────
202
+
203
+ export const DEFAULT_PI_CONFIG: PiConfig = {
204
+ settings: DEFAULT_SETTINGS,
205
+ auth: DEFAULT_AUTH,
206
+ modelsJson: DEFAULT_MODELS_JSON,
207
+ };
208
+
209
+ // ─── Helper: get builtin providers ────────────────────────
210
+
211
+ export function getBuiltinProviders(): Provider[] {
212
+ return BUILTIN_PROVIDERS;
213
+ }
214
+
215
+ // ─── Helper: merge custom providers from models.json ──────
216
+
217
+ export function getCustomProviders(modelsJson: PiModelsJson | null): Provider[] {
218
+ if (!modelsJson) return [];
219
+ return Object.entries(modelsJson.providers).map(([id, cfg]) => ({
220
+ id,
221
+ name: id.charAt(0).toUpperCase() + id.slice(1),
222
+ type: "custom" as const,
223
+ baseUrl: cfg.baseUrl,
224
+ api: cfg.api,
225
+ apiKey: cfg.apiKey,
226
+ authHeader: cfg.authHeader,
227
+ headers: cfg.headers,
228
+ compat: cfg.compat,
229
+ hasAuth: !!cfg.apiKey,
230
+ authMethod: (cfg.apiKey ? "file" : "none") as "file" | "none",
231
+ models: (cfg.models ?? []).map((m) => ({ ...m, enabled: true })),
232
+ }));
233
+ }
234
+
235
+ // ─── Helper: all providers merged ─────────────────────────
236
+
237
+ export function getAllProviders(config: PiConfig): Provider[] {
238
+ const builtins = getBuiltinProviders().map((p) => {
239
+ // Apply auth from settings
240
+ const authEntry = config.auth[p.id];
241
+ return {
242
+ ...p,
243
+ hasAuth: !!authEntry || p.hasAuth,
244
+ authMethod: authEntry ? "file" : p.authMethod,
245
+ };
246
+ });
247
+ const customs = getCustomProviders(config.modelsJson);
248
+ return [...builtins, ...customs];
249
+ }
250
+
251
+ // ─── Helper: all models flat ──────────────────────────────
252
+
253
+ export function getAllModels(config: PiConfig): (Model & { providerId: string; providerName: string })[] {
254
+ return getAllProviders(config).flatMap((p) =>
255
+ p.models.map((m) => ({
256
+ ...m,
257
+ providerId: p.id,
258
+ providerName: p.name,
259
+ }))
260
+ );
261
+ }
@@ -0,0 +1,153 @@
1
+ import type { UsageRecord } from "@/types";
2
+
3
+ // Generate 30 days of mock usage data
4
+ function generateMockUsage(): UsageRecord[] {
5
+ const records: UsageRecord[] = [];
6
+ const today = new Date();
7
+ const modelProviderPairs = [
8
+ { providerId: "opencode-go", modelId: "deepseek-v4-flash", baseTokens: 1_200_000, baseCost: 0.36 },
9
+ { providerId: "opencode-go", modelId: "deepseek-v4-pro", baseTokens: 400_000, baseCost: 0.80 },
10
+ { providerId: "opencode-go", modelId: "glm-5.1", baseTokens: 250_000, baseCost: 0.125 },
11
+ { providerId: "opencode-go", modelId: "qwen3.7-max", baseTokens: 180_000, baseCost: 0.27 },
12
+ { providerId: "opencode-go", modelId: "mimo-v2.5", baseTokens: 300_000, baseCost: 0.36 },
13
+ { providerId: "opencode", modelId: "deepseek-v4-flash-free", baseTokens: 800_000, baseCost: 0 },
14
+ { providerId: "opencode", modelId: "deepseek-v4-flash", baseTokens: 200_000, baseCost: 0.06 },
15
+ { providerId: "sensenova", modelId: "glm-5.2", baseTokens: 350_000, baseCost: 0.28 },
16
+ { providerId: "sensenova", modelId: "deepseek-v4-flash", baseTokens: 500_000, baseCost: 0.15 },
17
+ { providerId: "anthropic", modelId: "claude-sonnet-4", baseTokens: 150_000, baseCost: 0.45 },
18
+ { providerId: "anthropic", modelId: "claude-haiku-3-5", baseTokens: 90_000, baseCost: 0.036 },
19
+ { providerId: "openai", modelId: "gpt-4o", baseTokens: 80_000, baseCost: 0.20 },
20
+ { providerId: "openai", modelId: "gpt-4o-mini", baseTokens: 120_000, baseCost: 0.018 },
21
+ { providerId: "deepseek", modelId: "deepseek-chat", baseTokens: 60_000, baseCost: 0.0162 },
22
+ { providerId: "google", modelId: "gemini-2.5-flash", baseTokens: 40_000, baseCost: 0.006 },
23
+ ];
24
+
25
+ for (let day = 29; day >= 0; day--) {
26
+ const date = new Date(today);
27
+ date.setDate(date.getDate() - day);
28
+ const dateStr = date.toISOString().split("T")[0]!;
29
+
30
+ for (const pair of modelProviderPairs) {
31
+ // Add some daily variance (±40%)
32
+ const variance = 0.6 + Math.random() * 0.8;
33
+ const dayOfWeek = date.getDay();
34
+ const weekendFactor = dayOfWeek === 0 || dayOfWeek === 6 ? 0.5 : 1.0;
35
+ const trendFactor = 1 + (29 - day) * 0.01; // slight upward trend
36
+
37
+ const inputRatio = 0.7 + Math.random() * 0.1;
38
+ const baseTokens = pair.baseTokens * variance * weekendFactor * trendFactor;
39
+ const inputTokens = Math.round(baseTokens * inputRatio);
40
+ const outputTokens = Math.round(baseTokens * (1 - inputRatio));
41
+ const cacheReadTokens = Math.round(inputTokens * 0.3 * Math.random());
42
+ const cacheWriteTokens = Math.round(inputTokens * 0.1 * Math.random());
43
+ const requests = Math.round(20 + Math.random() * 60);
44
+
45
+ records.push({
46
+ date: dateStr!,
47
+ providerId: pair.providerId,
48
+ modelId: pair.modelId,
49
+ inputTokens,
50
+ outputTokens,
51
+ cacheReadTokens,
52
+ cacheWriteTokens,
53
+ requests,
54
+ cost: pair.baseCost * variance * weekendFactor * trendFactor,
55
+ });
56
+ }
57
+ }
58
+
59
+ return records;
60
+ }
61
+
62
+ export const MOCK_USAGE_DATA = generateMockUsage();
63
+
64
+ // Aggregate helpers
65
+
66
+ export function getDailyAggregates(): {
67
+ date: string;
68
+ totalTokens: number;
69
+ totalCost: number;
70
+ totalRequests: number;
71
+ inputTokens: number;
72
+ outputTokens: number;
73
+ }[] {
74
+ const daily = new Map<string, {
75
+ totalTokens: number; totalCost: number; totalRequests: number;
76
+ inputTokens: number; outputTokens: number;
77
+ }>();
78
+
79
+ for (const r of MOCK_USAGE_DATA) {
80
+ const d = daily.get(r.date) ?? {
81
+ totalTokens: 0, totalCost: 0, totalRequests: 0,
82
+ inputTokens: 0, outputTokens: 0,
83
+ };
84
+ d.totalTokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
85
+ d.totalCost += r.cost;
86
+ d.totalRequests += r.requests;
87
+ d.inputTokens += r.inputTokens;
88
+ d.outputTokens += r.outputTokens;
89
+ daily.set(r.date, d);
90
+ }
91
+
92
+ return Array.from(daily.entries())
93
+ .map(([date, agg]) => ({ date, ...agg }))
94
+ .sort((a, b) => a.date.localeCompare(b.date));
95
+ }
96
+
97
+ export function getProviderSummaries() {
98
+ const sums = new Map<string, {
99
+ totalTokens: number; totalCost: number; totalRequests: number;
100
+ }>();
101
+
102
+ for (const r of MOCK_USAGE_DATA) {
103
+ const s = sums.get(r.providerId) ?? { totalTokens: 0, totalCost: 0, totalRequests: 0 };
104
+ s.totalTokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
105
+ s.totalCost += r.cost;
106
+ s.totalRequests += r.requests;
107
+ sums.set(r.providerId, s);
108
+ }
109
+
110
+ return Array.from(sums.entries()).map(([providerId, s]) => ({ providerId, ...s }));
111
+ }
112
+
113
+ export function getModelSummaries() {
114
+ const sums = new Map<string, {
115
+ providerId: string; totalTokens: number; totalCost: number; totalRequests: number; count: number;
116
+ }>();
117
+
118
+ for (const r of MOCK_USAGE_DATA) {
119
+ const key = `${r.providerId}/${r.modelId}`;
120
+ const s = sums.get(key) ?? { providerId: r.providerId, totalTokens: 0, totalCost: 0, totalRequests: 0, count: 0 };
121
+ s.totalTokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
122
+ s.totalCost += r.cost;
123
+ s.totalRequests += r.requests;
124
+ s.count++;
125
+ sums.set(key, s);
126
+ }
127
+
128
+ return Array.from(sums.entries()).map(([key, s]) => {
129
+ const [providerId, modelId] = key.split("/");
130
+ return {
131
+ modelId: modelId!,
132
+ providerId: s.providerId,
133
+ totalTokens: s.totalTokens,
134
+ totalCost: s.totalCost,
135
+ totalRequests: s.totalRequests,
136
+ avgTokensPerRequest: Math.round(s.totalTokens / s.totalRequests),
137
+ };
138
+ });
139
+ }
140
+
141
+ export function getTotals() {
142
+ let totalTokens = 0;
143
+ let totalCost = 0;
144
+ let totalRequests = 0;
145
+
146
+ for (const r of MOCK_USAGE_DATA) {
147
+ totalTokens += r.inputTokens + r.outputTokens + r.cacheReadTokens + r.cacheWriteTokens;
148
+ totalCost += r.cost;
149
+ totalRequests += r.requests;
150
+ }
151
+
152
+ return { totalTokens, totalCost, totalRequests };
153
+ }
package/src/index.css ADDED
@@ -0,0 +1,217 @@
1
+ @import "tailwindcss";
2
+
3
+ /* ─── Theme Variables ──────────────────────────────────── */
4
+
5
+ :root,
6
+ .light {
7
+ --color-scheme: light;
8
+ --page-bg: #f9fafb;
9
+ --page-text: #111827;
10
+ --card-bg: #ffffff;
11
+ --card-border: #e5e7eb;
12
+ --card-header-border: #e5e7eb;
13
+ --card-header-text: #374151;
14
+ --input-bg: #ffffff;
15
+ --input-border: #d1d5db;
16
+ --input-text: #111827;
17
+ --muted-text: #6b7280;
18
+ --subtle-text: #9ca3af;
19
+ --accent-bg: #eff6ff;
20
+ --hover-bg: #f3f4f6;
21
+ --sidebar-bg: #ffffff;
22
+ --sidebar-border: #e5e7eb;
23
+ --sidebar-logo: #1e40af;
24
+ --sidebar-active-bg: #eff6ff;
25
+ --sidebar-active-text: #2563eb;
26
+ --sidebar-text: #6b7280;
27
+ --sidebar-hover-bg: #f3f4f6;
28
+ --sidebar-hover-text: #374151;
29
+ --scrollbar-thumb: #d1d5db;
30
+ --scrollbar-thumb-hover: #9ca3af;
31
+ }
32
+
33
+ .dark {
34
+ --color-scheme: dark;
35
+ --page-bg: #030712;
36
+ --page-text: #f3f4f6;
37
+ --card-bg: rgba(17, 24, 39, 0.5);
38
+ --card-border: #1f2937;
39
+ --card-header-border: #1f2937;
40
+ --card-header-text: #d1d5db;
41
+ --input-bg: #1f2937;
42
+ --input-border: #374151;
43
+ --input-text: #ffffff;
44
+ --muted-text: #9ca3af;
45
+ --subtle-text: #6b7280;
46
+ --accent-bg: rgba(37, 99, 235, 0.1);
47
+ --hover-bg: rgba(31, 41, 55, 0.5);
48
+ --sidebar-bg: #030712;
49
+ --sidebar-border: #1f2937;
50
+ --sidebar-logo: #2563eb;
51
+ --sidebar-active-bg: rgba(37, 99, 235, 0.1);
52
+ --sidebar-active-text: #60a5fa;
53
+ --sidebar-text: #9ca3af;
54
+ --sidebar-hover-bg: #1f2937;
55
+ --sidebar-hover-text: #e5e7eb;
56
+ --scrollbar-thumb: #4b5563;
57
+ --scrollbar-thumb-hover: #6b7280;
58
+ }
59
+
60
+ :root {
61
+ color-scheme: var(--color-scheme, dark);
62
+ }
63
+
64
+ body {
65
+ font-family: "Inter", "SF Pro", system-ui, -apple-system, sans-serif;
66
+ background-color: var(--page-bg);
67
+ color: var(--page-text);
68
+ }
69
+
70
+ /* Theme-aware utility classes */
71
+ .theme-card {
72
+ background-color: var(--card-bg);
73
+ border-color: var(--card-border);
74
+ }
75
+
76
+ .theme-input {
77
+ background-color: var(--input-bg);
78
+ border-color: var(--input-border);
79
+ color: var(--input-text);
80
+ }
81
+
82
+ /* Custom scrollbar */
83
+ ::-webkit-scrollbar {
84
+ width: 6px;
85
+ height: 6px;
86
+ }
87
+ ::-webkit-scrollbar-track {
88
+ background: transparent;
89
+ }
90
+ ::-webkit-scrollbar-thumb {
91
+ background: var(--scrollbar-thumb);
92
+ border-radius: 3px;
93
+ }
94
+ ::-webkit-scrollbar-thumb:hover {
95
+ background: var(--scrollbar-thumb-hover);
96
+ }
97
+
98
+ /* Smooth transitions for theme toggle */
99
+ * {
100
+ transition: background-color 0.15s ease, border-color 0.15s ease;
101
+ }
102
+
103
+ /* ─── Light Mode Overrides ──────────────────────────────
104
+ Remap hardcoded Tailwind dark-mode classes to light colors.
105
+ This is needed because most components use fixed classes
106
+ like text-white, text-gray-400, bg-gray-950, border-gray-800, etc. */
107
+
108
+ .light .text-white,
109
+ .light .text-gray-100 {
110
+ color: #111827;
111
+ }
112
+ .light .text-gray-200,
113
+ .light .text-gray-300 {
114
+ color: #374151;
115
+ }
116
+ .light .text-gray-400 {
117
+ color: #6b7280;
118
+ }
119
+ .light .text-gray-500,
120
+ .light .text-gray-600 {
121
+ color: #9ca3af;
122
+ }
123
+ .light .bg-gray-950,
124
+ .light .bg-gray-900,
125
+ .light .bg-gray-900\/50,
126
+ .light .bg-gray-900\/30,
127
+ .light .bg-gray-900\/70 {
128
+ background-color: #ffffff;
129
+ }
130
+ .light .bg-gray-800,
131
+ .light .bg-gray-800\/30,
132
+ .light .bg-gray-800\/50,
133
+ .light .bg-gray-800\/20 {
134
+ background-color: #f3f4f6;
135
+ }
136
+ .light .bg-black\/60 {
137
+ background-color: rgba(107, 114, 128, 0.5);
138
+ }
139
+ .light .border-gray-800,
140
+ .light .border-gray-800\/50,
141
+ .light .border-gray-700 {
142
+ border-color: #e5e7eb;
143
+ }
144
+ .light .border-gray-700 {
145
+ border-color: #d1d5db;
146
+ }
147
+ .light .border-gray-600 {
148
+ border-color: #d1d5db;
149
+ }
150
+ .light .text-blue-400 {
151
+ color: #2563eb;
152
+ }
153
+ .light .bg-blue-600 {
154
+ background-color: #2563eb;
155
+ }
156
+ .light .bg-blue-500\/10,
157
+ .light .bg-blue-600\/10 {
158
+ background-color: #eff6ff;
159
+ }
160
+ .light .text-emerald-400 {
161
+ color: #059669;
162
+ }
163
+ .light .text-amber-400 {
164
+ color: #d97706;
165
+ }
166
+ .light .text-purple-400 {
167
+ color: #7c3aed;
168
+ }
169
+ .light .text-red-400 {
170
+ color: #dc2626;
171
+ }
172
+ .light .bg-red-600 {
173
+ background-color: #dc2626;
174
+ }
175
+ .light .bg-red-950\/20 {
176
+ background-color: #fef2f2;
177
+ }
178
+ .light .border-red-700,
179
+ .light .border-red-900\/30 {
180
+ border-color: #fca5a5;
181
+ }
182
+ .light .bg-emerald-500\/10 {
183
+ background-color: #f0fdf4;
184
+ }
185
+ .light .text-emerald-400 {
186
+ color: #059669;
187
+ }
188
+ .light .bg-amber-500\/10 {
189
+ background-color: #fffbeb;
190
+ }
191
+ .light .hover\:bg-gray-800 {
192
+ &:hover {
193
+ background-color: #e5e7eb;
194
+ }
195
+ }
196
+ .light .hover\:bg-gray-700 {
197
+ &:hover {
198
+ background-color: #d1d5db;
199
+ }
200
+ }
201
+ .light .hover\:text-gray-200 {
202
+ &:hover {
203
+ color: #374151;
204
+ }
205
+ }
206
+ .light .placeholder-gray-500 {
207
+ &::placeholder {
208
+ color: #9ca3af;
209
+ }
210
+ }
211
+ .light input,
212
+ .light select,
213
+ .light textarea {
214
+ background-color: #ffffff;
215
+ border-color: #d1d5db;
216
+ color: #111827;
217
+ }
@@ -0,0 +1,56 @@
1
+ // ─── Config persistence helpers ──────────────────────────
2
+ // Pi config is now read/written directly to ~/.pi/agent/ via the API.
3
+ // This file provides localStorage-based import/export for backup.
4
+
5
+ import type { PiConfig, ExportPayload } from "@/types";
6
+
7
+ const STORAGE_KEY = "pi-web-switch-config";
8
+
9
+ export function saveLocalBackup(config: PiConfig): void {
10
+ try {
11
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(config, null, 2));
12
+ } catch {
13
+ // localStorage might be full
14
+ }
15
+ }
16
+
17
+ export function loadLocalBackup(): PiConfig | null {
18
+ try {
19
+ const raw = localStorage.getItem(STORAGE_KEY);
20
+ if (raw) return JSON.parse(raw) as PiConfig;
21
+ } catch {
22
+ // ignore
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export function exportConfig(config: PiConfig, filename?: string): void {
28
+ const payload: ExportPayload = {
29
+ version: "0.1.0",
30
+ exportedAt: new Date().toISOString(),
31
+ config,
32
+ };
33
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
34
+ const url = URL.createObjectURL(blob);
35
+ const a = document.createElement("a");
36
+ a.href = url;
37
+ a.download = filename ?? `pi-web-switch-config-${new Date().toISOString().split("T")[0]}.json`;
38
+ a.click();
39
+ URL.revokeObjectURL(url);
40
+ }
41
+
42
+ export function parseImportFile(json: string): PiConfig | null {
43
+ try {
44
+ const payload = JSON.parse(json) as ExportPayload;
45
+ if (payload.config?.settings && payload.config?.auth) {
46
+ return payload.config;
47
+ }
48
+ // Try raw config
49
+ if (json.includes("defaultProvider")) {
50
+ return JSON.parse(json) as PiConfig;
51
+ }
52
+ return null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }