@lobehub/chat 1.131.4 → 1.132.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 (39) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/context-engine/src/processors/MessageContent.ts +45 -10
  5. package/packages/context-engine/src/processors/__tests__/MessageContent.test.ts +179 -1
  6. package/packages/database/src/models/message.ts +9 -1
  7. package/packages/model-bank/src/aiModels/google.ts +7 -0
  8. package/packages/model-runtime/src/providers/google/index.ts +31 -8
  9. package/packages/model-runtime/src/types/chat.ts +6 -0
  10. package/packages/prompts/src/prompts/files/index.test.ts +148 -3
  11. package/packages/prompts/src/prompts/files/index.ts +17 -5
  12. package/packages/prompts/src/prompts/files/video.ts +17 -0
  13. package/packages/types/src/agent/index.ts +1 -1
  14. package/packages/types/src/message/chat.ts +2 -4
  15. package/packages/types/src/message/index.ts +1 -0
  16. package/packages/types/src/message/video.ts +5 -0
  17. package/packages/utils/src/client/index.ts +1 -0
  18. package/packages/utils/src/client/videoValidation.test.ts +53 -0
  19. package/packages/utils/src/client/videoValidation.ts +21 -0
  20. package/packages/utils/src/parseModels.ts +4 -0
  21. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +9 -4
  22. package/src/components/ModelSelect/index.tsx +14 -2
  23. package/src/features/ChatInput/ActionBar/Upload/ClientMode.tsx +7 -0
  24. package/src/features/ChatInput/ActionBar/Upload/ServerMode.tsx +29 -3
  25. package/src/features/ChatInput/components/UploadDetail/UploadStatus.tsx +1 -1
  26. package/src/features/Conversation/Messages/Assistant/index.tsx +4 -1
  27. package/src/features/Conversation/Messages/User/VideoFileListViewer.tsx +31 -0
  28. package/src/features/Conversation/Messages/User/index.tsx +3 -1
  29. package/src/hooks/useModelSupportVideo.ts +10 -0
  30. package/src/locales/default/chat.ts +4 -0
  31. package/src/locales/default/components.ts +1 -0
  32. package/src/server/routers/lambda/aiChat.ts +1 -0
  33. package/src/services/chat/contextEngineering.test.ts +0 -1
  34. package/src/services/chat/contextEngineering.ts +3 -1
  35. package/src/services/chat/helper.ts +4 -0
  36. package/src/services/upload.ts +1 -1
  37. package/src/store/aiInfra/slices/aiModel/selectors.ts +7 -0
  38. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +22 -0
  39. package/src/store/chat/slices/message/action.ts +15 -14
@@ -1,4 +1,4 @@
1
- import { ChatFileItem, ChatImageItem } from '@lobechat/types';
1
+ import { ChatFileItem, ChatImageItem, ChatVideoItem } from '@lobechat/types';
2
2
  import { describe, expect, it } from 'vitest';
3
3
 
4
4
  import { filesPrompts } from './index';
