@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.
Files changed (152) 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/FileSidePanel/index.tsx +1 -1
  71. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItem.tsx +80 -0
  72. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItemWrapper.tsx +27 -0
  73. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +104 -23
  74. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/MasonrySkeleton.tsx +62 -0
  75. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/index.tsx +3 -2
  76. package/src/features/KnowledgeBaseModal/CreateNew/CreateForm.tsx +1 -1
  77. package/src/features/KnowledgeManager/DocumentExplorer/DocumentActions.tsx +111 -0
  78. package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditor.tsx +723 -0
  79. package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditorPlaceholder.tsx +169 -0
  80. package/src/features/KnowledgeManager/DocumentExplorer/DocumentListItem.tsx +148 -0
  81. package/src/features/KnowledgeManager/DocumentExplorer/DocumentListSkeleton.tsx +39 -0
  82. package/src/features/KnowledgeManager/DocumentExplorer/NoteEditorModal.tsx +348 -0
  83. package/src/features/KnowledgeManager/DocumentExplorer/RenamePopover.tsx +163 -0
  84. package/src/features/KnowledgeManager/DocumentExplorer/index.tsx +318 -0
  85. package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/index.tsx +48 -9
  86. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/DefaultFileItem.tsx +149 -0
  87. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/ImageFileItem.tsx +245 -0
  88. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/MarkdownFileItem.tsx +232 -0
  89. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/NoteFileItem.tsx +230 -0
  90. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/index.tsx +398 -0
  91. package/src/features/KnowledgeManager/FileExplorer/ToolBar/ViewSwitcher.tsx +45 -0
  92. package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/index.tsx +55 -16
  93. package/src/features/KnowledgeManager/Header/AddButton.tsx +118 -0
  94. package/src/features/KnowledgeManager/Header/NewNoteButton.tsx +33 -0
  95. package/src/features/{FileManager → KnowledgeManager}/Header/index.tsx +3 -9
  96. package/src/features/KnowledgeManager/Home/RecentDocumentCard.tsx +116 -0
  97. package/src/features/KnowledgeManager/Home/RecentDocuments.tsx +77 -0
  98. package/src/features/KnowledgeManager/Home/RecentFileCard.tsx +121 -0
  99. package/src/features/KnowledgeManager/Home/RecentFiles.tsx +73 -0
  100. package/src/features/KnowledgeManager/Home/RecentFilesSkeleton.tsx +81 -0
  101. package/src/features/KnowledgeManager/Home/UploadEntries.tsx +208 -0
  102. package/src/features/KnowledgeManager/Home/index.tsx +221 -0
  103. package/src/features/KnowledgeManager/index.tsx +75 -0
  104. package/src/features/Portal/FilePreview/Body/index.tsx +1 -1
  105. package/src/features/Portal/FilePreview/Header.tsx +1 -1
  106. package/src/locales/default/common.ts +1 -0
  107. package/src/locales/default/file.ts +87 -2
  108. package/src/server/routers/lambda/__tests__/file.test.ts +85 -6
  109. package/src/server/routers/lambda/document.ts +57 -0
  110. package/src/server/routers/lambda/file.ts +72 -0
  111. package/src/server/routers/lambda/knowledge.ts +94 -0
  112. package/src/server/services/document/index.ts +103 -0
  113. package/src/services/document/index.ts +44 -0
  114. package/src/services/file/index.ts +5 -3
  115. package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +125 -229
  116. package/src/store/aiInfra/slices/aiProvider/action.ts +113 -33
  117. package/src/store/file/initialState.ts +6 -1
  118. package/src/store/file/slices/chat/action.ts +3 -3
  119. package/src/store/file/slices/document/action.ts +359 -0
  120. package/src/store/file/slices/document/index.ts +3 -0
  121. package/src/store/file/slices/document/initialState.ts +22 -0
  122. package/src/store/file/slices/document/selectors.ts +25 -0
  123. package/src/store/file/slices/fileManager/action.test.ts +16 -9
  124. package/src/store/file/slices/fileManager/action.ts +11 -11
  125. package/src/store/file/store.ts +3 -0
  126. package/src/store/global/initialState.ts +3 -1
  127. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/FileMenu.tsx +0 -75
  128. package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +0 -582
  129. package/src/features/FileManager/index.tsx +0 -36
  130. /package/src/features/{FileManager/FileList/ToolBar → KnowledgeBaseModal/AssignKnowledgeBase}/ViewSwitcher.tsx +0 -0
  131. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/ChunkItem.tsx +0 -0
  132. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/index.tsx +0 -0
  133. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Content.tsx +0 -0
  134. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Loading/index.tsx +0 -0
  135. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/Item.tsx +0 -0
  136. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/index.tsx +0 -0
  137. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/index.tsx +0 -0
  138. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/EmptyStatus.tsx +0 -0
  139. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/ChunkTag.tsx +0 -0
  140. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/DropdownMenu.tsx +0 -0
  141. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileSkeleton.tsx +0 -0
  142. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonryFileItem/MasonryItemWrapper.tsx +0 -0
  143. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonrySkeleton.tsx +0 -0
  144. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/Config.tsx +0 -0
  145. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/MultiSelectActions.tsx +0 -0
  146. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/index.tsx +0 -0
  147. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/useCheckTaskStatus.ts +0 -0
  148. /package/src/features/{FileManager → KnowledgeManager}/Header/FilesSearchBar.tsx +0 -0
  149. /package/src/features/{FileManager → KnowledgeManager}/Header/TogglePanelButton.tsx +0 -0
  150. /package/src/features/{FileManager → KnowledgeManager}/Header/UploadFileButton.tsx +0 -0
  151. /package/src/features/{FileManager → KnowledgeManager}/UploadDock/Item.tsx +0 -0
  152. /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,
@@ -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
- viewMode === 'masonry' ? (
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
- <div style={{ height: '100%', overflowY: 'auto' }}>
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 FileList;
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;