@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
@@ -0,0 +1,163 @@
1
+ 'use client';
2
+
3
+ import { Input, Popover } from 'antd';
4
+ import { createStyles } from 'antd-style';
5
+ import dynamic from 'next/dynamic';
6
+ import React, { memo, useEffect, useRef, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ import { useGlobalStore } from '@/store/global';
10
+ import { globalGeneralSelectors } from '@/store/global/selectors';
11
+
12
+ const EmojiPicker = dynamic(() => import('@lobehub/ui/es/EmojiPicker'), { ssr: false });
13
+
14
+ const useStyles = createStyles(({ css }) => ({
15
+ input: css`
16
+ flex: 1;
17
+ `,
18
+ inputGroup: css`
19
+ display: flex;
20
+ gap: 8px;
21
+ align-items: center;
22
+ `,
23
+ popoverContent: css`
24
+ width: 320px;
25
+ `,
26
+ }));
27
+
28
+ interface RenamePopoverProps {
29
+ children: React.ReactElement;
30
+ currentEmoji?: string;
31
+ currentTitle: string;
32
+ onConfirm: (title: string, emoji?: string) => void;
33
+ onOpenChange: (open: boolean) => void;
34
+ open: boolean;
35
+ }
36
+
37
+ const RenamePopover = memo<RenamePopoverProps>(
38
+ ({ children, currentTitle, currentEmoji, onConfirm, open, onOpenChange }) => {
39
+ const { t } = useTranslation(['file', 'editor']);
40
+ const { styles } = useStyles();
41
+ const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
42
+
43
+ const [title, setTitle] = useState(currentTitle);
44
+ const [emoji, setEmoji] = useState<string | undefined>(currentEmoji);
45
+ const [showEmojiPicker, setShowEmojiPicker] = useState(false);
46
+ const inputRef = useRef<any>(null);
47
+ const isInteractingWithEmojiPicker = useRef(false);
48
+
49
+ // Reset state when popover opens
50
+ const handleOpenChange = (nextOpen: boolean) => {
51
+ if (nextOpen) {
52
+ setTitle(currentTitle);
53
+ setEmoji(currentEmoji);
54
+ setShowEmojiPicker(false);
55
+ }
56
+ onOpenChange(nextOpen);
57
+ };
58
+
59
+ // Select all text when popover opens
60
+ useEffect(() => {
61
+ if (open && inputRef.current?.input) {
62
+ // Use a slightly longer timeout to ensure the input is fully rendered
63
+ const timer = setTimeout(() => {
64
+ inputRef.current.input.select();
65
+ }, 150);
66
+ return () => clearTimeout(timer);
67
+ }
68
+ }, [open]);
69
+
70
+ const handleTitleConfirm = () => {
71
+ if (title.trim() && title.trim() !== currentTitle) {
72
+ onConfirm(title.trim(), emoji);
73
+ }
74
+ onOpenChange(false);
75
+ };
76
+
77
+ const handleBlur = () => {
78
+ // Use setTimeout to check if we're interacting with emoji picker
79
+ setTimeout(() => {
80
+ // Don't close if emoji picker interaction is in progress
81
+ if (isInteractingWithEmojiPicker.current) {
82
+ return;
83
+ }
84
+
85
+ // Save title on blur if it changed
86
+ if (title.trim() && title.trim() !== currentTitle) {
87
+ onConfirm(title.trim(), emoji);
88
+ }
89
+ onOpenChange(false);
90
+ }, 150);
91
+ };
92
+
93
+ const content = (
94
+ <div className={styles.popoverContent}>
95
+ <div className={styles.inputGroup}>
96
+ <EmojiPicker
97
+ allowDelete
98
+ locale={locale}
99
+ onChange={(newEmoji) => {
100
+ setEmoji(newEmoji);
101
+ setShowEmojiPicker(false);
102
+ isInteractingWithEmojiPicker.current = false;
103
+ // Update emoji immediately
104
+ onConfirm(title, newEmoji);
105
+ // Refocus input after emoji selection
106
+ setTimeout(() => inputRef.current?.focus(), 100);
107
+ }}
108
+ onDelete={() => {
109
+ setEmoji(undefined);
110
+ setShowEmojiPicker(false);
111
+ isInteractingWithEmojiPicker.current = false;
112
+ // Update to remove emoji immediately
113
+ onConfirm(title, undefined);
114
+ // Refocus input after emoji deletion
115
+ setTimeout(() => inputRef.current?.focus(), 100);
116
+ }}
117
+ onOpenChange={(isOpen) => {
118
+ setShowEmojiPicker(isOpen);
119
+ isInteractingWithEmojiPicker.current = isOpen;
120
+ }}
121
+ open={showEmojiPicker}
122
+ size={32}
123
+ style={{
124
+ fontSize: 32,
125
+ }}
126
+ title={t('documentEditor.chooseIcon')}
127
+ value={emoji}
128
+ />
129
+ <Input
130
+ autoFocus
131
+ className={styles.input}
132
+ onBlur={handleBlur}
133
+ onChange={(e) => setTitle(e.target.value)}
134
+ onKeyDown={(e) => {
135
+ if (e.key === 'Enter') {
136
+ handleTitleConfirm();
137
+ } else if (e.key === 'Escape') {
138
+ onOpenChange(false);
139
+ }
140
+ }}
141
+ placeholder={t('documentEditor.titlePlaceholder')}
142
+ ref={inputRef}
143
+ value={title}
144
+ />
145
+ </div>
146
+ </div>
147
+ );
148
+
149
+ return (
150
+ <Popover
151
+ content={content}
152
+ onOpenChange={handleOpenChange}
153
+ open={open}
154
+ placement="bottom"
155
+ trigger={[]}
156
+ >
157
+ {children}
158
+ </Popover>
159
+ );
160
+ },
161
+ );
162
+
163
+ export default RenamePopover;
@@ -0,0 +1,318 @@
1
+ 'use client';
2
+
3
+ import { ActionIcon, SearchBar, Text } from '@lobehub/ui';
4
+ import { createStyles } from 'antd-style';
5
+ import { PlusIcon } from 'lucide-react';
6
+ import { memo, useEffect, useMemo, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Center } from 'react-layout-kit';
9
+ import { Virtuoso } from 'react-virtuoso';
10
+
11
+ import { useFileStore } from '@/store/file';
12
+ import { DocumentSourceType, LobeDocument } from '@/types/document';
13
+
14
+ import DocumentEditor from './DocumentEditor';
15
+ import DocumentEditorPlaceholder from './DocumentEditorPlaceholder';
16
+ import DocumentListItem from './DocumentListItem';
17
+ import DocumentListSkeleton from './DocumentListSkeleton';
18
+
19
+ const useStyles = createStyles(({ css, token }) => ({
20
+ container: css`
21
+ display: flex;
22
+ width: 100%;
23
+ height: 100%;
24
+ `,
25
+ documentList: css`
26
+ overflow-y: auto;
27
+ flex: 1;
28
+ padding-block: 4px;
29
+ `,
30
+ editorPanel: css`
31
+ overflow: hidden;
32
+ flex: 1;
33
+ background: ${token.colorBgContainer};
34
+ `,
35
+ header: css`
36
+ display: flex;
37
+ gap: 8px;
38
+ align-items: center;
39
+
40
+ padding-block: ${token.paddingXXS}px;
41
+ padding-inline: ${token.paddingXS}px;
42
+ border-block-end: 1px solid ${token.colorBorderSecondary};
43
+
44
+ background: ${token.colorBgContainer};
45
+ `,
46
+ listPanel: css`
47
+ display: flex;
48
+ flex-direction: column;
49
+
50
+ width: 280px;
51
+ min-width: 280px;
52
+ border-inline-end: 1px solid ${token.colorBorderSecondary};
53
+
54
+ background: ${token.colorBgContainer};
55
+ `,
56
+ }));
57
+
58
+ interface DocumentExplorerProps {
59
+ documentId?: string;
60
+ knowledgeBaseId?: string;
61
+ }
62
+
63
+ const updateUrl = (docId: string | null) => {
64
+ const newPath = docId ? `/knowledge/${docId}` : '/knowledge';
65
+ window.history.replaceState({}, '', newPath);
66
+ };
67
+
68
+ /**
69
+ * View, edit and create documents.
70
+ */
71
+ const DocumentExplorer = memo<DocumentExplorerProps>(({ knowledgeBaseId, documentId }) => {
72
+ const { t } = useTranslation('file');
73
+ const { styles } = useStyles();
74
+
75
+ const [selectedDocumentId, setSelectedDocumentId] = useState<string | null>(null);
76
+ const [isCreatingNew, setIsCreatingNew] = useState(false);
77
+ const [searchKeywords, setSearchKeywords] = useState<string>('');
78
+ const [renamingDocumentId, setRenamingDocumentId] = useState<string | null>(null);
79
+
80
+ const fetchDocuments = useFileStore((s) => s.fetchDocuments);
81
+ const getOptimisticDocuments = useFileStore((s) => s.getOptimisticDocuments);
82
+ const isDocumentListLoading = useFileStore((s) => s.isDocumentListLoading);
83
+ const createDocument = useFileStore((s) => s.createDocument);
84
+ const createOptimisticDocument = useFileStore((s) => s.createOptimisticDocument);
85
+ const replaceTempDocumentWithReal = useFileStore((s) => s.replaceTempDocumentWithReal);
86
+ const updateDocumentOptimistically = useFileStore((s) => s.updateDocumentOptimistically);
87
+ // Subscribe to localDocumentMap and documents to trigger re-render when documents are updated
88
+ useFileStore((s) => s.localDocumentMap);
89
+ useFileStore((s) => s.documents);
90
+
91
+ // Fetch documents on mount
92
+ useEffect(() => {
93
+ fetchDocuments();
94
+ }, [fetchDocuments]);
95
+
96
+ // If documentId is provided, automatically open that document
97
+ useEffect(() => {
98
+ if (documentId) {
99
+ setSelectedDocumentId(documentId);
100
+ setIsCreatingNew(false);
101
+ }
102
+ }, [documentId]);
103
+
104
+ // Get optimistic documents (merged local + server)
105
+ // Filter by knowledgeBaseId if provided
106
+ // Since the API call already filters by knowledgeBaseId, we trust that data
107
+ // But we also need to check local optimistic updates
108
+ // Re-compute when localDocumentMap changes to ensure list updates when documents are edited
109
+ const documents = getOptimisticDocuments();
110
+
111
+ // Filter documents based on search keywords and sort by creation date (newest first)
112
+ const filteredDocuments = useMemo(() => {
113
+ let result = documents;
114
+
115
+ // Filter by search keywords
116
+ if (searchKeywords.trim()) {
117
+ const lowerKeywords = searchKeywords.toLowerCase();
118
+ result = documents.filter((document) => {
119
+ const content = document.content?.toLowerCase() || '';
120
+ const title = document.title?.toLowerCase() || '';
121
+ return content.includes(lowerKeywords) || title.includes(lowerKeywords);
122
+ });
123
+ }
124
+
125
+ // Sort by creation date (newest first)
126
+ return result.sort((a, b) => {
127
+ const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
128
+ const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
129
+ return dateB - dateA;
130
+ });
131
+ }, [documents, searchKeywords]);
132
+
133
+ const handleDocumentSelect = (documentId: string) => {
134
+ if (selectedDocumentId === documentId) {
135
+ // Deselect if clicking the same document
136
+ setSelectedDocumentId(null);
137
+ updateUrl(null);
138
+ } else {
139
+ setSelectedDocumentId(documentId);
140
+ updateUrl(documentId);
141
+ }
142
+ setIsCreatingNew(false);
143
+ };
144
+
145
+ const handleNewDocument = async () => {
146
+ const untitledTitle = t('documentList.untitled');
147
+
148
+ // Create optimistic document immediately for instant UX
149
+ const tempDocumentId = createOptimisticDocument(untitledTitle);
150
+ setSelectedDocumentId(tempDocumentId);
151
+ setIsCreatingNew(true);
152
+
153
+ try {
154
+ // Create real document in background
155
+ const newDoc = await createDocument({
156
+ content: '',
157
+ knowledgeBaseId,
158
+ title: untitledTitle,
159
+ });
160
+
161
+ // Convert DocumentItem to EditorDocument
162
+ const realDocument: LobeDocument = {
163
+ content: newDoc.content || '',
164
+ createdAt: newDoc.createdAt ? new Date(newDoc.createdAt) : new Date(),
165
+ editorData:
166
+ typeof newDoc.editorData === 'string'
167
+ ? JSON.parse(newDoc.editorData)
168
+ : newDoc.editorData || null,
169
+ fileType: 'custom/document',
170
+ filename: newDoc.title || untitledTitle,
171
+ id: newDoc.id,
172
+ metadata: newDoc.metadata || {},
173
+ source: 'document',
174
+ sourceType: DocumentSourceType.EDITOR,
175
+ title: newDoc.title || untitledTitle,
176
+ totalCharCount: newDoc.content?.length || 0,
177
+ totalLineCount: 0,
178
+ updatedAt: newDoc.updatedAt ? new Date(newDoc.updatedAt) : new Date(),
179
+ };
180
+
181
+ // Replace optimistic document with real document (smooth UX, no flicker)
182
+ replaceTempDocumentWithReal(tempDocumentId, realDocument);
183
+
184
+ // Update selected document ID to real ID and update URL
185
+ setSelectedDocumentId(newDoc.id);
186
+ setIsCreatingNew(false);
187
+ updateUrl(newDoc.id);
188
+ } catch (error) {
189
+ console.error('Failed to create document:', error);
190
+ // On error, remove the optimistic document and deselect
191
+ useFileStore.getState().removeTempDocument(tempDocumentId);
192
+ setSelectedDocumentId(null);
193
+ setIsCreatingNew(false);
194
+ }
195
+ };
196
+
197
+ const handleDocumentIdChange = (newId: string) => {
198
+ // When a temp document gets a real ID, update the selected document ID
199
+ setSelectedDocumentId(newId);
200
+ setIsCreatingNew(false);
201
+ updateUrl(newId);
202
+ };
203
+
204
+ const handleRenameOpenChange = (documentId: string, open: boolean) => {
205
+ setRenamingDocumentId(open ? documentId : null);
206
+ };
207
+
208
+ const handleRenameConfirm = async (documentId: string, title: string, emoji?: string) => {
209
+ try {
210
+ await updateDocumentOptimistically(documentId, {
211
+ metadata: {
212
+ emoji,
213
+ },
214
+ title,
215
+ });
216
+ } catch (error) {
217
+ console.error('Failed to rename document:', error);
218
+ } finally {
219
+ setRenamingDocumentId(null);
220
+ }
221
+ };
222
+
223
+ return (
224
+ <div className={styles.container}>
225
+ {/* Left Panel - Documents List */}
226
+ <div className={styles.listPanel}>
227
+ <div className={styles.header}>
228
+ <SearchBar
229
+ allowClear
230
+ onChange={(e) => setSearchKeywords(e.target.value)}
231
+ placeholder={t('searchDocumentPlaceholder')}
232
+ style={{ flex: 1 }}
233
+ value={searchKeywords}
234
+ variant={'borderless'}
235
+ />
236
+ <ActionIcon
237
+ icon={PlusIcon}
238
+ onClick={handleNewDocument}
239
+ title={t('header.newDocumentButton')}
240
+ />
241
+ </div>
242
+ <div className={styles.documentList}>
243
+ {isDocumentListLoading ? (
244
+ <DocumentListSkeleton />
245
+ ) : filteredDocuments.length === 0 ? (
246
+ <div style={{ color: 'var(--lobe-text-secondary)', padding: 24, textAlign: 'center' }}>
247
+ {searchKeywords.trim() ? t('documentList.noResults') : t('documentList.empty')}
248
+ </div>
249
+ ) : (
250
+ <Virtuoso
251
+ components={{
252
+ Footer: () => (
253
+ <Center style={{ paddingBlock: 16 }}>
254
+ <Text style={{ fontSize: 12 }} type={'secondary'}>
255
+ {t('documentList.documentCount', { count: filteredDocuments.length })}
256
+ </Text>
257
+ </Center>
258
+ ),
259
+ }}
260
+ data={filteredDocuments}
261
+ itemContent={(_index, document) => {
262
+ const isSelected = selectedDocumentId === document.id;
263
+ const isRenaming = renamingDocumentId === document.id;
264
+ return (
265
+ <DocumentListItem
266
+ document={document}
267
+ isRenaming={isRenaming}
268
+ isSelected={isSelected}
269
+ key={document.id}
270
+ onDelete={() => {
271
+ if (selectedDocumentId === document.id) {
272
+ setSelectedDocumentId(null);
273
+ setIsCreatingNew(false);
274
+ updateUrl(null);
275
+ }
276
+ }}
277
+ onRenameConfirm={handleRenameConfirm}
278
+ onRenameOpenChange={handleRenameOpenChange}
279
+ onSelect={handleDocumentSelect}
280
+ untitledText={t('documentList.untitled')}
281
+ />
282
+ );
283
+ }}
284
+ style={{ height: '100%' }}
285
+ />
286
+ )}
287
+ </div>
288
+ </div>
289
+
290
+ {/* Right Panel - Editor */}
291
+ <div className={styles.editorPanel}>
292
+ {selectedDocumentId || isCreatingNew ? (
293
+ <DocumentEditor
294
+ documentId={selectedDocumentId || undefined}
295
+ knowledgeBaseId={knowledgeBaseId}
296
+ onDelete={() => {
297
+ setSelectedDocumentId(null);
298
+ setIsCreatingNew(false);
299
+ updateUrl(null);
300
+ }}
301
+ onDocumentIdChange={handleDocumentIdChange}
302
+ />
303
+ ) : (
304
+ <DocumentEditorPlaceholder
305
+ knowledgeBaseId={knowledgeBaseId}
306
+ onCreateNewNote={handleNewDocument}
307
+ onNoteCreated={(documentId) => {
308
+ setSelectedDocumentId(documentId);
309
+ setIsCreatingNew(false);
310
+ }}
311
+ />
312
+ )}
313
+ </div>
314
+ </div>
315
+ );
316
+ });
317
+
318
+ export default DocumentExplorer;
@@ -1,12 +1,12 @@
1
- import { Button, Tooltip } from '@lobehub/ui';
1
+ import { Button, Icon, Tooltip } from '@lobehub/ui';
2
2
  import { Checkbox } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
4
  import dayjs from 'dayjs';
5
5
  import relativeTime from 'dayjs/plugin/relativeTime';
6
6
  import { isNull } from 'lodash-es';
7
- import { FileBoxIcon } from 'lucide-react';
7
+ import { FileBoxIcon, FileText } from 'lucide-react';
8
8
  import { rgba } from 'polished';
9
- import { memo } from 'react';
9
+ import { memo, useMemo } from 'react';
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import { Center, Flexbox } from 'react-layout-kit';
12
12
  import { useSearchParams } from 'react-router-dom';
@@ -22,6 +22,15 @@ import DropdownMenu from './DropdownMenu';
22
22
 
23
23
  dayjs.extend(relativeTime);
24
24
 
25
+ // Helper to extract title from markdown content
26
+ const extractTitle = (content: string): string | null => {
27
+ if (!content) return null;
28
+
29
+ // Find first markdown header (# title)
30
+ const match = content.match(/^#\s+(.+)$/m);
31
+ return match ? match[1].trim() : null;
32
+ };
33
+
25
34
  export const FILE_DATE_WIDTH = 160;
26
35
  export const FILE_SIZE_WIDTH = 140;
27
36
 
@@ -33,9 +42,8 @@ const useStyles = createStyles(({ css, token, cx, isDarkMode }) => {
33
42
  checkbox: hover,
34
43
  container: css`
35
44
  cursor: pointer;
36
- margin-inline: 24px;
45
+ margin-inline: 16px;
37
46
  border-block-end: 1px solid ${isDarkMode ? token.colorSplit : rgba(token.colorSplit, 0.06)};
38
- border-radius: ${token.borderRadius}px;
39
47
 
40
48
  &:hover {
41
49
  background: ${token.colorFillTertiary};
@@ -101,8 +109,11 @@ const FileRenderItem = memo<FileRenderItemProps>(
101
109
  onSelectedChange,
102
110
  knowledgeBaseId,
103
111
  index,
112
+ content,
113
+ metadata,
114
+ sourceType,
104
115
  }) => {
105
- const { t } = useTranslation('components');
116
+ const { t } = useTranslation(['components', 'file']);
106
117
  const { styles, cx } = useStyles();
107
118
  const [, setSearchParams] = useSearchParams();
108
119
  const [isCreatingFileParseTask, parseFiles] = useFileStore((s) => [
@@ -111,6 +122,18 @@ const FileRenderItem = memo<FileRenderItemProps>(
111
122
  ]);
112
123
 
113
124
  const isSupportedForChunking = !isChunkingUnsupported(fileType);
125
+ const isNote = sourceType === 'document' || fileType === 'custom/document';
126
+
127
+ // Extract title and emoji for notes
128
+ const displayTitle = useMemo(() => {
129
+ if (isNote && content) {
130
+ const extractedTitle = extractTitle(content);
131
+ return extractedTitle || name || t('file:documentList.untitled');
132
+ }
133
+ return name;
134
+ }, [isNote, content, name, t]);
135
+
136
+ const emoji = isNote ? metadata?.emoji : null;
114
137
 
115
138
  const displayTime =
116
139
  dayjs().diff(dayjs(createdAt), 'd') < 7
@@ -121,7 +144,7 @@ const FileRenderItem = memo<FileRenderItemProps>(
121
144
  <Flexbox
122
145
  align={'center'}
123
146
  className={cx(styles.container, selected && styles.selected)}
124
- height={64}
147
+ height={48}
125
148
  horizontal
126
149
  paddingInline={8}
127
150
  >
@@ -158,8 +181,24 @@ const FileRenderItem = memo<FileRenderItemProps>(
158
181
  style={{ borderRadius: '50%' }}
159
182
  />
160
183
  </Center>
161
- <FileIcon fileName={name} fileType={fileType} />
162
- <span className={styles.name}>{name}</span>
184
+ <Flexbox
185
+ align={'center'}
186
+ justify={'center'}
187
+ style={{ fontSize: 24, marginInline: 8, width: 24 }}
188
+ >
189
+ {isNote ? (
190
+ emoji ? (
191
+ <span style={{ fontSize: 24 }}>{emoji}</span>
192
+ ) : (
193
+ <Center height={24} width={24}>
194
+ <Icon icon={FileText} size={24} />
195
+ </Center>
196
+ )
197
+ ) : (
198
+ <FileIcon fileName={name} fileType={fileType} size={24} />
199
+ )}
200
+ </Flexbox>
201
+ <span className={styles.name}>{displayTitle}</span>
163
202
  </Flexbox>
164
203
  <Flexbox
165
204
  align={'center'}