@geminilight/mindos 0.5.70 → 0.6.1
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 +124 -92
- package/app/app/api/mcp/agents/route.ts +53 -2
- package/app/app/api/mcp/status/route.ts +1 -1
- package/app/app/api/skills/route.ts +10 -114
- package/app/components/ActivityBar.tsx +3 -4
- package/app/components/CreateSpaceModal.tsx +31 -6
- package/app/components/FileTree.tsx +33 -2
- package/app/components/GuideCard.tsx +197 -131
- package/app/components/HomeContent.tsx +85 -18
- package/app/components/SidebarLayout.tsx +13 -0
- package/app/components/SpaceInitToast.tsx +173 -0
- package/app/components/agents/AgentDetailContent.tsx +32 -17
- package/app/components/agents/AgentsContentPage.tsx +2 -1
- package/app/components/agents/AgentsOverviewSection.tsx +1 -14
- package/app/components/agents/agents-content-model.ts +16 -8
- package/app/components/ask/AskContent.tsx +137 -50
- package/app/components/ask/MentionPopover.tsx +16 -8
- package/app/components/ask/SlashCommandPopover.tsx +62 -0
- package/app/components/settings/KnowledgeTab.tsx +61 -0
- package/app/components/walkthrough/steps.ts +11 -6
- package/app/hooks/useMention.ts +14 -6
- package/app/hooks/useSlashCommand.ts +114 -0
- package/app/lib/actions.ts +79 -2
- package/app/lib/agent/index.ts +1 -1
- package/app/lib/agent/prompt.ts +2 -0
- package/app/lib/agent/tools.ts +106 -0
- package/app/lib/core/create-space.ts +11 -4
- package/app/lib/core/index.ts +1 -1
- package/app/lib/i18n-en.ts +51 -46
- package/app/lib/i18n-zh.ts +50 -45
- package/app/lib/mcp-agents.ts +8 -0
- package/app/lib/pdf-extract.ts +33 -0
- package/app/lib/pi-integration/extensions.ts +68 -0
- package/app/lib/pi-integration/mcporter.ts +219 -0
- package/app/lib/pi-integration/session-store.ts +62 -0
- package/app/lib/pi-integration/skills.ts +116 -0
- package/app/lib/settings.ts +1 -1
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +1 -1
- package/app/package.json +2 -0
- package/mcp/src/index.ts +29 -0
- package/package.json +1 -1
|
@@ -1,37 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
-
import { X, Sparkles,
|
|
4
|
+
import { X, Sparkles, Upload, MessageCircle, ExternalLink, Check, ChevronRight, Copy } from 'lucide-react';
|
|
5
5
|
import Link from 'next/link';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { openAskModal } from '@/hooks/useAskModal';
|
|
8
|
-
import { extractEmoji, stripEmoji } from '@/lib/utils';
|
|
9
8
|
import { walkthroughSteps } from './walkthrough/steps';
|
|
10
9
|
import type { GuideState } from '@/lib/settings';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
interface RecentFile {
|
|
14
|
-
path: string;
|
|
15
|
-
mtime: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface GuideCardProps {
|
|
19
|
-
/** Called when user clicks a file/dir to open it in FileView */
|
|
20
|
-
onNavigate?: (path: string) => void;
|
|
21
|
-
/** Existing spaces for dynamic directory listing */
|
|
22
|
-
spaces?: SpaceInfo[];
|
|
23
|
-
/** Recent files for empty-template fallback */
|
|
24
|
-
recentFiles?: RecentFile[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }: GuideCardProps) {
|
|
10
|
+
export default function GuideCard() {
|
|
28
11
|
const { t } = useLocale();
|
|
29
12
|
const g = t.guide;
|
|
30
13
|
|
|
31
14
|
const [guideState, setGuideState] = useState<GuideState | null>(null);
|
|
32
|
-
const [expanded, setExpanded] = useState<'
|
|
15
|
+
const [expanded, setExpanded] = useState<'import' | 'ai' | 'agent' | null>(null);
|
|
33
16
|
const [isFirstVisit, setIsFirstVisit] = useState(false);
|
|
34
|
-
const [
|
|
17
|
+
const [copied, setCopied] = useState(false);
|
|
18
|
+
const [step3Done, setStep3Done] = useState(false);
|
|
35
19
|
|
|
36
20
|
// Fetch guide state from backend
|
|
37
21
|
const fetchGuideState = useCallback(() => {
|
|
@@ -41,7 +25,6 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
|
|
|
41
25
|
const gs = data.guideState;
|
|
42
26
|
if (gs?.active && !gs.dismissed) {
|
|
43
27
|
setGuideState(gs);
|
|
44
|
-
if (gs.step1Done) setBrowsedCount(1);
|
|
45
28
|
} else {
|
|
46
29
|
setGuideState(null);
|
|
47
30
|
}
|
|
@@ -65,12 +48,6 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
|
|
|
65
48
|
};
|
|
66
49
|
}, [fetchGuideState]);
|
|
67
50
|
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
if (isFirstVisit && guideState && !guideState.step1Done) {
|
|
70
|
-
setExpanded('kb');
|
|
71
|
-
}
|
|
72
|
-
}, [isFirstVisit, guideState]);
|
|
73
|
-
|
|
74
51
|
const patchGuide = useCallback((patch: Partial<GuideState>) => {
|
|
75
52
|
setGuideState(prev => prev ? { ...prev, ...patch } : prev);
|
|
76
53
|
fetch('/api/setup', {
|
|
@@ -85,21 +62,27 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
|
|
|
85
62
|
setGuideState(null);
|
|
86
63
|
}, [patchGuide]);
|
|
87
64
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
setTimeout(() => setExpanded(null), 300);
|
|
94
|
-
}
|
|
95
|
-
}, [browsedCount, patchGuide, onNavigate]);
|
|
65
|
+
// ── Step 1: Import ──
|
|
66
|
+
|
|
67
|
+
const handleImportClick = useCallback(() => {
|
|
68
|
+
window.dispatchEvent(new CustomEvent('mindos:open-import'));
|
|
69
|
+
}, []);
|
|
96
70
|
|
|
97
|
-
const
|
|
98
|
-
setBrowsedCount(1);
|
|
71
|
+
const handleSkipImport = useCallback(() => {
|
|
99
72
|
patchGuide({ step1Done: true });
|
|
100
73
|
setExpanded(null);
|
|
101
74
|
}, [patchGuide]);
|
|
102
75
|
|
|
76
|
+
// Auto-mark step 1 done when files change (import completes, space created, etc.)
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!guideState || guideState.step1Done) return;
|
|
79
|
+
const handler = () => patchGuide({ step1Done: true });
|
|
80
|
+
window.addEventListener('mindos:files-changed', handler);
|
|
81
|
+
return () => window.removeEventListener('mindos:files-changed', handler);
|
|
82
|
+
}, [guideState, guideState?.step1Done, patchGuide]);
|
|
83
|
+
|
|
84
|
+
// ── Step 2: AI verify ──
|
|
85
|
+
|
|
103
86
|
const handleStartAI = useCallback(() => {
|
|
104
87
|
const gs = guideState;
|
|
105
88
|
const isEmpty = gs?.template === 'empty';
|
|
@@ -107,6 +90,26 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
|
|
|
107
90
|
openAskModal(prompt, 'guide');
|
|
108
91
|
}, [guideState, g]);
|
|
109
92
|
|
|
93
|
+
// ── Step 3: Cross-agent copy ──
|
|
94
|
+
|
|
95
|
+
const handleCopyPrompt = useCallback(async () => {
|
|
96
|
+
try {
|
|
97
|
+
await navigator.clipboard.writeText(g.agent.copyPrompt);
|
|
98
|
+
setCopied(true);
|
|
99
|
+
setTimeout(() => setCopied(false), 2000);
|
|
100
|
+
} catch {
|
|
101
|
+
// Fallback: no clipboard API
|
|
102
|
+
}
|
|
103
|
+
}, [g]);
|
|
104
|
+
|
|
105
|
+
const handleStep3Done = useCallback(() => {
|
|
106
|
+
setStep3Done(true);
|
|
107
|
+
setExpanded(null);
|
|
108
|
+
patchGuide({ nextStepIndex: 0 });
|
|
109
|
+
}, [patchGuide]);
|
|
110
|
+
|
|
111
|
+
// ── Next-step prompts ──
|
|
112
|
+
|
|
110
113
|
const handleNextStepClick = useCallback(() => {
|
|
111
114
|
if (!guideState) return;
|
|
112
115
|
const idx = guideState.nextStepIndex;
|
|
@@ -117,25 +120,47 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
|
|
|
117
120
|
}
|
|
118
121
|
}, [guideState, g, patchGuide]);
|
|
119
122
|
|
|
120
|
-
|
|
121
|
-
// Open settings directly to the Sync tab (SidebarLayout listens for this event)
|
|
122
|
-
window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'sync' } }));
|
|
123
|
-
}, []);
|
|
123
|
+
// ── Auto-expand on step transitions ──
|
|
124
124
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const allNextDone_ = allDone_ && nextIdx_ >= g.done.steps.length;
|
|
125
|
+
// First visit: expand step 1
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (isFirstVisit && guideState && !guideState.step1Done) {
|
|
128
|
+
setExpanded('import');
|
|
129
|
+
}
|
|
130
|
+
}, [isFirstVisit, guideState]);
|
|
132
131
|
|
|
132
|
+
// Step transition: auto-expand next step
|
|
133
|
+
const prevStepRef = useRef({ s1: false, s2: false });
|
|
133
134
|
useEffect(() => {
|
|
134
|
-
if (
|
|
135
|
+
if (!guideState) return;
|
|
136
|
+
const prev = prevStepRef.current;
|
|
137
|
+
if (guideState.step1Done && !prev.s1) {
|
|
138
|
+
setExpanded('ai');
|
|
139
|
+
prev.s1 = true;
|
|
140
|
+
}
|
|
141
|
+
if (guideState.askedAI && !prev.s2) {
|
|
142
|
+
setExpanded('agent');
|
|
143
|
+
prev.s2 = true;
|
|
144
|
+
}
|
|
145
|
+
}, [guideState?.step1Done, guideState?.askedAI, guideState]);
|
|
146
|
+
|
|
147
|
+
// ── Auto-dismiss final state ──
|
|
148
|
+
|
|
149
|
+
const step1Done = guideState?.step1Done ?? false;
|
|
150
|
+
const step2Done = guideState?.askedAI ?? false;
|
|
151
|
+
const nextIdx = guideState?.nextStepIndex ?? 0;
|
|
152
|
+
const allCoreDone = step1Done && step2Done && step3Done;
|
|
153
|
+
const allNextDone = allCoreDone && nextIdx >= g.done.steps.length;
|
|
154
|
+
|
|
155
|
+
const autoDismissRef = useRef<ReturnType<typeof setTimeout>>(null);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (allNextDone) {
|
|
135
158
|
autoDismissRef.current = setTimeout(() => handleDismiss(), 8000);
|
|
136
159
|
}
|
|
137
160
|
return () => { if (autoDismissRef.current) clearTimeout(autoDismissRef.current); };
|
|
138
|
-
}, [
|
|
161
|
+
}, [allNextDone, handleDismiss]);
|
|
162
|
+
|
|
163
|
+
// ── Render guards ──
|
|
139
164
|
|
|
140
165
|
if (!guideState) return null;
|
|
141
166
|
|
|
@@ -145,21 +170,18 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
|
|
|
145
170
|
&& !guideState.walkthroughDismissed;
|
|
146
171
|
if (walkthroughActive) return null;
|
|
147
172
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const nextIdx = guideState.nextStepIndex;
|
|
152
|
-
const nextSteps = g.done.steps;
|
|
153
|
-
const allNextDone = nextIdx >= nextSteps.length;
|
|
154
|
-
const isEmptyTemplate = guideState.template === 'empty';
|
|
173
|
+
// If both steps 1+2 were already done on load (e.g. page refresh), step3Done is local
|
|
174
|
+
// and starts false — this lets the user see step 3 again, which is useful.
|
|
175
|
+
const showStep3 = step1Done && step2Done && !step3Done;
|
|
155
176
|
|
|
156
|
-
//
|
|
157
|
-
|
|
177
|
+
// ── Final state: all next-steps done ──
|
|
178
|
+
|
|
179
|
+
if (allCoreDone && allNextDone) {
|
|
158
180
|
return (
|
|
159
181
|
<div className="mb-6 rounded-xl border border-[var(--amber)] px-5 py-4 flex items-center gap-3 animate-in fade-in duration-300 bg-[var(--amber-subtle)]">
|
|
160
182
|
<Sparkles size={16} className="animate-spin-slow text-[var(--amber)]" />
|
|
161
183
|
<span className="text-sm font-semibold flex-1 text-foreground">
|
|
162
|
-
|
|
184
|
+
{g.done.titleFinal}
|
|
163
185
|
</span>
|
|
164
186
|
<Link
|
|
165
187
|
href="/explore"
|
|
@@ -174,15 +196,16 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
|
|
|
174
196
|
);
|
|
175
197
|
}
|
|
176
198
|
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
199
|
+
// ── Done state: next-step prompts ──
|
|
200
|
+
|
|
201
|
+
if (allCoreDone) {
|
|
202
|
+
const step = g.done.steps[nextIdx];
|
|
180
203
|
return (
|
|
181
204
|
<div className="mb-6 rounded-xl border border-[var(--amber)] px-5 py-4 animate-in fade-in duration-300 bg-[var(--amber-subtle)]">
|
|
182
205
|
<div className="flex items-center gap-3">
|
|
183
206
|
<Sparkles size={16} className="text-[var(--amber)]" />
|
|
184
207
|
<span className="text-sm font-semibold flex-1 text-foreground">
|
|
185
|
-
|
|
208
|
+
{g.done.title}
|
|
186
209
|
</span>
|
|
187
210
|
<button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground">
|
|
188
211
|
<X size={14} />
|
|
@@ -201,7 +224,8 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
|
|
|
201
224
|
);
|
|
202
225
|
}
|
|
203
226
|
|
|
204
|
-
// Main guide card with 3
|
|
227
|
+
// ── Main guide card with 3 steps ──
|
|
228
|
+
|
|
205
229
|
return (
|
|
206
230
|
<div className="mb-6 rounded-xl border border-[var(--amber)] overflow-hidden bg-[var(--amber-subtle)]">
|
|
207
231
|
|
|
@@ -218,65 +242,114 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
|
|
|
218
242
|
|
|
219
243
|
{/* Task cards */}
|
|
220
244
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 px-5 py-3">
|
|
221
|
-
<TaskCard
|
|
222
|
-
|
|
223
|
-
|
|
245
|
+
<TaskCard
|
|
246
|
+
icon={<Upload size={16} />}
|
|
247
|
+
title={g.import.title}
|
|
248
|
+
cta={g.import.cta}
|
|
249
|
+
done={step1Done}
|
|
250
|
+
active={expanded === 'import'}
|
|
251
|
+
onClick={() => !step1Done ? setExpanded(expanded === 'import' ? null : 'import') : null}
|
|
252
|
+
/>
|
|
253
|
+
<TaskCard
|
|
254
|
+
icon={<MessageCircle size={16} />}
|
|
255
|
+
title={g.ai.title}
|
|
256
|
+
cta={g.ai.cta}
|
|
257
|
+
done={step2Done}
|
|
258
|
+
active={expanded === 'ai'}
|
|
259
|
+
dimmed={!step1Done}
|
|
260
|
+
onClick={() => {
|
|
261
|
+
if (step2Done || !step1Done) return;
|
|
262
|
+
setExpanded(expanded === 'ai' ? null : 'ai');
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
<TaskCard
|
|
266
|
+
icon={<ExternalLink size={16} />}
|
|
267
|
+
title={g.agent.title}
|
|
268
|
+
cta={g.agent.cta}
|
|
269
|
+
done={step3Done}
|
|
270
|
+
active={expanded === 'agent'}
|
|
271
|
+
dimmed={!step1Done || !step2Done}
|
|
272
|
+
onClick={() => {
|
|
273
|
+
if (!step1Done || !step2Done) return;
|
|
274
|
+
setExpanded(expanded === 'agent' ? null : 'agent');
|
|
275
|
+
}}
|
|
276
|
+
/>
|
|
224
277
|
</div>
|
|
225
278
|
|
|
226
|
-
{/*
|
|
227
|
-
{expanded === '
|
|
279
|
+
{/* ── Step 1 expanded: Import files ── */}
|
|
280
|
+
{expanded === 'import' && !step1Done && (
|
|
228
281
|
<div className="px-5 pb-4 animate-in slide-in-from-top-2 duration-200">
|
|
229
282
|
<div className="rounded-lg border border-border bg-card p-4">
|
|
230
|
-
<p className="text-xs
|
|
231
|
-
{
|
|
283
|
+
<p className="text-xs text-muted-foreground mb-3">
|
|
284
|
+
{g.import.desc}
|
|
232
285
|
</p>
|
|
286
|
+
<div className="flex items-center justify-between">
|
|
287
|
+
<button
|
|
288
|
+
onClick={handleImportClick}
|
|
289
|
+
className="text-xs font-medium px-4 py-2 rounded-lg transition-all hover:opacity-90"
|
|
290
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
291
|
+
>
|
|
292
|
+
{g.import.button}
|
|
293
|
+
</button>
|
|
294
|
+
<button
|
|
295
|
+
onClick={handleSkipImport}
|
|
296
|
+
className="text-xs px-3 py-1 rounded-lg text-muted-foreground transition-colors hover:bg-muted"
|
|
297
|
+
>
|
|
298
|
+
{g.skip}
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
233
304
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
<button
|
|
279
|
-
|
|
305
|
+
{/* ── Step 2 expanded: AI verification ── */}
|
|
306
|
+
{expanded === 'ai' && step1Done && !step2Done && (
|
|
307
|
+
<div className="px-5 pb-4 animate-in slide-in-from-top-2 duration-200">
|
|
308
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
309
|
+
<p className="text-xs text-muted-foreground mb-3">
|
|
310
|
+
{g.ai.desc}
|
|
311
|
+
</p>
|
|
312
|
+
<button
|
|
313
|
+
onClick={handleStartAI}
|
|
314
|
+
className="text-xs font-medium px-4 py-2 rounded-lg transition-all hover:opacity-90"
|
|
315
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
316
|
+
>
|
|
317
|
+
{g.ai.cta} →
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
|
|
323
|
+
{/* ── Step 3 expanded: Cross-agent prompt ── */}
|
|
324
|
+
{expanded === 'agent' && showStep3 && (
|
|
325
|
+
<div className="px-5 pb-4 animate-in slide-in-from-top-2 duration-200">
|
|
326
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
327
|
+
<p className="text-xs text-muted-foreground mb-3">
|
|
328
|
+
{g.agent.desc}
|
|
329
|
+
</p>
|
|
330
|
+
<div className="relative rounded-lg border border-border bg-muted/50 p-3 pr-16">
|
|
331
|
+
<p className="text-xs font-mono text-foreground leading-relaxed whitespace-pre-wrap">
|
|
332
|
+
{g.agent.copyPrompt}
|
|
333
|
+
</p>
|
|
334
|
+
<button
|
|
335
|
+
onClick={handleCopyPrompt}
|
|
336
|
+
className={`
|
|
337
|
+
absolute top-2 right-2 flex items-center gap-1 text-2xs font-medium
|
|
338
|
+
px-2.5 py-1.5 rounded-md border transition-all
|
|
339
|
+
${copied
|
|
340
|
+
? 'border-success/30 bg-success/10 text-success'
|
|
341
|
+
: 'border-border bg-card text-muted-foreground hover:text-foreground hover:border-[var(--amber)]/30'}
|
|
342
|
+
`}
|
|
343
|
+
>
|
|
344
|
+
{copied ? <Check size={11} /> : <Copy size={11} />}
|
|
345
|
+
{copied ? g.agent.copied : g.agent.copy}
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
<div className="flex items-center justify-end mt-3 pt-3 border-t border-border">
|
|
349
|
+
<button
|
|
350
|
+
onClick={handleStep3Done}
|
|
351
|
+
className="text-xs px-3 py-1 rounded-lg text-muted-foreground transition-colors hover:bg-muted"
|
|
352
|
+
>
|
|
280
353
|
{g.skip}
|
|
281
354
|
</button>
|
|
282
355
|
</div>
|
|
@@ -287,25 +360,23 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
|
|
|
287
360
|
);
|
|
288
361
|
}
|
|
289
362
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
function TaskCard({ icon, title, cta, done, active, optional, onClick }: {
|
|
363
|
+
function TaskCard({ icon, title, cta, done, active, dimmed, onClick }: {
|
|
293
364
|
icon: React.ReactNode;
|
|
294
365
|
title: string;
|
|
295
366
|
cta: string;
|
|
296
367
|
done: boolean;
|
|
297
368
|
active: boolean;
|
|
298
|
-
|
|
369
|
+
dimmed?: boolean;
|
|
299
370
|
onClick: () => void;
|
|
300
371
|
}) {
|
|
301
372
|
return (
|
|
302
373
|
<button
|
|
303
374
|
onClick={onClick}
|
|
304
|
-
disabled={done}
|
|
375
|
+
disabled={done || dimmed}
|
|
305
376
|
className={`
|
|
306
377
|
flex flex-col items-center gap-1.5 px-3 py-3 rounded-lg border text-center
|
|
307
378
|
transition-all duration-150
|
|
308
|
-
${done ? 'opacity-60' : 'hover:border-[var(--amber)]/30 hover:bg-muted/50 cursor-pointer'}
|
|
379
|
+
${done ? 'opacity-60' : dimmed ? 'opacity-40 cursor-default' : 'hover:border-[var(--amber)]/30 hover:bg-muted/50 cursor-pointer'}
|
|
309
380
|
${active ? 'border-[var(--amber)]/40 bg-muted/50' : ''}
|
|
310
381
|
${done || active ? 'border-[var(--amber)]' : 'border-border'}
|
|
311
382
|
`}
|
|
@@ -316,12 +387,7 @@ function TaskCard({ icon, title, cta, done, active, optional, onClick }: {
|
|
|
316
387
|
<span className="text-xs font-medium text-foreground">
|
|
317
388
|
{title}
|
|
318
389
|
</span>
|
|
319
|
-
{
|
|
320
|
-
<span className="text-2xs px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
|
|
321
|
-
{optional}
|
|
322
|
-
</span>
|
|
323
|
-
)}
|
|
324
|
-
{!done && !optional && (
|
|
390
|
+
{!done && !dimmed && (
|
|
325
391
|
<span className="text-2xs text-[var(--amber)]">
|
|
326
392
|
{cta} →
|
|
327
393
|
</span>
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { FileText, Table, Clock, Sparkles, ArrowRight, FilePlus, Search, ChevronDown, Compass, Folder, Puzzle, Brain, Plus } from 'lucide-react';
|
|
5
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import { FileText, Table, Clock, Sparkles, ArrowRight, FilePlus, Search, ChevronDown, Compass, Folder, Puzzle, Brain, Plus, Trash2, Check, Loader2, X, FolderInput } from 'lucide-react';
|
|
5
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { encodePath, relativeTime, extractEmoji, stripEmoji } from '@/lib/utils';
|
|
8
8
|
import { getAllRenderers, getPluginRenderers } from '@/lib/renderers/registry';
|
|
9
9
|
import OnboardingView from './OnboardingView';
|
|
10
10
|
import GuideCard from './GuideCard';
|
|
11
11
|
import CreateSpaceModal from './CreateSpaceModal';
|
|
12
|
+
import { scanExampleFilesAction, cleanupExamplesAction } from '@/lib/actions';
|
|
12
13
|
import type { SpaceInfo } from '@/app/page';
|
|
13
14
|
|
|
14
15
|
interface RecentFile {
|
|
@@ -131,11 +132,8 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
|
|
|
131
132
|
|
|
132
133
|
return (
|
|
133
134
|
<div className="content-width px-4 md:px-6 py-8 md:py-12">
|
|
134
|
-
<GuideCard
|
|
135
|
-
|
|
136
|
-
spaces={spaceList}
|
|
137
|
-
recentFiles={recent.slice(0, 5)}
|
|
138
|
-
/>
|
|
135
|
+
<GuideCard />
|
|
136
|
+
<ExampleCleanupBanner />
|
|
139
137
|
|
|
140
138
|
{/* ── Hero ── */}
|
|
141
139
|
<div className="mb-10">
|
|
@@ -180,25 +178,32 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
|
|
|
180
178
|
|
|
181
179
|
{/* Quick Actions */}
|
|
182
180
|
<div className="flex flex-wrap gap-2.5 mt-4 pl-4">
|
|
181
|
+
<button
|
|
182
|
+
onClick={() => window.dispatchEvent(new CustomEvent('mindos:open-import'))}
|
|
183
|
+
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 bg-[var(--amber-dim)] text-[var(--amber)]"
|
|
184
|
+
>
|
|
185
|
+
<FolderInput size={14} />
|
|
186
|
+
<span>{t.fileTree.importFile}</span>
|
|
187
|
+
</button>
|
|
188
|
+
<Link
|
|
189
|
+
href="/view/Untitled.md"
|
|
190
|
+
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors bg-muted text-muted-foreground"
|
|
191
|
+
>
|
|
192
|
+
<FilePlus size={14} />
|
|
193
|
+
<span>{t.home.newNote}</span>
|
|
194
|
+
</Link>
|
|
183
195
|
{lastFile && (
|
|
184
196
|
<Link
|
|
185
197
|
href={`/view/${encodePath(lastFile.path)}`}
|
|
186
|
-
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-
|
|
198
|
+
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors text-muted-foreground hover:text-foreground"
|
|
187
199
|
>
|
|
188
200
|
<ArrowRight size={14} />
|
|
189
201
|
<span>{t.home.continueEditing}</span>
|
|
190
|
-
<span className="text-xs opacity-
|
|
202
|
+
<span className="text-xs opacity-50 truncate max-w-[140px]" suppressHydrationWarning>
|
|
191
203
|
{lastFile.path.split('/').pop()}
|
|
192
204
|
</span>
|
|
193
205
|
</Link>
|
|
194
206
|
)}
|
|
195
|
-
<Link
|
|
196
|
-
href="/view/Untitled.md"
|
|
197
|
-
className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors bg-muted text-muted-foreground"
|
|
198
|
-
>
|
|
199
|
-
<FilePlus size={14} />
|
|
200
|
-
<span>{t.home.newNote}</span>
|
|
201
|
-
</Link>
|
|
202
207
|
<Link
|
|
203
208
|
href="/explore"
|
|
204
209
|
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 text-[var(--amber)]"
|
|
@@ -229,10 +234,10 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
|
|
|
229
234
|
<Link
|
|
230
235
|
key={s.name}
|
|
231
236
|
href={`/view/${encodePath(s.path)}`}
|
|
232
|
-
className={`flex items-start gap-3 px-3.5 py-3 rounded-xl border transition-all duration-150
|
|
237
|
+
className={`flex items-start gap-3 px-3.5 py-3 rounded-xl border transition-all duration-150 ${
|
|
233
238
|
isEmpty
|
|
234
239
|
? 'border-dashed border-border/50 opacity-50 hover:opacity-70'
|
|
235
|
-
: 'border-border hover:border-[var(--amber)]/30 hover:
|
|
240
|
+
: 'border-border hover:border-[var(--amber)]/30 hover:shadow-sm'
|
|
236
241
|
}`}
|
|
237
242
|
>
|
|
238
243
|
{emoji ? (
|
|
@@ -517,3 +522,65 @@ function CreateSpaceButton({ t }: { t: ReturnType<typeof useLocale>['t'] }) {
|
|
|
517
522
|
);
|
|
518
523
|
}
|
|
519
524
|
|
|
525
|
+
/* ── Example files cleanup banner ── */
|
|
526
|
+
function ExampleCleanupBanner() {
|
|
527
|
+
const { t } = useLocale();
|
|
528
|
+
const [count, setCount] = useState<number | null>(null);
|
|
529
|
+
const [cleaning, setCleaning] = useState(false);
|
|
530
|
+
const [done, setDone] = useState(false);
|
|
531
|
+
const [dismissed, setDismissed] = useState(false);
|
|
532
|
+
|
|
533
|
+
useEffect(() => {
|
|
534
|
+
scanExampleFilesAction().then(r => {
|
|
535
|
+
if (r.files.length > 0) setCount(r.files.length);
|
|
536
|
+
}).catch(() => {});
|
|
537
|
+
}, []);
|
|
538
|
+
|
|
539
|
+
const handleCleanup = useCallback(async () => {
|
|
540
|
+
if (count === null) return;
|
|
541
|
+
setCleaning(true);
|
|
542
|
+
try {
|
|
543
|
+
const r = await cleanupExamplesAction();
|
|
544
|
+
if (r.success) {
|
|
545
|
+
setDone(true);
|
|
546
|
+
setTimeout(() => setDismissed(true), 2500);
|
|
547
|
+
}
|
|
548
|
+
} catch { /* silent — banner stays, user can retry */ }
|
|
549
|
+
setCleaning(false);
|
|
550
|
+
}, [count]);
|
|
551
|
+
|
|
552
|
+
if (dismissed || count === null || count === 0) return null;
|
|
553
|
+
|
|
554
|
+
if (done) {
|
|
555
|
+
return (
|
|
556
|
+
<div className="mb-6 flex items-center gap-2.5 px-4 py-3 rounded-xl border border-success/30 bg-success/5 animate-in fade-in duration-300">
|
|
557
|
+
<Check size={14} className="text-success shrink-0" />
|
|
558
|
+
<span className="text-xs text-success">{t.home.cleanupExamplesDone}</span>
|
|
559
|
+
</div>
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return (
|
|
564
|
+
<div className="mb-6 flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-muted/30 animate-in fade-in duration-300">
|
|
565
|
+
<span className="text-sm leading-none shrink-0">🧪</span>
|
|
566
|
+
<span className="text-xs text-muted-foreground flex-1">
|
|
567
|
+
{t.home.cleanupExamples(count)}
|
|
568
|
+
</span>
|
|
569
|
+
<button
|
|
570
|
+
onClick={handleCleanup}
|
|
571
|
+
disabled={cleaning}
|
|
572
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors shrink-0 disabled:opacity-50 bg-[var(--amber-dim)] text-[var(--amber)] hover:opacity-80"
|
|
573
|
+
>
|
|
574
|
+
{cleaning ? <Loader2 size={11} className="animate-spin" /> : <Trash2 size={11} />}
|
|
575
|
+
{t.home.cleanupExamplesButton}
|
|
576
|
+
</button>
|
|
577
|
+
<button
|
|
578
|
+
onClick={() => setDismissed(true)}
|
|
579
|
+
className="p-1 rounded hover:bg-muted transition-colors text-muted-foreground shrink-0"
|
|
580
|
+
>
|
|
581
|
+
<X size={12} />
|
|
582
|
+
</button>
|
|
583
|
+
</div>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
@@ -26,6 +26,7 @@ import AskModal from './AskModal';
|
|
|
26
26
|
import SettingsModal from './SettingsModal';
|
|
27
27
|
import KeyboardShortcuts from './KeyboardShortcuts';
|
|
28
28
|
import ChangesBanner from './changes/ChangesBanner';
|
|
29
|
+
import SpaceInitToast from './SpaceInitToast';
|
|
29
30
|
import { MobileSyncDot, useSyncStatus } from './SyncStatusBar';
|
|
30
31
|
import { FileNode } from '@/lib/types';
|
|
31
32
|
import { useLocale } from '@/lib/LocaleContext';
|
|
@@ -127,6 +128,16 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
127
128
|
return () => window.removeEventListener('mindos:open-import', handler);
|
|
128
129
|
}, [handleOpenImport]);
|
|
129
130
|
|
|
131
|
+
// Listen for cross-component "open panel" events (e.g. GuideCard → Agents)
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
const handler = (e: Event) => {
|
|
134
|
+
const panel = (e as CustomEvent).detail?.panel;
|
|
135
|
+
if (panel) lp.setActivePanel(panel);
|
|
136
|
+
};
|
|
137
|
+
window.addEventListener('mindos:open-panel', handler);
|
|
138
|
+
return () => window.removeEventListener('mindos:open-panel', handler);
|
|
139
|
+
}, [lp]);
|
|
140
|
+
|
|
130
141
|
// GuideCard first message handler
|
|
131
142
|
const handleFirstMessage = useCallback(() => {
|
|
132
143
|
const notifyGuide = () => window.dispatchEvent(new Event('guide-state-updated'));
|
|
@@ -459,6 +470,8 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
|
|
|
459
470
|
{children}
|
|
460
471
|
</div>
|
|
461
472
|
|
|
473
|
+
<SpaceInitToast />
|
|
474
|
+
|
|
462
475
|
{/* Global drag overlay */}
|
|
463
476
|
{dragOverlay && !importModalOpen && (
|
|
464
477
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm transition-opacity duration-200">
|