@geminilight/mindos 0.6.14 → 0.6.16
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/globals.css +49 -2
- package/app/app/page.tsx +1 -19
- package/app/components/CreateSpaceModal.tsx +3 -3
- package/app/components/CustomSelect.tsx +228 -0
- package/app/components/DirPicker.tsx +110 -63
- package/app/components/FileTree.tsx +54 -32
- package/app/components/HomeContent.tsx +69 -13
- package/app/components/ImportModal.tsx +92 -39
- package/app/components/Panel.tsx +87 -21
- package/app/components/SidebarLayout.tsx +18 -5
- package/app/components/changes/ChangesContentPage.tsx +34 -23
- package/app/components/renderers/csv/ConfigPanel.tsx +10 -7
- package/app/components/settings/KnowledgeTab.tsx +38 -12
- package/app/components/settings/McpAgentInstall.tsx +14 -30
- package/app/components/settings/McpSkillsSection.tsx +43 -27
- package/app/components/settings/McpTab.tsx +30 -37
- package/app/components/settings/UninstallTab.tsx +1 -1
- package/app/components/setup/StepAgents.tsx +1 -1
- package/app/lib/core/create-space.ts +12 -0
- package/app/lib/i18n-en.ts +19 -2
- package/app/lib/i18n-zh.ts +21 -4
- package/bin/cli.js +37 -14
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
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
|
+
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
13
14
|
|
|
14
15
|
function notifyFilesChanged() {
|
|
15
16
|
window.dispatchEvent(new Event('mindos:files-changed'));
|
|
@@ -124,12 +125,11 @@ const MENU_DIVIDER = "my-1 border-t border-border/50";
|
|
|
124
125
|
|
|
125
126
|
// ─── SpaceContextMenu ─────────────────────────────────────────────────────────
|
|
126
127
|
|
|
127
|
-
function SpaceContextMenu({ x, y, node, onClose, onRename, onImport }: {
|
|
128
|
-
x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void; onImport?: (space: string) => void;
|
|
128
|
+
function SpaceContextMenu({ x, y, node, onClose, onRename, onImport, onDelete }: {
|
|
129
|
+
x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void; onImport?: (space: string) => void; onDelete: () => void;
|
|
129
130
|
}) {
|
|
130
131
|
const router = useRouter();
|
|
131
132
|
const { t } = useLocale();
|
|
132
|
-
const [isPending, startTransition] = useTransition();
|
|
133
133
|
|
|
134
134
|
return (
|
|
135
135
|
<ContextMenuShell x={x} y={y} onClose={onClose}>
|
|
@@ -145,16 +145,9 @@ function SpaceContextMenu({ x, y, node, onClose, onRename, onImport }: {
|
|
|
145
145
|
<Pencil size={14} className="shrink-0" /> {t.fileTree.renameSpace}
|
|
146
146
|
</button>
|
|
147
147
|
<div className={MENU_DIVIDER} />
|
|
148
|
-
<button className={MENU_DANGER}
|
|
149
|
-
if (!confirm(t.fileTree.confirmDeleteSpace(node.name))) return;
|
|
150
|
-
startTransition(async () => {
|
|
151
|
-
const result = await deleteSpaceAction(node.path);
|
|
152
|
-
if (result.success) { router.push('/'); router.refresh(); notifyFilesChanged(); }
|
|
153
|
-
onClose();
|
|
154
|
-
});
|
|
155
|
-
}}>
|
|
148
|
+
<button className={MENU_DANGER} onClick={() => { onClose(); onDelete(); }}>
|
|
156
149
|
<Trash2 size={14} className="shrink-0" />
|
|
157
|
-
{
|
|
150
|
+
{t.fileTree.deleteSpace}
|
|
158
151
|
</button>
|
|
159
152
|
</ContextMenuShell>
|
|
160
153
|
);
|
|
@@ -162,8 +155,8 @@ function SpaceContextMenu({ x, y, node, onClose, onRename, onImport }: {
|
|
|
162
155
|
|
|
163
156
|
// ─── FolderContextMenu ────────────────────────────────────────────────────────
|
|
164
157
|
|
|
165
|
-
function FolderContextMenu({ x, y, node, onClose, onRename }: {
|
|
166
|
-
x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void;
|
|
158
|
+
function FolderContextMenu({ x, y, node, onClose, onRename, onDelete }: {
|
|
159
|
+
x: number; y: number; node: FileNode; onClose: () => void; onRename: () => void; onDelete: () => void;
|
|
167
160
|
}) {
|
|
168
161
|
const router = useRouter();
|
|
169
162
|
const { t } = useLocale();
|
|
@@ -184,16 +177,9 @@ function FolderContextMenu({ x, y, node, onClose, onRename }: {
|
|
|
184
177
|
<Pencil size={14} className="shrink-0" /> {t.fileTree.rename}
|
|
185
178
|
</button>
|
|
186
179
|
<div className={MENU_DIVIDER} />
|
|
187
|
-
<button className={MENU_DANGER}
|
|
188
|
-
if (!confirm(t.fileTree.confirmDeleteFolder(node.name))) return;
|
|
189
|
-
startTransition(async () => {
|
|
190
|
-
const result = await deleteFolderAction(node.path);
|
|
191
|
-
if (result.success) { router.push('/'); router.refresh(); notifyFilesChanged(); }
|
|
192
|
-
onClose();
|
|
193
|
-
});
|
|
194
|
-
}}>
|
|
180
|
+
<button className={MENU_DANGER} onClick={() => { onClose(); onDelete(); }}>
|
|
195
181
|
<Trash2 size={14} className="shrink-0" />
|
|
196
|
-
{
|
|
182
|
+
{t.fileTree.deleteFolder}
|
|
197
183
|
</button>
|
|
198
184
|
</ContextMenuShell>
|
|
199
185
|
);
|
|
@@ -291,6 +277,8 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
|
|
|
291
277
|
const [plusPopover, setPlusPopover] = useState(false);
|
|
292
278
|
const plusRef = useRef<HTMLButtonElement>(null);
|
|
293
279
|
const { t } = useLocale();
|
|
280
|
+
const [deleteConfirm, setDeleteConfirm] = useState<null | 'space' | 'folder'>(null);
|
|
281
|
+
const [isPendingDelete, startDeleteTransition] = useTransition();
|
|
294
282
|
|
|
295
283
|
const toggle = useCallback(() => setOpen(v => !v), []);
|
|
296
284
|
|
|
@@ -506,6 +494,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
|
|
|
506
494
|
onClose={() => setContextMenu(null)}
|
|
507
495
|
onRename={() => startRename()}
|
|
508
496
|
onImport={onImport}
|
|
497
|
+
onDelete={() => setDeleteConfirm('space')}
|
|
509
498
|
/>
|
|
510
499
|
) : (
|
|
511
500
|
<FolderContextMenu
|
|
@@ -514,9 +503,30 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
|
|
|
514
503
|
node={node}
|
|
515
504
|
onClose={() => setContextMenu(null)}
|
|
516
505
|
onRename={() => startRename()}
|
|
506
|
+
onDelete={() => setDeleteConfirm('folder')}
|
|
517
507
|
/>
|
|
518
508
|
))}
|
|
519
509
|
|
|
510
|
+
<ConfirmDialog
|
|
511
|
+
open={deleteConfirm !== null}
|
|
512
|
+
title={deleteConfirm === 'space' ? t.fileTree.deleteSpace : t.fileTree.deleteFolder}
|
|
513
|
+
message={deleteConfirm === 'space' ? t.fileTree.confirmDeleteSpace(node.name) : t.fileTree.confirmDeleteFolder(node.name)}
|
|
514
|
+
confirmLabel={deleteConfirm === 'space' ? t.fileTree.deleteSpace : t.fileTree.deleteFolder}
|
|
515
|
+
cancelLabel="Cancel"
|
|
516
|
+
variant="destructive"
|
|
517
|
+
onCancel={() => setDeleteConfirm(null)}
|
|
518
|
+
onConfirm={() => {
|
|
519
|
+
const kind = deleteConfirm;
|
|
520
|
+
setDeleteConfirm(null);
|
|
521
|
+
startDeleteTransition(async () => {
|
|
522
|
+
const result = kind === 'space'
|
|
523
|
+
? await deleteSpaceAction(node.path)
|
|
524
|
+
: await deleteFolderAction(node.path);
|
|
525
|
+
if (result.success) { router.push('/'); router.refresh(); notifyFilesChanged(); }
|
|
526
|
+
});
|
|
527
|
+
}}
|
|
528
|
+
/>
|
|
529
|
+
|
|
520
530
|
{plusPopover && plusRef.current && (() => {
|
|
521
531
|
const rect = plusRef.current!.getBoundingClientRect();
|
|
522
532
|
return (
|
|
@@ -546,8 +556,10 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
546
556
|
const [renaming, setRenaming] = useState(false);
|
|
547
557
|
const [renameValue, setRenameValue] = useState(node.name);
|
|
548
558
|
const [isPending, startTransition] = useTransition();
|
|
559
|
+
const [isPendingDelete, startDeleteTransition] = useTransition();
|
|
549
560
|
const renameRef = useRef<HTMLInputElement>(null);
|
|
550
561
|
const { t } = useLocale();
|
|
562
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
551
563
|
|
|
552
564
|
const handleClick = useCallback(() => {
|
|
553
565
|
if (renaming) return;
|
|
@@ -580,14 +592,8 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
580
592
|
|
|
581
593
|
const handleDelete = useCallback((e: React.MouseEvent) => {
|
|
582
594
|
e.stopPropagation();
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
await deleteFileAction(node.path);
|
|
586
|
-
if (currentPath === node.path) router.push('/');
|
|
587
|
-
router.refresh();
|
|
588
|
-
notifyFilesChanged();
|
|
589
|
-
});
|
|
590
|
-
}, [node.name, node.path, currentPath, router, t]);
|
|
595
|
+
setShowDeleteConfirm(true);
|
|
596
|
+
}, []);
|
|
591
597
|
|
|
592
598
|
const handleDragStart = useCallback((e: React.DragEvent) => {
|
|
593
599
|
e.dataTransfer.setData('text/mindos-path', node.path);
|
|
@@ -640,9 +646,25 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
|
|
|
640
646
|
<Pencil size={12} />
|
|
641
647
|
</button>
|
|
642
648
|
<button onClick={handleDelete} className="p-0.5 rounded text-muted-foreground hover:text-error hover:bg-muted transition-colors" title={t.fileTree.delete}>
|
|
643
|
-
<Trash2 size={12} />
|
|
649
|
+
{isPendingDelete ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
|
|
644
650
|
</button>
|
|
645
651
|
</div>
|
|
652
|
+
<ConfirmDialog
|
|
653
|
+
open={showDeleteConfirm}
|
|
654
|
+
title={t.fileTree.delete}
|
|
655
|
+
message={t.fileTree.confirmDelete(node.name)}
|
|
656
|
+
confirmLabel={t.fileTree.delete}
|
|
657
|
+
cancelLabel="Cancel"
|
|
658
|
+
variant="destructive"
|
|
659
|
+
onCancel={() => setShowDeleteConfirm(false)}
|
|
660
|
+
onConfirm={() => {
|
|
661
|
+
setShowDeleteConfirm(false);
|
|
662
|
+
startDeleteTransition(async () => {
|
|
663
|
+
const result = await deleteFileAction(node.path);
|
|
664
|
+
if (result.success) { router.push('/'); router.refresh(); notifyFilesChanged(); }
|
|
665
|
+
});
|
|
666
|
+
}}
|
|
667
|
+
/>
|
|
646
668
|
</div>
|
|
647
669
|
);
|
|
648
670
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
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 } 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 } from 'lucide-react';
|
|
5
|
+
import type { LucideIcon } from 'lucide-react';
|
|
5
6
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
6
7
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
8
|
import { encodePath, relativeTime, extractEmoji, stripEmoji } from '@/lib/utils';
|
|
8
9
|
import { getAllRenderers, getPluginRenderers } from '@/lib/renderers/registry';
|
|
9
10
|
import OnboardingView from './OnboardingView';
|
|
10
11
|
import GuideCard from './GuideCard';
|
|
11
|
-
import CreateSpaceModal from './CreateSpaceModal';
|
|
12
12
|
import { scanExampleFilesAction, cleanupExamplesAction } from '@/lib/actions';
|
|
13
13
|
import type { SpaceInfo } from '@/app/page';
|
|
14
14
|
|
|
@@ -138,7 +138,7 @@ function FileRow({ filePath, mtime, formatTime, subPath }: {
|
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
/**
|
|
141
|
+
/** Compact chip for extension renderers */
|
|
142
142
|
function FeatureChip({ id, icon, name, entryPath, active, inactiveTitle }: {
|
|
143
143
|
id: string;
|
|
144
144
|
icon: string;
|
|
@@ -164,11 +164,52 @@ function FeatureChip({ id, icon, name, entryPath, active, inactiveTitle }: {
|
|
|
164
164
|
return <span key={id} className={cls} title={inactiveTitle}>{inner}</span>;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
const TOOL_ICONS: Record<string, LucideIcon> = {
|
|
168
|
+
'agent-inspector': Search,
|
|
169
|
+
'config-panel': SlidersHorizontal,
|
|
170
|
+
'todo': ListTodo,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** Mini-card for built-in tools — visually distinct from plugin chips */
|
|
174
|
+
function ToolCard({ id, name, description, entryPath, active, inactiveTitle }: {
|
|
175
|
+
id: string;
|
|
176
|
+
name: string;
|
|
177
|
+
description: string;
|
|
178
|
+
entryPath?: string;
|
|
179
|
+
active: boolean;
|
|
180
|
+
inactiveTitle?: string;
|
|
181
|
+
}) {
|
|
182
|
+
const Icon = TOOL_ICONS[id] ?? Zap;
|
|
183
|
+
const inner = (
|
|
184
|
+
<div
|
|
185
|
+
className={`flex items-start gap-3 px-3.5 py-3 rounded-xl border transition-all duration-150 ${
|
|
186
|
+
active
|
|
187
|
+
? 'border-border hover:border-[var(--amber)]/30 hover:shadow-sm'
|
|
188
|
+
: 'border-dashed border-border/50 opacity-60'
|
|
189
|
+
}`}
|
|
190
|
+
title={active ? undefined : inactiveTitle}
|
|
191
|
+
>
|
|
192
|
+
<span className="shrink-0 mt-0.5 text-[var(--amber)]">
|
|
193
|
+
<Icon size={16} />
|
|
194
|
+
</span>
|
|
195
|
+
<div className="min-w-0 flex-1">
|
|
196
|
+
<span className="text-sm font-medium block text-foreground">{name}</span>
|
|
197
|
+
<span className="text-xs text-muted-foreground line-clamp-2 mt-0.5">{description}</span>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (active && entryPath) {
|
|
203
|
+
return <Link key={id} href={`/view/${encodePath(entryPath)}`}>{inner}</Link>;
|
|
204
|
+
}
|
|
205
|
+
return <div key={id}>{inner}</div>;
|
|
206
|
+
}
|
|
207
|
+
|
|
167
208
|
const FILES_PER_GROUP = 3;
|
|
168
209
|
const SPACES_PER_ROW = 6;
|
|
169
210
|
const PLUGINS_INITIAL = 4;
|
|
170
211
|
|
|
171
|
-
export default function HomeContent({ recent, existingFiles, spaces
|
|
212
|
+
export default function HomeContent({ recent, existingFiles, spaces }: { recent: RecentFile[]; existingFiles?: string[]; spaces?: SpaceInfo[] }) {
|
|
172
213
|
const { t } = useLocale();
|
|
173
214
|
const [showAll, setShowAll] = useState(false);
|
|
174
215
|
const [showAllSpaces, setShowAllSpaces] = useState(false);
|
|
@@ -348,24 +389,25 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
|
|
|
348
389
|
{t.home.noSpacesYet ?? 'No spaces yet. Create one to organize your knowledge.'}
|
|
349
390
|
</p>
|
|
350
391
|
)}
|
|
351
|
-
<CreateSpaceModal t={t} dirPaths={dirPaths ?? []} />
|
|
352
392
|
</section>
|
|
353
393
|
|
|
354
|
-
{/* ── Section 2:
|
|
394
|
+
{/* ── Section 2: Tools ── */}
|
|
355
395
|
{builtinFeatures.length > 0 && (
|
|
356
396
|
<section className="mb-8">
|
|
357
|
-
<SectionTitle icon={<
|
|
397
|
+
<SectionTitle icon={<Zap size={13} />} count={builtinFeatures.length}>
|
|
358
398
|
{t.home.builtinFeatures}
|
|
359
399
|
</SectionTitle>
|
|
360
|
-
<div className="
|
|
400
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
361
401
|
{builtinFeatures.map((r) => {
|
|
362
402
|
const active = !!r.entryPath && existingSet.has(r.entryPath);
|
|
403
|
+
const toolNames = t.home.toolName as Record<string, string> | undefined;
|
|
404
|
+
const toolDescs = t.home.toolDesc as Record<string, string> | undefined;
|
|
363
405
|
return (
|
|
364
|
-
<
|
|
406
|
+
<ToolCard
|
|
365
407
|
key={r.id}
|
|
366
408
|
id={r.id}
|
|
367
|
-
|
|
368
|
-
|
|
409
|
+
name={toolNames?.[r.id] ?? r.name}
|
|
410
|
+
description={toolDescs?.[r.id] ?? r.description}
|
|
369
411
|
entryPath={r.entryPath}
|
|
370
412
|
active={active}
|
|
371
413
|
inactiveTitle={r.entryPath ? t.home.createToActivate.replace('{file}', r.entryPath) : t.home.builtinInactive}
|
|
@@ -406,7 +448,21 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
|
|
|
406
448
|
{/* ── Section 4: Recently Edited ── */}
|
|
407
449
|
{recent.length > 0 && (
|
|
408
450
|
<section className="mb-12">
|
|
409
|
-
<SectionTitle
|
|
451
|
+
<SectionTitle
|
|
452
|
+
icon={<Clock size={13} />}
|
|
453
|
+
count={recent.length}
|
|
454
|
+
action={
|
|
455
|
+
<Link
|
|
456
|
+
href="/changes"
|
|
457
|
+
className="flex items-center gap-1.5 text-xs font-medium text-[var(--amber)] transition-colors hover:opacity-80 font-display"
|
|
458
|
+
>
|
|
459
|
+
<History size={12} />
|
|
460
|
+
<span>{t.home.changeHistory}</span>
|
|
461
|
+
</Link>
|
|
462
|
+
}
|
|
463
|
+
>
|
|
464
|
+
{t.home.recentlyEdited}
|
|
465
|
+
</SectionTitle>
|
|
410
466
|
|
|
411
467
|
{groups.length > 0 ? (
|
|
412
468
|
/* Space-Grouped View */
|
|
@@ -543,7 +599,7 @@ function CreateSpaceButton({ t }: { t: ReturnType<typeof useLocale>['t'] }) {
|
|
|
543
599
|
return (
|
|
544
600
|
<button
|
|
545
601
|
onClick={() => window.dispatchEvent(new Event('mindos:create-space'))}
|
|
546
|
-
className="flex items-center gap-1.5
|
|
602
|
+
className="flex items-center gap-1.5 text-xs font-medium text-[var(--amber)] transition-colors hover:opacity-80 cursor-pointer font-display"
|
|
547
603
|
>
|
|
548
604
|
<Plus size={12} />
|
|
549
605
|
<span>{t.home.newSpace}</span>
|
|
@@ -10,6 +10,8 @@ import { useFileImport, type ImportIntent, type ConflictMode } from '@/hooks/use
|
|
|
10
10
|
import type { useAiOrganize } from '@/hooks/useAiOrganize';
|
|
11
11
|
import { ALLOWED_IMPORT_EXTENSIONS } from '@/lib/core/file-convert';
|
|
12
12
|
import type { LocalAttachment } from '@/lib/types';
|
|
13
|
+
import { ConfirmDialog } from '@/components/agents/AgentsPrimitives';
|
|
14
|
+
import DirPicker from './DirPicker';
|
|
13
15
|
|
|
14
16
|
interface ImportModalProps {
|
|
15
17
|
open: boolean;
|
|
@@ -18,21 +20,24 @@ interface ImportModalProps {
|
|
|
18
20
|
initialFiles?: File[];
|
|
19
21
|
/** Lifted AI organize hook from SidebarLayout (shared with OrganizeToast) */
|
|
20
22
|
aiOrganize: ReturnType<typeof useAiOrganize>;
|
|
23
|
+
/** Flat list of directory paths for the DirPicker */
|
|
24
|
+
dirPaths: string[];
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
const ACCEPT = Array.from(ALLOWED_IMPORT_EXTENSIONS).join(',');
|
|
24
28
|
|
|
25
29
|
|
|
26
|
-
export default function ImportModal({ open, onClose, defaultSpace, initialFiles, aiOrganize }: ImportModalProps) {
|
|
30
|
+
export default function ImportModal({ open, onClose, defaultSpace, initialFiles, aiOrganize, dirPaths }: ImportModalProps) {
|
|
27
31
|
const { t } = useLocale();
|
|
28
32
|
const im = useFileImport();
|
|
29
33
|
const overlayRef = useRef<HTMLDivElement>(null);
|
|
30
34
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
31
|
-
const [spaces, setSpaces] = useState<Array<{ name: string; path: string }>>([]);
|
|
32
35
|
const [closing, setClosing] = useState(false);
|
|
33
36
|
const [showSuccess, setShowSuccess] = useState(false);
|
|
34
37
|
const [conflictFiles, setConflictFiles] = useState<string[]>([]);
|
|
35
38
|
const [showConflictOptions, setShowConflictOptions] = useState(false);
|
|
39
|
+
const [showDiscard, setShowDiscard] = useState(false);
|
|
40
|
+
const [recommendedSpace, setRecommendedSpace] = useState('');
|
|
36
41
|
const initializedRef = useRef(false);
|
|
37
42
|
|
|
38
43
|
useEffect(() => {
|
|
@@ -49,19 +54,21 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
|
|
|
49
54
|
if (initialFiles && initialFiles.length > 0) {
|
|
50
55
|
im.addFiles(initialFiles);
|
|
51
56
|
}
|
|
52
|
-
fetch('/api/file?op=list_spaces')
|
|
53
|
-
.then(r => r.json())
|
|
54
|
-
.then(d => { if (d.spaces) setSpaces(d.spaces); })
|
|
55
|
-
.catch(() => {});
|
|
56
57
|
}, [open, defaultSpace, initialFiles, im]);
|
|
57
58
|
|
|
59
|
+
const doClose = useCallback(() => {
|
|
60
|
+
setShowDiscard(false);
|
|
61
|
+
setClosing(true);
|
|
62
|
+
setTimeout(() => { setClosing(false); onClose(); im.reset(); setConflictFiles([]); setShowConflictOptions(false); setRecommendedSpace(''); }, 150);
|
|
63
|
+
}, [im, onClose]);
|
|
64
|
+
|
|
58
65
|
const handleClose = useCallback(() => {
|
|
59
66
|
if (im.files.length > 0 && im.step !== 'done') {
|
|
60
|
-
|
|
67
|
+
setShowDiscard(true);
|
|
68
|
+
return;
|
|
61
69
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}, [im, onClose, t]);
|
|
70
|
+
doClose();
|
|
71
|
+
}, [im, doClose]);
|
|
65
72
|
|
|
66
73
|
useEffect(() => {
|
|
67
74
|
if (!open) return;
|
|
@@ -132,6 +139,35 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
|
|
|
132
139
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
140
|
}, [im.targetSpace]);
|
|
134
141
|
|
|
142
|
+
// V1: keyword-based space recommendation when entering archive_config
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (im.step !== 'archive_config' || dirPaths.length === 0 || im.validFiles.length === 0) return;
|
|
145
|
+
if (defaultSpace) return;
|
|
146
|
+
|
|
147
|
+
const fileTokens = im.validFiles
|
|
148
|
+
.flatMap(f => f.name.replace(/\.[^.]+$/, '').toLowerCase().split(/[\s_\-/\\]+/))
|
|
149
|
+
.filter(w => w.length >= 2);
|
|
150
|
+
|
|
151
|
+
let bestPath = '';
|
|
152
|
+
let bestScore = 0;
|
|
153
|
+
for (const dp of dirPaths) {
|
|
154
|
+
const name = dp.split('/').pop() || dp;
|
|
155
|
+
const spaceTokens = name.toLowerCase().split(/[\s_\-/\\]+/).filter(w => w.length >= 2);
|
|
156
|
+
let score = 0;
|
|
157
|
+
for (const ft of fileTokens) {
|
|
158
|
+
for (const st of spaceTokens) {
|
|
159
|
+
if (ft.includes(st) || st.includes(ft)) score += Math.min(ft.length, st.length);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (score > bestScore) { bestScore = score; bestPath = dp; }
|
|
163
|
+
}
|
|
164
|
+
if (bestPath && bestScore >= 2) {
|
|
165
|
+
im.setTargetSpace(bestPath);
|
|
166
|
+
setRecommendedSpace(bestPath);
|
|
167
|
+
}
|
|
168
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
169
|
+
}, [im.step, dirPaths.length, im.validFiles.length]);
|
|
170
|
+
|
|
135
171
|
|
|
136
172
|
useEffect(() => {
|
|
137
173
|
if (im.step === 'done' && im.result) {
|
|
@@ -161,6 +197,15 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
|
|
|
161
197
|
|
|
162
198
|
return (
|
|
163
199
|
<>
|
|
200
|
+
<ConfirmDialog
|
|
201
|
+
open={showDiscard}
|
|
202
|
+
title={t.fileImport.discardTitle}
|
|
203
|
+
message={t.fileImport.discardMessage(im.files.length)}
|
|
204
|
+
confirmLabel={t.fileImport.discardConfirm}
|
|
205
|
+
cancelLabel={t.fileImport.discardCancel}
|
|
206
|
+
onConfirm={doClose}
|
|
207
|
+
onCancel={() => setShowDiscard(false)}
|
|
208
|
+
/>
|
|
164
209
|
<div
|
|
165
210
|
ref={overlayRef}
|
|
166
211
|
className={`fixed inset-0 z-50 modal-backdrop flex items-center justify-center p-4 transition-opacity duration-200 ${closing ? 'opacity-0' : 'opacity-100'}`}
|
|
@@ -305,29 +350,35 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
|
|
|
305
350
|
))}
|
|
306
351
|
</div>
|
|
307
352
|
|
|
308
|
-
{/* Archive config: target path preview */}
|
|
309
|
-
{isArchiveConfig && (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
353
|
+
{/* Archive config: target path preview — only shown when path differs from source */}
|
|
354
|
+
{isArchiveConfig && (() => {
|
|
355
|
+
const previews = im.validFiles.map((f) => {
|
|
356
|
+
const ext = f.name.split('.').pop()?.toLowerCase();
|
|
357
|
+
const targetExt = (ext === 'txt' || ext === 'html' || ext === 'htm' || ext === 'yaml' || ext === 'yml' || ext === 'xml')
|
|
358
|
+
? 'md' : ext;
|
|
359
|
+
const stem = f.name.replace(/\.[^.]+$/, '');
|
|
360
|
+
const targetName = `${stem}.${targetExt}`;
|
|
361
|
+
const targetPath = im.targetSpace ? `${im.targetSpace}/${targetName}` : targetName;
|
|
362
|
+
const hasConflict = conflictFiles.includes(f.name);
|
|
363
|
+
const changed = targetName !== f.name || !!im.targetSpace;
|
|
364
|
+
return { f, targetPath, hasConflict, changed };
|
|
365
|
+
});
|
|
366
|
+
const anyChanged = previews.some(p => p.changed || p.hasConflict);
|
|
367
|
+
if (!anyChanged) return null;
|
|
368
|
+
return (
|
|
369
|
+
<div className="flex flex-col gap-1 mt-2 max-h-[120px] overflow-y-auto">
|
|
370
|
+
{previews.filter(p => p.changed || p.hasConflict).map((p, idx) => (
|
|
320
371
|
<div key={`preview-${idx}`} className="flex items-center gap-1.5 text-xs text-muted-foreground px-3">
|
|
321
|
-
<span className="truncate">{f.name}</span>
|
|
372
|
+
<span className="truncate">{p.f.name}</span>
|
|
322
373
|
<span className="text-muted-foreground/50 shrink-0">{t.fileImport.arrowTo}</span>
|
|
323
374
|
<FolderOpen size={12} className="text-muted-foreground/60 shrink-0" />
|
|
324
|
-
<span className={`truncate ${hasConflict ? 'text-[var(--amber)]' : ''}`}>{targetPath}</span>
|
|
325
|
-
{hasConflict && <AlertTriangle size={11} className="text-[var(--amber)] shrink-0" />}
|
|
375
|
+
<span className={`truncate ${p.hasConflict ? 'text-[var(--amber)]' : ''}`}>{p.targetPath}</span>
|
|
376
|
+
{p.hasConflict && <AlertTriangle size={11} className="text-[var(--amber)] shrink-0" />}
|
|
326
377
|
</div>
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
)}
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
})()}
|
|
331
382
|
</div>
|
|
332
383
|
)}
|
|
333
384
|
|
|
@@ -361,16 +412,18 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
|
|
|
361
412
|
{/* Space selector */}
|
|
362
413
|
<div>
|
|
363
414
|
<label className="text-xs font-medium text-foreground mb-1 block">{t.fileImport.targetSpace}</label>
|
|
364
|
-
<
|
|
415
|
+
<DirPicker
|
|
416
|
+
dirPaths={dirPaths}
|
|
365
417
|
value={im.targetSpace}
|
|
366
|
-
onChange={(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
<
|
|
372
|
-
|
|
373
|
-
|
|
418
|
+
onChange={(val) => { im.setTargetSpace(val); setRecommendedSpace(''); }}
|
|
419
|
+
rootLabel={t.fileImport.rootDir}
|
|
420
|
+
/>
|
|
421
|
+
{recommendedSpace && im.targetSpace === recommendedSpace && (
|
|
422
|
+
<p className="text-2xs text-muted-foreground/70 mt-1 flex items-center gap-1">
|
|
423
|
+
<Sparkles size={10} className="text-[var(--amber)]" />
|
|
424
|
+
{t.fileImport.aiRecommendedHint}
|
|
425
|
+
</p>
|
|
426
|
+
)}
|
|
374
427
|
</div>
|
|
375
428
|
|
|
376
429
|
{/* Conflict strategy — progressive disclosure */}
|
|
@@ -404,7 +457,7 @@ export default function ImportModal({ open, onClose, defaultSpace, initialFiles,
|
|
|
404
457
|
value={opt.value}
|
|
405
458
|
checked={im.conflict === opt.value}
|
|
406
459
|
onChange={() => im.setConflict(opt.value)}
|
|
407
|
-
className="
|
|
460
|
+
className="form-radio"
|
|
408
461
|
/>
|
|
409
462
|
{opt.label}
|
|
410
463
|
{opt.value === 'overwrite' && (
|