@lobehub/lobehub 2.0.0-next.115 → 2.0.0-next.116
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 +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/context-engine/src/processors/MessageContent.ts +100 -6
- package/packages/context-engine/src/processors/__tests__/MessageContent.test.ts +239 -0
- package/packages/fetch-sse/src/fetchSSE.ts +30 -0
- package/packages/model-runtime/src/core/contextBuilders/google.test.ts +78 -24
- package/packages/model-runtime/src/core/contextBuilders/google.ts +10 -2
- package/packages/model-runtime/src/core/streams/google/google-ai.test.ts +451 -20
- package/packages/model-runtime/src/core/streams/google/index.ts +113 -3
- package/packages/model-runtime/src/core/streams/protocol.ts +19 -0
- package/packages/types/src/message/common/base.ts +26 -0
- package/packages/types/src/message/common/metadata.ts +7 -0
- package/packages/utils/src/index.ts +1 -0
- package/packages/utils/src/multimodalContent.ts +25 -0
- package/src/components/Thinking/index.tsx +3 -3
- package/src/features/ChatList/Messages/Assistant/DisplayContent.tsx +44 -0
- package/src/features/ChatList/Messages/Assistant/MessageBody.tsx +96 -0
- package/src/features/ChatList/Messages/Assistant/Reasoning/index.tsx +26 -13
- package/src/features/ChatList/Messages/Assistant/index.tsx +8 -6
- package/src/features/ChatList/Messages/Default.tsx +4 -7
- package/src/features/ChatList/components/RichContentRenderer.tsx +35 -0
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +244 -17
- package/src/features/ChatList/Messages/Assistant/MessageContent.tsx +0 -78
|
@@ -5,14 +5,17 @@ import { isDesktop } from '@lobechat/const';
|
|
|
5
5
|
import {
|
|
6
6
|
ChatImageItem,
|
|
7
7
|
ChatToolPayload,
|
|
8
|
+
MessageContentPart,
|
|
8
9
|
MessageToolCall,
|
|
9
10
|
ModelUsage,
|
|
10
11
|
TraceNameMap,
|
|
11
12
|
UIChatMessage,
|
|
12
13
|
} from '@lobechat/types';
|
|
14
|
+
import { serializePartsForStorage } from '@lobechat/utils';
|
|
13
15
|
import debug from 'debug';
|
|
14
16
|
import { t } from 'i18next';
|
|
15
17
|
import { throttle } from 'lodash-es';
|
|
18
|
+
import pMap from 'p-map';
|
|
16
19
|
import { StateCreator } from 'zustand/vanilla';
|
|
17
20
|
|
|
18
21
|
import { createAgentToolsEngine } from '@/helpers/toolEngineering';
|
|
@@ -272,14 +275,21 @@ export const streamingExecutor: StateCreator<
|
|
|
272
275
|
let finalUsage;
|
|
273
276
|
let msgTraceId: string | undefined;
|
|
274
277
|
let output = '';
|
|
275
|
-
|
|
278
|
+
|
|
279
|
+
let thinkingContent = '';
|
|
276
280
|
let thinkingStartAt: number;
|
|
277
|
-
let
|
|
281
|
+
let thinkingDuration: number | undefined;
|
|
278
282
|
let reasoningOperationId: string | undefined;
|
|
279
283
|
let finishType: string | undefined;
|
|
280
284
|
// to upload image
|
|
281
285
|
const uploadTasks: Map<string, Promise<{ id?: string; url?: string }>> = new Map();
|
|
282
286
|
|
|
287
|
+
// Multimodal content parts
|
|
288
|
+
let contentParts: MessageContentPart[] = [];
|
|
289
|
+
let reasoningParts: MessageContentPart[] = [];
|
|
290
|
+
const contentImageUploads: Map<number, Promise<string>> = new Map();
|
|
291
|
+
const reasoningImageUploads: Map<number, Promise<string>> = new Map();
|
|
292
|
+
|
|
283
293
|
// Throttle tool_calls updates to prevent excessive re-renders (max once per 300ms)
|
|
284
294
|
const throttledUpdateToolCalls = throttle(
|
|
285
295
|
(toolCalls: MessageToolCall[]) => {
|
|
@@ -344,7 +354,9 @@ export const streamingExecutor: StateCreator<
|
|
|
344
354
|
if (uploadTasks.size > 0) {
|
|
345
355
|
try {
|
|
346
356
|
// 等待所有上传任务完成
|
|
347
|
-
const uploadResults = await
|
|
357
|
+
const uploadResults = await pMap(Array.from(uploadTasks.values()), (task) => task, {
|
|
358
|
+
concurrency: 5,
|
|
359
|
+
});
|
|
348
360
|
|
|
349
361
|
// 使用上传后的 S3 URL 替换原始图像数据
|
|
350
362
|
finalImages = uploadResults.filter((i) => !!i.url) as ChatImageItem[];
|
|
@@ -353,6 +365,14 @@ export const streamingExecutor: StateCreator<
|
|
|
353
365
|
}
|
|
354
366
|
}
|
|
355
367
|
|
|
368
|
+
// Wait for all multimodal image uploads to complete
|
|
369
|
+
// Note: Arrays are already updated in-place when uploads complete
|
|
370
|
+
// Use Promise.allSettled to continue even if some uploads fail
|
|
371
|
+
await Promise.allSettled([
|
|
372
|
+
...Array.from(contentImageUploads.values()),
|
|
373
|
+
...Array.from(reasoningImageUploads.values()),
|
|
374
|
+
]);
|
|
375
|
+
|
|
356
376
|
let parsedToolCalls = toolCalls;
|
|
357
377
|
if (parsedToolCalls && parsedToolCalls.length > 0) {
|
|
358
378
|
// Flush any pending throttled updates before finalizing
|
|
@@ -384,18 +404,58 @@ export const streamingExecutor: StateCreator<
|
|
|
384
404
|
operationId,
|
|
385
405
|
);
|
|
386
406
|
|
|
407
|
+
// Check if there are any image parts
|
|
408
|
+
const hasContentImages = contentParts.some((part) => part.type === 'image');
|
|
409
|
+
const hasReasoningImages = reasoningParts.some((part) => part.type === 'image');
|
|
410
|
+
|
|
411
|
+
// Determine final content
|
|
412
|
+
// If has images, serialize contentParts; otherwise use accumulated output text
|
|
413
|
+
const finalContent = hasContentImages ? serializePartsForStorage(contentParts) : output;
|
|
414
|
+
|
|
415
|
+
const finalDuration =
|
|
416
|
+
thinkingDuration && !isNaN(thinkingDuration) ? thinkingDuration : undefined;
|
|
417
|
+
|
|
418
|
+
// Determine final reasoning content
|
|
419
|
+
// Priority: reasoningParts (multimodal) > thinkingContent (from reasoning_part text) > reasoning (from old reasoning event)
|
|
420
|
+
let finalReasoning: any = undefined;
|
|
421
|
+
if (hasReasoningImages) {
|
|
422
|
+
// Has images, use multimodal format
|
|
423
|
+
finalReasoning = {
|
|
424
|
+
content: serializePartsForStorage(reasoningParts),
|
|
425
|
+
duration: finalDuration,
|
|
426
|
+
isMultimodal: true,
|
|
427
|
+
};
|
|
428
|
+
} else if (thinkingContent) {
|
|
429
|
+
// Has text from reasoning_part but no images
|
|
430
|
+
finalReasoning = {
|
|
431
|
+
content: thinkingContent,
|
|
432
|
+
duration: finalDuration,
|
|
433
|
+
};
|
|
434
|
+
} else if (reasoning?.content) {
|
|
435
|
+
// Fallback to old reasoning event content
|
|
436
|
+
finalReasoning = {
|
|
437
|
+
...reasoning,
|
|
438
|
+
duration: finalDuration,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
387
442
|
// update the content after fetch result
|
|
388
443
|
await optimisticUpdateMessageContent(
|
|
389
444
|
messageId,
|
|
390
|
-
|
|
445
|
+
finalContent,
|
|
391
446
|
{
|
|
392
447
|
tools,
|
|
393
|
-
reasoning:
|
|
394
|
-
? { ...reasoning, duration: duration && !isNaN(duration) ? duration : undefined }
|
|
395
|
-
: undefined,
|
|
448
|
+
reasoning: finalReasoning,
|
|
396
449
|
search: !!grounding?.citations ? grounding : undefined,
|
|
397
450
|
imageList: finalImages.length > 0 ? finalImages : undefined,
|
|
398
|
-
metadata: {
|
|
451
|
+
metadata: {
|
|
452
|
+
...usage,
|
|
453
|
+
...speed,
|
|
454
|
+
performance: speed,
|
|
455
|
+
usage,
|
|
456
|
+
finishType: type,
|
|
457
|
+
...(hasContentImages && { isMultimodal: true }),
|
|
458
|
+
},
|
|
399
459
|
},
|
|
400
460
|
{ operationId },
|
|
401
461
|
);
|
|
@@ -457,8 +517,8 @@ export const streamingExecutor: StateCreator<
|
|
|
457
517
|
output += chunk.text;
|
|
458
518
|
|
|
459
519
|
// if there is no duration, it means the end of reasoning
|
|
460
|
-
if (!
|
|
461
|
-
|
|
520
|
+
if (!thinkingDuration) {
|
|
521
|
+
thinkingDuration = Date.now() - thinkingStartAt;
|
|
462
522
|
|
|
463
523
|
// Complete reasoning operation if it exists
|
|
464
524
|
if (reasoningOperationId) {
|
|
@@ -480,7 +540,9 @@ export const streamingExecutor: StateCreator<
|
|
|
480
540
|
type: 'updateMessage',
|
|
481
541
|
value: {
|
|
482
542
|
content: output,
|
|
483
|
-
reasoning: !!
|
|
543
|
+
reasoning: !!thinkingContent
|
|
544
|
+
? { content: thinkingContent, duration: thinkingDuration }
|
|
545
|
+
: undefined,
|
|
484
546
|
},
|
|
485
547
|
},
|
|
486
548
|
{ operationId },
|
|
@@ -505,13 +567,178 @@ export const streamingExecutor: StateCreator<
|
|
|
505
567
|
get().associateMessageWithOperation(messageId, reasoningOperationId);
|
|
506
568
|
}
|
|
507
569
|
|
|
508
|
-
|
|
570
|
+
thinkingContent += chunk.text;
|
|
509
571
|
|
|
510
572
|
internal_dispatchMessage(
|
|
511
573
|
{
|
|
512
574
|
id: messageId,
|
|
513
575
|
type: 'updateMessage',
|
|
514
|
-
value: { reasoning: { content:
|
|
576
|
+
value: { reasoning: { content: thinkingContent } },
|
|
577
|
+
},
|
|
578
|
+
{ operationId },
|
|
579
|
+
);
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
case 'reasoning_part': {
|
|
584
|
+
// Start reasoning if not started
|
|
585
|
+
if (!thinkingStartAt) {
|
|
586
|
+
thinkingStartAt = Date.now();
|
|
587
|
+
|
|
588
|
+
const { operationId: reasoningOpId } = get().startOperation({
|
|
589
|
+
type: 'reasoning',
|
|
590
|
+
context: { sessionId, topicId, messageId },
|
|
591
|
+
parentOperationId: operationId,
|
|
592
|
+
});
|
|
593
|
+
reasoningOperationId = reasoningOpId;
|
|
594
|
+
get().associateMessageWithOperation(messageId, reasoningOperationId);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const { partType, content: partContent, mimeType } = chunk;
|
|
598
|
+
|
|
599
|
+
if (partType === 'text') {
|
|
600
|
+
const lastPart = reasoningParts.at(-1);
|
|
601
|
+
|
|
602
|
+
// If last part is also text, merge chunks together
|
|
603
|
+
if (lastPart?.type === 'text') {
|
|
604
|
+
reasoningParts = [
|
|
605
|
+
...reasoningParts.slice(0, -1),
|
|
606
|
+
{ type: 'text', text: lastPart.text + partContent },
|
|
607
|
+
];
|
|
608
|
+
} else {
|
|
609
|
+
// Create new text part (first chunk, may contain thoughtSignature)
|
|
610
|
+
reasoningParts = [...reasoningParts, { type: 'text', text: partContent }];
|
|
611
|
+
}
|
|
612
|
+
thinkingContent += partContent;
|
|
613
|
+
} else if (partType === 'image') {
|
|
614
|
+
// Image part - create new array to avoid mutation
|
|
615
|
+
const tempImage = `data:${mimeType};base64,${partContent}`;
|
|
616
|
+
const partIndex = reasoningParts.length;
|
|
617
|
+
const newPart: MessageContentPart = { type: 'image', image: tempImage };
|
|
618
|
+
reasoningParts = [...reasoningParts, newPart];
|
|
619
|
+
|
|
620
|
+
// Start upload task and update array when done
|
|
621
|
+
const uploadTask = getFileStoreState()
|
|
622
|
+
.uploadBase64FileWithProgress(tempImage)
|
|
623
|
+
.then((file) => {
|
|
624
|
+
const url = file?.url || tempImage;
|
|
625
|
+
// Replace the part at index by creating a new array
|
|
626
|
+
const updatedParts = [...reasoningParts];
|
|
627
|
+
updatedParts[partIndex] = { type: 'image', image: url };
|
|
628
|
+
reasoningParts = updatedParts;
|
|
629
|
+
return url;
|
|
630
|
+
})
|
|
631
|
+
.catch((error) => {
|
|
632
|
+
console.error('[reasoning_part] Image upload failed:', error);
|
|
633
|
+
return tempImage;
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
reasoningImageUploads.set(partIndex, uploadTask);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Real-time update with display format
|
|
640
|
+
// Check if there are any image parts to determine if it's multimodal
|
|
641
|
+
const hasReasoningImages = reasoningParts.some((part) => part.type === 'image');
|
|
642
|
+
|
|
643
|
+
internal_dispatchMessage(
|
|
644
|
+
{
|
|
645
|
+
id: messageId,
|
|
646
|
+
type: 'updateMessage',
|
|
647
|
+
value: {
|
|
648
|
+
reasoning: hasReasoningImages
|
|
649
|
+
? { tempDisplayContent: reasoningParts, isMultimodal: true }
|
|
650
|
+
: { content: thinkingContent },
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
{ operationId },
|
|
654
|
+
);
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
case 'content_part': {
|
|
659
|
+
const { partType, content: partContent, mimeType } = chunk;
|
|
660
|
+
|
|
661
|
+
// End reasoning when content starts
|
|
662
|
+
if (!thinkingDuration && reasoningOperationId) {
|
|
663
|
+
thinkingDuration = Date.now() - thinkingStartAt;
|
|
664
|
+
get().completeOperation(reasoningOperationId);
|
|
665
|
+
reasoningOperationId = undefined;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (partType === 'text') {
|
|
669
|
+
const lastPart = contentParts.at(-1);
|
|
670
|
+
|
|
671
|
+
// If last part is also text, merge chunks together
|
|
672
|
+
if (lastPart?.type === 'text') {
|
|
673
|
+
contentParts = [
|
|
674
|
+
...contentParts.slice(0, -1),
|
|
675
|
+
{ type: 'text', text: lastPart.text + partContent },
|
|
676
|
+
];
|
|
677
|
+
} else {
|
|
678
|
+
// Create new text part (first chunk, may contain thoughtSignature)
|
|
679
|
+
contentParts = [...contentParts, { type: 'text', text: partContent }];
|
|
680
|
+
}
|
|
681
|
+
output += partContent;
|
|
682
|
+
} else if (partType === 'image') {
|
|
683
|
+
// Image part - create new array to avoid mutation
|
|
684
|
+
const tempImage = `data:${mimeType};base64,${partContent}`;
|
|
685
|
+
const partIndex = contentParts.length;
|
|
686
|
+
const newPart: MessageContentPart = {
|
|
687
|
+
type: 'image',
|
|
688
|
+
image: tempImage,
|
|
689
|
+
};
|
|
690
|
+
contentParts = [...contentParts, newPart];
|
|
691
|
+
|
|
692
|
+
// Start upload task and update array when done
|
|
693
|
+
const uploadTask = getFileStoreState()
|
|
694
|
+
.uploadBase64FileWithProgress(tempImage)
|
|
695
|
+
.then((file) => {
|
|
696
|
+
const url = file?.url || tempImage;
|
|
697
|
+
// Replace the part at index by creating a new array
|
|
698
|
+
const updatedParts = [...contentParts];
|
|
699
|
+
updatedParts[partIndex] = {
|
|
700
|
+
type: 'image',
|
|
701
|
+
image: url,
|
|
702
|
+
};
|
|
703
|
+
contentParts = updatedParts;
|
|
704
|
+
return url;
|
|
705
|
+
})
|
|
706
|
+
.catch((error) => {
|
|
707
|
+
console.error('[content_part] Image upload failed:', error);
|
|
708
|
+
return tempImage;
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
contentImageUploads.set(partIndex, uploadTask);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Real-time update with display format
|
|
715
|
+
// Check if there are any image parts to determine if it's multimodal
|
|
716
|
+
const hasContentImages = contentParts.some((part) => part.type === 'image');
|
|
717
|
+
|
|
718
|
+
const hasReasoningImages = reasoningParts.some((part) => part.type === 'image');
|
|
719
|
+
|
|
720
|
+
internal_dispatchMessage(
|
|
721
|
+
{
|
|
722
|
+
id: messageId,
|
|
723
|
+
type: 'updateMessage',
|
|
724
|
+
value: {
|
|
725
|
+
content: output,
|
|
726
|
+
reasoning: hasReasoningImages
|
|
727
|
+
? {
|
|
728
|
+
tempDisplayContent: reasoningParts,
|
|
729
|
+
isMultimodal: true,
|
|
730
|
+
duration: thinkingDuration,
|
|
731
|
+
}
|
|
732
|
+
: !!thinkingContent
|
|
733
|
+
? { content: thinkingContent, duration: thinkingDuration }
|
|
734
|
+
: undefined,
|
|
735
|
+
...(hasContentImages && {
|
|
736
|
+
metadata: {
|
|
737
|
+
isMultimodal: true,
|
|
738
|
+
tempDisplayContent: serializePartsForStorage(contentParts),
|
|
739
|
+
},
|
|
740
|
+
}),
|
|
741
|
+
},
|
|
515
742
|
},
|
|
516
743
|
{ operationId },
|
|
517
744
|
);
|
|
@@ -525,8 +752,8 @@ export const streamingExecutor: StateCreator<
|
|
|
525
752
|
isFunctionCall = true;
|
|
526
753
|
|
|
527
754
|
// Complete reasoning operation if it exists
|
|
528
|
-
if (!
|
|
529
|
-
|
|
755
|
+
if (!thinkingDuration && reasoningOperationId) {
|
|
756
|
+
thinkingDuration = Date.now() - thinkingStartAt;
|
|
530
757
|
get().completeOperation(reasoningOperationId);
|
|
531
758
|
reasoningOperationId = undefined;
|
|
532
759
|
}
|
|
@@ -535,8 +762,8 @@ export const streamingExecutor: StateCreator<
|
|
|
535
762
|
|
|
536
763
|
case 'stop': {
|
|
537
764
|
// Complete reasoning operation when receiving stop signal
|
|
538
|
-
if (!
|
|
539
|
-
|
|
765
|
+
if (!thinkingDuration && reasoningOperationId) {
|
|
766
|
+
thinkingDuration = Date.now() - thinkingStartAt;
|
|
540
767
|
get().completeOperation(reasoningOperationId);
|
|
541
768
|
reasoningOperationId = undefined;
|
|
542
769
|
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { LOADING_FLAT } from '@lobechat/const';
|
|
2
|
-
import { UIChatMessage } from '@lobechat/types';
|
|
3
|
-
import { ReactNode, memo } from 'react';
|
|
4
|
-
import { Flexbox } from 'react-layout-kit';
|
|
5
|
-
|
|
6
|
-
import { useChatStore } from '@/store/chat';
|
|
7
|
-
import { aiChatSelectors, messageStateSelectors } from '@/store/chat/selectors';
|
|
8
|
-
|
|
9
|
-
import { DefaultMessage } from '../Default';
|
|
10
|
-
import ImageFileListViewer from '../User/ImageFileListViewer';
|
|
11
|
-
import { CollapsedMessage } from './CollapsedMessage';
|
|
12
|
-
import FileChunks from './FileChunks';
|
|
13
|
-
import IntentUnderstanding from './IntentUnderstanding';
|
|
14
|
-
import Reasoning from './Reasoning';
|
|
15
|
-
import SearchGrounding from './SearchGrounding';
|
|
16
|
-
|
|
17
|
-
export const AssistantMessageContent = memo<
|
|
18
|
-
UIChatMessage & {
|
|
19
|
-
editableContent: ReactNode;
|
|
20
|
-
}
|
|
21
|
-
>(({ id, tools, content, chunksList, search, imageList, ...props }) => {
|
|
22
|
-
const [editing, generating, isCollapsed] = useChatStore((s) => [
|
|
23
|
-
messageStateSelectors.isMessageEditing(id)(s),
|
|
24
|
-
messageStateSelectors.isMessageGenerating(id)(s),
|
|
25
|
-
messageStateSelectors.isMessageCollapsed(id)(s),
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
|
|
29
|
-
|
|
30
|
-
const isReasoning = useChatStore(aiChatSelectors.isMessageInReasoning(id));
|
|
31
|
-
|
|
32
|
-
const isIntentUnderstanding = useChatStore(aiChatSelectors.isIntentUnderstanding(id));
|
|
33
|
-
|
|
34
|
-
const showSearch = !!search && !!search.citations?.length;
|
|
35
|
-
const showImageItems = !!imageList && imageList.length > 0;
|
|
36
|
-
|
|
37
|
-
// remove \n to avoid empty content
|
|
38
|
-
// refs: https://github.com/lobehub/lobe-chat/pull/6153
|
|
39
|
-
const showReasoning =
|
|
40
|
-
(!!props.reasoning && props.reasoning.content?.trim() !== '') ||
|
|
41
|
-
(!props.reasoning && isReasoning);
|
|
42
|
-
|
|
43
|
-
const showFileChunks = !!chunksList && chunksList.length > 0;
|
|
44
|
-
|
|
45
|
-
if (editing)
|
|
46
|
-
return (
|
|
47
|
-
<DefaultMessage
|
|
48
|
-
content={content}
|
|
49
|
-
id={id}
|
|
50
|
-
isToolCallGenerating={isToolCallGenerating}
|
|
51
|
-
{...props}
|
|
52
|
-
/>
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
if (isCollapsed) return <CollapsedMessage content={content} id={id} />;
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<Flexbox gap={8} id={id}>
|
|
59
|
-
{showSearch && (
|
|
60
|
-
<SearchGrounding citations={search?.citations} searchQueries={search?.searchQueries} />
|
|
61
|
-
)}
|
|
62
|
-
{showFileChunks && <FileChunks data={chunksList} />}
|
|
63
|
-
{showReasoning && <Reasoning {...props.reasoning} id={id} />}
|
|
64
|
-
{isIntentUnderstanding ? (
|
|
65
|
-
<IntentUnderstanding />
|
|
66
|
-
) : (
|
|
67
|
-
<DefaultMessage
|
|
68
|
-
addIdOnDOM={false}
|
|
69
|
-
content={content}
|
|
70
|
-
id={id}
|
|
71
|
-
isToolCallGenerating={isToolCallGenerating}
|
|
72
|
-
{...props}
|
|
73
|
-
/>
|
|
74
|
-
)}
|
|
75
|
-
{showImageItems && <ImageFileListViewer items={imageList} />}
|
|
76
|
-
</Flexbox>
|
|
77
|
-
);
|
|
78
|
-
});
|