@@ -19,6 +19,12 @@ describe('filesPrompts', () => {
19
19
  url: 'https://example.com/test.pdf',
20
20
  };
21
21
 
22
+ const mockVideo: ChatVideoItem = {
23
+ id: 'video-1',
24
+ alt: 'test video',
25
+ url: 'https://example.com/video.mp4',
26
+ };
27
+
22
28
  it('should generate prompt with only images', () => {
23
29
  const result = filesPrompts({
24
30
  imageList: [mockImage],
@@ -37,7 +43,6 @@ describe('filesPrompts', () => {
37
43
  <images_docstring>here are user upload images you can refer to</images_docstring>
38
44
  <image name="test image" url="https://example.com/image.jpg"></image>
39
45
  </images>
40
-
41
46
  </files_info>
42
47
  <!-- END SYSTEM CONTEXT -->`,
43
48
  );
@@ -57,7 +62,6 @@ describe('filesPrompts', () => {
57
62
  2. the context is only required when user's queries rely on it.
58
63
  </context.instruction>
59
64
  <files_info>
60
-
61
65
  <files>
62
66
  <files_docstring>here are user upload files you can refer to</files_docstring>
63
67
  <file id="file-1" name="test.pdf" type="application/pdf" size="1024" url="https://example.com/test.pdf"></file>
@@ -184,4 +188,145 @@ describe('filesPrompts', () => {
184
188
  <!-- END SYSTEM CONTEXT -->"
185
189
  `);
186
190
  });
191
+
192
+ describe('Video functionality', () => {
193
+ it('should generate prompt with only videos', () => {
194
+ const result = filesPrompts({
195
+ videoList: [mockVideo],
196
+ });
197
+
198
+ expect(result).toEqual(
199
+ `<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
200
+ <context.instruction>following part contains context information injected by the system. Please follow these instructions:
201
+
202
+ 1. Always prioritize handling user-visible content.
203
+ 2. the context is only required when user's queries rely on it.
204
+ </context.instruction>
205
+ <files_info>
206
+ <videos>
207
+ <videos_docstring>here are user upload videos you can refer to</videos_docstring>
208
+ <video name="test video" url="https://example.com/video.mp4"></video>
209
+ </videos>
210
+ </files_info>
211
+ <!-- END SYSTEM CONTEXT -->`,
212
+ );
213
+ });
214
+
215
+ it('should generate prompt with videos and images', () => {
216
+ const result = filesPrompts({
217
+ imageList: [mockImage],
218
+ videoList: [mockVideo],
219
+ });
220
+
221
+ expect(result).toEqual(
222
+ `<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
223
+ <context.instruction>following part contains context information injected by the system. Please follow these instructions:
224
+
225
+ 1. Always prioritize handling user-visible content.
226
+ 2. the context is only required when user's queries rely on it.
227
+ </context.instruction>
228
+ <files_info>
229
+ <images>
230
+ <images_docstring>here are user upload images you can refer to</images_docstring>
231
+ <image name="test image" url="https://example.com/image.jpg"></image>
232
+ </images>
233
+ <videos>
234
+ <videos_docstring>here are user upload videos you can refer to</videos_docstring>
235
+ <video name="test video" url="https://example.com/video.mp4"></video>
236
+ </videos>
237
+ </files_info>
238
+ <!-- END SYSTEM CONTEXT -->`,
239
+ );
240
+ });
241
+
242
+ it('should generate prompt with all media types', () => {
243
+ const result = filesPrompts({
244
+ imageList: [mockImage],
245
+ fileList: [mockFile],
246
+ videoList: [mockVideo],
247
+ });
248
+
249
+ expect(result).toEqual(
250
+ `<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
251
+ <context.instruction>following part contains context information injected by the system. Please follow these instructions:
252
+
253
+ 1. Always prioritize handling user-visible content.
254
+ 2. the context is only required when user's queries rely on it.
255
+ </context.instruction>
256
+ <files_info>
257
+ <images>
258
+ <images_docstring>here are user upload images you can refer to</images_docstring>
259
+ <image name="test image" url="https://example.com/image.jpg"></image>
260
+ </images>
261
+ <files>
262
+ <files_docstring>here are user upload files you can refer to</files_docstring>
263
+ <file id="file-1" name="test.pdf" type="application/pdf" size="1024" url="https://example.com/test.pdf"></file>
264
+ </files>
265
+ <videos>
266
+ <videos_docstring>here are user upload videos you can refer to</videos_docstring>
267
+ <video name="test video" url="https://example.com/video.mp4"></video>
268
+ </videos>
269
+ </files_info>
270
+ <!-- END SYSTEM CONTEXT -->`,
271
+ );
272
+ });
273
+
274
+ it('should handle multiple videos', () => {
275
+ const videos: ChatVideoItem[] = [
276
+ mockVideo,
277
+ {
278
+ id: 'video-2',
279
+ alt: 'second video',
280
+ url: 'https://example.com/video2.mp4',
281
+ },
282
+ ];
283
+
284
+ const result = filesPrompts({
285
+ videoList: videos,
286
+ });
287
+
288
+ expect(result).toContain('test video');
289
+ expect(result).toContain('second video');
290
+ expect(result).toMatch(/<video.*?>.*<video.*?>/s); // Check for multiple video tags
291
+ });
292
+
293
+ it('should handle videos without url when addUrl is false', () => {
294
+ const result = filesPrompts({
295
+ videoList: [mockVideo],
296
+ addUrl: false,
297
+ });
298
+
299
+ expect(result).toMatchInlineSnapshot(`
300
+ "<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
301
+ <context.instruction>following part contains context information injected by the system. Please follow these instructions:
302
+
303
+ 1. Always prioritize handling user-visible content.
304
+ 2. the context is only required when user's queries rely on it.
305
+ </context.instruction>
306
+ <files_info>
307
+ <videos>
308
+ <videos_docstring>here are user upload videos you can refer to</videos_docstring>
309
+ <video name="test video"></video>
310
+ </videos>
311
+ </files_info>
312
+ <!-- END SYSTEM CONTEXT -->"
313
+ `);
314
+ });
315
+
316
+ it('should return empty string when all lists are empty', () => {
317
+ const result = filesPrompts({
318
+ imageList: [],
319
+ fileList: [],
320
+ videoList: [],
321
+ });
322
+
323
+ expect(result).toEqual('');
324
+ });
325
+
326
+ it('should return empty string when no lists are provided', () => {
327
+ const result = filesPrompts({});
328
+
329
+ expect(result).toEqual('');
330
+ });
331
+ });
187
332
  });
@@ -1,18 +1,31 @@
1
- import { ChatFileItem, ChatImageItem } from '@lobechat/types';
1
+ import { ChatFileItem, ChatImageItem, ChatVideoItem } from '@lobechat/types';
2
2
 
3
3
  import { filePrompts } from './file';
4
4
  import { imagesPrompts } from './image';
5
+ import { videosPrompts } from './video';
5
6
 
6
7
  export const filesPrompts = ({
7
8
  imageList,
8
9
  fileList,
10
+ videoList,
9
11
  addUrl = true,
10
12
  }: {
11
13
  addUrl?: boolean;
12
14
  fileList?: ChatFileItem[];
13
- imageList: ChatImageItem[];
15
+ imageList?: ChatImageItem[];
16
+ videoList?: ChatVideoItem[];
14
17
  }) => {
15
- if (imageList.length === 0 && (fileList || []).length === 0) return '';
18
+ const hasImages = (imageList || []).length > 0;
19
+ const hasFiles = (fileList || []).length > 0;
20
+ const hasVideos = (videoList || []).length > 0;
21
+
22
+ if (!hasImages && !hasFiles && !hasVideos) return '';
23
+
24
+ const contentParts = [
25
+ hasImages ? imagesPrompts(imageList!, addUrl) : '',
26
+ hasFiles ? filePrompts(fileList!, addUrl) : '',
27
+ hasVideos ? videosPrompts(videoList!, addUrl) : '',
28
+ ].filter(Boolean);
16
29
 
17
30
  const prompt = `<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
18
31
  <context.instruction>following part contains context information injected by the system. Please follow these instructions:
@@ -21,8 +34,7 @@ export const filesPrompts = ({
21
34
  2. the context is only required when user's queries rely on it.
22
35
  </context.instruction>
23
36
  <files_info>
24
- ${imagesPrompts(imageList, addUrl)}
25
- ${fileList ? filePrompts(fileList, addUrl) : ''}
37
+ ${contentParts.join('\n')}
26
38
  </files_info>
27
39
  <!-- END SYSTEM CONTEXT -->`;
28
40
 
@@ -0,0 +1,17 @@
1
+ import { ChatVideoItem } from '@lobechat/types';
2
+
3
+ const videoPrompt = (item: ChatVideoItem, attachUrl: boolean) =>
4
+ attachUrl
5
+ ? `<video name="${item.alt}" url="${item.url}"></video>`
6
+ : `<video name="${item.alt}"></video>`;
7
+
8
+ export const videosPrompts = (videoList: ChatVideoItem[], addUrl: boolean = true) => {
9
+ if (videoList.length === 0) return '';
10
+
11
+ const prompt = `<videos>
12
+ <videos_docstring>here are user upload videos you can refer to</videos_docstring>
13
+ ${videoList.map((item) => videoPrompt(item, addUrl)).join('\n')}
14
+ </videos>`;
15
+
16
+ return prompt.trim();
17
+ };
@@ -1,4 +1,4 @@
1
- import { LLMParams } from '../../../model-bank/src/types/aiModel';
1
+ import { LLMParams } from 'model-bank';
2
2
  import { FileItem } from '../files';
3
3
  import { KnowledgeBaseItem } from '../knowledgeBase';
4
4
  import { FewShots } from '../llm';
@@ -5,6 +5,7 @@ import type { ChatMessageError, MessageMetadata, MessageRoleType, ModelReasoning
5
5
  import { ChatImageItem } from './image';
6
6
  import { ChatPluginPayload, ChatToolPayload } from './tools';
7
7
  import { Translate } from './translate';
8
+ import { ChatVideoItem } from './video';
8
9
 
9
10
  export interface ChatTranslate extends Translate {
10
11
  content?: string;
@@ -63,13 +64,11 @@ export interface ChatMessage {
63
64
  id: string;
64
65
  imageList?: ChatImageItem[];
65
66
  meta: MetaData;
66
-
67
67
  metadata?: MessageMetadata | null;
68
68
  /**
69
69
  * observation id
70
70
  */
71
71
  observationId?: string;
72
-
73
72
  /**
74
73
  * parent message id
75
74
  */
@@ -83,14 +82,12 @@ export interface ChatMessage {
83
82
  quotaId?: string;
84
83
  ragQuery?: string | null;
85
84
  ragQueryId?: string | null;
86
-
87
85
  ragRawQuery?: string | null;
88
86
  reasoning?: ModelReasoning | null;
89
87
  /**
90
88
  * message role type
91
89
  */
92
90
  role: MessageRoleType;
93
-
94
91
  search?: GroundingSearch | null;
95
92
  sessionId?: string;
96
93
  threadId?: string | null;
@@ -105,6 +102,7 @@ export interface ChatMessage {
105
102
  */
106
103
  traceId?: string;
107
104
  updatedAt: number;
105
+ videoList?: ChatVideoItem[];
108
106
  }
109
107
 
110
108
  export interface CreateMessageParams
@@ -5,6 +5,7 @@ export * from './chat';
5
5
  export * from './image';
6
6
  export * from './rag';
7
7
  export * from './tools';
8
+ export * from './video';
8
9
 
9
10
  export interface SendMessageParams {
10
11
  /**
@@ -0,0 +1,5 @@
1
+ export interface ChatVideoItem {
2
+ alt: string;
3
+ id: string;
4
+ url: string;
5
+ }
@@ -3,3 +3,4 @@ export * from './downloadFile';
3
3
  export * from './exportFile';
4
4
  export * from './fetchEventSource';
5
5
  export * from './sanitize';
6
+ export * from './videoValidation';
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { validateVideoFileSize } from './videoValidation';
4
+
5
+ describe('validateVideoFileSize', () => {
6
+ it('should return valid for non-video files', () => {
7
+ const mockFile = new File(['test'], 'test.txt', { type: 'text/plain' });
8
+ const result = validateVideoFileSize(mockFile);
9
+
10
+ expect(result.isValid).toBe(true);
11
+ expect(result.actualSize).toBeUndefined();
12
+ });
13
+
14
+ it('should return valid for video files under 20MB', () => {
15
+ const mockVideoFile = new File(['x'.repeat(10 * 1024 * 1024)], 'video.mp4', {
16
+ type: 'video/mp4',
17
+ });
18
+ const result = validateVideoFileSize(mockVideoFile);
19
+
20
+ expect(result.isValid).toBe(true);
21
+ expect(result.actualSize).toBe('10.0 MB');
22
+ });
23
+
24
+ it('should return invalid for video files over 20MB', () => {
25
+ const mockLargeVideoFile = new File(['x'.repeat(25 * 1024 * 1024)], 'large-video.mp4', {
26
+ type: 'video/mp4',
27
+ });
28
+ const result = validateVideoFileSize(mockLargeVideoFile);
29
+
30
+ expect(result.isValid).toBe(false);
31
+ expect(result.actualSize).toBe('25.0 MB');
32
+ });
33
+
34
+ it('should return invalid for video files exactly at 20MB limit plus 1 byte', () => {
35
+ const mockBoundaryFile = new File(['x'.repeat(20 * 1024 * 1024 + 1)], 'boundary.mp4', {
36
+ type: 'video/mp4',
37
+ });
38
+ const result = validateVideoFileSize(mockBoundaryFile);
39
+
40
+ expect(result.isValid).toBe(false);
41
+ expect(result.actualSize).toBe('20.0 MB');
42
+ });
43
+
44
+ it('should return valid for video files exactly at 20MB limit', () => {
45
+ const mockBoundaryFile = new File(['x'.repeat(20 * 1024 * 1024)], 'boundary.mp4', {
46
+ type: 'video/mp4',
47
+ });
48
+ const result = validateVideoFileSize(mockBoundaryFile);
49
+
50
+ expect(result.isValid).toBe(true);
51
+ expect(result.actualSize).toBe('20.0 MB');
52
+ });
53
+ });
@@ -0,0 +1,21 @@
1
+ import { formatSize } from '../format';
2
+
3
+ const VIDEO_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB in bytes
4
+
5
+ export interface VideoValidationResult {
6
+ actualSize?: string;
7
+ isValid: boolean;
8
+ }
9
+
10
+ export const validateVideoFileSize = (file: File): VideoValidationResult => {
11
+ if (!file.type.startsWith('video/')) {
12
+ return { isValid: true };
13
+ }
14
+
15
+ const isValid = file.size <= VIDEO_SIZE_LIMIT;
16
+
17
+ return {
18
+ actualSize: formatSize(file.size),
19
+ isValid,
20
+ };
21
+ };
@@ -90,6 +90,10 @@ export const parseModelString = async (
90
90
  model.abilities!.files = true;
91
91
  break;
92
92
  }
93
+ case 'video': {
94
+ model.abilities!.video = true;
95
+ break;
96
+ }
93
97
  case 'search': {
94
98
  model.abilities!.search = true;
95
99
  break;
@@ -38,13 +38,14 @@ export const useSend = () => {
38
38
  const { analytics } = useAnalytics();
39
39
  const checkGeminiChineseWarning = useGeminiChineseWarning();
40
40
 
41
- const fileList = fileChatSelectors.chatUploadFileList(useFileStore.getState());
41
+ // 使用订阅以保持最新文件列表
42
+ const reactiveFileList = useFileStore(fileChatSelectors.chatUploadFileList);
42
43
  const [isUploadingFiles, clearChatUploadFileList] = useFileStore((s) => [
43
44
  fileChatSelectors.isUploadingFiles(s),
44
45
  s.clearChatUploadFileList,
45
46
  ]);
46
47
 
47
- const isInputEmpty = isContentEmpty && fileList.length === 0;
48
+ const isInputEmpty = isContentEmpty && reactiveFileList.length === 0;
48
49
 
49
50
  const canNotSend =
50
51
  isInputEmpty || isUploadingFiles || isSendButtonDisabledByMessage || isSendingMessage;
@@ -63,6 +64,8 @@ export const useSend = () => {
63
64
  if (chatSelectors.isAIGenerating(store)) return;
64
65
 
65
66
  const inputMessage = store.inputMessage;
67
+ // 发送时再取一次最新的文件列表,防止闭包拿到旧值
68
+ const fileList = fileChatSelectors.chatUploadFileList(useFileStore.getState());
66
69
 
67
70
  // if there is no message and no image, then we should not send the message
68
71
  if (!inputMessage && fileList.length === 0) return;
@@ -92,9 +95,11 @@ export const useSend = () => {
92
95
  // 获取分析数据
93
96
  const userStore = getUserStoreState();
94
97
 
95
- // 直接使用现有数据结构判断消息类型
98
+ // 直接使用现有数据结构判断消息类型(支持 video)
96
99
  const hasImages = fileList.some((file) => file.file?.type?.startsWith('image'));
97
- const messageType = fileList.length === 0 ? 'text' : hasImages ? 'image' : 'file';
100
+ const hasVideos = fileList.some((file) => file.file?.type?.startsWith('video'));
101
+ const messageType =
102
+ fileList.length === 0 ? 'text' : hasVideos ? 'video' : hasImages ? 'image' : 'file';
98
103
 
99
104
  analytics?.track({
100
105
  name: 'send_message',
@@ -1,3 +1,4 @@
1
+ import { ChatModelCard } from '@lobechat/types';
1
2
  import { IconAvatarProps, ModelIcon, ProviderIcon } from '@lobehub/icons';
2
3
  import { Avatar, Icon, Tag, Text, Tooltip } from '@lobehub/ui';
3
4
  import { createStyles, useResponsive } from 'antd-style';
@@ -9,15 +10,15 @@ import {
9
10
  LucideImage,
10
11
  LucidePaperclip,
11
12
  ToyBrick,
13
+ Video,
12
14
  } from 'lucide-react';
15
+ import { ModelAbilities } from 'model-bank';
13
16
  import numeral from 'numeral';
14
17
  import { FC, memo } from 'react';
15
18
  import { useTranslation } from 'react-i18next';
16
19
  import { Flexbox } from 'react-layout-kit';
17
20
 
18
- import { ModelAbilities } from '../../../packages/model-bank/src/types/aiModel';
19
21
  import { AiProviderSourceType } from '@/types/aiProvider';
20
- import { ChatModelCard } from '@/types/llm';
21
22
  import { formatTokenNumber } from '@/utils/format';
22
23
 
23
24
  export const TAG_CLASSNAME = 'lobe-model-info-tags';
@@ -99,6 +100,17 @@ export const ModelInfoTags = memo<ModelInfoTagsProps>(
99
100
  </Tag>
100
101
  </Tooltip>
101
102
  )}
103
+ {model.video && (
104
+ <Tooltip
105
+ placement={placement}
106
+ styles={{ root: { pointerEvents: 'none' } }}
107
+ title={t('ModelSelect.featureTag.video')}
108
+ >
109
+ <Tag className={styles.tag} color={'magenta'} size={'small'}>
110
+ <Icon icon={Video} />
111
+ </Tag>
112
+ </Tooltip>
113
+ )}
102
114
  {model.functionCall && (
103
115
  <Tooltip
104
116
  placement={placement}
@@ -4,6 +4,7 @@ import { FileUp, LucideImage } from 'lucide-react';
4
4
  import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
+ import { message } from '@/components/AntdStaticMethods';
7
8
  import { useModelSupportFiles } from '@/hooks/useModelSupportFiles';
8
9
  import { useModelSupportVision } from '@/hooks/useModelSupportVision';
9
10
  import { useAgentStore } from '@/store/agent';
@@ -26,6 +27,12 @@ const FileUpload = memo(() => {
26
27
  <Upload
27
28
  accept={enabledFiles ? undefined : 'image/*'}
28
29
  beforeUpload={async (file) => {
30
+ // Check if trying to upload non-image files in client mode
31
+ if (!enabledFiles && !file.type.startsWith('image')) {
32
+ message.warning(t('upload.clientMode.fileNotSupported'));
33
+ return false;
34
+ }
35
+
29
36
  await upload([file]);
30
37
 
31
38
  return false;
@@ -1,3 +1,4 @@
1
+ import { validateVideoFileSize } from '@lobechat/utils/client';
1
2
  import { MenuProps, Tooltip } from '@lobehub/ui';
2
3
  import { Upload } from 'antd';
3
4
  import { css, cx } from 'antd-style';
@@ -5,9 +6,10 @@ import { FileUp, FolderUp, ImageUp, Paperclip } from 'lucide-react';
5
6
  import { memo } from 'react';
6
7
  import { useTranslation } from 'react-i18next';
7
8
 
9
+ import { message } from '@/components/AntdStaticMethods';
8
10
  import { useModelSupportVision } from '@/hooks/useModelSupportVision';
9
11
  import { useAgentStore } from '@/store/agent';
10
- import { agentSelectors } from '@/store/agent/slices/chat';
12
+ import { agentSelectors } from '@/store/agent/selectors';
11
13
  import { useFileStore } from '@/store/file';
12
14
 
13
15
  import Action from '../components/Action';
@@ -61,7 +63,19 @@ const FileUpload = memo(() => {
61
63
  label: (
62
64
  <Upload
63
65
  beforeUpload={async (file) => {
64
- if (!canUploadImage && file.type.startsWith('image')) return false;
66
+ if (!canUploadImage && (file.type.startsWith('image') || file.type.startsWith('video')))
67
+ return false;
68
+
69
+ // Validate video file size
70
+ const validation = validateVideoFileSize(file);
71
+ if (!validation.isValid) {
72
+ message.error(
73
+ t('upload.validation.videoSizeExceeded', {
74
+ actualSize: validation.actualSize,
75
+ }),
76
+ );
77
+ return false;
78
+ }
65
79
 
66
80
  await upload([file]);
67
81
 
@@ -80,7 +94,19 @@ const FileUpload = memo(() => {
80
94
  label: (
81
95
  <Upload
82
96
  beforeUpload={async (file) => {
83
- if (!canUploadImage && file.type.startsWith('image')) return false;
97
+ if (!canUploadImage && (file.type.startsWith('image') || file.type.startsWith('video')))
98
+ return false;
99
+
100
+ // Validate video file size
101
+ const validation = validateVideoFileSize(file);
102
+ if (!validation.isValid) {
103
+ message.error(
104
+ t('upload.validation.videoSizeExceeded', {
105
+ actualSize: validation.actualSize,
106
+ }),
107
+ );
108
+ return false;
109
+ }
84
110
 
85
111
  await upload([file]);
86
112
 
@@ -38,7 +38,7 @@ const UploadStatus = memo<UploadStateProps>(({ status, size, uploadState }) => {
38
38
  <Flexbox align={'center'} gap={4} horizontal>
39
39
  <Progress percent={uploadState?.progress} size={14} type="circle" />
40
40
  <Text style={{ fontSize: 12 }} type={'secondary'}>
41
- {formatSize(size * ((uploadState?.progress || 0) / 100), 0)} / {formatSize(size)}
41
+ {formatSize(size * ((uploadState?.progress || 0) / 100), 0)}
42
42
  </Text>
43
43
  </Flexbox>
44
44
  );
@@ -3,6 +3,7 @@ import { Flexbox } from 'react-layout-kit';
3
3
 
4
4
  import { LOADING_FLAT } from '@/const/message';
5
5
  import ImageFileListViewer from '@/features/Conversation/Messages/User/ImageFileListViewer';
6
+ import VideoFileListViewer from '@/features/Conversation/Messages/User/VideoFileListViewer';
6
7
  import { useChatStore } from '@/store/chat';
7
8
  import { aiChatSelectors, chatSelectors } from '@/store/chat/selectors';
8
9
  import { ChatMessage } from '@/types/message';
@@ -18,7 +19,7 @@ export const AssistantMessage = memo<
18
19
  ChatMessage & {
19
20
  editableContent: ReactNode;
20
21
  }
21
- >(({ id, tools, content, chunksList, search, imageList, ...props }) => {
22
+ >(({ id, tools, content, chunksList, search, imageList, videoList, ...props }) => {
22
23
  const editing = useChatStore(chatSelectors.isMessageEditing(id));
23
24
  const generating = useChatStore(chatSelectors.isMessageGenerating(id));
24
25
 
@@ -30,6 +31,7 @@ export const AssistantMessage = memo<
30
31
 
31
32
  const showSearch = !!search && !!search.citations?.length;
32
33
  const showImageItems = !!imageList && imageList.length > 0;
34
+ const showVideoItems = !!videoList && videoList.length > 0;
33
35
 
34
36
  // remove \n to avoid empty content
35
37
  // refs: https://github.com/lobehub/lobe-chat/pull/6153
@@ -67,6 +69,7 @@ export const AssistantMessage = memo<
67
69
  )
68
70
  )}
69
71
  {showImageItems && <ImageFileListViewer items={imageList} />}
72
+ {showVideoItems && <VideoFileListViewer items={videoList} />}
70
73
  {tools && (
71
74
  <Flexbox gap={8}>
72
75
  {tools.map((toolCall, index) => (
@@ -0,0 +1,31 @@
1
+ import { memo } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import { ChatVideoItem } from '@/types/message';
5
+
6
+ interface VideoFileListViewerProps {
7
+ items: ChatVideoItem[];
8
+ }
9
+
10
+ const VideoFileListViewer = memo<VideoFileListViewerProps>(({ items }) => {
11
+ return (
12
+ <Flexbox gap={8}>
13
+ {items.map((item) => (
14
+ <video
15
+ controls
16
+ key={item.id}
17
+ style={{
18
+ borderRadius: 8,
19
+ maxHeight: 400,
20
+ maxWidth: '100%',
21
+ }}
22
+ >
23
+ <source src={item.url} />
24
+ {item.alt}
25
+ </video>
26
+ ))}
27
+ </Flexbox>
28
+ );
29
+ });
30
+
31
+ export default VideoFileListViewer;
@@ -7,18 +7,20 @@ import { ChatMessage } from '@/types/message';
7
7
 
8
8
  import FileListViewer from './FileListViewer';
9
9
  import ImageFileListViewer from './ImageFileListViewer';
10
+ import VideoFileListViewer from './VideoFileListViewer';
10
11
 
11
12
  export const UserMessage = memo<
12
13
  ChatMessage & {
13
14
  editableContent: ReactNode;
14
15
  }
15
- >(({ id, editableContent, content, imageList, fileList }) => {
16
+ >(({ id, editableContent, content, imageList, videoList, fileList }) => {
16
17
  if (content === LOADING_FLAT) return <BubblesLoading />;
17
18
 
18
19
  return (
19
20
  <Flexbox gap={8} id={id}>
20
21
  {editableContent}
21
22
  {imageList && imageList?.length > 0 && <ImageFileListViewer items={imageList} />}
23
+ {videoList && videoList?.length > 0 && <VideoFileListViewer items={videoList} />}
22
24
  {fileList && fileList?.length > 0 && (
23
25
  <div style={{ marginTop: 8 }}>
24
26
  <FileListViewer items={fileList} />