@geminilight/mindos 0.5.22 → 0.5.23
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/ask/route.ts +7 -14
- package/app/app/api/bootstrap/route.ts +1 -0
- package/app/app/globals.css +14 -0
- package/app/app/setup/page.tsx +3 -2
- package/app/components/ActivityBar.tsx +183 -0
- package/app/components/AskFab.tsx +39 -97
- package/app/components/AskModal.tsx +13 -371
- package/app/components/Breadcrumb.tsx +4 -4
- package/app/components/FileTree.tsx +21 -4
- package/app/components/Logo.tsx +39 -0
- package/app/components/Panel.tsx +152 -0
- package/app/components/RightAskPanel.tsx +72 -0
- package/app/components/SettingsModal.tsx +9 -241
- package/app/components/SidebarLayout.tsx +426 -12
- package/app/components/SyncStatusBar.tsx +74 -53
- package/app/components/TableOfContents.tsx +4 -2
- package/app/components/ask/AskContent.tsx +418 -0
- package/app/components/ask/MessageList.tsx +2 -2
- package/app/components/panels/AgentsPanel.tsx +231 -0
- package/app/components/panels/PanelHeader.tsx +35 -0
- package/app/components/panels/PluginsPanel.tsx +106 -0
- package/app/components/panels/SearchPanel.tsx +178 -0
- package/app/components/panels/SyncPopover.tsx +105 -0
- package/app/components/renderers/csv/TableView.tsx +4 -4
- package/app/components/settings/AiTab.tsx +39 -1
- package/app/components/settings/KnowledgeTab.tsx +116 -2
- package/app/components/settings/McpTab.tsx +6 -6
- package/app/components/settings/SettingsContent.tsx +343 -0
- package/app/components/settings/types.ts +1 -1
- package/app/components/setup/index.tsx +2 -23
- package/app/hooks/useResizeDrag.ts +78 -0
- package/app/lib/agent/index.ts +0 -1
- package/app/lib/agent/model.ts +33 -10
- package/app/lib/format.ts +19 -0
- package/app/lib/i18n-en.ts +6 -6
- package/app/lib/i18n-zh.ts +5 -5
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +1 -1
- package/app/package.json +2 -2
- package/bin/cli.js +27 -97
- package/package.json +4 -2
- package/scripts/setup.js +2 -12
- package/skills/mindos/SKILL.md +226 -8
- package/skills/mindos-zh/SKILL.md +226 -8
- package/app/lib/agent/skill-rules.ts +0 -70
- package/app/package-lock.json +0 -15736
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
4
|
+
import { Settings, Loader2, AlertCircle, CheckCircle2, RotateCcw, Sparkles, Palette, Database, RefreshCw, Plug, X } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import { apiFetch } from '@/lib/api';
|
|
7
|
+
import type { AiSettings, AgentSettings, SettingsData, Tab } from './types';
|
|
8
|
+
import { AiTab } from './AiTab';
|
|
9
|
+
import { AppearanceTab } from './AppearanceTab';
|
|
10
|
+
import { KnowledgeTab } from './KnowledgeTab';
|
|
11
|
+
import { SyncTab } from './SyncTab';
|
|
12
|
+
import { McpTab } from './McpTab';
|
|
13
|
+
|
|
14
|
+
interface SettingsContentProps {
|
|
15
|
+
visible: boolean;
|
|
16
|
+
initialTab?: Tab;
|
|
17
|
+
variant: 'modal' | 'panel';
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function SettingsContent({ visible, initialTab, variant, onClose }: SettingsContentProps) {
|
|
22
|
+
const [tab, setTab] = useState<Tab>('ai');
|
|
23
|
+
const [data, setData] = useState<SettingsData | null>(null);
|
|
24
|
+
const [saving, setSaving] = useState(false);
|
|
25
|
+
const [status, setStatus] = useState<'idle' | 'saved' | 'error' | 'load-error'>('idle');
|
|
26
|
+
const { t, locale, setLocale } = useLocale();
|
|
27
|
+
const saveTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
28
|
+
const dataLoaded = useRef(false);
|
|
29
|
+
|
|
30
|
+
const [font, setFont] = useState('lora');
|
|
31
|
+
const [contentWidth, setContentWidth] = useState('780px');
|
|
32
|
+
const [dark, setDark] = useState(true);
|
|
33
|
+
|
|
34
|
+
const isPanel = variant === 'panel';
|
|
35
|
+
|
|
36
|
+
// Init data when becoming visible
|
|
37
|
+
const prevVisibleRef = useRef(false);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const justOpened = isPanel
|
|
40
|
+
? (visible && !prevVisibleRef.current)
|
|
41
|
+
: visible;
|
|
42
|
+
|
|
43
|
+
if (justOpened) {
|
|
44
|
+
apiFetch<SettingsData>('/api/settings').then(d => { setData(d); dataLoaded.current = true; }).catch(() => setStatus('load-error'));
|
|
45
|
+
setFont(localStorage.getItem('prose-font') ?? 'lora');
|
|
46
|
+
setContentWidth(localStorage.getItem('content-width') ?? '780px');
|
|
47
|
+
const stored = localStorage.getItem('theme');
|
|
48
|
+
setDark(stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
49
|
+
setStatus('idle');
|
|
50
|
+
}
|
|
51
|
+
if (!visible) { dataLoaded.current = false; }
|
|
52
|
+
prevVisibleRef.current = visible;
|
|
53
|
+
}, [visible, isPanel]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (visible && initialTab) setTab(initialTab);
|
|
57
|
+
}, [visible, initialTab]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const fontMap: Record<string, string> = {
|
|
61
|
+
'lora': "'Lora', Georgia, serif",
|
|
62
|
+
'ibm-plex-sans': "'IBM Plex Sans', sans-serif",
|
|
63
|
+
'geist': 'var(--font-geist-sans), sans-serif',
|
|
64
|
+
'ibm-plex-mono': "'IBM Plex Mono', monospace",
|
|
65
|
+
};
|
|
66
|
+
document.documentElement.style.setProperty('--prose-font-override', fontMap[font] ?? '');
|
|
67
|
+
localStorage.setItem('prose-font', font);
|
|
68
|
+
}, [font]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
document.documentElement.style.setProperty('--content-width-override', contentWidth);
|
|
72
|
+
localStorage.setItem('content-width', contentWidth);
|
|
73
|
+
}, [contentWidth]);
|
|
74
|
+
|
|
75
|
+
// Esc to close — modal only
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (variant !== 'modal' || !visible || !onClose) return;
|
|
78
|
+
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
|
79
|
+
window.addEventListener('keydown', handler);
|
|
80
|
+
return () => window.removeEventListener('keydown', handler);
|
|
81
|
+
}, [variant, visible, onClose]);
|
|
82
|
+
|
|
83
|
+
const doSave = useCallback(async (d: SettingsData) => {
|
|
84
|
+
setSaving(true);
|
|
85
|
+
try {
|
|
86
|
+
await apiFetch('/api/settings', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ ai: d.ai, agent: d.agent, mindRoot: d.mindRoot, webPassword: d.webPassword, authToken: d.authToken }),
|
|
90
|
+
});
|
|
91
|
+
setStatus('saved');
|
|
92
|
+
setTimeout(() => setStatus('idle'), 2500);
|
|
93
|
+
} catch {
|
|
94
|
+
setStatus('error');
|
|
95
|
+
setTimeout(() => setStatus('idle'), 2500);
|
|
96
|
+
} finally {
|
|
97
|
+
setSaving(false);
|
|
98
|
+
}
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!data || !dataLoaded.current) return;
|
|
103
|
+
clearTimeout(saveTimer.current);
|
|
104
|
+
saveTimer.current = setTimeout(() => doSave(data), 800);
|
|
105
|
+
return () => clearTimeout(saveTimer.current);
|
|
106
|
+
}, [data, doSave]);
|
|
107
|
+
|
|
108
|
+
const updateAi = useCallback((patch: Partial<AiSettings>) => {
|
|
109
|
+
setData(d => d ? { ...d, ai: { ...d.ai, ...patch } } : d);
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const updateAgent = useCallback((patch: Partial<AgentSettings>) => {
|
|
113
|
+
setData(d => d ? { ...d, agent: { ...(d.agent ?? {}), ...patch } } : d);
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const restoreFromEnv = useCallback(async () => {
|
|
117
|
+
if (!data) return;
|
|
118
|
+
const defaults: AiSettings = {
|
|
119
|
+
provider: 'anthropic',
|
|
120
|
+
providers: { anthropic: { apiKey: '', model: '' }, openai: { apiKey: '', model: '', baseUrl: '' } },
|
|
121
|
+
};
|
|
122
|
+
setData(d => d ? { ...d, ai: defaults } : d);
|
|
123
|
+
const DEBOUNCE_DELAY = 800;
|
|
124
|
+
const SAVE_OPERATION_TIME = 500;
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
apiFetch<SettingsData>('/api/settings').then(d => { setData(d); }).catch(() => setStatus('error'));
|
|
127
|
+
}, DEBOUNCE_DELAY + SAVE_OPERATION_TIME);
|
|
128
|
+
}, [data]);
|
|
129
|
+
|
|
130
|
+
const env = data?.envOverrides ?? {};
|
|
131
|
+
const iconSize = isPanel ? 12 : 13;
|
|
132
|
+
|
|
133
|
+
const TABS: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
|
134
|
+
{ id: 'ai', label: t.settings.tabs.ai, icon: <Sparkles size={iconSize} /> },
|
|
135
|
+
{ id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP & Skills', icon: <Plug size={iconSize} /> },
|
|
136
|
+
{ id: 'knowledge', label: t.settings.tabs.knowledge, icon: <Settings size={iconSize} /> },
|
|
137
|
+
{ id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
|
|
138
|
+
{ id: 'appearance', label: t.settings.tabs.appearance, icon: <Palette size={iconSize} /> },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const activeTabLabel = TABS.find(t2 => t2.id === tab)?.label ?? '';
|
|
142
|
+
|
|
143
|
+
/* ── Shared content & footer ── */
|
|
144
|
+
const renderContent = () => (
|
|
145
|
+
<div className={`flex-1 overflow-y-auto min-h-0 ${isPanel ? 'px-4 py-4 space-y-4' : 'px-5 py-5 space-y-5'}`}>
|
|
146
|
+
{status === 'load-error' && (tab === 'ai' || tab === 'knowledge') ? (
|
|
147
|
+
<div className="flex flex-col items-center gap-2 py-8 text-center">
|
|
148
|
+
<AlertCircle size={isPanel ? 18 : 20} className="text-destructive" />
|
|
149
|
+
<p className={`${isPanel ? 'text-xs' : 'text-sm'} text-destructive font-medium`}>Failed to load settings</p>
|
|
150
|
+
{!isPanel && <p className="text-xs text-muted-foreground">Check that the server is running and AUTH_TOKEN is configured correctly.</p>}
|
|
151
|
+
</div>
|
|
152
|
+
) : !data && tab !== 'appearance' && tab !== 'mcp' && tab !== 'sync' ? (
|
|
153
|
+
<div className="flex justify-center py-8">
|
|
154
|
+
<Loader2 size={isPanel ? 16 : 18} className="animate-spin text-muted-foreground" />
|
|
155
|
+
</div>
|
|
156
|
+
) : (
|
|
157
|
+
<>
|
|
158
|
+
{tab === 'ai' && data?.ai && <AiTab data={data} updateAi={updateAi} updateAgent={updateAgent} t={t} />}
|
|
159
|
+
{tab === 'appearance' && <AppearanceTab font={font} setFont={setFont} contentWidth={contentWidth} setContentWidth={setContentWidth} dark={dark} setDark={setDark} locale={locale} setLocale={setLocale} t={t} />}
|
|
160
|
+
{tab === 'knowledge' && data && <KnowledgeTab data={data} setData={setData} t={t} />}
|
|
161
|
+
{tab === 'sync' && <SyncTab t={t} />}
|
|
162
|
+
{tab === 'mcp' && <McpTab t={t} />}
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const renderFooter = () => (
|
|
169
|
+
(tab === 'ai' || tab === 'knowledge') ? (
|
|
170
|
+
<div className={`${isPanel ? 'px-4 py-2' : 'px-5 py-2.5'} border-t border-border shrink-0 flex items-center justify-between`}>
|
|
171
|
+
<div className="flex items-center gap-3">
|
|
172
|
+
{tab === 'ai' && Object.values(env).some(Boolean) && (
|
|
173
|
+
<button
|
|
174
|
+
onClick={restoreFromEnv}
|
|
175
|
+
disabled={saving || !data}
|
|
176
|
+
className={`flex items-center gap-1.5 ${isPanel ? 'px-2.5 py-1 text-[11px] rounded-md' : 'px-3 py-1 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`}
|
|
177
|
+
>
|
|
178
|
+
<RotateCcw size={isPanel ? 11 : 12} />
|
|
179
|
+
{t.settings.ai.restoreFromEnv}
|
|
180
|
+
</button>
|
|
181
|
+
)}
|
|
182
|
+
{tab === 'knowledge' && (
|
|
183
|
+
<a
|
|
184
|
+
href="/setup?force=1"
|
|
185
|
+
className={`flex items-center gap-1.5 ${isPanel ? 'px-2.5 py-1 text-[11px] rounded-md' : 'px-3 py-1 text-xs rounded-lg'} border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors`}
|
|
186
|
+
>
|
|
187
|
+
<RotateCcw size={isPanel ? 11 : 12} />
|
|
188
|
+
{t.settings.reconfigure}
|
|
189
|
+
</a>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
{!isPanel && (
|
|
193
|
+
<div className="flex items-center gap-1.5 text-xs" role="status" aria-live="polite">
|
|
194
|
+
{saving && <><Loader2 size={12} className="animate-spin text-muted-foreground" /><span className="text-muted-foreground">{t.settings.save}...</span></>}
|
|
195
|
+
{status === 'saved' && <><CheckCircle2 size={12} className="text-success" /><span className="text-success">{t.settings.saved}</span></>}
|
|
196
|
+
{status === 'error' && <><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
) : null
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
/* ── Panel variant: unchanged (horizontal tabs) ── */
|
|
204
|
+
if (isPanel) {
|
|
205
|
+
return (
|
|
206
|
+
<>
|
|
207
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
|
|
208
|
+
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
209
|
+
<Settings size={14} className="text-muted-foreground" />
|
|
210
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider font-display">Settings</span>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="flex items-center gap-1.5">
|
|
213
|
+
<div className="flex items-center gap-1.5 text-[10px]" role="status" aria-live="polite">
|
|
214
|
+
{saving && <Loader2 size={10} className="animate-spin text-muted-foreground" />}
|
|
215
|
+
{status === 'saved' && <CheckCircle2 size={10} className="text-success" />}
|
|
216
|
+
{status === 'error' && <AlertCircle size={10} className="text-destructive" />}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div className="flex border-b border-border px-3 shrink-0 overflow-x-auto scrollbar-none gap-0">
|
|
221
|
+
{TABS.map(tabItem => (
|
|
222
|
+
<button
|
|
223
|
+
key={tabItem.id}
|
|
224
|
+
onClick={() => setTab(tabItem.id)}
|
|
225
|
+
className={`flex items-center gap-1 px-2 py-2 text-[11px] font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
|
|
226
|
+
tab === tabItem.id
|
|
227
|
+
? 'border-amber-500 text-foreground'
|
|
228
|
+
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
229
|
+
}`}
|
|
230
|
+
>
|
|
231
|
+
{tabItem.icon}
|
|
232
|
+
{tabItem.label}
|
|
233
|
+
</button>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
{renderContent()}
|
|
237
|
+
{renderFooter()}
|
|
238
|
+
</>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* ── Modal variant ── */
|
|
243
|
+
return (
|
|
244
|
+
<>
|
|
245
|
+
{/* Mobile: original vertical layout */}
|
|
246
|
+
<div className="flex flex-col h-full md:hidden">
|
|
247
|
+
{/* Mobile header */}
|
|
248
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
|
|
249
|
+
<div className="flex justify-center pt-2 pb-0 absolute top-0 left-1/2 -translate-x-1/2">
|
|
250
|
+
<div className="w-8 h-1 rounded-full bg-muted-foreground/20" />
|
|
251
|
+
</div>
|
|
252
|
+
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
253
|
+
<Settings size={15} className="text-muted-foreground" />
|
|
254
|
+
<span className="font-display">{t.settings.title}</span>
|
|
255
|
+
</div>
|
|
256
|
+
<div className="flex items-center gap-1.5">
|
|
257
|
+
<div className="flex items-center gap-1.5 text-[10px]" role="status" aria-live="polite">
|
|
258
|
+
{saving && <Loader2 size={12} className="animate-spin text-muted-foreground" />}
|
|
259
|
+
{status === 'saved' && <><CheckCircle2 size={12} className="text-success" /><span className="text-success">{t.settings.saved}</span></>}
|
|
260
|
+
{status === 'error' && <><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>}
|
|
261
|
+
</div>
|
|
262
|
+
{onClose && (
|
|
263
|
+
<button onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
|
|
264
|
+
<X size={15} />
|
|
265
|
+
</button>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
{/* Mobile horizontal tabs */}
|
|
270
|
+
<div className="flex border-b border-border px-4 shrink-0 overflow-x-auto scrollbar-none gap-0">
|
|
271
|
+
{TABS.map(tabItem => (
|
|
272
|
+
<button
|
|
273
|
+
key={tabItem.id}
|
|
274
|
+
onClick={() => setTab(tabItem.id)}
|
|
275
|
+
className={`flex items-center gap-1.5 px-3 py-2.5 text-xs font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
|
|
276
|
+
tab === tabItem.id
|
|
277
|
+
? 'border-amber-500 text-foreground'
|
|
278
|
+
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
279
|
+
}`}
|
|
280
|
+
>
|
|
281
|
+
{tabItem.icon}
|
|
282
|
+
{tabItem.label}
|
|
283
|
+
</button>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
{renderContent()}
|
|
287
|
+
{renderFooter()}
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{/* Desktop: left-right layout */}
|
|
291
|
+
<div className="hidden md:flex flex-row h-full min-h-0">
|
|
292
|
+
{/* Left sidebar — vertical tabs */}
|
|
293
|
+
<div className="w-[180px] shrink-0 border-r border-border flex flex-col">
|
|
294
|
+
<div className="flex items-center gap-2 px-4 py-3 border-b border-border">
|
|
295
|
+
<Settings size={15} className="text-muted-foreground" />
|
|
296
|
+
<span className="text-sm font-medium font-display text-foreground">{t.settings.title}</span>
|
|
297
|
+
</div>
|
|
298
|
+
<nav className="flex-1 overflow-y-auto py-1.5">
|
|
299
|
+
{TABS.map(tabItem => (
|
|
300
|
+
<button
|
|
301
|
+
key={tabItem.id}
|
|
302
|
+
onClick={() => setTab(tabItem.id)}
|
|
303
|
+
className={`flex items-center gap-2 w-full px-4 py-2 text-xs font-medium transition-colors relative ${
|
|
304
|
+
tab === tabItem.id
|
|
305
|
+
? 'text-foreground bg-muted'
|
|
306
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
|
307
|
+
}`}
|
|
308
|
+
>
|
|
309
|
+
{tab === tabItem.id && (
|
|
310
|
+
<div className="absolute left-0 top-1 bottom-1 w-[3px] rounded-r-full bg-amber-500" />
|
|
311
|
+
)}
|
|
312
|
+
{tabItem.icon}
|
|
313
|
+
{tabItem.label}
|
|
314
|
+
</button>
|
|
315
|
+
))}
|
|
316
|
+
</nav>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{/* Right content area */}
|
|
320
|
+
<div className="flex-1 flex flex-col min-w-0 min-h-0">
|
|
321
|
+
{/* Right header: tab title + status + close */}
|
|
322
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-border shrink-0">
|
|
323
|
+
<span className="text-sm font-medium text-foreground font-display">{activeTabLabel}</span>
|
|
324
|
+
<div className="flex items-center gap-1.5">
|
|
325
|
+
<div className="flex items-center gap-1.5 text-[10px]" role="status" aria-live="polite">
|
|
326
|
+
{saving && <Loader2 size={12} className="animate-spin text-muted-foreground" />}
|
|
327
|
+
{status === 'saved' && <><CheckCircle2 size={12} className="text-success" /><span className="text-success">{t.settings.saved}</span></>}
|
|
328
|
+
{status === 'error' && <><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>}
|
|
329
|
+
</div>
|
|
330
|
+
{onClose && (
|
|
331
|
+
<button onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
|
|
332
|
+
<X size={15} />
|
|
333
|
+
</button>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
{renderContent()}
|
|
338
|
+
{renderFooter()}
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
@@ -33,7 +33,7 @@ export interface SettingsData {
|
|
|
33
33
|
envValues?: Record<string, string>;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export type Tab = 'ai' | 'appearance' | 'knowledge' | 'mcp' | '
|
|
36
|
+
export type Tab = 'ai' | 'appearance' | 'knowledge' | 'mcp' | 'sync';
|
|
37
37
|
|
|
38
38
|
export const CONTENT_WIDTHS = [
|
|
39
39
|
{ value: '680px', label: 'Narrow (680px)' },
|
|
@@ -102,20 +102,6 @@ async function installAgents(
|
|
|
102
102
|
return updated;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/** Phase 3: Install skill to agents. Returns result. */
|
|
106
|
-
async function installSkill(
|
|
107
|
-
template: string,
|
|
108
|
-
agentKeys: string[],
|
|
109
|
-
): Promise<{ ok?: boolean; skill?: string; error?: string }> {
|
|
110
|
-
const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
|
|
111
|
-
const res = await fetch('/api/mcp/install-skill', {
|
|
112
|
-
method: 'POST',
|
|
113
|
-
headers: { 'Content-Type': 'application/json' },
|
|
114
|
-
body: JSON.stringify({ skill: skillName, agents: agentKeys }),
|
|
115
|
-
});
|
|
116
|
-
return await res.json();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
105
|
// ─── Component ───────────────────────────────────────────────────────────────
|
|
120
106
|
|
|
121
107
|
export default function SetupWizard() {
|
|
@@ -318,15 +304,8 @@ export default function SetupWizard() {
|
|
|
318
304
|
}
|
|
319
305
|
}
|
|
320
306
|
|
|
321
|
-
// Phase 3:
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
const skillData = await installSkill(state.template, agentKeys);
|
|
325
|
-
setSkillInstallResult(skillData);
|
|
326
|
-
} catch (e) {
|
|
327
|
-
console.warn('[SetupWizard] skill install failed:', e);
|
|
328
|
-
setSkillInstallResult({ error: 'Failed to install skill' });
|
|
329
|
-
}
|
|
307
|
+
// Phase 3: Skill is now built into SKILL.md — no install needed.
|
|
308
|
+
// user-skill-rules.md will be created on first preference capture.
|
|
330
309
|
|
|
331
310
|
setSubmitting(false);
|
|
332
311
|
setCompleted(true);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface UseResizeDragOptions {
|
|
6
|
+
/** Current width */
|
|
7
|
+
width: number;
|
|
8
|
+
/** Min allowed width */
|
|
9
|
+
minWidth: number;
|
|
10
|
+
/** Max allowed width (absolute) */
|
|
11
|
+
maxWidth: number;
|
|
12
|
+
/** Max width as ratio of viewport */
|
|
13
|
+
maxWidthRatio: number;
|
|
14
|
+
/** 'right' = right-edge drag (mouse right → wider), 'left' = left-edge drag (mouse left → wider) */
|
|
15
|
+
direction: 'right' | 'left';
|
|
16
|
+
/** Skip if true (e.g. maximized state) */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** Called on every mousemove with new width */
|
|
19
|
+
onResize: (width: number) => void;
|
|
20
|
+
/** Called on mouseup with final width */
|
|
21
|
+
onResizeEnd: (width: number) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Shared drag-resize logic for panel edges.
|
|
26
|
+
* Returns a mousedown handler for the resize handle element.
|
|
27
|
+
*/
|
|
28
|
+
export function useResizeDrag({
|
|
29
|
+
width,
|
|
30
|
+
minWidth,
|
|
31
|
+
maxWidth,
|
|
32
|
+
maxWidthRatio,
|
|
33
|
+
direction,
|
|
34
|
+
disabled,
|
|
35
|
+
onResize,
|
|
36
|
+
onResizeEnd,
|
|
37
|
+
}: UseResizeDragOptions) {
|
|
38
|
+
const dragging = useRef(false);
|
|
39
|
+
const startX = useRef(0);
|
|
40
|
+
const startWidth = useRef(0);
|
|
41
|
+
const latestWidthRef = useRef(width);
|
|
42
|
+
latestWidthRef.current = width;
|
|
43
|
+
|
|
44
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
45
|
+
if (disabled) return;
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
dragging.current = true;
|
|
48
|
+
startX.current = e.clientX;
|
|
49
|
+
startWidth.current = width;
|
|
50
|
+
|
|
51
|
+
document.body.classList.add('select-none');
|
|
52
|
+
document.body.style.cursor = 'col-resize';
|
|
53
|
+
|
|
54
|
+
const onMouseMove = (ev: MouseEvent) => {
|
|
55
|
+
if (!dragging.current) return;
|
|
56
|
+
const delta = direction === 'right'
|
|
57
|
+
? ev.clientX - startX.current
|
|
58
|
+
: startX.current - ev.clientX;
|
|
59
|
+
const maxW = Math.min(maxWidth, window.innerWidth * maxWidthRatio);
|
|
60
|
+
const newWidth = Math.round(Math.max(minWidth, Math.min(maxW, startWidth.current + delta)));
|
|
61
|
+
onResize(newWidth);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onMouseUp = () => {
|
|
65
|
+
dragging.current = false;
|
|
66
|
+
document.body.classList.remove('select-none');
|
|
67
|
+
document.body.style.cursor = '';
|
|
68
|
+
onResizeEnd(latestWidthRef.current);
|
|
69
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
70
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
74
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
75
|
+
}, [width, minWidth, maxWidth, maxWidthRatio, direction, disabled, onResize, onResizeEnd]);
|
|
76
|
+
|
|
77
|
+
return handleMouseDown;
|
|
78
|
+
}
|
package/app/lib/agent/index.ts
CHANGED
package/app/lib/agent/model.ts
CHANGED
|
@@ -22,20 +22,28 @@ export function getModelConfig(): {
|
|
|
22
22
|
if (cfg.provider === 'openai') {
|
|
23
23
|
const modelName = cfg.openaiModel;
|
|
24
24
|
let model: Model<any>;
|
|
25
|
-
let apiVariant: string = 'openai-responses'; // Default to responses API
|
|
26
25
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// API variant: 'openai-completions' = /chat/completions (widest compatibility),
|
|
27
|
+
// 'openai-responses' = /responses (OpenAI native). Custom proxies (baseUrl set)
|
|
28
|
+
// almost always only support chat completions, so default to that when baseUrl is set.
|
|
29
|
+
const hasCustomBase = !!cfg.openaiBaseUrl;
|
|
30
|
+
const defaultApi = hasCustomBase ? 'openai-completions' : 'openai-responses';
|
|
29
31
|
const customApiVariant = (cfg as any).openaiApiVariant; // May exist in extended config
|
|
30
32
|
|
|
31
33
|
try {
|
|
32
|
-
|
|
34
|
+
const resolved = piGetModel('openai', modelName as any);
|
|
35
|
+
if (!resolved) throw new Error('Model not in registry');
|
|
36
|
+
model = resolved;
|
|
37
|
+
// If user has a custom baseUrl, override API to completions for compatibility
|
|
38
|
+
if (hasCustomBase && !customApiVariant) {
|
|
39
|
+
model = { ...model, api: defaultApi };
|
|
40
|
+
}
|
|
33
41
|
} catch {
|
|
34
42
|
// Model not in pi-ai registry — construct manually for custom/proxy endpoints
|
|
35
43
|
model = {
|
|
36
44
|
id: modelName,
|
|
37
45
|
name: modelName,
|
|
38
|
-
api: (customApiVariant ??
|
|
46
|
+
api: (customApiVariant ?? defaultApi) as any,
|
|
39
47
|
provider: 'openai',
|
|
40
48
|
baseUrl: 'https://api.openai.com/v1',
|
|
41
49
|
reasoning: false,
|
|
@@ -46,10 +54,23 @@ export function getModelConfig(): {
|
|
|
46
54
|
};
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
// For custom proxy endpoints, set conservative compat flags.
|
|
58
|
+
// Most proxies (Azure, Bedrock relays, corporate gateways) only support
|
|
59
|
+
// a subset of OpenAI's features. These defaults prevent silent failures.
|
|
60
|
+
if (hasCustomBase) {
|
|
61
|
+
model = {
|
|
62
|
+
...model,
|
|
63
|
+
baseUrl: cfg.openaiBaseUrl,
|
|
64
|
+
compat: {
|
|
65
|
+
...(model as any).compat,
|
|
66
|
+
supportsStore: false,
|
|
67
|
+
supportsDeveloperRole: false,
|
|
68
|
+
supportsReasoningEffort: false,
|
|
69
|
+
supportsUsageInStreaming: false,
|
|
70
|
+
supportsStrictMode: false,
|
|
71
|
+
maxTokensField: 'max_tokens' as const,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
53
74
|
if (customApiVariant) {
|
|
54
75
|
model = { ...model, api: customApiVariant };
|
|
55
76
|
}
|
|
@@ -63,7 +84,9 @@ export function getModelConfig(): {
|
|
|
63
84
|
let model: Model<any>;
|
|
64
85
|
|
|
65
86
|
try {
|
|
66
|
-
|
|
87
|
+
const resolved = piGetModel('anthropic', modelName as any);
|
|
88
|
+
if (!resolved) throw new Error('Model not in registry');
|
|
89
|
+
model = resolved;
|
|
67
90
|
} catch {
|
|
68
91
|
// Unknown Anthropic model — construct manually
|
|
69
92
|
model = {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Format bytes to human-readable string (B / KB / MB / GB) */
|
|
2
|
+
export function formatBytes(bytes: number): string {
|
|
3
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
4
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
5
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
6
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Format milliseconds to human-readable uptime string (s / m / h / d) */
|
|
10
|
+
export function formatUptime(ms: number): string {
|
|
11
|
+
const s = Math.floor(ms / 1000);
|
|
12
|
+
if (s < 60) return `${s}s`;
|
|
13
|
+
const m = Math.floor(s / 60);
|
|
14
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
15
|
+
const h = Math.floor(m / 60);
|
|
16
|
+
if (h < 24) return `${h}h ${m % 60}m`;
|
|
17
|
+
const d = Math.floor(h / 24);
|
|
18
|
+
return `${d}d ${h % 24}h`;
|
|
19
|
+
}
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const en = {
|
|
|
16
16
|
createToActivate: 'Create {file} to activate',
|
|
17
17
|
shortcuts: {
|
|
18
18
|
searchFiles: 'Search files',
|
|
19
|
-
askAI: '
|
|
19
|
+
askAI: 'MindOS Agent',
|
|
20
20
|
editFile: 'Edit file',
|
|
21
21
|
save: 'Save',
|
|
22
22
|
settings: 'Settings',
|
|
@@ -30,7 +30,7 @@ export const en = {
|
|
|
30
30
|
},
|
|
31
31
|
sidebar: {
|
|
32
32
|
searchTitle: 'Search (⌘K)',
|
|
33
|
-
askTitle: '
|
|
33
|
+
askTitle: 'MindOS Agent (⌘/)',
|
|
34
34
|
settingsTitle: 'Settings (⌘,)',
|
|
35
35
|
collapseTitle: 'Collapse sidebar',
|
|
36
36
|
expandTitle: 'Expand sidebar',
|
|
@@ -104,7 +104,7 @@ export const en = {
|
|
|
104
104
|
},
|
|
105
105
|
settings: {
|
|
106
106
|
title: 'Settings',
|
|
107
|
-
tabs: { ai: 'AI', appearance: 'Appearance', knowledge: '
|
|
107
|
+
tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'General', sync: 'Sync', mcp: 'MCP & Skills', plugins: 'Plugins', shortcuts: 'Shortcuts', monitoring: 'Monitoring', agents: 'Agents' },
|
|
108
108
|
ai: {
|
|
109
109
|
provider: 'Provider',
|
|
110
110
|
model: 'Model',
|
|
@@ -323,12 +323,12 @@ export const en = {
|
|
|
323
323
|
},
|
|
324
324
|
shortcuts: [
|
|
325
325
|
{ keys: ['⌘', 'K'], description: 'Search' },
|
|
326
|
-
{ keys: ['⌘', '/'], description: '
|
|
326
|
+
{ keys: ['⌘', '/'], description: 'MindOS Agent' },
|
|
327
327
|
{ keys: ['⌘', ','], description: 'Settings' },
|
|
328
328
|
{ keys: ['E'], description: 'Edit current file' },
|
|
329
329
|
{ keys: ['⌘', 'S'], description: 'Save' },
|
|
330
330
|
{ keys: ['Esc'], description: 'Cancel edit / close modal' },
|
|
331
|
-
{ keys: ['@'], description: 'Attach file in
|
|
331
|
+
{ keys: ['@'], description: 'Attach file in MindOS Agent' },
|
|
332
332
|
],
|
|
333
333
|
login: {
|
|
334
334
|
tagline: 'You think here, Agents act there.',
|
|
@@ -468,7 +468,7 @@ export const en = {
|
|
|
468
468
|
welcomeTitle: 'Welcome to MindOS!',
|
|
469
469
|
welcomeDesc: 'Setup is complete. Start by asking AI a question, browsing your knowledge base, or configuring MCP agents.',
|
|
470
470
|
welcomeLinkReconfigure: 'Reconfigure',
|
|
471
|
-
welcomeLinkAskAI: '
|
|
471
|
+
welcomeLinkAskAI: 'MindOS Agent',
|
|
472
472
|
welcomeLinkMCP: 'MCP Settings',
|
|
473
473
|
},
|
|
474
474
|
guide: {
|
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -41,7 +41,7 @@ export const zh = {
|
|
|
41
41
|
createToActivate: '创建 {file} 以启用此插件',
|
|
42
42
|
shortcuts: {
|
|
43
43
|
searchFiles: '搜索文件',
|
|
44
|
-
askAI: '
|
|
44
|
+
askAI: 'MindOS Agent',
|
|
45
45
|
editFile: '编辑文件',
|
|
46
46
|
save: '保存',
|
|
47
47
|
settings: '设置',
|
|
@@ -55,7 +55,7 @@ export const zh = {
|
|
|
55
55
|
},
|
|
56
56
|
sidebar: {
|
|
57
57
|
searchTitle: '搜索 (⌘K)',
|
|
58
|
-
askTitle: '
|
|
58
|
+
askTitle: 'MindOS Agent (⌘/)',
|
|
59
59
|
settingsTitle: '设置 (⌘,)',
|
|
60
60
|
collapseTitle: '收起侧栏',
|
|
61
61
|
expandTitle: '展开侧栏',
|
|
@@ -129,7 +129,7 @@ export const zh = {
|
|
|
129
129
|
},
|
|
130
130
|
settings: {
|
|
131
131
|
title: '设置',
|
|
132
|
-
tabs: { ai: 'AI', appearance: '外观', knowledge: '
|
|
132
|
+
tabs: { ai: 'AI', appearance: '外观', knowledge: '通用', sync: '同步', mcp: 'MCP & Skills', plugins: '插件', shortcuts: '快捷键', monitoring: '监控', agents: 'Agents' },
|
|
133
133
|
ai: {
|
|
134
134
|
provider: '服务商',
|
|
135
135
|
model: '模型',
|
|
@@ -348,7 +348,7 @@ export const zh = {
|
|
|
348
348
|
},
|
|
349
349
|
shortcuts: [
|
|
350
350
|
{ keys: ['⌘', 'K'], description: '搜索' },
|
|
351
|
-
{ keys: ['⌘', '/'], description: '
|
|
351
|
+
{ keys: ['⌘', '/'], description: 'MindOS Agent' },
|
|
352
352
|
{ keys: ['⌘', ','], description: '设置' },
|
|
353
353
|
{ keys: ['E'], description: '编辑当前文件' },
|
|
354
354
|
{ keys: ['⌘', 'S'], description: '保存' },
|
|
@@ -493,7 +493,7 @@ export const zh = {
|
|
|
493
493
|
welcomeTitle: '欢迎使用 MindOS!',
|
|
494
494
|
welcomeDesc: '初始化完成。可以开始向 AI 提问、浏览知识库,或配置 MCP Agent。',
|
|
495
495
|
welcomeLinkReconfigure: '重新配置',
|
|
496
|
-
welcomeLinkAskAI: '
|
|
496
|
+
welcomeLinkAskAI: 'MindOS Agent',
|
|
497
497
|
welcomeLinkMCP: 'MCP 设置',
|
|
498
498
|
},
|
|
499
499
|
guide: {
|