|
|
@@ -36,6 +36,7 @@ function JobStatusBadge({ status }: { status: KBJob['status'] | string }) {
|
|
|
|
|
|
// Recursive tree node for a single directory
|
|
|
function DirTreeNode({
|
|
|
+ kbId,
|
|
|
node,
|
|
|
ignoredSet,
|
|
|
ancestorIgnored,
|
|
|
@@ -43,6 +44,7 @@ function DirTreeNode({
|
|
|
onToggle,
|
|
|
saving,
|
|
|
}: {
|
|
|
+ kbId: string
|
|
|
node: KBDirNode
|
|
|
ignoredSet: Set<string>
|
|
|
ancestorIgnored: boolean
|
|
|
@@ -50,10 +52,32 @@ function DirTreeNode({
|
|
|
onToggle: (path: string, shouldIgnore: boolean) => void
|
|
|
saving: boolean
|
|
|
}) {
|
|
|
- const [expanded, setExpanded] = useState(depth === 0)
|
|
|
+ const [expanded, setExpanded] = useState(false)
|
|
|
+ // Children are loaded lazily: null = not yet fetched, [] = fetched (possibly empty)
|
|
|
+ const [lazyChildren, setLazyChildren] = useState<KBDirNode[] | null>(
|
|
|
+ node.children.length > 0 ? node.children : null
|
|
|
+ )
|
|
|
+ const [loadingChildren, setLoadingChildren] = useState(false)
|
|
|
+
|
|
|
const selfIgnored = ignoredSet.has(node.relative_path)
|
|
|
const effectivelyIgnored = selfIgnored || ancestorIgnored
|
|
|
- const hasChildren = node.children.length > 0
|
|
|
+ const canExpand = node.has_children || node.children.length > 0
|
|
|
+
|
|
|
+ const handleExpand = async () => {
|
|
|
+ const next = !expanded
|
|
|
+ setExpanded(next)
|
|
|
+ if (next && lazyChildren === null) {
|
|
|
+ setLoadingChildren(true)
|
|
|
+ try {
|
|
|
+ const res = await knowledgeApi.listDirs(kbId, node.relative_path)
|
|
|
+ setLazyChildren(res.dirs)
|
|
|
+ } catch {
|
|
|
+ setLazyChildren([])
|
|
|
+ } finally {
|
|
|
+ setLoadingChildren(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
<div>
|
|
|
@@ -64,19 +88,19 @@ function DirTreeNode({
|
|
|
>
|
|
|
{/* Expand / collapse arrow */}
|
|
|
<button
|
|
|
- onClick={() => setExpanded(e => !e)}
|
|
|
- className={`shrink-0 text-gray-400 hover:text-gray-600 transition-transform ${hasChildren ? '' : 'invisible'}`}
|
|
|
+ onClick={handleExpand}
|
|
|
+ className={`shrink-0 text-gray-400 hover:text-gray-600 ${canExpand ? '' : 'invisible'}`}
|
|
|
>
|
|
|
- {expanded ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
|
|
|
+ {loadingChildren
|
|
|
+ ? <RefreshCw size={13} className="animate-spin text-indigo-400"/>
|
|
|
+ : expanded ? <ChevronDown size={13}/> : <ChevronRight size={13}/>}
|
|
|
</button>
|
|
|
|
|
|
- {/* Folder icon */}
|
|
|
<Folder
|
|
|
size={14}
|
|
|
className={selfIgnored ? 'text-orange-300 shrink-0' : ancestorIgnored ? 'text-gray-300 shrink-0' : 'text-indigo-400 shrink-0'}
|
|
|
/>
|
|
|
|
|
|
- {/* Dir name */}
|
|
|
<span
|
|
|
className={`flex-1 text-sm font-mono truncate
|
|
|
${selfIgnored ? 'line-through text-gray-400'
|
|
|
@@ -86,12 +110,10 @@ function DirTreeNode({
|
|
|
{node.name}/
|
|
|
</span>
|
|
|
|
|
|
- {/* Inherited-ignore tag */}
|
|
|
{ancestorIgnored && !selfIgnored && (
|
|
|
<span className="text-xs text-gray-400 px-1.5">继承</span>
|
|
|
)}
|
|
|
|
|
|
- {/* Eye toggle — only when not hidden by an ancestor */}
|
|
|
{!ancestorIgnored && (
|
|
|
<button
|
|
|
onClick={() => onToggle(node.relative_path, !selfIgnored)}
|
|
|
@@ -102,15 +124,16 @@ function DirTreeNode({
|
|
|
? 'text-orange-500 hover:bg-orange-100 !opacity-100'
|
|
|
: 'text-gray-400 hover:text-gray-700 hover:bg-gray-200'}`}
|
|
|
>
|
|
|
- {selfIgnored ? <Eye size={14} /> : <EyeOff size={14} />}
|
|
|
+ {selfIgnored ? <Eye size={14}/> : <EyeOff size={14}/>}
|
|
|
</button>
|
|
|
)}
|
|
|
</div>
|
|
|
|
|
|
- {/* Children */}
|
|
|
- {expanded && hasChildren && node.children.map(child => (
|
|
|
+ {/* Lazily-loaded children */}
|
|
|
+ {expanded && lazyChildren && lazyChildren.map(child => (
|
|
|
<DirTreeNode
|
|
|
key={child.relative_path}
|
|
|
+ kbId={kbId}
|
|
|
node={child}
|
|
|
ignoredSet={ignoredSet}
|
|
|
ancestorIgnored={effectivelyIgnored}
|
|
|
@@ -143,12 +166,14 @@ function collectIgnoredPaths(nodes: KBDirNode[], ignoredSet: Set<string>, acc: s
|
|
|
}
|
|
|
|
|
|
function DirIgnorePanel({
|
|
|
+ kbId,
|
|
|
dirs,
|
|
|
ignoredDirs,
|
|
|
onToggle,
|
|
|
onClearAll,
|
|
|
saving,
|
|
|
}: {
|
|
|
+ kbId: string
|
|
|
dirs: KBDirNode[]
|
|
|
ignoredDirs: string[]
|
|
|
onToggle: (path: string, shouldIgnore: boolean) => void
|
|
|
@@ -161,7 +186,6 @@ function DirIgnorePanel({
|
|
|
|
|
|
return (
|
|
|
<div className="border border-gray-200 rounded-xl overflow-hidden">
|
|
|
- {/* Header */}
|
|
|
<button
|
|
|
onClick={() => setOpen(o => !o)}
|
|
|
className="w-full flex items-center justify-between px-4 py-2.5 bg-gray-50 hover:bg-gray-100 transition-colors"
|
|
|
@@ -171,14 +195,13 @@ function DirIgnorePanel({
|
|
|
目录过滤
|
|
|
{ignoredCount > 0 && (
|
|
|
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-xs bg-orange-100 text-orange-700 font-medium">
|
|
|
- <EyeOff size={10} /> {ignoredCount} 已隐藏
|
|
|
+ <EyeOff size={10}/> {ignoredCount} 已隐藏
|
|
|
</span>
|
|
|
)}
|
|
|
</div>
|
|
|
- <ChevronDown size={14} className={`text-gray-400 transition-transform ${open ? 'rotate-180' : ''}`} />
|
|
|
+ <ChevronDown size={14} className={`text-gray-400 transition-transform ${open ? 'rotate-180' : ''}`}/>
|
|
|
</button>
|
|
|
|
|
|
- {/* Tree */}
|
|
|
{open && (
|
|
|
<>
|
|
|
{dirs.length === 0 ? (
|
|
|
@@ -188,6 +211,7 @@ function DirIgnorePanel({
|
|
|
{dirs.map(node => (
|
|
|
<DirTreeNode
|
|
|
key={node.relative_path}
|
|
|
+ kbId={kbId}
|
|
|
node={node}
|
|
|
ignoredSet={ignoredSet}
|
|
|
ancestorIgnored={false}
|
|
|
@@ -198,17 +222,11 @@ function DirIgnorePanel({
|
|
|
))}
|
|
|
</div>
|
|
|
)}
|
|
|
-
|
|
|
{ignoredCount > 0 && (
|
|
|
<div className="px-4 py-2 bg-orange-50 border-t border-orange-100 flex items-center justify-between">
|
|
|
- <p className="text-xs text-orange-600">
|
|
|
- 已隐藏的目录不会出现在文件列表中,也不会被加入知识体。
|
|
|
- </p>
|
|
|
- <button
|
|
|
- onClick={onClearAll}
|
|
|
- disabled={saving}
|
|
|
- className="text-xs text-orange-700 hover:underline ml-4 shrink-0 disabled:opacity-50"
|
|
|
- >
|
|
|
+ <p className="text-xs text-orange-600">已隐藏的目录不会出现在文件列表中,也不会被加入知识体。</p>
|
|
|
+ <button onClick={onClearAll} disabled={saving}
|
|
|
+ className="text-xs text-orange-700 hover:underline ml-4 shrink-0 disabled:opacity-50">
|
|
|
全部恢复
|
|
|
</button>
|
|
|
</div>
|
|
|
@@ -222,11 +240,23 @@ function DirIgnorePanel({
|
|
|
function FileManager({ kbId }: { kbId: string }) {
|
|
|
const qc = useQueryClient()
|
|
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
|
+ // Current browsed subdirectory path (empty = root)
|
|
|
+ const [currentPath, setCurrentPath] = useState('')
|
|
|
+ // Filename search (debounced to query)
|
|
|
+ const [nameInput, setNameInput] = useState('')
|
|
|
+ const [nameFilter, setNameFilter] = useState('')
|
|
|
|
|
|
- const { data, isLoading, refetch } = useQuery({
|
|
|
- queryKey: ['kb-files', kbId],
|
|
|
- queryFn: () => knowledgeApi.listFiles(kbId),
|
|
|
- })
|
|
|
+ // Debounce name filter
|
|
|
+ useEffect(() => {
|
|
|
+ const t = setTimeout(() => setNameFilter(nameInput), 350)
|
|
|
+ return () => clearTimeout(t)
|
|
|
+ }, [nameInput])
|
|
|
+
|
|
|
+ const { data, isLoading, isFetching, refetch } = useQuery({
|
|
|
+ queryKey: ['kb-files', kbId, currentPath, nameFilter],
|
|
|
+ queryFn: () => knowledgeApi.listFiles(kbId, { path: currentPath, q: nameFilter }),
|
|
|
+ keepPreviousData: true,
|
|
|
+ } as any)
|
|
|
|
|
|
const addMut = useMutation({
|
|
|
mutationFn: (paths: string[]) => knowledgeApi.addFiles(kbId, paths),
|
|
|
@@ -245,55 +275,54 @@ function FileManager({ kbId }: { kbId: string }) {
|
|
|
})
|
|
|
|
|
|
const handleToggleDir = (dirPath: string, shouldIgnore: boolean) => {
|
|
|
- const current = new Set(data?.ignored_dirs ?? [])
|
|
|
+ const current = new Set((data as any)?.ignored_dirs ?? [])
|
|
|
if (shouldIgnore) current.add(dirPath)
|
|
|
else current.delete(dirPath)
|
|
|
- ignoreMut.mutate(Array.from(current))
|
|
|
+ ignoreMut.mutate(Array.from(current) as string[])
|
|
|
}
|
|
|
|
|
|
const handleClearAll = () => ignoreMut.mutate([])
|
|
|
|
|
|
- const files = data?.files ?? []
|
|
|
- const dirs = data?.dirs ?? []
|
|
|
- const ignoredDirs = data?.ignored_dirs ?? []
|
|
|
+ const d = data as any
|
|
|
+ const files: KBFile[] = d?.files ?? []
|
|
|
+ const dirs = d?.dirs ?? []
|
|
|
+ const ignoredDirs: string[] = d?.ignored_dirs ?? []
|
|
|
const notAdded = files.filter(f => !f.added)
|
|
|
const added = files.filter(f => f.added)
|
|
|
|
|
|
- const toggle = (path: string) => {
|
|
|
- setSelected(s => {
|
|
|
- const n = new Set(s)
|
|
|
- n.has(path) ? n.delete(path) : n.add(path)
|
|
|
- return n
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
+ const toggle = (path: string) => setSelected(s => {
|
|
|
+ const n = new Set(s); n.has(path) ? n.delete(path) : n.add(path); return n
|
|
|
+ })
|
|
|
const toggleAll = () => {
|
|
|
if (selected.size === notAdded.length) setSelected(new Set())
|
|
|
else setSelected(new Set(notAdded.map(f => f.path)))
|
|
|
}
|
|
|
|
|
|
const extIcon = (type: string) => {
|
|
|
- if (type === 'pdf') return <FileText size={14} className="text-red-400" />
|
|
|
- if (type === 'md') return <FileText size={14} className="text-blue-400" />
|
|
|
- return <File size={14} className="text-gray-400" />
|
|
|
+ if (type === 'pdf') return <FileText size={14} className="text-red-400"/>
|
|
|
+ if (type === 'md') return <FileText size={14} className="text-blue-400"/>
|
|
|
+ return <File size={14} className="text-gray-400"/>
|
|
|
}
|
|
|
-
|
|
|
const fmtSize = (b: number) => b > 1024*1024 ? `${(b/1024/1024).toFixed(1)}MB` : `${(b/1024).toFixed(0)}KB`
|
|
|
|
|
|
- if (isLoading) return <div className="flex justify-center p-8"><Spinner /></div>
|
|
|
+ // Breadcrumb parts
|
|
|
+ const crumbs = currentPath ? currentPath.split('/') : []
|
|
|
+ const navigateTo = (idx: number) => {
|
|
|
+ setCurrentPath(idx < 0 ? '' : crumbs.slice(0, idx + 1).join('/'))
|
|
|
+ setSelected(new Set())
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
<div className="space-y-4">
|
|
|
- {/* Header */}
|
|
|
- <div className="flex items-center justify-between">
|
|
|
- <p className="text-sm text-gray-600">
|
|
|
- 根目录: <code className="bg-gray-100 px-1 rounded text-xs">{data?.root_dir}</code>
|
|
|
- 共 <b>{files.length}</b> 个文件(<b>{added.length}</b> 已加入 / <b>{notAdded.length}</b> 待加入)
|
|
|
+ {/* Header row */}
|
|
|
+ <div className="flex items-center justify-between gap-2 flex-wrap">
|
|
|
+ <p className="text-xs text-gray-500">
|
|
|
+ 根目录: <code className="bg-gray-100 px-1 rounded">{d?.root_dir}</code>
|
|
|
</p>
|
|
|
- <div className="flex gap-2">
|
|
|
- <Button variant="secondary" size="sm" icon={<RefreshCw size={13} />} onClick={() => refetch()}>刷新</Button>
|
|
|
+ <div className="flex gap-2 flex-wrap">
|
|
|
+ <Button variant="secondary" size="sm" icon={<RefreshCw size={13} className={isFetching ? 'animate-spin' : ''}/>} onClick={() => refetch()}>刷新</Button>
|
|
|
{selected.size > 0 && (
|
|
|
- <Button size="sm" icon={<Play size={13} />}
|
|
|
+ <Button size="sm" icon={<Play size={13}/>}
|
|
|
onClick={() => addMut.mutate(Array.from(selected))}
|
|
|
disabled={addMut.isPending}>
|
|
|
加入选中 ({selected.size})
|
|
|
@@ -303,15 +332,46 @@ function FileManager({ kbId }: { kbId: string }) {
|
|
|
<Button size="sm" variant="secondary"
|
|
|
onClick={() => addMut.mutate(notAdded.map(f => f.path))}
|
|
|
disabled={addMut.isPending}>
|
|
|
- 一键加入全部
|
|
|
+ 一键加入全部 ({notAdded.length})
|
|
|
</Button>
|
|
|
)}
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ {/* Breadcrumb + search row */}
|
|
|
+ <div className="flex items-center gap-2 flex-wrap">
|
|
|
+ {/* Breadcrumb */}
|
|
|
+ <div className="flex items-center gap-1 text-sm min-w-0 flex-1">
|
|
|
+ <button onClick={() => navigateTo(-1)}
|
|
|
+ className={`flex items-center gap-1 px-2 py-1 rounded hover:bg-gray-100 transition-colors shrink-0 ${currentPath === '' ? 'text-gray-400 cursor-default pointer-events-none' : 'text-indigo-600'}`}>
|
|
|
+ <FolderOpen size={13}/> <span className="text-xs">根目录</span>
|
|
|
+ </button>
|
|
|
+ {crumbs.map((part, idx) => (
|
|
|
+ <span key={idx} className="flex items-center gap-1 min-w-0">
|
|
|
+ <ChevronRight size={12} className="text-gray-300 shrink-0"/>
|
|
|
+ <button onClick={() => navigateTo(idx)}
|
|
|
+ className={`px-1.5 py-0.5 rounded text-xs font-mono hover:bg-gray-100 truncate max-w-[140px] ${idx === crumbs.length - 1 ? 'text-gray-700 font-semibold' : 'text-indigo-600'}`}>
|
|
|
+ {part}
|
|
|
+ </button>
|
|
|
+ </span>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ {/* Search box */}
|
|
|
+ <div className="relative shrink-0">
|
|
|
+ <Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none"/>
|
|
|
+ <input
|
|
|
+ value={nameInput}
|
|
|
+ onChange={e => setNameInput(e.target.value)}
|
|
|
+ placeholder="文件名过滤..."
|
|
|
+ className="pl-7 pr-3 py-1.5 text-xs border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-400 outline-none w-44"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
{/* Directory ignore panel */}
|
|
|
{dirs.length > 0 && (
|
|
|
<DirIgnorePanel
|
|
|
+ kbId={kbId}
|
|
|
dirs={dirs}
|
|
|
ignoredDirs={ignoredDirs}
|
|
|
onToggle={handleToggleDir}
|
|
|
@@ -320,62 +380,67 @@ function FileManager({ kbId }: { kbId: string }) {
|
|
|
/>
|
|
|
)}
|
|
|
|
|
|
- {/* Already added */}
|
|
|
- {added.length > 0 && (
|
|
|
- <div className="bg-green-50 border border-green-200 rounded-lg overflow-hidden">
|
|
|
- <div className="px-4 py-2 bg-green-100 text-xs font-semibold text-green-700">
|
|
|
- 已加入知识体 ({added.length})
|
|
|
- </div>
|
|
|
- <div className="divide-y divide-green-100 max-h-48 overflow-y-auto">
|
|
|
- {added.map(f => (
|
|
|
- <div key={f.path} className="flex items-center gap-2 px-4 py-2 text-sm">
|
|
|
- {extIcon(f.type)}
|
|
|
- <span className="flex-1 truncate text-gray-700">{f.relative_path}</span>
|
|
|
- <span className="text-xs text-gray-400">{fmtSize(f.size)}</span>
|
|
|
- <CheckCircle size={14} className="text-green-500" />
|
|
|
+ {isLoading ? (
|
|
|
+ <div className="flex justify-center py-8"><Spinner /></div>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ {/* Already added */}
|
|
|
+ {added.length > 0 && (
|
|
|
+ <div className="bg-green-50 border border-green-200 rounded-lg overflow-hidden">
|
|
|
+ <div className="px-4 py-2 bg-green-100 text-xs font-semibold text-green-700">
|
|
|
+ 已加入知识体 ({added.length})
|
|
|
</div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
+ <div className="divide-y divide-green-100 max-h-48 overflow-y-auto">
|
|
|
+ {added.map(f => (
|
|
|
+ <div key={f.path} className="flex items-center gap-2 px-4 py-2 text-sm">
|
|
|
+ {extIcon(f.type)}
|
|
|
+ <span className="flex-1 truncate text-gray-700 font-mono text-xs">{f.relative_path}</span>
|
|
|
+ <span className="text-xs text-gray-400 shrink-0">{fmtSize(f.size)}</span>
|
|
|
+ <CheckCircle size={14} className="text-green-500 shrink-0"/>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
|
|
|
- {/* Not yet added */}
|
|
|
- {notAdded.length > 0 && (
|
|
|
- <div className="border border-gray-200 rounded-lg overflow-hidden">
|
|
|
- <div className="px-4 py-2 bg-gray-50 text-xs font-semibold text-gray-600 flex items-center gap-2">
|
|
|
- <button onClick={toggleAll} className="text-indigo-600 hover:text-indigo-800">
|
|
|
- {selected.size === notAdded.length ? <CheckSquare size={14}/> : <Square size={14}/>}
|
|
|
- </button>
|
|
|
- 待加入 ({notAdded.length})
|
|
|
- </div>
|
|
|
- <div className="divide-y divide-gray-100 max-h-80 overflow-y-auto">
|
|
|
- {notAdded.map(f => (
|
|
|
- <div
|
|
|
- key={f.path}
|
|
|
- onClick={() => toggle(f.path)}
|
|
|
- className={`flex items-center gap-2 px-4 py-2 text-sm cursor-pointer hover:bg-gray-50 ${selected.has(f.path) ? 'bg-indigo-50' : ''}`}
|
|
|
- >
|
|
|
- <button onClick={e => { e.stopPropagation(); toggle(f.path) }}>
|
|
|
- {selected.has(f.path) ? <CheckSquare size={14} className="text-indigo-500"/> : <Square size={14} className="text-gray-300"/>}
|
|
|
+ {/* Not yet added */}
|
|
|
+ {notAdded.length > 0 && (
|
|
|
+ <div className="border border-gray-200 rounded-lg overflow-hidden">
|
|
|
+ <div className="px-4 py-2 bg-gray-50 text-xs font-semibold text-gray-600 flex items-center gap-2">
|
|
|
+ <button onClick={toggleAll} className="text-indigo-600 hover:text-indigo-800">
|
|
|
+ {selected.size === notAdded.length ? <CheckSquare size={14}/> : <Square size={14}/>}
|
|
|
</button>
|
|
|
- {extIcon(f.type)}
|
|
|
- <span className="flex-1 truncate text-gray-700">{f.relative_path}</span>
|
|
|
- <span className="text-xs text-gray-400">{fmtSize(f.size)}</span>
|
|
|
+ 待加入 ({notAdded.length})
|
|
|
+ {nameFilter && <span className="font-normal text-gray-400">(含过滤条件"{nameFilter}")</span>}
|
|
|
</div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
+ <div className="divide-y divide-gray-100 max-h-96 overflow-y-auto">
|
|
|
+ {notAdded.map(f => (
|
|
|
+ <div key={f.path} onClick={() => toggle(f.path)}
|
|
|
+ className={`flex items-center gap-2 px-4 py-2 text-sm cursor-pointer hover:bg-gray-50 ${selected.has(f.path) ? 'bg-indigo-50' : ''}`}>
|
|
|
+ <button onClick={e => { e.stopPropagation(); toggle(f.path) }}>
|
|
|
+ {selected.has(f.path) ? <CheckSquare size={14} className="text-indigo-500"/> : <Square size={14} className="text-gray-300"/>}
|
|
|
+ </button>
|
|
|
+ {extIcon(f.type)}
|
|
|
+ <span className="flex-1 truncate text-gray-700 font-mono text-xs">{f.relative_path}</span>
|
|
|
+ <span className="text-xs text-gray-400 shrink-0">{fmtSize(f.size)}</span>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
|
|
|
- {files.length === 0 && !isLoading && (
|
|
|
- <div className="text-center py-10 text-gray-400">
|
|
|
- <FolderOpen size={32} className="mx-auto mb-2" />
|
|
|
- <p className="text-sm">
|
|
|
- {ignoredDirs.length > 0
|
|
|
- ? `根目录中未找到文件(${ignoredDirs.length} 个目录已隐藏,展开"目录过滤"可恢复)`
|
|
|
- : '根目录中没有找到支持的文件(PDF/TXT/DOCX/MD/HTML/CSV)'}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
+ {files.length === 0 && (
|
|
|
+ <div className="text-center py-10 text-gray-400">
|
|
|
+ <FolderOpen size={32} className="mx-auto mb-2"/>
|
|
|
+ <p className="text-sm">
|
|
|
+ {nameFilter ? `没有匹配"${nameFilter}"的文件` :
|
|
|
+ ignoredDirs.length > 0
|
|
|
+ ? `此目录下未找到文件(${ignoredDirs.length} 个目录已隐藏)`
|
|
|
+ : '此目录没有支持的文件(PDF/TXT/DOCX/MD/HTML/CSV)'}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
)}
|
|
|
</div>
|
|
|
)
|