@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.
- package/README.ja.md +137 -0
- package/README.md +277 -0
- package/README.zh-CN.md +176 -0
- package/index.html +13 -0
- package/package.json +44 -0
- package/pi-package/index.ts +100 -0
- package/pi-package/skills/pi-web-switch/SKILL.md +60 -0
- package/public/pi.svg +4 -0
- package/server/pi-reader.ts +678 -0
- package/src/App.tsx +25 -0
- package/src/components/dashboard/DashboardPage.tsx +607 -0
- package/src/components/layout/AppShell.tsx +18 -0
- package/src/components/layout/Sidebar.tsx +116 -0
- package/src/components/models/ModelsPage.tsx +570 -0
- package/src/components/providers/ProvidersPage.tsx +466 -0
- package/src/components/sessions/MemoryPage.tsx +177 -0
- package/src/components/sessions/SessionsPage.tsx +347 -0
- package/src/components/settings/SettingsPage.tsx +351 -0
- package/src/components/ui/Badge.tsx +29 -0
- package/src/components/ui/EmptyState.tsx +20 -0
- package/src/components/ui/Modal.tsx +41 -0
- package/src/components/ui/StatCard.tsx +37 -0
- package/src/data/builtin-providers.ts +148 -0
- package/src/data/mock-config.ts +261 -0
- package/src/data/mock-usage.ts +153 -0
- package/src/index.css +217 -0
- package/src/lib/config.ts +56 -0
- package/src/lib/currency.ts +48 -0
- package/src/lib/i18n.tsx +98 -0
- package/src/lib/translations/en.ts +168 -0
- package/src/lib/translations/index.ts +14 -0
- package/src/lib/translations/ja.ts +158 -0
- package/src/lib/translations/zh-CN.ts +158 -0
- package/src/lib/translations/zh-TW.ts +158 -0
- package/src/lib/utils.ts +51 -0
- package/src/main.tsx +106 -0
- package/src/store/config-store.ts +459 -0
- package/src/types/index.ts +187 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +24 -0
- 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
|
+
}
|