@lobehub/chat 1.140.0 → 1.141.1

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 (125) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/changelog/v1.json +24 -0
  3. package/locales/ar/chat.json +13 -0
  4. package/locales/ar/common.json +1 -0
  5. package/locales/ar/components.json +4 -0
  6. package/locales/ar/file.json +2 -2
  7. package/locales/bg-BG/chat.json +13 -0
  8. package/locales/bg-BG/common.json +1 -0
  9. package/locales/bg-BG/components.json +4 -0
  10. package/locales/bg-BG/file.json +2 -2
  11. package/locales/de-DE/chat.json +13 -0
  12. package/locales/de-DE/common.json +1 -0
  13. package/locales/de-DE/components.json +4 -0
  14. package/locales/de-DE/file.json +2 -2
  15. package/locales/en-US/chat.json +13 -0
  16. package/locales/en-US/common.json +1 -0
  17. package/locales/en-US/components.json +4 -0
  18. package/locales/en-US/file.json +2 -2
  19. package/locales/es-ES/chat.json +13 -0
  20. package/locales/es-ES/common.json +1 -0
  21. package/locales/es-ES/components.json +4 -0
  22. package/locales/es-ES/file.json +2 -2
  23. package/locales/fa-IR/chat.json +13 -0
  24. package/locales/fa-IR/common.json +1 -0
  25. package/locales/fa-IR/components.json +4 -0
  26. package/locales/fa-IR/file.json +2 -2
  27. package/locales/fr-FR/chat.json +13 -0
  28. package/locales/fr-FR/common.json +1 -0
  29. package/locales/fr-FR/components.json +4 -0
  30. package/locales/fr-FR/file.json +2 -2
  31. package/locales/it-IT/chat.json +13 -0
  32. package/locales/it-IT/common.json +1 -0
  33. package/locales/it-IT/components.json +4 -0
  34. package/locales/it-IT/file.json +2 -2
  35. package/locales/ja-JP/chat.json +13 -0
  36. package/locales/ja-JP/common.json +1 -0
  37. package/locales/ja-JP/components.json +4 -0
  38. package/locales/ja-JP/file.json +2 -2
  39. package/locales/ko-KR/chat.json +13 -0
  40. package/locales/ko-KR/common.json +1 -0
  41. package/locales/ko-KR/components.json +4 -0
  42. package/locales/ko-KR/file.json +2 -2
  43. package/locales/nl-NL/chat.json +13 -0
  44. package/locales/nl-NL/common.json +1 -0
  45. package/locales/nl-NL/components.json +4 -0
  46. package/locales/nl-NL/file.json +2 -2
  47. package/locales/pl-PL/chat.json +13 -0
  48. package/locales/pl-PL/common.json +1 -0
  49. package/locales/pl-PL/components.json +4 -0
  50. package/locales/pl-PL/file.json +2 -2
  51. package/locales/pt-BR/chat.json +13 -0
  52. package/locales/pt-BR/common.json +1 -0
  53. package/locales/pt-BR/components.json +4 -0
  54. package/locales/pt-BR/file.json +2 -2
  55. package/locales/ru-RU/chat.json +13 -0
  56. package/locales/ru-RU/common.json +1 -0
  57. package/locales/ru-RU/components.json +4 -0
  58. package/locales/ru-RU/file.json +2 -2
  59. package/locales/tr-TR/chat.json +13 -0
  60. package/locales/tr-TR/common.json +1 -0
  61. package/locales/tr-TR/components.json +4 -0
  62. package/locales/tr-TR/file.json +2 -2
  63. package/locales/vi-VN/chat.json +13 -0
  64. package/locales/vi-VN/common.json +1 -0
  65. package/locales/vi-VN/components.json +4 -0
  66. package/locales/vi-VN/file.json +2 -2
  67. package/locales/zh-CN/chat.json +13 -0
  68. package/locales/zh-CN/common.json +1 -0
  69. package/locales/zh-CN/components.json +4 -0
  70. package/locales/zh-CN/file.json +2 -2
  71. package/locales/zh-TW/chat.json +13 -0
  72. package/locales/zh-TW/common.json +1 -0
  73. package/locales/zh-TW/components.json +4 -0
  74. package/locales/zh-TW/file.json +2 -2
  75. package/next.config.ts +5 -6
  76. package/package.json +8 -2
  77. package/packages/context-engine/src/__tests__/pipeline.test.ts +7 -27
  78. package/packages/context-engine/src/pipeline.ts +5 -21
  79. package/packages/context-engine/src/types.ts +2 -2
  80. package/packages/database/src/models/__tests__/message.test.ts +200 -2
  81. package/packages/database/src/models/message.ts +13 -0
  82. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +313 -0
  83. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +21 -5
  84. package/packages/model-runtime/src/providers/azureai/index.test.ts +12 -2
  85. package/packages/model-runtime/src/providers/groq/index.test.ts +449 -0
  86. package/packages/model-runtime/src/providers/groq/index.ts +46 -0
  87. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +3 -2
  88. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Tags/index.tsx +1 -1
  89. package/src/app/[variants]/(main)/files/(content)/@menu/features/KnowledgeBase/Item/index.tsx +10 -2
  90. package/src/features/ChatInput/InputEditor/index.tsx +2 -0
  91. package/src/features/Conversation/Messages/User/index.tsx +7 -17
  92. package/src/features/Conversation/components/ChatItem/ShareMessageModal/SharePdf/PdfPreview.tsx +361 -0
  93. package/src/features/Conversation/components/ChatItem/ShareMessageModal/SharePdf/index.tsx +119 -0
  94. package/src/features/Conversation/components/ChatItem/ShareMessageModal/SharePdf/style.ts +63 -0
  95. package/src/features/Conversation/components/ChatItem/ShareMessageModal/SharePdf/template.ts +24 -0
  96. package/src/features/Conversation/components/ChatItem/ShareMessageModal/SharePdf/usePdfGeneration.ts +93 -0
  97. package/src/features/Conversation/components/ShareMessageModal/ShareImage/Preview.tsx +1 -1
  98. package/src/features/Conversation/components/ShareMessageModal/index.tsx +39 -14
  99. package/src/features/FileManager/FileList/MasonryFileItem/MasonryItemWrapper.tsx +44 -0
  100. package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +553 -0
  101. package/src/features/FileManager/FileList/MasonrySkeleton.tsx +57 -0
  102. package/src/features/FileManager/FileList/ToolBar/ViewSwitcher.tsx +45 -0
  103. package/src/features/FileManager/FileList/ToolBar/index.tsx +9 -1
  104. package/src/features/FileManager/FileList/index.tsx +83 -13
  105. package/src/features/FileManager/Header/FilesSearchBar.tsx +7 -2
  106. package/src/features/ShareModal/ShareImage/Preview.tsx +1 -1
  107. package/src/features/ShareModal/SharePdf/PdfPreview.tsx +361 -0
  108. package/src/features/ShareModal/SharePdf/index.tsx +194 -0
  109. package/src/features/ShareModal/SharePdf/usePdfGeneration.ts +90 -0
  110. package/src/features/ShareModal/index.tsx +40 -14
  111. package/src/features/ShareModal/style.ts +8 -5
  112. package/src/helpers/toolEngineering/index.ts +7 -1
  113. package/src/helpers/toolFilters.ts +35 -0
  114. package/src/libs/trpc/client/lambda.ts +7 -1
  115. package/src/locales/default/chat.ts +13 -0
  116. package/src/locales/default/common.ts +1 -0
  117. package/src/locales/default/components.ts +4 -0
  118. package/src/locales/default/file.ts +2 -2
  119. package/src/server/globalConfig/parseSystemAgent.ts +4 -2
  120. package/src/server/routers/lambda/exporter.ts +173 -3
  121. package/src/server/routers/lambda/message.ts +11 -0
  122. package/src/services/chat/contextEngineering.ts +1 -9
  123. package/src/store/agent/slices/chat/selectors/agent.ts +16 -6
  124. package/src/store/global/initialState.ts +2 -0
  125. package/src/store/tool/slices/builtin/selectors.ts +15 -5
