@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
@@ -0,0 +1,230 @@
1
+ import { Button, Tooltip } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import { isNull } from 'lodash-es';
4
+ import { FileBoxIcon } from 'lucide-react';
5
+ import markdownToTxt from 'markdown-to-txt';
6
+ import { memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ import FileIcon from '@/components/FileIcon';
10
+ import { fileManagerSelectors, useFileStore } from '@/store/file';
11
+ import { AsyncTaskStatus, IAsyncTaskError } from '@/types/asyncTask';
12
+ import { isChunkingUnsupported } from '@/utils/isChunkingUnsupported';
13
+
14
+ import ChunksBadge from '../FileListItem/ChunkTag';
15
+
16
+ const useStyles = createStyles(({ css, token }) => ({
17
+ floatingChunkBadge: css`
18
+ position: absolute;
19
+ z-index: 3;
20
+ inset-block-end: 8px;
21
+ inset-inline-end: 8px;
22
+
23
+ border-radius: ${token.borderRadius}px;
24
+
25
+ opacity: 0;
26
+ background: ${token.colorBgContainer};
27
+ box-shadow: ${token.boxShadow};
28
+
29
+ transition: opacity ${token.motionDurationMid};
30
+ `,
31
+ iconWrapper: css`
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+
36
+ height: 120px;
37
+ margin-block-end: 12px;
38
+ border-radius: ${token.borderRadius}px;
39
+
40
+ background: ${token.colorFillQuaternary};
41
+ `,
42
+ markdownLoading: css`
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+
47
+ min-height: 120px;
48
+ border-radius: ${token.borderRadiusLG}px;
49
+
50
+ font-size: 12px;
51
+ color: ${token.colorTextTertiary};
52
+
53
+ background: ${token.colorFillQuaternary};
54
+ `,
55
+ noteContent: css`
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 12px;
59
+
60
+ width: 100%;
61
+ min-height: 120px;
62
+ padding: 16px;
63
+ border-radius: ${token.borderRadiusLG}px;
64
+
65
+ background: ${token.colorFillQuaternary};
66
+ `,
67
+ notePreview: css`
68
+ overflow: hidden;
69
+ display: -webkit-box;
70
+ -webkit-box-orient: vertical;
71
+ -webkit-line-clamp: 6;
72
+
73
+ font-size: 13px;
74
+ line-height: 1.6;
75
+ color: ${token.colorTextSecondary};
76
+ `,
77
+ noteTitle: css`
78
+ display: flex;
79
+ gap: 8px;
80
+ align-items: center;
81
+
82
+ font-size: 16px;
83
+ font-weight: ${token.fontWeightStrong};
84
+ line-height: 1.4;
85
+ color: ${token.colorText};
86
+ `,
87
+ }));
88
+
89
+ // Helper to extract title from markdown content
90
+ const extractTitle = (content: string): string | null => {
91
+ if (!content) return null;
92
+
93
+ // Find first markdown header (# title)
94
+ const match = content.match(/^#\s+(.+)$/m);
95
+ return match ? match[1].trim() : null;
96
+ };
97
+
98
+ // Helper to extract preview text from note content
99
+ const getPreviewText = (content: string): string => {
100
+ if (!content) return '';
101
+
102
+ // Convert markdown to plain text
103
+ let plainText = markdownToTxt(content);
104
+
105
+ // Remove the title line if it exists
106
+ const title = extractTitle(content);
107
+ if (title) {
108
+ plainText = plainText.replace(title, '').trim();
109
+ }
110
+
111
+ // Limit to first 400 characters for preview
112
+ return plainText.slice(0, 400);
113
+ };
114
+
115
+ interface NoteFileItemProps {
116
+ chunkCount?: number | null;
117
+ chunkingError?: IAsyncTaskError | null;
118
+ chunkingStatus?: AsyncTaskStatus | null;
119
+ embeddingError?: IAsyncTaskError | null;
120
+ embeddingStatus?: AsyncTaskStatus | null;
121
+ fileType?: string;
122
+ finishEmbedding?: boolean;
123
+ id: string;
124
+ isLoadingMarkdown: boolean;
125
+ markdownContent: string;
126
+ metadata?: Record<string, any> | null;
127
+ name: string;
128
+ }
129
+
130
+ const NoteFileItem = memo<NoteFileItemProps>(
131
+ ({
132
+ chunkCount,
133
+ chunkingError,
134
+ chunkingStatus,
135
+ embeddingError,
136
+ embeddingStatus,
137
+ fileType,
138
+ finishEmbedding,
139
+ id,
140
+ isLoadingMarkdown,
141
+ markdownContent,
142
+ name,
143
+ metadata,
144
+ }) => {
145
+ const { t } = useTranslation(['components', 'file']);
146
+ const { styles, cx } = useStyles();
147
+ const [isCreatingFileParseTask, parseFiles] = useFileStore((s) => [
148
+ fileManagerSelectors.isCreatingFileParseTask(id)(s),
149
+ s.parseFilesToChunks,
150
+ ]);
151
+
152
+ const isSupportedForChunking = !isChunkingUnsupported(fileType || '');
153
+
154
+ const extractedTitle = markdownContent ? extractTitle(markdownContent) : null;
155
+ const displayTitle = extractedTitle || name || t('file:documentList.untitled');
156
+ const emoji = metadata?.emoji;
157
+ const previewText = markdownContent ? getPreviewText(markdownContent) : '';
158
+
159
+ return (
160
+ <>
161
+ <div style={{ position: 'relative' }}>
162
+ {isLoadingMarkdown ? (
163
+ <div className={styles.markdownLoading}>Loading preview...</div>
164
+ ) : markdownContent ? (
165
+ <div className={styles.noteContent}>
166
+ <div className={styles.noteTitle}>
167
+ {emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}
168
+ <span>{displayTitle}</span>
169
+ </div>
170
+ {previewText ? (
171
+ <div className={styles.notePreview}>{previewText}</div>
172
+ ) : (
173
+ <div className={styles.notePreview}>
174
+ <span style={{ color: 'var(--lobe-text-tertiary)', fontStyle: 'italic' }}>
175
+ No content
176
+ </span>
177
+ </div>
178
+ )}
179
+ </div>
180
+ ) : (
181
+ <div className={styles.iconWrapper}>
182
+ <FileIcon fileName={name} fileType={fileType} size={64} />
183
+ </div>
184
+ )}
185
+ </div>
186
+ {/* Floating chunk badge or action button */}
187
+ {!isNull(chunkingStatus) && chunkingStatus ? (
188
+ <div
189
+ className={cx('floatingChunkBadge', styles.floatingChunkBadge)}
190
+ onClick={(e) => e.stopPropagation()}
191
+ >
192
+ <ChunksBadge
193
+ chunkCount={chunkCount}
194
+ chunkingError={chunkingError}
195
+ chunkingStatus={chunkingStatus}
196
+ embeddingError={embeddingError}
197
+ embeddingStatus={embeddingStatus}
198
+ finishEmbedding={finishEmbedding}
199
+ id={id}
200
+ />
201
+ </div>
202
+ ) : (
203
+ isSupportedForChunking && (
204
+ <Tooltip title={t('FileManager.actions.chunkingTooltip')}>
205
+ <div
206
+ className={cx('floatingChunkBadge', styles.floatingChunkBadge)}
207
+ onClick={(e) => {
208
+ e.stopPropagation();
209
+ if (!isCreatingFileParseTask) {
210
+ parseFiles([id]);
211
+ }
212
+ }}
213
+ style={{ cursor: 'pointer' }}
214
+ >
215
+ <Button
216
+ icon={FileBoxIcon}
217
+ loading={isCreatingFileParseTask}
218
+ size={'small'}
219
+ type={'text'}
220
+ />
221
+ </div>
222
+ </Tooltip>
223
+ )
224
+ )}
225
+ </>
226
+ );
227
+ },
228
+ );
229
+
230
+ export default NoteFileItem;
@@ -0,0 +1,398 @@
1
+ import { Checkbox } from 'antd';
2
+ import { createStyles } from 'antd-style';
3
+ import React, { memo, useEffect, useRef, useState } from 'react';
4
+
5
+ import { documentService } from '@/services/document';
6
+ import { FileListItem } from '@/types/files';
7
+
8
+ import NoteEditorModal from '../../DocumentExplorer/NoteEditorModal';
9
+ import DropdownMenu from '../FileListItem/DropdownMenu';
10
+ import DefaultFileItem from './DefaultFileItem';
11
+ import ImageFileItem from './ImageFileItem';
12
+ import MarkdownFileItem from './MarkdownFileItem';
13
+ import NoteFileItem from './NoteFileItem';
14
+
15
+ // Image file types
16
+ const IMAGE_TYPES = new Set([
17
+ 'image/png',
18
+ 'image/jpeg',
19
+ 'image/jpg',
20
+ 'image/gif',
21
+ 'image/webp',
22
+ 'image/svg+xml',
23
+ ]);
24
+
25
+ // Markdown file types
26
+ const MARKDOWN_TYPES = new Set(['text/markdown', 'text/x-markdown']);
27
+
28
+ // Custom note file type
29
+ const CUSTOM_NOTE_TYPE = 'custom/document';
30
+
31
+ // Helper to check if filename ends with .md or is a custom note
32
+ const isMarkdownFile = (name: string, fileType?: string) => {
33
+ return (
34
+ name.toLowerCase().endsWith('.md') ||
35
+ name.toLowerCase().endsWith('.markdown') ||
36
+ (fileType && MARKDOWN_TYPES.has(fileType))
37
+ );
38
+ };
39
+
40
+ // Helper to check if it's a custom note that should be rendered
41
+ const isCustomNote = (fileType?: string) => {
42
+ return fileType === CUSTOM_NOTE_TYPE;
43
+ };
44
+
45
+ // Helper function to extract text from editor's JSON format for preview
46
+ const extractTextFromEditorJSON = (editorData: any): string => {
47
+ if (!editorData || !editorData.root || !editorData.root.children) {
48
+ return '';
49
+ }
50
+
51
+ const extractFromNode = (node: any): string => {
52
+ if (!node) return '';
53
+
54
+ // If node has text, return it
55
+ if (node.text) return node.text;
56
+
57
+ // If node has children, recursively extract text
58
+ if (node.children && Array.isArray(node.children)) {
59
+ return node.children.map((child: any) => extractFromNode(child)).join('');
60
+ }
61
+
62
+ return '';
63
+ };
64
+
65
+ return editorData.root.children.map((node: any) => extractFromNode(node)).join('\n');
66
+ };
67
+
68
+ const useStyles = createStyles(({ css, token }) => ({
69
+ actions: css`
70
+ opacity: 0;
71
+ transition: opacity ${token.motionDurationMid};
72
+ `,
73
+ card: css`
74
+ cursor: pointer;
75
+
76
+ position: relative;
77
+
78
+ overflow: hidden;
79
+
80
+ border: 1px solid ${token.colorBorderSecondary};
81
+ border-radius: ${token.borderRadiusLG}px;
82
+
83
+ background: ${token.colorBgContainer};
84
+
85
+ transition: all ${token.motionDurationMid};
86
+
87
+ &:hover {
88
+ border-color: ${token.colorPrimary};
89
+ box-shadow: ${token.boxShadowTertiary};
90
+
91
+ .actions {
92
+ opacity: 1;
93
+ }
94
+
95
+ .checkbox {
96
+ opacity: 1;
97
+ }
98
+
99
+ .dropdown {
100
+ opacity: 1;
101
+ }
102
+
103
+ .floatingChunkBadge {
104
+ opacity: 1;
105
+ }
106
+ }
107
+ `,
108
+ checkbox: css`
109
+ position: absolute;
110
+ z-index: 2;
111
+ inset-block-start: 8px;
112
+ inset-inline-start: 8px;
113
+
114
+ opacity: 0;
115
+
116
+ transition: opacity ${token.motionDurationMid};
117
+ `,
118
+ content: css`
119
+ position: relative;
120
+ `,
121
+ contentWithPadding: css`
122
+ padding: 12px;
123
+ `,
124
+ dropdown: css`
125
+ position: absolute;
126
+ z-index: 2;
127
+ inset-block-start: 8px;
128
+ inset-inline-end: 8px;
129
+
130
+ opacity: 0;
131
+
132
+ transition: opacity ${token.motionDurationMid};
133
+ `,
134
+ selected: css`
135
+ border-color: ${token.colorPrimary};
136
+ background: ${token.colorPrimaryBg};
137
+
138
+ .checkbox {
139
+ opacity: 1;
140
+ }
141
+ `,
142
+ }));
143
+
144
+ interface MasonryFileItemProps extends FileListItem {
145
+ knowledgeBaseId?: string;
146
+ onOpen: (id: string) => void;
147
+ onSelectedChange: (id: string, selected: boolean) => void;
148
+ selected?: boolean;
149
+ }
150
+
151
+ const MasonryFileItem = memo<MasonryFileItemProps>(
152
+ ({
153
+ chunkingError,
154
+ editorData,
155
+ embeddingError,
156
+ embeddingStatus,
157
+ finishEmbedding,
158
+ chunkCount,
159
+ url,
160
+ name,
161
+ fileType,
162
+ id,
163
+ selected,
164
+ chunkingStatus,
165
+ onSelectedChange,
166
+ knowledgeBaseId,
167
+ size,
168
+ onOpen,
169
+ metadata,
170
+ }) => {
171
+ const { styles, cx } = useStyles();
172
+ const [markdownContent, setMarkdownContent] = useState<string>('');
173
+ const [isLoadingMarkdown, setIsLoadingMarkdown] = useState(false);
174
+ const [isNoteModalOpen, setIsNoteModalOpen] = useState(false);
175
+
176
+ const isImage = fileType && IMAGE_TYPES.has(fileType);
177
+ const isMarkdown = isMarkdownFile(name, fileType);
178
+ const isNote = isCustomNote(fileType);
179
+
180
+ // Debug: Log editorData for notes
181
+ useEffect(() => {
182
+ if (isNote) {
183
+ console.log('[MasonryFileItem] Note item:', {
184
+ editorDataPreview: editorData ? JSON.stringify(editorData).slice(0, 100) : null,
185
+ editorDataType: typeof editorData,
186
+ hasEditorData: !!editorData,
187
+ id,
188
+ name,
189
+ });
190
+ }
191
+ }, [isNote, id, name, editorData]);
192
+
193
+ const cardRef = useRef<HTMLDivElement>(null);
194
+ const [isInView, setIsInView] = useState(false);
195
+
196
+ // Use Intersection Observer to detect when card enters viewport
197
+ useEffect(() => {
198
+ if (!cardRef.current) return;
199
+
200
+ const observer = new IntersectionObserver(
201
+ (entries) => {
202
+ entries.forEach((entry) => {
203
+ if (entry.isIntersecting && !isInView) {
204
+ setIsInView(true);
205
+ }
206
+ });
207
+ },
208
+ {
209
+ rootMargin: '200px', // Increased margin to load content earlier
210
+ threshold: 0.01, // Lower threshold for earlier triggering
211
+ },
212
+ );
213
+
214
+ observer.observe(cardRef.current);
215
+
216
+ return () => {
217
+ observer.disconnect();
218
+ };
219
+ }, [isInView]);
220
+
221
+ // Fetch markdown content only when in viewport
222
+ useEffect(() => {
223
+ if ((isMarkdown || isNote) && isInView && !markdownContent) {
224
+ setIsLoadingMarkdown(true);
225
+
226
+ const fetchContent = async () => {
227
+ try {
228
+ let text: string;
229
+
230
+ if (isNote) {
231
+ // For custom notes, fetch from document service
232
+ const document = await documentService.getDocumentById(id);
233
+ const content = document?.content || '';
234
+
235
+ // Try to parse as JSON (editor's native format) and convert to markdown for preview
236
+ try {
237
+ const editorData = JSON.parse(content);
238
+ // Since we can't easily convert JSON to markdown here without an editor instance,
239
+ // we'll extract plain text from the JSON structure for preview
240
+ text = extractTextFromEditorJSON(editorData);
241
+ } catch {
242
+ // If it's not JSON, use it as-is (might be old markdown format)
243
+ text = content;
244
+ }
245
+ } else if (url) {
246
+ // For regular markdown files, fetch from URL
247
+ const res = await fetch(url);
248
+ text = await res.text();
249
+ } else {
250
+ text = '';
251
+ }
252
+
253
+ // For custom notes, take more content for better preview; for regular markdown, take first 500 chars
254
+ const preview = isNote ? text.slice(0, 1000) : text.slice(0, 500);
255
+ setMarkdownContent(preview);
256
+ } catch (error) {
257
+ console.error('Failed to fetch markdown content:', error);
258
+ setMarkdownContent('');
259
+ } finally {
260
+ setIsLoadingMarkdown(false);
261
+ }
262
+ };
263
+
264
+ fetchContent();
265
+ }
266
+ }, [isMarkdown, isNote, url, isInView, markdownContent, id]);
267
+
268
+ return (
269
+ <div className={cx(styles.card, selected && styles.selected)} ref={cardRef}>
270
+ <div
271
+ className={cx('checkbox', styles.checkbox)}
272
+ onClick={(e) => {
273
+ e.stopPropagation();
274
+ onSelectedChange(id, !selected);
275
+ }}
276
+ >
277
+ <Checkbox checked={selected} />
278
+ </div>
279
+
280
+ <div className={cx('dropdown', styles.dropdown)} onClick={(e) => e.stopPropagation()}>
281
+ <DropdownMenu filename={name} id={id} knowledgeBaseId={knowledgeBaseId} url={url} />
282
+ </div>
283
+
284
+ <div
285
+ className={cx(
286
+ styles.content,
287
+ !isImage && !isMarkdown && !isNote && styles.contentWithPadding,
288
+ )}
289
+ onClick={() => {
290
+ if (isNote) {
291
+ console.log('[MasonryFileItem] Opening note modal with:', {
292
+ editorDataType: typeof editorData,
293
+ hasEditorData: !!editorData,
294
+ id,
295
+ name,
296
+ });
297
+ setIsNoteModalOpen(true);
298
+ } else {
299
+ onOpen(id);
300
+ }
301
+ }}
302
+ >
303
+ {(() => {
304
+ switch (true) {
305
+ case isImage && !!url: {
306
+ return (
307
+ <ImageFileItem
308
+ chunkCount={chunkCount ?? undefined}
309
+ chunkingError={chunkingError}
310
+ chunkingStatus={chunkingStatus ?? undefined}
311
+ embeddingError={embeddingError}
312
+ embeddingStatus={embeddingStatus ?? undefined}
313
+ fileType={fileType}
314
+ finishEmbedding={finishEmbedding}
315
+ id={id}
316
+ isInView={isInView}
317
+ name={name}
318
+ size={size}
319
+ url={url}
320
+ />
321
+ );
322
+ }
323
+ case isNote: {
324
+ return (
325
+ <NoteFileItem
326
+ chunkCount={chunkCount ?? undefined}
327
+ chunkingError={chunkingError}
328
+ chunkingStatus={chunkingStatus ?? undefined}
329
+ embeddingError={embeddingError}
330
+ embeddingStatus={embeddingStatus ?? undefined}
331
+ fileType={fileType}
332
+ finishEmbedding={finishEmbedding}
333
+ id={id}
334
+ isLoadingMarkdown={isLoadingMarkdown}
335
+ markdownContent={markdownContent}
336
+ metadata={metadata}
337
+ name={name}
338
+ />
339
+ );
340
+ }
341
+ case isMarkdown: {
342
+ return (
343
+ <MarkdownFileItem
344
+ chunkCount={chunkCount ?? undefined}
345
+ chunkingError={chunkingError}
346
+ chunkingStatus={chunkingStatus ?? undefined}
347
+ embeddingError={embeddingError}
348
+ embeddingStatus={embeddingStatus ?? undefined}
349
+ fileType={fileType}
350
+ finishEmbedding={finishEmbedding}
351
+ id={id}
352
+ isLoadingMarkdown={isLoadingMarkdown}
353
+ markdownContent={markdownContent}
354
+ name={name}
355
+ size={size}
356
+ />
357
+ );
358
+ }
359
+ default: {
360
+ return (
361
+ <DefaultFileItem
362
+ chunkCount={chunkCount ?? undefined}
363
+ chunkingError={chunkingError}
364
+ chunkingStatus={chunkingStatus ?? undefined}
365
+ embeddingError={embeddingError}
366
+ embeddingStatus={embeddingStatus ?? undefined}
367
+ fileType={fileType}
368
+ finishEmbedding={finishEmbedding}
369
+ id={id}
370
+ name={name}
371
+ size={size}
372
+ />
373
+ );
374
+ }
375
+ }
376
+ })()}
377
+ </div>
378
+
379
+ {/* Note Editor Modal */}
380
+ {isNote && (
381
+ <NoteEditorModal
382
+ documentId={id}
383
+ documentTitle={name}
384
+ editorData={editorData}
385
+ knowledgeBaseId={knowledgeBaseId}
386
+ onClose={() => {
387
+ console.log('[MasonryFileItem] Closing note modal');
388
+ setIsNoteModalOpen(false);
389
+ }}
390
+ open={isNoteModalOpen}
391
+ />
392
+ )}
393
+ </div>
394
+ );
395
+ },
396
+ );
397
+
398
+ export default MasonryFileItem;
@@ -0,0 +1,45 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import { Grid3x3Icon, ListIcon } from 'lucide-react';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ export type ViewMode = 'list' | 'masonry';
9
+
10
+ interface ViewSwitcherProps {
11
+ onViewChange: (view: ViewMode) => void;
12
+ view: ViewMode;
13
+ }
14
+
15
+ const useStyles = createStyles(({ css }) => ({
16
+ container: css`
17
+ gap: 4px;
18
+ `,
19
+ }));
20
+
21
+ const ViewSwitcher = memo<ViewSwitcherProps>(({ onViewChange, view }) => {
22
+ const { t } = useTranslation('components');
23
+ const { styles } = useStyles();
24
+
25
+ return (
26
+ <Flexbox className={styles.container} horizontal>
27
+ <ActionIcon
28
+ active={view === 'list'}
29
+ icon={ListIcon}
30
+ onClick={() => onViewChange('list')}
31
+ size={16}
32
+ title={t('FileManager.view.list')}
33
+ />
34
+ <ActionIcon
35
+ active={view === 'masonry'}
36
+ icon={Grid3x3Icon}
37
+ onClick={() => onViewChange('masonry')}
38
+ size={16}
39
+ title={t('FileManager.view.masonry')}
40
+ />
41
+ </Flexbox>
42
+ );
43
+ });
44
+
45
+ export default ViewSwitcher;