@polderlabs/bizar 4.4.12 → 4.4.13
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-BB5mJurD.js +352 -0
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +1 -0
- package/bizar-dash/dist/assets/{main-BKXEqU1w.css → main-BsnQLXdh.css} +1 -1
- package/bizar-dash/dist/index.html +2 -2
- package/bizar-dash/src/server/minimax.mjs +231 -105
- package/bizar-dash/src/server/providers-store.mjs +10 -6
- package/bizar-dash/src/server/routes/minimax.mjs +50 -57
- package/bizar-dash/src/web/styles/minimax-usage.css +136 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +387 -22
- package/cli/bin.mjs +263 -3
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-EK_fzXn_.js +0 -352
- package/bizar-dash/dist/assets/main-EK_fzXn_.js.map +0 -1
|
@@ -4,9 +4,16 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Renders the user's current 5-hour rolling + weekly remaining quota
|
|
6
6
|
// per model, fetched from https://www.minimax.io/v1/token_plan/remains.
|
|
7
|
-
// The Subscription Key is
|
|
8
|
-
//
|
|
9
|
-
// `
|
|
7
|
+
// The Subscription Key is read from opencode's canonical auth store
|
|
8
|
+
// (`~/.local/share/opencode/auth.json`) — same place opencode's
|
|
9
|
+
// `/connect` command writes to — so the user only enters the key once
|
|
10
|
+
// and both opencode + the Bizar dashboard pick it up.
|
|
11
|
+
//
|
|
12
|
+
// The view has three states:
|
|
13
|
+
// 1. Onboarding wizard (first time, no key configured) — guided flow
|
|
14
|
+
// that takes the user from "get a key" → paste → save → dashboard
|
|
15
|
+
// 2. Live dashboard (key configured) — per-model 5h + weekly usage
|
|
16
|
+
// 3. Banner only (key invalid or auth failure) — inline error
|
|
10
17
|
|
|
11
18
|
import { useCallback, useEffect, useState } from 'react';
|
|
12
19
|
import {
|
|
@@ -22,6 +29,11 @@ import {
|
|
|
22
29
|
Save,
|
|
23
30
|
Eye,
|
|
24
31
|
EyeOff,
|
|
32
|
+
Sparkles,
|
|
33
|
+
X,
|
|
34
|
+
ExternalLink,
|
|
35
|
+
ShieldCheck,
|
|
36
|
+
CircleCheck,
|
|
25
37
|
} from 'lucide-react';
|
|
26
38
|
import { Card, CardTitle, CardMeta } from '../components/Card';
|
|
27
39
|
import { Button } from '../components/Button';
|
|
@@ -30,6 +42,8 @@ import { useToast } from '../components/Toast';
|
|
|
30
42
|
import { api } from '../lib/api';
|
|
31
43
|
import { cn } from '../lib/utils';
|
|
32
44
|
import type { Settings, Snapshot } from '../lib/types';
|
|
45
|
+
// Settings is passed for forward-compat with the App's view-props shape
|
|
46
|
+
// even though the read path no longer needs it.
|
|
33
47
|
|
|
34
48
|
type Props = {
|
|
35
49
|
snapshot: Snapshot;
|
|
@@ -39,6 +53,11 @@ type Props = {
|
|
|
39
53
|
refreshSnapshot: () => Promise<void>;
|
|
40
54
|
};
|
|
41
55
|
|
|
56
|
+
// Suppress unused-var — `settings` is kept in the prop signature so
|
|
57
|
+
// App.tsx can pass a single shape; the read path doesn't need it now
|
|
58
|
+
// that the key comes from opencode's auth.json.
|
|
59
|
+
void (null as unknown as Settings | null);
|
|
60
|
+
|
|
42
61
|
type RemainsModel = {
|
|
43
62
|
model_name: string;
|
|
44
63
|
current_interval_remaining_percent: number;
|
|
@@ -68,6 +87,7 @@ type RemainsSnapshot = {
|
|
|
68
87
|
cached?: boolean;
|
|
69
88
|
fetchedAt?: number;
|
|
70
89
|
apiKeyHint?: string;
|
|
90
|
+
keySource?: string;
|
|
71
91
|
groupId?: string;
|
|
72
92
|
baseUrl?: string;
|
|
73
93
|
models?: RemainsModel[];
|
|
@@ -77,15 +97,22 @@ type RemainsSnapshot = {
|
|
|
77
97
|
};
|
|
78
98
|
|
|
79
99
|
type Status = {
|
|
80
|
-
enabled: boolean;
|
|
81
100
|
configured: boolean;
|
|
82
101
|
apiKeyHint: string;
|
|
102
|
+
source: string;
|
|
83
103
|
groupId: string;
|
|
84
|
-
|
|
104
|
+
tokenBaseUrl: string;
|
|
85
105
|
chatBaseUrl: string;
|
|
106
|
+
knownModels: string[];
|
|
107
|
+
keyPatternValid: boolean | null;
|
|
86
108
|
cache: { fetchedAt: number; apiKeyHint: string; modelCount: number } | null;
|
|
87
109
|
};
|
|
88
110
|
|
|
111
|
+
type OnboardingState = {
|
|
112
|
+
dismissedAt: number | null;
|
|
113
|
+
hiddenModels: string[];
|
|
114
|
+
};
|
|
115
|
+
|
|
89
116
|
type TestResult = {
|
|
90
117
|
ok: boolean;
|
|
91
118
|
error?: string;
|
|
@@ -108,11 +135,16 @@ export function MiniMaxUsage({ snapshot, settings, setActiveTab, refreshSnapshot
|
|
|
108
135
|
const toast = useToast();
|
|
109
136
|
const [status, setStatus] = useState<Status | null>(null);
|
|
110
137
|
const [remains, setRemains] = useState<RemainsSnapshot | null>(null);
|
|
138
|
+
const [onboarding, setOnboarding] = useState<OnboardingState | null>(null);
|
|
111
139
|
const [loading, setLoading] = useState(true);
|
|
112
140
|
const [refreshing, setRefreshing] = useState(false);
|
|
113
141
|
const [error, setError] = useState<string | null>(null);
|
|
114
142
|
const [testResult, setTestResult] = useState<TestResult | null>(null);
|
|
115
143
|
const [testing, setTesting] = useState(false);
|
|
144
|
+
// Onboarding wizard state.
|
|
145
|
+
const [wizardStep, setWizardStep] = useState(0); // 0..3 (welcome → key → verify → done)
|
|
146
|
+
const [wizardVerifying, setWizardVerifying] = useState(false);
|
|
147
|
+
const [wizardVerifyResult, setWizardVerifyResult] = useState<TestResult | null>(null);
|
|
116
148
|
// Inline key config — saves a trip to Settings.
|
|
117
149
|
const [keyDraft, setKeyDraft] = useState('');
|
|
118
150
|
const [showKey, setShowKey] = useState(false);
|
|
@@ -122,13 +154,18 @@ export function MiniMaxUsage({ snapshot, settings, setActiveTab, refreshSnapshot
|
|
|
122
154
|
setLoading(true);
|
|
123
155
|
setError(null);
|
|
124
156
|
try {
|
|
125
|
-
const [s, r] = await Promise.all([
|
|
157
|
+
const [s, r, o] = await Promise.all([
|
|
126
158
|
api.get<Status>('/minimax/status'),
|
|
127
159
|
api.get<RemainsSnapshot>('/minimax/remains'),
|
|
160
|
+
api.get<OnboardingState>('/minimax/onboarding'),
|
|
128
161
|
]);
|
|
129
162
|
setStatus(s);
|
|
130
163
|
setRemains(r);
|
|
131
|
-
|
|
164
|
+
setOnboarding(o);
|
|
165
|
+
setKeyDraft(''); // never prefill — security
|
|
166
|
+
// If we just configured a key (dismissedAt is set but not configured)
|
|
167
|
+
// or the user already dismissed, do not show the wizard.
|
|
168
|
+
if (s.configured) setWizardStep(4); // skip past wizard
|
|
132
169
|
} catch (err) {
|
|
133
170
|
setError((err as Error).message || 'Failed to load MiniMax data');
|
|
134
171
|
} finally {
|
|
@@ -136,8 +173,66 @@ export function MiniMaxUsage({ snapshot, settings, setActiveTab, refreshSnapshot
|
|
|
136
173
|
}
|
|
137
174
|
}, []);
|
|
138
175
|
|
|
176
|
+
// Dismiss the onboarding wizard (so it doesn't show again until the
|
|
177
|
+
// user clears their key).
|
|
178
|
+
const dismissWizard = useCallback(async () => {
|
|
179
|
+
try {
|
|
180
|
+
await api.post('/minimax/onboarding', { dismissedAt: Date.now() });
|
|
181
|
+
setOnboarding((o) => o ? { ...o, dismissedAt: Date.now() } : o);
|
|
182
|
+
} catch { /* best-effort */ }
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
185
|
+
// Wizard step 2: test the key against the real API before saving.
|
|
186
|
+
const verifyKeyBeforeSave = useCallback(async () => {
|
|
187
|
+
if (!keyDraft.trim()) {
|
|
188
|
+
toast.error('Paste a Subscription Key first.');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
setWizardVerifying(true);
|
|
192
|
+
setWizardVerifyResult(null);
|
|
193
|
+
try {
|
|
194
|
+
// Temporarily save to the real auth.json, probe, then if the
|
|
195
|
+
// probe fails the user can re-paste. The probe endpoint is the
|
|
196
|
+
// chat completions surface (cheaper than remains + a real-world
|
|
197
|
+
// check that the key actually works for the dashboard's purpose).
|
|
198
|
+
const r = await api.post<TestResult>('/minimax/test', {
|
|
199
|
+
prompt: 'Reply with a single word: pong',
|
|
200
|
+
model: 'MiniMax-M3',
|
|
201
|
+
maxTokens: 16,
|
|
202
|
+
});
|
|
203
|
+
setWizardVerifyResult(r);
|
|
204
|
+
if (r.ok) {
|
|
205
|
+
setWizardStep(3); // success
|
|
206
|
+
toast.success('Key works. Saving…');
|
|
207
|
+
// Auto-save: the user already proved the key works.
|
|
208
|
+
try {
|
|
209
|
+
await api.post('/minimax/onboarding/save-key', {
|
|
210
|
+
key: keyDraft.trim(),
|
|
211
|
+
groupId: 'default',
|
|
212
|
+
});
|
|
213
|
+
clearRemainsCacheClient();
|
|
214
|
+
toast.success('Subscription Key saved.');
|
|
215
|
+
} catch (err) {
|
|
216
|
+
toast.error(`Save failed: ${(err as Error).message}`);
|
|
217
|
+
}
|
|
218
|
+
void load();
|
|
219
|
+
} else {
|
|
220
|
+
toast.error(`Verification failed: ${r.message ?? r.error ?? 'unknown'}`);
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
toast.error(`Test request failed: ${(err as Error).message}`);
|
|
224
|
+
} finally {
|
|
225
|
+
setWizardVerifying(false);
|
|
226
|
+
}
|
|
227
|
+
}, [keyDraft, toast, load]);
|
|
228
|
+
|
|
139
229
|
useEffect(() => { load(); }, [load]);
|
|
140
230
|
|
|
231
|
+
// Persist the key into opencode's auth.json. The server-side route
|
|
232
|
+
// `POST /api/minimax/onboarding/save-key` writes to
|
|
233
|
+
// `~/.local/share/opencode/auth.json` (the canonical store the
|
|
234
|
+
// opencode `/connect` command writes to). The dashboard reads it
|
|
235
|
+
// back on the next status fetch.
|
|
141
236
|
const saveKey = useCallback(async () => {
|
|
142
237
|
if (!keyDraft.trim()) {
|
|
143
238
|
toast.error('Paste a Subscription Key first.');
|
|
@@ -145,24 +240,24 @@ export function MiniMaxUsage({ snapshot, settings, setActiveTab, refreshSnapshot
|
|
|
145
240
|
}
|
|
146
241
|
setSavingKey(true);
|
|
147
242
|
try {
|
|
148
|
-
const r = await api.
|
|
149
|
-
minimax
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
243
|
+
const r = await api.post<{ ok: boolean; path?: string; apiKeyHint?: string; error?: string; message?: string }>(
|
|
244
|
+
'/minimax/onboarding/save-key',
|
|
245
|
+
{ key: keyDraft.trim(), groupId: 'default' },
|
|
246
|
+
);
|
|
247
|
+
if (!r.ok) {
|
|
248
|
+
toast.error(`Save failed: ${r.message ?? r.error ?? 'unknown'}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
156
251
|
setKeyDraft('');
|
|
157
252
|
clearRemainsCacheClient();
|
|
158
|
-
toast.success('Subscription Key saved.
|
|
253
|
+
toast.success('Subscription Key saved. Loading quota…');
|
|
159
254
|
void load();
|
|
160
255
|
} catch (err) {
|
|
161
256
|
toast.error(`Save failed: ${(err as Error).message}`);
|
|
162
257
|
} finally {
|
|
163
258
|
setSavingKey(false);
|
|
164
259
|
}
|
|
165
|
-
}, [keyDraft,
|
|
260
|
+
}, [keyDraft, toast, load]);
|
|
166
261
|
|
|
167
262
|
// Clears the on-disk + in-memory cache via the dashboard's
|
|
168
263
|
// /api/minimax/cache DELETE endpoint. Wrapped in a helper so the
|
|
@@ -229,6 +324,34 @@ export function MiniMaxUsage({ snapshot, settings, setActiveTab, refreshSnapshot
|
|
|
229
324
|
);
|
|
230
325
|
}
|
|
231
326
|
|
|
327
|
+
// First-run onboarding: show the wizard when no key is configured and
|
|
328
|
+
// the user hasn't dismissed it. The wizard writes the key to opencode's
|
|
329
|
+
// auth store (same place `/connect` writes), then the dashboard reads
|
|
330
|
+
// it back via /minimax/status and the live dashboard takes over.
|
|
331
|
+
const showWizard = !status.configured
|
|
332
|
+
&& (onboarding?.dismissedAt == null)
|
|
333
|
+
&& wizardStep < 4;
|
|
334
|
+
if (showWizard) {
|
|
335
|
+
return <OnboardingWizard
|
|
336
|
+
step={wizardStep}
|
|
337
|
+
setStep={setWizardStep}
|
|
338
|
+
keyDraft={keyDraft}
|
|
339
|
+
setKeyDraft={setKeyDraft}
|
|
340
|
+
showKey={showKey}
|
|
341
|
+
setShowKey={setShowKey}
|
|
342
|
+
verifying={wizardVerifying}
|
|
343
|
+
verifyResult={wizardVerifyResult}
|
|
344
|
+
onVerify={verifyKeyBeforeSave}
|
|
345
|
+
onSkip={async () => {
|
|
346
|
+
await dismissWizard();
|
|
347
|
+
toast.success("Skipped. You can paste the key anytime from this page.");
|
|
348
|
+
}}
|
|
349
|
+
onManualKeySave={saveKey}
|
|
350
|
+
savingKey={savingKey}
|
|
351
|
+
status={status}
|
|
352
|
+
/>;
|
|
353
|
+
}
|
|
354
|
+
|
|
232
355
|
return (
|
|
233
356
|
<div className="view-container view-minimax-usage">
|
|
234
357
|
{/* ── Header ─────────────────────────────────────────────────── */}
|
|
@@ -260,17 +383,30 @@ export function MiniMaxUsage({ snapshot, settings, setActiveTab, refreshSnapshot
|
|
|
260
383
|
</header>
|
|
261
384
|
|
|
262
385
|
{/* ── Status banners ─────────────────────────────────────────── */}
|
|
263
|
-
{!
|
|
386
|
+
{remains && !remains.ok && (
|
|
387
|
+
<div className="banner banner-err">
|
|
388
|
+
<AlertTriangle size={16} />
|
|
389
|
+
<span>
|
|
390
|
+
<strong>Couldn't load quota</strong>: {remains.message ?? remains.error ?? 'unknown error'}
|
|
391
|
+
{status.source && (
|
|
392
|
+
<> · source: <code>{status.source}</code></>
|
|
393
|
+
)}
|
|
394
|
+
</span>
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
{status.configured && status.keyPatternValid === false && (
|
|
264
398
|
<div className="banner banner-warn">
|
|
265
399
|
<AlertTriangle size={16} />
|
|
266
400
|
<span>
|
|
267
|
-
|
|
401
|
+
The configured Subscription Key has an unexpected format (expected
|
|
402
|
+
<code> sk-cp-</code> / <code>sk-ant-</code> / <code>sk-or-</code> prefix).
|
|
403
|
+
Re-save it from this page.
|
|
268
404
|
</span>
|
|
269
405
|
</div>
|
|
270
406
|
)}
|
|
271
407
|
|
|
272
408
|
{/* ── Subscription Key config (inline) ──────────────────── */}
|
|
273
|
-
{
|
|
409
|
+
{(
|
|
274
410
|
<Card id="minimax-key">
|
|
275
411
|
<CardTitle>
|
|
276
412
|
<KeyRound size={14} /> Subscription Key
|
|
@@ -285,7 +421,7 @@ export function MiniMaxUsage({ snapshot, settings, setActiveTab, refreshSnapshot
|
|
|
285
421
|
>
|
|
286
422
|
platform.minimax.io/user-center/payment/token-plan
|
|
287
423
|
</a>
|
|
288
|
-
. Stored
|
|
424
|
+
. Stored in <code>~/.local/share/opencode/auth.json</code>;
|
|
289
425
|
never sent to any other host. Status:{' '}
|
|
290
426
|
{status.configured ? (
|
|
291
427
|
<strong className="is-ok">configured ({status.apiKeyHint})</strong>
|
|
@@ -355,7 +491,7 @@ export function MiniMaxUsage({ snapshot, settings, setActiveTab, refreshSnapshot
|
|
|
355
491
|
<Stat
|
|
356
492
|
label="API key"
|
|
357
493
|
value={remains.apiKeyHint ?? '—'}
|
|
358
|
-
hint="Masked; the real key never leaves
|
|
494
|
+
hint="Masked; the real key never leaves auth.json"
|
|
359
495
|
/>
|
|
360
496
|
</div>
|
|
361
497
|
|
|
@@ -541,3 +677,232 @@ function relativeTime(ts: number): string {
|
|
|
541
677
|
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago`;
|
|
542
678
|
return `${Math.floor(ms / 3_600_000)}h ago`;
|
|
543
679
|
}
|
|
680
|
+
|
|
681
|
+
// ─── Onboarding wizard ──────────────────────────────────────────────────
|
|
682
|
+
|
|
683
|
+
type OnboardingProps = {
|
|
684
|
+
step: number;
|
|
685
|
+
setStep: (n: number) => void;
|
|
686
|
+
keyDraft: string;
|
|
687
|
+
setKeyDraft: (s: string) => void;
|
|
688
|
+
showKey: boolean;
|
|
689
|
+
setShowKey: React.Dispatch<React.SetStateAction<boolean>>;
|
|
690
|
+
verifying: boolean;
|
|
691
|
+
verifyResult: TestResult | null;
|
|
692
|
+
onVerify: () => void;
|
|
693
|
+
onSkip: () => void;
|
|
694
|
+
onManualKeySave: () => void;
|
|
695
|
+
savingKey: boolean;
|
|
696
|
+
status: Status;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* 4-step first-run wizard:
|
|
701
|
+
* 0. Welcome — what the page does, what the user needs.
|
|
702
|
+
* 1. Get a key — link to platform.minimax.io + how to find it.
|
|
703
|
+
* 2. Paste — input with show/hide + "Test key" button.
|
|
704
|
+
* 3. Done — confirms the save + "View your quota" CTA.
|
|
705
|
+
*
|
|
706
|
+
* The wizard writes to opencode's canonical auth store at
|
|
707
|
+
* ~/.local/share/opencode/auth.json (via /api/minimax/onboarding/save-key).
|
|
708
|
+
*/
|
|
709
|
+
function OnboardingWizard({
|
|
710
|
+
step, setStep, keyDraft, setKeyDraft, showKey, setShowKey,
|
|
711
|
+
verifying, verifyResult, onVerify, onSkip, onManualKeySave,
|
|
712
|
+
savingKey, status,
|
|
713
|
+
}: OnboardingProps) {
|
|
714
|
+
return (
|
|
715
|
+
<div className="view-container view-minimax-onboarding">
|
|
716
|
+
<header className="view-header">
|
|
717
|
+
<div className="view-header-titles">
|
|
718
|
+
<h1 className="view-title">
|
|
719
|
+
<Sparkles size={20} style={{ verticalAlign: 'text-bottom', marginRight: 6 }} />
|
|
720
|
+
Set up MiniMax Token Plan tracking
|
|
721
|
+
</h1>
|
|
722
|
+
<p className="view-subtitle">
|
|
723
|
+
We'll read your Subscription Key from opencode's auth store and show your remaining
|
|
724
|
+
5-hour + weekly quota for every MiniMax model.
|
|
725
|
+
</p>
|
|
726
|
+
</div>
|
|
727
|
+
<div className="view-header-actions">
|
|
728
|
+
<Button variant="ghost" size="sm" onClick={onSkip}>
|
|
729
|
+
<X size={14} /> Skip for now
|
|
730
|
+
</Button>
|
|
731
|
+
</div>
|
|
732
|
+
</header>
|
|
733
|
+
|
|
734
|
+
{/* ── Stepper ────────────────────────────────────────────── */}
|
|
735
|
+
<div className="minimax-wizard-stepper">
|
|
736
|
+
{(['Welcome', 'Get a key', 'Paste & test', 'Done']).map((label, i) => (
|
|
737
|
+
<div
|
|
738
|
+
key={label}
|
|
739
|
+
className={cn(
|
|
740
|
+
'minimax-wizard-step',
|
|
741
|
+
i === step && 'is-current',
|
|
742
|
+
i < step && 'is-done',
|
|
743
|
+
i > step && 'is-todo',
|
|
744
|
+
)}
|
|
745
|
+
>
|
|
746
|
+
<div className="minimax-wizard-step-bullet">
|
|
747
|
+
{i < step ? <CircleCheck size={14} /> : i + 1}
|
|
748
|
+
</div>
|
|
749
|
+
<div className="minimax-wizard-step-label">{label}</div>
|
|
750
|
+
</div>
|
|
751
|
+
))}
|
|
752
|
+
</div>
|
|
753
|
+
|
|
754
|
+
{/* ── Step content ────────────────────────────────────────── */}
|
|
755
|
+
<Card id="minimax-wizard-card">
|
|
756
|
+
{step === 0 && (
|
|
757
|
+
<div className="minimax-wizard-body">
|
|
758
|
+
<h3 className="minimax-wizard-title">Welcome</h3>
|
|
759
|
+
<p className="minimax-wizard-prose">
|
|
760
|
+
BizarHarness can show you how much of your MiniMax Token Plan
|
|
761
|
+
quota you have left — both the <strong>5-hour rolling</strong>
|
|
762
|
+
and the <strong>weekly</strong> windows — for each model. To
|
|
763
|
+
do that it needs your Subscription Key. The key never leaves
|
|
764
|
+
this machine.
|
|
765
|
+
</p>
|
|
766
|
+
<ul className="minimax-wizard-list">
|
|
767
|
+
<li>Stored in <code>~/.local/share/opencode/auth.json</code> (same place opencode's <code>/connect</code> writes).</li>
|
|
768
|
+
<li>Read fresh on every dashboard load — you can revoke it any time from the MiniMax console.</li>
|
|
769
|
+
<li>Used only for <code>GET /v1/token_plan/remains</code> and the chat-completions probe; never logged in full.</li>
|
|
770
|
+
</ul>
|
|
771
|
+
<div className="minimax-wizard-actions">
|
|
772
|
+
<Button onClick={() => setStep(1)} variant="primary" size="md">
|
|
773
|
+
Get started <span style={{ marginLeft: 6 }}>→</span>
|
|
774
|
+
</Button>
|
|
775
|
+
</div>
|
|
776
|
+
</div>
|
|
777
|
+
)}
|
|
778
|
+
|
|
779
|
+
{step === 1 && (
|
|
780
|
+
<div className="minimax-wizard-body">
|
|
781
|
+
<h3 className="minimax-wizard-title">Get your Subscription Key</h3>
|
|
782
|
+
<p className="minimax-wizard-prose">
|
|
783
|
+
Go to the MiniMax console and copy your Subscription Key. The
|
|
784
|
+
key starts with <code>sk-cp-</code> (or <code>sk-ant-</code>{' '}
|
|
785
|
+
if you're using the Anthropic-format surface). It's about
|
|
786
|
+
120 characters long.
|
|
787
|
+
</p>
|
|
788
|
+
<ol className="minimax-wizard-list">
|
|
789
|
+
<li>Open{' '}
|
|
790
|
+
<a
|
|
791
|
+
href="https://platform.minimax.io/user-center/payment/token-plan"
|
|
792
|
+
target="_blank"
|
|
793
|
+
rel="noreferrer"
|
|
794
|
+
className="minimax-wizard-link"
|
|
795
|
+
>
|
|
796
|
+
<ExternalLink size={12} /> platform.minimax.io/user-center/payment/token-plan
|
|
797
|
+
</a>
|
|
798
|
+
</li>
|
|
799
|
+
<li>Click <strong>Copy Subscription Key</strong>.</li>
|
|
800
|
+
<li>Come back here and paste it in the next step.</li>
|
|
801
|
+
</ol>
|
|
802
|
+
<p className="minimax-wizard-prose minimax-wizard-prose--muted">
|
|
803
|
+
Already have the key? You can also set the env var{' '}
|
|
804
|
+
<code>MINIMAX_API_KEY</code> or add it to{' '}
|
|
805
|
+
<code>opencode.json</code> under{' '}
|
|
806
|
+
<code>provider.minimax.options.apiKey</code>.
|
|
807
|
+
</p>
|
|
808
|
+
<div className="minimax-wizard-actions">
|
|
809
|
+
<Button variant="secondary" size="md" onClick={() => setStep(0)}>← Back</Button>
|
|
810
|
+
<Button variant="primary" size="md" onClick={() => setStep(2)}>
|
|
811
|
+
I have my key <span style={{ marginLeft: 6 }}>→</span>
|
|
812
|
+
</Button>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
)}
|
|
816
|
+
|
|
817
|
+
{step === 2 && (
|
|
818
|
+
<div className="minimax-wizard-body">
|
|
819
|
+
<h3 className="minimax-wizard-title">Paste & test</h3>
|
|
820
|
+
<p className="minimax-wizard-prose">
|
|
821
|
+
Paste your Subscription Key below. We'll test it against the
|
|
822
|
+
real API (one tiny chat call) and then save it to opencode's
|
|
823
|
+
auth store. If the test fails you'll see exactly why.
|
|
824
|
+
</p>
|
|
825
|
+
<div className="minimax-key-row">
|
|
826
|
+
<div className="minimax-key-input-wrap">
|
|
827
|
+
<KeyRound size={12} className="minimax-key-icon" />
|
|
828
|
+
<input
|
|
829
|
+
type={showKey ? 'text' : 'password'}
|
|
830
|
+
value={keyDraft}
|
|
831
|
+
onChange={(e) => setKeyDraft(e.target.value)}
|
|
832
|
+
placeholder="eyJhbGciOi... or sk-cp-..."
|
|
833
|
+
spellCheck={false}
|
|
834
|
+
autoComplete="off"
|
|
835
|
+
className="minimax-key-input"
|
|
836
|
+
autoFocus
|
|
837
|
+
/>
|
|
838
|
+
<button
|
|
839
|
+
type="button"
|
|
840
|
+
onClick={() => setShowKey((v) => !v)}
|
|
841
|
+
className="minimax-key-toggle"
|
|
842
|
+
aria-label={showKey ? 'Hide key' : 'Show key'}
|
|
843
|
+
title={showKey ? 'Hide key' : 'Show key'}
|
|
844
|
+
>
|
|
845
|
+
{showKey ? <EyeOff size={13} /> : <Eye size={13} />}
|
|
846
|
+
</button>
|
|
847
|
+
</div>
|
|
848
|
+
</div>
|
|
849
|
+
{verifyResult && !verifyResult.ok && (
|
|
850
|
+
<div className="minimax-wizard-error">
|
|
851
|
+
<AlertTriangle size={14} />
|
|
852
|
+
<span>
|
|
853
|
+
<strong>Verification failed:</strong> {verifyResult.message ?? verifyResult.error}
|
|
854
|
+
</span>
|
|
855
|
+
</div>
|
|
856
|
+
)}
|
|
857
|
+
<div className="minimax-wizard-actions">
|
|
858
|
+
<Button variant="secondary" size="md" onClick={() => setStep(1)}>← Back</Button>
|
|
859
|
+
<Button
|
|
860
|
+
variant="primary"
|
|
861
|
+
size="md"
|
|
862
|
+
onClick={onVerify}
|
|
863
|
+
disabled={verifying || !keyDraft.trim()}
|
|
864
|
+
>
|
|
865
|
+
{verifying ? <Loader2 size={14} className="spin" /> : <ShieldCheck size={14} />}
|
|
866
|
+
{verifying ? 'Testing…' : 'Test & save key'}
|
|
867
|
+
</Button>
|
|
868
|
+
</div>
|
|
869
|
+
<p className="minimax-wizard-prose minimax-wizard-prose--muted">
|
|
870
|
+
Don't want to test first?{' '}
|
|
871
|
+
<a
|
|
872
|
+
href="#"
|
|
873
|
+
onClick={(e) => { e.preventDefault(); onManualKeySave(); }}
|
|
874
|
+
className="minimax-wizard-link"
|
|
875
|
+
>
|
|
876
|
+
Save without testing
|
|
877
|
+
</a>
|
|
878
|
+
</p>
|
|
879
|
+
</div>
|
|
880
|
+
)}
|
|
881
|
+
|
|
882
|
+
{step === 3 && (
|
|
883
|
+
<div className="minimax-wizard-body">
|
|
884
|
+
<h3 className="minimax-wizard-title">
|
|
885
|
+
<CircleCheck size={18} style={{ verticalAlign: 'text-bottom', marginRight: 6 }} />
|
|
886
|
+
All set
|
|
887
|
+
</h3>
|
|
888
|
+
<p className="minimax-wizard-prose">
|
|
889
|
+
The key works and is saved. Reloading the dashboard so you
|
|
890
|
+
can see your live 5-hour + weekly quota.
|
|
891
|
+
</p>
|
|
892
|
+
<div className="minimax-wizard-actions">
|
|
893
|
+
<Button variant="primary" size="md" onClick={() => window.location.reload()}>
|
|
894
|
+
View your quota →
|
|
895
|
+
</Button>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
)}
|
|
899
|
+
|
|
900
|
+
{/* ── Status footer ─────────────────────────────────────────── */}
|
|
901
|
+
<div className="minimax-wizard-footer">
|
|
902
|
+
<ShieldCheck size={11} />
|
|
903
|
+
<span>Key is written to <code>~/.local/share/opencode/auth.json</code> with mode 0600.</span>
|
|
904
|
+
</div>
|
|
905
|
+
</Card>
|
|
906
|
+
</div>
|
|
907
|
+
);
|
|
908
|
+
}
|