@geminilight/mindos 0.5.28 → 0.5.29
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/update/route.ts +41 -0
- package/app/app/explore/page.tsx +12 -0
- package/app/components/ActivityBar.tsx +14 -7
- package/app/components/GuideCard.tsx +21 -7
- package/app/components/HomeContent.tsx +12 -1
- package/app/components/KeyboardShortcuts.tsx +102 -0
- package/app/components/Panel.tsx +12 -7
- package/app/components/SidebarLayout.tsx +18 -1
- package/app/components/UpdateBanner.tsx +19 -21
- package/app/components/explore/ExploreContent.tsx +100 -0
- package/app/components/explore/UseCaseCard.tsx +50 -0
- package/app/components/explore/use-cases.ts +30 -0
- package/app/components/panels/AgentsPanel.tsx +86 -95
- package/app/components/panels/PluginsPanel.tsx +9 -6
- package/app/components/settings/AiTab.tsx +5 -3
- package/app/components/settings/SettingsContent.tsx +5 -2
- package/app/components/settings/UpdateTab.tsx +195 -0
- package/app/components/settings/types.ts +1 -1
- package/app/components/walkthrough/WalkthroughOverlay.tsx +224 -0
- package/app/components/walkthrough/WalkthroughProvider.tsx +133 -0
- package/app/components/walkthrough/WalkthroughTooltip.tsx +129 -0
- package/app/components/walkthrough/index.ts +3 -0
- package/app/components/walkthrough/steps.ts +21 -0
- package/app/lib/i18n-en.ts +164 -5
- package/app/lib/i18n-zh.ts +163 -4
- package/app/lib/settings.ts +4 -0
- package/app/next-env.d.ts +1 -1
- package/app/package.json +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type UseCaseCategory = 'getting-started' | 'cross-agent' | 'knowledge-evolution' | 'advanced';
|
|
2
|
+
|
|
3
|
+
export interface UseCase {
|
|
4
|
+
id: string;
|
|
5
|
+
icon: string;
|
|
6
|
+
category: UseCaseCategory;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* C1-C9 use case definitions.
|
|
11
|
+
* All display text (title, description, prompt) comes from i18n — this file is structure only.
|
|
12
|
+
*/
|
|
13
|
+
export const useCases: UseCase[] = [
|
|
14
|
+
{ id: 'c1', icon: '👤', category: 'getting-started' },
|
|
15
|
+
{ id: 'c2', icon: '📥', category: 'getting-started' },
|
|
16
|
+
{ id: 'c3', icon: '🔄', category: 'cross-agent' },
|
|
17
|
+
{ id: 'c4', icon: '🔁', category: 'knowledge-evolution' },
|
|
18
|
+
{ id: 'c5', icon: '💡', category: 'cross-agent' },
|
|
19
|
+
{ id: 'c6', icon: '🚀', category: 'cross-agent' },
|
|
20
|
+
{ id: 'c7', icon: '🔍', category: 'knowledge-evolution' },
|
|
21
|
+
{ id: 'c8', icon: '🤝', category: 'knowledge-evolution' },
|
|
22
|
+
{ id: 'c9', icon: '🛡️', category: 'advanced' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const categories: UseCaseCategory[] = [
|
|
26
|
+
'getting-started',
|
|
27
|
+
'cross-agent',
|
|
28
|
+
'knowledge-evolution',
|
|
29
|
+
'advanced',
|
|
30
|
+
];
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
-
import { Loader2, RefreshCw, ChevronDown, ChevronRight,
|
|
4
|
+
import { Loader2, RefreshCw, ChevronDown, ChevronRight, CheckCircle2, AlertCircle } from 'lucide-react';
|
|
5
5
|
import { apiFetch } from '@/lib/api';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
7
|
import type { McpStatus, AgentInfo } from '../settings/types';
|
|
7
8
|
import PanelHeader from './PanelHeader';
|
|
8
9
|
|
|
@@ -10,11 +11,11 @@ interface AgentsPanelProps {
|
|
|
10
11
|
active: boolean;
|
|
11
12
|
maximized?: boolean;
|
|
12
13
|
onMaximize?: () => void;
|
|
13
|
-
/** Opens Settings Modal on a specific tab */
|
|
14
|
-
onOpenSettings?: (tab: 'mcp') => void;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
export default function AgentsPanel({ active, maximized, onMaximize
|
|
16
|
+
export default function AgentsPanel({ active, maximized, onMaximize }: AgentsPanelProps) {
|
|
17
|
+
const { t } = useLocale();
|
|
18
|
+
const p = t.panels.agents;
|
|
18
19
|
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
|
19
20
|
const [mcpStatus, setMcpStatus] = useState<McpStatus | null>(null);
|
|
20
21
|
const [loading, setLoading] = useState(true);
|
|
@@ -40,49 +41,34 @@ export default function AgentsPanel({ active, maximized, onMaximize, onOpenSetti
|
|
|
40
41
|
setRefreshing(false);
|
|
41
42
|
}, []);
|
|
42
43
|
|
|
43
|
-
// Fetch when panel becomes active + 30s auto-refresh
|
|
44
44
|
const prevActive = useRef(false);
|
|
45
45
|
useEffect(() => {
|
|
46
|
-
if (active && !prevActive.current)
|
|
47
|
-
fetchAll();
|
|
48
|
-
}
|
|
46
|
+
if (active && !prevActive.current) fetchAll();
|
|
49
47
|
prevActive.current = active;
|
|
50
48
|
}, [active, fetchAll]);
|
|
51
49
|
|
|
52
50
|
useEffect(() => {
|
|
53
|
-
if (!active) {
|
|
54
|
-
clearInterval(intervalRef.current);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
51
|
+
if (!active) { clearInterval(intervalRef.current); return; }
|
|
57
52
|
intervalRef.current = setInterval(() => fetchAll(true), 30_000);
|
|
58
53
|
return () => clearInterval(intervalRef.current);
|
|
59
54
|
}, [active, fetchAll]);
|
|
60
55
|
|
|
61
|
-
const handleRefresh = () => {
|
|
62
|
-
setRefreshing(true);
|
|
63
|
-
fetchAll();
|
|
64
|
-
};
|
|
56
|
+
const handleRefresh = () => { setRefreshing(true); fetchAll(); };
|
|
65
57
|
|
|
66
|
-
// Group agents
|
|
67
58
|
const connected = agents.filter(a => a.present && a.installed);
|
|
68
59
|
const detected = agents.filter(a => a.present && !a.installed);
|
|
69
60
|
const notFound = agents.filter(a => !a.present);
|
|
70
|
-
const connectedCount = connected.length;
|
|
71
61
|
|
|
72
62
|
return (
|
|
73
63
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
74
|
-
<PanelHeader title=
|
|
64
|
+
<PanelHeader title={p.title} maximized={maximized} onMaximize={onMaximize}>
|
|
75
65
|
<div className="flex items-center gap-1.5">
|
|
76
66
|
{!loading && (
|
|
77
|
-
<span className="text-2xs text-muted-foreground">{
|
|
67
|
+
<span className="text-2xs text-muted-foreground">{connected.length} {p.connected}</span>
|
|
78
68
|
)}
|
|
79
|
-
<button
|
|
80
|
-
onClick={handleRefresh}
|
|
81
|
-
disabled={refreshing}
|
|
69
|
+
<button onClick={handleRefresh} disabled={refreshing}
|
|
82
70
|
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground disabled:opacity-50 transition-colors"
|
|
83
|
-
aria-label=
|
|
84
|
-
title="Refresh agent status"
|
|
85
|
-
>
|
|
71
|
+
aria-label={p.refresh} title={p.refresh}>
|
|
86
72
|
<RefreshCw size={12} className={refreshing ? 'animate-spin' : ''} />
|
|
87
73
|
</button>
|
|
88
74
|
</div>
|
|
@@ -90,24 +76,19 @@ export default function AgentsPanel({ active, maximized, onMaximize, onOpenSetti
|
|
|
90
76
|
|
|
91
77
|
<div className="flex-1 overflow-y-auto min-h-0">
|
|
92
78
|
{loading ? (
|
|
93
|
-
<div className="flex justify-center py-8">
|
|
94
|
-
<Loader2 size={16} className="animate-spin text-muted-foreground" />
|
|
95
|
-
</div>
|
|
79
|
+
<div className="flex justify-center py-8"><Loader2 size={16} className="animate-spin text-muted-foreground" /></div>
|
|
96
80
|
) : error && agents.length === 0 ? (
|
|
97
81
|
<div className="flex flex-col items-center gap-2 py-8 text-center px-4">
|
|
98
|
-
<p className="text-xs text-destructive">
|
|
99
|
-
<button
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
>
|
|
103
|
-
<RefreshCw size={11} /> Retry
|
|
82
|
+
<p className="text-xs text-destructive">{p.failedToLoad}</p>
|
|
83
|
+
<button onClick={handleRefresh}
|
|
84
|
+
className="flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
85
|
+
<RefreshCw size={11} /> {p.retry}
|
|
104
86
|
</button>
|
|
105
87
|
</div>
|
|
106
88
|
) : (
|
|
107
89
|
<div className="px-3 py-3 space-y-4">
|
|
108
|
-
{/* MCP Server Status — compact */}
|
|
109
90
|
<div className="rounded-lg border border-border bg-card/50 px-3 py-2.5 flex items-center justify-between">
|
|
110
|
-
<span className="text-xs font-medium text-foreground">
|
|
91
|
+
<span className="text-xs font-medium text-foreground">{p.mcpServer}</span>
|
|
111
92
|
{mcpStatus?.running ? (
|
|
112
93
|
<span className="flex items-center gap-1.5 text-[11px]">
|
|
113
94
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 inline-block" />
|
|
@@ -116,115 +97,125 @@ export default function AgentsPanel({ active, maximized, onMaximize, onOpenSetti
|
|
|
116
97
|
) : (
|
|
117
98
|
<span className="flex items-center gap-1.5 text-[11px]">
|
|
118
99
|
<span className="w-1.5 h-1.5 rounded-full bg-zinc-400 inline-block" />
|
|
119
|
-
<span className="text-muted-foreground">
|
|
100
|
+
<span className="text-muted-foreground">{p.stopped}</span>
|
|
120
101
|
</span>
|
|
121
102
|
)}
|
|
122
103
|
</div>
|
|
123
104
|
|
|
124
|
-
{/* Connected */}
|
|
125
105
|
{connected.length > 0 && (
|
|
126
106
|
<section>
|
|
127
|
-
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">
|
|
128
|
-
Connected ({connected.length})
|
|
129
|
-
</h3>
|
|
107
|
+
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionConnected} ({connected.length})</h3>
|
|
130
108
|
<div className="space-y-1.5">
|
|
131
|
-
{connected.map(agent => (
|
|
132
|
-
<AgentCard key={agent.key} agent={agent} status="connected" />
|
|
133
|
-
))}
|
|
109
|
+
{connected.map(agent => (<AgentCard key={agent.key} agent={agent} status="connected" t={p} />))}
|
|
134
110
|
</div>
|
|
135
111
|
</section>
|
|
136
112
|
)}
|
|
137
113
|
|
|
138
|
-
{/* Detected but not configured */}
|
|
139
114
|
{detected.length > 0 && (
|
|
140
115
|
<section>
|
|
141
|
-
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">
|
|
142
|
-
Detected ({detected.length})
|
|
143
|
-
</h3>
|
|
116
|
+
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{p.sectionDetected} ({detected.length})</h3>
|
|
144
117
|
<div className="space-y-1.5">
|
|
145
118
|
{detected.map(agent => (
|
|
146
|
-
<AgentCard
|
|
147
|
-
key={agent.key}
|
|
148
|
-
agent={agent}
|
|
149
|
-
status="detected"
|
|
150
|
-
onConnect={() => onOpenSettings?.('mcp')}
|
|
151
|
-
/>
|
|
119
|
+
<AgentCard key={agent.key} agent={agent} status="detected" onInstalled={() => fetchAll()} t={p} />
|
|
152
120
|
))}
|
|
153
121
|
</div>
|
|
154
122
|
</section>
|
|
155
123
|
)}
|
|
156
124
|
|
|
157
|
-
{/* Not Detected — collapsible */}
|
|
158
125
|
{notFound.length > 0 && (
|
|
159
126
|
<section>
|
|
160
|
-
<button
|
|
161
|
-
|
|
162
|
-
className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors"
|
|
163
|
-
>
|
|
127
|
+
<button onClick={() => setShowNotDetected(!showNotDetected)}
|
|
128
|
+
className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors">
|
|
164
129
|
{showNotDetected ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
165
|
-
|
|
130
|
+
{p.sectionNotDetected} ({notFound.length})
|
|
166
131
|
</button>
|
|
167
132
|
{showNotDetected && (
|
|
168
133
|
<div className="space-y-1.5">
|
|
169
|
-
{notFound.map(agent => (
|
|
170
|
-
<AgentCard key={agent.key} agent={agent} status="notFound" />
|
|
171
|
-
))}
|
|
134
|
+
{notFound.map(agent => (<AgentCard key={agent.key} agent={agent} status="notFound" t={p} />))}
|
|
172
135
|
</div>
|
|
173
136
|
)}
|
|
174
137
|
</section>
|
|
175
138
|
)}
|
|
176
139
|
|
|
177
|
-
{/* Empty state */}
|
|
178
140
|
{agents.length === 0 && (
|
|
179
|
-
<p className="text-xs text-muted-foreground text-center py-4">
|
|
180
|
-
No agents detected.
|
|
181
|
-
</p>
|
|
141
|
+
<p className="text-xs text-muted-foreground text-center py-4">{p.noAgents}</p>
|
|
182
142
|
)}
|
|
183
143
|
</div>
|
|
184
144
|
)}
|
|
185
145
|
</div>
|
|
186
146
|
|
|
187
|
-
{/* Footer */}
|
|
188
147
|
<div className="px-3 py-2 border-t border-border shrink-0">
|
|
189
|
-
<p className="text-2xs text-muted-foreground/60">
|
|
148
|
+
<p className="text-2xs text-muted-foreground/60">{p.autoRefresh}</p>
|
|
190
149
|
</div>
|
|
191
150
|
</div>
|
|
192
151
|
);
|
|
193
152
|
}
|
|
194
153
|
|
|
195
|
-
/* ── Agent Card
|
|
154
|
+
/* ── Agent Card ── */
|
|
196
155
|
|
|
197
|
-
function AgentCard({
|
|
198
|
-
agent,
|
|
199
|
-
status,
|
|
200
|
-
onConnect,
|
|
201
|
-
}: {
|
|
156
|
+
function AgentCard({ agent, status, onInstalled, t }: {
|
|
202
157
|
agent: AgentInfo;
|
|
203
158
|
status: 'connected' | 'detected' | 'notFound';
|
|
204
|
-
|
|
159
|
+
onInstalled?: () => void;
|
|
160
|
+
t: Record<string, any>;
|
|
205
161
|
}) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
162
|
+
const [expanded, setExpanded] = useState(false);
|
|
163
|
+
const [installing, setInstalling] = useState(false);
|
|
164
|
+
const [result, setResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
165
|
+
|
|
166
|
+
const dot = status === 'connected' ? 'bg-emerald-500' : status === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
|
|
167
|
+
|
|
168
|
+
const handleInstall = async () => {
|
|
169
|
+
setInstalling(true); setResult(null);
|
|
170
|
+
try {
|
|
171
|
+
const res = await apiFetch<{ results: Array<{ key: string; ok: boolean; error?: string }> }>('/api/mcp/install', {
|
|
172
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify({ agents: [{ key: agent.key, scope: agent.hasProjectScope ? 'project' : 'global', transport: agent.preferredTransport }], transport: 'auto' }),
|
|
174
|
+
});
|
|
175
|
+
const r = res.results?.[0];
|
|
176
|
+
if (r?.ok) { setResult({ type: 'success', text: `${agent.name} ${t.connected}` }); setTimeout(() => onInstalled?.(), 1500); }
|
|
177
|
+
else { setResult({ type: 'error', text: r?.error ?? 'Install failed' }); }
|
|
178
|
+
} catch { setResult({ type: 'error', text: 'Network error' }); }
|
|
179
|
+
setInstalling(false);
|
|
180
|
+
};
|
|
210
181
|
|
|
211
182
|
return (
|
|
212
|
-
<div className="rounded-lg border border-border/60 bg-card/30
|
|
213
|
-
<div className="flex items-center gap-2
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
183
|
+
<div className="rounded-lg border border-border/60 bg-card/30 overflow-hidden">
|
|
184
|
+
<div className="px-3 py-2 flex items-center justify-between gap-2">
|
|
185
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
186
|
+
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
|
|
187
|
+
<span className="text-xs font-medium text-foreground truncate">{agent.name}</span>
|
|
188
|
+
{status === 'connected' && agent.transport && (
|
|
189
|
+
<span className="text-2xs px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{agent.transport}</span>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
{status === 'detected' && (
|
|
193
|
+
<button onClick={() => setExpanded(v => !v)}
|
|
194
|
+
className="flex items-center gap-1 px-2 py-0.5 text-2xs rounded-md bg-amber-500/10 text-amber-600 dark:text-amber-400 hover:bg-amber-500/20 transition-colors shrink-0">
|
|
195
|
+
{t.connect}
|
|
196
|
+
{expanded ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
|
|
197
|
+
</button>
|
|
218
198
|
)}
|
|
219
199
|
</div>
|
|
220
|
-
{status === 'detected' &&
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
<
|
|
227
|
-
|
|
200
|
+
{status === 'detected' && expanded && (
|
|
201
|
+
<div className="px-3 pb-2.5 pt-1 border-t border-border/40 space-y-2">
|
|
202
|
+
<div className="flex items-center justify-between text-2xs text-muted-foreground">
|
|
203
|
+
<span>Transport: <span className="font-medium text-foreground">{agent.preferredTransport}</span></span>
|
|
204
|
+
<span>Scope: <span className="font-medium text-foreground">{agent.hasProjectScope ? 'project' : 'global'}</span></span>
|
|
205
|
+
</div>
|
|
206
|
+
<button onClick={handleInstall} disabled={installing}
|
|
207
|
+
className="w-full flex items-center justify-center gap-1.5 px-2.5 py-1.5 text-2xs rounded-md font-medium text-white disabled:opacity-50 transition-colors"
|
|
208
|
+
style={{ background: 'var(--amber)' }}>
|
|
209
|
+
{installing ? <Loader2 size={11} className="animate-spin" /> : null}
|
|
210
|
+
{installing ? t.installing : t.install(agent.name)}
|
|
211
|
+
</button>
|
|
212
|
+
{result && (
|
|
213
|
+
<div className={`flex items-center gap-1.5 text-2xs ${result.type === 'success' ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive'}`}>
|
|
214
|
+
{result.type === 'success' ? <CheckCircle2 size={11} /> : <AlertCircle size={11} />}
|
|
215
|
+
{result.text}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
228
219
|
)}
|
|
229
220
|
</div>
|
|
230
221
|
);
|
|
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
|
|
5
5
|
import { getAllRenderers, isRendererEnabled, setRendererEnabled, loadDisabledState } from '@/lib/renderers/registry';
|
|
6
6
|
import { Toggle } from '../settings/Primitives';
|
|
7
7
|
import PanelHeader from './PanelHeader';
|
|
8
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
8
9
|
|
|
9
10
|
interface PluginsPanelProps {
|
|
10
11
|
active: boolean;
|
|
@@ -16,6 +17,8 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
|
|
|
16
17
|
const [mounted, setMounted] = useState(false);
|
|
17
18
|
const [, forceUpdate] = useState(0);
|
|
18
19
|
const router = useRouter();
|
|
20
|
+
const { t } = useLocale();
|
|
21
|
+
const p = t.panels.plugins;
|
|
19
22
|
|
|
20
23
|
// Defer renderer reads to client only — avoids hydration mismatch
|
|
21
24
|
useEffect(() => {
|
|
@@ -35,14 +38,14 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
|
|
|
35
38
|
return (
|
|
36
39
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
37
40
|
{/* Header */}
|
|
38
|
-
<PanelHeader title=
|
|
39
|
-
<span className="text-2xs text-muted-foreground">{enabledCount}/{renderers.length} active</span>
|
|
41
|
+
<PanelHeader title={p.title} maximized={maximized} onMaximize={onMaximize}>
|
|
42
|
+
<span className="text-2xs text-muted-foreground">{enabledCount}/{renderers.length} {p.active}</span>
|
|
40
43
|
</PanelHeader>
|
|
41
44
|
|
|
42
45
|
{/* Plugin list */}
|
|
43
46
|
<div className="flex-1 overflow-y-auto min-h-0">
|
|
44
47
|
{mounted && renderers.length === 0 && (
|
|
45
|
-
<p className="px-4 py-8 text-sm text-muted-foreground text-center">
|
|
48
|
+
<p className="px-4 py-8 text-sm text-muted-foreground text-center">{p.noPlugins}</p>
|
|
46
49
|
)}
|
|
47
50
|
{renderers.map(r => {
|
|
48
51
|
const enabled = isRendererEnabled(r.id);
|
|
@@ -57,7 +60,7 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
|
|
|
57
60
|
<span className="text-base shrink-0">{r.icon}</span>
|
|
58
61
|
<span className="text-sm font-medium text-foreground truncate">{r.name}</span>
|
|
59
62
|
{r.core && (
|
|
60
|
-
<span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground shrink-0">
|
|
63
|
+
<span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{p.core}</span>
|
|
61
64
|
)}
|
|
62
65
|
</div>
|
|
63
66
|
<Toggle
|
|
@@ -65,7 +68,7 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
|
|
|
65
68
|
onChange={(v) => handleToggle(r.id, v)}
|
|
66
69
|
size="sm"
|
|
67
70
|
disabled={r.core}
|
|
68
|
-
title={r.core ?
|
|
71
|
+
title={r.core ? p.coreDisabled : undefined}
|
|
69
72
|
/>
|
|
70
73
|
</div>
|
|
71
74
|
|
|
@@ -98,7 +101,7 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
|
|
|
98
101
|
{/* Footer info */}
|
|
99
102
|
<div className="px-4 py-2 border-t border-border shrink-0">
|
|
100
103
|
<p className="text-2xs text-muted-foreground">
|
|
101
|
-
|
|
104
|
+
{p.footer}
|
|
102
105
|
</p>
|
|
103
106
|
</div>
|
|
104
107
|
</div>
|
|
@@ -4,6 +4,7 @@ import { useState, useRef, useCallback, useEffect } from 'react';
|
|
|
4
4
|
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
5
5
|
import type { AiSettings, AgentSettings, ProviderConfig, SettingsData, AiTabProps } from './types';
|
|
6
6
|
import { Field, Select, Input, EnvBadge, ApiKeyInput, Toggle, SectionLabel } from './Primitives';
|
|
7
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
8
|
|
|
8
9
|
type TestState = 'idle' | 'testing' | 'ok' | 'error';
|
|
9
10
|
type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
|
|
@@ -287,6 +288,7 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
287
288
|
/* ── Ask AI Display Mode (localStorage-based, no server roundtrip) ── */
|
|
288
289
|
|
|
289
290
|
function AskDisplayMode() {
|
|
291
|
+
const { t } = useLocale();
|
|
290
292
|
const [mode, setMode] = useState<'panel' | 'popup'>('panel');
|
|
291
293
|
|
|
292
294
|
useEffect(() => {
|
|
@@ -308,10 +310,10 @@ function AskDisplayMode() {
|
|
|
308
310
|
<div className="pt-3 border-t border-border">
|
|
309
311
|
<SectionLabel>MindOS Agent</SectionLabel>
|
|
310
312
|
<div className="space-y-4">
|
|
311
|
-
<Field label=
|
|
313
|
+
<Field label={t.settings.askDisplayMode?.label ?? 'Display Mode'} hint={t.settings.askDisplayMode?.hint ?? 'Side panel stays docked on the right. Popup opens a floating dialog.'}>
|
|
312
314
|
<Select value={mode} onChange={e => handleChange(e.target.value)}>
|
|
313
|
-
<option value="panel">Side Panel</option>
|
|
314
|
-
<option value="popup">Popup</option>
|
|
315
|
+
<option value="panel">{t.settings.askDisplayMode?.panel ?? 'Side Panel'}</option>
|
|
316
|
+
<option value="popup">{t.settings.askDisplayMode?.popup ?? 'Popup'}</option>
|
|
315
317
|
</Select>
|
|
316
318
|
</Field>
|
|
317
319
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
4
|
-
import { Settings, Loader2, AlertCircle, CheckCircle2, RotateCcw, Sparkles, Palette, Database, RefreshCw, Plug, X } from 'lucide-react';
|
|
4
|
+
import { Settings, Loader2, AlertCircle, CheckCircle2, RotateCcw, Sparkles, Palette, Database, RefreshCw, Plug, Download, X } from 'lucide-react';
|
|
5
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
6
|
import { apiFetch } from '@/lib/api';
|
|
7
7
|
import type { AiSettings, AgentSettings, SettingsData, Tab } from './types';
|
|
@@ -10,6 +10,7 @@ import { AppearanceTab } from './AppearanceTab';
|
|
|
10
10
|
import { KnowledgeTab } from './KnowledgeTab';
|
|
11
11
|
import { SyncTab } from './SyncTab';
|
|
12
12
|
import { McpTab } from './McpTab';
|
|
13
|
+
import { UpdateTab } from './UpdateTab';
|
|
13
14
|
|
|
14
15
|
interface SettingsContentProps {
|
|
15
16
|
visible: boolean;
|
|
@@ -136,6 +137,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
136
137
|
{ id: 'knowledge', label: t.settings.tabs.knowledge, icon: <Settings size={iconSize} /> },
|
|
137
138
|
{ id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
|
|
138
139
|
{ id: 'appearance', label: t.settings.tabs.appearance, icon: <Palette size={iconSize} /> },
|
|
140
|
+
{ id: 'update', label: t.settings.tabs.update ?? 'Update', icon: <Download size={iconSize} /> },
|
|
139
141
|
];
|
|
140
142
|
|
|
141
143
|
const activeTabLabel = TABS.find(t2 => t2.id === tab)?.label ?? '';
|
|
@@ -149,7 +151,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
149
151
|
<p className={`${isPanel ? 'text-xs' : 'text-sm'} text-destructive font-medium`}>Failed to load settings</p>
|
|
150
152
|
{!isPanel && <p className="text-xs text-muted-foreground">Check that the server is running and AUTH_TOKEN is configured correctly.</p>}
|
|
151
153
|
</div>
|
|
152
|
-
) : !data && tab !== 'appearance' && tab !== 'mcp' && tab !== 'sync' ? (
|
|
154
|
+
) : !data && tab !== 'appearance' && tab !== 'mcp' && tab !== 'sync' && tab !== 'update' ? (
|
|
153
155
|
<div className="flex justify-center py-8">
|
|
154
156
|
<Loader2 size={isPanel ? 16 : 18} className="animate-spin text-muted-foreground" />
|
|
155
157
|
</div>
|
|
@@ -160,6 +162,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
160
162
|
{tab === 'knowledge' && data && <KnowledgeTab data={data} setData={setData} t={t} />}
|
|
161
163
|
{tab === 'sync' && <SyncTab t={t} />}
|
|
162
164
|
{tab === 'mcp' && <McpTab t={t} />}
|
|
165
|
+
{tab === 'update' && <UpdateTab />}
|
|
163
166
|
</>
|
|
164
167
|
)}
|
|
165
168
|
</div>
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
+
import { Download, RefreshCw, CheckCircle2, AlertCircle, Loader2, ExternalLink } from 'lucide-react';
|
|
5
|
+
import { apiFetch } from '@/lib/api';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
+
|
|
8
|
+
interface UpdateInfo {
|
|
9
|
+
current: string;
|
|
10
|
+
latest: string;
|
|
11
|
+
hasUpdate: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type UpdateState = 'idle' | 'checking' | 'updating' | 'updated' | 'error' | 'timeout';
|
|
15
|
+
|
|
16
|
+
const CHANGELOG_URL = 'https://github.com/GeminiLight/MindOS/releases';
|
|
17
|
+
const POLL_INTERVAL = 5_000;
|
|
18
|
+
const POLL_TIMEOUT = 4 * 60 * 1000; // 4 minutes
|
|
19
|
+
|
|
20
|
+
export function UpdateTab() {
|
|
21
|
+
const { t } = useLocale();
|
|
22
|
+
const u = t.settings.update;
|
|
23
|
+
const [info, setInfo] = useState<UpdateInfo | null>(null);
|
|
24
|
+
const [state, setState] = useState<UpdateState>('idle');
|
|
25
|
+
const [errorMsg, setErrorMsg] = useState('');
|
|
26
|
+
const pollRef = useRef<ReturnType<typeof setInterval>>(undefined);
|
|
27
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
28
|
+
const originalVersion = useRef<string>('');
|
|
29
|
+
|
|
30
|
+
const checkUpdate = useCallback(async () => {
|
|
31
|
+
setState('checking');
|
|
32
|
+
setErrorMsg('');
|
|
33
|
+
try {
|
|
34
|
+
const data = await apiFetch<UpdateInfo>('/api/update-check');
|
|
35
|
+
setInfo(data);
|
|
36
|
+
if (!originalVersion.current) originalVersion.current = data.current;
|
|
37
|
+
setState('idle');
|
|
38
|
+
} catch {
|
|
39
|
+
setState('error');
|
|
40
|
+
setErrorMsg(u?.error ?? 'Failed to check for updates.');
|
|
41
|
+
}
|
|
42
|
+
}, [u]);
|
|
43
|
+
|
|
44
|
+
useEffect(() => { checkUpdate(); }, [checkUpdate]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
return () => {
|
|
48
|
+
clearInterval(pollRef.current);
|
|
49
|
+
clearTimeout(timeoutRef.current);
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const handleUpdate = useCallback(async () => {
|
|
54
|
+
setState('updating');
|
|
55
|
+
setErrorMsg('');
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await apiFetch('/api/update', { method: 'POST' });
|
|
59
|
+
} catch {
|
|
60
|
+
// Expected — server may die during update
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pollRef.current = setInterval(async () => {
|
|
64
|
+
try {
|
|
65
|
+
const data = await apiFetch<UpdateInfo>('/api/update-check');
|
|
66
|
+
if (data.current !== originalVersion.current) {
|
|
67
|
+
clearInterval(pollRef.current);
|
|
68
|
+
clearTimeout(timeoutRef.current);
|
|
69
|
+
setInfo(data);
|
|
70
|
+
setState('updated');
|
|
71
|
+
setTimeout(() => window.location.reload(), 2000);
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Server still restarting
|
|
75
|
+
}
|
|
76
|
+
}, POLL_INTERVAL);
|
|
77
|
+
|
|
78
|
+
timeoutRef.current = setTimeout(() => {
|
|
79
|
+
clearInterval(pollRef.current);
|
|
80
|
+
setState('timeout');
|
|
81
|
+
}, POLL_TIMEOUT);
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="space-y-5">
|
|
86
|
+
{/* Version Card */}
|
|
87
|
+
<div className="rounded-xl border border-border bg-card p-4 space-y-3">
|
|
88
|
+
<div className="flex items-center justify-between">
|
|
89
|
+
<span className="text-sm font-medium text-foreground">MindOS</span>
|
|
90
|
+
{info && (
|
|
91
|
+
<span className="text-xs font-mono text-muted-foreground">v{info.current}</span>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{state === 'checking' && (
|
|
96
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
97
|
+
<Loader2 size={13} className="animate-spin" />
|
|
98
|
+
{u?.checking ?? 'Checking for updates...'}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{state === 'idle' && info && !info.hasUpdate && (
|
|
103
|
+
<div className="flex items-center gap-2 text-xs text-emerald-600 dark:text-emerald-400">
|
|
104
|
+
<CheckCircle2 size={13} />
|
|
105
|
+
{u?.upToDate ?? "You're up to date"}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{state === 'idle' && info?.hasUpdate && (
|
|
110
|
+
<div className="flex items-center gap-2 text-xs" style={{ color: 'var(--amber)' }}>
|
|
111
|
+
<Download size={13} />
|
|
112
|
+
{u?.available ? u.available(info.current, info.latest) : `Update available: v${info.current} → v${info.latest}`}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{state === 'updating' && (
|
|
117
|
+
<div className="space-y-2">
|
|
118
|
+
<div className="flex items-center gap-2 text-xs" style={{ color: 'var(--amber)' }}>
|
|
119
|
+
<Loader2 size={13} className="animate-spin" />
|
|
120
|
+
{u?.updating ?? 'Updating MindOS... The server will restart shortly.'}
|
|
121
|
+
</div>
|
|
122
|
+
<p className="text-2xs text-muted-foreground">
|
|
123
|
+
{u?.updatingHint ?? 'This may take 1–3 minutes. Do not close this page.'}
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{state === 'updated' && (
|
|
129
|
+
<div className="flex items-center gap-2 text-xs text-emerald-600 dark:text-emerald-400">
|
|
130
|
+
<CheckCircle2 size={13} />
|
|
131
|
+
{u?.updated ?? 'Updated successfully! Reloading...'}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{state === 'timeout' && (
|
|
136
|
+
<div className="space-y-1">
|
|
137
|
+
<div className="flex items-center gap-2 text-xs text-amber-600 dark:text-amber-400">
|
|
138
|
+
<AlertCircle size={13} />
|
|
139
|
+
{u?.timeout ?? 'Update may still be in progress.'}
|
|
140
|
+
</div>
|
|
141
|
+
<p className="text-2xs text-muted-foreground">
|
|
142
|
+
{u?.timeoutHint ?? 'Check your terminal:'} <code className="font-mono bg-muted px-1 py-0.5 rounded">mindos logs</code>
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{state === 'error' && (
|
|
148
|
+
<div className="flex items-center gap-2 text-xs text-destructive">
|
|
149
|
+
<AlertCircle size={13} />
|
|
150
|
+
{errorMsg}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Actions */}
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<button
|
|
158
|
+
onClick={checkUpdate}
|
|
159
|
+
disabled={state === 'checking' || state === 'updating'}
|
|
160
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
161
|
+
>
|
|
162
|
+
<RefreshCw size={12} className={state === 'checking' ? 'animate-spin' : ''} />
|
|
163
|
+
{u?.checkButton ?? 'Check for Updates'}
|
|
164
|
+
</button>
|
|
165
|
+
|
|
166
|
+
{info?.hasUpdate && state !== 'updating' && state !== 'updated' && (
|
|
167
|
+
<button
|
|
168
|
+
onClick={handleUpdate}
|
|
169
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-medium text-white transition-colors"
|
|
170
|
+
style={{ background: 'var(--amber)' }}
|
|
171
|
+
>
|
|
172
|
+
<Download size={12} />
|
|
173
|
+
{u?.updateButton ? u.updateButton(info.latest) : `Update to v${info.latest}`}
|
|
174
|
+
</button>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Info */}
|
|
179
|
+
<div className="border-t border-border pt-4 space-y-2">
|
|
180
|
+
<a
|
|
181
|
+
href={CHANGELOG_URL}
|
|
182
|
+
target="_blank"
|
|
183
|
+
rel="noopener noreferrer"
|
|
184
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
185
|
+
>
|
|
186
|
+
<ExternalLink size={12} />
|
|
187
|
+
{u?.releaseNotes ?? 'View release notes'}
|
|
188
|
+
</a>
|
|
189
|
+
<p className="text-2xs text-muted-foreground/60">
|
|
190
|
+
{u?.hint ?? 'Updates are installed via npm. Equivalent to running'} <code className="font-mono bg-muted px-1 py-0.5 rounded">mindos update</code> {u?.inTerminal ?? 'in your terminal.'}
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|