@geminilight/mindos 0.5.0 → 0.5.1
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/app/app/layout.tsx +4 -3
- package/app/components/SetupWizard.tsx +435 -408
- package/package.json +1 -1
package/app/app/layout.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
|
-
import {
|
|
2
|
+
import { Inter, IBM_Plex_Mono, IBM_Plex_Sans, Lora } from 'next/font/google';
|
|
3
3
|
import './globals.css';
|
|
4
4
|
import { getFileTree } from '@/lib/fs';
|
|
5
5
|
import ShellLayout from '@/components/ShellLayout';
|
|
@@ -9,13 +9,14 @@ import ErrorBoundary from '@/components/ErrorBoundary';
|
|
|
9
9
|
import RegisterSW from './register-sw';
|
|
10
10
|
import UpdateBanner from '@/components/UpdateBanner';
|
|
11
11
|
|
|
12
|
-
const geistSans =
|
|
12
|
+
const geistSans = Inter({
|
|
13
13
|
variable: '--font-geist-sans',
|
|
14
14
|
subsets: ['latin'],
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
const geistMono =
|
|
17
|
+
const geistMono = IBM_Plex_Mono({
|
|
18
18
|
variable: '--font-geist-mono',
|
|
19
|
+
weight: ['400', '600'],
|
|
19
20
|
subsets: ['latin'],
|
|
20
21
|
});
|
|
21
22
|
|
|
@@ -28,7 +28,7 @@ interface SetupState {
|
|
|
28
28
|
|
|
29
29
|
interface PortStatus {
|
|
30
30
|
checking: boolean;
|
|
31
|
-
available: boolean | null;
|
|
31
|
+
available: boolean | null;
|
|
32
32
|
suggestion: number | null;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -40,7 +40,6 @@ interface AgentEntry {
|
|
|
40
40
|
hasGlobalScope: boolean;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Per-agent install tracking (live, in Step 5)
|
|
44
43
|
type AgentInstallState = 'pending' | 'installing' | 'ok' | 'error';
|
|
45
44
|
interface AgentInstallStatus {
|
|
46
45
|
state: AgentInstallState;
|
|
@@ -58,10 +57,8 @@ const STEP_KB = 0;
|
|
|
58
57
|
const STEP_PORTS = 2;
|
|
59
58
|
const STEP_AGENTS = 4;
|
|
60
59
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// parent re-renders (declaring inside SetupWizard would remount it)
|
|
64
|
-
// -------------------------------------------------------------------
|
|
60
|
+
// ─── Step 4 (Security) ────────────────────────────────────────────────────────
|
|
61
|
+
// Extracted at module level so its local seed/showSeed state survives parent re-renders
|
|
65
62
|
function Step4Inner({
|
|
66
63
|
authToken, tokenCopied, onCopy, onGenerate, webPassword, onPasswordChange, s,
|
|
67
64
|
}: {
|
|
@@ -120,9 +117,7 @@ function Step4Inner({
|
|
|
120
117
|
);
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
//
|
|
124
|
-
// PortField — input + inline availability badge + suggestion button
|
|
125
|
-
// -------------------------------------------------------------------
|
|
120
|
+
// ─── PortField ────────────────────────────────────────────────────────────────
|
|
126
121
|
function PortField({
|
|
127
122
|
label, hint, value, onChange, status, onCheckPort, s,
|
|
128
123
|
}: {
|
|
@@ -173,9 +168,370 @@ function PortField({
|
|
|
173
168
|
);
|
|
174
169
|
}
|
|
175
170
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
171
|
+
// ─── Step 1: Knowledge Base ───────────────────────────────────────────────────
|
|
172
|
+
function Step1({
|
|
173
|
+
state, update, t,
|
|
174
|
+
}: {
|
|
175
|
+
state: SetupState;
|
|
176
|
+
update: <K extends keyof SetupState>(key: K, val: SetupState[K]) => void;
|
|
177
|
+
t: ReturnType<typeof useLocale>['t'];
|
|
178
|
+
}) {
|
|
179
|
+
const s = t.setup;
|
|
180
|
+
return (
|
|
181
|
+
<div className="space-y-6">
|
|
182
|
+
<Field label={s.kbPath} hint={s.kbPathHint}>
|
|
183
|
+
<Input value={state.mindRoot} onChange={e => update('mindRoot', e.target.value)} placeholder={s.kbPathDefault} />
|
|
184
|
+
</Field>
|
|
185
|
+
<div>
|
|
186
|
+
<label className="text-sm text-foreground font-medium mb-3 block">{s.template}</label>
|
|
187
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
188
|
+
{TEMPLATES.map(tpl => (
|
|
189
|
+
<button key={tpl.id} onClick={() => update('template', tpl.id)}
|
|
190
|
+
className="flex flex-col items-start gap-2 p-4 rounded-xl border text-left transition-all duration-150"
|
|
191
|
+
style={{
|
|
192
|
+
background: state.template === tpl.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
|
|
193
|
+
borderColor: state.template === tpl.id ? 'var(--amber)' : 'var(--border)',
|
|
194
|
+
}}>
|
|
195
|
+
<div className="flex items-center gap-2">
|
|
196
|
+
<span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
|
|
197
|
+
<span className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>
|
|
198
|
+
{t.onboarding.templates[tpl.id as 'en' | 'zh' | 'empty'].title}
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
<div className="w-full rounded-lg px-2.5 py-1.5 text-[11px] leading-relaxed font-display"
|
|
202
|
+
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
|
|
203
|
+
{tpl.dirs.map(d => <div key={d}>{d}</div>)}
|
|
204
|
+
</div>
|
|
205
|
+
</button>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── Step 2: AI Provider ──────────────────────────────────────────────────────
|
|
214
|
+
function Step2({
|
|
215
|
+
state, update, s,
|
|
216
|
+
}: {
|
|
217
|
+
state: SetupState;
|
|
218
|
+
update: <K extends keyof SetupState>(key: K, val: SetupState[K]) => void;
|
|
219
|
+
s: ReturnType<typeof useLocale>['t']['setup'];
|
|
220
|
+
}) {
|
|
221
|
+
const providers = [
|
|
222
|
+
{ id: 'anthropic' as const, icon: <Brain size={18} />, label: 'Anthropic', desc: 'Claude — claude-sonnet-4-6' },
|
|
223
|
+
{ id: 'openai' as const, icon: <Zap size={18} />, label: 'OpenAI', desc: 'GPT or any OpenAI-compatible API' },
|
|
224
|
+
{ id: 'skip' as const, icon: <SkipForward size={18} />, label: s.aiSkipTitle, desc: s.aiSkipDesc },
|
|
225
|
+
];
|
|
226
|
+
return (
|
|
227
|
+
<div className="space-y-5">
|
|
228
|
+
<div className="grid grid-cols-1 gap-3">
|
|
229
|
+
{providers.map(p => (
|
|
230
|
+
<button key={p.id} onClick={() => update('provider', p.id)}
|
|
231
|
+
className="flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150"
|
|
232
|
+
style={{
|
|
233
|
+
background: state.provider === p.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
|
|
234
|
+
borderColor: state.provider === p.id ? 'var(--amber)' : 'var(--border)',
|
|
235
|
+
}}>
|
|
236
|
+
<span className="mt-0.5" style={{ color: state.provider === p.id ? 'var(--amber)' : 'var(--muted-foreground)' }}>
|
|
237
|
+
{p.icon}
|
|
238
|
+
</span>
|
|
239
|
+
<div>
|
|
240
|
+
<p className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>{p.label}</p>
|
|
241
|
+
<p className="text-xs mt-0.5" style={{ color: 'var(--muted-foreground)' }}>{p.desc}</p>
|
|
242
|
+
</div>
|
|
243
|
+
{state.provider === p.id && (
|
|
244
|
+
<CheckCircle2 size={16} className="ml-auto mt-0.5 shrink-0" style={{ color: 'var(--amber)' }} />
|
|
245
|
+
)}
|
|
246
|
+
</button>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
{state.provider !== 'skip' && (
|
|
250
|
+
<div className="space-y-4 pt-2">
|
|
251
|
+
<Field label={s.apiKey}>
|
|
252
|
+
<ApiKeyInput
|
|
253
|
+
value={state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey}
|
|
254
|
+
onChange={v => update(state.provider === 'anthropic' ? 'anthropicKey' : 'openaiKey', v)}
|
|
255
|
+
placeholder={state.provider === 'anthropic' ? 'sk-ant-...' : 'sk-...'}
|
|
256
|
+
/>
|
|
257
|
+
</Field>
|
|
258
|
+
<Field label={s.model}>
|
|
259
|
+
<Input
|
|
260
|
+
value={state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel}
|
|
261
|
+
onChange={e => update(state.provider === 'anthropic' ? 'anthropicModel' : 'openaiModel', e.target.value)}
|
|
262
|
+
/>
|
|
263
|
+
</Field>
|
|
264
|
+
{state.provider === 'openai' && (
|
|
265
|
+
<Field label={s.baseUrl} hint={s.baseUrlHint}>
|
|
266
|
+
<Input value={state.openaiBaseUrl} onChange={e => update('openaiBaseUrl', e.target.value)}
|
|
267
|
+
placeholder="https://api.openai.com/v1" />
|
|
268
|
+
</Field>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Step 3: Ports ────────────────────────────────────────────────────────────
|
|
277
|
+
function Step3({
|
|
278
|
+
state, update, webPortStatus, mcpPortStatus, setWebPortStatus, setMcpPortStatus, checkPort, portConflict, s,
|
|
279
|
+
}: {
|
|
280
|
+
state: SetupState;
|
|
281
|
+
update: <K extends keyof SetupState>(key: K, val: SetupState[K]) => void;
|
|
282
|
+
webPortStatus: PortStatus;
|
|
283
|
+
mcpPortStatus: PortStatus;
|
|
284
|
+
setWebPortStatus: (s: PortStatus) => void;
|
|
285
|
+
setMcpPortStatus: (s: PortStatus) => void;
|
|
286
|
+
checkPort: (port: number, which: 'web' | 'mcp') => void;
|
|
287
|
+
portConflict: boolean;
|
|
288
|
+
s: ReturnType<typeof useLocale>['t']['setup'];
|
|
289
|
+
}) {
|
|
290
|
+
return (
|
|
291
|
+
<div className="space-y-5">
|
|
292
|
+
<PortField
|
|
293
|
+
label={s.webPort} hint={s.portHint} value={state.webPort}
|
|
294
|
+
onChange={v => { update('webPort', v); setWebPortStatus({ checking: false, available: null, suggestion: null }); }}
|
|
295
|
+
status={webPortStatus}
|
|
296
|
+
onCheckPort={port => checkPort(port, 'web')}
|
|
297
|
+
s={s}
|
|
298
|
+
/>
|
|
299
|
+
<PortField
|
|
300
|
+
label={s.mcpPort} hint={s.portHint} value={state.mcpPort}
|
|
301
|
+
onChange={v => { update('mcpPort', v); setMcpPortStatus({ checking: false, available: null, suggestion: null }); }}
|
|
302
|
+
status={mcpPortStatus}
|
|
303
|
+
onCheckPort={port => checkPort(port, 'mcp')}
|
|
304
|
+
s={s}
|
|
305
|
+
/>
|
|
306
|
+
{portConflict && (
|
|
307
|
+
<p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--amber)' }}>
|
|
308
|
+
<AlertTriangle size={12} /> {s.portConflict}
|
|
309
|
+
</p>
|
|
310
|
+
)}
|
|
311
|
+
{!portConflict && (webPortStatus.available === null || mcpPortStatus.available === null) && !webPortStatus.checking && !mcpPortStatus.checking && (
|
|
312
|
+
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.portVerifyHint}</p>
|
|
313
|
+
)}
|
|
314
|
+
<p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
|
|
315
|
+
<AlertTriangle size={12} /> {s.portRestartWarning}
|
|
316
|
+
</p>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Step 5: Agent Tools ──────────────────────────────────────────────────────
|
|
322
|
+
function Step5({
|
|
323
|
+
agents, agentsLoading, selectedAgents, setSelectedAgents,
|
|
324
|
+
agentTransport, setAgentTransport, agentScope, setAgentScope,
|
|
325
|
+
agentStatuses, s, settingsMcp,
|
|
326
|
+
}: {
|
|
327
|
+
agents: AgentEntry[];
|
|
328
|
+
agentsLoading: boolean;
|
|
329
|
+
selectedAgents: Set<string>;
|
|
330
|
+
setSelectedAgents: React.Dispatch<React.SetStateAction<Set<string>>>;
|
|
331
|
+
agentTransport: 'stdio' | 'http';
|
|
332
|
+
setAgentTransport: (v: 'stdio' | 'http') => void;
|
|
333
|
+
agentScope: 'global' | 'project';
|
|
334
|
+
setAgentScope: (v: 'global' | 'project') => void;
|
|
335
|
+
agentStatuses: Record<string, AgentInstallStatus>;
|
|
336
|
+
s: ReturnType<typeof useLocale>['t']['setup'];
|
|
337
|
+
settingsMcp: ReturnType<typeof useLocale>['t']['settings']['mcp'];
|
|
338
|
+
}) {
|
|
339
|
+
const toggleAgent = (key: string) => {
|
|
340
|
+
setSelectedAgents(prev => {
|
|
341
|
+
const next = new Set(prev);
|
|
342
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
343
|
+
return next;
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const getStatusBadge = (key: string, installed: boolean) => {
|
|
348
|
+
const st = agentStatuses[key];
|
|
349
|
+
if (st) {
|
|
350
|
+
if (st.state === 'installing') return (
|
|
351
|
+
<span className="flex items-center gap-1 text-[11px]" style={{ color: 'var(--muted-foreground)' }}>
|
|
352
|
+
<Loader2 size={10} className="animate-spin" /> {s.agentInstalling}
|
|
353
|
+
</span>
|
|
354
|
+
);
|
|
355
|
+
if (st.state === 'ok') return (
|
|
356
|
+
<span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
|
|
357
|
+
style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
|
|
358
|
+
<CheckCircle2 size={10} /> {s.agentStatusOk}
|
|
359
|
+
</span>
|
|
360
|
+
);
|
|
361
|
+
if (st.state === 'error') return (
|
|
362
|
+
<span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
|
|
363
|
+
style={{ background: 'rgba(239,68,68,0.1)', color: '#ef4444' }}>
|
|
364
|
+
<XCircle size={10} /> {s.agentStatusError}
|
|
365
|
+
{st.message && <span className="ml-1 text-[10px]">({st.message})</span>}
|
|
366
|
+
</span>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (installed) return (
|
|
370
|
+
<span className="text-[11px] px-1.5 py-0.5 rounded"
|
|
371
|
+
style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
|
|
372
|
+
{settingsMcp.installed}
|
|
373
|
+
</span>
|
|
374
|
+
);
|
|
375
|
+
return (
|
|
376
|
+
<span className="text-[11px] px-1.5 py-0.5 rounded"
|
|
377
|
+
style={{ background: 'rgba(100,100,120,0.1)', color: 'var(--muted-foreground)' }}>
|
|
378
|
+
{s.agentNotInstalled}
|
|
379
|
+
</span>
|
|
380
|
+
);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<div className="space-y-5">
|
|
385
|
+
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.agentToolsHint}</p>
|
|
386
|
+
{agentsLoading ? (
|
|
387
|
+
<div className="flex items-center gap-2 py-4" style={{ color: 'var(--muted-foreground)' }}>
|
|
388
|
+
<Loader2 size={14} className="animate-spin" />
|
|
389
|
+
<span className="text-sm">{s.agentToolsLoading}</span>
|
|
390
|
+
</div>
|
|
391
|
+
) : agents.length === 0 ? (
|
|
392
|
+
<p className="text-sm py-4 text-center" style={{ color: 'var(--muted-foreground)' }}>
|
|
393
|
+
{s.agentToolsEmpty}
|
|
394
|
+
</p>
|
|
395
|
+
) : (
|
|
396
|
+
<>
|
|
397
|
+
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
398
|
+
{agents.map((agent, i) => (
|
|
399
|
+
<label key={agent.key}
|
|
400
|
+
className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
|
|
401
|
+
style={{
|
|
402
|
+
background: i % 2 === 0 ? 'var(--card)' : 'transparent',
|
|
403
|
+
borderTop: i > 0 ? '1px solid var(--border)' : undefined,
|
|
404
|
+
}}>
|
|
405
|
+
<input
|
|
406
|
+
type="checkbox"
|
|
407
|
+
checked={selectedAgents.has(agent.key)}
|
|
408
|
+
onChange={() => toggleAgent(agent.key)}
|
|
409
|
+
className="accent-amber-500"
|
|
410
|
+
disabled={agentStatuses[agent.key]?.state === 'installing'}
|
|
411
|
+
/>
|
|
412
|
+
<span className="text-sm flex-1" style={{ color: 'var(--foreground)' }}>{agent.name}</span>
|
|
413
|
+
{getStatusBadge(agent.key, agent.installed)}
|
|
414
|
+
</label>
|
|
415
|
+
))}
|
|
416
|
+
</div>
|
|
417
|
+
<div className="grid grid-cols-2 gap-4">
|
|
418
|
+
<Field label={s.agentTransport}>
|
|
419
|
+
<Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'stdio' | 'http')}>
|
|
420
|
+
<option value="stdio">{settingsMcp.transportStdio}</option>
|
|
421
|
+
<option value="http">{settingsMcp.transportHttp}</option>
|
|
422
|
+
</Select>
|
|
423
|
+
</Field>
|
|
424
|
+
<Field label={s.agentScope}>
|
|
425
|
+
<Select value={agentScope} onChange={e => setAgentScope(e.target.value as 'global' | 'project')}>
|
|
426
|
+
<option value="global">{settingsMcp.global}</option>
|
|
427
|
+
<option value="project">{settingsMcp.project}</option>
|
|
428
|
+
</Select>
|
|
429
|
+
</Field>
|
|
430
|
+
</div>
|
|
431
|
+
{selectedAgents.size === 0 && (
|
|
432
|
+
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.agentNoneSelected}</p>
|
|
433
|
+
)}
|
|
434
|
+
</>
|
|
435
|
+
)}
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ─── Step 6: Review ───────────────────────────────────────────────────────────
|
|
441
|
+
function Step6({
|
|
442
|
+
state, selectedAgents, error, portChanged, maskKey, s,
|
|
443
|
+
}: {
|
|
444
|
+
state: SetupState;
|
|
445
|
+
selectedAgents: Set<string>;
|
|
446
|
+
error: string;
|
|
447
|
+
portChanged: boolean;
|
|
448
|
+
maskKey: (key: string) => string;
|
|
449
|
+
s: ReturnType<typeof useLocale>['t']['setup'];
|
|
450
|
+
}) {
|
|
451
|
+
const rows: [string, string][] = [
|
|
452
|
+
[s.kbPath, state.mindRoot],
|
|
453
|
+
[s.template, state.template || '—'],
|
|
454
|
+
[s.aiProvider, state.provider === 'skip' ? s.aiSkipTitle : state.provider],
|
|
455
|
+
...(state.provider !== 'skip' ? [
|
|
456
|
+
[s.apiKey, maskKey(state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey)] as [string, string],
|
|
457
|
+
[s.model, state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel] as [string, string],
|
|
458
|
+
] : []),
|
|
459
|
+
[s.webPort, String(state.webPort)],
|
|
460
|
+
[s.mcpPort, String(state.mcpPort)],
|
|
461
|
+
[s.authToken, state.authToken || '—'],
|
|
462
|
+
[s.webPassword, state.webPassword ? '••••••••' : '(none)'],
|
|
463
|
+
[s.agentToolsTitle, selectedAgents.size > 0 ? Array.from(selectedAgents).join(', ') : '—'],
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<div className="space-y-5">
|
|
468
|
+
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.reviewHint}</p>
|
|
469
|
+
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
470
|
+
{rows.map(([label, value], i) => (
|
|
471
|
+
<div key={i} className="flex items-center justify-between px-4 py-3 text-sm"
|
|
472
|
+
style={{
|
|
473
|
+
background: i % 2 === 0 ? 'var(--card)' : 'transparent',
|
|
474
|
+
borderTop: i > 0 ? '1px solid var(--border)' : undefined,
|
|
475
|
+
}}>
|
|
476
|
+
<span style={{ color: 'var(--muted-foreground)' }}>{label}</span>
|
|
477
|
+
<span className="font-mono text-xs" style={{ color: 'var(--foreground)' }}>{value}</span>
|
|
478
|
+
</div>
|
|
479
|
+
))}
|
|
480
|
+
</div>
|
|
481
|
+
{error && (
|
|
482
|
+
<div className="p-3 rounded-lg text-sm text-red-500" style={{ background: 'rgba(239,68,68,0.1)' }}>
|
|
483
|
+
{s.completeFailed}: {error}
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
{portChanged && (
|
|
487
|
+
<div className="space-y-3">
|
|
488
|
+
<div className="p-3 rounded-lg text-sm flex items-center gap-2"
|
|
489
|
+
style={{ background: 'rgba(200,135,30,0.1)', color: 'var(--amber)' }}>
|
|
490
|
+
<AlertTriangle size={14} /> {s.portChanged}
|
|
491
|
+
</div>
|
|
492
|
+
<a href="/" className="inline-flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors"
|
|
493
|
+
style={{ background: 'var(--amber)', color: 'white' }}>
|
|
494
|
+
{s.completeDone} →
|
|
495
|
+
</a>
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ─── Step dots ────────────────────────────────────────────────────────────────
|
|
503
|
+
function StepDots({ step, setStep, stepTitles }: {
|
|
504
|
+
step: number;
|
|
505
|
+
setStep: (s: number) => void;
|
|
506
|
+
stepTitles: readonly string[];
|
|
507
|
+
}) {
|
|
508
|
+
return (
|
|
509
|
+
<div className="flex items-center gap-2 mb-8">
|
|
510
|
+
{stepTitles.map((title: string, i: number) => (
|
|
511
|
+
<div key={i} className="flex items-center gap-2">
|
|
512
|
+
{i > 0 && <div className="w-8 h-px" style={{ background: i <= step ? 'var(--amber)' : 'var(--border)' }} />}
|
|
513
|
+
<button onClick={() => i < step && setStep(i)} className="flex items-center gap-1.5" disabled={i > step}>
|
|
514
|
+
<div
|
|
515
|
+
className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
|
|
516
|
+
style={{
|
|
517
|
+
background: i <= step ? 'var(--amber)' : 'var(--muted)',
|
|
518
|
+
color: i <= step ? 'white' : 'var(--muted-foreground)',
|
|
519
|
+
opacity: i <= step ? 1 : 0.5,
|
|
520
|
+
}}>
|
|
521
|
+
{i + 1}
|
|
522
|
+
</div>
|
|
523
|
+
<span className="text-xs hidden sm:inline"
|
|
524
|
+
style={{ color: i === step ? 'var(--foreground)' : 'var(--muted-foreground)', opacity: i <= step ? 1 : 0.5 }}>
|
|
525
|
+
{title}
|
|
526
|
+
</span>
|
|
527
|
+
</button>
|
|
528
|
+
</div>
|
|
529
|
+
))}
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ─── Main component ───────────────────────────────────────────────────────────
|
|
179
535
|
export default function SetupWizard() {
|
|
180
536
|
const { t } = useLocale();
|
|
181
537
|
const s = t.setup;
|
|
@@ -200,17 +556,14 @@ export default function SetupWizard() {
|
|
|
200
556
|
const [error, setError] = useState('');
|
|
201
557
|
const [portChanged, setPortChanged] = useState(false);
|
|
202
558
|
|
|
203
|
-
// Port availability
|
|
204
559
|
const [webPortStatus, setWebPortStatus] = useState<PortStatus>({ checking: false, available: null, suggestion: null });
|
|
205
560
|
const [mcpPortStatus, setMcpPortStatus] = useState<PortStatus>({ checking: false, available: null, suggestion: null });
|
|
206
561
|
|
|
207
|
-
// Agent Tools
|
|
208
562
|
const [agents, setAgents] = useState<AgentEntry[]>([]);
|
|
209
563
|
const [agentsLoading, setAgentsLoading] = useState(false);
|
|
210
564
|
const [selectedAgents, setSelectedAgents] = useState<Set<string>>(new Set());
|
|
211
565
|
const [agentTransport, setAgentTransport] = useState<'stdio' | 'http'>('stdio');
|
|
212
566
|
const [agentScope, setAgentScope] = useState<'global' | 'project'>('global');
|
|
213
|
-
// Live per-agent install status (shown inline in Step 5 during/after submit)
|
|
214
567
|
const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentInstallStatus>>({});
|
|
215
568
|
|
|
216
569
|
// Generate token on mount
|
|
@@ -288,11 +641,34 @@ export default function SetupWizard() {
|
|
|
288
641
|
}
|
|
289
642
|
}, []);
|
|
290
643
|
|
|
644
|
+
const maskKey = (key: string) => {
|
|
645
|
+
if (!key) return '(not set)';
|
|
646
|
+
if (key.length <= 8) return '•••';
|
|
647
|
+
return key.slice(0, 6) + '•••' + key.slice(-3);
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const portConflict = state.webPort === state.mcpPort;
|
|
651
|
+
|
|
652
|
+
const canNext = () => {
|
|
653
|
+
if (step === STEP_KB) return state.mindRoot.trim().length > 0;
|
|
654
|
+
if (step === STEP_PORTS) {
|
|
655
|
+
if (portConflict) return false;
|
|
656
|
+
if (webPortStatus.checking || mcpPortStatus.checking) return false;
|
|
657
|
+
if (webPortStatus.available !== true || mcpPortStatus.available !== true) return false;
|
|
658
|
+
return (
|
|
659
|
+
state.webPort >= 1024 && state.webPort <= 65535 &&
|
|
660
|
+
state.mcpPort >= 1024 && state.mcpPort <= 65535
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
return true;
|
|
664
|
+
};
|
|
665
|
+
|
|
291
666
|
const handleComplete = async () => {
|
|
292
667
|
setSubmitting(true);
|
|
293
668
|
setError('');
|
|
669
|
+
let didPortChange = false;
|
|
294
670
|
|
|
295
|
-
// 1. Save setup config
|
|
671
|
+
// 1. Save setup config
|
|
296
672
|
try {
|
|
297
673
|
const payload = {
|
|
298
674
|
mindRoot: state.mindRoot,
|
|
@@ -316,16 +692,16 @@ export default function SetupWizard() {
|
|
|
316
692
|
});
|
|
317
693
|
const data = await res.json();
|
|
318
694
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
319
|
-
|
|
695
|
+
didPortChange = !!data.portChanged;
|
|
696
|
+
if (didPortChange) setPortChanged(true);
|
|
320
697
|
} catch (e) {
|
|
321
698
|
setError(e instanceof Error ? e.message : String(e));
|
|
322
699
|
setSubmitting(false);
|
|
323
700
|
return;
|
|
324
701
|
}
|
|
325
702
|
|
|
326
|
-
// 2. Install agents after config saved
|
|
703
|
+
// 2. Install agents after config saved
|
|
327
704
|
if (selectedAgents.size > 0) {
|
|
328
|
-
// Mark all selected as "installing"
|
|
329
705
|
const initialStatuses: Record<string, AgentInstallStatus> = {};
|
|
330
706
|
for (const key of selectedAgents) initialStatuses[key] = { state: 'installing' };
|
|
331
707
|
setAgentStatuses(initialStatuses);
|
|
@@ -346,15 +722,11 @@ export default function SetupWizard() {
|
|
|
346
722
|
if (data.results) {
|
|
347
723
|
const updated: Record<string, AgentInstallStatus> = {};
|
|
348
724
|
for (const r of data.results as Array<{ agent: string; status: string; message?: string }>) {
|
|
349
|
-
updated[r.agent] = {
|
|
350
|
-
state: r.status === 'ok' ? 'ok' : 'error',
|
|
351
|
-
message: r.message,
|
|
352
|
-
};
|
|
725
|
+
updated[r.agent] = { state: r.status === 'ok' ? 'ok' : 'error', message: r.message };
|
|
353
726
|
}
|
|
354
727
|
setAgentStatuses(updated);
|
|
355
728
|
}
|
|
356
729
|
} catch {
|
|
357
|
-
// Mark all as error
|
|
358
730
|
const errStatuses: Record<string, AgentInstallStatus> = {};
|
|
359
731
|
for (const key of selectedAgents) errStatuses[key] = { state: 'error' };
|
|
360
732
|
setAgentStatuses(errStatuses);
|
|
@@ -362,396 +734,18 @@ export default function SetupWizard() {
|
|
|
362
734
|
}
|
|
363
735
|
|
|
364
736
|
setSubmitting(false);
|
|
365
|
-
if (!portChanged) window.location.href = '/';
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const portConflict = state.webPort === state.mcpPort;
|
|
369
737
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (portConflict) return false;
|
|
374
|
-
if (webPortStatus.checking || mcpPortStatus.checking) return false;
|
|
375
|
-
if (webPortStatus.available !== true || mcpPortStatus.available !== true) return false;
|
|
376
|
-
return (
|
|
377
|
-
state.webPort >= 1024 && state.webPort <= 65535 &&
|
|
378
|
-
state.mcpPort >= 1024 && state.mcpPort <= 65535
|
|
379
|
-
);
|
|
738
|
+
if (didPortChange) {
|
|
739
|
+
// Port changed — stay on page, show restart hint
|
|
740
|
+
return;
|
|
380
741
|
}
|
|
381
|
-
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
const maskKey = (key: string) => {
|
|
385
|
-
if (!key) return '(not set)';
|
|
386
|
-
if (key.length <= 8) return '•••';
|
|
387
|
-
return key.slice(0, 6) + '•••' + key.slice(-3);
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
// ----------------------------------------------------------------
|
|
391
|
-
// Step dots
|
|
392
|
-
// ----------------------------------------------------------------
|
|
393
|
-
const StepDots = () => (
|
|
394
|
-
<div className="flex items-center gap-2 mb-8">
|
|
395
|
-
{s.stepTitles.map((title: string, i: number) => (
|
|
396
|
-
<div key={i} className="flex items-center gap-2">
|
|
397
|
-
{i > 0 && <div className="w-8 h-px" style={{ background: i <= step ? 'var(--amber)' : 'var(--border)' }} />}
|
|
398
|
-
<button onClick={() => i < step && setStep(i)} className="flex items-center gap-1.5" disabled={i > step}>
|
|
399
|
-
<div
|
|
400
|
-
className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
|
|
401
|
-
style={{
|
|
402
|
-
background: i <= step ? 'var(--amber)' : 'var(--muted)',
|
|
403
|
-
color: i <= step ? 'white' : 'var(--muted-foreground)',
|
|
404
|
-
opacity: i <= step ? 1 : 0.5,
|
|
405
|
-
}}
|
|
406
|
-
>
|
|
407
|
-
{i + 1}
|
|
408
|
-
</div>
|
|
409
|
-
<span className="text-xs hidden sm:inline"
|
|
410
|
-
style={{ color: i === step ? 'var(--foreground)' : 'var(--muted-foreground)', opacity: i <= step ? 1 : 0.5 }}>
|
|
411
|
-
{title}
|
|
412
|
-
</span>
|
|
413
|
-
</button>
|
|
414
|
-
</div>
|
|
415
|
-
))}
|
|
416
|
-
</div>
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
// ----------------------------------------------------------------
|
|
420
|
-
// Step 1: Knowledge Base
|
|
421
|
-
// ----------------------------------------------------------------
|
|
422
|
-
const Step1 = () => (
|
|
423
|
-
<div className="space-y-6">
|
|
424
|
-
<Field label={s.kbPath} hint={s.kbPathHint}>
|
|
425
|
-
<Input value={state.mindRoot} onChange={e => update('mindRoot', e.target.value)} placeholder={s.kbPathDefault} />
|
|
426
|
-
</Field>
|
|
427
|
-
<div>
|
|
428
|
-
<label className="text-sm text-foreground font-medium mb-3 block">{s.template}</label>
|
|
429
|
-
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
430
|
-
{TEMPLATES.map(tpl => (
|
|
431
|
-
<button key={tpl.id} onClick={() => update('template', tpl.id)}
|
|
432
|
-
className="flex flex-col items-start gap-2 p-4 rounded-xl border text-left transition-all duration-150"
|
|
433
|
-
style={{
|
|
434
|
-
background: state.template === tpl.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
|
|
435
|
-
borderColor: state.template === tpl.id ? 'var(--amber)' : 'var(--border)',
|
|
436
|
-
}}>
|
|
437
|
-
<div className="flex items-center gap-2">
|
|
438
|
-
<span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
|
|
439
|
-
<span className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>
|
|
440
|
-
{t.onboarding.templates[tpl.id as 'en' | 'zh' | 'empty'].title}
|
|
441
|
-
</span>
|
|
442
|
-
</div>
|
|
443
|
-
<div className="w-full rounded-lg px-2.5 py-1.5 text-[11px] leading-relaxed font-display"
|
|
444
|
-
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
|
|
445
|
-
{tpl.dirs.map(d => <div key={d}>{d}</div>)}
|
|
446
|
-
</div>
|
|
447
|
-
</button>
|
|
448
|
-
))}
|
|
449
|
-
</div>
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
// ----------------------------------------------------------------
|
|
455
|
-
// Step 2: AI Provider — card-based selection including skip
|
|
456
|
-
// ----------------------------------------------------------------
|
|
457
|
-
const PROVIDERS = [
|
|
458
|
-
{
|
|
459
|
-
id: 'anthropic' as const,
|
|
460
|
-
icon: <Brain size={18} />,
|
|
461
|
-
label: 'Anthropic',
|
|
462
|
-
desc: 'Claude — claude-sonnet-4-6',
|
|
463
|
-
},
|
|
464
|
-
{
|
|
465
|
-
id: 'openai' as const,
|
|
466
|
-
icon: <Zap size={18} />,
|
|
467
|
-
label: 'OpenAI',
|
|
468
|
-
desc: 'GPT or any OpenAI-compatible API',
|
|
469
|
-
},
|
|
470
|
-
{
|
|
471
|
-
id: 'skip' as const,
|
|
472
|
-
icon: <SkipForward size={18} />,
|
|
473
|
-
label: s.aiSkipTitle,
|
|
474
|
-
desc: s.aiSkipDesc,
|
|
475
|
-
},
|
|
476
|
-
];
|
|
477
|
-
|
|
478
|
-
const Step2 = () => (
|
|
479
|
-
<div className="space-y-5">
|
|
480
|
-
<div className="grid grid-cols-1 gap-3">
|
|
481
|
-
{PROVIDERS.map(p => (
|
|
482
|
-
<button key={p.id} onClick={() => update('provider', p.id)}
|
|
483
|
-
className="flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150"
|
|
484
|
-
style={{
|
|
485
|
-
background: state.provider === p.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
|
|
486
|
-
borderColor: state.provider === p.id ? 'var(--amber)' : 'var(--border)',
|
|
487
|
-
}}>
|
|
488
|
-
<span className="mt-0.5" style={{ color: state.provider === p.id ? 'var(--amber)' : 'var(--muted-foreground)' }}>
|
|
489
|
-
{p.icon}
|
|
490
|
-
</span>
|
|
491
|
-
<div>
|
|
492
|
-
<p className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>{p.label}</p>
|
|
493
|
-
<p className="text-xs mt-0.5" style={{ color: 'var(--muted-foreground)' }}>{p.desc}</p>
|
|
494
|
-
</div>
|
|
495
|
-
{state.provider === p.id && (
|
|
496
|
-
<CheckCircle2 size={16} className="ml-auto mt-0.5 shrink-0" style={{ color: 'var(--amber)' }} />
|
|
497
|
-
)}
|
|
498
|
-
</button>
|
|
499
|
-
))}
|
|
500
|
-
</div>
|
|
501
|
-
|
|
502
|
-
{state.provider !== 'skip' && (
|
|
503
|
-
<div className="space-y-4 pt-2">
|
|
504
|
-
<Field label={s.apiKey}>
|
|
505
|
-
<ApiKeyInput
|
|
506
|
-
value={state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey}
|
|
507
|
-
onChange={v => update(state.provider === 'anthropic' ? 'anthropicKey' : 'openaiKey', v)}
|
|
508
|
-
placeholder={state.provider === 'anthropic' ? 'sk-ant-...' : 'sk-...'}
|
|
509
|
-
/>
|
|
510
|
-
</Field>
|
|
511
|
-
<Field label={s.model}>
|
|
512
|
-
<Input
|
|
513
|
-
value={state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel}
|
|
514
|
-
onChange={e => update(state.provider === 'anthropic' ? 'anthropicModel' : 'openaiModel', e.target.value)}
|
|
515
|
-
/>
|
|
516
|
-
</Field>
|
|
517
|
-
{state.provider === 'openai' && (
|
|
518
|
-
<Field label={s.baseUrl} hint={s.baseUrlHint}>
|
|
519
|
-
<Input value={state.openaiBaseUrl} onChange={e => update('openaiBaseUrl', e.target.value)}
|
|
520
|
-
placeholder="https://api.openai.com/v1" />
|
|
521
|
-
</Field>
|
|
522
|
-
)}
|
|
523
|
-
</div>
|
|
524
|
-
)}
|
|
525
|
-
</div>
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
// ----------------------------------------------------------------
|
|
529
|
-
// Step 3: Ports
|
|
530
|
-
// ----------------------------------------------------------------
|
|
531
|
-
const Step3 = () => (
|
|
532
|
-
<div className="space-y-5">
|
|
533
|
-
<PortField
|
|
534
|
-
label={s.webPort} hint={s.portHint} value={state.webPort}
|
|
535
|
-
onChange={v => { update('webPort', v); setWebPortStatus({ checking: false, available: null, suggestion: null }); }}
|
|
536
|
-
status={webPortStatus}
|
|
537
|
-
onCheckPort={port => checkPort(port, 'web')}
|
|
538
|
-
s={s}
|
|
539
|
-
/>
|
|
540
|
-
<PortField
|
|
541
|
-
label={s.mcpPort} hint={s.portHint} value={state.mcpPort}
|
|
542
|
-
onChange={v => { update('mcpPort', v); setMcpPortStatus({ checking: false, available: null, suggestion: null }); }}
|
|
543
|
-
status={mcpPortStatus}
|
|
544
|
-
onCheckPort={port => checkPort(port, 'mcp')}
|
|
545
|
-
s={s}
|
|
546
|
-
/>
|
|
547
|
-
{portConflict && (
|
|
548
|
-
<p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--amber)' }}>
|
|
549
|
-
<AlertTriangle size={12} /> {s.portConflict}
|
|
550
|
-
</p>
|
|
551
|
-
)}
|
|
552
|
-
{!portConflict && (webPortStatus.available === null || mcpPortStatus.available === null) && !webPortStatus.checking && !mcpPortStatus.checking && (
|
|
553
|
-
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.portVerifyHint}</p>
|
|
554
|
-
)}
|
|
555
|
-
<p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
|
|
556
|
-
<AlertTriangle size={12} /> {s.portRestartWarning}
|
|
557
|
-
</p>
|
|
558
|
-
</div>
|
|
559
|
-
);
|
|
560
|
-
|
|
561
|
-
// ----------------------------------------------------------------
|
|
562
|
-
// Step 5: Agent Tools
|
|
563
|
-
// ----------------------------------------------------------------
|
|
564
|
-
const Step5 = () => {
|
|
565
|
-
const toggleAgent = (key: string) => {
|
|
566
|
-
setSelectedAgents(prev => {
|
|
567
|
-
const next = new Set(prev);
|
|
568
|
-
if (next.has(key)) next.delete(key); else next.add(key);
|
|
569
|
-
return next;
|
|
570
|
-
});
|
|
571
|
-
};
|
|
572
|
-
|
|
573
|
-
const getStatusBadge = (key: string, installed: boolean) => {
|
|
574
|
-
const st = agentStatuses[key];
|
|
575
|
-
|
|
576
|
-
// Show install result if we've run setup
|
|
577
|
-
if (st) {
|
|
578
|
-
if (st.state === 'installing') return (
|
|
579
|
-
<span className="flex items-center gap-1 text-[11px]" style={{ color: 'var(--muted-foreground)' }}>
|
|
580
|
-
<Loader2 size={10} className="animate-spin" /> {s.agentInstalling}
|
|
581
|
-
</span>
|
|
582
|
-
);
|
|
583
|
-
if (st.state === 'ok') return (
|
|
584
|
-
<span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
|
|
585
|
-
style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
|
|
586
|
-
<CheckCircle2 size={10} /> {s.agentStatusOk}
|
|
587
|
-
</span>
|
|
588
|
-
);
|
|
589
|
-
if (st.state === 'error') return (
|
|
590
|
-
<span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
|
|
591
|
-
style={{ background: 'rgba(239,68,68,0.1)', color: '#ef4444' }}>
|
|
592
|
-
<XCircle size={10} /> {s.agentStatusError}
|
|
593
|
-
{st.message && <span className="ml-1 text-[10px]">({st.message})</span>}
|
|
594
|
-
</span>
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Show app install status (before setup runs)
|
|
599
|
-
if (installed) return (
|
|
600
|
-
<span className="text-[11px] px-1.5 py-0.5 rounded"
|
|
601
|
-
style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
|
|
602
|
-
{t.settings.mcp.installed}
|
|
603
|
-
</span>
|
|
604
|
-
);
|
|
605
|
-
return (
|
|
606
|
-
<span className="text-[11px] px-1.5 py-0.5 rounded"
|
|
607
|
-
style={{ background: 'rgba(100,100,120,0.1)', color: 'var(--muted-foreground)' }}>
|
|
608
|
-
{s.agentNotInstalled}
|
|
609
|
-
</span>
|
|
610
|
-
);
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
return (
|
|
614
|
-
<div className="space-y-5">
|
|
615
|
-
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.agentToolsHint}</p>
|
|
616
|
-
|
|
617
|
-
{agentsLoading ? (
|
|
618
|
-
<div className="flex items-center gap-2 py-4" style={{ color: 'var(--muted-foreground)' }}>
|
|
619
|
-
<Loader2 size={14} className="animate-spin" />
|
|
620
|
-
<span className="text-sm">{s.agentToolsLoading}</span>
|
|
621
|
-
</div>
|
|
622
|
-
) : agents.length === 0 ? (
|
|
623
|
-
<p className="text-sm py-4 text-center" style={{ color: 'var(--muted-foreground)' }}>
|
|
624
|
-
{s.agentToolsEmpty}
|
|
625
|
-
</p>
|
|
626
|
-
) : (
|
|
627
|
-
<>
|
|
628
|
-
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
629
|
-
{agents.map((agent, i) => (
|
|
630
|
-
<label key={agent.key}
|
|
631
|
-
className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
|
|
632
|
-
style={{
|
|
633
|
-
background: i % 2 === 0 ? 'var(--card)' : 'transparent',
|
|
634
|
-
borderTop: i > 0 ? '1px solid var(--border)' : undefined,
|
|
635
|
-
}}>
|
|
636
|
-
<input
|
|
637
|
-
type="checkbox"
|
|
638
|
-
checked={selectedAgents.has(agent.key)}
|
|
639
|
-
onChange={() => toggleAgent(agent.key)}
|
|
640
|
-
className="accent-amber-500"
|
|
641
|
-
disabled={agentStatuses[agent.key]?.state === 'installing'}
|
|
642
|
-
/>
|
|
643
|
-
<span className="text-sm flex-1" style={{ color: 'var(--foreground)' }}>{agent.name}</span>
|
|
644
|
-
{getStatusBadge(agent.key, agent.installed)}
|
|
645
|
-
</label>
|
|
646
|
-
))}
|
|
647
|
-
</div>
|
|
648
|
-
|
|
649
|
-
<div className="grid grid-cols-2 gap-4">
|
|
650
|
-
<Field label={s.agentTransport}>
|
|
651
|
-
<Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'stdio' | 'http')}>
|
|
652
|
-
<option value="stdio">{t.settings.mcp.transportStdio}</option>
|
|
653
|
-
<option value="http">{t.settings.mcp.transportHttp}</option>
|
|
654
|
-
</Select>
|
|
655
|
-
</Field>
|
|
656
|
-
<Field label={s.agentScope}>
|
|
657
|
-
<Select value={agentScope} onChange={e => setAgentScope(e.target.value as 'global' | 'project')}>
|
|
658
|
-
<option value="global">{t.settings.mcp.global}</option>
|
|
659
|
-
<option value="project">{t.settings.mcp.project}</option>
|
|
660
|
-
</Select>
|
|
661
|
-
</Field>
|
|
662
|
-
</div>
|
|
663
|
-
|
|
664
|
-
{selectedAgents.size === 0 && (
|
|
665
|
-
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.agentNoneSelected}</p>
|
|
666
|
-
)}
|
|
667
|
-
</>
|
|
668
|
-
)}
|
|
669
|
-
</div>
|
|
670
|
-
);
|
|
742
|
+
window.location.href = '/';
|
|
671
743
|
};
|
|
672
744
|
|
|
673
|
-
// ----------------------------------------------------------------
|
|
674
|
-
// Step 6: Review
|
|
675
|
-
// ----------------------------------------------------------------
|
|
676
|
-
const Step6 = () => {
|
|
677
|
-
const rows: [string, string][] = [
|
|
678
|
-
[s.kbPath, state.mindRoot],
|
|
679
|
-
[s.template, state.template || '—'],
|
|
680
|
-
[s.aiProvider, state.provider === 'skip' ? s.aiSkipTitle : state.provider],
|
|
681
|
-
...(state.provider !== 'skip' ? [
|
|
682
|
-
[s.apiKey, maskKey(state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey)] as [string, string],
|
|
683
|
-
[s.model, state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel] as [string, string],
|
|
684
|
-
] : []),
|
|
685
|
-
[s.webPort, String(state.webPort)],
|
|
686
|
-
[s.mcpPort, String(state.mcpPort)],
|
|
687
|
-
[s.authToken, state.authToken || '—'],
|
|
688
|
-
[s.webPassword, state.webPassword ? '••••••••' : '(none)'],
|
|
689
|
-
[s.agentToolsTitle, selectedAgents.size > 0 ? Array.from(selectedAgents).join(', ') : '—'],
|
|
690
|
-
];
|
|
691
|
-
|
|
692
|
-
return (
|
|
693
|
-
<div className="space-y-5">
|
|
694
|
-
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.reviewHint}</p>
|
|
695
|
-
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
696
|
-
{rows.map(([label, value], i) => (
|
|
697
|
-
<div key={i} className="flex items-center justify-between px-4 py-3 text-sm"
|
|
698
|
-
style={{
|
|
699
|
-
background: i % 2 === 0 ? 'var(--card)' : 'transparent',
|
|
700
|
-
borderTop: i > 0 ? '1px solid var(--border)' : undefined,
|
|
701
|
-
}}>
|
|
702
|
-
<span style={{ color: 'var(--muted-foreground)' }}>{label}</span>
|
|
703
|
-
<span className="font-mono text-xs" style={{ color: 'var(--foreground)' }}>{value}</span>
|
|
704
|
-
</div>
|
|
705
|
-
))}
|
|
706
|
-
</div>
|
|
707
|
-
|
|
708
|
-
{error && (
|
|
709
|
-
<div className="p-3 rounded-lg text-sm text-red-500" style={{ background: 'rgba(239,68,68,0.1)' }}>
|
|
710
|
-
{s.completeFailed}: {error}
|
|
711
|
-
</div>
|
|
712
|
-
)}
|
|
713
|
-
|
|
714
|
-
{portChanged && (
|
|
715
|
-
<div className="space-y-3">
|
|
716
|
-
<div className="p-3 rounded-lg text-sm flex items-center gap-2"
|
|
717
|
-
style={{ background: 'rgba(200,135,30,0.1)', color: 'var(--amber)' }}>
|
|
718
|
-
<AlertTriangle size={14} /> {s.portChanged}
|
|
719
|
-
</div>
|
|
720
|
-
<a href="/" className="inline-flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors"
|
|
721
|
-
style={{ background: 'var(--amber)', color: 'white' }}>
|
|
722
|
-
{s.completeDone} →
|
|
723
|
-
</a>
|
|
724
|
-
</div>
|
|
725
|
-
)}
|
|
726
|
-
</div>
|
|
727
|
-
);
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
const steps = [
|
|
731
|
-
Step1,
|
|
732
|
-
Step2,
|
|
733
|
-
Step3,
|
|
734
|
-
() => (
|
|
735
|
-
<Step4Inner
|
|
736
|
-
authToken={state.authToken}
|
|
737
|
-
tokenCopied={tokenCopied}
|
|
738
|
-
onCopy={copyToken}
|
|
739
|
-
onGenerate={generateToken}
|
|
740
|
-
webPassword={state.webPassword}
|
|
741
|
-
onPasswordChange={v => update('webPassword', v)}
|
|
742
|
-
s={s}
|
|
743
|
-
/>
|
|
744
|
-
),
|
|
745
|
-
Step5,
|
|
746
|
-
Step6,
|
|
747
|
-
];
|
|
748
|
-
const CurrentStep = steps[step];
|
|
749
|
-
|
|
750
745
|
return (
|
|
751
746
|
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
|
752
747
|
style={{ background: 'var(--background)' }}>
|
|
753
748
|
<div className="w-full max-w-xl mx-auto px-6 py-12">
|
|
754
|
-
{/* Header */}
|
|
755
749
|
<div className="text-center mb-8">
|
|
756
750
|
<div className="inline-flex items-center gap-2 mb-2">
|
|
757
751
|
<Sparkles size={18} style={{ color: 'var(--amber)' }} />
|
|
@@ -762,14 +756,47 @@ export default function SetupWizard() {
|
|
|
762
756
|
</div>
|
|
763
757
|
|
|
764
758
|
<div className="flex justify-center">
|
|
765
|
-
<StepDots />
|
|
759
|
+
<StepDots step={step} setStep={setStep} stepTitles={s.stepTitles} />
|
|
766
760
|
</div>
|
|
767
761
|
|
|
768
762
|
<h2 className="text-lg font-semibold mb-5" style={{ color: 'var(--foreground)' }}>
|
|
769
763
|
{s.stepTitles[step]}
|
|
770
764
|
</h2>
|
|
771
765
|
|
|
772
|
-
<
|
|
766
|
+
{step === 0 && <Step1 state={state} update={update} t={t} />}
|
|
767
|
+
{step === 1 && <Step2 state={state} update={update} s={s} />}
|
|
768
|
+
{step === 2 && (
|
|
769
|
+
<Step3
|
|
770
|
+
state={state} update={update}
|
|
771
|
+
webPortStatus={webPortStatus} mcpPortStatus={mcpPortStatus}
|
|
772
|
+
setWebPortStatus={setWebPortStatus} setMcpPortStatus={setMcpPortStatus}
|
|
773
|
+
checkPort={checkPort} portConflict={portConflict} s={s}
|
|
774
|
+
/>
|
|
775
|
+
)}
|
|
776
|
+
{step === 3 && (
|
|
777
|
+
<Step4Inner
|
|
778
|
+
authToken={state.authToken} tokenCopied={tokenCopied}
|
|
779
|
+
onCopy={copyToken} onGenerate={generateToken}
|
|
780
|
+
webPassword={state.webPassword} onPasswordChange={v => update('webPassword', v)}
|
|
781
|
+
s={s}
|
|
782
|
+
/>
|
|
783
|
+
)}
|
|
784
|
+
{step === 4 && (
|
|
785
|
+
<Step5
|
|
786
|
+
agents={agents} agentsLoading={agentsLoading}
|
|
787
|
+
selectedAgents={selectedAgents} setSelectedAgents={setSelectedAgents}
|
|
788
|
+
agentTransport={agentTransport} setAgentTransport={setAgentTransport}
|
|
789
|
+
agentScope={agentScope} setAgentScope={setAgentScope}
|
|
790
|
+
agentStatuses={agentStatuses} s={s} settingsMcp={t.settings.mcp}
|
|
791
|
+
/>
|
|
792
|
+
)}
|
|
793
|
+
{step === 5 && (
|
|
794
|
+
<Step6
|
|
795
|
+
state={state} selectedAgents={selectedAgents}
|
|
796
|
+
error={error} portChanged={portChanged}
|
|
797
|
+
maskKey={maskKey} s={s}
|
|
798
|
+
/>
|
|
799
|
+
)}
|
|
773
800
|
|
|
774
801
|
{/* Navigation */}
|
|
775
802
|
<div className="flex items-center justify-between mt-8 pt-6" style={{ borderTop: '1px solid var(--border)' }}>
|
package/package.json
CHANGED