@geminilight/mindos 0.5.8 → 0.5.10
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/README.md +9 -10
- package/README_zh.md +8 -9
- package/app/app/api/mcp/agents/route.ts +7 -0
- package/app/app/api/mcp/install-skill/route.ts +6 -0
- package/app/app/api/setup/check-port/route.ts +27 -3
- package/app/app/api/setup/route.ts +2 -9
- package/app/app/api/skills/route.ts +1 -1
- package/app/app/globals.css +28 -4
- package/app/app/login/page.tsx +2 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +15 -10
- package/app/app/view/[...path]/not-found.tsx +1 -1
- package/app/components/AskModal.tsx +5 -5
- package/app/components/Breadcrumb.tsx +2 -2
- package/app/components/DirView.tsx +6 -6
- package/app/components/FileTree.tsx +7 -7
- package/app/components/HomeContent.tsx +8 -8
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/SearchModal.tsx +1 -1
- package/app/components/SettingsModal.tsx +2 -2
- package/app/components/SetupWizard.tsx +1 -1258
- package/app/components/Sidebar.tsx +4 -4
- package/app/components/SidebarLayout.tsx +9 -0
- package/app/components/SyncStatusBar.tsx +6 -6
- package/app/components/TableOfContents.tsx +1 -1
- package/app/components/UpdateBanner.tsx +1 -1
- package/app/components/ask/FileChip.tsx +1 -1
- package/app/components/ask/MentionPopover.tsx +4 -4
- package/app/components/ask/MessageList.tsx +3 -3
- package/app/components/ask/SessionHistory.tsx +3 -3
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +5 -5
- package/app/components/renderers/config/ConfigRenderer.tsx +4 -4
- package/app/components/renderers/csv/BoardView.tsx +2 -2
- package/app/components/renderers/csv/ConfigPanel.tsx +5 -5
- package/app/components/renderers/csv/GalleryView.tsx +1 -1
- package/app/components/renderers/csv/types.ts +1 -1
- package/app/components/renderers/diff/DiffRenderer.tsx +9 -9
- package/app/components/renderers/graph/GraphRenderer.tsx +1 -1
- package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
- package/app/components/renderers/timeline/TimelineRenderer.tsx +1 -1
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +4 -4
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpTab.tsx +93 -47
- package/app/components/settings/PluginsTab.tsx +4 -4
- package/app/components/settings/Primitives.tsx +4 -4
- package/app/components/settings/SyncTab.tsx +13 -13
- package/app/components/setup/StepAI.tsx +67 -0
- package/app/components/setup/StepAgents.tsx +237 -0
- package/app/components/setup/StepDots.tsx +39 -0
- package/app/components/setup/StepKB.tsx +237 -0
- package/app/components/setup/StepPorts.tsx +121 -0
- package/app/components/setup/StepReview.tsx +211 -0
- package/app/components/setup/StepSecurity.tsx +78 -0
- package/app/components/setup/constants.tsx +13 -0
- package/app/components/setup/index.tsx +464 -0
- package/app/components/setup/types.ts +53 -0
- package/app/lib/i18n.ts +52 -8
- package/app/lib/mcp-agents.ts +81 -0
- package/bin/lib/gateway.js +44 -4
- package/bin/lib/mcp-agents.js +81 -0
- package/bin/lib/mcp-install.js +34 -4
- package/package.json +3 -1
- package/scripts/setup.js +43 -6
- package/skills/project-wiki/SKILL.md +92 -63
- package/app/public/landing/index.html +0 -353
- package/app/public/landing/style.css +0 -216
|
@@ -1,1258 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
Sparkles, Globe, BookOpen, FileText, Copy, Check, RefreshCw,
|
|
6
|
-
Loader2, ChevronLeft, ChevronRight, AlertTriangle, CheckCircle2,
|
|
7
|
-
XCircle, Zap, Brain, SkipForward, Info,
|
|
8
|
-
} from 'lucide-react';
|
|
9
|
-
import { useLocale } from '@/lib/LocaleContext';
|
|
10
|
-
import { Field, Input, Select, ApiKeyInput } from '@/components/settings/Primitives';
|
|
11
|
-
|
|
12
|
-
type Template = 'en' | 'zh' | 'empty' | '';
|
|
13
|
-
|
|
14
|
-
interface SetupState {
|
|
15
|
-
mindRoot: string;
|
|
16
|
-
template: Template;
|
|
17
|
-
provider: 'anthropic' | 'openai' | 'skip';
|
|
18
|
-
anthropicKey: string;
|
|
19
|
-
anthropicModel: string;
|
|
20
|
-
openaiKey: string;
|
|
21
|
-
openaiModel: string;
|
|
22
|
-
openaiBaseUrl: string;
|
|
23
|
-
webPort: number;
|
|
24
|
-
mcpPort: number;
|
|
25
|
-
authToken: string;
|
|
26
|
-
webPassword: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface PortStatus {
|
|
30
|
-
checking: boolean;
|
|
31
|
-
available: boolean | null;
|
|
32
|
-
isSelf: boolean;
|
|
33
|
-
suggestion: number | null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface AgentEntry {
|
|
37
|
-
key: string;
|
|
38
|
-
name: string;
|
|
39
|
-
present: boolean;
|
|
40
|
-
installed: boolean;
|
|
41
|
-
hasProjectScope: boolean;
|
|
42
|
-
hasGlobalScope: boolean;
|
|
43
|
-
preferredTransport: 'stdio' | 'http';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
type AgentInstallState = 'pending' | 'installing' | 'ok' | 'error';
|
|
47
|
-
interface AgentInstallStatus {
|
|
48
|
-
state: AgentInstallState;
|
|
49
|
-
message?: string;
|
|
50
|
-
transport?: string;
|
|
51
|
-
verified?: boolean;
|
|
52
|
-
verifyError?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const TEMPLATES: Array<{ id: Template; icon: React.ReactNode; dirs: string[] }> = [
|
|
56
|
-
{ id: 'en', icon: <Globe size={18} />, dirs: ['Profile/', 'Connections/', 'Notes/', 'Workflows/', 'Resources/', 'Projects/'] },
|
|
57
|
-
{ id: 'zh', icon: <BookOpen size={18} />, dirs: ['画像/', '关系/', '笔记/', '流程/', '资源/', '项目/'] },
|
|
58
|
-
{ id: 'empty', icon: <FileText size={18} />, dirs: ['README.md', 'CONFIG.json', 'INSTRUCTION.md'] },
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
const TOTAL_STEPS = 6;
|
|
62
|
-
const STEP_KB = 0;
|
|
63
|
-
const STEP_PORTS = 2;
|
|
64
|
-
const STEP_AGENTS = 4;
|
|
65
|
-
|
|
66
|
-
// ─── Step 4 (Security) ────────────────────────────────────────────────────────
|
|
67
|
-
// Extracted at module level so its local seed/showSeed state survives parent re-renders
|
|
68
|
-
function Step4Inner({
|
|
69
|
-
authToken, tokenCopied, onCopy, onGenerate, webPassword, onPasswordChange, s,
|
|
70
|
-
}: {
|
|
71
|
-
authToken: string;
|
|
72
|
-
tokenCopied: boolean;
|
|
73
|
-
onCopy: () => void;
|
|
74
|
-
onGenerate: (seed?: string) => void;
|
|
75
|
-
webPassword: string;
|
|
76
|
-
onPasswordChange: (v: string) => void;
|
|
77
|
-
s: {
|
|
78
|
-
authToken: string; authTokenHint: string; authTokenUsage: string; authTokenUsageWhat: string;
|
|
79
|
-
authTokenSeed: string; authTokenSeedHint: string;
|
|
80
|
-
generateToken: string; copyToken: string; copiedToken: string;
|
|
81
|
-
webPassword: string; webPasswordHint: string;
|
|
82
|
-
};
|
|
83
|
-
}) {
|
|
84
|
-
const [seed, setSeed] = useState('');
|
|
85
|
-
const [showSeed, setShowSeed] = useState(false);
|
|
86
|
-
const [showUsage, setShowUsage] = useState(false);
|
|
87
|
-
return (
|
|
88
|
-
<div className="space-y-5">
|
|
89
|
-
<Field label={s.authToken} hint={s.authTokenHint}>
|
|
90
|
-
<div className="flex gap-2">
|
|
91
|
-
<Input value={authToken} readOnly className="font-mono text-xs" />
|
|
92
|
-
<button onClick={onCopy}
|
|
93
|
-
className="flex items-center gap-1 px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
|
|
94
|
-
style={{ color: 'var(--foreground)' }}>
|
|
95
|
-
{tokenCopied ? <Check size={14} /> : <Copy size={14} />}
|
|
96
|
-
{tokenCopied ? s.copiedToken : s.copyToken}
|
|
97
|
-
</button>
|
|
98
|
-
<button onClick={() => onGenerate()}
|
|
99
|
-
className="flex items-center gap-1 px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
|
|
100
|
-
style={{ color: 'var(--foreground)' }}>
|
|
101
|
-
<RefreshCw size={14} />
|
|
102
|
-
</button>
|
|
103
|
-
</div>
|
|
104
|
-
</Field>
|
|
105
|
-
<div className="space-y-1.5">
|
|
106
|
-
<button onClick={() => setShowUsage(!showUsage)} className="text-xs underline"
|
|
107
|
-
style={{ color: 'var(--muted-foreground)' }}>
|
|
108
|
-
{s.authTokenUsageWhat}
|
|
109
|
-
</button>
|
|
110
|
-
{showUsage && (
|
|
111
|
-
<p className="text-xs leading-relaxed px-3 py-2 rounded-lg"
|
|
112
|
-
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
|
|
113
|
-
{s.authTokenUsage}
|
|
114
|
-
</p>
|
|
115
|
-
)}
|
|
116
|
-
</div>
|
|
117
|
-
<div>
|
|
118
|
-
<button onClick={() => setShowSeed(!showSeed)} className="text-xs underline"
|
|
119
|
-
style={{ color: 'var(--muted-foreground)' }}>
|
|
120
|
-
{s.authTokenSeed}
|
|
121
|
-
</button>
|
|
122
|
-
{showSeed && (
|
|
123
|
-
<div className="mt-2 flex gap-2">
|
|
124
|
-
<Input value={seed} onChange={e => setSeed(e.target.value)} placeholder={s.authTokenSeedHint} />
|
|
125
|
-
<button onClick={() => { if (seed.trim()) onGenerate(seed); }}
|
|
126
|
-
className="px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
|
|
127
|
-
style={{ color: 'var(--foreground)' }}>
|
|
128
|
-
{s.generateToken}
|
|
129
|
-
</button>
|
|
130
|
-
</div>
|
|
131
|
-
)}
|
|
132
|
-
</div>
|
|
133
|
-
<Field label={s.webPassword} hint={s.webPasswordHint}>
|
|
134
|
-
<Input type="password" value={webPassword} onChange={e => onPasswordChange(e.target.value)} placeholder="(optional)" />
|
|
135
|
-
</Field>
|
|
136
|
-
</div>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ─── PortField ────────────────────────────────────────────────────────────────
|
|
141
|
-
function PortField({
|
|
142
|
-
label, hint, value, onChange, status, onCheckPort, s,
|
|
143
|
-
}: {
|
|
144
|
-
label: string; hint: string; value: number;
|
|
145
|
-
onChange: (v: number) => void;
|
|
146
|
-
status: PortStatus;
|
|
147
|
-
onCheckPort: (port: number) => void;
|
|
148
|
-
s: { portChecking: string; portInUse: (p: number) => string; portSuggest: (p: number) => string; portAvailable: string; portSelf: string };
|
|
149
|
-
}) {
|
|
150
|
-
return (
|
|
151
|
-
<Field label={label} hint={hint}>
|
|
152
|
-
<div className="space-y-1.5">
|
|
153
|
-
<Input
|
|
154
|
-
type="number" min={1024} max={65535} value={value}
|
|
155
|
-
onChange={e => onChange(parseInt(e.target.value, 10) || value)}
|
|
156
|
-
onBlur={() => onCheckPort(value)}
|
|
157
|
-
/>
|
|
158
|
-
{status.checking && (
|
|
159
|
-
<p className="text-xs flex items-center gap-1" style={{ color: 'var(--muted-foreground)' }}>
|
|
160
|
-
<Loader2 size={11} className="animate-spin" /> {s.portChecking}
|
|
161
|
-
</p>
|
|
162
|
-
)}
|
|
163
|
-
{!status.checking && status.available === false && (
|
|
164
|
-
<div className="flex items-center gap-2">
|
|
165
|
-
<p className="text-xs flex items-center gap-1" style={{ color: 'var(--amber)' }}>
|
|
166
|
-
<AlertTriangle size={11} /> {s.portInUse(value)}
|
|
167
|
-
</p>
|
|
168
|
-
{status.suggestion !== null && (
|
|
169
|
-
<button type="button"
|
|
170
|
-
onClick={() => {
|
|
171
|
-
onChange(status.suggestion!);
|
|
172
|
-
setTimeout(() => onCheckPort(status.suggestion!), 0);
|
|
173
|
-
}}
|
|
174
|
-
className="text-xs px-2 py-0.5 rounded border transition-colors"
|
|
175
|
-
style={{ borderColor: 'var(--amber)', color: 'var(--amber)' }}>
|
|
176
|
-
{s.portSuggest(status.suggestion)}
|
|
177
|
-
</button>
|
|
178
|
-
)}
|
|
179
|
-
</div>
|
|
180
|
-
)}
|
|
181
|
-
{!status.checking && status.available === true && (
|
|
182
|
-
<p className="text-xs flex items-center gap-1" style={{ color: '#22c55e' }}>
|
|
183
|
-
<CheckCircle2 size={11} /> {status.isSelf ? s.portSelf : s.portAvailable}
|
|
184
|
-
</p>
|
|
185
|
-
)}
|
|
186
|
-
</div>
|
|
187
|
-
</Field>
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Derive parent dir from current input for ls — supports both / and \ separators
|
|
192
|
-
function getParentDir(p: string): string {
|
|
193
|
-
if (!p.trim()) return '';
|
|
194
|
-
const trimmed = p.trim();
|
|
195
|
-
// Already a directory (ends with separator)
|
|
196
|
-
if (trimmed.endsWith('/') || trimmed.endsWith('\\')) return trimmed;
|
|
197
|
-
// Find last separator (/ or \)
|
|
198
|
-
const lastSlash = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
|
|
199
|
-
return lastSlash >= 0 ? trimmed.slice(0, lastSlash + 1) : '';
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ─── Step 1: Knowledge Base ───────────────────────────────────────────────────
|
|
203
|
-
function Step1({
|
|
204
|
-
state, update, t, homeDir,
|
|
205
|
-
}: {
|
|
206
|
-
state: SetupState;
|
|
207
|
-
update: <K extends keyof SetupState>(key: K, val: SetupState[K]) => void;
|
|
208
|
-
t: ReturnType<typeof useLocale>['t'];
|
|
209
|
-
homeDir: string;
|
|
210
|
-
}) {
|
|
211
|
-
const s = t.setup;
|
|
212
|
-
// Build platform-aware placeholder, e.g. /Users/alice/MindOS/mind or C:\Users\alice\MindOS\mind
|
|
213
|
-
// Windows homedir always contains \, e.g. C:\Users\Alice — safe to detect by separator
|
|
214
|
-
const sep = homeDir.includes('\\') ? '\\' : '/';
|
|
215
|
-
const placeholder = homeDir !== '~' ? [homeDir, 'MindOS', 'mind'].join(sep) : s.kbPathDefault;
|
|
216
|
-
const [pathInfo, setPathInfo] = useState<{ exists: boolean; empty: boolean; count: number } | null>(null);
|
|
217
|
-
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
218
|
-
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
219
|
-
const [activeSuggestion, setActiveSuggestion] = useState(-1);
|
|
220
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
221
|
-
|
|
222
|
-
// Debounced autocomplete
|
|
223
|
-
useEffect(() => {
|
|
224
|
-
if (!state.mindRoot.trim()) { setSuggestions([]); return; }
|
|
225
|
-
const timer = setTimeout(() => {
|
|
226
|
-
const parent = getParentDir(state.mindRoot) || homeDir;
|
|
227
|
-
fetch('/api/setup/ls', {
|
|
228
|
-
method: 'POST',
|
|
229
|
-
headers: { 'Content-Type': 'application/json' },
|
|
230
|
-
body: JSON.stringify({ path: parent }),
|
|
231
|
-
})
|
|
232
|
-
.then(r => r.json())
|
|
233
|
-
.then(d => {
|
|
234
|
-
if (!d.dirs?.length) { setSuggestions([]); return; }
|
|
235
|
-
// Normalize parent to end with a separator (preserve existing / or \)
|
|
236
|
-
const endsWithSep = parent.endsWith('/') || parent.endsWith('\\');
|
|
237
|
-
const localSep = parent.includes('\\') ? '\\' : '/';
|
|
238
|
-
const parentNorm = endsWithSep ? parent : parent + localSep;
|
|
239
|
-
const typed = state.mindRoot.trim();
|
|
240
|
-
const full: string[] = (d.dirs as string[]).map((dir: string) => parentNorm + dir);
|
|
241
|
-
const endsWithAnySep = typed.endsWith('/') || typed.endsWith('\\');
|
|
242
|
-
const filtered = endsWithAnySep ? full : full.filter(f => f.startsWith(typed));
|
|
243
|
-
setSuggestions(filtered.slice(0, 8));
|
|
244
|
-
setShowSuggestions(filtered.length > 0);
|
|
245
|
-
setActiveSuggestion(-1);
|
|
246
|
-
})
|
|
247
|
-
.catch(() => setSuggestions([]));
|
|
248
|
-
}, 300);
|
|
249
|
-
return () => clearTimeout(timer);
|
|
250
|
-
}, [state.mindRoot, homeDir]);
|
|
251
|
-
|
|
252
|
-
// Debounced path check
|
|
253
|
-
useEffect(() => {
|
|
254
|
-
if (!state.mindRoot.trim()) { setPathInfo(null); return; }
|
|
255
|
-
const timer = setTimeout(() => {
|
|
256
|
-
fetch('/api/setup/check-path', {
|
|
257
|
-
method: 'POST',
|
|
258
|
-
headers: { 'Content-Type': 'application/json' },
|
|
259
|
-
body: JSON.stringify({ path: state.mindRoot }),
|
|
260
|
-
})
|
|
261
|
-
.then(r => r.json())
|
|
262
|
-
.then(d => setPathInfo(d))
|
|
263
|
-
.catch(() => setPathInfo(null));
|
|
264
|
-
}, 600);
|
|
265
|
-
return () => clearTimeout(timer);
|
|
266
|
-
}, [state.mindRoot]);
|
|
267
|
-
|
|
268
|
-
const hideSuggestions = () => {
|
|
269
|
-
setSuggestions([]);
|
|
270
|
-
setShowSuggestions(false);
|
|
271
|
-
setActiveSuggestion(-1);
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
const selectSuggestion = (val: string) => {
|
|
275
|
-
update('mindRoot', val);
|
|
276
|
-
hideSuggestions();
|
|
277
|
-
inputRef.current?.focus();
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
281
|
-
if (!showSuggestions || suggestions.length === 0) return;
|
|
282
|
-
if (e.key === 'ArrowDown') {
|
|
283
|
-
e.preventDefault();
|
|
284
|
-
setActiveSuggestion(i => Math.min(i + 1, suggestions.length - 1));
|
|
285
|
-
} else if (e.key === 'ArrowUp') {
|
|
286
|
-
e.preventDefault();
|
|
287
|
-
setActiveSuggestion(i => Math.max(i - 1, -1));
|
|
288
|
-
} else if (e.key === 'Enter' && activeSuggestion >= 0) {
|
|
289
|
-
e.preventDefault();
|
|
290
|
-
selectSuggestion(suggestions[activeSuggestion]);
|
|
291
|
-
} else if (e.key === 'Escape') {
|
|
292
|
-
setShowSuggestions(false);
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
return (
|
|
297
|
-
<div className="space-y-6">
|
|
298
|
-
<Field label={s.kbPath} hint={s.kbPathHint}>
|
|
299
|
-
<div className="relative">
|
|
300
|
-
<input
|
|
301
|
-
ref={inputRef}
|
|
302
|
-
value={state.mindRoot}
|
|
303
|
-
onChange={e => { update('mindRoot', e.target.value); setShowSuggestions(true); }}
|
|
304
|
-
onKeyDown={handleKeyDown}
|
|
305
|
-
onBlur={() => setTimeout(() => hideSuggestions(), 150)}
|
|
306
|
-
onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
|
|
307
|
-
placeholder={placeholder}
|
|
308
|
-
className="w-full px-3 py-2 text-sm rounded-lg border outline-none transition-colors"
|
|
309
|
-
style={{
|
|
310
|
-
background: 'var(--input, var(--card))',
|
|
311
|
-
borderColor: 'var(--border)',
|
|
312
|
-
color: 'var(--foreground)',
|
|
313
|
-
}}
|
|
314
|
-
/>
|
|
315
|
-
{showSuggestions && suggestions.length > 0 && (
|
|
316
|
-
<div
|
|
317
|
-
className="absolute z-50 left-0 right-0 top-full mt-1 rounded-lg border overflow-auto"
|
|
318
|
-
style={{
|
|
319
|
-
background: 'var(--card)',
|
|
320
|
-
borderColor: 'var(--border)',
|
|
321
|
-
boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
|
|
322
|
-
maxHeight: '220px',
|
|
323
|
-
}}>
|
|
324
|
-
{suggestions.map((suggestion, i) => (
|
|
325
|
-
<button
|
|
326
|
-
key={suggestion}
|
|
327
|
-
type="button"
|
|
328
|
-
onMouseDown={() => selectSuggestion(suggestion)}
|
|
329
|
-
className="w-full text-left px-3 py-2 text-sm font-mono transition-colors"
|
|
330
|
-
style={{
|
|
331
|
-
background: i === activeSuggestion ? 'var(--muted)' : 'transparent',
|
|
332
|
-
color: 'var(--foreground)',
|
|
333
|
-
borderTop: i > 0 ? '1px solid var(--border)' : undefined,
|
|
334
|
-
}}>
|
|
335
|
-
{suggestion}
|
|
336
|
-
</button>
|
|
337
|
-
))}
|
|
338
|
-
</div>
|
|
339
|
-
)}
|
|
340
|
-
</div>
|
|
341
|
-
{pathInfo?.exists && !pathInfo.empty && (
|
|
342
|
-
<p className="text-xs flex items-center gap-1 mt-1.5" style={{ color: 'var(--amber)' }}>
|
|
343
|
-
<AlertTriangle size={11} /> {s.kbPathExists(pathInfo.count)}
|
|
344
|
-
</p>
|
|
345
|
-
)}
|
|
346
|
-
</Field>
|
|
347
|
-
<div>
|
|
348
|
-
<label className="text-sm text-foreground font-medium mb-3 block">{s.template}</label>
|
|
349
|
-
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
350
|
-
{TEMPLATES.map(tpl => (
|
|
351
|
-
<button key={tpl.id} onClick={() => update('template', tpl.id)}
|
|
352
|
-
className="flex flex-col items-start gap-2 p-4 rounded-xl border text-left transition-all duration-150"
|
|
353
|
-
style={{
|
|
354
|
-
background: state.template === tpl.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
|
|
355
|
-
borderColor: state.template === tpl.id ? 'var(--amber)' : 'var(--border)',
|
|
356
|
-
}}>
|
|
357
|
-
<div className="flex items-center gap-2">
|
|
358
|
-
<span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
|
|
359
|
-
<span className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>
|
|
360
|
-
{t.onboarding.templates[tpl.id as 'en' | 'zh' | 'empty'].title}
|
|
361
|
-
</span>
|
|
362
|
-
</div>
|
|
363
|
-
<div className="w-full rounded-lg px-2.5 py-1.5 text-[11px] leading-relaxed font-display"
|
|
364
|
-
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}>
|
|
365
|
-
{tpl.dirs.map(d => <div key={d}>{d}</div>)}
|
|
366
|
-
</div>
|
|
367
|
-
</button>
|
|
368
|
-
))}
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
371
|
-
</div>
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// ─── Step 2: AI Provider ──────────────────────────────────────────────────────
|
|
376
|
-
function Step2({
|
|
377
|
-
state, update, s,
|
|
378
|
-
}: {
|
|
379
|
-
state: SetupState;
|
|
380
|
-
update: <K extends keyof SetupState>(key: K, val: SetupState[K]) => void;
|
|
381
|
-
s: ReturnType<typeof useLocale>['t']['setup'];
|
|
382
|
-
}) {
|
|
383
|
-
const providers = [
|
|
384
|
-
{ id: 'anthropic' as const, icon: <Brain size={18} />, label: 'Anthropic', desc: 'Claude — claude-sonnet-4-6' },
|
|
385
|
-
{ id: 'openai' as const, icon: <Zap size={18} />, label: 'OpenAI', desc: 'GPT or any OpenAI-compatible API' },
|
|
386
|
-
{ id: 'skip' as const, icon: <SkipForward size={18} />, label: s.aiSkipTitle, desc: s.aiSkipDesc },
|
|
387
|
-
];
|
|
388
|
-
return (
|
|
389
|
-
<div className="space-y-5">
|
|
390
|
-
<div className="grid grid-cols-1 gap-3">
|
|
391
|
-
{providers.map(p => (
|
|
392
|
-
<button key={p.id} onClick={() => update('provider', p.id)}
|
|
393
|
-
className="flex items-start gap-3 p-4 rounded-xl border text-left transition-all duration-150"
|
|
394
|
-
style={{
|
|
395
|
-
background: state.provider === p.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
|
|
396
|
-
borderColor: state.provider === p.id ? 'var(--amber)' : 'var(--border)',
|
|
397
|
-
}}>
|
|
398
|
-
<span className="mt-0.5" style={{ color: state.provider === p.id ? 'var(--amber)' : 'var(--muted-foreground)' }}>
|
|
399
|
-
{p.icon}
|
|
400
|
-
</span>
|
|
401
|
-
<div>
|
|
402
|
-
<p className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>{p.label}</p>
|
|
403
|
-
<p className="text-xs mt-0.5" style={{ color: 'var(--muted-foreground)' }}>{p.desc}</p>
|
|
404
|
-
</div>
|
|
405
|
-
{state.provider === p.id && (
|
|
406
|
-
<CheckCircle2 size={16} className="ml-auto mt-0.5 shrink-0" style={{ color: 'var(--amber)' }} />
|
|
407
|
-
)}
|
|
408
|
-
</button>
|
|
409
|
-
))}
|
|
410
|
-
</div>
|
|
411
|
-
{state.provider !== 'skip' && (
|
|
412
|
-
<div className="space-y-4 pt-2">
|
|
413
|
-
<Field label={s.apiKey}>
|
|
414
|
-
<ApiKeyInput
|
|
415
|
-
value={state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey}
|
|
416
|
-
onChange={v => update(state.provider === 'anthropic' ? 'anthropicKey' : 'openaiKey', v)}
|
|
417
|
-
placeholder={state.provider === 'anthropic' ? 'sk-ant-...' : 'sk-...'}
|
|
418
|
-
/>
|
|
419
|
-
</Field>
|
|
420
|
-
<Field label={s.model}>
|
|
421
|
-
<Input
|
|
422
|
-
value={state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel}
|
|
423
|
-
onChange={e => update(state.provider === 'anthropic' ? 'anthropicModel' : 'openaiModel', e.target.value)}
|
|
424
|
-
/>
|
|
425
|
-
</Field>
|
|
426
|
-
{state.provider === 'openai' && (
|
|
427
|
-
<Field label={s.baseUrl} hint={s.baseUrlHint}>
|
|
428
|
-
<Input value={state.openaiBaseUrl} onChange={e => update('openaiBaseUrl', e.target.value)}
|
|
429
|
-
placeholder="https://api.openai.com/v1" />
|
|
430
|
-
</Field>
|
|
431
|
-
)}
|
|
432
|
-
</div>
|
|
433
|
-
)}
|
|
434
|
-
</div>
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// ─── Step 3: Ports ────────────────────────────────────────────────────────────
|
|
439
|
-
function Step3({
|
|
440
|
-
state, update, webPortStatus, mcpPortStatus, setWebPortStatus, setMcpPortStatus, checkPort, portConflict, s,
|
|
441
|
-
}: {
|
|
442
|
-
state: SetupState;
|
|
443
|
-
update: <K extends keyof SetupState>(key: K, val: SetupState[K]) => void;
|
|
444
|
-
webPortStatus: PortStatus;
|
|
445
|
-
mcpPortStatus: PortStatus;
|
|
446
|
-
setWebPortStatus: (s: PortStatus) => void;
|
|
447
|
-
setMcpPortStatus: (s: PortStatus) => void;
|
|
448
|
-
checkPort: (port: number, which: 'web' | 'mcp') => void;
|
|
449
|
-
portConflict: boolean;
|
|
450
|
-
s: ReturnType<typeof useLocale>['t']['setup'];
|
|
451
|
-
}) {
|
|
452
|
-
return (
|
|
453
|
-
<div className="space-y-5">
|
|
454
|
-
<PortField
|
|
455
|
-
label={s.webPort} hint={s.portHint} value={state.webPort}
|
|
456
|
-
onChange={v => { update('webPort', v); setWebPortStatus({ checking: false, available: null, isSelf: false, suggestion: null }); }}
|
|
457
|
-
status={webPortStatus}
|
|
458
|
-
onCheckPort={port => checkPort(port, 'web')}
|
|
459
|
-
s={s}
|
|
460
|
-
/>
|
|
461
|
-
<PortField
|
|
462
|
-
label={s.mcpPort} hint={s.portHint} value={state.mcpPort}
|
|
463
|
-
onChange={v => { update('mcpPort', v); setMcpPortStatus({ checking: false, available: null, isSelf: false, suggestion: null }); }}
|
|
464
|
-
status={mcpPortStatus}
|
|
465
|
-
onCheckPort={port => checkPort(port, 'mcp')}
|
|
466
|
-
s={s}
|
|
467
|
-
/>
|
|
468
|
-
{portConflict && (
|
|
469
|
-
<p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--amber)' }}>
|
|
470
|
-
<AlertTriangle size={12} /> {s.portConflict}
|
|
471
|
-
</p>
|
|
472
|
-
)}
|
|
473
|
-
{!portConflict && (webPortStatus.available === null || mcpPortStatus.available === null) && !webPortStatus.checking && !mcpPortStatus.checking && (
|
|
474
|
-
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.portVerifyHint}</p>
|
|
475
|
-
)}
|
|
476
|
-
<p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
|
|
477
|
-
<Info size={12} /> {s.portRestartWarning}
|
|
478
|
-
</p>
|
|
479
|
-
</div>
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// ─── Step 5: Agent Tools ──────────────────────────────────────────────────────
|
|
484
|
-
function Step5({
|
|
485
|
-
agents, agentsLoading, selectedAgents, setSelectedAgents,
|
|
486
|
-
agentTransport, setAgentTransport, agentScope, setAgentScope,
|
|
487
|
-
agentStatuses, s, settingsMcp, template,
|
|
488
|
-
}: {
|
|
489
|
-
agents: AgentEntry[];
|
|
490
|
-
agentsLoading: boolean;
|
|
491
|
-
selectedAgents: Set<string>;
|
|
492
|
-
setSelectedAgents: React.Dispatch<React.SetStateAction<Set<string>>>;
|
|
493
|
-
agentTransport: 'auto' | 'stdio' | 'http';
|
|
494
|
-
setAgentTransport: (v: 'auto' | 'stdio' | 'http') => void;
|
|
495
|
-
agentScope: 'global' | 'project';
|
|
496
|
-
setAgentScope: (v: 'global' | 'project') => void;
|
|
497
|
-
agentStatuses: Record<string, AgentInstallStatus>;
|
|
498
|
-
s: ReturnType<typeof useLocale>['t']['setup'];
|
|
499
|
-
settingsMcp: ReturnType<typeof useLocale>['t']['settings']['mcp'];
|
|
500
|
-
template: Template;
|
|
501
|
-
}) {
|
|
502
|
-
const toggleAgent = (key: string) => {
|
|
503
|
-
setSelectedAgents(prev => {
|
|
504
|
-
const next = new Set(prev);
|
|
505
|
-
if (next.has(key)) next.delete(key); else next.add(key);
|
|
506
|
-
return next;
|
|
507
|
-
});
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
const getEffectiveTransport = (agent: AgentEntry) => {
|
|
511
|
-
if (agentTransport === 'auto') return agent.preferredTransport;
|
|
512
|
-
return agentTransport;
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
const getStatusBadge = (key: string, agent: AgentEntry) => {
|
|
516
|
-
const st = agentStatuses[key];
|
|
517
|
-
if (st) {
|
|
518
|
-
if (st.state === 'installing') return (
|
|
519
|
-
<span className="flex items-center gap-1 text-[11px]" style={{ color: 'var(--muted-foreground)' }}>
|
|
520
|
-
<Loader2 size={10} className="animate-spin" /> {s.agentInstalling}
|
|
521
|
-
</span>
|
|
522
|
-
);
|
|
523
|
-
if (st.state === 'ok') return (
|
|
524
|
-
<span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
|
|
525
|
-
style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
|
|
526
|
-
<CheckCircle2 size={10} /> {s.agentStatusOk}
|
|
527
|
-
</span>
|
|
528
|
-
);
|
|
529
|
-
if (st.state === 'error') return (
|
|
530
|
-
<span className="flex items-center gap-1 text-[11px] px-1.5 py-0.5 rounded"
|
|
531
|
-
style={{ background: 'rgba(239,68,68,0.1)', color: '#ef4444' }}>
|
|
532
|
-
<XCircle size={10} /> {s.agentStatusError}
|
|
533
|
-
{st.message && <span className="ml-1 text-[10px]">({st.message})</span>}
|
|
534
|
-
</span>
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
if (agent.installed) return (
|
|
538
|
-
<span className="text-[11px] px-1.5 py-0.5 rounded"
|
|
539
|
-
style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
|
|
540
|
-
{settingsMcp.installed}
|
|
541
|
-
</span>
|
|
542
|
-
);
|
|
543
|
-
if (agent.present) return (
|
|
544
|
-
<span className="text-[11px] px-1.5 py-0.5 rounded"
|
|
545
|
-
style={{ background: 'rgba(245,158,11,0.12)', color: '#f59e0b' }}>
|
|
546
|
-
{s.agentDetected ?? 'detected'}
|
|
547
|
-
</span>
|
|
548
|
-
);
|
|
549
|
-
return (
|
|
550
|
-
<span className="text-[11px] px-1.5 py-0.5 rounded"
|
|
551
|
-
style={{ background: 'rgba(100,100,120,0.1)', color: 'var(--muted-foreground)' }}>
|
|
552
|
-
{s.agentNotFound ?? s.agentNotInstalled}
|
|
553
|
-
</span>
|
|
554
|
-
);
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
return (
|
|
558
|
-
<div className="space-y-5">
|
|
559
|
-
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.agentToolsHint}</p>
|
|
560
|
-
{agentsLoading ? (
|
|
561
|
-
<div className="flex items-center gap-2 py-4" style={{ color: 'var(--muted-foreground)' }}>
|
|
562
|
-
<Loader2 size={14} className="animate-spin" />
|
|
563
|
-
<span className="text-sm">{s.agentToolsLoading}</span>
|
|
564
|
-
</div>
|
|
565
|
-
) : agents.length === 0 ? (
|
|
566
|
-
<p className="text-sm py-4 text-center" style={{ color: 'var(--muted-foreground)' }}>
|
|
567
|
-
{s.agentToolsEmpty}
|
|
568
|
-
</p>
|
|
569
|
-
) : (
|
|
570
|
-
<>
|
|
571
|
-
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
572
|
-
{agents.map((agent, i) => (
|
|
573
|
-
<label key={agent.key}
|
|
574
|
-
className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors"
|
|
575
|
-
style={{
|
|
576
|
-
background: i % 2 === 0 ? 'var(--card)' : 'transparent',
|
|
577
|
-
borderTop: i > 0 ? '1px solid var(--border)' : undefined,
|
|
578
|
-
}}>
|
|
579
|
-
<input
|
|
580
|
-
type="checkbox"
|
|
581
|
-
checked={selectedAgents.has(agent.key)}
|
|
582
|
-
onChange={() => toggleAgent(agent.key)}
|
|
583
|
-
className="accent-amber-500"
|
|
584
|
-
disabled={agentStatuses[agent.key]?.state === 'installing'}
|
|
585
|
-
/>
|
|
586
|
-
<span className="text-sm flex-1" style={{ color: 'var(--foreground)' }}>{agent.name}</span>
|
|
587
|
-
<span className="text-[10px] px-1.5 py-0.5 rounded font-mono"
|
|
588
|
-
style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
|
|
589
|
-
{getEffectiveTransport(agent)}
|
|
590
|
-
</span>
|
|
591
|
-
{getStatusBadge(agent.key, agent)}
|
|
592
|
-
</label>
|
|
593
|
-
))}
|
|
594
|
-
</div>
|
|
595
|
-
{/* Skill auto-install hint */}
|
|
596
|
-
<div className="flex items-center gap-2 px-3 py-2 rounded-lg text-xs"
|
|
597
|
-
style={{ background: 'rgba(100,100,120,0.06)', color: 'var(--muted-foreground)' }}>
|
|
598
|
-
<Brain size={13} className="shrink-0" />
|
|
599
|
-
<span>{s.skillAutoHint(template === 'zh' ? 'mindos-zh' : 'mindos')}</span>
|
|
600
|
-
</div>
|
|
601
|
-
<div className="grid grid-cols-2 gap-4">
|
|
602
|
-
<Field label={s.agentTransport}>
|
|
603
|
-
<Select value={agentTransport} onChange={e => setAgentTransport(e.target.value as 'auto' | 'stdio' | 'http')}>
|
|
604
|
-
<option value="auto">{s.agentTransportAuto}</option>
|
|
605
|
-
<option value="stdio">{settingsMcp.transportStdio}</option>
|
|
606
|
-
<option value="http">{settingsMcp.transportHttp}</option>
|
|
607
|
-
</Select>
|
|
608
|
-
</Field>
|
|
609
|
-
<Field label={s.agentScope}>
|
|
610
|
-
<Select value={agentScope} onChange={e => setAgentScope(e.target.value as 'global' | 'project')}>
|
|
611
|
-
<option value="global">{settingsMcp.global}</option>
|
|
612
|
-
<option value="project">{settingsMcp.project}</option>
|
|
613
|
-
</Select>
|
|
614
|
-
</Field>
|
|
615
|
-
</div>
|
|
616
|
-
<button
|
|
617
|
-
type="button"
|
|
618
|
-
onClick={() => setSelectedAgents(new Set())}
|
|
619
|
-
className="text-xs underline mt-1"
|
|
620
|
-
style={{ color: 'var(--muted-foreground)' }}>
|
|
621
|
-
{s.agentSkipLater}
|
|
622
|
-
</button>
|
|
623
|
-
</>
|
|
624
|
-
)}
|
|
625
|
-
</div>
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// ─── Restart Block ────────────────────────────────────────────────────────────
|
|
630
|
-
function RestartBlock({ s, newPort }: { s: ReturnType<typeof useLocale>['t']['setup']; newPort: number }) {
|
|
631
|
-
const [restarting, setRestarting] = useState(false);
|
|
632
|
-
const [done, setDone] = useState(false);
|
|
633
|
-
|
|
634
|
-
const handleRestart = async () => {
|
|
635
|
-
setRestarting(true);
|
|
636
|
-
try {
|
|
637
|
-
await fetch('/api/restart', { method: 'POST' });
|
|
638
|
-
setDone(true);
|
|
639
|
-
const redirect = () => { window.location.href = `http://localhost:${newPort}/?welcome=1`; };
|
|
640
|
-
// Poll the new port until ready, then redirect
|
|
641
|
-
let attempts = 0;
|
|
642
|
-
const poll = setInterval(async () => {
|
|
643
|
-
attempts++;
|
|
644
|
-
try {
|
|
645
|
-
const r = await fetch(`http://localhost:${newPort}/api/health`);
|
|
646
|
-
if (r.status < 500) { clearInterval(poll); redirect(); return; }
|
|
647
|
-
} catch { /* not ready yet */ }
|
|
648
|
-
if (attempts >= 10) { clearInterval(poll); redirect(); }
|
|
649
|
-
}, 800);
|
|
650
|
-
} catch {
|
|
651
|
-
setRestarting(false);
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
if (done) {
|
|
656
|
-
return (
|
|
657
|
-
<div className="p-3 rounded-lg text-sm flex items-center gap-2"
|
|
658
|
-
style={{ background: 'rgba(34,197,94,0.1)', color: '#22c55e' }}>
|
|
659
|
-
<CheckCircle2 size={14} /> {s.restartDone}
|
|
660
|
-
</div>
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
return (
|
|
665
|
-
<div className="space-y-3">
|
|
666
|
-
<div className="p-3 rounded-lg text-sm flex items-center gap-2"
|
|
667
|
-
style={{ background: 'rgba(200,135,30,0.1)', color: 'var(--amber)' }}>
|
|
668
|
-
<AlertTriangle size={14} /> {s.restartRequired}
|
|
669
|
-
</div>
|
|
670
|
-
<div className="flex items-center gap-3">
|
|
671
|
-
<button
|
|
672
|
-
type="button"
|
|
673
|
-
onClick={handleRestart}
|
|
674
|
-
disabled={restarting}
|
|
675
|
-
className="flex items-center gap-1.5 px-4 py-2 text-sm rounded-lg transition-colors disabled:opacity-50"
|
|
676
|
-
style={{ background: 'var(--amber)', color: 'white' }}>
|
|
677
|
-
{restarting ? <Loader2 size={13} className="animate-spin" /> : null}
|
|
678
|
-
{restarting ? s.restarting : s.restartNow}
|
|
679
|
-
</button>
|
|
680
|
-
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
|
|
681
|
-
{s.restartManual} <code className="font-mono">mindos start</code>
|
|
682
|
-
</span>
|
|
683
|
-
</div>
|
|
684
|
-
</div>
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// ─── Step 6: Review ───────────────────────────────────────────────────────────
|
|
689
|
-
function Step6({
|
|
690
|
-
state, selectedAgents, agentStatuses, onRetryAgent, error, needsRestart, maskKey, s,
|
|
691
|
-
skillInstallResult,
|
|
692
|
-
}: {
|
|
693
|
-
state: SetupState;
|
|
694
|
-
selectedAgents: Set<string>;
|
|
695
|
-
agentStatuses: Record<string, AgentInstallStatus>;
|
|
696
|
-
onRetryAgent: (key: string) => void;
|
|
697
|
-
error: string;
|
|
698
|
-
needsRestart: boolean;
|
|
699
|
-
maskKey: (key: string) => string;
|
|
700
|
-
s: ReturnType<typeof useLocale>['t']['setup'];
|
|
701
|
-
skillInstallResult: { ok?: boolean; skill?: string; error?: string } | null;
|
|
702
|
-
}) {
|
|
703
|
-
const skillName = state.template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
704
|
-
const rows: [string, string][] = [
|
|
705
|
-
[s.kbPath, state.mindRoot],
|
|
706
|
-
[s.template, state.template || '—'],
|
|
707
|
-
[s.aiProvider, state.provider === 'skip' ? s.aiSkipTitle : state.provider],
|
|
708
|
-
...(state.provider !== 'skip' ? [
|
|
709
|
-
[s.apiKey, maskKey(state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey)] as [string, string],
|
|
710
|
-
[s.model, state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel] as [string, string],
|
|
711
|
-
] : []),
|
|
712
|
-
[s.webPort, String(state.webPort)],
|
|
713
|
-
[s.mcpPort, String(state.mcpPort)],
|
|
714
|
-
[s.authToken, state.authToken || '—'],
|
|
715
|
-
[s.webPassword, state.webPassword ? '••••••••' : '(none)'],
|
|
716
|
-
[s.agentToolsTitle, selectedAgents.size > 0 ? Array.from(selectedAgents).join(', ') : '—'],
|
|
717
|
-
[s.skillLabel, skillName],
|
|
718
|
-
];
|
|
719
|
-
|
|
720
|
-
const failedAgents = Object.entries(agentStatuses).filter(([, v]) => v.state === 'error');
|
|
721
|
-
const successAgents = Object.entries(agentStatuses).filter(([, v]) => v.state === 'ok');
|
|
722
|
-
|
|
723
|
-
return (
|
|
724
|
-
<div className="space-y-5">
|
|
725
|
-
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.reviewHint}</p>
|
|
726
|
-
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
727
|
-
{rows.map(([label, value], i) => (
|
|
728
|
-
<div key={i} className="flex items-center justify-between px-4 py-3 text-sm"
|
|
729
|
-
style={{
|
|
730
|
-
background: i % 2 === 0 ? 'var(--card)' : 'transparent',
|
|
731
|
-
borderTop: i > 0 ? '1px solid var(--border)' : undefined,
|
|
732
|
-
}}>
|
|
733
|
-
<span style={{ color: 'var(--muted-foreground)' }}>{label}</span>
|
|
734
|
-
<span className="font-mono text-xs" style={{ color: 'var(--foreground)' }}>{value}</span>
|
|
735
|
-
</div>
|
|
736
|
-
))}
|
|
737
|
-
</div>
|
|
738
|
-
|
|
739
|
-
{/* Agent verification results */}
|
|
740
|
-
{successAgents.length > 0 && (
|
|
741
|
-
<div className="space-y-1.5">
|
|
742
|
-
<p className="text-xs font-medium" style={{ color: 'var(--muted-foreground)' }}>{s.reviewInstallResults}</p>
|
|
743
|
-
{successAgents.map(([key, st]) => (
|
|
744
|
-
<div key={key} className="flex items-center gap-2 text-xs px-3 py-1.5 rounded"
|
|
745
|
-
style={{ background: 'rgba(34,197,94,0.06)' }}>
|
|
746
|
-
<CheckCircle2 size={11} className="text-green-500 shrink-0" />
|
|
747
|
-
<span style={{ color: 'var(--foreground)' }}>{key}</span>
|
|
748
|
-
<span className="font-mono text-[10px] px-1 py-0.5 rounded"
|
|
749
|
-
style={{ background: 'rgba(100,100,120,0.08)', color: 'var(--muted-foreground)' }}>
|
|
750
|
-
{st.transport || 'stdio'}
|
|
751
|
-
</span>
|
|
752
|
-
{st.transport === 'http' ? (
|
|
753
|
-
st.verified ? (
|
|
754
|
-
<span className="text-[10px] px-1.5 py-0.5 rounded"
|
|
755
|
-
style={{ background: 'rgba(34,197,94,0.12)', color: '#22c55e' }}>
|
|
756
|
-
{s.agentVerified}
|
|
757
|
-
</span>
|
|
758
|
-
) : (
|
|
759
|
-
<span className="text-[10px] px-1.5 py-0.5 rounded"
|
|
760
|
-
style={{ background: 'rgba(239,168,68,0.12)', color: '#f59e0b' }}
|
|
761
|
-
title={st.verifyError}>
|
|
762
|
-
{s.agentUnverified}
|
|
763
|
-
</span>
|
|
764
|
-
)
|
|
765
|
-
) : (
|
|
766
|
-
<span className="text-[10px]" style={{ color: 'var(--muted-foreground)' }}>
|
|
767
|
-
{s.agentVerifyNote}
|
|
768
|
-
</span>
|
|
769
|
-
)}
|
|
770
|
-
</div>
|
|
771
|
-
))}
|
|
772
|
-
</div>
|
|
773
|
-
)}
|
|
774
|
-
|
|
775
|
-
{failedAgents.length > 0 && (
|
|
776
|
-
<div className="p-3 rounded-lg space-y-2" style={{ background: 'rgba(239,68,68,0.08)' }}>
|
|
777
|
-
<p className="text-xs font-medium" style={{ color: '#ef4444' }}>{s.reviewInstallResults}</p>
|
|
778
|
-
{failedAgents.map(([key, st]) => (
|
|
779
|
-
<div key={key} className="flex items-center justify-between gap-2">
|
|
780
|
-
<span className="text-xs flex items-center gap-1" style={{ color: '#ef4444' }}>
|
|
781
|
-
<XCircle size={11} /> {key}{st.message ? ` — ${st.message}` : ''}
|
|
782
|
-
</span>
|
|
783
|
-
<button
|
|
784
|
-
type="button"
|
|
785
|
-
onClick={() => onRetryAgent(key)}
|
|
786
|
-
disabled={st.state === 'installing'}
|
|
787
|
-
className="text-xs px-2 py-0.5 rounded border transition-colors disabled:opacity-40"
|
|
788
|
-
style={{ borderColor: '#ef4444', color: '#ef4444' }}>
|
|
789
|
-
{st.state === 'installing' ? <Loader2 size={10} className="animate-spin inline" /> : s.retryAgent}
|
|
790
|
-
</button>
|
|
791
|
-
</div>
|
|
792
|
-
))}
|
|
793
|
-
<p className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{s.agentFailureNote}</p>
|
|
794
|
-
</div>
|
|
795
|
-
)}
|
|
796
|
-
{/* Skill install result */}
|
|
797
|
-
{skillInstallResult && (
|
|
798
|
-
<div className={`flex items-center gap-2 text-xs px-3 py-2 rounded-lg ${
|
|
799
|
-
skillInstallResult.ok ? '' : ''
|
|
800
|
-
}`} style={{
|
|
801
|
-
background: skillInstallResult.ok ? 'rgba(34,197,94,0.06)' : 'rgba(239,68,68,0.06)',
|
|
802
|
-
}}>
|
|
803
|
-
{skillInstallResult.ok ? (
|
|
804
|
-
<><CheckCircle2 size={11} className="text-green-500 shrink-0" />
|
|
805
|
-
<span style={{ color: 'var(--foreground)' }}>{s.skillInstalled} — {skillInstallResult.skill}</span></>
|
|
806
|
-
) : (
|
|
807
|
-
<><XCircle size={11} className="text-red-500 shrink-0" />
|
|
808
|
-
<span style={{ color: '#ef4444' }}>{s.skillFailed}{skillInstallResult.error ? `: ${skillInstallResult.error}` : ''}</span></>
|
|
809
|
-
)}
|
|
810
|
-
</div>
|
|
811
|
-
)}
|
|
812
|
-
{error && (
|
|
813
|
-
<div className="p-3 rounded-lg text-sm text-red-500" style={{ background: 'rgba(239,68,68,0.1)' }}>
|
|
814
|
-
{s.completeFailed}: {error}
|
|
815
|
-
</div>
|
|
816
|
-
)}
|
|
817
|
-
{needsRestart && <RestartBlock s={s} newPort={state.webPort} />}
|
|
818
|
-
</div>
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// ─── Step dots ────────────────────────────────────────────────────────────────
|
|
823
|
-
function StepDots({ step, setStep, stepTitles }: {
|
|
824
|
-
step: number;
|
|
825
|
-
setStep: (s: number) => void;
|
|
826
|
-
stepTitles: readonly string[];
|
|
827
|
-
}) {
|
|
828
|
-
return (
|
|
829
|
-
<div className="flex items-center gap-2 mb-8">
|
|
830
|
-
{stepTitles.map((title: string, i: number) => (
|
|
831
|
-
<div key={i} className="flex items-center gap-2">
|
|
832
|
-
{i > 0 && <div className="w-8 h-px" style={{ background: i <= step ? 'var(--amber)' : 'var(--border)' }} />}
|
|
833
|
-
<button onClick={() => i < step && setStep(i)} className="flex items-center gap-1.5" disabled={i > step}>
|
|
834
|
-
<div
|
|
835
|
-
className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
|
|
836
|
-
style={{
|
|
837
|
-
background: i <= step ? 'var(--amber)' : 'var(--muted)',
|
|
838
|
-
color: i <= step ? 'white' : 'var(--muted-foreground)',
|
|
839
|
-
opacity: i <= step ? 1 : 0.5,
|
|
840
|
-
}}>
|
|
841
|
-
{i + 1}
|
|
842
|
-
</div>
|
|
843
|
-
<span className="text-xs hidden sm:inline"
|
|
844
|
-
style={{ color: i === step ? 'var(--foreground)' : 'var(--muted-foreground)', opacity: i <= step ? 1 : 0.5 }}>
|
|
845
|
-
{title}
|
|
846
|
-
</span>
|
|
847
|
-
</button>
|
|
848
|
-
</div>
|
|
849
|
-
))}
|
|
850
|
-
</div>
|
|
851
|
-
);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// ─── Main component ───────────────────────────────────────────────────────────
|
|
855
|
-
export default function SetupWizard() {
|
|
856
|
-
const { t } = useLocale();
|
|
857
|
-
const s = t.setup;
|
|
858
|
-
|
|
859
|
-
const [step, setStep] = useState(0);
|
|
860
|
-
const [state, setState] = useState<SetupState>({
|
|
861
|
-
mindRoot: '~/MindOS/mind',
|
|
862
|
-
template: 'en',
|
|
863
|
-
provider: 'anthropic',
|
|
864
|
-
anthropicKey: '',
|
|
865
|
-
anthropicModel: 'claude-sonnet-4-6',
|
|
866
|
-
openaiKey: '',
|
|
867
|
-
openaiModel: 'gpt-5.4',
|
|
868
|
-
openaiBaseUrl: '',
|
|
869
|
-
webPort: 3000,
|
|
870
|
-
mcpPort: 8787,
|
|
871
|
-
authToken: '',
|
|
872
|
-
webPassword: '',
|
|
873
|
-
});
|
|
874
|
-
const [homeDir, setHomeDir] = useState('~');
|
|
875
|
-
const [tokenCopied, setTokenCopied] = useState(false);
|
|
876
|
-
const [submitting, setSubmitting] = useState(false);
|
|
877
|
-
const [completed, setCompleted] = useState(false);
|
|
878
|
-
const [error, setError] = useState('');
|
|
879
|
-
const [needsRestart, setNeedsRestart] = useState(false);
|
|
880
|
-
|
|
881
|
-
const [webPortStatus, setWebPortStatus] = useState<PortStatus>({ checking: false, available: null, isSelf: false, suggestion: null });
|
|
882
|
-
const [mcpPortStatus, setMcpPortStatus] = useState<PortStatus>({ checking: false, available: null, isSelf: false, suggestion: null });
|
|
883
|
-
|
|
884
|
-
const [agents, setAgents] = useState<AgentEntry[]>([]);
|
|
885
|
-
const [agentsLoading, setAgentsLoading] = useState(false);
|
|
886
|
-
const [selectedAgents, setSelectedAgents] = useState<Set<string>>(new Set());
|
|
887
|
-
const [agentTransport, setAgentTransport] = useState<'auto' | 'stdio' | 'http'>('auto');
|
|
888
|
-
const [agentScope, setAgentScope] = useState<'global' | 'project'>('global');
|
|
889
|
-
const [agentStatuses, setAgentStatuses] = useState<Record<string, AgentInstallStatus>>({});
|
|
890
|
-
const [skillInstallResult, setSkillInstallResult] = useState<{ ok?: boolean; skill?: string; error?: string } | null>(null);
|
|
891
|
-
|
|
892
|
-
// Load existing config as defaults on mount, generate token if none exists
|
|
893
|
-
useEffect(() => {
|
|
894
|
-
fetch('/api/setup')
|
|
895
|
-
.then(r => r.json())
|
|
896
|
-
.then(data => {
|
|
897
|
-
if (data.homeDir) setHomeDir(data.homeDir);
|
|
898
|
-
setState(prev => ({
|
|
899
|
-
...prev,
|
|
900
|
-
mindRoot: data.mindRoot || prev.mindRoot,
|
|
901
|
-
webPort: typeof data.port === 'number' ? data.port : prev.webPort,
|
|
902
|
-
mcpPort: typeof data.mcpPort === 'number' ? data.mcpPort : prev.mcpPort,
|
|
903
|
-
authToken: data.authToken || prev.authToken,
|
|
904
|
-
webPassword: data.webPassword || prev.webPassword,
|
|
905
|
-
provider: (data.provider === 'anthropic' || data.provider === 'openai') ? data.provider : prev.provider,
|
|
906
|
-
anthropicModel: data.anthropicModel || prev.anthropicModel,
|
|
907
|
-
openaiModel: data.openaiModel || prev.openaiModel,
|
|
908
|
-
openaiBaseUrl: data.openaiBaseUrl ?? prev.openaiBaseUrl,
|
|
909
|
-
}));
|
|
910
|
-
// Generate a new token only if none exists yet
|
|
911
|
-
if (!data.authToken) {
|
|
912
|
-
fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
913
|
-
.then(r => r.json())
|
|
914
|
-
.then(tokenData => { if (tokenData.token) setState(p => ({ ...p, authToken: tokenData.token })); })
|
|
915
|
-
.catch(() => {});
|
|
916
|
-
}
|
|
917
|
-
})
|
|
918
|
-
.catch(() => {
|
|
919
|
-
// Fallback: generate token on failure
|
|
920
|
-
fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
921
|
-
.then(r => r.json())
|
|
922
|
-
.then(data => { if (data.token) setState(prev => ({ ...prev, authToken: data.token })); })
|
|
923
|
-
.catch(() => {});
|
|
924
|
-
});
|
|
925
|
-
}, []);
|
|
926
|
-
|
|
927
|
-
// Auto-check ports when entering Step 3
|
|
928
|
-
useEffect(() => {
|
|
929
|
-
if (step === STEP_PORTS) {
|
|
930
|
-
checkPort(state.webPort, 'web');
|
|
931
|
-
checkPort(state.mcpPort, 'mcp');
|
|
932
|
-
}
|
|
933
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
934
|
-
}, [step]);
|
|
935
|
-
|
|
936
|
-
// Load agents when entering Step 5
|
|
937
|
-
useEffect(() => {
|
|
938
|
-
if (step === STEP_AGENTS && agents.length === 0 && !agentsLoading) {
|
|
939
|
-
setAgentsLoading(true);
|
|
940
|
-
fetch('/api/mcp/agents')
|
|
941
|
-
.then(r => r.json())
|
|
942
|
-
.then(data => {
|
|
943
|
-
if (data.agents) {
|
|
944
|
-
setAgents(data.agents);
|
|
945
|
-
setSelectedAgents(new Set(
|
|
946
|
-
(data.agents as AgentEntry[]).filter(a => a.installed || a.present).map(a => a.key)
|
|
947
|
-
));
|
|
948
|
-
}
|
|
949
|
-
})
|
|
950
|
-
.catch(() => {})
|
|
951
|
-
.finally(() => setAgentsLoading(false));
|
|
952
|
-
}
|
|
953
|
-
}, [step, agents.length, agentsLoading]);
|
|
954
|
-
|
|
955
|
-
const update = useCallback(<K extends keyof SetupState>(key: K, val: SetupState[K]) => {
|
|
956
|
-
setState(prev => ({ ...prev, [key]: val }));
|
|
957
|
-
}, []);
|
|
958
|
-
|
|
959
|
-
const generateToken = useCallback(async (seed?: string) => {
|
|
960
|
-
try {
|
|
961
|
-
const res = await fetch('/api/setup/generate-token', {
|
|
962
|
-
method: 'POST',
|
|
963
|
-
headers: { 'Content-Type': 'application/json' },
|
|
964
|
-
body: JSON.stringify({ seed: seed || undefined }),
|
|
965
|
-
});
|
|
966
|
-
const data = await res.json();
|
|
967
|
-
if (data.token) setState(prev => ({ ...prev, authToken: data.token }));
|
|
968
|
-
} catch { /* ignore */ }
|
|
969
|
-
}, []);
|
|
970
|
-
|
|
971
|
-
const copyToken = useCallback(() => {
|
|
972
|
-
setState(prev => { navigator.clipboard.writeText(prev.authToken); return prev; });
|
|
973
|
-
setTokenCopied(true);
|
|
974
|
-
setTimeout(() => setTokenCopied(false), 2000);
|
|
975
|
-
}, []);
|
|
976
|
-
|
|
977
|
-
const checkPort = useCallback(async (port: number, which: 'web' | 'mcp') => {
|
|
978
|
-
if (port < 1024 || port > 65535) return;
|
|
979
|
-
const setStatus = which === 'web' ? setWebPortStatus : setMcpPortStatus;
|
|
980
|
-
setStatus({ checking: true, available: null, isSelf: false, suggestion: null });
|
|
981
|
-
try {
|
|
982
|
-
const res = await fetch('/api/setup/check-port', {
|
|
983
|
-
method: 'POST',
|
|
984
|
-
headers: { 'Content-Type': 'application/json' },
|
|
985
|
-
body: JSON.stringify({ port }),
|
|
986
|
-
});
|
|
987
|
-
const data = await res.json();
|
|
988
|
-
setStatus({ checking: false, available: data.available ?? null, isSelf: !!data.isSelf, suggestion: data.suggestion ?? null });
|
|
989
|
-
} catch {
|
|
990
|
-
setStatus({ checking: false, available: null, isSelf: false, suggestion: null });
|
|
991
|
-
}
|
|
992
|
-
}, []);
|
|
993
|
-
|
|
994
|
-
const maskKey = (key: string) => {
|
|
995
|
-
if (!key) return '(not set)';
|
|
996
|
-
if (key.length <= 8) return '•••';
|
|
997
|
-
return key.slice(0, 6) + '•••' + key.slice(-3);
|
|
998
|
-
};
|
|
999
|
-
|
|
1000
|
-
const portConflict = state.webPort === state.mcpPort;
|
|
1001
|
-
|
|
1002
|
-
const canNext = () => {
|
|
1003
|
-
if (step === STEP_KB) return state.mindRoot.trim().length > 0;
|
|
1004
|
-
if (step === STEP_PORTS) {
|
|
1005
|
-
if (portConflict) return false;
|
|
1006
|
-
if (webPortStatus.checking || mcpPortStatus.checking) return false;
|
|
1007
|
-
if (webPortStatus.available !== true || mcpPortStatus.available !== true) return false;
|
|
1008
|
-
return (
|
|
1009
|
-
state.webPort >= 1024 && state.webPort <= 65535 &&
|
|
1010
|
-
state.mcpPort >= 1024 && state.mcpPort <= 65535
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
return true;
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
const handleComplete = async () => {
|
|
1017
|
-
setSubmitting(true);
|
|
1018
|
-
setError('');
|
|
1019
|
-
let restartNeeded = false;
|
|
1020
|
-
|
|
1021
|
-
// 1. Save setup config
|
|
1022
|
-
try {
|
|
1023
|
-
const payload = {
|
|
1024
|
-
mindRoot: state.mindRoot,
|
|
1025
|
-
template: state.template || undefined,
|
|
1026
|
-
port: state.webPort,
|
|
1027
|
-
mcpPort: state.mcpPort,
|
|
1028
|
-
authToken: state.authToken,
|
|
1029
|
-
webPassword: state.webPassword,
|
|
1030
|
-
ai: state.provider === 'skip' ? undefined : {
|
|
1031
|
-
provider: state.provider,
|
|
1032
|
-
providers: {
|
|
1033
|
-
anthropic: { apiKey: state.anthropicKey, model: state.anthropicModel },
|
|
1034
|
-
openai: { apiKey: state.openaiKey, model: state.openaiModel, baseUrl: state.openaiBaseUrl },
|
|
1035
|
-
},
|
|
1036
|
-
},
|
|
1037
|
-
};
|
|
1038
|
-
const res = await fetch('/api/setup', {
|
|
1039
|
-
method: 'POST',
|
|
1040
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1041
|
-
body: JSON.stringify(payload),
|
|
1042
|
-
});
|
|
1043
|
-
const data = await res.json();
|
|
1044
|
-
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
1045
|
-
restartNeeded = !!data.needsRestart;
|
|
1046
|
-
if (restartNeeded) setNeedsRestart(true);
|
|
1047
|
-
} catch (e) {
|
|
1048
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
1049
|
-
setSubmitting(false);
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
// 2. Install agents after config saved
|
|
1054
|
-
if (selectedAgents.size > 0) {
|
|
1055
|
-
const initialStatuses: Record<string, AgentInstallStatus> = {};
|
|
1056
|
-
for (const key of selectedAgents) initialStatuses[key] = { state: 'installing' };
|
|
1057
|
-
setAgentStatuses(initialStatuses);
|
|
1058
|
-
|
|
1059
|
-
try {
|
|
1060
|
-
const agentsPayload = Array.from(selectedAgents).map(key => {
|
|
1061
|
-
const agent = agents.find(a => a.key === key);
|
|
1062
|
-
const effectiveTransport = agentTransport === 'auto'
|
|
1063
|
-
? (agent?.preferredTransport || 'stdio')
|
|
1064
|
-
: agentTransport;
|
|
1065
|
-
return { key, scope: agentScope, transport: effectiveTransport };
|
|
1066
|
-
});
|
|
1067
|
-
const res = await fetch('/api/mcp/install', {
|
|
1068
|
-
method: 'POST',
|
|
1069
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1070
|
-
body: JSON.stringify({
|
|
1071
|
-
agents: agentsPayload,
|
|
1072
|
-
transport: agentTransport,
|
|
1073
|
-
url: `http://localhost:${state.mcpPort}/mcp`,
|
|
1074
|
-
token: state.authToken || undefined,
|
|
1075
|
-
}),
|
|
1076
|
-
});
|
|
1077
|
-
const data = await res.json();
|
|
1078
|
-
if (data.results) {
|
|
1079
|
-
const updated: Record<string, AgentInstallStatus> = {};
|
|
1080
|
-
for (const r of data.results as Array<{ agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string }>) {
|
|
1081
|
-
updated[r.agent] = {
|
|
1082
|
-
state: r.status === 'ok' ? 'ok' : 'error',
|
|
1083
|
-
message: r.message,
|
|
1084
|
-
transport: r.transport,
|
|
1085
|
-
verified: r.verified,
|
|
1086
|
-
verifyError: r.verifyError,
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
setAgentStatuses(updated);
|
|
1090
|
-
}
|
|
1091
|
-
} catch {
|
|
1092
|
-
const errStatuses: Record<string, AgentInstallStatus> = {};
|
|
1093
|
-
for (const key of selectedAgents) errStatuses[key] = { state: 'error' };
|
|
1094
|
-
setAgentStatuses(errStatuses);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// 3. Install skill to agents
|
|
1099
|
-
const skillName = state.template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
1100
|
-
try {
|
|
1101
|
-
const skillRes = await fetch('/api/mcp/install-skill', {
|
|
1102
|
-
method: 'POST',
|
|
1103
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1104
|
-
body: JSON.stringify({ skill: skillName, agents: Array.from(selectedAgents) }),
|
|
1105
|
-
});
|
|
1106
|
-
const skillData = await skillRes.json();
|
|
1107
|
-
setSkillInstallResult(skillData);
|
|
1108
|
-
} catch {
|
|
1109
|
-
setSkillInstallResult({ error: 'Failed to install skill' });
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
setSubmitting(false);
|
|
1113
|
-
setCompleted(true);
|
|
1114
|
-
|
|
1115
|
-
if (restartNeeded) {
|
|
1116
|
-
// Config changed requiring restart — stay on page, show restart block
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
window.location.href = '/?welcome=1';
|
|
1120
|
-
};
|
|
1121
|
-
|
|
1122
|
-
const retryAgent = useCallback(async (key: string) => {
|
|
1123
|
-
setAgentStatuses(prev => ({ ...prev, [key]: { state: 'installing' } }));
|
|
1124
|
-
try {
|
|
1125
|
-
const agent = agents.find(a => a.key === key);
|
|
1126
|
-
const effectiveTransport = agentTransport === 'auto'
|
|
1127
|
-
? (agent?.preferredTransport || 'stdio')
|
|
1128
|
-
: agentTransport;
|
|
1129
|
-
const res = await fetch('/api/mcp/install', {
|
|
1130
|
-
method: 'POST',
|
|
1131
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1132
|
-
body: JSON.stringify({
|
|
1133
|
-
agents: [{ key, scope: agentScope, transport: effectiveTransport }],
|
|
1134
|
-
transport: agentTransport,
|
|
1135
|
-
url: `http://localhost:${state.mcpPort}/mcp`,
|
|
1136
|
-
token: state.authToken || undefined,
|
|
1137
|
-
}),
|
|
1138
|
-
});
|
|
1139
|
-
const data = await res.json();
|
|
1140
|
-
if (data.results?.[0]) {
|
|
1141
|
-
const r = data.results[0] as { agent: string; status: string; message?: string; transport?: string; verified?: boolean; verifyError?: string };
|
|
1142
|
-
setAgentStatuses(prev => ({
|
|
1143
|
-
...prev,
|
|
1144
|
-
[key]: {
|
|
1145
|
-
state: r.status === 'ok' ? 'ok' : 'error',
|
|
1146
|
-
message: r.message,
|
|
1147
|
-
transport: r.transport,
|
|
1148
|
-
verified: r.verified,
|
|
1149
|
-
verifyError: r.verifyError,
|
|
1150
|
-
},
|
|
1151
|
-
}));
|
|
1152
|
-
}
|
|
1153
|
-
} catch {
|
|
1154
|
-
setAgentStatuses(prev => ({ ...prev, [key]: { state: 'error' } }));
|
|
1155
|
-
}
|
|
1156
|
-
}, [agents, agentScope, agentTransport, state.mcpPort, state.authToken]);
|
|
1157
|
-
|
|
1158
|
-
return (
|
|
1159
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
|
1160
|
-
style={{ background: 'var(--background)' }}>
|
|
1161
|
-
<div className="w-full max-w-xl mx-auto px-6 py-12">
|
|
1162
|
-
<div className="text-center mb-8">
|
|
1163
|
-
<div className="inline-flex items-center gap-2 mb-2">
|
|
1164
|
-
<Sparkles size={18} style={{ color: 'var(--amber)' }} />
|
|
1165
|
-
<h1 className="text-2xl font-semibold tracking-tight font-display" style={{ color: 'var(--foreground)' }}>
|
|
1166
|
-
MindOS
|
|
1167
|
-
</h1>
|
|
1168
|
-
</div>
|
|
1169
|
-
</div>
|
|
1170
|
-
|
|
1171
|
-
<div className="flex justify-center">
|
|
1172
|
-
<StepDots step={step} setStep={setStep} stepTitles={s.stepTitles} />
|
|
1173
|
-
</div>
|
|
1174
|
-
|
|
1175
|
-
<h2 className="text-lg font-semibold mb-5" style={{ color: 'var(--foreground)' }}>
|
|
1176
|
-
{s.stepTitles[step]}
|
|
1177
|
-
</h2>
|
|
1178
|
-
|
|
1179
|
-
{step === 0 && <Step1 state={state} update={update} t={t} homeDir={homeDir} />}
|
|
1180
|
-
{step === 1 && <Step2 state={state} update={update} s={s} />}
|
|
1181
|
-
{step === 2 && (
|
|
1182
|
-
<Step3
|
|
1183
|
-
state={state} update={update}
|
|
1184
|
-
webPortStatus={webPortStatus} mcpPortStatus={mcpPortStatus}
|
|
1185
|
-
setWebPortStatus={setWebPortStatus} setMcpPortStatus={setMcpPortStatus}
|
|
1186
|
-
checkPort={checkPort} portConflict={portConflict} s={s}
|
|
1187
|
-
/>
|
|
1188
|
-
)}
|
|
1189
|
-
{step === 3 && (
|
|
1190
|
-
<Step4Inner
|
|
1191
|
-
authToken={state.authToken} tokenCopied={tokenCopied}
|
|
1192
|
-
onCopy={copyToken} onGenerate={generateToken}
|
|
1193
|
-
webPassword={state.webPassword} onPasswordChange={v => update('webPassword', v)}
|
|
1194
|
-
s={s}
|
|
1195
|
-
/>
|
|
1196
|
-
)}
|
|
1197
|
-
{step === 4 && (
|
|
1198
|
-
<Step5
|
|
1199
|
-
agents={agents} agentsLoading={agentsLoading}
|
|
1200
|
-
selectedAgents={selectedAgents} setSelectedAgents={setSelectedAgents}
|
|
1201
|
-
agentTransport={agentTransport} setAgentTransport={setAgentTransport}
|
|
1202
|
-
agentScope={agentScope} setAgentScope={setAgentScope}
|
|
1203
|
-
agentStatuses={agentStatuses} s={s} settingsMcp={t.settings.mcp}
|
|
1204
|
-
template={state.template}
|
|
1205
|
-
/>
|
|
1206
|
-
)}
|
|
1207
|
-
{step === 5 && (
|
|
1208
|
-
<Step6
|
|
1209
|
-
state={state} selectedAgents={selectedAgents}
|
|
1210
|
-
agentStatuses={agentStatuses} onRetryAgent={retryAgent}
|
|
1211
|
-
error={error} needsRestart={needsRestart}
|
|
1212
|
-
maskKey={maskKey} s={s}
|
|
1213
|
-
skillInstallResult={skillInstallResult}
|
|
1214
|
-
/>
|
|
1215
|
-
)}
|
|
1216
|
-
|
|
1217
|
-
{/* Navigation */}
|
|
1218
|
-
<div className="flex items-center justify-between mt-8 pt-6" style={{ borderTop: '1px solid var(--border)' }}>
|
|
1219
|
-
<button
|
|
1220
|
-
onClick={() => setStep(step - 1)}
|
|
1221
|
-
disabled={step === 0}
|
|
1222
|
-
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"
|
|
1223
|
-
style={{ color: 'var(--foreground)' }}>
|
|
1224
|
-
<ChevronLeft size={14} /> {s.back}
|
|
1225
|
-
</button>
|
|
1226
|
-
|
|
1227
|
-
{step < TOTAL_STEPS - 1 ? (
|
|
1228
|
-
<button
|
|
1229
|
-
onClick={() => setStep(step + 1)}
|
|
1230
|
-
disabled={!canNext()}
|
|
1231
|
-
className="flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
1232
|
-
style={{ background: 'var(--amber)', color: 'white' }}>
|
|
1233
|
-
{s.next} <ChevronRight size={14} />
|
|
1234
|
-
</button>
|
|
1235
|
-
) : completed ? (
|
|
1236
|
-
// After completing: show Done link (no restart needed) or nothing (RestartBlock handles it)
|
|
1237
|
-
!needsRestart ? (
|
|
1238
|
-
<a href="/?welcome=1"
|
|
1239
|
-
className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors"
|
|
1240
|
-
style={{ background: 'var(--amber)', color: 'white' }}>
|
|
1241
|
-
{s.completeDone} →
|
|
1242
|
-
</a>
|
|
1243
|
-
) : null
|
|
1244
|
-
) : (
|
|
1245
|
-
<button
|
|
1246
|
-
onClick={handleComplete}
|
|
1247
|
-
disabled={submitting}
|
|
1248
|
-
className="flex items-center gap-1 px-5 py-2 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
|
1249
|
-
style={{ background: 'var(--amber)', color: 'white' }}>
|
|
1250
|
-
{submitting && <Loader2 size={14} className="animate-spin" />}
|
|
1251
|
-
{submitting ? s.completing : s.complete}
|
|
1252
|
-
</button>
|
|
1253
|
-
)}
|
|
1254
|
-
</div>
|
|
1255
|
-
</div>
|
|
1256
|
-
</div>
|
|
1257
|
-
);
|
|
1258
|
-
}
|
|
1
|
+
export { default } from './setup';
|