@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,41 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* POST /api/update — trigger `mindos update` as a detached child process.
|
|
8
|
+
*
|
|
9
|
+
* Similar to /api/restart: spawns the CLI command and returns immediately.
|
|
10
|
+
* The update process will npm install, remove build stamp, and restart
|
|
11
|
+
* the server. The current process will be killed during restart.
|
|
12
|
+
*/
|
|
13
|
+
export async function POST() {
|
|
14
|
+
try {
|
|
15
|
+
const cliPath = process.env.MINDOS_CLI_PATH || resolve(process.cwd(), '..', 'bin', 'cli.js');
|
|
16
|
+
const nodeBin = process.env.MINDOS_NODE_BIN || process.execPath;
|
|
17
|
+
|
|
18
|
+
// Strip MINDOS_* env vars so the child reads fresh config
|
|
19
|
+
const childEnv = { ...process.env };
|
|
20
|
+
delete childEnv.MINDOS_WEB_PORT;
|
|
21
|
+
delete childEnv.MINDOS_MCP_PORT;
|
|
22
|
+
delete childEnv.MIND_ROOT;
|
|
23
|
+
delete childEnv.AUTH_TOKEN;
|
|
24
|
+
delete childEnv.WEB_PASSWORD;
|
|
25
|
+
|
|
26
|
+
const child = spawn(nodeBin, [cliPath, 'update'], {
|
|
27
|
+
detached: true,
|
|
28
|
+
stdio: 'ignore',
|
|
29
|
+
env: childEnv,
|
|
30
|
+
});
|
|
31
|
+
child.unref();
|
|
32
|
+
|
|
33
|
+
// Unlike /api/restart, we do NOT process.exit() here.
|
|
34
|
+
// `mindos update` will npm install first (30s+), then restart which
|
|
35
|
+
// kills this process. Exiting early would break the response.
|
|
36
|
+
return NextResponse.json({ ok: true });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { redirect } from 'next/navigation';
|
|
2
|
+
import { readSettings } from '@/lib/settings';
|
|
3
|
+
import ExploreContent from '@/components/explore/ExploreContent';
|
|
4
|
+
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export default function ExplorePage() {
|
|
8
|
+
const settings = readSettings();
|
|
9
|
+
if (settings.setupPending) redirect('/setup');
|
|
10
|
+
|
|
11
|
+
return <ExploreContent />;
|
|
12
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useRef, useCallback } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { FolderTree, Search, Settings, RefreshCw, Blocks, Bot, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
7
|
import { DOT_COLORS, getStatusLevel } from './SyncStatusBar';
|
|
7
8
|
import type { SyncStatus } from './settings/SyncTab';
|
|
8
9
|
import Logo from './Logo';
|
|
@@ -32,9 +33,11 @@ interface RailButtonProps {
|
|
|
32
33
|
buttonRef?: React.Ref<HTMLButtonElement>;
|
|
33
34
|
/** Optional overlay badge (e.g. status dot) rendered inside the button */
|
|
34
35
|
badge?: React.ReactNode;
|
|
36
|
+
/** Optional data-walkthrough attribute for interactive walkthrough targeting */
|
|
37
|
+
walkthroughId?: string;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
function RailButton({ icon, label, shortcut, active = false, expanded, onClick, buttonRef, badge }: RailButtonProps) {
|
|
40
|
+
function RailButton({ icon, label, shortcut, active = false, expanded, onClick, buttonRef, badge, walkthroughId }: RailButtonProps) {
|
|
38
41
|
return (
|
|
39
42
|
<button
|
|
40
43
|
ref={buttonRef}
|
|
@@ -42,6 +45,7 @@ function RailButton({ icon, label, shortcut, active = false, expanded, onClick,
|
|
|
42
45
|
aria-pressed={active}
|
|
43
46
|
aria-label={label}
|
|
44
47
|
title={expanded ? undefined : (shortcut ? `${label} (${shortcut})` : label)}
|
|
48
|
+
data-walkthrough={walkthroughId}
|
|
45
49
|
className={`
|
|
46
50
|
relative flex items-center ${expanded ? 'justify-start px-3 w-full' : 'justify-center w-10'} h-10 rounded-md transition-colors
|
|
47
51
|
${active
|
|
@@ -79,6 +83,7 @@ export default function ActivityBar({
|
|
|
79
83
|
}: ActivityBarProps) {
|
|
80
84
|
const lastClickRef = useRef(0);
|
|
81
85
|
const syncBtnRef = useRef<HTMLButtonElement>(null);
|
|
86
|
+
const { t } = useLocale();
|
|
82
87
|
|
|
83
88
|
/** Debounce rapid clicks (300ms) — shared across all Rail buttons */
|
|
84
89
|
const debounced = useCallback((fn: () => void) => {
|
|
@@ -109,6 +114,7 @@ export default function ActivityBar({
|
|
|
109
114
|
role="toolbar"
|
|
110
115
|
aria-label="Navigation"
|
|
111
116
|
aria-orientation="vertical"
|
|
117
|
+
data-walkthrough="activity-bar"
|
|
112
118
|
>
|
|
113
119
|
{/* Content wrapper — overflow-hidden prevents text flash during width transitions */}
|
|
114
120
|
<div className="flex flex-col h-full w-full overflow-hidden">
|
|
@@ -126,10 +132,10 @@ export default function ActivityBar({
|
|
|
126
132
|
|
|
127
133
|
{/* ── Middle: Core panel toggles ── */}
|
|
128
134
|
<div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
|
|
129
|
-
<RailButton icon={<FolderTree size={18} />} label=
|
|
130
|
-
<RailButton icon={<Search size={18} />} label=
|
|
131
|
-
<RailButton icon={<Blocks size={18} />} label=
|
|
132
|
-
<RailButton icon={<Bot size={18} />} label=
|
|
135
|
+
<RailButton icon={<FolderTree size={18} />} label={t.sidebar.files} active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} walkthroughId="files-panel" />
|
|
136
|
+
<RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} walkthroughId="search-button" />
|
|
137
|
+
<RailButton icon={<Blocks size={18} />} label={t.sidebar.plugins} active={activePanel === 'plugins'} expanded={expanded} onClick={() => toggle('plugins')} />
|
|
138
|
+
<RailButton icon={<Bot size={18} />} label={t.sidebar.agents} active={activePanel === 'agents'} expanded={expanded} onClick={() => toggle('agents')} />
|
|
133
139
|
</div>
|
|
134
140
|
|
|
135
141
|
{/* ── Spacer ── */}
|
|
@@ -140,14 +146,15 @@ export default function ActivityBar({
|
|
|
140
146
|
<div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
|
|
141
147
|
<RailButton
|
|
142
148
|
icon={<Settings size={18} />}
|
|
143
|
-
label=
|
|
149
|
+
label={t.sidebar.settingsTitle}
|
|
144
150
|
shortcut="⌘,"
|
|
145
151
|
expanded={expanded}
|
|
146
152
|
onClick={() => debounced(onSettingsClick)}
|
|
153
|
+
walkthroughId="settings-button"
|
|
147
154
|
/>
|
|
148
155
|
<RailButton
|
|
149
156
|
icon={<RefreshCw size={18} />}
|
|
150
|
-
label=
|
|
157
|
+
label={t.sidebar.syncLabel}
|
|
151
158
|
expanded={expanded}
|
|
152
159
|
buttonRef={syncBtnRef}
|
|
153
160
|
badge={syncBadge}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
4
|
import { X, Sparkles, FolderOpen, MessageCircle, RefreshCw, Check, ChevronRight } from 'lucide-react';
|
|
5
|
+
import Link from 'next/link';
|
|
5
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
7
|
import { openAskModal } from '@/hooks/useAskModal';
|
|
8
|
+
import { walkthroughSteps } from './walkthrough/steps';
|
|
7
9
|
import type { GuideState } from '@/lib/settings';
|
|
8
10
|
|
|
9
11
|
const DIR_ICONS: Record<string, string> = {
|
|
@@ -47,20 +49,18 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
47
49
|
useEffect(() => {
|
|
48
50
|
fetchGuideState();
|
|
49
51
|
|
|
50
|
-
//
|
|
51
|
-
const
|
|
52
|
-
if (params.get('welcome') === '1') {
|
|
52
|
+
// Listen for walkthrough-triggered first visit (WalkthroughProvider owns ?welcome=1 detection)
|
|
53
|
+
const handleFirstVisit = () => {
|
|
53
54
|
setIsFirstVisit(true);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
window.history.replaceState({}, '', url.pathname + (url.search || ''));
|
|
57
|
-
}
|
|
55
|
+
};
|
|
56
|
+
window.addEventListener('mindos:first-visit', handleFirstVisit);
|
|
58
57
|
|
|
59
58
|
// Re-fetch when guide state is updated (e.g. after AskFab patches askedAI)
|
|
60
59
|
const handleGuideUpdate = () => fetchGuideState();
|
|
61
60
|
window.addEventListener('focus', handleGuideUpdate);
|
|
62
61
|
window.addEventListener('guide-state-updated', handleGuideUpdate);
|
|
63
62
|
return () => {
|
|
63
|
+
window.removeEventListener('mindos:first-visit', handleFirstVisit);
|
|
64
64
|
window.removeEventListener('focus', handleGuideUpdate);
|
|
65
65
|
window.removeEventListener('guide-state-updated', handleGuideUpdate);
|
|
66
66
|
};
|
|
@@ -147,6 +147,13 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
147
147
|
// Don't render if no active guide
|
|
148
148
|
if (!guideState) return null;
|
|
149
149
|
|
|
150
|
+
// Hide GuideCard while walkthrough is active
|
|
151
|
+
const walkthroughActive = guideState.walkthroughStep !== undefined
|
|
152
|
+
&& guideState.walkthroughStep >= 0
|
|
153
|
+
&& guideState.walkthroughStep < walkthroughSteps.length
|
|
154
|
+
&& !guideState.walkthroughDismissed;
|
|
155
|
+
if (walkthroughActive) return null;
|
|
156
|
+
|
|
150
157
|
const step1Done = guideState.step1Done;
|
|
151
158
|
const step2Done = guideState.askedAI;
|
|
152
159
|
const allDone = step1Done && step2Done;
|
|
@@ -164,6 +171,13 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
|
|
|
164
171
|
<span className="text-sm font-semibold flex-1" style={{ color: 'var(--foreground)' }}>
|
|
165
172
|
✨ {g.done.titleFinal}
|
|
166
173
|
</span>
|
|
174
|
+
<Link
|
|
175
|
+
href="/explore"
|
|
176
|
+
className="text-xs font-medium transition-colors hover:opacity-80"
|
|
177
|
+
style={{ color: 'var(--amber)' }}
|
|
178
|
+
>
|
|
179
|
+
{t.walkthrough.exploreCta}
|
|
180
|
+
</Link>
|
|
167
181
|
<button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors"
|
|
168
182
|
style={{ color: 'var(--muted-foreground)' }}>
|
|
169
183
|
<X size={14} />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown } from 'lucide-react';
|
|
4
|
+
import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown, Compass } from 'lucide-react';
|
|
5
5
|
import { useState, useEffect, useRef } from 'react';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { encodePath, relativeTime } from '@/lib/utils';
|
|
@@ -94,6 +94,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
94
94
|
<button
|
|
95
95
|
onClick={triggerAsk}
|
|
96
96
|
title="⌘/"
|
|
97
|
+
data-walkthrough="ask-button"
|
|
97
98
|
className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/8"
|
|
98
99
|
style={{ background: 'var(--card)', borderColor: 'var(--border)' }}
|
|
99
100
|
>
|
|
@@ -153,6 +154,16 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
|
|
|
153
154
|
<FilePlus size={14} />
|
|
154
155
|
<span>{t.home.newNote}</span>
|
|
155
156
|
</Link>
|
|
157
|
+
<Link
|
|
158
|
+
href="/explore"
|
|
159
|
+
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-all duration-150 hover:translate-x-0.5"
|
|
160
|
+
style={{
|
|
161
|
+
color: 'var(--amber)',
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<Compass size={14} />
|
|
165
|
+
<span>{t.explore.title}</span>
|
|
166
|
+
</Link>
|
|
156
167
|
</div>
|
|
157
168
|
|
|
158
169
|
</div>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
|
|
7
|
+
export default function KeyboardShortcuts() {
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
const { t } = useLocale();
|
|
10
|
+
const s = t.shortcutPanel;
|
|
11
|
+
|
|
12
|
+
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent);
|
|
13
|
+
const mod = isMac ? '⌘' : 'Ctrl';
|
|
14
|
+
|
|
15
|
+
const shortcuts = useMemo(() => [
|
|
16
|
+
{ keys: `${mod} K`, label: s.toggleSearch, section: s.navigation },
|
|
17
|
+
{ keys: `${mod} /`, label: s.toggleAskAI, section: s.navigation },
|
|
18
|
+
{ keys: `${mod} ,`, label: s.openSettings, section: s.navigation },
|
|
19
|
+
{ keys: `${mod} ?`, label: s.keyboardShortcuts, section: s.navigation },
|
|
20
|
+
{ keys: 'Esc', label: s.closePanel, section: s.panelsSection },
|
|
21
|
+
{ keys: `${mod} S`, label: s.saveFile, section: s.editor },
|
|
22
|
+
{ keys: `${mod} Z`, label: s.undo, section: s.editor },
|
|
23
|
+
{ keys: `${mod} Shift Z`, label: s.redo, section: s.editor },
|
|
24
|
+
], [mod, s]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handler = (e: KeyboardEvent) => {
|
|
28
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === '/') {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
setOpen(v => !v);
|
|
31
|
+
}
|
|
32
|
+
if (e.key === 'Escape' && open) {
|
|
33
|
+
setOpen(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
window.addEventListener('keydown', handler);
|
|
37
|
+
return () => window.removeEventListener('keydown', handler);
|
|
38
|
+
}, [open]);
|
|
39
|
+
|
|
40
|
+
if (!open) return null;
|
|
41
|
+
|
|
42
|
+
const sections = [...new Set(shortcuts.map(s => s.section))];
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className="fixed inset-0 z-50 flex items-center justify-center modal-backdrop"
|
|
47
|
+
onClick={e => e.target === e.currentTarget && setOpen(false)}
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
role="dialog"
|
|
51
|
+
aria-modal="true"
|
|
52
|
+
aria-label={s.title}
|
|
53
|
+
className="w-full max-w-md mx-4 bg-card border border-border rounded-xl shadow-2xl overflow-hidden"
|
|
54
|
+
>
|
|
55
|
+
{/* Header */}
|
|
56
|
+
<div className="flex items-center justify-between px-5 py-3.5 border-b border-border">
|
|
57
|
+
<span className="text-sm font-medium font-display text-foreground">{s.title}</span>
|
|
58
|
+
<button
|
|
59
|
+
onClick={() => setOpen(false)}
|
|
60
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
61
|
+
>
|
|
62
|
+
<X size={15} />
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Content */}
|
|
67
|
+
<div className="px-5 py-4 space-y-4 max-h-[60vh] overflow-y-auto">
|
|
68
|
+
{sections.map(section => (
|
|
69
|
+
<div key={section}>
|
|
70
|
+
<h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{section}</h3>
|
|
71
|
+
<div className="space-y-1">
|
|
72
|
+
{shortcuts.filter(s => s.section === section).map(s => (
|
|
73
|
+
<div key={s.keys} className="flex items-center justify-between py-1.5">
|
|
74
|
+
<span className="text-xs text-foreground">{s.label}</span>
|
|
75
|
+
<div className="flex items-center gap-1">
|
|
76
|
+
{s.keys.split(' ').map((key, i) => (
|
|
77
|
+
<kbd
|
|
78
|
+
key={i}
|
|
79
|
+
className="px-1.5 py-0.5 text-2xs rounded border border-border bg-muted text-muted-foreground font-mono min-w-[24px] text-center"
|
|
80
|
+
>
|
|
81
|
+
{key}
|
|
82
|
+
</kbd>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Footer */}
|
|
93
|
+
<div className="px-5 py-2.5 border-t border-border">
|
|
94
|
+
<p className="text-2xs text-muted-foreground/60">
|
|
95
|
+
Press <kbd className="px-1 py-0.5 text-2xs rounded border border-border bg-muted font-mono">{mod}</kbd>
|
|
96
|
+
<kbd className="px-1 py-0.5 text-2xs rounded border border-border bg-muted font-mono ml-0.5">?</kbd> {s.toggleHint}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
package/app/components/Panel.tsx
CHANGED
|
@@ -8,6 +8,9 @@ import FileTree from './FileTree';
|
|
|
8
8
|
import SyncStatusBar from './SyncStatusBar';
|
|
9
9
|
import PanelHeader from './panels/PanelHeader';
|
|
10
10
|
import { useResizeDrag } from '@/hooks/useResizeDrag';
|
|
11
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
12
|
+
|
|
13
|
+
const noop = () => {};
|
|
11
14
|
|
|
12
15
|
/** Compute the maximum directory depth of a file tree */
|
|
13
16
|
function getMaxDepth(nodes: FileNode[], current = 0): number {
|
|
@@ -68,6 +71,8 @@ export default function Panel({
|
|
|
68
71
|
const defaultWidth = activePanel ? DEFAULT_PANEL_WIDTH[activePanel] : 280;
|
|
69
72
|
const width = maximized ? undefined : (panelWidth ?? defaultWidth);
|
|
70
73
|
|
|
74
|
+
const { t } = useLocale();
|
|
75
|
+
|
|
71
76
|
// File tree depth control: null = manual (no override), number = forced max open depth
|
|
72
77
|
const [maxOpenDepth, setMaxOpenDepth] = useState<number | null>(null);
|
|
73
78
|
const treeMaxDepth = useMemo(() => getMaxDepth(fileTree), [fileTree]);
|
|
@@ -79,8 +84,8 @@ export default function Panel({
|
|
|
79
84
|
maxWidthRatio: MAX_PANEL_WIDTH_RATIO,
|
|
80
85
|
direction: 'right',
|
|
81
86
|
disabled: maximized,
|
|
82
|
-
onResize: onWidthChange ??
|
|
83
|
-
onResizeEnd: onWidthCommit ??
|
|
87
|
+
onResize: onWidthChange ?? noop,
|
|
88
|
+
onResizeEnd: onWidthCommit ?? noop,
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
return (
|
|
@@ -97,7 +102,7 @@ export default function Panel({
|
|
|
97
102
|
>
|
|
98
103
|
{/* Files panel — always mounted to preserve tree expand/collapse state */}
|
|
99
104
|
<div className={`flex flex-col h-full ${activePanel === 'files' ? '' : 'hidden'}`}>
|
|
100
|
-
<PanelHeader title=
|
|
105
|
+
<PanelHeader title={t.sidebar.files}>
|
|
101
106
|
<div className="flex items-center gap-0.5">
|
|
102
107
|
<button
|
|
103
108
|
onClick={() => setMaxOpenDepth(prev => {
|
|
@@ -105,8 +110,8 @@ export default function Panel({
|
|
|
105
110
|
return Math.max(-1, current - 1);
|
|
106
111
|
})}
|
|
107
112
|
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
108
|
-
aria-label=
|
|
109
|
-
title=
|
|
113
|
+
aria-label={t.sidebar.collapseLevel}
|
|
114
|
+
title={t.sidebar.collapseLevel}
|
|
110
115
|
>
|
|
111
116
|
<ChevronsDownUp size={13} />
|
|
112
117
|
</button>
|
|
@@ -120,8 +125,8 @@ export default function Panel({
|
|
|
120
125
|
return next;
|
|
121
126
|
})}
|
|
122
127
|
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
123
|
-
aria-label=
|
|
124
|
-
title=
|
|
128
|
+
aria-label={t.sidebar.expandLevel}
|
|
129
|
+
title={t.sidebar.expandLevel}
|
|
125
130
|
>
|
|
126
131
|
<ChevronsUpDown size={13} />
|
|
127
132
|
</button>
|
|
@@ -17,10 +17,12 @@ import SyncPopover from './panels/SyncPopover';
|
|
|
17
17
|
import SearchModal from './SearchModal';
|
|
18
18
|
import AskModal from './AskModal';
|
|
19
19
|
import SettingsModal from './SettingsModal';
|
|
20
|
+
import KeyboardShortcuts from './KeyboardShortcuts';
|
|
20
21
|
import { MobileSyncDot, useSyncStatus } from './SyncStatusBar';
|
|
21
22
|
import { useAskModal } from '@/hooks/useAskModal';
|
|
22
23
|
import { FileNode } from '@/lib/types';
|
|
23
24
|
import { useLocale } from '@/lib/LocaleContext';
|
|
25
|
+
import { WalkthroughProvider } from './walkthrough';
|
|
24
26
|
import type { Tab } from './settings/types';
|
|
25
27
|
|
|
26
28
|
interface SidebarLayoutProps {
|
|
@@ -177,6 +179,17 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
177
179
|
try { localStorage.setItem('rail-expanded', String(expanded)); } catch {}
|
|
178
180
|
}, []);
|
|
179
181
|
|
|
182
|
+
// Listen for cross-component "open settings" events (e.g. from UpdateBanner)
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
const handler = (e: Event) => {
|
|
185
|
+
const tab = (e as CustomEvent).detail?.tab;
|
|
186
|
+
if (tab) setSettingsTab(tab);
|
|
187
|
+
setSettingsOpen(true);
|
|
188
|
+
};
|
|
189
|
+
window.addEventListener('mindos:open-settings', handler);
|
|
190
|
+
return () => window.removeEventListener('mindos:open-settings', handler);
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
180
193
|
// Bridge useAskModal store → right Ask panel or popup
|
|
181
194
|
useEffect(() => {
|
|
182
195
|
if (askModal.open) {
|
|
@@ -290,6 +303,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
290
303
|
const effectivePanelWidth = panelWidth ?? (activePanel ? PANEL_WIDTH[activePanel] : 280);
|
|
291
304
|
|
|
292
305
|
return (
|
|
306
|
+
<WalkthroughProvider>
|
|
293
307
|
<>
|
|
294
308
|
{/* Skip link */}
|
|
295
309
|
<a
|
|
@@ -335,7 +349,6 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
335
349
|
active={activePanel === 'agents'}
|
|
336
350
|
maximized={panelMaximized}
|
|
337
351
|
onMaximize={handlePanelMaximize}
|
|
338
|
-
onOpenSettings={openSettingsTab}
|
|
339
352
|
/>
|
|
340
353
|
</div>
|
|
341
354
|
</Panel>
|
|
@@ -368,6 +381,9 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
368
381
|
{/* ── Ask AI FAB (desktop only — toggles right panel or popup) ── */}
|
|
369
382
|
<AskFab onToggle={toggleAskPanel} askPanelOpen={askPanelOpen || desktopAskPopupOpen} />
|
|
370
383
|
|
|
384
|
+
{/* ── Keyboard Shortcuts (⌘?) ── */}
|
|
385
|
+
<KeyboardShortcuts />
|
|
386
|
+
|
|
371
387
|
{/* ── Settings Modal (desktop overlay — does not affect panel) ── */}
|
|
372
388
|
<SettingsModal
|
|
373
389
|
open={settingsOpen}
|
|
@@ -455,5 +471,6 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
455
471
|
}
|
|
456
472
|
`}</style>
|
|
457
473
|
</>
|
|
474
|
+
</WalkthroughProvider>
|
|
458
475
|
);
|
|
459
476
|
}
|
|
@@ -45,6 +45,13 @@ export default function UpdateBanner() {
|
|
|
45
45
|
setInfo(null);
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
const handleOpenUpdate = () => {
|
|
49
|
+
window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'update' } }));
|
|
50
|
+
// Dismiss banner once user engages with Update tab
|
|
51
|
+
localStorage.setItem('mindos_update_dismissed', info.latest);
|
|
52
|
+
setInfo(null);
|
|
53
|
+
};
|
|
54
|
+
|
|
48
55
|
const updateT = t.updateBanner;
|
|
49
56
|
|
|
50
57
|
return (
|
|
@@ -58,32 +65,23 @@ export default function UpdateBanner() {
|
|
|
58
65
|
? updateT.newVersion(info.latest, info.current)
|
|
59
66
|
: `MindOS v${info.latest} available (current: v${info.current})`}
|
|
60
67
|
</span>
|
|
61
|
-
<
|
|
62
|
-
{
|
|
63
|
-
|
|
68
|
+
<button
|
|
69
|
+
onClick={handleOpenUpdate}
|
|
70
|
+
className="px-2 py-0.5 rounded-md text-xs font-medium text-white transition-colors hover:opacity-90"
|
|
71
|
+
style={{ background: 'var(--amber)' }}
|
|
72
|
+
>
|
|
73
|
+
{updateT?.updateNow ?? 'Update'}
|
|
74
|
+
</button>
|
|
75
|
+
<span className="text-muted-foreground hidden sm:inline">
|
|
64
76
|
{updateT?.orSee ? (
|
|
65
77
|
<>
|
|
66
|
-
{
|
|
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>
|
|
78
|
+
{updateT.orSee}{' '}
|
|
79
|
+
<a href="https://github.com/GeminiLight/mindos/releases" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground transition-colors">{updateT.releaseNotes}</a>
|
|
75
80
|
</>
|
|
76
81
|
) : (
|
|
77
82
|
<>
|
|
78
|
-
|
|
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>
|
|
83
|
+
or{' '}
|
|
84
|
+
<a href="https://github.com/GeminiLight/mindos/releases" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground transition-colors">release notes</a>
|
|
87
85
|
</>
|
|
88
86
|
)}
|
|
89
87
|
</span>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
5
|
+
import { useCases, categories, type UseCaseCategory } from './use-cases';
|
|
6
|
+
import UseCaseCard from './UseCaseCard';
|
|
7
|
+
|
|
8
|
+
export default function ExploreContent() {
|
|
9
|
+
const { t } = useLocale();
|
|
10
|
+
const e = t.explore;
|
|
11
|
+
const [activeCategory, setActiveCategory] = useState<UseCaseCategory | 'all'>('all');
|
|
12
|
+
|
|
13
|
+
const filtered = activeCategory === 'all'
|
|
14
|
+
? useCases
|
|
15
|
+
: useCases.filter(uc => uc.category === activeCategory);
|
|
16
|
+
|
|
17
|
+
/** Type-safe lookup for use case i18n data by id */
|
|
18
|
+
const getUseCaseText = (id: string): { title: string; desc: string; prompt: string } | undefined => {
|
|
19
|
+
const map: Record<string, { title: string; desc: string; prompt: string }> = {
|
|
20
|
+
c1: e.c1, c2: e.c2, c3: e.c3, c4: e.c4, c5: e.c5,
|
|
21
|
+
c6: e.c6, c7: e.c7, c8: e.c8, c9: e.c9,
|
|
22
|
+
};
|
|
23
|
+
return map[id];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="content-width px-4 md:px-6 py-8 md:py-12">
|
|
28
|
+
{/* Header */}
|
|
29
|
+
<div className="mb-8">
|
|
30
|
+
<div className="flex items-center gap-2 mb-3">
|
|
31
|
+
<div className="w-1 h-5 rounded-full" style={{ background: 'var(--amber)' }} />
|
|
32
|
+
<h1
|
|
33
|
+
className="text-2xl font-semibold tracking-tight font-display"
|
|
34
|
+
style={{ color: 'var(--foreground)' }}
|
|
35
|
+
>
|
|
36
|
+
{e.title}
|
|
37
|
+
</h1>
|
|
38
|
+
</div>
|
|
39
|
+
<p
|
|
40
|
+
className="text-sm leading-relaxed"
|
|
41
|
+
style={{ color: 'var(--muted-foreground)', paddingLeft: '1rem' }}
|
|
42
|
+
>
|
|
43
|
+
{e.subtitle}
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Category tabs */}
|
|
48
|
+
<div className="flex flex-wrap gap-2 mb-6" style={{ paddingLeft: '1rem' }}>
|
|
49
|
+
<CategoryChip
|
|
50
|
+
label={e.all}
|
|
51
|
+
active={activeCategory === 'all'}
|
|
52
|
+
onClick={() => setActiveCategory('all')}
|
|
53
|
+
/>
|
|
54
|
+
{categories.map(cat => (
|
|
55
|
+
<CategoryChip
|
|
56
|
+
key={cat}
|
|
57
|
+
label={(e.categories as Record<string, string>)[cat]}
|
|
58
|
+
active={activeCategory === cat}
|
|
59
|
+
onClick={() => setActiveCategory(cat)}
|
|
60
|
+
/>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* Card grid */}
|
|
65
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3" style={{ paddingLeft: '1rem' }}>
|
|
66
|
+
{filtered.map(uc => {
|
|
67
|
+
const data = getUseCaseText(uc.id);
|
|
68
|
+
if (!data) return null;
|
|
69
|
+
return (
|
|
70
|
+
<UseCaseCard
|
|
71
|
+
key={uc.id}
|
|
72
|
+
icon={uc.icon}
|
|
73
|
+
title={data.title}
|
|
74
|
+
description={data.desc}
|
|
75
|
+
prompt={data.prompt}
|
|
76
|
+
tryItLabel={e.tryIt}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function CategoryChip({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
|
|
86
|
+
return (
|
|
87
|
+
<button
|
|
88
|
+
onClick={onClick}
|
|
89
|
+
className={`
|
|
90
|
+
px-3 py-1.5 rounded-full text-xs font-medium transition-all duration-150
|
|
91
|
+
${active
|
|
92
|
+
? 'text-[var(--amber)] bg-[var(--amber-dim)]'
|
|
93
|
+
: 'text-[var(--muted-foreground)] bg-[var(--muted)] hover:text-[var(--foreground)] hover:bg-[var(--muted)]/80'
|
|
94
|
+
}
|
|
95
|
+
`}
|
|
96
|
+
>
|
|
97
|
+
{label}
|
|
98
|
+
</button>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { openAskModal } from '@/hooks/useAskModal';
|
|
4
|
+
|
|
5
|
+
interface UseCaseCardProps {
|
|
6
|
+
icon: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
prompt: string;
|
|
10
|
+
tryItLabel: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function UseCaseCard({ icon, title, description, prompt, tryItLabel }: UseCaseCardProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className="group flex flex-col gap-3 p-4 rounded-xl border transition-all duration-150 hover:border-amber-500/30 hover:bg-muted/50"
|
|
17
|
+
style={{ borderColor: 'var(--border)', background: 'var(--card)' }}
|
|
18
|
+
>
|
|
19
|
+
<div className="flex items-start gap-3">
|
|
20
|
+
<span className="text-xl leading-none shrink-0 mt-0.5" suppressHydrationWarning>
|
|
21
|
+
{icon}
|
|
22
|
+
</span>
|
|
23
|
+
<div className="flex-1 min-w-0">
|
|
24
|
+
<h3
|
|
25
|
+
className="text-sm font-semibold font-display truncate"
|
|
26
|
+
style={{ color: 'var(--foreground)' }}
|
|
27
|
+
>
|
|
28
|
+
{title}
|
|
29
|
+
</h3>
|
|
30
|
+
<p
|
|
31
|
+
className="text-xs leading-relaxed mt-1 line-clamp-2"
|
|
32
|
+
style={{ color: 'var(--muted-foreground)' }}
|
|
33
|
+
>
|
|
34
|
+
{description}
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => openAskModal(prompt, 'user')}
|
|
40
|
+
className="self-start inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-150 hover:opacity-80 cursor-pointer"
|
|
41
|
+
style={{
|
|
42
|
+
background: 'var(--amber-dim)',
|
|
43
|
+
color: 'var(--amber)',
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{tryItLabel} →
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|