@@ -1,15 +1,18 @@
1
- import { Modal, Segmented, type SegmentedProps } from '@lobehub/ui';
1
+ import { Modal, Segmented, Tabs } from '@lobehub/ui';
2
2
  import { memo, useId, useMemo, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
+ import { isServerMode } from '@/const/version';
6
7
  import { useIsMobile } from '@/hooks/useIsMobile';
7
8
  import { ChatMessage } from '@/types/message';
8
9
 
9
10
  import ShareImage from './ShareImage';
10
11
  import ShareText from './ShareText';
12
+ import SharePdf from '@/features/ShareModal/SharePdf';
11
13
 
12
14
  enum Tab {
15
+ PDF = 'pdf',
13
16
  Screenshot = 'screenshot',
14
17
  Text = 'text',
15
18
  }
@@ -24,26 +27,39 @@ const ShareModal = memo<ShareModalProps>(({ onCancel, open, message }) => {
24
27
  const [tab, setTab] = useState<Tab>(Tab.Screenshot);
25
28
  const { t } = useTranslation('chat');
26
29
  const uniqueId = useId();
30
+ const isMobile = useIsMobile();
27
31
 
28
- const options: SegmentedProps['options'] = useMemo(
29
- () => [
32
+ const tabItems = useMemo(() => {
33
+ const items = [
30
34
  {
35
+ children: <ShareImage message={message} mobile={isMobile} uniqueId={uniqueId} />,
36
+ key: Tab.Screenshot,
31
37
  label: t('shareModal.screenshot'),
32
- value: Tab.Screenshot,
33
38
  },
34
39
  {
40
+ children: <ShareText item={message} />,
41
+ key: Tab.Text,
35
42
  label: t('shareModal.text'),
36
- value: Tab.Text,
37
43
  },
38
- ],
39
- [],
40
- );
44
+ ];
45
+
46
+ // Only add PDF tab in server mode
47
+ if (isServerMode) {
48
+ items.push({
49
+ children: <SharePdf message={message} />,
50
+ key: Tab.PDF,
51
+ label: t('shareModal.pdf'),
52
+ });
53
+ }
54
+
55
+ return items;
56
+ }, [isMobile, message, uniqueId, t]);
41
57
 
42
- const isMobile = useIsMobile();
43
58
  return (
44
59
  <Modal
45
60
  allowFullscreen
46
61
  centered={false}
62
+ destroyOnHidden={true}
47
63
  footer={null}
48
64
  onCancel={onCancel}
49
65
  open={open}
@@ -54,15 +70,24 @@ const ShareModal = memo<ShareModalProps>(({ onCancel, open, message }) => {
54
70
  <Segmented
55
71
  block
56
72
  onChange={(value) => setTab(value as Tab)}
57
- options={options}
73
+ options={tabItems.map((item) => {
74
+ return {
75
+ label: item?.label,
76
+ value: item?.key,
77
+ };
78
+ })}
58
79
  style={{ width: '100%' }}
59
80
  value={tab}
60
81
  variant={'filled'}
61
82
  />
62
- {tab === Tab.Screenshot && (
63
- <ShareImage message={message} mobile={isMobile} uniqueId={uniqueId} />
64
- )}
65
- {tab === Tab.Text && <ShareText item={message} />}
83
+ <Tabs
84
+ activeKey={tab}
85
+ indicator={{ align: 'center', size: (origin) => origin - 20 }}
86
+ items={tabItems}
87
+ onChange={(key) => setTab(key as Tab)}
88
+ // eslint-disable-next-line react/jsx-no-useless-fragment
89
+ renderTabBar={() => <></>}
90
+ />
66
91
  </Flexbox>
67
92
  </Modal>
68
93
  );
@@ -0,0 +1,44 @@
1
+ import { memo } from 'react';
2
+
3
+ import { FileListItem } from '@/types/files';
4
+
5
+ import MasonryFileItem from '.';
6
+
7
+ interface MasonryItemWrapperProps {
8
+ context: {
9
+ knowledgeBaseId?: string;
10
+ selectFileIds: string[];
11
+ setSelectedFileIds: (updater: (prev: string[]) => string[]) => void;
12
+ };
13
+ data: FileListItem;
14
+ index: number;
15
+ }
16
+
17
+ const MasonryItemWrapper = memo<MasonryItemWrapperProps>(({ data: item, context }) => {
18
+ // Safety check: return null if item is undefined (can happen during deletion)
19
+ if (!item || !item.id) {
20
+ return null;
21
+ }
22
+
23
+ return (
24
+ <div style={{ padding: '8px' }}>
25
+ <MasonryFileItem
26
+ knowledgeBaseId={context.knowledgeBaseId}
27
+ onSelectedChange={(id, checked) => {
28
+ context.setSelectedFileIds((prev: string[]) => {
29
+ if (checked) {
30
+ return [...prev, id];
31
+ }
32
+ return prev.filter((item) => item !== id);
33
+ });
34
+ }}
35
+ selected={context.selectFileIds.includes(item.id)}
36
+ {...item}
37
+ />
38
+ </div>
39
+ );
40
+ });
41
+
42
+ MasonryItemWrapper.displayName = 'MasonryItemWrapper';
43
+
44
+ export default MasonryItemWrapper;
@@ -0,0 +1,553 @@
1
+ import { Button, Tooltip } from '@lobehub/ui';
2
+ import { Checkbox, Image } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { isNull } from 'lodash-es';
5
+ import { FileBoxIcon } from 'lucide-react';
6
+ import { useRouter } from 'next/navigation';
7
+ import { memo, useEffect, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { Flexbox } from 'react-layout-kit';
10
+
11
+ import FileIcon from '@/components/FileIcon';
12
+ import { fileManagerSelectors, useFileStore } from '@/store/file';
13
+ import { FileListItem } from '@/types/files';
14
+ import { formatSize } from '@/utils/format';
15
+ import { isChunkingUnsupported } from '@/utils/isChunkingUnsupported';
16
+
17
+ import ChunksBadge from '../FileListItem/ChunkTag';
18
+ import DropdownMenu from '../FileListItem/DropdownMenu';
19
+
20
+ // Image file types
21
+ const IMAGE_TYPES = new Set([
22
+ 'image/png',
23
+ 'image/jpeg',
24
+ 'image/jpg',
25
+ 'image/gif',
26
+ 'image/webp',
27
+ 'image/svg+xml',
28
+ ]);
29
+
30
+ // Markdown file types
31
+ const MARKDOWN_TYPES = new Set(['text/markdown', 'text/x-markdown']);
32
+
33
+ // Helper to check if filename ends with .md
34
+ const isMarkdownFile = (name: string, fileType?: string) => {
35
+ return (
36
+ name.toLowerCase().endsWith('.md') ||
37
+ name.toLowerCase().endsWith('.markdown') ||
38
+ (fileType && MARKDOWN_TYPES.has(fileType))
39
+ );
40
+ };
41
+
42
+ const useStyles = createStyles(({ css, token }) => ({
43
+ actions: css`
44
+ opacity: 0;
45
+ transition: opacity ${token.motionDurationMid};
46
+ `,
47
+ card: css`
48
+ cursor: pointer;
49
+
50
+ position: relative;
51
+
52
+ overflow: visible;
53
+
54
+ border: 1px solid ${token.colorBorderSecondary};
55
+ border-radius: ${token.borderRadiusLG}px;
56
+
57
+ background: ${token.colorBgContainer};
58
+
59
+ transition: all ${token.motionDurationMid};
60
+
61
+ &:hover {
62
+ border-color: ${token.colorPrimary};
63
+ box-shadow: ${token.boxShadowTertiary};
64
+
65
+ .actions {
66
+ opacity: 1;
67
+ }
68
+
69
+ .checkbox {
70
+ opacity: 1;
71
+ }
72
+
73
+ .dropdown {
74
+ opacity: 1;
75
+ }
76
+
77
+ .floatingChunkBadge {
78
+ opacity: 1;
79
+ }
80
+ }
81
+ `,
82
+ checkbox: css`
83
+ position: absolute;
84
+ z-index: 2;
85
+ inset-block-start: 8px;
86
+ inset-inline-start: 8px;
87
+
88
+ opacity: 0;
89
+
90
+ transition: opacity ${token.motionDurationMid};
91
+ `,
92
+ content: css`
93
+ position: relative;
94
+ `,
95
+ contentWithPadding: css`
96
+ padding: 12px;
97
+ `,
98
+ dropdown: css`
99
+ position: absolute;
100
+ z-index: 2;
101
+ inset-block-start: 8px;
102
+ inset-inline-end: 8px;
103
+
104
+ opacity: 0;
105
+
106
+ transition: opacity ${token.motionDurationMid};
107
+ `,
108
+ floatingChunkBadge: css`
109
+ position: absolute;
110
+ z-index: 3;
111
+ inset-block-end: 8px;
112
+ inset-inline-end: 8px;
113
+
114
+ padding-block: 4px;
115
+ padding-inline: 8px;
116
+ border-radius: ${token.borderRadius}px;
117
+
118
+ opacity: 0;
119
+ background: ${token.colorBgContainer};
120
+ box-shadow: ${token.boxShadow};
121
+
122
+ transition: opacity ${token.motionDurationMid};
123
+ `,
124
+ hoverOverlay: css`
125
+ position: absolute;
126
+ z-index: 1;
127
+ inset: 0;
128
+
129
+ display: flex;
130
+ flex-direction: column;
131
+ align-items: center;
132
+ justify-content: center;
133
+
134
+ padding: 16px;
135
+ border-radius: ${token.borderRadiusLG}px;
136
+
137
+ opacity: 0;
138
+ background: ${token.colorBgMask};
139
+
140
+ transition: opacity ${token.motionDurationMid};
141
+
142
+ &:hover {
143
+ opacity: 1;
144
+ }
145
+ `,
146
+ iconWrapper: css`
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+
151
+ height: 120px;
152
+ margin-block-end: 12px;
153
+ border-radius: ${token.borderRadius}px;
154
+
155
+ background: ${token.colorFillQuaternary};
156
+ `,
157
+ imagePlaceholder: css`
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: center;
161
+
162
+ min-height: 120px;
163
+
164
+ background: ${token.colorFillQuaternary};
165
+ `,
166
+ imageWrapper: css`
167
+ position: relative;
168
+
169
+ overflow: hidden;
170
+
171
+ width: 100%;
172
+ border-radius: ${token.borderRadiusLG}px;
173
+
174
+ background: ${token.colorFillQuaternary};
175
+
176
+ img {
177
+ display: block;
178
+ width: 100%;
179
+ height: auto;
180
+ }
181
+ `,
182
+ markdownLoading: css`
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+
187
+ min-height: 120px;
188
+ border-radius: ${token.borderRadiusLG}px;
189
+
190
+ font-size: 12px;
191
+ color: ${token.colorTextTertiary};
192
+
193
+ background: ${token.colorFillQuaternary};
194
+ `,
195
+ markdownPreview: css`
196
+ position: relative;
197
+
198
+ overflow: hidden;
199
+
200
+ width: 100%;
201
+ min-height: 120px;
202
+ max-height: 300px;
203
+ padding: 16px;
204
+ border-radius: ${token.borderRadiusLG}px;
205
+
206
+ font-size: 13px;
207
+ line-height: 1.6;
208
+ color: ${token.colorTextSecondary};
209
+ word-wrap: break-word;
210
+ white-space: pre-wrap;
211
+
212
+ background: ${token.colorFillQuaternary};
213
+
214
+ &::after {
215
+ pointer-events: none;
216
+ content: '';
217
+
218
+ position: absolute;
219
+ inset-block-end: 0;
220
+ inset-inline: 0;
221
+
222
+ height: 60px;
223
+
224
+ background: linear-gradient(to bottom, transparent, ${token.colorFillQuaternary});
225
+ }
226
+ `,
227
+ name: css`
228
+ overflow: hidden;
229
+ display: -webkit-box;
230
+ -webkit-box-orient: vertical;
231
+ -webkit-line-clamp: 2;
232
+
233
+ margin-block-end: 12px;
234
+
235
+ font-weight: ${token.fontWeightStrong};
236
+ color: ${token.colorText};
237
+ word-break: break-word;
238
+ `,
239
+ overlaySize: css`
240
+ font-size: 12px;
241
+ color: ${token.colorTextLightSolid};
242
+ opacity: 0.9;
243
+ `,
244
+ overlayTitle: css`
245
+ overflow: hidden;
246
+ display: -webkit-box;
247
+ -webkit-box-orient: vertical;
248
+ -webkit-line-clamp: 3;
249
+
250
+ max-width: 100%;
251
+ margin-block-end: 8px;
252
+
253
+ font-size: 14px;
254
+ font-weight: ${token.fontWeightStrong};
255
+ color: ${token.colorTextLightSolid};
256
+ text-align: center;
257
+ word-break: break-word;
258
+ `,
259
+ selected: css`
260
+ border-color: ${token.colorPrimary};
261
+ background: ${token.colorPrimaryBg};
262
+
263
+ .checkbox {
264
+ opacity: 1;
265
+ }
266
+ `,
267
+ }));
268
+
269
+ interface MasonryFileItemProps extends FileListItem {
270
+ knowledgeBaseId?: string;
271
+ onSelectedChange: (id: string, selected: boolean) => void;
272
+ selected?: boolean;
273
+ }
274
+
275
+ const MasonryFileItem = memo<MasonryFileItemProps>(
276
+ ({
277
+ chunkingError,
278
+ embeddingError,
279
+ embeddingStatus,
280
+ finishEmbedding,
281
+ chunkCount,
282
+ url,
283
+ name,
284
+ fileType,
285
+ id,
286
+ selected,
287
+ chunkingStatus,
288
+ onSelectedChange,
289
+ knowledgeBaseId,
290
+ size,
291
+ }) => {
292
+ const { t } = useTranslation('components');
293
+ const { styles, cx } = useStyles();
294
+ const router = useRouter();
295
+ const [imageLoaded, setImageLoaded] = useState(false);
296
+ const [markdownContent, setMarkdownContent] = useState<string>('');
297
+ const [isLoadingMarkdown, setIsLoadingMarkdown] = useState(false);
298
+ const [isCreatingFileParseTask, parseFiles] = useFileStore((s) => [
299
+ fileManagerSelectors.isCreatingFileParseTask(id)(s),
300
+ s.parseFilesToChunks,
301
+ ]);
302
+
303
+ const isSupportedForChunking = !isChunkingUnsupported(fileType);
304
+ const isImage = fileType && IMAGE_TYPES.has(fileType);
305
+ const isMarkdown = isMarkdownFile(name, fileType);
306
+
307
+ // Fetch markdown content
308
+ useEffect(() => {
309
+ if (isMarkdown && url) {
310
+ setIsLoadingMarkdown(true);
311
+ fetch(url)
312
+ .then((res) => res.text())
313
+ .then((text) => {
314
+ // Take first 500 characters for preview
315
+ const preview = text.slice(0, 500);
316
+ setMarkdownContent(preview);
317
+ })
318
+ .catch((error) => {
319
+ console.error('Failed to fetch markdown content:', error);
320
+ setMarkdownContent('');
321
+ })
322
+ .finally(() => {
323
+ setIsLoadingMarkdown(false);
324
+ });
325
+ }
326
+ }, [isMarkdown, url]);
327
+
328
+ return (
329
+ <div className={cx(styles.card, selected && styles.selected)}>
330
+ <div
331
+ className={cx('checkbox', styles.checkbox)}
332
+ onClick={(e) => {
333
+ e.stopPropagation();
334
+ onSelectedChange(id, !selected);
335
+ }}
336
+ >
337
+ <Checkbox checked={selected} />
338
+ </div>
339
+
340
+ <div className={cx('dropdown', styles.dropdown)} onClick={(e) => e.stopPropagation()}>
341
+ <DropdownMenu filename={name} id={id} knowledgeBaseId={knowledgeBaseId} url={url} />
342
+ </div>
343
+
344
+ <div
345
+ className={cx(styles.content, !isImage && !isMarkdown && styles.contentWithPadding)}
346
+ onClick={() => {
347
+ router.push(`/files/${id}`);
348
+ }}
349
+ >
350
+ {isImage && url ? (
351
+ <>
352
+ <div className={styles.imageWrapper}>
353
+ {!imageLoaded && (
354
+ <div className={styles.imagePlaceholder}>
355
+ <FileIcon fileName={name} fileType={fileType} size={64} />
356
+ </div>
357
+ )}
358
+ <Image
359
+ alt={name}
360
+ onError={() => setImageLoaded(false)}
361
+ onLoad={() => setImageLoaded(true)}
362
+ preview={{
363
+ src: url,
364
+ }}
365
+ src={url}
366
+ style={{
367
+ display: 'block',
368
+ height: 'auto',
369
+ opacity: imageLoaded ? 1 : 0,
370
+ transition: 'opacity 0.3s',
371
+ width: '100%',
372
+ }}
373
+ wrapperStyle={{
374
+ display: 'block',
375
+ width: '100%',
376
+ }}
377
+ />
378
+ {/* Hover overlay */}
379
+ <div className={styles.hoverOverlay}>
380
+ <div className={styles.overlayTitle}>{name}</div>
381
+ <div className={styles.overlaySize}>{formatSize(size)}</div>
382
+ </div>
383
+ </div>
384
+ {/* Floating chunk badge or action button */}
385
+ {!isNull(chunkingStatus) && chunkingStatus ? (
386
+ <div
387
+ className={cx('floatingChunkBadge', styles.floatingChunkBadge)}
388
+ onClick={(e) => e.stopPropagation()}
389
+ >
390
+ <ChunksBadge
391
+ chunkCount={chunkCount}
392
+ chunkingError={chunkingError}
393
+ chunkingStatus={chunkingStatus}
394
+ embeddingError={embeddingError}
395
+ embeddingStatus={embeddingStatus}
396
+ finishEmbedding={finishEmbedding}
397
+ id={id}
398
+ />
399
+ </div>
400
+ ) : (
401
+ isSupportedForChunking && (
402
+ <Tooltip title={t('FileManager.actions.chunkingTooltip')}>
403
+ <div
404
+ className={cx('floatingChunkBadge', styles.floatingChunkBadge)}
405
+ onClick={(e) => {
406
+ e.stopPropagation();
407
+ if (!isCreatingFileParseTask) {
408
+ parseFiles([id]);
409
+ }
410
+ }}
411
+ style={{ cursor: 'pointer' }}
412
+ >
413
+ <Button
414
+ icon={FileBoxIcon}
415
+ loading={isCreatingFileParseTask}
416
+ size={'small'}
417
+ type={'text'}
418
+ />
419
+ </div>
420
+ </Tooltip>
421
+ )
422
+ )}
423
+ </>
424
+ ) : isMarkdown ? (
425
+ <>
426
+ <div style={{ position: 'relative' }}>
427
+ {isLoadingMarkdown ? (
428
+ <div className={styles.markdownLoading}>Loading preview...</div>
429
+ ) : markdownContent ? (
430
+ <div className={styles.markdownPreview}>{markdownContent}</div>
431
+ ) : (
432
+ <div className={styles.iconWrapper}>
433
+ <FileIcon fileName={name} fileType={fileType} size={64} />
434
+ </div>
435
+ )}
436
+ {/* Hover overlay */}
437
+ <div className={styles.hoverOverlay}>
438
+ <div className={styles.overlayTitle}>{name}</div>
439
+ <div className={styles.overlaySize}>{formatSize(size)}</div>
440
+ </div>
441
+ </div>
442
+ {/* Floating chunk badge or action button */}
443
+ {!isNull(chunkingStatus) && chunkingStatus ? (
444
+ <div
445
+ className={cx('floatingChunkBadge', styles.floatingChunkBadge)}
446
+ onClick={(e) => e.stopPropagation()}
447
+ >
448
+ <ChunksBadge
449
+ chunkCount={chunkCount}
450
+ chunkingError={chunkingError}
451
+ chunkingStatus={chunkingStatus}
452
+ embeddingError={embeddingError}
453
+ embeddingStatus={embeddingStatus}
454
+ finishEmbedding={finishEmbedding}
455
+ id={id}
456
+ />
457
+ </div>
458
+ ) : (
459
+ isSupportedForChunking && (
460
+ <Tooltip title={t('FileManager.actions.chunkingTooltip')}>
461
+ <div
462
+ className={cx('floatingChunkBadge', styles.floatingChunkBadge)}
463
+ onClick={(e) => {
464
+ e.stopPropagation();
465
+ if (!isCreatingFileParseTask) {
466
+ parseFiles([id]);
467
+ }
468
+ }}
469
+ style={{ cursor: 'pointer' }}
470
+ >
471
+ <Button
472
+ icon={FileBoxIcon}
473
+ loading={isCreatingFileParseTask}
474
+ size={'small'}
475
+ type={'text'}
476
+ />
477
+ </div>
478
+ </Tooltip>
479
+ )
480
+ )}
481
+ </>
482
+ ) : (
483
+ <>
484
+ <Flexbox
485
+ align={'center'}
486
+ gap={12}
487
+ justify={'center'}
488
+ paddingBlock={24}
489
+ paddingInline={12}
490
+ style={{ minHeight: 180 }}
491
+ >
492
+ <FileIcon fileName={name} fileType={fileType} size={64} />
493
+ <div className={styles.name} style={{ textAlign: 'center' }}>
494
+ {name}
495
+ </div>
496
+ <div
497
+ style={{
498
+ color: 'var(--lobe-chat-text-tertiary)',
499
+ fontSize: 12,
500
+ textAlign: 'center',
501
+ }}
502
+ >
503
+ {formatSize(size)}
504
+ </div>
505
+ </Flexbox>
506
+ {/* Floating chunk badge or action button */}
507
+ {!isNull(chunkingStatus) && chunkingStatus ? (
508
+ <div
509
+ className={cx('floatingChunkBadge', styles.floatingChunkBadge)}
510
+ onClick={(e) => e.stopPropagation()}
511
+ >
512
+ <ChunksBadge
513
+ chunkCount={chunkCount}
514
+ chunkingError={chunkingError}
515
+ chunkingStatus={chunkingStatus}
516
+ embeddingError={embeddingError}
517
+ embeddingStatus={embeddingStatus}
518
+ finishEmbedding={finishEmbedding}
519
+ id={id}
520
+ />
521
+ </div>
522
+ ) : (
523
+ isSupportedForChunking && (
524
+ <Tooltip title={t('FileManager.actions.chunkingTooltip')}>
525
+ <div
526
+ className={cx('floatingChunkBadge', styles.floatingChunkBadge)}
527
+ onClick={(e) => {
528
+ e.stopPropagation();
529
+ if (!isCreatingFileParseTask) {
530
+ parseFiles([id]);
531
+ }
532
+ }}
533
+ style={{ cursor: 'pointer' }}
534
+ >
535
+ <Button
536
+ icon={FileBoxIcon}
537
+ loading={isCreatingFileParseTask}
538
+ size={'small'}
539
+ type={'text'}
540
+ />
541
+ </div>
542
+ </Tooltip>
543
+ )
544
+ )}
545
+ </>
546
+ )}
547
+ </div>
548
+ </div>
549
+ );
550
+ },
551
+ );
552
+
553
+ export default MasonryFileItem;