@geminilight/mindos 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/app/api/mcp/agents/route.ts +72 -0
- package/app/app/api/mcp/install/route.ts +95 -0
- package/app/app/api/mcp/status/route.ts +47 -0
- package/app/app/api/setup/check-port/route.ts +41 -0
- package/app/app/api/skills/route.ts +208 -0
- package/app/app/api/sync/route.ts +54 -3
- package/app/app/api/update-check/route.ts +52 -0
- package/app/app/globals.css +12 -0
- package/app/app/layout.tsx +4 -2
- package/app/app/login/page.tsx +20 -13
- package/app/app/page.tsx +19 -2
- package/app/app/setup/page.tsx +2 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
- package/app/app/view/[...path]/loading.tsx +1 -1
- package/app/app/view/[...path]/not-found.tsx +101 -0
- package/app/components/AskFab.tsx +1 -1
- package/app/components/AskModal.tsx +1 -1
- package/app/components/Backlinks.tsx +1 -1
- package/app/components/Breadcrumb.tsx +13 -3
- package/app/components/CsvView.tsx +5 -6
- package/app/components/DirView.tsx +42 -21
- package/app/components/FindInPage.tsx +211 -0
- package/app/components/HomeContent.tsx +97 -44
- package/app/components/JsonView.tsx +1 -2
- package/app/components/MarkdownEditor.tsx +1 -2
- package/app/components/OnboardingView.tsx +6 -7
- package/app/components/SettingsModal.tsx +5 -2
- package/app/components/SetupWizard.tsx +499 -172
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/UpdateBanner.tsx +101 -0
- package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
- package/app/components/renderers/agent-inspector/manifest.ts +14 -0
- package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
- package/app/components/renderers/backlinks/manifest.ts +14 -0
- package/app/components/renderers/config/manifest.ts +14 -0
- package/app/components/renderers/csv/BoardView.tsx +12 -12
- package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
- package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
- package/app/components/renderers/csv/GalleryView.tsx +3 -3
- package/app/components/renderers/csv/TableView.tsx +4 -5
- package/app/components/renderers/csv/manifest.ts +14 -0
- package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
- package/app/components/renderers/diff/manifest.ts +14 -0
- package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
- package/app/components/renderers/graph/manifest.ts +14 -0
- package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
- package/app/components/renderers/summary/manifest.ts +14 -0
- package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
- package/app/components/renderers/timeline/manifest.ts +14 -0
- package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
- package/app/components/renderers/todo/manifest.ts +14 -0
- package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
- package/app/components/renderers/workflow/manifest.ts +14 -0
- package/app/components/settings/McpTab.tsx +549 -0
- package/app/components/settings/SyncTab.tsx +139 -50
- package/app/components/settings/types.ts +1 -1
- package/app/data/pages/home.png +0 -0
- package/app/lib/i18n.ts +226 -19
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +3 -0
- package/app/package.json +1 -0
- package/app/types/semver.d.ts +8 -0
- package/bin/cli.js +137 -24
- package/bin/lib/build.js +53 -18
- package/bin/lib/colors.js +3 -1
- package/bin/lib/config.js +4 -0
- package/bin/lib/constants.js +2 -0
- package/bin/lib/debug.js +10 -0
- package/bin/lib/mcp-install.js +4 -1
- package/bin/lib/port.js +8 -2
- package/bin/lib/startup.js +21 -20
- package/bin/lib/stop.js +41 -3
- package/bin/lib/sync.js +65 -53
- package/bin/lib/update-check.js +94 -0
- package/bin/lib/utils.js +2 -2
- package/package.json +1 -1
- package/scripts/gen-renderer-index.js +57 -0
- package/scripts/setup.js +205 -10
- /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
Sparkles, Globe, BookOpen, FileText, Copy, Check, RefreshCw,
|
|
6
|
+
Loader2, ChevronLeft, ChevronRight, AlertTriangle, CheckCircle2,
|
|
7
|
+
XCircle, Zap, Brain, SkipForward,
|
|
8
|
+
} from 'lucide-react';
|
|
6
9
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
10
|
import { Field, Input, Select, ApiKeyInput } from '@/components/settings/Primitives';
|
|
8
11
|
|
|
@@ -23,21 +26,158 @@ interface SetupState {
|
|
|
23
26
|
webPassword: string;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
29
|
+
interface PortStatus {
|
|
30
|
+
checking: boolean;
|
|
31
|
+
available: boolean | null; // null = not yet checked
|
|
32
|
+
suggestion: number | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AgentEntry {
|
|
36
|
+
key: string;
|
|
37
|
+
name: string;
|
|
38
|
+
installed: boolean;
|
|
39
|
+
hasProjectScope: boolean;
|
|
40
|
+
hasGlobalScope: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Per-agent install tracking (live, in Step 5)
|
|
44
|
+
type AgentInstallState = 'pending' | 'installing' | 'ok' | 'error';
|
|
45
|
+
interface AgentInstallStatus {
|
|
46
|
+
state: AgentInstallState;
|
|
47
|
+
message?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const TEMPLATES: Array<{ id: Template; icon: React.ReactNode; dirs: string[] }> = [
|
|
31
51
|
{ id: 'en', icon: <Globe size={18} />, dirs: ['Profile/', 'Connections/', 'Notes/', 'Workflows/', 'Resources/', 'Projects/'] },
|
|
32
52
|
{ id: 'zh', icon: <BookOpen size={18} />, dirs: ['画像/', '关系/', '笔记/', '流程/', '资源/', '项目/'] },
|
|
33
53
|
{ id: 'empty', icon: <FileText size={18} />, dirs: ['README.md', 'CONFIG.json', 'INSTRUCTION.md'] },
|
|
34
54
|
];
|
|
35
55
|
|
|
36
|
-
const TOTAL_STEPS =
|
|
56
|
+
const TOTAL_STEPS = 6;
|
|
57
|
+
const STEP_KB = 0;
|
|
58
|
+
const STEP_PORTS = 2;
|
|
59
|
+
const STEP_AGENTS = 4;
|
|
60
|
+
|
|
61
|
+
// -------------------------------------------------------------------
|
|
62
|
+
// Step4Inner — extracted so its local seed/showSeed state survives
|
|
63
|
+
// parent re-renders (declaring inside SetupWizard would remount it)
|
|
64
|
+
// -------------------------------------------------------------------
|
|
65
|
+
function Step4Inner({
|
|
66
|
+
authToken, tokenCopied, onCopy, onGenerate, webPassword, onPasswordChange, s,
|
|
67
|
+
}: {
|
|
68
|
+
authToken: string;
|
|
69
|
+
tokenCopied: boolean;
|
|
70
|
+
onCopy: () => void;
|
|
71
|
+
onGenerate: (seed?: string) => void;
|
|
72
|
+
webPassword: string;
|
|
73
|
+
onPasswordChange: (v: string) => void;
|
|
74
|
+
s: {
|
|
75
|
+
authToken: string; authTokenHint: string; authTokenSeed: string; authTokenSeedHint: string;
|
|
76
|
+
generateToken: string; copyToken: string; copiedToken: string;
|
|
77
|
+
webPassword: string; webPasswordHint: string;
|
|
78
|
+
};
|
|
79
|
+
}) {
|
|
80
|
+
const [seed, setSeed] = useState('');
|
|
81
|
+
const [showSeed, setShowSeed] = useState(false);
|
|
82
|
+
return (
|
|
83
|
+
<div className="space-y-5">
|
|
84
|
+
<Field label={s.authToken} hint={s.authTokenHint}>
|
|
85
|
+
<div className="flex gap-2">
|
|
86
|
+
<Input value={authToken} readOnly className="font-mono text-xs" />
|
|
87
|
+
<button onClick={onCopy}
|
|
88
|
+
className="flex items-center gap-1 px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
|
|
89
|
+
style={{ color: 'var(--foreground)' }}>
|
|
90
|
+
{tokenCopied ? <Check size={14} /> : <Copy size={14} />}
|
|
91
|
+
{tokenCopied ? s.copiedToken : s.copyToken}
|
|
92
|
+
</button>
|
|
93
|
+
<button onClick={() => onGenerate()}
|
|
94
|
+
className="flex items-center gap-1 px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
|
|
95
|
+
style={{ color: 'var(--foreground)' }}>
|
|
96
|
+
<RefreshCw size={14} />
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
</Field>
|
|
100
|
+
<div>
|
|
101
|
+
<button onClick={() => setShowSeed(!showSeed)} className="text-xs underline"
|
|
102
|
+
style={{ color: 'var(--muted-foreground)' }}>
|
|
103
|
+
{s.authTokenSeed}
|
|
104
|
+
</button>
|
|
105
|
+
{showSeed && (
|
|
106
|
+
<div className="mt-2 flex gap-2">
|
|
107
|
+
<Input value={seed} onChange={e => setSeed(e.target.value)} placeholder={s.authTokenSeedHint} />
|
|
108
|
+
<button onClick={() => { if (seed.trim()) onGenerate(seed); }}
|
|
109
|
+
className="px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
|
|
110
|
+
style={{ color: 'var(--foreground)' }}>
|
|
111
|
+
{s.generateToken}
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
<Field label={s.webPassword} hint={s.webPasswordHint}>
|
|
117
|
+
<Input type="password" value={webPassword} onChange={e => onPasswordChange(e.target.value)} placeholder="(optional)" />
|
|
118
|
+
</Field>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
37
122
|
|
|
123
|
+
// -------------------------------------------------------------------
|
|
124
|
+
// PortField — input + inline availability badge + suggestion button
|
|
125
|
+
// -------------------------------------------------------------------
|
|
126
|
+
function PortField({
|
|
127
|
+
label, hint, value, onChange, status, onCheckPort, s,
|
|
128
|
+
}: {
|
|
129
|
+
label: string; hint: string; value: number;
|
|
130
|
+
onChange: (v: number) => void;
|
|
131
|
+
status: PortStatus;
|
|
132
|
+
onCheckPort: (port: number) => void;
|
|
133
|
+
s: { portChecking: string; portInUse: (p: number) => string; portSuggest: (p: number) => string; portAvailable: string };
|
|
134
|
+
}) {
|
|
135
|
+
return (
|
|
136
|
+
<Field label={label} hint={hint}>
|
|
137
|
+
<div className="space-y-1.5">
|
|
138
|
+
<Input
|
|
139
|
+
type="number" min={1024} max={65535} value={value}
|
|
140
|
+
onChange={e => onChange(parseInt(e.target.value, 10) || value)}
|
|
141
|
+
onBlur={() => onCheckPort(value)}
|
|
142
|
+
/>
|
|
143
|
+
{status.checking && (
|
|
144
|
+
<p className="text-xs flex items-center gap-1" style={{ color: 'var(--muted-foreground)' }}>
|
|
145
|
+
<Loader2 size={11} className="animate-spin" /> {s.portChecking}
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
{!status.checking && status.available === false && (
|
|
149
|
+
<div className="flex items-center gap-2">
|
|
150
|
+
<p className="text-xs flex items-center gap-1" style={{ color: 'var(--amber)' }}>
|
|
151
|
+
<AlertTriangle size={11} /> {s.portInUse(value)}
|
|
152
|
+
</p>
|
|
153
|
+
{status.suggestion !== null && (
|
|
154
|
+
<button type="button"
|
|
155
|
+
onClick={() => {
|
|
156
|
+
onChange(status.suggestion!);
|
|
157
|
+
setTimeout(() => onCheckPort(status.suggestion!), 0);
|
|
158
|
+
}}
|
|
159
|
+
className="text-xs px-2 py-0.5 rounded border transition-colors"
|
|
160
|
+
style={{ borderColor: 'var(--amber)', color: 'var(--amber)' }}>
|
|
161
|
+
{s.portSuggest(status.suggestion)}
|
|
162
|
+
</button>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
{!status.checking && status.available === true && (
|
|
167
|
+
<p className="text-xs flex items-center gap-1" style={{ color: '#22c55e' }}>
|
|
168
|
+
<CheckCircle2 size={11} /> {s.portAvailable}
|
|
169
|
+
</p>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</Field>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// -------------------------------------------------------------------
|
|
177
|
+
// Main component
|
|
178
|
+
// -------------------------------------------------------------------
|
|
38
179
|
export default function SetupWizard() {
|
|
39
180
|
const { t } = useLocale();
|
|
40
|
-
const router = useRouter();
|
|
41
181
|
const s = t.setup;
|
|
42
182
|
|
|
43
183
|
const [step, setStep] = useState(0);
|
|
@@ -60,6 +200,19 @@ export default function SetupWizard() {
|
|
|
60
200
|
const [error, setError] = useState('');
|
|
61
201
|
const [portChanged, setPortChanged] = useState(false);
|
|
62
202
|
|
|
203
|
+
// Port availability
|
|
204
|
+
const [webPortStatus, setWebPortStatus] = useState<PortStatus>({ checking: false, available: null, suggestion: null });
|
|
205
|
+
const [mcpPortStatus, setMcpPortStatus] = useState<PortStatus>({ checking: false, available: null, suggestion: null });
|
|
206
|
+
|
|
207
|
+
// Agent Tools
|
|
208
|
+
const [agents, setAgents] = useState<AgentEntry[]>([]);
|
|
209
|
+
const [agentsLoading, setAgentsLoading] = useState(false);
|
|
210
|
+
const [selectedAgents, setSelectedAgents] = useState<Set<string>>(new Set());
|
|
211
|
+
const [agentTransport, setAgentTransport] = useState<'stdio' | 'http'>('stdio');
|
|
212
|
+
const [agentScope, setAgentScope] = useState<'global' | 'project'>('global');
|
|
213
|
+
// Live per-agent install status (shown inline in Step 5 during/after submit)
|
|
214
|
+
const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentInstallStatus>>({});
|
|
215
|
+
|
|
63
216
|
// Generate token on mount
|
|
64
217
|
useEffect(() => {
|
|
65
218
|
fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
@@ -68,11 +221,39 @@ export default function SetupWizard() {
|
|
|
68
221
|
.catch(() => {});
|
|
69
222
|
}, []);
|
|
70
223
|
|
|
224
|
+
// Auto-check ports when entering Step 3
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (step === STEP_PORTS) {
|
|
227
|
+
checkPort(state.webPort, 'web');
|
|
228
|
+
checkPort(state.mcpPort, 'mcp');
|
|
229
|
+
}
|
|
230
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
231
|
+
}, [step]);
|
|
232
|
+
|
|
233
|
+
// Load agents when entering Step 5
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (step === STEP_AGENTS && agents.length === 0 && !agentsLoading) {
|
|
236
|
+
setAgentsLoading(true);
|
|
237
|
+
fetch('/api/mcp/agents')
|
|
238
|
+
.then(r => r.json())
|
|
239
|
+
.then(data => {
|
|
240
|
+
if (data.agents) {
|
|
241
|
+
setAgents(data.agents);
|
|
242
|
+
setSelectedAgents(new Set(
|
|
243
|
+
(data.agents as AgentEntry[]).filter(a => a.installed).map(a => a.key)
|
|
244
|
+
));
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
.catch(() => {})
|
|
248
|
+
.finally(() => setAgentsLoading(false));
|
|
249
|
+
}
|
|
250
|
+
}, [step, agents.length, agentsLoading]);
|
|
251
|
+
|
|
71
252
|
const update = useCallback(<K extends keyof SetupState>(key: K, val: SetupState[K]) => {
|
|
72
253
|
setState(prev => ({ ...prev, [key]: val }));
|
|
73
254
|
}, []);
|
|
74
255
|
|
|
75
|
-
const generateToken = async (seed?: string) => {
|
|
256
|
+
const generateToken = useCallback(async (seed?: string) => {
|
|
76
257
|
try {
|
|
77
258
|
const res = await fetch('/api/setup/generate-token', {
|
|
78
259
|
method: 'POST',
|
|
@@ -80,24 +261,41 @@ export default function SetupWizard() {
|
|
|
80
261
|
body: JSON.stringify({ seed: seed || undefined }),
|
|
81
262
|
});
|
|
82
263
|
const data = await res.json();
|
|
83
|
-
if (data.token)
|
|
264
|
+
if (data.token) setState(prev => ({ ...prev, authToken: data.token }));
|
|
84
265
|
} catch { /* ignore */ }
|
|
85
|
-
};
|
|
266
|
+
}, []);
|
|
86
267
|
|
|
87
|
-
const copyToken = () => {
|
|
88
|
-
navigator.clipboard.writeText(
|
|
268
|
+
const copyToken = useCallback(() => {
|
|
269
|
+
setState(prev => { navigator.clipboard.writeText(prev.authToken); return prev; });
|
|
89
270
|
setTokenCopied(true);
|
|
90
271
|
setTimeout(() => setTokenCopied(false), 2000);
|
|
91
|
-
};
|
|
272
|
+
}, []);
|
|
273
|
+
|
|
274
|
+
const checkPort = useCallback(async (port: number, which: 'web' | 'mcp') => {
|
|
275
|
+
if (port < 1024 || port > 65535) return;
|
|
276
|
+
const setStatus = which === 'web' ? setWebPortStatus : setMcpPortStatus;
|
|
277
|
+
setStatus({ checking: true, available: null, suggestion: null });
|
|
278
|
+
try {
|
|
279
|
+
const res = await fetch('/api/setup/check-port', {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: { 'Content-Type': 'application/json' },
|
|
282
|
+
body: JSON.stringify({ port }),
|
|
283
|
+
});
|
|
284
|
+
const data = await res.json();
|
|
285
|
+
setStatus({ checking: false, available: data.available ?? null, suggestion: data.suggestion ?? null });
|
|
286
|
+
} catch {
|
|
287
|
+
setStatus({ checking: false, available: null, suggestion: null });
|
|
288
|
+
}
|
|
289
|
+
}, []);
|
|
92
290
|
|
|
93
291
|
const handleComplete = async () => {
|
|
94
292
|
setSubmitting(true);
|
|
95
293
|
setError('');
|
|
294
|
+
|
|
295
|
+
// 1. Save setup config first
|
|
96
296
|
try {
|
|
97
297
|
const payload = {
|
|
98
|
-
mindRoot: state.mindRoot
|
|
99
|
-
? state.mindRoot // server will resolve
|
|
100
|
-
: state.mindRoot,
|
|
298
|
+
mindRoot: state.mindRoot,
|
|
101
299
|
template: state.template || undefined,
|
|
102
300
|
port: state.webPort,
|
|
103
301
|
mcpPort: state.mcpPort,
|
|
@@ -119,17 +317,67 @@ export default function SetupWizard() {
|
|
|
119
317
|
const data = await res.json();
|
|
120
318
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
121
319
|
if (data.portChanged) setPortChanged(true);
|
|
122
|
-
else router.push('/');
|
|
123
320
|
} catch (e) {
|
|
124
321
|
setError(e instanceof Error ? e.message : String(e));
|
|
125
|
-
} finally {
|
|
126
322
|
setSubmitting(false);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 2. Install agents after config saved — update statuses live
|
|
327
|
+
if (selectedAgents.size > 0) {
|
|
328
|
+
// Mark all selected as "installing"
|
|
329
|
+
const initialStatuses: Record<string, AgentInstallStatus> = {};
|
|
330
|
+
for (const key of selectedAgents) initialStatuses[key] = { state: 'installing' };
|
|
331
|
+
setAgentStatuses(initialStatuses);
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const agentsPayload = Array.from(selectedAgents).map(key => ({ key, scope: agentScope }));
|
|
335
|
+
const res = await fetch('/api/mcp/install', {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
338
|
+
body: JSON.stringify({
|
|
339
|
+
agents: agentsPayload,
|
|
340
|
+
transport: agentTransport,
|
|
341
|
+
url: `http://localhost:${state.mcpPort}/mcp`,
|
|
342
|
+
token: state.authToken || undefined,
|
|
343
|
+
}),
|
|
344
|
+
});
|
|
345
|
+
const data = await res.json();
|
|
346
|
+
if (data.results) {
|
|
347
|
+
const updated: Record<string, AgentInstallStatus> = {};
|
|
348
|
+
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
|
+
};
|
|
353
|
+
}
|
|
354
|
+
setAgentStatuses(updated);
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
// Mark all as error
|
|
358
|
+
const errStatuses: Record<string, AgentInstallStatus> = {};
|
|
359
|
+
for (const key of selectedAgents) errStatuses[key] = { state: 'error' };
|
|
360
|
+
setAgentStatuses(errStatuses);
|
|
361
|
+
}
|
|
127
362
|
}
|
|
363
|
+
|
|
364
|
+
setSubmitting(false);
|
|
365
|
+
if (!portChanged) window.location.href = '/';
|
|
128
366
|
};
|
|
129
367
|
|
|
368
|
+
const portConflict = state.webPort === state.mcpPort;
|
|
369
|
+
|
|
130
370
|
const canNext = () => {
|
|
131
|
-
if (step ===
|
|
132
|
-
if (step ===
|
|
371
|
+
if (step === STEP_KB) return state.mindRoot.trim().length > 0;
|
|
372
|
+
if (step === STEP_PORTS) {
|
|
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
|
+
);
|
|
380
|
+
}
|
|
133
381
|
return true;
|
|
134
382
|
};
|
|
135
383
|
|
|
@@ -139,31 +387,27 @@ export default function SetupWizard() {
|
|
|
139
387
|
return key.slice(0, 6) + '•••' + key.slice(-3);
|
|
140
388
|
};
|
|
141
389
|
|
|
142
|
-
//
|
|
390
|
+
// ----------------------------------------------------------------
|
|
391
|
+
// Step dots
|
|
392
|
+
// ----------------------------------------------------------------
|
|
143
393
|
const StepDots = () => (
|
|
144
394
|
<div className="flex items-center gap-2 mb-8">
|
|
145
395
|
{s.stepTitles.map((title: string, i: number) => (
|
|
146
396
|
<div key={i} className="flex items-center gap-2">
|
|
147
397
|
{i > 0 && <div className="w-8 h-px" style={{ background: i <= step ? 'var(--amber)' : 'var(--border)' }} />}
|
|
148
|
-
<button
|
|
149
|
-
onClick={() => i < step && setStep(i)}
|
|
150
|
-
className="flex items-center gap-1.5"
|
|
151
|
-
disabled={i > step}
|
|
152
|
-
>
|
|
398
|
+
<button onClick={() => i < step && setStep(i)} className="flex items-center gap-1.5" disabled={i > step}>
|
|
153
399
|
<div
|
|
154
400
|
className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
|
|
155
401
|
style={{
|
|
156
|
-
background: i
|
|
402
|
+
background: i <= step ? 'var(--amber)' : 'var(--muted)',
|
|
157
403
|
color: i <= step ? 'white' : 'var(--muted-foreground)',
|
|
158
404
|
opacity: i <= step ? 1 : 0.5,
|
|
159
405
|
}}
|
|
160
406
|
>
|
|
161
407
|
{i + 1}
|
|
162
408
|
</div>
|
|
163
|
-
<span
|
|
164
|
-
|
|
165
|
-
style={{ color: i === step ? 'var(--foreground)' : 'var(--muted-foreground)', opacity: i <= step ? 1 : 0.5 }}
|
|
166
|
-
>
|
|
409
|
+
<span className="text-xs hidden sm:inline"
|
|
410
|
+
style={{ color: i === step ? 'var(--foreground)' : 'var(--muted-foreground)', opacity: i <= step ? 1 : 0.5 }}>
|
|
167
411
|
{title}
|
|
168
412
|
</span>
|
|
169
413
|
</button>
|
|
@@ -172,39 +416,32 @@ export default function SetupWizard() {
|
|
|
172
416
|
</div>
|
|
173
417
|
);
|
|
174
418
|
|
|
419
|
+
// ----------------------------------------------------------------
|
|
175
420
|
// Step 1: Knowledge Base
|
|
421
|
+
// ----------------------------------------------------------------
|
|
176
422
|
const Step1 = () => (
|
|
177
423
|
<div className="space-y-6">
|
|
178
424
|
<Field label={s.kbPath} hint={s.kbPathHint}>
|
|
179
|
-
<Input
|
|
180
|
-
value={state.mindRoot}
|
|
181
|
-
onChange={e => update('mindRoot', e.target.value)}
|
|
182
|
-
placeholder={s.kbPathDefault}
|
|
183
|
-
/>
|
|
425
|
+
<Input value={state.mindRoot} onChange={e => update('mindRoot', e.target.value)} placeholder={s.kbPathDefault} />
|
|
184
426
|
</Field>
|
|
185
427
|
<div>
|
|
186
428
|
<label className="text-sm text-foreground font-medium mb-3 block">{s.template}</label>
|
|
187
429
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
188
430
|
{TEMPLATES.map(tpl => (
|
|
189
|
-
<button
|
|
190
|
-
key={tpl.id}
|
|
191
|
-
onClick={() => update('template', tpl.id)}
|
|
431
|
+
<button key={tpl.id} onClick={() => update('template', tpl.id)}
|
|
192
432
|
className="flex flex-col items-start gap-2 p-4 rounded-xl border text-left transition-all duration-150"
|
|
193
433
|
style={{
|
|
194
434
|
background: state.template === tpl.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
|
|
195
435
|
borderColor: state.template === tpl.id ? 'var(--amber)' : 'var(--border)',
|
|
196
|
-
}}
|
|
197
|
-
>
|
|
436
|
+
}}>
|
|
198
437
|
<div className="flex items-center gap-2">
|
|
199
438
|
<span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
|
|
200
439
|
<span className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>
|
|
201
440
|
{t.onboarding.templates[tpl.id as 'en' | 'zh' | 'empty'].title}
|
|
202
441
|
</span>
|
|
203
442
|
</div>
|
|
204
|
-
<div
|
|
205
|
-
|
|
206
|
-
style={{ background: 'var(--muted)', fontFamily: "'IBM Plex Mono', monospace", color: 'var(--muted-foreground)' }}
|
|
207
|
-
>
|
|
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)' }}>
|
|
208
445
|
{tpl.dirs.map(d => <div key={d}>{d}</div>)}
|
|
209
446
|
</div>
|
|
210
447
|
</button>
|
|
@@ -214,18 +451,56 @@ export default function SetupWizard() {
|
|
|
214
451
|
</div>
|
|
215
452
|
);
|
|
216
453
|
|
|
217
|
-
//
|
|
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
|
+
|
|
218
478
|
const Step2 = () => (
|
|
219
479
|
<div className="space-y-5">
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
|
|
227
502
|
{state.provider !== 'skip' && (
|
|
228
|
-
|
|
503
|
+
<div className="space-y-4 pt-2">
|
|
229
504
|
<Field label={s.apiKey}>
|
|
230
505
|
<ApiKeyInput
|
|
231
506
|
value={state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey}
|
|
@@ -241,118 +516,168 @@ export default function SetupWizard() {
|
|
|
241
516
|
</Field>
|
|
242
517
|
{state.provider === 'openai' && (
|
|
243
518
|
<Field label={s.baseUrl} hint={s.baseUrlHint}>
|
|
244
|
-
<Input
|
|
245
|
-
|
|
246
|
-
onChange={e => update('openaiBaseUrl', e.target.value)}
|
|
247
|
-
placeholder="https://api.openai.com/v1"
|
|
248
|
-
/>
|
|
519
|
+
<Input value={state.openaiBaseUrl} onChange={e => update('openaiBaseUrl', e.target.value)}
|
|
520
|
+
placeholder="https://api.openai.com/v1" />
|
|
249
521
|
</Field>
|
|
250
522
|
)}
|
|
251
|
-
|
|
523
|
+
</div>
|
|
252
524
|
)}
|
|
253
525
|
</div>
|
|
254
526
|
);
|
|
255
527
|
|
|
528
|
+
// ----------------------------------------------------------------
|
|
256
529
|
// Step 3: Ports
|
|
530
|
+
// ----------------------------------------------------------------
|
|
257
531
|
const Step3 = () => (
|
|
258
532
|
<div className="space-y-5">
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
)}
|
|
277
555
|
<p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
|
|
278
|
-
<AlertTriangle size={12} />
|
|
279
|
-
{s.portRestartWarning}
|
|
556
|
+
<AlertTriangle size={12} /> {s.portRestartWarning}
|
|
280
557
|
</p>
|
|
281
558
|
</div>
|
|
282
559
|
);
|
|
283
560
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
};
|
|
288
612
|
|
|
289
613
|
return (
|
|
290
614
|
<div className="space-y-5">
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
style={{ color: 'var(--foreground)' }}
|
|
298
|
-
>
|
|
299
|
-
{tokenCopied ? <Check size={14} /> : <Copy size={14} />}
|
|
300
|
-
{tokenCopied ? s.copiedToken : s.copyToken}
|
|
301
|
-
</button>
|
|
302
|
-
<button
|
|
303
|
-
onClick={() => generateToken()}
|
|
304
|
-
className="flex items-center gap-1 px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
|
|
305
|
-
style={{ color: 'var(--foreground)' }}
|
|
306
|
-
>
|
|
307
|
-
<RefreshCw size={14} />
|
|
308
|
-
</button>
|
|
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>
|
|
309
621
|
</div>
|
|
310
|
-
|
|
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>
|
|
311
648
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
placeholder={s.authTokenSeedHint}
|
|
326
|
-
/>
|
|
327
|
-
<button
|
|
328
|
-
onClick={() => { if (seed.trim()) generateToken(seed); }}
|
|
329
|
-
className="px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
|
|
330
|
-
style={{ color: 'var(--foreground)' }}
|
|
331
|
-
>
|
|
332
|
-
{s.generateToken}
|
|
333
|
-
</button>
|
|
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>
|
|
334
662
|
</div>
|
|
335
|
-
)}
|
|
336
|
-
</div>
|
|
337
663
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
placeholder="(optional)"
|
|
344
|
-
/>
|
|
345
|
-
</Field>
|
|
664
|
+
{selectedAgents.size === 0 && (
|
|
665
|
+
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.agentNoneSelected}</p>
|
|
666
|
+
)}
|
|
667
|
+
</>
|
|
668
|
+
)}
|
|
346
669
|
</div>
|
|
347
670
|
);
|
|
348
671
|
};
|
|
349
672
|
|
|
350
|
-
//
|
|
351
|
-
|
|
673
|
+
// ----------------------------------------------------------------
|
|
674
|
+
// Step 6: Review
|
|
675
|
+
// ----------------------------------------------------------------
|
|
676
|
+
const Step6 = () => {
|
|
352
677
|
const rows: [string, string][] = [
|
|
353
678
|
[s.kbPath, state.mindRoot],
|
|
354
679
|
[s.template, state.template || '—'],
|
|
355
|
-
[s.aiProvider, state.provider],
|
|
680
|
+
[s.aiProvider, state.provider === 'skip' ? s.aiSkipTitle : state.provider],
|
|
356
681
|
...(state.provider !== 'skip' ? [
|
|
357
682
|
[s.apiKey, maskKey(state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey)] as [string, string],
|
|
358
683
|
[s.model, state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel] as [string, string],
|
|
@@ -361,6 +686,7 @@ export default function SetupWizard() {
|
|
|
361
686
|
[s.mcpPort, String(state.mcpPort)],
|
|
362
687
|
[s.authToken, state.authToken || '—'],
|
|
363
688
|
[s.webPassword, state.webPassword ? '••••••••' : '(none)'],
|
|
689
|
+
[s.agentToolsTitle, selectedAgents.size > 0 ? Array.from(selectedAgents).join(', ') : '—'],
|
|
364
690
|
];
|
|
365
691
|
|
|
366
692
|
return (
|
|
@@ -368,14 +694,11 @@ export default function SetupWizard() {
|
|
|
368
694
|
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.reviewHint}</p>
|
|
369
695
|
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
370
696
|
{rows.map(([label, value], i) => (
|
|
371
|
-
<div
|
|
372
|
-
key={i}
|
|
373
|
-
className="flex items-center justify-between px-4 py-3 text-sm"
|
|
697
|
+
<div key={i} className="flex items-center justify-between px-4 py-3 text-sm"
|
|
374
698
|
style={{
|
|
375
699
|
background: i % 2 === 0 ? 'var(--card)' : 'transparent',
|
|
376
700
|
borderTop: i > 0 ? '1px solid var(--border)' : undefined,
|
|
377
|
-
}}
|
|
378
|
-
>
|
|
701
|
+
}}>
|
|
379
702
|
<span style={{ color: 'var(--muted-foreground)' }}>{label}</span>
|
|
380
703
|
<span className="font-mono text-xs" style={{ color: 'var(--foreground)' }}>{value}</span>
|
|
381
704
|
</div>
|
|
@@ -390,15 +713,12 @@ export default function SetupWizard() {
|
|
|
390
713
|
|
|
391
714
|
{portChanged && (
|
|
392
715
|
<div className="space-y-3">
|
|
393
|
-
<div className="p-3 rounded-lg text-sm flex items-center gap-2"
|
|
394
|
-
|
|
395
|
-
{s.portChanged}
|
|
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}
|
|
396
719
|
</div>
|
|
397
|
-
<a
|
|
398
|
-
|
|
399
|
-
className="inline-flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors"
|
|
400
|
-
style={{ background: 'var(--amber)', color: 'white' }}
|
|
401
|
-
>
|
|
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' }}>
|
|
402
722
|
{s.completeDone} →
|
|
403
723
|
</a>
|
|
404
724
|
</div>
|
|
@@ -407,36 +727,48 @@ export default function SetupWizard() {
|
|
|
407
727
|
);
|
|
408
728
|
};
|
|
409
729
|
|
|
410
|
-
const steps = [
|
|
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
|
+
];
|
|
411
748
|
const CurrentStep = steps[step];
|
|
412
749
|
|
|
413
750
|
return (
|
|
414
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
|
751
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
|
752
|
+
style={{ background: 'var(--background)' }}>
|
|
415
753
|
<div className="w-full max-w-xl mx-auto px-6 py-12">
|
|
416
754
|
{/* Header */}
|
|
417
755
|
<div className="text-center mb-8">
|
|
418
756
|
<div className="inline-flex items-center gap-2 mb-2">
|
|
419
757
|
<Sparkles size={18} style={{ color: 'var(--amber)' }} />
|
|
420
|
-
<h1
|
|
421
|
-
className="text-2xl font-semibold tracking-tight"
|
|
422
|
-
style={{ fontFamily: "'IBM Plex Mono', monospace", color: 'var(--foreground)' }}
|
|
423
|
-
>
|
|
758
|
+
<h1 className="text-2xl font-semibold tracking-tight font-display" style={{ color: 'var(--foreground)' }}>
|
|
424
759
|
MindOS
|
|
425
760
|
</h1>
|
|
426
761
|
</div>
|
|
427
762
|
</div>
|
|
428
763
|
|
|
429
|
-
{/* Step dots */}
|
|
430
764
|
<div className="flex justify-center">
|
|
431
765
|
<StepDots />
|
|
432
766
|
</div>
|
|
433
767
|
|
|
434
|
-
{/* Step title */}
|
|
435
768
|
<h2 className="text-lg font-semibold mb-5" style={{ color: 'var(--foreground)' }}>
|
|
436
769
|
{s.stepTitles[step]}
|
|
437
770
|
</h2>
|
|
438
771
|
|
|
439
|
-
{/* Step content */}
|
|
440
772
|
<CurrentStep />
|
|
441
773
|
|
|
442
774
|
{/* Navigation */}
|
|
@@ -445,10 +777,8 @@ export default function SetupWizard() {
|
|
|
445
777
|
onClick={() => setStep(step - 1)}
|
|
446
778
|
disabled={step === 0}
|
|
447
779
|
className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
448
|
-
style={{ color: 'var(--foreground)' }}
|
|
449
|
-
|
|
450
|
-
<ChevronLeft size={14} />
|
|
451
|
-
{s.back}
|
|
780
|
+
style={{ color: 'var(--foreground)' }}>
|
|
781
|
+
<ChevronLeft size={14} /> {s.back}
|
|
452
782
|
</button>
|
|
453
783
|
|
|
454
784
|
{step < TOTAL_STEPS - 1 ? (
|
|
@@ -456,18 +786,15 @@ export default function SetupWizard() {
|
|
|
456
786
|
onClick={() => setStep(step + 1)}
|
|
457
787
|
disabled={!canNext()}
|
|
458
788
|
className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
459
|
-
style={{ background: 'var(--amber)', color: 'white' }}
|
|
460
|
-
|
|
461
|
-
{s.next}
|
|
462
|
-
<ChevronRight size={14} />
|
|
789
|
+
style={{ background: 'var(--amber)', color: 'white' }}>
|
|
790
|
+
{s.next} <ChevronRight size={14} />
|
|
463
791
|
</button>
|
|
464
792
|
) : (
|
|
465
793
|
<button
|
|
466
794
|
onClick={handleComplete}
|
|
467
795
|
disabled={submitting || portChanged}
|
|
468
796
|
className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
|
469
|
-
style={{ background: 'var(--amber)', color: 'white' }}
|
|
470
|
-
>
|
|
797
|
+
style={{ background: 'var(--amber)', color: 'white' }}>
|
|
471
798
|
{submitting && <Loader2 size={14} className="animate-spin" />}
|
|
472
799
|
{submitting ? s.completing : portChanged ? s.completeDone : s.complete}
|
|
473
800
|
</button>
|