@geminilight/mindos 0.6.30 → 0.6.31
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/README_zh.md +10 -4
- package/app/app/api/ask/route.ts +12 -7
- package/app/app/api/export/route.ts +105 -0
- package/app/app/globals.css +2 -2
- package/app/app/trash/page.tsx +7 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
- package/app/components/ExportModal.tsx +220 -0
- package/app/components/FileTree.tsx +22 -2
- package/app/components/HomeContent.tsx +91 -20
- package/app/components/MarkdownView.tsx +45 -10
- package/app/components/Sidebar.tsx +10 -1
- package/app/components/TrashPageClient.tsx +263 -0
- package/app/components/ask/ToolCallBlock.tsx +102 -18
- package/app/components/changes/ChangesContentPage.tsx +58 -14
- package/app/components/explore/ExploreContent.tsx +4 -7
- package/app/components/explore/UseCaseCard.tsx +18 -1
- package/app/components/explore/use-cases.generated.ts +76 -0
- package/app/components/explore/use-cases.yaml +185 -0
- package/app/components/panels/DiscoverPanel.tsx +1 -1
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +98 -91
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +82 -72
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +163 -120
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +61 -61
- package/app/components/renderers/workflow-yaml/execution.ts +64 -12
- package/app/components/renderers/workflow-yaml/selectors.tsx +64 -12
- package/app/components/settings/AiTab.tsx +191 -174
- package/app/components/settings/AppearanceTab.tsx +168 -77
- package/app/components/settings/KnowledgeTab.tsx +131 -136
- package/app/components/settings/McpTab.tsx +11 -11
- package/app/components/settings/Primitives.tsx +60 -0
- package/app/components/settings/SettingsContent.tsx +15 -8
- package/app/components/settings/SyncTab.tsx +12 -12
- package/app/components/settings/UninstallTab.tsx +8 -18
- package/app/components/settings/UpdateTab.tsx +82 -82
- package/app/components/settings/types.ts +17 -8
- package/app/lib/acp/session.ts +12 -3
- package/app/lib/actions.ts +57 -3
- package/app/lib/agent/stream-consumer.ts +18 -0
- package/app/lib/agent/tools.ts +56 -9
- package/app/lib/core/export.ts +116 -0
- package/app/lib/core/trash.ts +241 -0
- package/app/lib/fs.ts +47 -0
- package/app/lib/hooks/usePinnedFiles.ts +90 -0
- package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
- package/app/lib/i18n/index.ts +3 -0
- package/app/lib/i18n/modules/knowledge.ts +120 -6
- package/app/lib/i18n/modules/onboarding.ts +2 -134
- package/app/lib/i18n/modules/settings.ts +12 -0
- package/app/package.json +8 -2
- package/app/scripts/generate-explore.ts +145 -0
- package/package.json +1 -1
- package/app/components/explore/use-cases.ts +0 -58
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useRef } from 'react';
|
|
4
|
+
import { X, Download, FileText, Globe, Archive, Check, Loader2 } from 'lucide-react';
|
|
5
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
+
import { toast } from '@/lib/toast';
|
|
7
|
+
|
|
8
|
+
type ExportFormat = 'md' | 'html' | 'zip' | 'zip-html';
|
|
9
|
+
|
|
10
|
+
interface ExportModalProps {
|
|
11
|
+
open: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
filePath: string;
|
|
14
|
+
isDirectory: boolean;
|
|
15
|
+
fileName: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ExportModal({ open, onClose, filePath, isDirectory, fileName }: ExportModalProps) {
|
|
19
|
+
const { t } = useLocale();
|
|
20
|
+
const [format, setFormat] = useState<ExportFormat>(isDirectory ? 'zip' : 'md');
|
|
21
|
+
const [state, setState] = useState<'idle' | 'exporting' | 'done' | 'error'>('idle');
|
|
22
|
+
const [error, setError] = useState('');
|
|
23
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
24
|
+
|
|
25
|
+
const handleExport = useCallback(() => {
|
|
26
|
+
setState('exporting');
|
|
27
|
+
setError('');
|
|
28
|
+
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
abortRef.current = controller;
|
|
31
|
+
|
|
32
|
+
const url = `/api/export?path=${encodeURIComponent(filePath)}&format=${format}`;
|
|
33
|
+
|
|
34
|
+
fetch(url, { signal: controller.signal })
|
|
35
|
+
.then(res => {
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const ct = res.headers.get('content-type') ?? '';
|
|
38
|
+
if (ct.includes('json')) {
|
|
39
|
+
return res.json().then((data: { error?: string }) => { throw new Error(data.error || 'Export failed'); });
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Export failed (${res.status})`);
|
|
42
|
+
}
|
|
43
|
+
return res.blob().then(blob => ({ blob, res }));
|
|
44
|
+
})
|
|
45
|
+
.then(({ blob, res }) => {
|
|
46
|
+
const disposition = res.headers.get('Content-Disposition') ?? '';
|
|
47
|
+
const match = disposition.match(/filename="?([^"]+)"?/);
|
|
48
|
+
const downloadName = match ? decodeURIComponent(match[1]) : fileName;
|
|
49
|
+
|
|
50
|
+
const link = document.createElement('a');
|
|
51
|
+
link.href = URL.createObjectURL(blob);
|
|
52
|
+
link.download = downloadName;
|
|
53
|
+
document.body.appendChild(link);
|
|
54
|
+
link.click();
|
|
55
|
+
document.body.removeChild(link);
|
|
56
|
+
URL.revokeObjectURL(link.href);
|
|
57
|
+
|
|
58
|
+
setState('done');
|
|
59
|
+
toast.success(t.export?.downloaded ?? 'Downloaded');
|
|
60
|
+
})
|
|
61
|
+
.catch((err: Error) => {
|
|
62
|
+
if (err.name === 'AbortError') return; // user cancelled
|
|
63
|
+
setState('error');
|
|
64
|
+
setError(err.message ?? 'Export failed');
|
|
65
|
+
})
|
|
66
|
+
.finally(() => { abortRef.current = null; });
|
|
67
|
+
}, [filePath, format, fileName, t]);
|
|
68
|
+
|
|
69
|
+
const handleCancel = useCallback(() => {
|
|
70
|
+
if (abortRef.current) {
|
|
71
|
+
abortRef.current.abort();
|
|
72
|
+
abortRef.current = null;
|
|
73
|
+
}
|
|
74
|
+
setState('idle');
|
|
75
|
+
setError('');
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const handleRetry = useCallback(() => {
|
|
79
|
+
setState('idle');
|
|
80
|
+
setError('');
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const handleClose = useCallback(() => {
|
|
84
|
+
if (abortRef.current) abortRef.current.abort();
|
|
85
|
+
abortRef.current = null;
|
|
86
|
+
setState('idle');
|
|
87
|
+
setError('');
|
|
88
|
+
onClose();
|
|
89
|
+
}, [onClose]);
|
|
90
|
+
|
|
91
|
+
if (!open) return null;
|
|
92
|
+
|
|
93
|
+
const formats: { value: ExportFormat; label: string; desc: string; icon: React.ReactNode; disabled?: boolean }[] = isDirectory
|
|
94
|
+
? [
|
|
95
|
+
{ value: 'zip', label: t.export?.formatZipMd ?? 'Markdown ZIP', desc: t.export?.formatZipMdDesc ?? 'All files in original format', icon: <Archive size={14} /> },
|
|
96
|
+
{ value: 'zip-html', label: t.export?.formatZipHtml ?? 'HTML ZIP', desc: t.export?.formatZipHtmlDesc ?? 'All files as webpages', icon: <Globe size={14} /> },
|
|
97
|
+
]
|
|
98
|
+
: [
|
|
99
|
+
{ value: 'md', label: t.export?.formatMd ?? 'Markdown (.md)', desc: t.export?.formatMdDesc ?? 'Original format, editable', icon: <FileText size={14} /> },
|
|
100
|
+
{ value: 'html', label: t.export?.formatHtml ?? 'HTML (.html)', desc: t.export?.formatHtmlDesc ?? 'Static webpage, shareable', icon: <Globe size={14} /> },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" onClick={state === 'exporting' ? undefined : handleClose}>
|
|
105
|
+
<div
|
|
106
|
+
className="bg-card border border-border rounded-xl shadow-xl max-w-md w-full mx-4 animate-in fade-in-0 zoom-in-95 duration-200"
|
|
107
|
+
onClick={e => e.stopPropagation()}
|
|
108
|
+
>
|
|
109
|
+
{/* Header */}
|
|
110
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-border">
|
|
111
|
+
<div className="flex items-center gap-2">
|
|
112
|
+
<Download size={15} className="text-[var(--amber)]" />
|
|
113
|
+
<h3 className="text-sm font-semibold font-display">
|
|
114
|
+
{isDirectory ? (t.export?.exportSpace ?? 'Export Space') : (t.export?.exportFile ?? 'Export File')}
|
|
115
|
+
</h3>
|
|
116
|
+
</div>
|
|
117
|
+
<button onClick={handleClose} className="p-1 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
118
|
+
<X size={14} />
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Body */}
|
|
123
|
+
<div className="px-5 py-4">
|
|
124
|
+
{state === 'done' ? (
|
|
125
|
+
<div className="text-center py-6">
|
|
126
|
+
<div className="w-10 h-10 rounded-full bg-success/10 flex items-center justify-center mx-auto mb-3">
|
|
127
|
+
<Check size={20} className="text-success" />
|
|
128
|
+
</div>
|
|
129
|
+
<p className="text-sm font-medium font-display">{t.export?.done ?? 'Export Complete'}</p>
|
|
130
|
+
<p className="text-xs text-muted-foreground mt-1">{fileName}</p>
|
|
131
|
+
</div>
|
|
132
|
+
) : state === 'error' ? (
|
|
133
|
+
<div className="text-center py-6">
|
|
134
|
+
<p className="text-sm font-medium text-error">{t.export?.error ?? 'Export failed'}</p>
|
|
135
|
+
<p className="text-xs text-muted-foreground mt-1">{error}</p>
|
|
136
|
+
</div>
|
|
137
|
+
) : (
|
|
138
|
+
<>
|
|
139
|
+
<p className="text-xs text-muted-foreground mb-3 truncate" title={filePath}>{filePath}</p>
|
|
140
|
+
<p className="text-xs font-medium font-display text-foreground mb-2">{t.export?.chooseFormat ?? 'Choose format'}</p>
|
|
141
|
+
<div className="space-y-1.5">
|
|
142
|
+
{formats.map(f => (
|
|
143
|
+
<label
|
|
144
|
+
key={f.value}
|
|
145
|
+
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
|
|
146
|
+
format === f.value
|
|
147
|
+
? 'border-[var(--amber)]/40 bg-[var(--amber-dim)]'
|
|
148
|
+
: 'border-border hover:border-border/80 hover:bg-muted/30'
|
|
149
|
+
} ${f.disabled ? 'opacity-40 cursor-not-allowed' : ''}`}
|
|
150
|
+
>
|
|
151
|
+
<input
|
|
152
|
+
type="radio"
|
|
153
|
+
name="export-format"
|
|
154
|
+
value={f.value}
|
|
155
|
+
checked={format === f.value}
|
|
156
|
+
onChange={() => !f.disabled && setFormat(f.value)}
|
|
157
|
+
disabled={f.disabled}
|
|
158
|
+
className="sr-only"
|
|
159
|
+
/>
|
|
160
|
+
<span className={`mt-0.5 ${format === f.value ? 'text-[var(--amber)]' : 'text-muted-foreground'}`}>
|
|
161
|
+
{f.icon}
|
|
162
|
+
</span>
|
|
163
|
+
<div>
|
|
164
|
+
<span className="text-sm font-medium text-foreground">{f.label}</span>
|
|
165
|
+
<span className="text-xs text-muted-foreground block mt-0.5">{f.desc}</span>
|
|
166
|
+
</div>
|
|
167
|
+
</label>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Footer */}
|
|
175
|
+
<div className="flex items-center justify-end gap-2 px-5 py-3 border-t border-border">
|
|
176
|
+
{state === 'done' ? (
|
|
177
|
+
<>
|
|
178
|
+
<button onClick={handleRetry} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
179
|
+
{t.export?.downloadAgain ?? 'Download Again'}
|
|
180
|
+
</button>
|
|
181
|
+
<button onClick={handleClose} className="px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors">
|
|
182
|
+
{t.export?.cancel === 'Cancel' ? 'Done' : (t.export?.done ?? 'Done')}
|
|
183
|
+
</button>
|
|
184
|
+
</>
|
|
185
|
+
) : state === 'error' ? (
|
|
186
|
+
<>
|
|
187
|
+
<button onClick={handleClose} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
188
|
+
{t.export?.cancel ?? 'Cancel'}
|
|
189
|
+
</button>
|
|
190
|
+
<button onClick={handleRetry} className="px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors">
|
|
191
|
+
{t.export?.retry ?? 'Retry'}
|
|
192
|
+
</button>
|
|
193
|
+
</>
|
|
194
|
+
) : state === 'exporting' ? (
|
|
195
|
+
<>
|
|
196
|
+
<button onClick={handleCancel} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
197
|
+
{t.export?.cancel ?? 'Cancel'}
|
|
198
|
+
</button>
|
|
199
|
+
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium text-[var(--amber-text)] opacity-70">
|
|
200
|
+
<Loader2 size={12} className="animate-spin" /> {t.export?.exporting ?? 'Exporting...'}
|
|
201
|
+
</span>
|
|
202
|
+
</>
|
|
203
|
+
) : (
|
|
204
|
+
<>
|
|
205
|
+
<button onClick={handleClose} className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors">
|
|
206
|
+
{t.export?.cancel ?? 'Cancel'}
|
|
207
|
+
</button>
|
|
208
|
+
<button
|
|
209
|
+
onClick={handleExport}
|
|
210
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] hover:opacity-80 transition-colors"
|
|
211
|
+
>
|
|
212
|
+
<Download size={12} /> {t.export?.exportButton ?? 'Export'}
|
|
213
|
+
</button>
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
@@ -6,11 +6,12 @@ import { FileNode } from '@/lib/types';
|
|
|
6
6
|
import { encodePath } from '@/lib/utils';
|
|
7
7
|
import {
|
|
8
8
|
ChevronDown, FileText, Table, Folder, FolderOpen, Plus, Loader2,
|
|
9
|
-
Trash2, Pencil, Layers, ScrollText, FolderInput, Copy, MoreHorizontal,
|
|
9
|
+
Trash2, Pencil, Layers, ScrollText, FolderInput, Copy, MoreHorizontal, Star,
|
|
10
10
|
} from 'lucide-react';
|
|
11
11
|
import { createFileAction, deleteFileAction, renameFileAction, renameSpaceAction, deleteSpaceAction, convertToSpaceAction, deleteFolderAction } from '@/lib/actions';
|
|
12
12
|
import { useLocale } from '@/lib/LocaleContext';
|
|
13
13
|
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
14
|
+
import { usePinnedFiles } from '@/lib/hooks/usePinnedFiles';
|
|
14
15
|
|
|
15
16
|
function notifyFilesChanged() {
|
|
16
17
|
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
@@ -134,9 +135,15 @@ function SpaceContextMenu({ x, y, node, onClose, onRename, onImport, onDelete }:
|
|
|
134
135
|
}) {
|
|
135
136
|
const router = useRouter();
|
|
136
137
|
const { t } = useLocale();
|
|
138
|
+
const { isPinned, togglePin } = usePinnedFiles();
|
|
139
|
+
const pinned = isPinned(node.path);
|
|
137
140
|
|
|
138
141
|
return (
|
|
139
142
|
<ContextMenuShell x={x} y={y} onClose={onClose}>
|
|
143
|
+
<button className={MENU_ITEM} onClick={() => { togglePin(node.path); onClose(); }}>
|
|
144
|
+
<Star size={14} className={`shrink-0 ${pinned ? 'fill-[var(--amber)] text-[var(--amber)]' : ''}`} />
|
|
145
|
+
{pinned ? t.fileTree.removeFromFavorites : t.fileTree.pinToFavorites}
|
|
146
|
+
</button>
|
|
140
147
|
<button className={MENU_ITEM} onClick={() => { router.push(`/view/${encodePath(`${node.path}/INSTRUCTION.md`)}`); onClose(); }}>
|
|
141
148
|
<ScrollText size={14} className="shrink-0" /> {t.fileTree.editRules}
|
|
142
149
|
</button>
|
|
@@ -168,9 +175,15 @@ function FolderContextMenu({ x, y, node, onClose, onRename, onDelete }: {
|
|
|
168
175
|
const router = useRouter();
|
|
169
176
|
const { t } = useLocale();
|
|
170
177
|
const [isPending, startTransition] = useTransition();
|
|
178
|
+
const { isPinned, togglePin } = usePinnedFiles();
|
|
179
|
+
const pinned = isPinned(node.path);
|
|
171
180
|
|
|
172
181
|
return (
|
|
173
|
-
<ContextMenuShell x={x} y={y} onClose={onClose} menuHeight={
|
|
182
|
+
<ContextMenuShell x={x} y={y} onClose={onClose} menuHeight={180}>
|
|
183
|
+
<button className={MENU_ITEM} onClick={() => { togglePin(node.path); onClose(); }}>
|
|
184
|
+
<Star size={14} className={`shrink-0 ${pinned ? 'fill-[var(--amber)] text-[var(--amber)]' : ''}`} />
|
|
185
|
+
{pinned ? t.fileTree.removeFromFavorites : t.fileTree.pinToFavorites}
|
|
186
|
+
</button>
|
|
174
187
|
<button className={MENU_ITEM} disabled={isPending} onClick={() => {
|
|
175
188
|
startTransition(async () => {
|
|
176
189
|
const result = await convertToSpaceAction(node.path);
|
|
@@ -570,6 +583,8 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
570
583
|
const [, startDeleteTransition] = useTransition();
|
|
571
584
|
const renameRef = useRef<HTMLInputElement>(null);
|
|
572
585
|
const { t } = useLocale();
|
|
586
|
+
const { isPinned, togglePin } = usePinnedFiles();
|
|
587
|
+
const pinned = isPinned(node.path);
|
|
573
588
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
574
589
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
575
590
|
|
|
@@ -659,6 +674,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
659
674
|
>
|
|
660
675
|
{getIcon(node)}
|
|
661
676
|
<span className="truncate leading-5" suppressHydrationWarning>{node.name}</span>
|
|
677
|
+
{pinned && <Star size={10} className="shrink-0 fill-[var(--amber)] text-[var(--amber)] opacity-60" />}
|
|
662
678
|
</button>
|
|
663
679
|
<div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/file:flex items-center gap-0.5">
|
|
664
680
|
<button
|
|
@@ -684,6 +700,10 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
684
700
|
<button className={MENU_ITEM} onClick={() => { copyPathToClipboard(node.path); setContextMenu(null); }}>
|
|
685
701
|
<Copy size={14} className="shrink-0" /> {t.fileTree.copyPath}
|
|
686
702
|
</button>
|
|
703
|
+
<button className={MENU_ITEM} onClick={() => { togglePin(node.path); setContextMenu(null); }}>
|
|
704
|
+
<Star size={14} className={`shrink-0 ${pinned ? 'fill-[var(--amber)] text-[var(--amber)]' : ''}`} />
|
|
705
|
+
{pinned ? t.fileTree.removeFromFavorites : t.fileTree.pinToFavorites}
|
|
706
|
+
</button>
|
|
687
707
|
<button className={MENU_ITEM} onClick={(e) => { setContextMenu(null); startRename(e); }}>
|
|
688
708
|
<Pencil size={14} className="shrink-0" /> {t.fileTree.rename}
|
|
689
709
|
</button>
|
|
@@ -1,11 +1,12 @@
|
|
|
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, Trash2, Check, Loader2, X, FolderInput, Zap, History, SlidersHorizontal, ListTodo } from 'lucide-react';
|
|
4
|
+
import { FileText, Table, Clock, Sparkles, ArrowRight, FilePlus, Search, ChevronDown, Compass, Folder, Puzzle, Brain, Plus, Trash2, Check, Loader2, X, FolderInput, Zap, History, SlidersHorizontal, ListTodo, Star } from 'lucide-react';
|
|
5
5
|
import type { LucideIcon } from 'lucide-react';
|
|
6
6
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
7
7
|
import { useLocale } from '@/lib/LocaleContext';
|
|
8
8
|
import { encodePath, relativeTime, extractEmoji, stripEmoji } from '@/lib/utils';
|
|
9
|
+
import { usePinnedFiles } from '@/lib/hooks/usePinnedFiles';
|
|
9
10
|
import { getAllRenderers, getPluginRenderers } from '@/lib/renderers/registry';
|
|
10
11
|
import OnboardingView from './OnboardingView';
|
|
11
12
|
import GuideCard from './GuideCard';
|
|
@@ -252,14 +253,14 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
252
253
|
<ExampleCleanupBanner />
|
|
253
254
|
|
|
254
255
|
{/* ── Hero ── */}
|
|
255
|
-
<div className="mb-
|
|
256
|
+
<div className="mb-14">
|
|
256
257
|
<div className="flex items-center gap-2 mb-3">
|
|
257
|
-
<div className="w-1 h-
|
|
258
|
+
<div className="w-1 h-6 rounded-full bg-gradient-to-b from-[var(--amber)] to-[var(--amber)]/30" />
|
|
258
259
|
<h1 className="text-2xl font-semibold tracking-tight font-display text-foreground">
|
|
259
260
|
MindOS
|
|
260
261
|
</h1>
|
|
261
262
|
</div>
|
|
262
|
-
<p className="text-
|
|
263
|
+
<p className="text-base leading-relaxed mb-5 text-muted-foreground pl-4 max-w-md">
|
|
263
264
|
{t.app.tagline}
|
|
264
265
|
</p>
|
|
265
266
|
|
|
@@ -269,7 +270,7 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
269
270
|
onClick={triggerAsk}
|
|
270
271
|
title="⌘/"
|
|
271
272
|
data-walkthrough="ask-button"
|
|
272
|
-
className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card transition-all duration-150 hover:border-[var(--amber)]/50 hover:bg-[var(--amber-dim)]"
|
|
273
|
+
className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border border-border/60 shadow-sm bg-card transition-all duration-150 hover:border-[var(--amber)]/50 hover:bg-[var(--amber-dim)]"
|
|
273
274
|
>
|
|
274
275
|
<Sparkles size={15} className="shrink-0 text-[var(--amber)]" />
|
|
275
276
|
<span className="text-sm flex-1 text-left text-foreground">
|
|
@@ -293,7 +294,7 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
293
294
|
</div>
|
|
294
295
|
|
|
295
296
|
{/* Quick Actions */}
|
|
296
|
-
<div className="flex flex-wrap gap-
|
|
297
|
+
<div className="flex flex-wrap gap-3 mt-5 pl-4">
|
|
297
298
|
<button
|
|
298
299
|
onClick={() => window.dispatchEvent(new CustomEvent('mindos:open-import'))}
|
|
299
300
|
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-text)]"
|
|
@@ -331,8 +332,11 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
331
332
|
</div>
|
|
332
333
|
</div>
|
|
333
334
|
|
|
334
|
-
{/* ── Section
|
|
335
|
-
<
|
|
335
|
+
{/* ── Section: Pinned Files ── */}
|
|
336
|
+
<PinnedFilesSection formatTime={formatTime} />
|
|
337
|
+
|
|
338
|
+
{/* ── Section: Spaces ── */}
|
|
339
|
+
<section className="mb-12">
|
|
336
340
|
<SectionTitle
|
|
337
341
|
icon={<Brain size={13} />}
|
|
338
342
|
count={spaceList.length > 0 ? spaceList.length : undefined}
|
|
@@ -392,9 +396,9 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
392
396
|
)}
|
|
393
397
|
</section>
|
|
394
398
|
|
|
395
|
-
{/* ── Section
|
|
399
|
+
{/* ── Section: Tools ── */}
|
|
396
400
|
{builtinFeatures.length > 0 && (
|
|
397
|
-
<section className="mb-
|
|
401
|
+
<section className="mb-12">
|
|
398
402
|
<SectionTitle icon={<Zap size={13} />} count={builtinFeatures.length}>
|
|
399
403
|
{t.home.builtinFeatures}
|
|
400
404
|
</SectionTitle>
|
|
@@ -419,9 +423,9 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
419
423
|
</section>
|
|
420
424
|
)}
|
|
421
425
|
|
|
422
|
-
{/* ── Section
|
|
426
|
+
{/* ── Section: Extensions ── */}
|
|
423
427
|
{availablePlugins.length > 0 && (
|
|
424
|
-
<section className="mb-
|
|
428
|
+
<section className="mb-12">
|
|
425
429
|
<SectionTitle
|
|
426
430
|
icon={<Puzzle size={13} />}
|
|
427
431
|
count={availablePlugins.length}
|
|
@@ -446,7 +450,7 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
446
450
|
</section>
|
|
447
451
|
)}
|
|
448
452
|
|
|
449
|
-
{/* ── Section
|
|
453
|
+
{/* ── Section: Recently Edited ── */}
|
|
450
454
|
{recent.length > 0 && (
|
|
451
455
|
<section className="mb-12">
|
|
452
456
|
<SectionTitle
|
|
@@ -465,8 +469,30 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
465
469
|
{t.home.recentlyEdited}
|
|
466
470
|
</SectionTitle>
|
|
467
471
|
|
|
472
|
+
{/* Spotlight — latest file */}
|
|
473
|
+
<Link
|
|
474
|
+
href={`/view/${encodePath(recent[0].path)}`}
|
|
475
|
+
className="block mb-5 p-4 rounded-xl border border-border/50 bg-gradient-to-r from-[var(--amber-subtle)] to-transparent hover:border-[var(--amber)]/40 hover:shadow-sm transition-all group"
|
|
476
|
+
>
|
|
477
|
+
<div className="flex items-center gap-3">
|
|
478
|
+
<div className="w-10 h-10 rounded-lg bg-[var(--amber)]/10 flex items-center justify-center shrink-0">
|
|
479
|
+
{recent[0].path.endsWith('.csv')
|
|
480
|
+
? <Table size={18} className="text-[var(--amber)]" />
|
|
481
|
+
: <FileText size={18} className="text-[var(--amber)]" />}
|
|
482
|
+
</div>
|
|
483
|
+
<div className="flex-1 min-w-0">
|
|
484
|
+
<span className="text-base font-medium text-foreground block truncate">
|
|
485
|
+
{recent[0].path.split('/').pop()}
|
|
486
|
+
</span>
|
|
487
|
+
<span className="text-xs text-muted-foreground mt-0.5 block" suppressHydrationWarning>
|
|
488
|
+
{recent[0].path.split('/').slice(0, -1).join('/') || 'Root'} · {formatTime(recent[0].mtime)}
|
|
489
|
+
</span>
|
|
490
|
+
</div>
|
|
491
|
+
<ArrowRight size={16} className="text-muted-foreground group-hover:text-[var(--amber)] transition-colors shrink-0" />
|
|
492
|
+
</div>
|
|
493
|
+
</Link>
|
|
494
|
+
|
|
468
495
|
{groups.length > 0 ? (
|
|
469
|
-
/* Space-Grouped View */
|
|
470
496
|
<div className="flex flex-col gap-4">
|
|
471
497
|
{groups.map((group) => {
|
|
472
498
|
const visibleFiles = showAll ? group.files : group.files.slice(0, FILES_PER_GROUP);
|
|
@@ -505,7 +531,6 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
505
531
|
);
|
|
506
532
|
})}
|
|
507
533
|
|
|
508
|
-
{/* Root-level files (Other) */}
|
|
509
534
|
{rootFiles.length > 0 && (
|
|
510
535
|
<div>
|
|
511
536
|
<div className="flex items-center gap-2 px-1 py-1.5">
|
|
@@ -522,7 +547,6 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
522
547
|
</div>
|
|
523
548
|
)}
|
|
524
549
|
|
|
525
|
-
{/* Show more / less */}
|
|
526
550
|
{groups.some(g => g.files.length > FILES_PER_GROUP) && (
|
|
527
551
|
<ToggleButton
|
|
528
552
|
expanded={showAll}
|
|
@@ -534,7 +558,6 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
534
558
|
)}
|
|
535
559
|
</div>
|
|
536
560
|
) : (
|
|
537
|
-
/* Flat Timeline Fallback */
|
|
538
561
|
<div className="relative pl-4">
|
|
539
562
|
<div className="absolute left-0 top-1 bottom-1 w-px bg-border" />
|
|
540
563
|
<div className="flex flex-col gap-0.5">
|
|
@@ -548,8 +571,8 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
548
571
|
aria-hidden="true"
|
|
549
572
|
className={`absolute -left-4 top-1/2 -translate-y-1/2 rounded-full transition-all duration-150 group-hover:scale-150 ${
|
|
550
573
|
idx === 0
|
|
551
|
-
? 'w-2 h-2 bg-[var(--amber)]
|
|
552
|
-
: 'w-1.5 h-1.5 bg-
|
|
574
|
+
? 'w-2.5 h-2.5 bg-[var(--amber)] ring-2 ring-[var(--amber)]/20'
|
|
575
|
+
: 'w-1.5 h-1.5 bg-muted-foreground/30'
|
|
553
576
|
}`}
|
|
554
577
|
/>
|
|
555
578
|
<Link
|
|
@@ -587,7 +610,7 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
587
610
|
)}
|
|
588
611
|
|
|
589
612
|
{/* Footer */}
|
|
590
|
-
<div className="
|
|
613
|
+
<div className="py-6 border-t border-border/30 flex items-center gap-1.5 text-xs font-display text-muted-foreground opacity-40">
|
|
591
614
|
<Sparkles size={10} className="text-[var(--amber)]" />
|
|
592
615
|
<span>{t.app.footer}</span>
|
|
593
616
|
</div>
|
|
@@ -595,6 +618,54 @@ export default function HomeContent({ recent, existingFiles, spaces }: { recent:
|
|
|
595
618
|
);
|
|
596
619
|
}
|
|
597
620
|
|
|
621
|
+
/* ── Pinned Files Section ── */
|
|
622
|
+
function PinnedFilesSection({ formatTime }: { formatTime: (t: number) => string }) {
|
|
623
|
+
const { t } = useLocale();
|
|
624
|
+
const { pinnedFiles, removePin } = usePinnedFiles();
|
|
625
|
+
|
|
626
|
+
if (pinnedFiles.length === 0) return null;
|
|
627
|
+
|
|
628
|
+
return (
|
|
629
|
+
<section className="mb-12">
|
|
630
|
+
<SectionTitle icon={<Star size={13} />} count={pinnedFiles.length}>
|
|
631
|
+
{t.pinnedFiles.title}
|
|
632
|
+
</SectionTitle>
|
|
633
|
+
<div className="flex flex-col gap-0.5">
|
|
634
|
+
{pinnedFiles.map((filePath) => {
|
|
635
|
+
const name = filePath.split('/').pop() || filePath;
|
|
636
|
+
const dir = filePath.split('/').slice(0, -1).join('/');
|
|
637
|
+
const isCSV = filePath.endsWith('.csv');
|
|
638
|
+
return (
|
|
639
|
+
<div key={filePath} className="group/pin relative">
|
|
640
|
+
<Link
|
|
641
|
+
href={`/view/${encodePath(filePath)}`}
|
|
642
|
+
className="flex items-center gap-3 px-3 py-2 rounded-lg transition-all duration-100 hover:translate-x-0.5 hover:bg-muted"
|
|
643
|
+
>
|
|
644
|
+
<Star size={12} className="shrink-0 fill-[var(--amber)] text-[var(--amber)]" />
|
|
645
|
+
{isCSV
|
|
646
|
+
? <Table size={12} className="shrink-0 text-success" />
|
|
647
|
+
: <FileText size={12} className="shrink-0 text-muted-foreground" />
|
|
648
|
+
}
|
|
649
|
+
<div className="flex-1 min-w-0">
|
|
650
|
+
<span className="text-sm truncate block text-foreground" suppressHydrationWarning>{name}</span>
|
|
651
|
+
{dir && <span className="text-xs truncate block text-muted-foreground opacity-50" suppressHydrationWarning>{dir}</span>}
|
|
652
|
+
</div>
|
|
653
|
+
</Link>
|
|
654
|
+
<button
|
|
655
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation(); removePin(filePath); }}
|
|
656
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 hidden group-hover/pin:flex p-1 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
657
|
+
title={t.pinnedFiles.removedToast}
|
|
658
|
+
>
|
|
659
|
+
<X size={12} />
|
|
660
|
+
</button>
|
|
661
|
+
</div>
|
|
662
|
+
);
|
|
663
|
+
})}
|
|
664
|
+
</div>
|
|
665
|
+
</section>
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
598
669
|
/* ── Create Space: title-bar button ── */
|
|
599
670
|
function CreateSpaceButton({ t }: { t: ReturnType<typeof useLocale>['t'] }) {
|
|
600
671
|
return (
|
|
@@ -6,13 +6,17 @@ import rehypeHighlight from 'rehype-highlight';
|
|
|
6
6
|
import rehypeRaw from 'rehype-raw';
|
|
7
7
|
import rehypeSlug from 'rehype-slug';
|
|
8
8
|
import { useState, useCallback } from 'react';
|
|
9
|
-
import { Copy, Check } from 'lucide-react';
|
|
9
|
+
import { Copy, Check, X } from 'lucide-react';
|
|
10
10
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
11
11
|
import { toast } from '@/lib/toast';
|
|
12
12
|
import type { Components } from 'react-markdown';
|
|
13
13
|
|
|
14
14
|
interface MarkdownViewProps {
|
|
15
15
|
content: string;
|
|
16
|
+
/** Lines changed by AI (1-indexed). Shows banner + fades after timeout. */
|
|
17
|
+
highlightLines?: number[];
|
|
18
|
+
/** Callback to dismiss the highlight banner */
|
|
19
|
+
onDismissHighlight?: () => void;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
function CopyButton({ code }: { code: string }) {
|
|
@@ -111,16 +115,47 @@ function extractText(node: React.ReactNode): string {
|
|
|
111
115
|
return '';
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
export default function MarkdownView({ content }: MarkdownViewProps) {
|
|
118
|
+
export default function MarkdownView({ content, highlightLines, onDismissHighlight }: MarkdownViewProps) {
|
|
119
|
+
const hasHighlights = highlightLines && highlightLines.length > 0;
|
|
120
|
+
|
|
115
121
|
return (
|
|
116
|
-
<div
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
<div>
|
|
123
|
+
{/* Change indicator banner */}
|
|
124
|
+
{hasHighlights && (
|
|
125
|
+
<div
|
|
126
|
+
className="mb-4 flex items-center gap-2 rounded-md border px-3 py-2 text-xs animate-in fade-in-0 duration-300"
|
|
127
|
+
style={{
|
|
128
|
+
borderColor: 'color-mix(in srgb, var(--amber) 40%, var(--border))',
|
|
129
|
+
background: 'color-mix(in srgb, var(--amber) 8%, var(--card))',
|
|
130
|
+
color: 'var(--amber)',
|
|
131
|
+
}}
|
|
132
|
+
data-highlight-line
|
|
133
|
+
>
|
|
134
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)] animate-pulse shrink-0" />
|
|
135
|
+
<span className="font-display font-medium flex-1">
|
|
136
|
+
{highlightLines.length} line{highlightLines.length !== 1 ? 's' : ''} updated by AI
|
|
137
|
+
</span>
|
|
138
|
+
{onDismissHighlight && (
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
onClick={onDismissHighlight}
|
|
142
|
+
className="p-0.5 rounded hover:bg-[var(--amber)]/15 transition-colors shrink-0"
|
|
143
|
+
aria-label="Dismiss"
|
|
144
|
+
>
|
|
145
|
+
<X size={12} />
|
|
146
|
+
</button>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
<div className="prose max-w-none">
|
|
151
|
+
<ReactMarkdown
|
|
152
|
+
remarkPlugins={[remarkGfm]}
|
|
153
|
+
rehypePlugins={[rehypeSlug, rehypeHighlight, rehypeRaw]}
|
|
154
|
+
components={components}
|
|
155
|
+
>
|
|
156
|
+
{content}
|
|
157
|
+
</ReactMarkdown>
|
|
158
|
+
</div>
|
|
124
159
|
</div>
|
|
125
160
|
);
|
|
126
161
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { useRouter, usePathname } from 'next/navigation';
|
|
6
|
-
import { Search, PanelLeftClose, PanelLeftOpen, Menu, X, Settings } from 'lucide-react';
|
|
6
|
+
import { Search, PanelLeftClose, PanelLeftOpen, Menu, X, Settings, Trash2 } from 'lucide-react';
|
|
7
7
|
import FileTree from './FileTree';
|
|
8
8
|
import SearchModal from './SearchModal';
|
|
9
9
|
import AskModal from './AskModal';
|
|
@@ -117,6 +117,15 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
|
|
|
117
117
|
<div className="flex-1 overflow-y-auto min-h-0 px-2 py-2">
|
|
118
118
|
<FileTree nodes={fileTree} onNavigate={() => setMobileOpen(false)} />
|
|
119
119
|
</div>
|
|
120
|
+
<div className="px-2 pb-1">
|
|
121
|
+
<Link
|
|
122
|
+
href="/trash"
|
|
123
|
+
className="flex items-center gap-2 px-2 py-1.5 rounded-md text-xs text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
124
|
+
>
|
|
125
|
+
<Trash2 size={13} />
|
|
126
|
+
<span>{t.trash?.title ?? 'Trash'}</span>
|
|
127
|
+
</Link>
|
|
128
|
+
</div>
|
|
120
129
|
<SyncStatusBar
|
|
121
130
|
collapsed={collapsed}
|
|
122
131
|
onOpenSyncSettings={openSyncSettings}
|