@lobehub/lobehub 2.0.0-next.54 → 2.0.0-next.56
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/CHANGELOG.md +52 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/common.json +1 -0
- package/locales/ar/file.json +85 -2
- package/locales/bg-BG/common.json +1 -0
- package/locales/bg-BG/file.json +85 -2
- package/locales/de-DE/common.json +1 -0
- package/locales/de-DE/file.json +85 -2
- package/locales/en-US/common.json +1 -0
- package/locales/en-US/file.json +85 -2
- package/locales/es-ES/common.json +1 -0
- package/locales/es-ES/file.json +85 -2
- package/locales/fa-IR/common.json +1 -0
- package/locales/fa-IR/file.json +85 -2
- package/locales/fr-FR/common.json +1 -0
- package/locales/fr-FR/file.json +85 -2
- package/locales/it-IT/common.json +1 -0
- package/locales/it-IT/file.json +85 -2
- package/locales/ja-JP/common.json +1 -0
- package/locales/ja-JP/file.json +85 -2
- package/locales/ko-KR/common.json +1 -0
- package/locales/ko-KR/file.json +85 -2
- package/locales/nl-NL/common.json +1 -0
- package/locales/nl-NL/file.json +85 -2
- package/locales/pl-PL/common.json +1 -0
- package/locales/pl-PL/file.json +85 -2
- package/locales/pt-BR/common.json +1 -0
- package/locales/pt-BR/file.json +85 -2
- package/locales/ru-RU/common.json +1 -0
- package/locales/ru-RU/file.json +85 -2
- package/locales/tr-TR/common.json +1 -0
- package/locales/tr-TR/file.json +85 -2
- package/locales/vi-VN/common.json +1 -0
- package/locales/vi-VN/file.json +85 -2
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-CN/file.json +85 -2
- package/locales/zh-TW/common.json +1 -0
- package/locales/zh-TW/file.json +85 -2
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/file.test.ts +94 -29
- package/packages/database/src/models/file.ts +15 -4
- package/packages/database/src/repositories/knowledge/index.test.ts +300 -0
- package/packages/database/src/repositories/knowledge/index.ts +420 -0
- package/packages/model-bank/src/aiModels/aihubmix.ts +1 -0
- package/packages/model-bank/src/aiModels/google.ts +9 -5
- package/packages/model-bank/src/aiModels/openai.ts +2 -35
- package/packages/model-bank/src/aiModels/openrouter.ts +1 -0
- package/packages/model-bank/src/aiModels/vertexai.ts +2 -0
- package/packages/model-bank/src/types/aiModel.ts +15 -2
- package/packages/model-runtime/src/core/usageConverters/index.ts +1 -0
- package/packages/model-runtime/src/core/usageConverters/utils/resolveImageSinglePrice.ts +34 -0
- package/packages/types/src/document/index.ts +14 -2
- package/packages/types/src/files/index.ts +2 -0
- package/packages/types/src/files/list.ts +10 -0
- package/packages/types/src/llm.ts +1 -1
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +93 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/{ModelSelect.tsx → ModelSelect/index.tsx} +17 -2
- package/src/app/[variants]/(main)/knowledge/KnowledgeRouter.tsx +2 -1
- package/src/app/[variants]/(main)/knowledge/components/KnowledgeBaseItem/index.tsx +0 -2
- package/src/app/[variants]/(main)/knowledge/hooks/useFileCategory.ts +6 -3
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/index.tsx +2 -2
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/{MenuItems.tsx → CategoryMenu.tsx} +3 -3
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/Menu.tsx +2 -2
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/index.tsx +40 -18
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/layout/Container.tsx +1 -1
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/CategoryMenu.tsx +148 -0
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/KnowledgeBase.tsx +20 -7
- package/src/components/FileIcon/index.tsx +3 -1
- package/src/features/ChatInput/ActionBar/Knowledge/index.tsx +2 -2
- package/src/features/FileSidePanel/index.tsx +1 -1
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItem.tsx +80 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItemWrapper.tsx +27 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +104 -23
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/MasonrySkeleton.tsx +62 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/index.tsx +3 -2
- package/src/features/KnowledgeBaseModal/CreateNew/CreateForm.tsx +1 -1
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentActions.tsx +111 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditor.tsx +723 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditorPlaceholder.tsx +169 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentListItem.tsx +148 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentListSkeleton.tsx +39 -0
- package/src/features/KnowledgeManager/DocumentExplorer/NoteEditorModal.tsx +348 -0
- package/src/features/KnowledgeManager/DocumentExplorer/RenamePopover.tsx +163 -0
- package/src/features/KnowledgeManager/DocumentExplorer/index.tsx +318 -0
- package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/index.tsx +48 -9
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/DefaultFileItem.tsx +149 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/ImageFileItem.tsx +245 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/MarkdownFileItem.tsx +232 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/NoteFileItem.tsx +230 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/index.tsx +398 -0
- package/src/features/KnowledgeManager/FileExplorer/ToolBar/ViewSwitcher.tsx +45 -0
- package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/index.tsx +55 -16
- package/src/features/KnowledgeManager/Header/AddButton.tsx +118 -0
- package/src/features/KnowledgeManager/Header/NewNoteButton.tsx +33 -0
- package/src/features/{FileManager → KnowledgeManager}/Header/index.tsx +3 -9
- package/src/features/KnowledgeManager/Home/RecentDocumentCard.tsx +116 -0
- package/src/features/KnowledgeManager/Home/RecentDocuments.tsx +77 -0
- package/src/features/KnowledgeManager/Home/RecentFileCard.tsx +121 -0
- package/src/features/KnowledgeManager/Home/RecentFiles.tsx +73 -0
- package/src/features/KnowledgeManager/Home/RecentFilesSkeleton.tsx +81 -0
- package/src/features/KnowledgeManager/Home/UploadEntries.tsx +208 -0
- package/src/features/KnowledgeManager/Home/index.tsx +221 -0
- package/src/features/KnowledgeManager/index.tsx +75 -0
- package/src/features/Portal/FilePreview/Body/index.tsx +1 -1
- package/src/features/Portal/FilePreview/Header.tsx +1 -1
- package/src/locales/default/common.ts +1 -0
- package/src/locales/default/file.ts +87 -2
- package/src/server/routers/lambda/__tests__/file.test.ts +85 -6
- package/src/server/routers/lambda/document.ts +57 -0
- package/src/server/routers/lambda/file.ts +72 -0
- package/src/server/routers/lambda/knowledge.ts +94 -0
- package/src/server/services/document/index.ts +103 -0
- package/src/services/document/index.ts +44 -0
- package/src/services/file/index.ts +5 -3
- package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +125 -229
- package/src/store/aiInfra/slices/aiProvider/action.ts +113 -33
- package/src/store/file/initialState.ts +6 -1
- package/src/store/file/slices/chat/action.ts +3 -3
- package/src/store/file/slices/document/action.ts +359 -0
- package/src/store/file/slices/document/index.ts +3 -0
- package/src/store/file/slices/document/initialState.ts +22 -0
- package/src/store/file/slices/document/selectors.ts +25 -0
- package/src/store/file/slices/fileManager/action.test.ts +16 -9
- package/src/store/file/slices/fileManager/action.ts +11 -11
- package/src/store/file/store.ts +3 -0
- package/src/store/global/initialState.ts +3 -1
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/FileMenu.tsx +0 -75
- package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +0 -582
- package/src/features/FileManager/index.tsx +0 -36
- /package/src/features/{FileManager/FileList/ToolBar → KnowledgeBaseModal/AssignKnowledgeBase}/ViewSwitcher.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/ChunkItem.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Content.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Loading/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/Item.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/index.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/EmptyStatus.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/ChunkTag.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/DropdownMenu.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileSkeleton.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonryFileItem/MasonryItemWrapper.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonrySkeleton.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/Config.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/MultiSelectActions.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/index.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/useCheckTaskStatus.ts +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/FilesSearchBar.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/TogglePanelButton.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/UploadFileButton.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/UploadDock/Item.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/UploadDock/index.tsx +0 -0
|
@@ -12,7 +12,7 @@ import { Virtuoso } from 'react-virtuoso';
|
|
|
12
12
|
|
|
13
13
|
import { useFileStore } from '@/store/file';
|
|
14
14
|
import { useGlobalStore } from '@/store/global';
|
|
15
|
-
import { SortType } from '@/types/files';
|
|
15
|
+
import { FilesTabs, SortType } from '@/types/files';
|
|
16
16
|
|
|
17
17
|
import EmptyStatus from './EmptyStatus';
|
|
18
18
|
import FileListItem, { FILE_DATE_WIDTH, FILE_SIZE_WIDTH } from './FileListItem';
|
|
@@ -40,13 +40,13 @@ const useStyles = createStyles(({ css, token, isDarkMode }) => ({
|
|
|
40
40
|
`,
|
|
41
41
|
}));
|
|
42
42
|
|
|
43
|
-
interface
|
|
43
|
+
interface FileExplorerProps {
|
|
44
44
|
category?: string;
|
|
45
45
|
knowledgeBaseId?: string;
|
|
46
46
|
onOpenFile: (id: string) => void;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const
|
|
49
|
+
const FileExplorer = memo<FileExplorerProps>(({ knowledgeBaseId, category, onOpenFile }) => {
|
|
50
50
|
const { t } = useTranslation('components');
|
|
51
51
|
const { styles } = useStyles();
|
|
52
52
|
|
|
@@ -54,11 +54,19 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
|
|
|
54
54
|
const [viewConfig, setViewConfig] = useState({ showFilesInKnowledgeBase: false });
|
|
55
55
|
const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(null);
|
|
56
56
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
57
|
+
const [isMasonryReady, setIsMasonryReady] = useState(false);
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
// Always use masonry view for Images category, ignore stored preference
|
|
60
|
+
const storedViewMode = useGlobalStore((s) => s.status.fileManagerViewMode);
|
|
61
|
+
const viewMode = (
|
|
62
|
+
category === FilesTabs.Images ? 'masonry' : storedViewMode || 'list'
|
|
63
|
+
) as ViewMode;
|
|
59
64
|
const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
|
|
60
65
|
const setViewMode = (mode: ViewMode) => {
|
|
61
66
|
setIsTransitioning(true);
|
|
67
|
+
if (mode === 'masonry') {
|
|
68
|
+
setIsMasonryReady(false);
|
|
69
|
+
}
|
|
62
70
|
updateSystemStatus({ fileManagerViewMode: mode });
|
|
63
71
|
};
|
|
64
72
|
|
|
@@ -71,7 +79,7 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
|
|
|
71
79
|
setColumnCount(2);
|
|
72
80
|
} else if (width < 1024) {
|
|
73
81
|
setColumnCount(3);
|
|
74
|
-
} else if (width <
|
|
82
|
+
} else if (width < 1536) {
|
|
75
83
|
setColumnCount(4);
|
|
76
84
|
} else {
|
|
77
85
|
setColumnCount(5);
|
|
@@ -100,9 +108,9 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
|
|
|
100
108
|
defaultValue: SortType.Desc,
|
|
101
109
|
});
|
|
102
110
|
|
|
103
|
-
const
|
|
111
|
+
const useFetchKnowledgeItems = useFileStore((s) => s.useFetchKnowledgeItems);
|
|
104
112
|
|
|
105
|
-
const { data, isLoading } =
|
|
113
|
+
const { data, isLoading } = useFetchKnowledgeItems({
|
|
106
114
|
category,
|
|
107
115
|
knowledgeBaseId,
|
|
108
116
|
q: query,
|
|
@@ -124,6 +132,20 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
|
|
|
124
132
|
}
|
|
125
133
|
}, [isTransitioning, viewMode, data]);
|
|
126
134
|
|
|
135
|
+
// Mark masonry as ready after it has time to mount and render
|
|
136
|
+
React.useEffect(() => {
|
|
137
|
+
if (viewMode === 'masonry' && data && !isLoading && !isTransitioning) {
|
|
138
|
+
// Give VirtuosoMasonry enough time to fully render and calculate layout
|
|
139
|
+
const timer = setTimeout(() => {
|
|
140
|
+
setIsMasonryReady(true);
|
|
141
|
+
}, 300);
|
|
142
|
+
return () => clearTimeout(timer);
|
|
143
|
+
} else if (viewMode === 'list') {
|
|
144
|
+
// Reset when switching to list view
|
|
145
|
+
setIsMasonryReady(false);
|
|
146
|
+
}
|
|
147
|
+
}, [viewMode, data, isLoading, isTransitioning]);
|
|
148
|
+
|
|
127
149
|
useCheckTaskStatus(data);
|
|
128
150
|
|
|
129
151
|
// Clean up selected files that no longer exist in the data
|
|
@@ -188,12 +210,8 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
|
|
|
188
210
|
</Flexbox>
|
|
189
211
|
)}
|
|
190
212
|
</Flexbox>
|
|
191
|
-
{isLoading || isTransitioning ? (
|
|
192
|
-
|
|
193
|
-
<MasonrySkeleton columnCount={columnCount} />
|
|
194
|
-
) : (
|
|
195
|
-
<FileSkeleton />
|
|
196
|
-
)
|
|
213
|
+
{isLoading || (viewMode === 'list' && isTransitioning) ? (
|
|
214
|
+
<FileSkeleton />
|
|
197
215
|
) : viewMode === 'list' ? (
|
|
198
216
|
<Virtuoso
|
|
199
217
|
components={{
|
|
@@ -243,8 +261,29 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
|
|
|
243
261
|
style={{ flex: 1 }}
|
|
244
262
|
/>
|
|
245
263
|
) : (
|
|
246
|
-
<div style={{ flex: 1, overflow: 'hidden' }}>
|
|
247
|
-
|
|
264
|
+
<div style={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
|
|
265
|
+
{/* Skeleton overlay */}
|
|
266
|
+
{(isTransitioning || !isMasonryReady) && (
|
|
267
|
+
<div
|
|
268
|
+
style={{
|
|
269
|
+
background: 'inherit',
|
|
270
|
+
inset: 0,
|
|
271
|
+
position: 'absolute',
|
|
272
|
+
zIndex: 10,
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
<MasonrySkeleton columnCount={columnCount} />
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
{/* Masonry content - always rendered but hidden until ready */}
|
|
279
|
+
<div
|
|
280
|
+
style={{
|
|
281
|
+
height: '100%',
|
|
282
|
+
opacity: isMasonryReady ? 1 : 0,
|
|
283
|
+
overflowY: 'auto',
|
|
284
|
+
transition: 'opacity 0.2s ease-in-out',
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
248
287
|
<div style={{ paddingBlockEnd: 64, paddingBlockStart: 12, paddingInline: 24 }}>
|
|
249
288
|
<VirtuosoMasonry
|
|
250
289
|
ItemContent={MasonryItemWrapper}
|
|
@@ -263,4 +302,4 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
|
|
|
263
302
|
);
|
|
264
303
|
});
|
|
265
304
|
|
|
266
|
-
export default
|
|
305
|
+
export default FileExplorer;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button, Dropdown, Icon, MenuProps } from '@lobehub/ui';
|
|
4
|
+
import { Upload } from 'antd';
|
|
5
|
+
import { css, cx } from 'antd-style';
|
|
6
|
+
import { FilePenLine, FileUp, FolderIcon, FolderUp, Plus } from 'lucide-react';
|
|
7
|
+
import { useMemo, useState } from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
|
|
10
|
+
import DragUpload from '@/components/DragUpload';
|
|
11
|
+
import { useFileStore } from '@/store/file';
|
|
12
|
+
|
|
13
|
+
import NoteEditorModal from '../DocumentExplorer/NoteEditorModal';
|
|
14
|
+
|
|
15
|
+
const hotArea = css`
|
|
16
|
+
&::before {
|
|
17
|
+
content: '';
|
|
18
|
+
position: absolute;
|
|
19
|
+
inset: 0;
|
|
20
|
+
background-color: transparent;
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const AddButton = ({ knowledgeBaseId }: { knowledgeBaseId?: string }) => {
|
|
25
|
+
const { t } = useTranslation('file');
|
|
26
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
27
|
+
const pushDockFileList = useFileStore((s) => s.pushDockFileList);
|
|
28
|
+
|
|
29
|
+
const handleOpenNoteEditor = () => {
|
|
30
|
+
setIsModalOpen(true);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleCloseNoteEditor = () => {
|
|
34
|
+
setIsModalOpen(false);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleCreateFolder = () => {
|
|
38
|
+
setIsModalOpen(false);
|
|
39
|
+
console.log('create folder');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const items = useMemo<MenuProps['items']>(
|
|
43
|
+
() => [
|
|
44
|
+
{
|
|
45
|
+
icon: <Icon icon={FilePenLine} />,
|
|
46
|
+
key: 'create-note',
|
|
47
|
+
label: t('header.actions.newPage'),
|
|
48
|
+
onClick: handleOpenNoteEditor,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
icon: <Icon icon={FolderIcon} />,
|
|
52
|
+
key: 'create-folder',
|
|
53
|
+
label: t('header.actions.newFolder'),
|
|
54
|
+
onClick: handleCreateFolder,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'divider',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
icon: <Icon icon={FileUp} />,
|
|
61
|
+
key: 'upload-file',
|
|
62
|
+
label: (
|
|
63
|
+
<Upload
|
|
64
|
+
beforeUpload={async (file) => {
|
|
65
|
+
await pushDockFileList([file], knowledgeBaseId);
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
}}
|
|
69
|
+
multiple={true}
|
|
70
|
+
showUploadList={false}
|
|
71
|
+
>
|
|
72
|
+
<div className={cx(hotArea)}>{t('header.actions.uploadFile')}</div>
|
|
73
|
+
</Upload>
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
icon: <Icon icon={FolderUp} />,
|
|
78
|
+
key: 'upload-folder',
|
|
79
|
+
label: (
|
|
80
|
+
<Upload
|
|
81
|
+
beforeUpload={async (file) => {
|
|
82
|
+
await pushDockFileList([file], knowledgeBaseId);
|
|
83
|
+
|
|
84
|
+
return false;
|
|
85
|
+
}}
|
|
86
|
+
directory
|
|
87
|
+
multiple={true}
|
|
88
|
+
showUploadList={false}
|
|
89
|
+
>
|
|
90
|
+
<div className={cx(hotArea)}>{t('header.actions.uploadFolder')}</div>
|
|
91
|
+
</Upload>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
[knowledgeBaseId, pushDockFileList],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
<Dropdown menu={{ items }} placement="bottomRight">
|
|
101
|
+
<Button icon={Plus} type="primary">
|
|
102
|
+
{t('addKnowledge')}
|
|
103
|
+
</Button>
|
|
104
|
+
</Dropdown>
|
|
105
|
+
<DragUpload
|
|
106
|
+
enabledFiles
|
|
107
|
+
onUploadFiles={(files) => pushDockFileList(files, knowledgeBaseId)}
|
|
108
|
+
/>
|
|
109
|
+
<NoteEditorModal
|
|
110
|
+
knowledgeBaseId={knowledgeBaseId}
|
|
111
|
+
onClose={handleCloseNoteEditor}
|
|
112
|
+
open={isModalOpen}
|
|
113
|
+
/>
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export default AddButton;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@lobehub/ui';
|
|
4
|
+
import { FilePenLine } from 'lucide-react';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
|
|
8
|
+
import NoteEditorModal from '../DocumentExplorer/NoteEditorModal';
|
|
9
|
+
|
|
10
|
+
const NewNoteButton = ({ knowledgeBaseId }: { knowledgeBaseId?: string }) => {
|
|
11
|
+
const { t } = useTranslation('file');
|
|
12
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
13
|
+
|
|
14
|
+
const handleOpen = () => {
|
|
15
|
+
setIsModalOpen(true);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const handleClose = () => {
|
|
19
|
+
setIsModalOpen(false);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<Button icon={FilePenLine} onClick={handleOpen} type="primary">
|
|
25
|
+
{t('header.newDocumentButton')}
|
|
26
|
+
</Button>
|
|
27
|
+
|
|
28
|
+
<NoteEditorModal knowledgeBaseId={knowledgeBaseId} onClose={handleClose} open={isModalOpen} />
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default NewNoteButton;
|
|
@@ -3,20 +3,14 @@
|
|
|
3
3
|
import { ChatHeader } from '@lobehub/ui/chat';
|
|
4
4
|
import { memo } from 'react';
|
|
5
5
|
|
|
6
|
+
import AddButton from './AddButton';
|
|
6
7
|
import FilesSearchBar from './FilesSearchBar';
|
|
7
|
-
import TogglePanelButton from './TogglePanelButton';
|
|
8
|
-
import UploadFileButton from './UploadFileButton';
|
|
9
8
|
|
|
10
9
|
const Header = memo<{ knowledgeBaseId?: string }>(({ knowledgeBaseId }) => {
|
|
11
10
|
return (
|
|
12
11
|
<ChatHeader
|
|
13
|
-
left={
|
|
14
|
-
|
|
15
|
-
<TogglePanelButton />
|
|
16
|
-
<FilesSearchBar />
|
|
17
|
-
</>
|
|
18
|
-
}
|
|
19
|
-
right={<UploadFileButton knowledgeBaseId={knowledgeBaseId} />}
|
|
12
|
+
left={<FilesSearchBar />}
|
|
13
|
+
right={<AddButton knowledgeBaseId={knowledgeBaseId} />}
|
|
20
14
|
styles={{
|
|
21
15
|
left: { padding: 0 },
|
|
22
16
|
}}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createStyles } from 'antd-style';
|
|
4
|
+
import markdownToTxt from 'markdown-to-txt';
|
|
5
|
+
import { memo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { FileListItem } from '@/types/files';
|
|
8
|
+
|
|
9
|
+
// Helper to extract title from markdown content
|
|
10
|
+
const extractTitle = (content: string): string | null => {
|
|
11
|
+
if (!content) return null;
|
|
12
|
+
|
|
13
|
+
// Find first markdown header (# title)
|
|
14
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
15
|
+
return match ? match[1].trim() : null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Helper to extract preview text from note content
|
|
19
|
+
const getPreviewText = (item: FileListItem): string => {
|
|
20
|
+
if (!item.content) return '';
|
|
21
|
+
|
|
22
|
+
// Convert markdown to plain text
|
|
23
|
+
let plainText = markdownToTxt(item.content);
|
|
24
|
+
|
|
25
|
+
// Remove the title line if it exists
|
|
26
|
+
const title = extractTitle(item.content);
|
|
27
|
+
if (title) {
|
|
28
|
+
plainText = plainText.replace(title, '').trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Limit to first 200 characters for preview
|
|
32
|
+
return plainText.slice(0, 200);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const useStyles = createStyles(({ css, token }) => ({
|
|
36
|
+
card: css`
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
user-select: none;
|
|
39
|
+
|
|
40
|
+
position: relative;
|
|
41
|
+
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
|
|
45
|
+
width: 280px;
|
|
46
|
+
height: 180px;
|
|
47
|
+
border: 1px solid ${token.colorBorderSecondary};
|
|
48
|
+
border-radius: ${token.borderRadiusLG}px;
|
|
49
|
+
|
|
50
|
+
background: ${token.colorBgContainer};
|
|
51
|
+
|
|
52
|
+
transition: all ${token.motionDurationMid};
|
|
53
|
+
|
|
54
|
+
&:hover {
|
|
55
|
+
border-color: ${token.colorPrimary};
|
|
56
|
+
box-shadow: ${token.boxShadowTertiary};
|
|
57
|
+
}
|
|
58
|
+
`,
|
|
59
|
+
noteContent: css`
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 8px;
|
|
64
|
+
|
|
65
|
+
height: 100%;
|
|
66
|
+
padding: 12px;
|
|
67
|
+
`,
|
|
68
|
+
notePreview: css`
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
display: -webkit-box;
|
|
71
|
+
flex: 1;
|
|
72
|
+
-webkit-box-orient: vertical;
|
|
73
|
+
-webkit-line-clamp: 3;
|
|
74
|
+
|
|
75
|
+
font-size: 13px;
|
|
76
|
+
line-height: 1.6;
|
|
77
|
+
color: ${token.colorTextSecondary};
|
|
78
|
+
`,
|
|
79
|
+
noteTitle: css`
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
|
|
82
|
+
font-size: 14px;
|
|
83
|
+
font-weight: ${token.fontWeightStrong};
|
|
84
|
+
line-height: 1.4;
|
|
85
|
+
color: ${token.colorText};
|
|
86
|
+
text-overflow: ellipsis;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
`,
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
interface RecentDocumentCardProps {
|
|
92
|
+
document: FileListItem;
|
|
93
|
+
onClick: () => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const RecentDocumentCard = memo<RecentDocumentCardProps>(({ document, onClick }) => {
|
|
97
|
+
const { styles } = useStyles();
|
|
98
|
+
|
|
99
|
+
const title = document.name || '';
|
|
100
|
+
const previewText = getPreviewText(document);
|
|
101
|
+
const emoji = document.metadata?.emoji;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className={styles.card} onClick={onClick} role="button" tabIndex={0}>
|
|
105
|
+
<div className={styles.noteContent}>
|
|
106
|
+
<div style={{ alignItems: 'center', display: 'flex', gap: 8 }}>
|
|
107
|
+
{emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}
|
|
108
|
+
<div className={styles.noteTitle}>{title}</div>
|
|
109
|
+
</div>
|
|
110
|
+
{previewText && <div className={styles.notePreview}>{previewText}</div>}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export default RecentDocumentCard;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createStyles } from 'antd-style';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { FileListItem } from '@/types/files';
|
|
7
|
+
|
|
8
|
+
import RecentDocumentCard from './RecentDocumentCard';
|
|
9
|
+
import RecentFilesSkeleton from './RecentFilesSkeleton';
|
|
10
|
+
|
|
11
|
+
const useStyles = createStyles(({ css, token }) => ({
|
|
12
|
+
container: css`
|
|
13
|
+
position: relative;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
`,
|
|
16
|
+
fadeEdge: css`
|
|
17
|
+
pointer-events: none;
|
|
18
|
+
|
|
19
|
+
position: absolute;
|
|
20
|
+
inset-block: 0 0;
|
|
21
|
+
inset-inline-end: 0;
|
|
22
|
+
|
|
23
|
+
width: 80px;
|
|
24
|
+
|
|
25
|
+
background: linear-gradient(to left, ${token.colorBgContainerSecondary}, transparent);
|
|
26
|
+
`,
|
|
27
|
+
scrollContainer: css`
|
|
28
|
+
scroll-behavior: smooth;
|
|
29
|
+
|
|
30
|
+
/* Hide scrollbar */
|
|
31
|
+
scrollbar-width: none;
|
|
32
|
+
|
|
33
|
+
overflow: auto hidden;
|
|
34
|
+
display: flex;
|
|
35
|
+
gap: 16px;
|
|
36
|
+
|
|
37
|
+
padding-block-end: 8px;
|
|
38
|
+
padding-inline-end: 80px;
|
|
39
|
+
|
|
40
|
+
-ms-overflow-style: none;
|
|
41
|
+
|
|
42
|
+
&::-webkit-scrollbar {
|
|
43
|
+
display: none;
|
|
44
|
+
}
|
|
45
|
+
`,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
interface RecentDocumentsProps {
|
|
49
|
+
documents: FileListItem[];
|
|
50
|
+
isLoading?: boolean;
|
|
51
|
+
onOpenDocument: (id: string) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const RecentDocuments = memo<RecentDocumentsProps>(({ documents, isLoading, onOpenDocument }) => {
|
|
55
|
+
const { styles } = useStyles();
|
|
56
|
+
|
|
57
|
+
if (isLoading) {
|
|
58
|
+
return <RecentFilesSkeleton />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={styles.container}>
|
|
63
|
+
<div className={styles.scrollContainer}>
|
|
64
|
+
{documents.map((document) => (
|
|
65
|
+
<RecentDocumentCard
|
|
66
|
+
document={document}
|
|
67
|
+
key={document.id}
|
|
68
|
+
onClick={() => onOpenDocument(document.id)}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
<div className={styles.fadeEdge} />
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export default RecentDocuments;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { formatSize } from '@lobechat/utils/format';
|
|
4
|
+
import { Image as LobeImage, Text } from '@lobehub/ui';
|
|
5
|
+
import { createStyles } from 'antd-style';
|
|
6
|
+
import dayjs from 'dayjs';
|
|
7
|
+
import { memo } from 'react';
|
|
8
|
+
import { Flexbox } from 'react-layout-kit';
|
|
9
|
+
|
|
10
|
+
import FileIcon from '@/components/FileIcon';
|
|
11
|
+
import { FileListItem } from '@/types/files';
|
|
12
|
+
|
|
13
|
+
const IMAGE_FILE_TYPES = new Set([
|
|
14
|
+
'image/png',
|
|
15
|
+
'image/jpeg',
|
|
16
|
+
'image/jpg',
|
|
17
|
+
'image/gif',
|
|
18
|
+
'image/webp',
|
|
19
|
+
'image/svg+xml',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const useStyles = createStyles(({ css, token }) => ({
|
|
23
|
+
card: css`
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
|
|
26
|
+
position: relative;
|
|
27
|
+
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
|
|
31
|
+
width: 280px;
|
|
32
|
+
padding: 12px;
|
|
33
|
+
border: 1px solid ${token.colorBorderSecondary};
|
|
34
|
+
border-radius: ${token.borderRadiusLG}px;
|
|
35
|
+
|
|
36
|
+
background: ${token.colorBgContainer};
|
|
37
|
+
|
|
38
|
+
transition: all ${token.motionDurationMid};
|
|
39
|
+
|
|
40
|
+
&:hover {
|
|
41
|
+
border-color: ${token.colorPrimary};
|
|
42
|
+
box-shadow: ${token.boxShadowTertiary};
|
|
43
|
+
}
|
|
44
|
+
`,
|
|
45
|
+
iconWrapper: css`
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
|
|
50
|
+
height: 160px;
|
|
51
|
+
margin-block-end: 12px;
|
|
52
|
+
border-radius: ${token.borderRadius}px;
|
|
53
|
+
|
|
54
|
+
background: ${token.colorBgLayout};
|
|
55
|
+
`,
|
|
56
|
+
imagePreview: css`
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: 160px;
|
|
59
|
+
margin-block-end: 12px;
|
|
60
|
+
border-radius: ${token.borderRadius}px;
|
|
61
|
+
|
|
62
|
+
object-fit: cover;
|
|
63
|
+
background: ${token.colorBgLayout};
|
|
64
|
+
`,
|
|
65
|
+
info: css`
|
|
66
|
+
font-size: 12px;
|
|
67
|
+
color: ${token.colorTextDescription};
|
|
68
|
+
`,
|
|
69
|
+
title: css`
|
|
70
|
+
margin: 0 !important;
|
|
71
|
+
font-size: 14px;
|
|
72
|
+
font-weight: 500;
|
|
73
|
+
line-height: 1.4;
|
|
74
|
+
`,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
interface RecentFileCardProps {
|
|
78
|
+
file: FileListItem;
|
|
79
|
+
onClick: () => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const RecentFileCard = memo<RecentFileCardProps>(({ file, onClick }) => {
|
|
83
|
+
const { styles } = useStyles();
|
|
84
|
+
|
|
85
|
+
const isImage = IMAGE_FILE_TYPES.has(file.fileType);
|
|
86
|
+
const relativeTime = dayjs(file.updatedAt).fromNow();
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className={styles.card} onClick={onClick} role="button" tabIndex={0}>
|
|
90
|
+
<Flexbox gap={12} style={{ position: 'relative' }}>
|
|
91
|
+
{/* Preview or Icon */}
|
|
92
|
+
{isImage && file.url ? (
|
|
93
|
+
<LobeImage
|
|
94
|
+
alt={file.name}
|
|
95
|
+
className={styles.imagePreview}
|
|
96
|
+
preview={false}
|
|
97
|
+
src={file.url}
|
|
98
|
+
/>
|
|
99
|
+
) : (
|
|
100
|
+
<div className={styles.iconWrapper}>
|
|
101
|
+
<FileIcon fileName={file.name} fileType={file.fileType} size={48} />
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{/* File Info */}
|
|
106
|
+
<Flexbox gap={6} style={{ overflow: 'hidden', position: 'relative' }}>
|
|
107
|
+
<Text className={styles.title} ellipsis={{ rows: 2 }}>
|
|
108
|
+
{file.name}
|
|
109
|
+
</Text>
|
|
110
|
+
<Flexbox className={styles.info} gap={8} horizontal>
|
|
111
|
+
<span>{relativeTime}</span>
|
|
112
|
+
<span>•</span>
|
|
113
|
+
<span>{formatSize(file.size)}</span>
|
|
114
|
+
</Flexbox>
|
|
115
|
+
</Flexbox>
|
|
116
|
+
</Flexbox>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export default RecentFileCard;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createStyles } from 'antd-style';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { FileListItem } from '@/types/files';
|
|
7
|
+
|
|
8
|
+
import RecentFileCard from './RecentFileCard';
|
|
9
|
+
import RecentFilesSkeleton from './RecentFilesSkeleton';
|
|
10
|
+
|
|
11
|
+
const useStyles = createStyles(({ css, token }) => ({
|
|
12
|
+
container: css`
|
|
13
|
+
position: relative;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
`,
|
|
16
|
+
fadeEdge: css`
|
|
17
|
+
pointer-events: none;
|
|
18
|
+
|
|
19
|
+
position: absolute;
|
|
20
|
+
inset-block: 0 0;
|
|
21
|
+
inset-inline-end: 0;
|
|
22
|
+
|
|
23
|
+
width: 80px;
|
|
24
|
+
|
|
25
|
+
background: linear-gradient(to left, ${token.colorBgContainerSecondary}, transparent);
|
|
26
|
+
`,
|
|
27
|
+
scrollContainer: css`
|
|
28
|
+
scroll-behavior: smooth;
|
|
29
|
+
|
|
30
|
+
/* Hide scrollbar */
|
|
31
|
+
scrollbar-width: none;
|
|
32
|
+
|
|
33
|
+
overflow: auto hidden;
|
|
34
|
+
display: flex;
|
|
35
|
+
gap: 16px;
|
|
36
|
+
|
|
37
|
+
padding-block-end: 8px;
|
|
38
|
+
padding-inline-end: 80px;
|
|
39
|
+
|
|
40
|
+
-ms-overflow-style: none;
|
|
41
|
+
|
|
42
|
+
&::-webkit-scrollbar {
|
|
43
|
+
display: none;
|
|
44
|
+
}
|
|
45
|
+
`,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
interface RecentFilesProps {
|
|
49
|
+
files: FileListItem[];
|
|
50
|
+
isLoading?: boolean;
|
|
51
|
+
onOpenFile: (id: string) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const RecentFiles = memo<RecentFilesProps>(({ files, isLoading, onOpenFile }) => {
|
|
55
|
+
const { styles } = useStyles();
|
|
56
|
+
|
|
57
|
+
if (isLoading) {
|
|
58
|
+
return <RecentFilesSkeleton />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={styles.container}>
|
|
63
|
+
<div className={styles.scrollContainer}>
|
|
64
|
+
{files.map((file) => (
|
|
65
|
+
<RecentFileCard file={file} key={file.id} onClick={() => onOpenFile(file.id)} />
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
<div className={styles.fadeEdge} />
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export default RecentFiles;
|