@polderlabs/bizar 4.4.10 → 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/mods-loader.mjs +169 -1
- 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/cli/bin.mjs +39 -6
- package/cli/provision.mjs +182 -2
- 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/cli/bin.mjs
CHANGED
|
@@ -156,10 +156,11 @@ function showInstallHelp() {
|
|
|
156
156
|
bizar install — Run the unified BizarHarness installer
|
|
157
157
|
|
|
158
158
|
Usage:
|
|
159
|
-
bizar install
|
|
160
|
-
bizar install --dry-run
|
|
161
|
-
bizar install --force
|
|
162
|
-
bizar install --
|
|
159
|
+
bizar install Install (or refresh) every component
|
|
160
|
+
bizar install --dry-run Print what would happen, change nothing
|
|
161
|
+
bizar install --force Overwrite existing files
|
|
162
|
+
bizar install --with-mods a,b,c Opt-in: install specific mods as part of the run
|
|
163
|
+
bizar install --help Show this help
|
|
163
164
|
|
|
164
165
|
Description:
|
|
165
166
|
v4.4.7+ — unified installer. Same code path as 'bizar update'; the
|
|
@@ -194,6 +195,7 @@ function showUpdateHelp() {
|
|
|
194
195
|
bizar update --dry-run Print what would happen, change nothing
|
|
195
196
|
bizar update --force Override .bizar/PRE_PUSH_NOTES.md blockers
|
|
196
197
|
bizar update --yes Same as --force, but named for one-line scripts
|
|
198
|
+
bizar update --with-mods a,b,c Opt-in: install specific mods as part of the run
|
|
197
199
|
bizar update --help Show this help
|
|
198
200
|
|
|
199
201
|
Components updated:
|
|
@@ -580,6 +582,22 @@ function parseFlag(name) {
|
|
|
580
582
|
return args[idx + 1] || null;
|
|
581
583
|
}
|
|
582
584
|
|
|
585
|
+
/**
|
|
586
|
+
* v4.4.11 — Parse `--with-mods <csv>` from the given subargs slice.
|
|
587
|
+
* Returns `null` if the flag isn't present (the provisioner's
|
|
588
|
+
* "don't touch mods" default), or a string[] of mod ids if it is.
|
|
589
|
+
*/
|
|
590
|
+
function parseWithModsFlag(subargs) {
|
|
591
|
+
const idx = subargs.indexOf('--with-mods');
|
|
592
|
+
if (idx === -1) return null;
|
|
593
|
+
const raw = subargs[idx + 1];
|
|
594
|
+
if (!raw || raw.startsWith('--')) return [];
|
|
595
|
+
return raw
|
|
596
|
+
.split(',')
|
|
597
|
+
.map((s) => s.trim())
|
|
598
|
+
.filter(Boolean);
|
|
599
|
+
}
|
|
600
|
+
|
|
583
601
|
async function readAutoLaunchWeb() {
|
|
584
602
|
try {
|
|
585
603
|
const fs = await import('node:fs');
|
|
@@ -677,7 +695,19 @@ async function main() {
|
|
|
677
695
|
else await runTestGate();
|
|
678
696
|
} else if (args[0] === 'update') {
|
|
679
697
|
if (isHelpRequest) showUpdateHelp();
|
|
680
|
-
else
|
|
698
|
+
else {
|
|
699
|
+
// v4.4.11 — Same --with-mods opt-in for update.
|
|
700
|
+
const withMods = parseWithModsFlag(args.slice(1));
|
|
701
|
+
// runUpdate expects (subargs: string[], opts?: object). Splice
|
|
702
|
+
// --with-mods <csv> out of subargs since the provisioner now
|
|
703
|
+
// takes it via opts, not as a positional arg.
|
|
704
|
+
const subargs = args.slice(1).filter((a, i, arr) => {
|
|
705
|
+
if (a === '--with-mods') return false;
|
|
706
|
+
if (arr[i - 1] === '--with-mods') return false;
|
|
707
|
+
return true;
|
|
708
|
+
});
|
|
709
|
+
await runUpdate(subargs, { withMods });
|
|
710
|
+
}
|
|
681
711
|
} else if (args[0] === 'dev-link') {
|
|
682
712
|
if (isHelpRequest) showDevLinkHelp();
|
|
683
713
|
else {
|
|
@@ -741,7 +771,10 @@ async function main() {
|
|
|
741
771
|
} else if (args[0] === 'install') {
|
|
742
772
|
if (isHelpRequest) showInstallHelp();
|
|
743
773
|
else {
|
|
744
|
-
|
|
774
|
+
// v4.4.11 — Parse --with-mods <csv> to opt into mod installs
|
|
775
|
+
// during the run. Default: mods are NEVER touched.
|
|
776
|
+
const withMods = parseWithModsFlag(args.slice(1));
|
|
777
|
+
await runInstaller({ withMods });
|
|
745
778
|
// v4.4.3 — After install, repair any stale bin symlinks so the
|
|
746
779
|
// user picks up the new code (the installer itself may have
|
|
747
780
|
// been running from a stale install path).
|