@geminilight/mindos 0.2.1 → 0.4.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/init/route.ts +7 -41
- 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/settings/route.ts +3 -0
- package/app/app/api/setup/generate-token/route.ts +23 -0
- package/app/app/api/setup/route.ts +81 -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 +22 -2
- package/app/app/setup/page.tsx +9 -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 +479 -0
- 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 +270 -10
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +15 -1
- package/app/lib/template.ts +45 -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/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 +117 -1
- /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
|
@@ -14,6 +14,7 @@ import { KnowledgeTab } from './settings/KnowledgeTab';
|
|
|
14
14
|
import { PluginsTab } from './settings/PluginsTab';
|
|
15
15
|
import { ShortcutsTab } from './settings/ShortcutsTab';
|
|
16
16
|
import { SyncTab } from './settings/SyncTab';
|
|
17
|
+
import { McpTab } from './settings/McpTab';
|
|
17
18
|
|
|
18
19
|
interface SettingsModalProps {
|
|
19
20
|
open: boolean;
|
|
@@ -139,6 +140,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
139
140
|
{ id: 'appearance', label: t.settings.tabs.appearance },
|
|
140
141
|
{ id: 'knowledge', label: t.settings.tabs.knowledge },
|
|
141
142
|
{ id: 'sync', label: t.settings.tabs.sync ?? 'Sync' },
|
|
143
|
+
{ id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP' },
|
|
142
144
|
{ id: 'plugins', label: t.settings.tabs.plugins },
|
|
143
145
|
{ id: 'shortcuts', label: t.settings.tabs.shortcuts },
|
|
144
146
|
];
|
|
@@ -158,7 +160,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
158
160
|
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
|
|
159
161
|
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
160
162
|
<Settings size={15} className="text-muted-foreground" />
|
|
161
|
-
<span
|
|
163
|
+
<span className="font-display">{t.settings.title}</span>
|
|
162
164
|
</div>
|
|
163
165
|
<button onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
|
|
164
166
|
<X size={15} />
|
|
@@ -190,7 +192,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
190
192
|
<p className="text-sm text-destructive font-medium">Failed to load settings</p>
|
|
191
193
|
<p className="text-xs text-muted-foreground">Check that the server is running and AUTH_TOKEN is configured correctly.</p>
|
|
192
194
|
</div>
|
|
193
|
-
) : !data && tab !== 'shortcuts' && tab !== 'appearance' ? (
|
|
195
|
+
) : !data && tab !== 'shortcuts' && tab !== 'appearance' && tab !== 'mcp' && tab !== 'sync' ? (
|
|
194
196
|
<div className="flex justify-center py-8">
|
|
195
197
|
<Loader2 size={18} className="animate-spin text-muted-foreground" />
|
|
196
198
|
</div>
|
|
@@ -202,6 +204,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
202
204
|
{tab === 'plugins' && <PluginsTab pluginStates={pluginStates} setPluginStates={setPluginStates} t={t} />}
|
|
203
205
|
{tab === 'shortcuts' && <ShortcutsTab t={t} />}
|
|
204
206
|
{tab === 'sync' && <SyncTab t={t} />}
|
|
207
|
+
{tab === 'mcp' && <McpTab t={t} />}
|
|
205
208
|
</>
|
|
206
209
|
)}
|
|
207
210
|
</div>
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { Sparkles, Globe, BookOpen, FileText, Copy, Check, RefreshCw, Loader2, ChevronLeft, ChevronRight, AlertTriangle } from 'lucide-react';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
+
import { Field, Input, Select, ApiKeyInput } from '@/components/settings/Primitives';
|
|
8
|
+
|
|
9
|
+
type Template = 'en' | 'zh' | 'empty' | '';
|
|
10
|
+
|
|
11
|
+
interface SetupState {
|
|
12
|
+
mindRoot: string;
|
|
13
|
+
template: Template;
|
|
14
|
+
provider: 'anthropic' | 'openai' | 'skip';
|
|
15
|
+
anthropicKey: string;
|
|
16
|
+
anthropicModel: string;
|
|
17
|
+
openaiKey: string;
|
|
18
|
+
openaiModel: string;
|
|
19
|
+
openaiBaseUrl: string;
|
|
20
|
+
webPort: number;
|
|
21
|
+
mcpPort: number;
|
|
22
|
+
authToken: string;
|
|
23
|
+
webPassword: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TEMPLATES: Array<{
|
|
27
|
+
id: Template;
|
|
28
|
+
icon: React.ReactNode;
|
|
29
|
+
dirs: string[];
|
|
30
|
+
}> = [
|
|
31
|
+
{ id: 'en', icon: <Globe size={18} />, dirs: ['Profile/', 'Connections/', 'Notes/', 'Workflows/', 'Resources/', 'Projects/'] },
|
|
32
|
+
{ id: 'zh', icon: <BookOpen size={18} />, dirs: ['画像/', '关系/', '笔记/', '流程/', '资源/', '项目/'] },
|
|
33
|
+
{ id: 'empty', icon: <FileText size={18} />, dirs: ['README.md', 'CONFIG.json', 'INSTRUCTION.md'] },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const TOTAL_STEPS = 5;
|
|
37
|
+
|
|
38
|
+
export default function SetupWizard() {
|
|
39
|
+
const { t } = useLocale();
|
|
40
|
+
const router = useRouter();
|
|
41
|
+
const s = t.setup;
|
|
42
|
+
|
|
43
|
+
const [step, setStep] = useState(0);
|
|
44
|
+
const [state, setState] = useState<SetupState>({
|
|
45
|
+
mindRoot: '~/MindOS',
|
|
46
|
+
template: 'en',
|
|
47
|
+
provider: 'anthropic',
|
|
48
|
+
anthropicKey: '',
|
|
49
|
+
anthropicModel: 'claude-sonnet-4-6',
|
|
50
|
+
openaiKey: '',
|
|
51
|
+
openaiModel: 'gpt-5.4',
|
|
52
|
+
openaiBaseUrl: '',
|
|
53
|
+
webPort: 3000,
|
|
54
|
+
mcpPort: 8787,
|
|
55
|
+
authToken: '',
|
|
56
|
+
webPassword: '',
|
|
57
|
+
});
|
|
58
|
+
const [tokenCopied, setTokenCopied] = useState(false);
|
|
59
|
+
const [submitting, setSubmitting] = useState(false);
|
|
60
|
+
const [error, setError] = useState('');
|
|
61
|
+
const [portChanged, setPortChanged] = useState(false);
|
|
62
|
+
|
|
63
|
+
// Generate token on mount
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
fetch('/api/setup/generate-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
66
|
+
.then(r => r.json())
|
|
67
|
+
.then(data => { if (data.token) setState(prev => ({ ...prev, authToken: data.token })); })
|
|
68
|
+
.catch(() => {});
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const update = useCallback(<K extends keyof SetupState>(key: K, val: SetupState[K]) => {
|
|
72
|
+
setState(prev => ({ ...prev, [key]: val }));
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const generateToken = async (seed?: string) => {
|
|
76
|
+
try {
|
|
77
|
+
const res = await fetch('/api/setup/generate-token', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({ seed: seed || undefined }),
|
|
81
|
+
});
|
|
82
|
+
const data = await res.json();
|
|
83
|
+
if (data.token) update('authToken', data.token);
|
|
84
|
+
} catch { /* ignore */ }
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const copyToken = () => {
|
|
88
|
+
navigator.clipboard.writeText(state.authToken);
|
|
89
|
+
setTokenCopied(true);
|
|
90
|
+
setTimeout(() => setTokenCopied(false), 2000);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleComplete = async () => {
|
|
94
|
+
setSubmitting(true);
|
|
95
|
+
setError('');
|
|
96
|
+
try {
|
|
97
|
+
const payload = {
|
|
98
|
+
mindRoot: state.mindRoot.startsWith('~')
|
|
99
|
+
? state.mindRoot // server will resolve
|
|
100
|
+
: state.mindRoot,
|
|
101
|
+
template: state.template || undefined,
|
|
102
|
+
port: state.webPort,
|
|
103
|
+
mcpPort: state.mcpPort,
|
|
104
|
+
authToken: state.authToken,
|
|
105
|
+
webPassword: state.webPassword,
|
|
106
|
+
ai: state.provider === 'skip' ? undefined : {
|
|
107
|
+
provider: state.provider,
|
|
108
|
+
providers: {
|
|
109
|
+
anthropic: { apiKey: state.anthropicKey, model: state.anthropicModel },
|
|
110
|
+
openai: { apiKey: state.openaiKey, model: state.openaiModel, baseUrl: state.openaiBaseUrl },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const res = await fetch('/api/setup', {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: { 'Content-Type': 'application/json' },
|
|
117
|
+
body: JSON.stringify(payload),
|
|
118
|
+
});
|
|
119
|
+
const data = await res.json();
|
|
120
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
121
|
+
if (data.portChanged) setPortChanged(true);
|
|
122
|
+
else router.push('/');
|
|
123
|
+
} catch (e) {
|
|
124
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
125
|
+
} finally {
|
|
126
|
+
setSubmitting(false);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const canNext = () => {
|
|
131
|
+
if (step === 0) return state.mindRoot.trim().length > 0;
|
|
132
|
+
if (step === 2) return state.webPort >= 1024 && state.webPort <= 65535 && state.mcpPort >= 1024 && state.mcpPort <= 65535;
|
|
133
|
+
return true;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const maskKey = (key: string) => {
|
|
137
|
+
if (!key) return '(not set)';
|
|
138
|
+
if (key.length <= 8) return '•••';
|
|
139
|
+
return key.slice(0, 6) + '•••' + key.slice(-3);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Step indicator dots
|
|
143
|
+
const StepDots = () => (
|
|
144
|
+
<div className="flex items-center gap-2 mb-8">
|
|
145
|
+
{s.stepTitles.map((title: string, i: number) => (
|
|
146
|
+
<div key={i} className="flex items-center gap-2">
|
|
147
|
+
{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
|
+
>
|
|
153
|
+
<div
|
|
154
|
+
className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
|
|
155
|
+
style={{
|
|
156
|
+
background: i === step ? 'var(--amber)' : i < step ? 'var(--amber)' : 'var(--muted)',
|
|
157
|
+
color: i <= step ? 'white' : 'var(--muted-foreground)',
|
|
158
|
+
opacity: i <= step ? 1 : 0.5,
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{i + 1}
|
|
162
|
+
</div>
|
|
163
|
+
<span
|
|
164
|
+
className="text-xs hidden sm:inline"
|
|
165
|
+
style={{ color: i === step ? 'var(--foreground)' : 'var(--muted-foreground)', opacity: i <= step ? 1 : 0.5 }}
|
|
166
|
+
>
|
|
167
|
+
{title}
|
|
168
|
+
</span>
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Step 1: Knowledge Base
|
|
176
|
+
const Step1 = () => (
|
|
177
|
+
<div className="space-y-6">
|
|
178
|
+
<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
|
+
/>
|
|
184
|
+
</Field>
|
|
185
|
+
<div>
|
|
186
|
+
<label className="text-sm text-foreground font-medium mb-3 block">{s.template}</label>
|
|
187
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
188
|
+
{TEMPLATES.map(tpl => (
|
|
189
|
+
<button
|
|
190
|
+
key={tpl.id}
|
|
191
|
+
onClick={() => update('template', tpl.id)}
|
|
192
|
+
className="flex flex-col items-start gap-2 p-4 rounded-xl border text-left transition-all duration-150"
|
|
193
|
+
style={{
|
|
194
|
+
background: state.template === tpl.id ? 'var(--amber-subtle, rgba(200,135,30,0.08))' : 'var(--card)',
|
|
195
|
+
borderColor: state.template === tpl.id ? 'var(--amber)' : 'var(--border)',
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<div className="flex items-center gap-2">
|
|
199
|
+
<span style={{ color: 'var(--amber)' }}>{tpl.icon}</span>
|
|
200
|
+
<span className="text-sm font-medium" style={{ color: 'var(--foreground)' }}>
|
|
201
|
+
{t.onboarding.templates[tpl.id as 'en' | 'zh' | 'empty'].title}
|
|
202
|
+
</span>
|
|
203
|
+
</div>
|
|
204
|
+
<div
|
|
205
|
+
className="w-full rounded-lg px-2.5 py-1.5 text-[11px] leading-relaxed font-display"
|
|
206
|
+
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}
|
|
207
|
+
>
|
|
208
|
+
{tpl.dirs.map(d => <div key={d}>{d}</div>)}
|
|
209
|
+
</div>
|
|
210
|
+
</button>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Step 2: AI Provider
|
|
218
|
+
const Step2 = () => (
|
|
219
|
+
<div className="space-y-5">
|
|
220
|
+
<Field label={s.aiProvider} hint={s.aiProviderHint}>
|
|
221
|
+
<Select value={state.provider} onChange={e => update('provider', e.target.value as SetupState['provider'])}>
|
|
222
|
+
<option value="anthropic">Anthropic</option>
|
|
223
|
+
<option value="openai">OpenAI</option>
|
|
224
|
+
<option value="skip">{s.aiSkip}</option>
|
|
225
|
+
</Select>
|
|
226
|
+
</Field>
|
|
227
|
+
{state.provider !== 'skip' && (
|
|
228
|
+
<>
|
|
229
|
+
<Field label={s.apiKey}>
|
|
230
|
+
<ApiKeyInput
|
|
231
|
+
value={state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey}
|
|
232
|
+
onChange={v => update(state.provider === 'anthropic' ? 'anthropicKey' : 'openaiKey', v)}
|
|
233
|
+
placeholder={state.provider === 'anthropic' ? 'sk-ant-...' : 'sk-...'}
|
|
234
|
+
/>
|
|
235
|
+
</Field>
|
|
236
|
+
<Field label={s.model}>
|
|
237
|
+
<Input
|
|
238
|
+
value={state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel}
|
|
239
|
+
onChange={e => update(state.provider === 'anthropic' ? 'anthropicModel' : 'openaiModel', e.target.value)}
|
|
240
|
+
/>
|
|
241
|
+
</Field>
|
|
242
|
+
{state.provider === 'openai' && (
|
|
243
|
+
<Field label={s.baseUrl} hint={s.baseUrlHint}>
|
|
244
|
+
<Input
|
|
245
|
+
value={state.openaiBaseUrl}
|
|
246
|
+
onChange={e => update('openaiBaseUrl', e.target.value)}
|
|
247
|
+
placeholder="https://api.openai.com/v1"
|
|
248
|
+
/>
|
|
249
|
+
</Field>
|
|
250
|
+
)}
|
|
251
|
+
</>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Step 3: Ports
|
|
257
|
+
const Step3 = () => (
|
|
258
|
+
<div className="space-y-5">
|
|
259
|
+
<Field label={s.webPort} hint={s.portHint}>
|
|
260
|
+
<Input
|
|
261
|
+
type="number"
|
|
262
|
+
min={1024}
|
|
263
|
+
max={65535}
|
|
264
|
+
value={state.webPort}
|
|
265
|
+
onChange={e => update('webPort', parseInt(e.target.value, 10) || 3000)}
|
|
266
|
+
/>
|
|
267
|
+
</Field>
|
|
268
|
+
<Field label={s.mcpPort} hint={s.portHint}>
|
|
269
|
+
<Input
|
|
270
|
+
type="number"
|
|
271
|
+
min={1024}
|
|
272
|
+
max={65535}
|
|
273
|
+
value={state.mcpPort}
|
|
274
|
+
onChange={e => update('mcpPort', parseInt(e.target.value, 10) || 8787)}
|
|
275
|
+
/>
|
|
276
|
+
</Field>
|
|
277
|
+
<p className="text-xs flex items-center gap-1.5" style={{ color: 'var(--muted-foreground)' }}>
|
|
278
|
+
<AlertTriangle size={12} />
|
|
279
|
+
{s.portRestartWarning}
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Step 4: Security
|
|
285
|
+
const Step4 = () => {
|
|
286
|
+
const [seed, setSeed] = useState('');
|
|
287
|
+
const [showSeed, setShowSeed] = useState(false);
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div className="space-y-5">
|
|
291
|
+
<Field label={s.authToken} hint={s.authTokenHint}>
|
|
292
|
+
<div className="flex gap-2">
|
|
293
|
+
<Input value={state.authToken} readOnly className="font-mono text-xs" />
|
|
294
|
+
<button
|
|
295
|
+
onClick={copyToken}
|
|
296
|
+
className="flex items-center gap-1 px-3 py-2 text-xs rounded-lg border border-border hover:bg-muted transition-colors shrink-0"
|
|
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>
|
|
309
|
+
</div>
|
|
310
|
+
</Field>
|
|
311
|
+
|
|
312
|
+
<div>
|
|
313
|
+
<button
|
|
314
|
+
onClick={() => setShowSeed(!showSeed)}
|
|
315
|
+
className="text-xs underline"
|
|
316
|
+
style={{ color: 'var(--muted-foreground)' }}
|
|
317
|
+
>
|
|
318
|
+
{s.authTokenSeed}
|
|
319
|
+
</button>
|
|
320
|
+
{showSeed && (
|
|
321
|
+
<div className="mt-2 flex gap-2">
|
|
322
|
+
<Input
|
|
323
|
+
value={seed}
|
|
324
|
+
onChange={e => setSeed(e.target.value)}
|
|
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>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<Field label={s.webPassword} hint={s.webPasswordHint}>
|
|
339
|
+
<Input
|
|
340
|
+
type="password"
|
|
341
|
+
value={state.webPassword}
|
|
342
|
+
onChange={e => update('webPassword', e.target.value)}
|
|
343
|
+
placeholder="(optional)"
|
|
344
|
+
/>
|
|
345
|
+
</Field>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Step 5: Review
|
|
351
|
+
const Step5 = () => {
|
|
352
|
+
const rows: [string, string][] = [
|
|
353
|
+
[s.kbPath, state.mindRoot],
|
|
354
|
+
[s.template, state.template || '—'],
|
|
355
|
+
[s.aiProvider, state.provider],
|
|
356
|
+
...(state.provider !== 'skip' ? [
|
|
357
|
+
[s.apiKey, maskKey(state.provider === 'anthropic' ? state.anthropicKey : state.openaiKey)] as [string, string],
|
|
358
|
+
[s.model, state.provider === 'anthropic' ? state.anthropicModel : state.openaiModel] as [string, string],
|
|
359
|
+
] : []),
|
|
360
|
+
[s.webPort, String(state.webPort)],
|
|
361
|
+
[s.mcpPort, String(state.mcpPort)],
|
|
362
|
+
[s.authToken, state.authToken || '—'],
|
|
363
|
+
[s.webPassword, state.webPassword ? '••••••••' : '(none)'],
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<div className="space-y-5">
|
|
368
|
+
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>{s.reviewHint}</p>
|
|
369
|
+
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
370
|
+
{rows.map(([label, value], i) => (
|
|
371
|
+
<div
|
|
372
|
+
key={i}
|
|
373
|
+
className="flex items-center justify-between px-4 py-3 text-sm"
|
|
374
|
+
style={{
|
|
375
|
+
background: i % 2 === 0 ? 'var(--card)' : 'transparent',
|
|
376
|
+
borderTop: i > 0 ? '1px solid var(--border)' : undefined,
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
<span style={{ color: 'var(--muted-foreground)' }}>{label}</span>
|
|
380
|
+
<span className="font-mono text-xs" style={{ color: 'var(--foreground)' }}>{value}</span>
|
|
381
|
+
</div>
|
|
382
|
+
))}
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
{error && (
|
|
386
|
+
<div className="p-3 rounded-lg text-sm text-red-500" style={{ background: 'rgba(239,68,68,0.1)' }}>
|
|
387
|
+
{s.completeFailed}: {error}
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
|
|
391
|
+
{portChanged && (
|
|
392
|
+
<div className="space-y-3">
|
|
393
|
+
<div className="p-3 rounded-lg text-sm flex items-center gap-2" style={{ background: 'rgba(200,135,30,0.1)', color: 'var(--amber)' }}>
|
|
394
|
+
<AlertTriangle size={14} />
|
|
395
|
+
{s.portChanged}
|
|
396
|
+
</div>
|
|
397
|
+
<a
|
|
398
|
+
href="/"
|
|
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
|
+
>
|
|
402
|
+
{s.completeDone} →
|
|
403
|
+
</a>
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const steps = [Step1, Step2, Step3, Step4, Step5];
|
|
411
|
+
const CurrentStep = steps[step];
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" style={{ background: 'var(--background)' }}>
|
|
415
|
+
<div className="w-full max-w-xl mx-auto px-6 py-12">
|
|
416
|
+
{/* Header */}
|
|
417
|
+
<div className="text-center mb-8">
|
|
418
|
+
<div className="inline-flex items-center gap-2 mb-2">
|
|
419
|
+
<Sparkles size={18} style={{ color: 'var(--amber)' }} />
|
|
420
|
+
<h1
|
|
421
|
+
className="text-2xl font-semibold tracking-tight font-display"
|
|
422
|
+
style={{ color: 'var(--foreground)' }}
|
|
423
|
+
>
|
|
424
|
+
MindOS
|
|
425
|
+
</h1>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
{/* Step dots */}
|
|
430
|
+
<div className="flex justify-center">
|
|
431
|
+
<StepDots />
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
{/* Step title */}
|
|
435
|
+
<h2 className="text-lg font-semibold mb-5" style={{ color: 'var(--foreground)' }}>
|
|
436
|
+
{s.stepTitles[step]}
|
|
437
|
+
</h2>
|
|
438
|
+
|
|
439
|
+
{/* Step content */}
|
|
440
|
+
<CurrentStep />
|
|
441
|
+
|
|
442
|
+
{/* Navigation */}
|
|
443
|
+
<div className="flex items-center justify-between mt-8 pt-6" style={{ borderTop: '1px solid var(--border)' }}>
|
|
444
|
+
<button
|
|
445
|
+
onClick={() => setStep(step - 1)}
|
|
446
|
+
disabled={step === 0}
|
|
447
|
+
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}
|
|
452
|
+
</button>
|
|
453
|
+
|
|
454
|
+
{step < TOTAL_STEPS - 1 ? (
|
|
455
|
+
<button
|
|
456
|
+
onClick={() => setStep(step + 1)}
|
|
457
|
+
disabled={!canNext()}
|
|
458
|
+
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} />
|
|
463
|
+
</button>
|
|
464
|
+
) : (
|
|
465
|
+
<button
|
|
466
|
+
onClick={handleComplete}
|
|
467
|
+
disabled={submitting || portChanged}
|
|
468
|
+
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
|
+
>
|
|
471
|
+
{submitting && <Loader2 size={14} className="animate-spin" />}
|
|
472
|
+
{submitting ? s.completing : portChanged ? s.completeDone : s.complete}
|
|
473
|
+
</button>
|
|
474
|
+
)}
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
@@ -73,7 +73,7 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
|
|
|
73
73
|
<div className="flex items-center justify-between px-4 py-4 border-b border-border shrink-0">
|
|
74
74
|
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
|
75
75
|
<Logo id="desktop" />
|
|
76
|
-
<span className="font-semibold text-foreground text-sm tracking-wide
|
|
76
|
+
<span className="font-semibold text-foreground text-sm tracking-wide font-display">MindOS</span>
|
|
77
77
|
</Link>
|
|
78
78
|
{/* Mobile close */}
|
|
79
79
|
<button onClick={() => setMobileOpen(false)} className="md:hidden p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import { apiFetch } from '@/lib/api';
|
|
7
|
+
|
|
8
|
+
interface UpdateInfo {
|
|
9
|
+
current: string;
|
|
10
|
+
latest: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function UpdateBanner() {
|
|
14
|
+
const { t } = useLocale();
|
|
15
|
+
const [info, setInfo] = useState<UpdateInfo | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
// Don't check for updates on setup or login pages
|
|
19
|
+
if (typeof window !== 'undefined') {
|
|
20
|
+
const path = window.location.pathname;
|
|
21
|
+
if (path === '/setup' || path === '/login') return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const timer = setTimeout(async () => {
|
|
25
|
+
try {
|
|
26
|
+
const data = await apiFetch<{ hasUpdate: boolean; latest: string; current: string }>('/api/update-check');
|
|
27
|
+
if (!data.hasUpdate) return;
|
|
28
|
+
|
|
29
|
+
const dismissed = localStorage.getItem('mindos_update_dismissed');
|
|
30
|
+
if (data.latest === dismissed) return;
|
|
31
|
+
|
|
32
|
+
setInfo({ latest: data.latest, current: data.current });
|
|
33
|
+
} catch {
|
|
34
|
+
// Network error / API failure — silent
|
|
35
|
+
}
|
|
36
|
+
}, 3000); // Check 3s after page load, don't block first paint
|
|
37
|
+
|
|
38
|
+
return () => clearTimeout(timer);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
if (!info) return null;
|
|
42
|
+
|
|
43
|
+
const handleDismiss = () => {
|
|
44
|
+
localStorage.setItem('mindos_update_dismissed', info.latest);
|
|
45
|
+
setInfo(null);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const updateT = t.updateBanner;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className="flex items-center justify-between gap-3 px-4 py-2 text-xs"
|
|
53
|
+
style={{ background: 'var(--amber-subtle, rgba(200,135,30,0.08))', borderBottom: '1px solid var(--border)' }}
|
|
54
|
+
>
|
|
55
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
56
|
+
<span className="font-medium" style={{ color: 'var(--amber)' }}>
|
|
57
|
+
{updateT?.newVersion
|
|
58
|
+
? updateT.newVersion(info.latest, info.current)
|
|
59
|
+
: `MindOS v${info.latest} available (current: v${info.current})`}
|
|
60
|
+
</span>
|
|
61
|
+
<span className="text-muted-foreground">
|
|
62
|
+
{updateT?.runUpdate ?? 'Run'}{' '}
|
|
63
|
+
<code className="px-1 py-0.5 rounded bg-muted font-mono text-[11px]">mindos update</code>
|
|
64
|
+
{updateT?.orSee ? (
|
|
65
|
+
<>
|
|
66
|
+
{' '}{updateT.orSee}{' '}
|
|
67
|
+
<a
|
|
68
|
+
href="https://github.com/GeminiLight/mindos/releases"
|
|
69
|
+
target="_blank"
|
|
70
|
+
rel="noopener noreferrer"
|
|
71
|
+
className="underline hover:text-foreground transition-colors"
|
|
72
|
+
>
|
|
73
|
+
{updateT.releaseNotes}
|
|
74
|
+
</a>
|
|
75
|
+
</>
|
|
76
|
+
) : (
|
|
77
|
+
<>
|
|
78
|
+
{' '}or{' '}
|
|
79
|
+
<a
|
|
80
|
+
href="https://github.com/GeminiLight/mindos/releases"
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener noreferrer"
|
|
83
|
+
className="underline hover:text-foreground transition-colors"
|
|
84
|
+
>
|
|
85
|
+
view release notes
|
|
86
|
+
</a>
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
<button
|
|
92
|
+
onClick={handleDismiss}
|
|
93
|
+
className="p-0.5 rounded hover:bg-muted transition-colors shrink-0"
|
|
94
|
+
style={{ color: 'var(--muted-foreground)' }}
|
|
95
|
+
title="Dismiss"
|
|
96
|
+
>
|
|
97
|
+
<X size={14} />
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|