@polderlabs/bizar 4.4.11 → 4.4.12
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/bizar-dash/dist/assets/{main-Nq8Dq3VR.css → main-BKXEqU1w.css} +1 -1
- package/bizar-dash/dist/assets/main-EK_fzXn_.js +352 -0
- package/bizar-dash/dist/assets/main-EK_fzXn_.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +1 -1
- package/bizar-dash/dist/index.html +2 -2
- package/bizar-dash/src/server/api.mjs +2 -0
- package/bizar-dash/src/server/minimax.mjs +352 -0
- package/bizar-dash/src/server/routes/_shared.mjs +15 -0
- package/bizar-dash/src/server/routes/minimax.mjs +140 -0
- package/bizar-dash/src/web/App.tsx +2 -0
- package/bizar-dash/src/web/components/Topbar.tsx +2 -0
- package/bizar-dash/src/web/lib/types.ts +12 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +371 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +543 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-CEazNxxy.js +0 -347
- package/bizar-dash/dist/assets/main-CEazNxxy.js.map +0 -1
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
// src/views/MiniMaxUsage.tsx — v4.5.0
|
|
2
|
+
//
|
|
3
|
+
// Token Plan usage tracking dashboard.
|
|
4
|
+
//
|
|
5
|
+
// Renders the user's current 5-hour rolling + weekly remaining quota
|
|
6
|
+
// per model, fetched from https://www.minimax.io/v1/token_plan/remains.
|
|
7
|
+
// The Subscription Key is set inline on this page (no separate Settings
|
|
8
|
+
// trip needed) — the value is written to settings.json under
|
|
9
|
+
// `minimax.apiKey` and read back on next load.
|
|
10
|
+
|
|
11
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
12
|
+
import {
|
|
13
|
+
Coins,
|
|
14
|
+
RefreshCw,
|
|
15
|
+
AlertTriangle,
|
|
16
|
+
CheckCircle2,
|
|
17
|
+
KeyRound,
|
|
18
|
+
Send,
|
|
19
|
+
Loader2,
|
|
20
|
+
Calendar,
|
|
21
|
+
Activity,
|
|
22
|
+
Save,
|
|
23
|
+
Eye,
|
|
24
|
+
EyeOff,
|
|
25
|
+
} from 'lucide-react';
|
|
26
|
+
import { Card, CardTitle, CardMeta } from '../components/Card';
|
|
27
|
+
import { Button } from '../components/Button';
|
|
28
|
+
import { Spinner } from '../components/Spinner';
|
|
29
|
+
import { useToast } from '../components/Toast';
|
|
30
|
+
import { api } from '../lib/api';
|
|
31
|
+
import { cn } from '../lib/utils';
|
|
32
|
+
import type { Settings, Snapshot } from '../lib/types';
|
|
33
|
+
|
|
34
|
+
type Props = {
|
|
35
|
+
snapshot: Snapshot;
|
|
36
|
+
settings: Settings;
|
|
37
|
+
activeTab: string;
|
|
38
|
+
setActiveTab: (id: string) => void;
|
|
39
|
+
refreshSnapshot: () => Promise<void>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type RemainsModel = {
|
|
43
|
+
model_name: string;
|
|
44
|
+
current_interval_remaining_percent: number;
|
|
45
|
+
current_weekly_remaining_percent: number;
|
|
46
|
+
current_interval_consumed_percent?: number;
|
|
47
|
+
weekly_consumed_percent?: number;
|
|
48
|
+
current_interval_status: number;
|
|
49
|
+
current_weekly_status: number;
|
|
50
|
+
current_interval_total_count: number;
|
|
51
|
+
current_interval_usage_count: number;
|
|
52
|
+
current_weekly_total_count: number;
|
|
53
|
+
current_weekly_usage_count: number;
|
|
54
|
+
endTimeISO?: string;
|
|
55
|
+
weeklyEndTimeISO?: string;
|
|
56
|
+
intervalResetInMs?: number;
|
|
57
|
+
weeklyResetInMs?: number;
|
|
58
|
+
intervalResetInHuman?: string;
|
|
59
|
+
weeklyResetInHuman?: string;
|
|
60
|
+
intervalUsed?: number;
|
|
61
|
+
intervalTotal?: number;
|
|
62
|
+
weeklyUsed?: number;
|
|
63
|
+
weeklyTotal?: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type RemainsSnapshot = {
|
|
67
|
+
ok: boolean;
|
|
68
|
+
cached?: boolean;
|
|
69
|
+
fetchedAt?: number;
|
|
70
|
+
apiKeyHint?: string;
|
|
71
|
+
groupId?: string;
|
|
72
|
+
baseUrl?: string;
|
|
73
|
+
models?: RemainsModel[];
|
|
74
|
+
baseResp?: { status_code: number; status_msg: string };
|
|
75
|
+
error?: string;
|
|
76
|
+
message?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type Status = {
|
|
80
|
+
enabled: boolean;
|
|
81
|
+
configured: boolean;
|
|
82
|
+
apiKeyHint: string;
|
|
83
|
+
groupId: string;
|
|
84
|
+
baseUrl: string;
|
|
85
|
+
chatBaseUrl: string;
|
|
86
|
+
cache: { fetchedAt: number; apiKeyHint: string; modelCount: number } | null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type TestResult = {
|
|
90
|
+
ok: boolean;
|
|
91
|
+
error?: string;
|
|
92
|
+
message?: string;
|
|
93
|
+
model?: string;
|
|
94
|
+
content?: string;
|
|
95
|
+
reasoning?: string;
|
|
96
|
+
finishReason?: string;
|
|
97
|
+
usage?: {
|
|
98
|
+
total_tokens?: number;
|
|
99
|
+
prompt_tokens?: number;
|
|
100
|
+
completion_tokens?: number;
|
|
101
|
+
prompt_tokens_details?: { cached_tokens?: number };
|
|
102
|
+
};
|
|
103
|
+
status?: number;
|
|
104
|
+
raw?: unknown;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export function MiniMaxUsage({ snapshot, settings, setActiveTab, refreshSnapshot }: Props) {
|
|
108
|
+
const toast = useToast();
|
|
109
|
+
const [status, setStatus] = useState<Status | null>(null);
|
|
110
|
+
const [remains, setRemains] = useState<RemainsSnapshot | null>(null);
|
|
111
|
+
const [loading, setLoading] = useState(true);
|
|
112
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
113
|
+
const [error, setError] = useState<string | null>(null);
|
|
114
|
+
const [testResult, setTestResult] = useState<TestResult | null>(null);
|
|
115
|
+
const [testing, setTesting] = useState(false);
|
|
116
|
+
// Inline key config — saves a trip to Settings.
|
|
117
|
+
const [keyDraft, setKeyDraft] = useState('');
|
|
118
|
+
const [showKey, setShowKey] = useState(false);
|
|
119
|
+
const [savingKey, setSavingKey] = useState(false);
|
|
120
|
+
|
|
121
|
+
const load = useCallback(async () => {
|
|
122
|
+
setLoading(true);
|
|
123
|
+
setError(null);
|
|
124
|
+
try {
|
|
125
|
+
const [s, r] = await Promise.all([
|
|
126
|
+
api.get<Status>('/minimax/status'),
|
|
127
|
+
api.get<RemainsSnapshot>('/minimax/remains'),
|
|
128
|
+
]);
|
|
129
|
+
setStatus(s);
|
|
130
|
+
setRemains(r);
|
|
131
|
+
setKeyDraft(s.configured ? '' : ''); // never prefill — security
|
|
132
|
+
} catch (err) {
|
|
133
|
+
setError((err as Error).message || 'Failed to load MiniMax data');
|
|
134
|
+
} finally {
|
|
135
|
+
setLoading(false);
|
|
136
|
+
}
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
useEffect(() => { load(); }, [load]);
|
|
140
|
+
|
|
141
|
+
const saveKey = useCallback(async () => {
|
|
142
|
+
if (!keyDraft.trim()) {
|
|
143
|
+
toast.error('Paste a Subscription Key first.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
setSavingKey(true);
|
|
147
|
+
try {
|
|
148
|
+
const r = await api.put<{ minimax: { apiKey: string } }>('/settings', {
|
|
149
|
+
minimax: {
|
|
150
|
+
...(settings.minimax || {}),
|
|
151
|
+
apiKey: keyDraft.trim(),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
// Wipe the in-memory draft + clear the remains cache so the
|
|
155
|
+
// next load picks up the new key.
|
|
156
|
+
setKeyDraft('');
|
|
157
|
+
clearRemainsCacheClient();
|
|
158
|
+
toast.success('Subscription Key saved. Refreshing…');
|
|
159
|
+
void load();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
toast.error(`Save failed: ${(err as Error).message}`);
|
|
162
|
+
} finally {
|
|
163
|
+
setSavingKey(false);
|
|
164
|
+
}
|
|
165
|
+
}, [keyDraft, settings, toast, load]);
|
|
166
|
+
|
|
167
|
+
// Clears the on-disk + in-memory cache via the dashboard's
|
|
168
|
+
// /api/minimax/cache DELETE endpoint. Wrapped in a helper so the
|
|
169
|
+
// success path can await the round-trip.
|
|
170
|
+
const clearRemainsCacheClient = useCallback(async () => {
|
|
171
|
+
try { await api.del('/minimax/cache'); } catch { /* best-effort */ }
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
const refresh = useCallback(async () => {
|
|
175
|
+
setRefreshing(true);
|
|
176
|
+
setError(null);
|
|
177
|
+
try {
|
|
178
|
+
await api.post('/minimax/remains/refresh');
|
|
179
|
+
await load();
|
|
180
|
+
toast.success('Refreshed');
|
|
181
|
+
} catch (err) {
|
|
182
|
+
toast.error(`Refresh failed: ${(err as Error).message}`);
|
|
183
|
+
} finally {
|
|
184
|
+
setRefreshing(false);
|
|
185
|
+
}
|
|
186
|
+
}, [load, toast]);
|
|
187
|
+
|
|
188
|
+
const sendTest = useCallback(async () => {
|
|
189
|
+
setTesting(true);
|
|
190
|
+
setTestResult(null);
|
|
191
|
+
try {
|
|
192
|
+
const r = await api.post<TestResult>('/minimax/test', {
|
|
193
|
+
prompt: 'Reply with a single word: pong',
|
|
194
|
+
model: 'MiniMax-M3',
|
|
195
|
+
maxTokens: 16,
|
|
196
|
+
});
|
|
197
|
+
setTestResult(r);
|
|
198
|
+
if (r.ok) {
|
|
199
|
+
toast.success(`Test ok — used ${r.usage?.total_tokens ?? '?'} tokens`);
|
|
200
|
+
} else {
|
|
201
|
+
toast.error(`Test failed: ${r.message ?? r.error ?? 'unknown'}`);
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
toast.error(`Test request failed: ${(err as Error).message}`);
|
|
205
|
+
} finally {
|
|
206
|
+
setTesting(false);
|
|
207
|
+
}
|
|
208
|
+
}, [toast]);
|
|
209
|
+
|
|
210
|
+
if (loading && !status) {
|
|
211
|
+
return (
|
|
212
|
+
<div className="view-container">
|
|
213
|
+
<Spinner /> Loading MiniMax data…
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!status) {
|
|
219
|
+
return (
|
|
220
|
+
<div className="view-container">
|
|
221
|
+
<div className="error-card">
|
|
222
|
+
<AlertTriangle size={20} />
|
|
223
|
+
<div>
|
|
224
|
+
<strong>Couldn't load MiniMax status</strong>
|
|
225
|
+
<pre>{error ?? 'unknown error'}</pre>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div className="view-container view-minimax-usage">
|
|
234
|
+
{/* ── Header ─────────────────────────────────────────────────── */}
|
|
235
|
+
<header className="view-header">
|
|
236
|
+
<div className="view-header-titles">
|
|
237
|
+
<h1 className="view-title">
|
|
238
|
+
<Coins size={20} style={{ verticalAlign: 'text-bottom', marginRight: 6 }} />
|
|
239
|
+
MiniMax Token Plan
|
|
240
|
+
</h1>
|
|
241
|
+
<p className="view-subtitle">
|
|
242
|
+
Remaining 5-hour + weekly quota per model. Fetches live from{' '}
|
|
243
|
+
<code>www.minimax.io/v1/token_plan/remains</code>.
|
|
244
|
+
</p>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="view-header-actions">
|
|
247
|
+
<Button
|
|
248
|
+
variant="secondary"
|
|
249
|
+
size="sm"
|
|
250
|
+
onClick={() => setActiveTab('settings')}
|
|
251
|
+
title="Configure your Subscription Key"
|
|
252
|
+
>
|
|
253
|
+
<KeyRound size={14} /> {status.configured ? `Key ${status.apiKeyHint}` : 'Add key'}
|
|
254
|
+
</Button>
|
|
255
|
+
<Button variant="secondary" size="sm" onClick={refresh} disabled={refreshing}>
|
|
256
|
+
{refreshing ? <Loader2 size={14} className="spin" /> : <RefreshCw size={14} />}
|
|
257
|
+
Refresh
|
|
258
|
+
</Button>
|
|
259
|
+
</div>
|
|
260
|
+
</header>
|
|
261
|
+
|
|
262
|
+
{/* ── Status banners ─────────────────────────────────────────── */}
|
|
263
|
+
{!status.enabled && (
|
|
264
|
+
<div className="banner banner-warn">
|
|
265
|
+
<AlertTriangle size={16} />
|
|
266
|
+
<span>
|
|
267
|
+
MiniMax integration is <strong>disabled</strong> in settings. Enable it in Settings → MiniMax.
|
|
268
|
+
</span>
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{/* ── Subscription Key config (inline) ──────────────────── */}
|
|
273
|
+
{status.enabled && (
|
|
274
|
+
<Card id="minimax-key">
|
|
275
|
+
<CardTitle>
|
|
276
|
+
<KeyRound size={14} /> Subscription Key
|
|
277
|
+
</CardTitle>
|
|
278
|
+
<CardMeta>
|
|
279
|
+
Get it from{' '}
|
|
280
|
+
<a
|
|
281
|
+
href="https://platform.minimax.io/user-center/payment/token-plan"
|
|
282
|
+
target="_blank"
|
|
283
|
+
rel="noreferrer"
|
|
284
|
+
className="minimax-link"
|
|
285
|
+
>
|
|
286
|
+
platform.minimax.io/user-center/payment/token-plan
|
|
287
|
+
</a>
|
|
288
|
+
. Stored locally in <code>~/.config/bizar/settings.json</code>;
|
|
289
|
+
never sent to any other host. Status:{' '}
|
|
290
|
+
{status.configured ? (
|
|
291
|
+
<strong className="is-ok">configured ({status.apiKeyHint})</strong>
|
|
292
|
+
) : (
|
|
293
|
+
<strong className="is-warn">not configured</strong>
|
|
294
|
+
)}
|
|
295
|
+
.
|
|
296
|
+
</CardMeta>
|
|
297
|
+
<div className="minimax-key-row">
|
|
298
|
+
<div className="minimax-key-input-wrap">
|
|
299
|
+
<KeyRound size={12} className="minimax-key-icon" />
|
|
300
|
+
<input
|
|
301
|
+
type={showKey ? 'text' : 'password'}
|
|
302
|
+
value={keyDraft}
|
|
303
|
+
onChange={(e) => setKeyDraft(e.target.value)}
|
|
304
|
+
placeholder={status.configured ? '•••• paste a new key to replace' : 'eyJhbGciOi...'}
|
|
305
|
+
spellCheck={false}
|
|
306
|
+
autoComplete="off"
|
|
307
|
+
className="minimax-key-input"
|
|
308
|
+
/>
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
onClick={() => setShowKey((v) => !v)}
|
|
312
|
+
className="minimax-key-toggle"
|
|
313
|
+
aria-label={showKey ? 'Hide key' : 'Show key'}
|
|
314
|
+
title={showKey ? 'Hide key' : 'Show key'}
|
|
315
|
+
>
|
|
316
|
+
{showKey ? <EyeOff size={13} /> : <Eye size={13} />}
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
<Button onClick={saveKey} disabled={savingKey || !keyDraft.trim()} size="sm">
|
|
320
|
+
{savingKey ? <Loader2 size={14} className="spin" /> : <Save size={14} />}
|
|
321
|
+
Save key
|
|
322
|
+
</Button>
|
|
323
|
+
</div>
|
|
324
|
+
</Card>
|
|
325
|
+
)}
|
|
326
|
+
|
|
327
|
+
{remains && !remains.ok && (
|
|
328
|
+
<div className="banner banner-err">
|
|
329
|
+
<AlertTriangle size={16} />
|
|
330
|
+
<span>
|
|
331
|
+
<strong>Couldn't load quota</strong>: {remains.message ?? remains.error ?? 'unknown error'}
|
|
332
|
+
</span>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
{/* ── Summary stats ─────────────────────────────────────────── */}
|
|
337
|
+
{remains?.ok && remains.models && (
|
|
338
|
+
<>
|
|
339
|
+
<div className="minimax-stats-row">
|
|
340
|
+
<Stat
|
|
341
|
+
label="Models tracked"
|
|
342
|
+
value={String(remains.models.length)}
|
|
343
|
+
hint="Distinct model quotas returned by the API"
|
|
344
|
+
/>
|
|
345
|
+
<Stat
|
|
346
|
+
label="Last fetched"
|
|
347
|
+
value={remains.fetchedAt ? relativeTime(remains.fetchedAt) : '—'}
|
|
348
|
+
hint="Refreshed on demand + every 60s"
|
|
349
|
+
/>
|
|
350
|
+
<Stat
|
|
351
|
+
label="Group"
|
|
352
|
+
value={remains.groupId ?? '—'}
|
|
353
|
+
hint="Usually 'default' for an individual team"
|
|
354
|
+
/>
|
|
355
|
+
<Stat
|
|
356
|
+
label="API key"
|
|
357
|
+
value={remains.apiKeyHint ?? '—'}
|
|
358
|
+
hint="Masked; the real key never leaves settings.json"
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{/* ── Per-model quota cards ──────────────────────────────── */}
|
|
363
|
+
<div className="minimax-models-grid">
|
|
364
|
+
{remains.models.map((m) => (
|
|
365
|
+
<ModelQuotaCard key={m.model_name} model={m} />
|
|
366
|
+
))}
|
|
367
|
+
</div>
|
|
368
|
+
</>
|
|
369
|
+
)}
|
|
370
|
+
|
|
371
|
+
{/* ── Test prompt card ─────────────────────────────────────── */}
|
|
372
|
+
{status.configured && (
|
|
373
|
+
<Card id="minimax-test">
|
|
374
|
+
<CardTitle>
|
|
375
|
+
<Send size={14} /> Test the API key
|
|
376
|
+
</CardTitle>
|
|
377
|
+
<CardMeta>
|
|
378
|
+
Sends a one-shot chat completion to{' '}
|
|
379
|
+
<code>{status.chatBaseUrl}/chat/completions</code> to verify the
|
|
380
|
+
key works and surface live token usage.
|
|
381
|
+
</CardMeta>
|
|
382
|
+
<div className="minimax-test-actions">
|
|
383
|
+
<Button onClick={sendTest} disabled={testing} variant="primary" size="sm">
|
|
384
|
+
{testing ? <Loader2 size={14} className="spin" /> : <Send size={14} />}
|
|
385
|
+
Send test prompt
|
|
386
|
+
</Button>
|
|
387
|
+
<code className="minimax-test-prompt">"Reply with a single word: pong"</code>
|
|
388
|
+
</div>
|
|
389
|
+
{testResult && (
|
|
390
|
+
<div className={cn('minimax-test-result', testResult.ok ? 'ok' : 'err')}>
|
|
391
|
+
{testResult.ok ? <CheckCircle2 size={14} /> : <AlertTriangle size={14} />}
|
|
392
|
+
<div className="minimax-test-body">
|
|
393
|
+
{testResult.ok ? (
|
|
394
|
+
<>
|
|
395
|
+
<div className="minimax-test-line">
|
|
396
|
+
<strong>model:</strong> <code>{testResult.model}</code> ·{' '}
|
|
397
|
+
<strong>finish:</strong> {testResult.finishReason ?? '—'}
|
|
398
|
+
</div>
|
|
399
|
+
{testResult.content && (
|
|
400
|
+
<div className="minimax-test-content">
|
|
401
|
+
<code>{testResult.content}</code>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
{testResult.usage && (
|
|
405
|
+
<div className="minimax-test-usage">
|
|
406
|
+
<strong>usage:</strong> total{' '}
|
|
407
|
+
<code>{testResult.usage.total_tokens ?? '?'}</code> · prompt{' '}
|
|
408
|
+
<code>{testResult.usage.prompt_tokens ?? '?'}</code> · completion{' '}
|
|
409
|
+
<code>{testResult.usage.completion_tokens ?? '?'}</code>
|
|
410
|
+
{testResult.usage.prompt_tokens_details?.cached_tokens != null && (
|
|
411
|
+
<> · cached <code>{testResult.usage.prompt_tokens_details.cached_tokens}</code></>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
</>
|
|
416
|
+
) : (
|
|
417
|
+
<pre>{testResult.message ?? testResult.error ?? 'unknown error'}</pre>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
</Card>
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ─── Sub-components ────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
function Stat({ label, value, hint }: { label: string; value: string; hint?: string }) {
|
|
431
|
+
return (
|
|
432
|
+
<div className="stat">
|
|
433
|
+
<div className="stat-label">{label}</div>
|
|
434
|
+
<div className="stat-value">{value}</div>
|
|
435
|
+
{hint && <div className="stat-hint">{hint}</div>}
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function ModelQuotaCard({ model }: { model: RemainsModel }) {
|
|
441
|
+
const fivePct = clamp(model.current_interval_remaining_percent ?? 0, 0, 100);
|
|
442
|
+
const weekPct = clamp(model.current_weekly_remaining_percent ?? 0, 0, 100);
|
|
443
|
+
const fiveConsumed = clamp(100 - fivePct, 0, 100);
|
|
444
|
+
const weekConsumed = clamp(100 - weekPct, 0, 100);
|
|
445
|
+
return (
|
|
446
|
+
<Card id={`minimax-model-${model.model_name}`} className="minimax-model-card">
|
|
447
|
+
<div className="minimax-model-head">
|
|
448
|
+
<div className="minimax-model-name">{model.model_name}</div>
|
|
449
|
+
<div
|
|
450
|
+
className={cn(
|
|
451
|
+
'minimax-model-status',
|
|
452
|
+
fivePct < 25 || weekPct < 25 ? 'is-warn' : 'is-ok'
|
|
453
|
+
)}
|
|
454
|
+
>
|
|
455
|
+
{model.current_interval_status === 1 ? 'active' : 'idle'} ·{' '}
|
|
456
|
+
{model.current_weekly_status === 1 ? 'week active' : 'week idle'}
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
<div className="minimax-quota-rows">
|
|
461
|
+
<QuotaBar
|
|
462
|
+
icon={<Activity size={12} />}
|
|
463
|
+
label="5-hour rolling"
|
|
464
|
+
remainingPct={fivePct}
|
|
465
|
+
consumedPct={fiveConsumed}
|
|
466
|
+
used={model.current_interval_usage_count ?? 0}
|
|
467
|
+
total={model.current_interval_total_count ?? 0}
|
|
468
|
+
resetIn={model.intervalResetInHuman}
|
|
469
|
+
resetISO={model.endTimeISO}
|
|
470
|
+
/>
|
|
471
|
+
<QuotaBar
|
|
472
|
+
icon={<Calendar size={12} />}
|
|
473
|
+
label="Weekly"
|
|
474
|
+
remainingPct={weekPct}
|
|
475
|
+
consumedPct={weekConsumed}
|
|
476
|
+
used={model.current_weekly_usage_count ?? 0}
|
|
477
|
+
total={model.current_weekly_total_count ?? 0}
|
|
478
|
+
resetIn={model.weeklyResetInHuman}
|
|
479
|
+
resetISO={model.weeklyEndTimeISO}
|
|
480
|
+
/>
|
|
481
|
+
</div>
|
|
482
|
+
</Card>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function QuotaBar({
|
|
487
|
+
icon,
|
|
488
|
+
label,
|
|
489
|
+
remainingPct,
|
|
490
|
+
consumedPct,
|
|
491
|
+
used,
|
|
492
|
+
total,
|
|
493
|
+
resetIn,
|
|
494
|
+
resetISO,
|
|
495
|
+
}: {
|
|
496
|
+
icon: React.ReactNode;
|
|
497
|
+
label: string;
|
|
498
|
+
remainingPct: number;
|
|
499
|
+
consumedPct: number;
|
|
500
|
+
used: number;
|
|
501
|
+
total: number;
|
|
502
|
+
resetIn?: string;
|
|
503
|
+
resetISO?: string;
|
|
504
|
+
}) {
|
|
505
|
+
const tone = remainingPct >= 75 ? 'good' : remainingPct >= 25 ? 'warn' : 'low';
|
|
506
|
+
return (
|
|
507
|
+
<div className={`minimax-quota-row is-${tone}`}>
|
|
508
|
+
<div className="minimax-quota-head">
|
|
509
|
+
<span className="minimax-quota-label">
|
|
510
|
+
{icon} {label}
|
|
511
|
+
</span>
|
|
512
|
+
<span className="minimax-quota-remaining">{remainingPct}% remaining</span>
|
|
513
|
+
</div>
|
|
514
|
+
<div className="minimax-bar">
|
|
515
|
+
<div
|
|
516
|
+
className="minimax-bar-fill"
|
|
517
|
+
style={{ width: `${consumedPct}%` }}
|
|
518
|
+
aria-label={`${consumedPct}% consumed`}
|
|
519
|
+
/>
|
|
520
|
+
</div>
|
|
521
|
+
<div className="minimax-quota-meta">
|
|
522
|
+
<span>
|
|
523
|
+
<strong>{used}</strong> / {total || '—'} requests
|
|
524
|
+
</span>
|
|
525
|
+
<span title={resetISO}>
|
|
526
|
+
{resetIn ? `resets in ${resetIn}` : '—'}
|
|
527
|
+
</span>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function clamp(n: number, lo: number, hi: number) {
|
|
534
|
+
return Math.max(lo, Math.min(hi, n));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function relativeTime(ts: number): string {
|
|
538
|
+
const ms = Date.now() - ts;
|
|
539
|
+
if (ms < 5_000) return 'just now';
|
|
540
|
+
if (ms < 60_000) return `${Math.floor(ms / 1000)}s ago`;
|
|
541
|
+
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago`;
|
|
542
|
+
return `${Math.floor(ms / 3_600_000)}h ago`;
|
|
543
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polderlabs/bizar",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.12",
|
|
4
4
|
"description": "Norse-pantheon multi-agent system for opencode — 13 agents across 4 cost tiers with cost-aware routing, plans, and a configurable agent harness. v4 ships as a single npm package bundling the dashboard server, opencode plugin, and typed SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|