@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,169 @@
1
+ import { FileTypeIcon, Icon, Text } from '@lobehub/ui';
2
+ import { Upload } from 'antd';
3
+ import { createStyles, useTheme } from 'antd-style';
4
+ import { ArrowUpIcon, PlusIcon } from 'lucide-react';
5
+ import { memo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Center, Flexbox } from 'react-layout-kit';
8
+
9
+ import { useFileStore } from '@/store/file';
10
+
11
+ const ICON_SIZE = 80;
12
+
13
+ const useStyles = createStyles(({ css, token }) => ({
14
+ actionTitle: css`
15
+ margin-block-start: 12px;
16
+ font-size: 16px;
17
+ color: ${token.colorTextSecondary};
18
+ `,
19
+ card: css`
20
+ cursor: pointer;
21
+
22
+ position: relative;
23
+
24
+ overflow: hidden;
25
+
26
+ width: 200px;
27
+ height: 140px;
28
+ border-radius: ${token.borderRadiusLG}px;
29
+
30
+ font-weight: 500;
31
+ text-align: center;
32
+
33
+ background: ${token.colorFillTertiary};
34
+ box-shadow: 0 0 0 1px ${token.colorFillTertiary} inset;
35
+
36
+ transition: background 0.3s ease-in-out;
37
+
38
+ &:hover {
39
+ background: ${token.colorFillSecondary};
40
+ }
41
+ `,
42
+ glow: css`
43
+ position: absolute;
44
+ inset-block-end: -12px;
45
+ inset-inline-end: 0;
46
+
47
+ width: 48px;
48
+ height: 48px;
49
+
50
+ opacity: 0.5;
51
+ filter: blur(24px);
52
+ `,
53
+ icon: css`
54
+ position: absolute;
55
+ z-index: 1;
56
+ inset-block-end: -24px;
57
+ inset-inline-end: 8px;
58
+
59
+ flex: none;
60
+ `,
61
+ }));
62
+
63
+ interface DocumentEditorPlaceholderProps {
64
+ knowledgeBaseId?: string;
65
+ onCreateNewNote: () => void;
66
+ onNoteCreated?: (noteId: string) => void;
67
+ }
68
+
69
+ const DocumentEditorPlaceholder = memo<DocumentEditorPlaceholderProps>(
70
+ ({ knowledgeBaseId, onCreateNewNote, onNoteCreated }) => {
71
+ const { t } = useTranslation(['file', 'common']);
72
+ const theme = useTheme();
73
+ const { styles } = useStyles();
74
+ const [isUploading, setIsUploading] = useState(false);
75
+ const createDocument = useFileStore((s) => s.createDocument);
76
+
77
+ const handleUploadMarkdown = async (file: File) => {
78
+ try {
79
+ setIsUploading(true);
80
+
81
+ // Read markdown file content
82
+ const content = await file.text();
83
+
84
+ // Create document with markdown content
85
+ const newDoc = await createDocument({
86
+ content,
87
+ knowledgeBaseId,
88
+ title: file.name.replace(/\.md$|\.markdown$/i, ''),
89
+ });
90
+
91
+ // Notify parent component
92
+ onNoteCreated?.(newDoc.id);
93
+ } catch (error) {
94
+ console.error('Failed to upload markdown:', error);
95
+ } finally {
96
+ setIsUploading(false);
97
+ }
98
+
99
+ return false; // Prevent default upload behavior
100
+ };
101
+
102
+ return (
103
+ <Center gap={24} height={'100%'} style={{ paddingBottom: 100 }} width={'100%'}>
104
+ <Flexbox justify={'center'} style={{ textAlign: 'center' }}>
105
+ <Text as={'h4'}>{t('documentEditor.empty.title')}</Text>
106
+ <Text type={'secondary'}>{t('or', { ns: 'common' })}</Text>
107
+ </Flexbox>
108
+ <Flexbox gap={12} horizontal>
109
+ {/* Create New Note */}
110
+ <Flexbox className={styles.card} onClick={onCreateNewNote} padding={16}>
111
+ <span className={styles.actionTitle}>
112
+ {t('documentEditor.empty.createNewDocument')}
113
+ </span>
114
+ <div className={styles.glow} style={{ background: theme.purple }} />
115
+ <FileTypeIcon
116
+ className={styles.icon}
117
+ color={theme.purple}
118
+ icon={<Icon color={'#fff'} icon={PlusIcon} />}
119
+ size={ICON_SIZE}
120
+ type={'file'}
121
+ />
122
+ </Flexbox>
123
+
124
+ {/* Upload Markdown File */}
125
+ <Upload
126
+ accept=".md,.markdown"
127
+ beforeUpload={handleUploadMarkdown}
128
+ disabled={isUploading}
129
+ multiple={false}
130
+ showUploadList={false}
131
+ >
132
+ <Flexbox
133
+ className={styles.card}
134
+ padding={16}
135
+ style={{ opacity: isUploading ? 0.5 : 1 }}
136
+ >
137
+ <span className={styles.actionTitle}>
138
+ {isUploading ? 'Uploading...' : t('documentEditor.empty.uploadMarkdown')}
139
+ </span>
140
+ <div className={styles.glow} style={{ background: theme.gold }} />
141
+ <FileTypeIcon
142
+ className={styles.icon}
143
+ color={theme.gold}
144
+ icon={<Icon color={'#fff'} icon={ArrowUpIcon} />}
145
+ size={ICON_SIZE}
146
+ type={'file'}
147
+ />
148
+ </Flexbox>
149
+ </Upload>
150
+
151
+ {/* Import from Notion */}
152
+ {/* <Flexbox className={styles.card} onClick={handleImportFromNotion} padding={16}>
153
+ <span className={styles.actionTitle}>Import from Notion</span>
154
+ <div className={styles.glow} style={{ background: theme.geekblue }} />
155
+ <FileTypeIcon
156
+ className={styles.icon}
157
+ color={theme.geekblue}
158
+ icon={<Icon color={'#fff'} icon={FileTextIcon} />}
159
+ size={ICON_SIZE}
160
+ type={'doc'}
161
+ />
162
+ </Flexbox> */}
163
+ </Flexbox>
164
+ </Center>
165
+ );
166
+ },
167
+ );
168
+
169
+ export default DocumentEditorPlaceholder;
@@ -0,0 +1,148 @@
1
+ 'use client';
2
+
3
+ import { Icon } from '@lobehub/ui';
4
+ import { createStyles } from 'antd-style';
5
+ import { FileText } from 'lucide-react';
6
+ import { memo } from 'react';
7
+
8
+ import { LobeDocument } from '@/types/document';
9
+
10
+ import DocumentActions from './DocumentActions';
11
+ import RenamePopover from './RenamePopover';
12
+
13
+ const useStyles = createStyles(({ css, token }) => ({
14
+ documentActions: css`
15
+ opacity: 0;
16
+ transition: opacity ${token.motionDurationMid};
17
+ `,
18
+ documentCard: css`
19
+ cursor: pointer;
20
+ user-select: none;
21
+
22
+ position: relative;
23
+
24
+ display: flex;
25
+ gap: 12px;
26
+ align-items: center;
27
+
28
+ min-height: 36px;
29
+ margin-block: 4px;
30
+ margin-inline: 8px;
31
+ padding-block: 8px;
32
+ padding-inline: 12px;
33
+ border-radius: ${token.borderRadius}px;
34
+
35
+ color: ${token.colorTextSecondary};
36
+
37
+ background: transparent;
38
+
39
+ transition: all ${token.motionDurationMid};
40
+
41
+ &:hover {
42
+ background: ${token.colorFillTertiary};
43
+
44
+ .document-actions {
45
+ opacity: 1;
46
+ }
47
+ }
48
+
49
+ &.selected {
50
+ color: ${token.colorText};
51
+ background: ${token.colorFillSecondary};
52
+ }
53
+ `,
54
+ documentContent: css`
55
+ display: flex;
56
+ flex: 1;
57
+ gap: 12px;
58
+ align-items: center;
59
+
60
+ min-width: 0;
61
+ `,
62
+ documentTitle: css`
63
+ overflow: hidden;
64
+ flex: 1;
65
+
66
+ min-width: 0;
67
+
68
+ font-size: 14px;
69
+ line-height: 20px;
70
+ text-overflow: ellipsis;
71
+ white-space: nowrap;
72
+ `,
73
+ emoji: css`
74
+ font-size: 16px;
75
+ line-height: 1;
76
+ `,
77
+ icon: css`
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ color: ${token.colorTextSecondary};
82
+ `,
83
+ }));
84
+
85
+ interface DocumentListItemProps {
86
+ document: LobeDocument;
87
+ isRenaming: boolean;
88
+ isSelected: boolean;
89
+ onDelete: () => void;
90
+ onRenameConfirm: (documentId: string, title: string, emoji?: string) => void;
91
+ onRenameOpenChange: (documentId: string, open: boolean) => void;
92
+ onSelect: (documentId: string) => void;
93
+ untitledText: string;
94
+ }
95
+
96
+ const DocumentListItem = memo<DocumentListItemProps>(
97
+ ({
98
+ document,
99
+ isRenaming,
100
+ isSelected,
101
+ onDelete,
102
+ onRenameConfirm,
103
+ onRenameOpenChange,
104
+ onSelect,
105
+ untitledText,
106
+ }) => {
107
+ const { styles, cx } = useStyles();
108
+
109
+ const title = document.title || untitledText;
110
+ const emoji = document.metadata?.emoji;
111
+
112
+ return (
113
+ <div
114
+ className={cx(styles.documentCard, isSelected && 'selected')}
115
+ onClick={() => !isRenaming && onSelect(document.id)}
116
+ >
117
+ <RenamePopover
118
+ currentEmoji={emoji}
119
+ currentTitle={title}
120
+ onConfirm={(newTitle, newEmoji) => {
121
+ onRenameConfirm(document.id, newTitle, newEmoji);
122
+ }}
123
+ onOpenChange={(open) => onRenameOpenChange(document.id, open)}
124
+ open={isRenaming}
125
+ >
126
+ <div className={styles.documentContent}>
127
+ {emoji ? (
128
+ <span className={styles.emoji}>{emoji}</span>
129
+ ) : (
130
+ <Icon className={styles.icon} icon={FileText} size={16} />
131
+ )}
132
+ <div className={styles.documentTitle}>{title}</div>
133
+ </div>
134
+ </RenamePopover>
135
+ <div className={cx(styles.documentActions, 'document-actions')}>
136
+ <DocumentActions
137
+ documentContent={document.content || undefined}
138
+ documentId={document.id}
139
+ onDelete={onDelete}
140
+ onRename={() => onRenameOpenChange(document.id, true)}
141
+ />
142
+ </div>
143
+ </div>
144
+ );
145
+ },
146
+ );
147
+
148
+ export default DocumentListItem;
@@ -0,0 +1,39 @@
1
+ import { Skeleton } from 'antd';
2
+ import { createStyles } from 'antd-style';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ const useStyles = createStyles(({ css, token }) => ({
6
+ skeletonCard: css`
7
+ display: flex;
8
+ gap: 12px;
9
+ align-items: center;
10
+
11
+ min-height: 36px;
12
+ margin-block: 4px;
13
+ margin-inline: 8px;
14
+ padding-block: 8px;
15
+ padding-inline: 12px;
16
+ border-radius: ${token.borderRadius}px;
17
+
18
+ background: transparent;
19
+ `,
20
+ }));
21
+
22
+ const DocumentListSkeleton = () => {
23
+ const { styles } = useStyles();
24
+
25
+ return (
26
+ <Flexbox>
27
+ {Array.from({ length: 8 }).map((_, index) => (
28
+ <div className={styles.skeletonCard} key={index}>
29
+ {/* Icon skeleton */}
30
+ <Skeleton.Avatar active shape="square" size={16} />
31
+ {/* Title skeleton */}
32
+ <Skeleton.Input active size="small" style={{ height: 20, minWidth: 120, width: 120 }} />
33
+ </div>
34
+ ))}
35
+ </Flexbox>
36
+ );
37
+ };
38
+
39
+ export default DocumentListSkeleton;
@@ -0,0 +1,348 @@
1
+ 'use client';
2
+
3
+ import {
4
+ HotkeyEnum,
5
+ ReactCodePlugin,
6
+ ReactCodeblockPlugin,
7
+ ReactHRPlugin,
8
+ ReactLinkHighlightPlugin,
9
+ ReactListPlugin,
10
+ ReactMathPlugin,
11
+ ReactTablePlugin,
12
+ getHotkeyById,
13
+ } from '@lobehub/editor';
14
+ import {
15
+ ChatInputActionBar,
16
+ ChatInputActions,
17
+ type ChatInputActionsProps,
18
+ CodeLanguageSelect,
19
+ Editor,
20
+ useEditor,
21
+ useEditorState,
22
+ } from '@lobehub/editor/react';
23
+ import { Modal } from '@lobehub/ui';
24
+ import { css, cx, useTheme } from 'antd-style';
25
+ import {
26
+ BoldIcon,
27
+ CodeXmlIcon,
28
+ ItalicIcon,
29
+ ListIcon,
30
+ ListOrderedIcon,
31
+ ListTodoIcon,
32
+ MessageSquareQuote,
33
+ SigmaIcon,
34
+ SquareDashedBottomCodeIcon,
35
+ StrikethroughIcon,
36
+ UnderlineIcon,
37
+ } from 'lucide-react';
38
+ import { memo, useMemo, useState } from 'react';
39
+ import { useTranslation } from 'react-i18next';
40
+ import { Flexbox } from 'react-layout-kit';
41
+
42
+ import { message } from '@/components/AntdStaticMethods';
43
+ import { documentService } from '@/services/document';
44
+ import { useFileStore } from '@/store/file';
45
+
46
+ const editorClassName = cx(css`
47
+ p {
48
+ margin-block-end: 0;
49
+ }
50
+ `);
51
+
52
+ interface NoteEditorModalProps {
53
+ documentId?: string;
54
+ documentTitle?: string;
55
+ editorData?: Record<string, any> | null;
56
+ knowledgeBaseId?: string;
57
+ onClose: () => void;
58
+ open: boolean;
59
+ }
60
+
61
+ const NoteEditorModal = memo<NoteEditorModalProps>(
62
+ ({ open, onClose, documentId, documentTitle, editorData: cachedEditorData, knowledgeBaseId }) => {
63
+ const { t } = useTranslation(['file', 'editor']);
64
+ const theme = useTheme();
65
+
66
+ const editor = useEditor();
67
+ const editorState = useEditorState(editor);
68
+
69
+ const [isSaving, setIsSaving] = useState(false);
70
+ const [noteTitle, setNoteTitle] = useState('');
71
+ const refreshFileList = useFileStore((s) => s.refreshFileList);
72
+ const isEditMode = !!documentId;
73
+
74
+ const handleClose = () => {
75
+ // Clean up editor state
76
+ editor?.cleanDocument();
77
+ setNoteTitle('');
78
+ onClose();
79
+ };
80
+
81
+ const handleSave = async () => {
82
+ if (!editor || isSaving) return;
83
+
84
+ // Get editor content as JSON (native format)
85
+ const editorData = editor.getDocument('json');
86
+
87
+ // Check if editor is empty by getting text content
88
+ const textContent = (editor.getDocument('markdown') as unknown as string) || '';
89
+ if (!textContent || textContent.trim() === '') {
90
+ message.warning(t('header.newNoteDialog.emptyContent', { ns: 'file' }));
91
+ return;
92
+ }
93
+
94
+ setIsSaving(true);
95
+
96
+ try {
97
+ if (isEditMode) {
98
+ // Update existing note
99
+ await documentService.updateDocument({
100
+ content: textContent,
101
+ editorData: JSON.stringify(editorData),
102
+ id: documentId,
103
+ title: noteTitle,
104
+ });
105
+ message.success(t('header.newNoteDialog.updateSuccess', { ns: 'file' }));
106
+ } else {
107
+ // Create new note
108
+ const now = Date.now();
109
+ const timestamp = new Date(now).toLocaleString('en-US', {
110
+ day: '2-digit',
111
+ hour: '2-digit',
112
+ minute: '2-digit',
113
+ month: 'short',
114
+ year: 'numeric',
115
+ });
116
+ const title = noteTitle || `Note - ${timestamp}`;
117
+
118
+ await documentService.createDocument({
119
+ content: textContent,
120
+ editorData: JSON.stringify(editorData),
121
+ fileType: 'custom/document',
122
+ knowledgeBaseId,
123
+ metadata: {
124
+ createdAt: now,
125
+ },
126
+ title,
127
+ });
128
+
129
+ message.success(t('header.newNoteDialog.saveSuccess', { ns: 'file' }));
130
+ editor.cleanDocument();
131
+ }
132
+
133
+ onClose();
134
+ await refreshFileList();
135
+ } catch (error) {
136
+ console.error('Failed to save note:', error);
137
+ message.error(t('header.newNoteDialog.saveError', { ns: 'file' }));
138
+ } finally {
139
+ setIsSaving(false);
140
+ }
141
+ };
142
+
143
+ const handleOpenChange = (isOpen: boolean) => {
144
+ // When modal opens, load document content if in edit mode
145
+ if (isOpen && documentId && editor) {
146
+ console.log('[NoteEditorModal] Loading content:', {
147
+ cachedEditorDataPreview: cachedEditorData
148
+ ? JSON.stringify(cachedEditorData).slice(0, 100)
149
+ : null,
150
+ cachedEditorDataType: typeof cachedEditorData,
151
+ documentId,
152
+ documentTitle,
153
+ hasCachedEditorData: !!cachedEditorData,
154
+ });
155
+
156
+ // If editorData is already cached (from list), use it directly
157
+ if (cachedEditorData) {
158
+ console.log('[NoteEditorModal] Using cached editorData', cachedEditorData);
159
+ setNoteTitle(documentTitle || '');
160
+ editor.setDocument('json', JSON.stringify(cachedEditorData));
161
+ return;
162
+ }
163
+
164
+ // Otherwise, fetch full content from API
165
+ console.log('[NoteEditorModal] Fetching from API');
166
+ documentService
167
+ .getDocumentById(documentId)
168
+ .then((doc) => {
169
+ if (doc && doc.content) {
170
+ setNoteTitle(doc.title || doc.filename || '');
171
+
172
+ console.log('[NoteEditorModal] Fetched doc.editorData:', {
173
+ editorDataPreview: doc.editorData
174
+ ? JSON.stringify(doc.editorData).slice(0, 100)
175
+ : null,
176
+ editorDataType: typeof doc.editorData,
177
+ hasEditorData: !!doc.editorData,
178
+ });
179
+
180
+ editor.setDocument('json', doc.editorData);
181
+ }
182
+ })
183
+ .catch((error) => {
184
+ console.error('[NoteEditorModal] Failed to load document:', error);
185
+ message.error(t('header.newNoteDialog.loadError', { ns: 'file' }));
186
+ });
187
+ }
188
+
189
+ // When modal closes, clean up state
190
+ if (!isOpen) {
191
+ editor?.cleanDocument();
192
+ setNoteTitle('');
193
+ }
194
+ };
195
+
196
+ const toolbarItems: ChatInputActionsProps['items'] = useMemo(
197
+ () =>
198
+ [
199
+ {
200
+ active: editorState.isBold,
201
+ icon: BoldIcon,
202
+ key: 'bold',
203
+ label: t('typobar.bold', { ns: 'editor' }),
204
+ onClick: editorState.bold,
205
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Bold).keys },
206
+ },
207
+ {
208
+ active: editorState.isItalic,
209
+ icon: ItalicIcon,
210
+ key: 'italic',
211
+ label: t('typobar.italic', { ns: 'editor' }),
212
+ onClick: editorState.italic,
213
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Italic).keys },
214
+ },
215
+ {
216
+ active: editorState.isUnderline,
217
+ icon: UnderlineIcon,
218
+ key: 'underline',
219
+ label: t('typobar.underline', { ns: 'editor' }),
220
+ onClick: editorState.underline,
221
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Underline).keys },
222
+ },
223
+ {
224
+ active: editorState.isStrikethrough,
225
+ icon: StrikethroughIcon,
226
+ key: 'strikethrough',
227
+ label: t('typobar.strikethrough', { ns: 'editor' }),
228
+ onClick: editorState.strikethrough,
229
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Strikethrough).keys },
230
+ },
231
+ {
232
+ type: 'divider',
233
+ },
234
+ {
235
+ icon: ListIcon,
236
+ key: 'bulletList',
237
+ label: t('typobar.bulletList', { ns: 'editor' }),
238
+ onClick: editorState.bulletList,
239
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.BulletList).keys },
240
+ },
241
+ {
242
+ icon: ListOrderedIcon,
243
+ key: 'numberlist',
244
+ label: t('typobar.numberList', { ns: 'editor' }),
245
+ onClick: editorState.numberList,
246
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.NumberList).keys },
247
+ },
248
+ {
249
+ icon: ListTodoIcon,
250
+ key: 'tasklist',
251
+ label: t('typobar.taskList', { ns: 'editor' }),
252
+ onClick: editorState.checkList,
253
+ },
254
+ {
255
+ type: 'divider',
256
+ },
257
+ {
258
+ active: editorState.isBlockquote,
259
+ icon: MessageSquareQuote,
260
+ key: 'blockquote',
261
+ label: t('typobar.blockquote', { ns: 'editor' }),
262
+ onClick: editorState.blockquote,
263
+ },
264
+ {
265
+ type: 'divider',
266
+ },
267
+ {
268
+ icon: SigmaIcon,
269
+ key: 'math',
270
+ label: t('typobar.tex', { ns: 'editor' }),
271
+ onClick: editorState.insertMath,
272
+ },
273
+ {
274
+ active: editorState.isCode,
275
+ icon: CodeXmlIcon,
276
+ key: 'code',
277
+ label: t('typobar.code', { ns: 'editor' }),
278
+ onClick: editorState.code,
279
+ tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.CodeInline).keys },
280
+ },
281
+ {
282
+ icon: SquareDashedBottomCodeIcon,
283
+ key: 'codeblock',
284
+ label: t('typobar.codeblock', { ns: 'editor' }),
285
+ onClick: editorState.codeblock,
286
+ },
287
+ editorState.isCodeblock && {
288
+ children: (
289
+ <CodeLanguageSelect
290
+ onSelect={(value) => editorState.updateCodeblockLang(value)}
291
+ value={editorState.codeblockLang}
292
+ />
293
+ ),
294
+ disabled: !editorState.isCodeblock,
295
+ key: 'codeblockLang',
296
+ },
297
+ ].filter(Boolean) as ChatInputActionsProps['items'],
298
+ [editorState, t],
299
+ );
300
+
301
+ return (
302
+ <Modal
303
+ afterOpenChange={handleOpenChange}
304
+ okButtonProps={{ loading: isSaving }}
305
+ okText={t('header.newNoteDialog.save')}
306
+ onCancel={handleClose}
307
+ onOk={handleSave}
308
+ open={open}
309
+ title={isEditMode ? t('header.newNoteDialog.editTitle') : t('header.newNoteDialog.title')}
310
+ width={800}
311
+ >
312
+ <Flexbox gap={0}>
313
+ <ChatInputActionBar
314
+ left={<ChatInputActions items={toolbarItems} />}
315
+ style={{
316
+ background: theme.colorFillQuaternary,
317
+ borderTopLeftRadius: 8,
318
+ borderTopRightRadius: 8,
319
+ }}
320
+ />
321
+ <Flexbox padding={16}>
322
+ <Editor
323
+ // autoFocus
324
+ className={editorClassName}
325
+ content={''}
326
+ editor={editor}
327
+ plugins={[
328
+ ReactListPlugin,
329
+ ReactCodePlugin,
330
+ ReactCodeblockPlugin,
331
+ ReactHRPlugin,
332
+ ReactLinkHighlightPlugin,
333
+ ReactTablePlugin,
334
+ ReactMathPlugin,
335
+ ]}
336
+ style={{
337
+ minHeight: 400,
338
+ }}
339
+ type={'text'}
340
+ />
341
+ </Flexbox>
342
+ </Flexbox>
343
+ </Modal>
344
+ );
345
+ },
346
+ );
347
+
348
+ export default NoteEditorModal;