@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.
- package/CHANGELOG.md +52 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/common.json +1 -0
- package/locales/ar/file.json +85 -2
- package/locales/bg-BG/common.json +1 -0
- package/locales/bg-BG/file.json +85 -2
- package/locales/de-DE/common.json +1 -0
- package/locales/de-DE/file.json +85 -2
- package/locales/en-US/common.json +1 -0
- package/locales/en-US/file.json +85 -2
- package/locales/es-ES/common.json +1 -0
- package/locales/es-ES/file.json +85 -2
- package/locales/fa-IR/common.json +1 -0
- package/locales/fa-IR/file.json +85 -2
- package/locales/fr-FR/common.json +1 -0
- package/locales/fr-FR/file.json +85 -2
- package/locales/it-IT/common.json +1 -0
- package/locales/it-IT/file.json +85 -2
- package/locales/ja-JP/common.json +1 -0
- package/locales/ja-JP/file.json +85 -2
- package/locales/ko-KR/common.json +1 -0
- package/locales/ko-KR/file.json +85 -2
- package/locales/nl-NL/common.json +1 -0
- package/locales/nl-NL/file.json +85 -2
- package/locales/pl-PL/common.json +1 -0
- package/locales/pl-PL/file.json +85 -2
- package/locales/pt-BR/common.json +1 -0
- package/locales/pt-BR/file.json +85 -2
- package/locales/ru-RU/common.json +1 -0
- package/locales/ru-RU/file.json +85 -2
- package/locales/tr-TR/common.json +1 -0
- package/locales/tr-TR/file.json +85 -2
- package/locales/vi-VN/common.json +1 -0
- package/locales/vi-VN/file.json +85 -2
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-CN/file.json +85 -2
- package/locales/zh-TW/common.json +1 -0
- package/locales/zh-TW/file.json +85 -2
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/file.test.ts +94 -29
- package/packages/database/src/models/file.ts +15 -4
- package/packages/database/src/repositories/knowledge/index.test.ts +300 -0
- package/packages/database/src/repositories/knowledge/index.ts +420 -0
- package/packages/model-bank/src/aiModels/aihubmix.ts +1 -0
- package/packages/model-bank/src/aiModels/google.ts +9 -5
- package/packages/model-bank/src/aiModels/openai.ts +2 -35
- package/packages/model-bank/src/aiModels/openrouter.ts +1 -0
- package/packages/model-bank/src/aiModels/vertexai.ts +2 -0
- package/packages/model-bank/src/types/aiModel.ts +15 -2
- package/packages/model-runtime/src/core/usageConverters/index.ts +1 -0
- package/packages/model-runtime/src/core/usageConverters/utils/resolveImageSinglePrice.ts +34 -0
- package/packages/types/src/document/index.ts +14 -2
- package/packages/types/src/files/index.ts +2 -0
- package/packages/types/src/files/list.ts +10 -0
- package/packages/types/src/llm.ts +1 -1
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +93 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/{ModelSelect.tsx → ModelSelect/index.tsx} +17 -2
- package/src/app/[variants]/(main)/knowledge/KnowledgeRouter.tsx +2 -1
- package/src/app/[variants]/(main)/knowledge/components/KnowledgeBaseItem/index.tsx +0 -2
- package/src/app/[variants]/(main)/knowledge/hooks/useFileCategory.ts +6 -3
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/index.tsx +2 -2
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/{MenuItems.tsx → CategoryMenu.tsx} +3 -3
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/Menu.tsx +2 -2
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/index.tsx +40 -18
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/layout/Container.tsx +1 -1
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/CategoryMenu.tsx +148 -0
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/KnowledgeBase.tsx +20 -7
- package/src/components/FileIcon/index.tsx +3 -1
- package/src/features/ChatInput/ActionBar/Knowledge/index.tsx +2 -2
- package/src/features/Conversation/Messages/Assistant/index.tsx +7 -1
- package/src/features/FileSidePanel/index.tsx +1 -1
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItem.tsx +80 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItemWrapper.tsx +27 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +104 -23
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/MasonrySkeleton.tsx +62 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/index.tsx +3 -2
- package/src/features/KnowledgeBaseModal/CreateNew/CreateForm.tsx +1 -1
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentActions.tsx +111 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditor.tsx +723 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditorPlaceholder.tsx +169 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentListItem.tsx +148 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentListSkeleton.tsx +39 -0
- package/src/features/KnowledgeManager/DocumentExplorer/NoteEditorModal.tsx +348 -0
- package/src/features/KnowledgeManager/DocumentExplorer/RenamePopover.tsx +163 -0
- package/src/features/KnowledgeManager/DocumentExplorer/index.tsx +318 -0
- package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/index.tsx +48 -9
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/DefaultFileItem.tsx +149 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/ImageFileItem.tsx +245 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/MarkdownFileItem.tsx +232 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/NoteFileItem.tsx +230 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/index.tsx +398 -0
- package/src/features/KnowledgeManager/FileExplorer/ToolBar/ViewSwitcher.tsx +45 -0
- package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/index.tsx +68 -16
- package/src/features/KnowledgeManager/Header/AddButton.tsx +107 -0
- package/src/features/KnowledgeManager/Header/NewNoteButton.tsx +33 -0
- package/src/features/{FileManager → KnowledgeManager}/Header/index.tsx +3 -9
- package/src/features/KnowledgeManager/Home/RecentDocumentCard.tsx +116 -0
- package/src/features/KnowledgeManager/Home/RecentDocuments.tsx +77 -0
- package/src/features/KnowledgeManager/Home/RecentFileCard.tsx +121 -0
- package/src/features/KnowledgeManager/Home/RecentFiles.tsx +73 -0
- package/src/features/KnowledgeManager/Home/RecentFilesSkeleton.tsx +83 -0
- package/src/features/KnowledgeManager/Home/UploadEntries.tsx +208 -0
- package/src/features/KnowledgeManager/Home/index.tsx +221 -0
- package/src/features/KnowledgeManager/index.tsx +75 -0
- package/src/features/Portal/FilePreview/Body/index.tsx +1 -1
- package/src/features/Portal/FilePreview/Header.tsx +1 -1
- package/src/locales/default/common.ts +1 -0
- package/src/locales/default/file.ts +85 -2
- package/src/locales/default/tool.ts +8 -0
- package/src/server/routers/lambda/__tests__/file.test.ts +85 -6
- package/src/server/routers/lambda/document.ts +57 -0
- package/src/server/routers/lambda/file.ts +72 -0
- package/src/server/routers/lambda/knowledge.ts +94 -0
- package/src/server/services/document/index.ts +103 -0
- package/src/services/document/index.ts +44 -0
- package/src/services/file/index.ts +5 -3
- package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +125 -229
- package/src/store/aiInfra/slices/aiProvider/action.ts +113 -33
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +1 -1
- package/src/store/file/initialState.ts +6 -1
- package/src/store/file/slices/chat/action.ts +3 -3
- package/src/store/file/slices/document/action.ts +359 -0
- package/src/store/file/slices/document/index.ts +3 -0
- package/src/store/file/slices/document/initialState.ts +22 -0
- package/src/store/file/slices/document/selectors.ts +25 -0
- package/src/store/file/slices/fileManager/action.test.ts +16 -9
- package/src/store/file/slices/fileManager/action.ts +11 -11
- package/src/store/file/store.ts +3 -0
- package/src/store/global/initialState.ts +3 -1
- package/src/tools/interventions.ts +3 -5
- package/src/tools/local-system/Intervention/MoveLocalFiles/MoveFileItem.tsx +56 -0
- package/src/tools/local-system/Intervention/MoveLocalFiles/index.tsx +26 -0
- package/src/tools/local-system/Intervention/RunCommand/index.tsx +1 -2
- package/src/tools/local-system/Intervention/index.ts +11 -0
- package/src/tools/local-system/Render/MoveLocalFiles/MoveFileItem.tsx +56 -0
- package/src/tools/local-system/Render/MoveLocalFiles/index.tsx +26 -0
- package/src/tools/local-system/Render/index.ts +21 -0
- package/src/tools/renders.ts +6 -24
- package/src/tools/web-browsing/Render/index.ts +13 -0
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/FileMenu.tsx +0 -75
- package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +0 -582
- package/src/features/FileManager/index.tsx +0 -36
- /package/src/features/{FileManager/FileList/ToolBar → KnowledgeBaseModal/AssignKnowledgeBase}/ViewSwitcher.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/ChunkItem.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Content.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Loading/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/Item.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/index.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/EmptyStatus.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/ChunkTag.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/DropdownMenu.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileSkeleton.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonryFileItem/MasonryItemWrapper.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonrySkeleton.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/Config.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/MultiSelectActions.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/index.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/useCheckTaskStatus.ts +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/FilesSearchBar.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/TogglePanelButton.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/UploadFileButton.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/UploadDock/Item.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/UploadDock/index.tsx +0 -0
|
@@ -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;
|
package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/index.tsx
RENAMED
|
@@ -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:
|
|
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={
|
|
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
|
-
<
|
|
162
|
-
|
|
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'}
|