@lobehub/lobehub 2.0.0-next.53 → 2.0.0-next.55

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/common.json +1 -0
  4. package/locales/ar/file.json +85 -2
  5. package/locales/bg-BG/common.json +1 -0
  6. package/locales/bg-BG/file.json +85 -2
  7. package/locales/de-DE/common.json +1 -0
  8. package/locales/de-DE/file.json +85 -2
  9. package/locales/en-US/common.json +1 -0
  10. package/locales/en-US/file.json +85 -2
  11. package/locales/es-ES/common.json +1 -0
  12. package/locales/es-ES/file.json +85 -2
  13. package/locales/fa-IR/common.json +1 -0
  14. package/locales/fa-IR/file.json +85 -2
  15. package/locales/fr-FR/common.json +1 -0
  16. package/locales/fr-FR/file.json +85 -2
  17. package/locales/it-IT/common.json +1 -0
  18. package/locales/it-IT/file.json +85 -2
  19. package/locales/ja-JP/common.json +1 -0
  20. package/locales/ja-JP/file.json +85 -2
  21. package/locales/ko-KR/common.json +1 -0
  22. package/locales/ko-KR/file.json +85 -2
  23. package/locales/nl-NL/common.json +1 -0
  24. package/locales/nl-NL/file.json +85 -2
  25. package/locales/pl-PL/common.json +1 -0
  26. package/locales/pl-PL/file.json +85 -2
  27. package/locales/pt-BR/common.json +1 -0
  28. package/locales/pt-BR/file.json +85 -2
  29. package/locales/ru-RU/common.json +1 -0
  30. package/locales/ru-RU/file.json +85 -2
  31. package/locales/tr-TR/common.json +1 -0
  32. package/locales/tr-TR/file.json +85 -2
  33. package/locales/vi-VN/common.json +1 -0
  34. package/locales/vi-VN/file.json +85 -2
  35. package/locales/zh-CN/common.json +1 -0
  36. package/locales/zh-CN/file.json +85 -2
  37. package/locales/zh-TW/common.json +1 -0
  38. package/locales/zh-TW/file.json +85 -2
  39. package/package.json +1 -1
  40. package/packages/database/src/models/__tests__/file.test.ts +94 -29
  41. package/packages/database/src/models/file.ts +15 -4
  42. package/packages/database/src/repositories/knowledge/index.test.ts +300 -0
  43. package/packages/database/src/repositories/knowledge/index.ts +420 -0
  44. package/packages/model-bank/src/aiModels/aihubmix.ts +1 -0
  45. package/packages/model-bank/src/aiModels/google.ts +9 -5
  46. package/packages/model-bank/src/aiModels/openai.ts +2 -35
  47. package/packages/model-bank/src/aiModels/openrouter.ts +1 -0
  48. package/packages/model-bank/src/aiModels/vertexai.ts +2 -0
  49. package/packages/model-bank/src/types/aiModel.ts +15 -2
  50. package/packages/model-runtime/src/core/usageConverters/index.ts +1 -0
  51. package/packages/model-runtime/src/core/usageConverters/utils/resolveImageSinglePrice.ts +34 -0
  52. package/packages/types/src/document/index.ts +14 -2
  53. package/packages/types/src/files/index.ts +2 -0
  54. package/packages/types/src/files/list.ts +10 -0
  55. package/packages/types/src/llm.ts +1 -1
  56. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +93 -0
  57. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/{ModelSelect.tsx → ModelSelect/index.tsx} +17 -2
  58. package/src/app/[variants]/(main)/knowledge/KnowledgeRouter.tsx +2 -1
  59. package/src/app/[variants]/(main)/knowledge/components/KnowledgeBaseItem/index.tsx +0 -2
  60. package/src/app/[variants]/(main)/knowledge/hooks/useFileCategory.ts +6 -3
  61. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/index.tsx +2 -2
  62. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/{MenuItems.tsx → CategoryMenu.tsx} +3 -3
  63. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/Menu.tsx +2 -2
  64. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/index.tsx +40 -18
  65. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/layout/Container.tsx +1 -1
  66. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/CategoryMenu.tsx +148 -0
  67. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/KnowledgeBase.tsx +20 -7
  68. package/src/components/FileIcon/index.tsx +3 -1
  69. package/src/features/ChatInput/ActionBar/Knowledge/index.tsx +2 -2
  70. package/src/features/Conversation/Messages/Assistant/index.tsx +7 -1
  71. package/src/features/FileSidePanel/index.tsx +1 -1
  72. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItem.tsx +80 -0
  73. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItemWrapper.tsx +27 -0
  74. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +104 -23
  75. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/MasonrySkeleton.tsx +62 -0
  76. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/index.tsx +3 -2
  77. package/src/features/KnowledgeBaseModal/CreateNew/CreateForm.tsx +1 -1
  78. package/src/features/KnowledgeManager/DocumentExplorer/DocumentActions.tsx +111 -0
  79. package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditor.tsx +723 -0
  80. package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditorPlaceholder.tsx +169 -0
  81. package/src/features/KnowledgeManager/DocumentExplorer/DocumentListItem.tsx +148 -0
  82. package/src/features/KnowledgeManager/DocumentExplorer/DocumentListSkeleton.tsx +39 -0
  83. package/src/features/KnowledgeManager/DocumentExplorer/NoteEditorModal.tsx +348 -0
  84. package/src/features/KnowledgeManager/DocumentExplorer/RenamePopover.tsx +163 -0
  85. package/src/features/KnowledgeManager/DocumentExplorer/index.tsx +318 -0
  86. package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/index.tsx +48 -9
  87. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/DefaultFileItem.tsx +149 -0
  88. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/ImageFileItem.tsx +245 -0
  89. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/MarkdownFileItem.tsx +232 -0
  90. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/NoteFileItem.tsx +230 -0
  91. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/index.tsx +398 -0
  92. package/src/features/KnowledgeManager/FileExplorer/ToolBar/ViewSwitcher.tsx +45 -0
  93. package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/index.tsx +68 -16
  94. package/src/features/KnowledgeManager/Header/AddButton.tsx +107 -0
  95. package/src/features/KnowledgeManager/Header/NewNoteButton.tsx +33 -0
  96. package/src/features/{FileManager → KnowledgeManager}/Header/index.tsx +3 -9
  97. package/src/features/KnowledgeManager/Home/RecentDocumentCard.tsx +116 -0
  98. package/src/features/KnowledgeManager/Home/RecentDocuments.tsx +77 -0
  99. package/src/features/KnowledgeManager/Home/RecentFileCard.tsx +121 -0
  100. package/src/features/KnowledgeManager/Home/RecentFiles.tsx +73 -0
  101. package/src/features/KnowledgeManager/Home/RecentFilesSkeleton.tsx +83 -0
  102. package/src/features/KnowledgeManager/Home/UploadEntries.tsx +208 -0
  103. package/src/features/KnowledgeManager/Home/index.tsx +221 -0
  104. package/src/features/KnowledgeManager/index.tsx +75 -0
  105. package/src/features/Portal/FilePreview/Body/index.tsx +1 -1
  106. package/src/features/Portal/FilePreview/Header.tsx +1 -1
  107. package/src/locales/default/common.ts +1 -0
  108. package/src/locales/default/file.ts +85 -2
  109. package/src/locales/default/tool.ts +8 -0
  110. package/src/server/routers/lambda/__tests__/file.test.ts +85 -6
  111. package/src/server/routers/lambda/document.ts +57 -0
  112. package/src/server/routers/lambda/file.ts +72 -0
  113. package/src/server/routers/lambda/knowledge.ts +94 -0
  114. package/src/server/services/document/index.ts +103 -0
  115. package/src/services/document/index.ts +44 -0
  116. package/src/services/file/index.ts +5 -3
  117. package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +125 -229
  118. package/src/store/aiInfra/slices/aiProvider/action.ts +113 -33
  119. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +1 -1
  120. package/src/store/file/initialState.ts +6 -1
  121. package/src/store/file/slices/chat/action.ts +3 -3
  122. package/src/store/file/slices/document/action.ts +359 -0
  123. package/src/store/file/slices/document/index.ts +3 -0
  124. package/src/store/file/slices/document/initialState.ts +22 -0
  125. package/src/store/file/slices/document/selectors.ts +25 -0
  126. package/src/store/file/slices/fileManager/action.test.ts +16 -9
  127. package/src/store/file/slices/fileManager/action.ts +11 -11
  128. package/src/store/file/store.ts +3 -0
  129. package/src/store/global/initialState.ts +3 -1
  130. package/src/tools/interventions.ts +3 -5
  131. package/src/tools/local-system/Intervention/MoveLocalFiles/MoveFileItem.tsx +56 -0
  132. package/src/tools/local-system/Intervention/MoveLocalFiles/index.tsx +26 -0
  133. package/src/tools/local-system/Intervention/RunCommand/index.tsx +1 -2
  134. package/src/tools/local-system/Intervention/index.ts +11 -0
  135. package/src/tools/local-system/Render/MoveLocalFiles/MoveFileItem.tsx +56 -0
  136. package/src/tools/local-system/Render/MoveLocalFiles/index.tsx +26 -0
  137. package/src/tools/local-system/Render/index.ts +21 -0
  138. package/src/tools/renders.ts +6 -24
  139. package/src/tools/web-browsing/Render/index.ts +13 -0
  140. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/FileMenu.tsx +0 -75
  141. package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +0 -582
  142. package/src/features/FileManager/index.tsx +0 -36
  143. /package/src/features/{FileManager/FileList/ToolBar → KnowledgeBaseModal/AssignKnowledgeBase}/ViewSwitcher.tsx +0 -0
  144. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/ChunkItem.tsx +0 -0
  145. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/index.tsx +0 -0
  146. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Content.tsx +0 -0
  147. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Loading/index.tsx +0 -0
  148. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/Item.tsx +0 -0
  149. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/index.tsx +0 -0
  150. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/index.tsx +0 -0
  151. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/EmptyStatus.tsx +0 -0
  152. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/ChunkTag.tsx +0 -0
  153. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/DropdownMenu.tsx +0 -0
  154. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileSkeleton.tsx +0 -0
  155. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonryFileItem/MasonryItemWrapper.tsx +0 -0
  156. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonrySkeleton.tsx +0 -0
  157. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/Config.tsx +0 -0
  158. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/MultiSelectActions.tsx +0 -0
  159. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/index.tsx +0 -0
  160. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/useCheckTaskStatus.ts +0 -0
  161. /package/src/features/{FileManager → KnowledgeManager}/Header/FilesSearchBar.tsx +0 -0
  162. /package/src/features/{FileManager → KnowledgeManager}/Header/TogglePanelButton.tsx +0 -0
  163. /package/src/features/{FileManager → KnowledgeManager}/Header/UploadFileButton.tsx +0 -0
  164. /package/src/features/{FileManager → KnowledgeManager}/UploadDock/Item.tsx +0 -0
  165. /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 FileListProps {
43
+ interface FileExplorerProps {
44
44
  category?: string;
45
45
  knowledgeBaseId?: string;
46
46
  onOpenFile: (id: string) => void;
47
47
  }
48
48
 
49
- const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile }) => {
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
- const viewMode = useGlobalStore((s) => s.status.fileManagerViewMode || 'list') as ViewMode;
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 < 1440) {
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 useFetchFileManage = useFileStore((s) => s.useFetchFileManage);
111
+ const useFetchKnowledgeItems = useFileStore((s) => s.useFetchKnowledgeItems);
104
112
 
105
- const { data, isLoading } = useFetchFileManage({
113
+ const { data, isLoading } = useFetchKnowledgeItems({
106
114
  category,
107
115
  knowledgeBaseId,
108
116
  q: query,
@@ -111,6 +119,19 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
111
119
  ...viewConfig,
112
120
  });
113
121
 
122
+ // Debug: Log received data
123
+ React.useEffect(() => {
124
+ if (data) {
125
+ console.log('[FileList] Received data:', {
126
+ count: data.length,
127
+ documents: data.filter((item) => item.sourceType === 'document'),
128
+ sampleDocumentWithEditorData: data.find(
129
+ (item) => item.sourceType === 'document' && item.editorData,
130
+ ),
131
+ });
132
+ }
133
+ }, [data]);
134
+
114
135
  // Handle view transition with a brief delay to show skeleton
115
136
  React.useEffect(() => {
116
137
  if (isTransitioning && data) {
@@ -124,6 +145,20 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
124
145
  }
125
146
  }, [isTransitioning, viewMode, data]);
126
147
 
148
+ // Mark masonry as ready after it has time to mount and render
149
+ React.useEffect(() => {
150
+ if (viewMode === 'masonry' && data && !isLoading && !isTransitioning) {
151
+ // Give VirtuosoMasonry enough time to fully render and calculate layout
152
+ const timer = setTimeout(() => {
153
+ setIsMasonryReady(true);
154
+ }, 300);
155
+ return () => clearTimeout(timer);
156
+ } else if (viewMode === 'list') {
157
+ // Reset when switching to list view
158
+ setIsMasonryReady(false);
159
+ }
160
+ }, [viewMode, data, isLoading, isTransitioning]);
161
+
127
162
  useCheckTaskStatus(data);
128
163
 
129
164
  // Clean up selected files that no longer exist in the data
@@ -188,12 +223,8 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
188
223
  </Flexbox>
189
224
  )}
190
225
  </Flexbox>
191
- {isLoading || isTransitioning ? (
192
- viewMode === 'masonry' ? (
193
- <MasonrySkeleton columnCount={columnCount} />
194
- ) : (
195
- <FileSkeleton />
196
- )
226
+ {isLoading || (viewMode === 'list' && isTransitioning) ? (
227
+ <FileSkeleton />
197
228
  ) : viewMode === 'list' ? (
198
229
  <Virtuoso
199
230
  components={{
@@ -243,8 +274,29 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
243
274
  style={{ flex: 1 }}
244
275
  />
245
276
  ) : (
246
- <div style={{ flex: 1, overflow: 'hidden' }}>
247
- <div style={{ height: '100%', overflowY: 'auto' }}>
277
+ <div style={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
278
+ {/* Skeleton overlay */}
279
+ {(isTransitioning || !isMasonryReady) && (
280
+ <div
281
+ style={{
282
+ background: 'inherit',
283
+ inset: 0,
284
+ position: 'absolute',
285
+ zIndex: 10,
286
+ }}
287
+ >
288
+ <MasonrySkeleton columnCount={columnCount} />
289
+ </div>
290
+ )}
291
+ {/* Masonry content - always rendered but hidden until ready */}
292
+ <div
293
+ style={{
294
+ height: '100%',
295
+ opacity: isMasonryReady ? 1 : 0,
296
+ overflowY: 'auto',
297
+ transition: 'opacity 0.2s ease-in-out',
298
+ }}
299
+ >
248
300
  <div style={{ paddingBlockEnd: 64, paddingBlockStart: 12, paddingInline: 24 }}>
249
301
  <VirtuosoMasonry
250
302
  ItemContent={MasonryItemWrapper}
@@ -263,4 +315,4 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category, onOpenFile })
263
315
  );
264
316
  });
265
317
 
266
- export default FileList;
318
+ export default FileExplorer;
@@ -0,0 +1,107 @@
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, 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 items = useMemo<MenuProps['items']>(
38
+ () => [
39
+ {
40
+ icon: <Icon icon={FilePenLine} />,
41
+ key: 'create-note',
42
+ label: t('addPage'),
43
+ onClick: handleOpenNoteEditor,
44
+ },
45
+ {
46
+ type: 'divider',
47
+ },
48
+ {
49
+ icon: <Icon icon={FileUp} />,
50
+ key: 'upload-file',
51
+ label: (
52
+ <Upload
53
+ beforeUpload={async (file) => {
54
+ await pushDockFileList([file], knowledgeBaseId);
55
+
56
+ return false;
57
+ }}
58
+ multiple={true}
59
+ showUploadList={false}
60
+ >
61
+ <div className={cx(hotArea)}>Upload File</div>
62
+ </Upload>
63
+ ),
64
+ },
65
+ {
66
+ icon: <Icon icon={FolderUp} />,
67
+ key: 'upload-folder',
68
+ label: (
69
+ <Upload
70
+ beforeUpload={async (file) => {
71
+ await pushDockFileList([file], knowledgeBaseId);
72
+
73
+ return false;
74
+ }}
75
+ directory
76
+ multiple={true}
77
+ showUploadList={false}
78
+ >
79
+ <div className={cx(hotArea)}>Upload Folder</div>
80
+ </Upload>
81
+ ),
82
+ },
83
+ ],
84
+ [knowledgeBaseId, pushDockFileList],
85
+ );
86
+
87
+ return (
88
+ <>
89
+ <Dropdown menu={{ items }} placement="bottomRight">
90
+ <Button icon={Plus} type="primary">
91
+ {t('addKnowledge')}
92
+ </Button>
93
+ </Dropdown>
94
+ <DragUpload
95
+ enabledFiles
96
+ onUploadFiles={(files) => pushDockFileList(files, knowledgeBaseId)}
97
+ />
98
+ <NoteEditorModal
99
+ knowledgeBaseId={knowledgeBaseId}
100
+ onClose={handleCloseNoteEditor}
101
+ open={isModalOpen}
102
+ />
103
+ </>
104
+ );
105
+ };
106
+
107
+ 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;