@lobehub/lobehub 2.0.0-next.114 → 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 +50 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/const/src/models.ts +6 -0
- 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-bank/src/aiModels/aihubmix.ts +35 -1
- package/packages/model-bank/src/aiModels/anthropic.ts +37 -2
- package/packages/model-bank/src/aiModels/bedrock.ts +26 -11
- package/packages/model-bank/src/aiModels/openrouter.ts +28 -1
- package/packages/model-bank/src/aiModels/zenmux.ts +30 -1
- 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/parameterResolver.ts +3 -0
- 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
|
@@ -77,6 +77,10 @@ export interface StreamProtocolChunk {
|
|
|
77
77
|
| 'reasoning_signature'
|
|
78
78
|
// flagged reasoning signature
|
|
79
79
|
| 'flagged_reasoning_signature'
|
|
80
|
+
// multimodal content part in reasoning
|
|
81
|
+
| 'reasoning_part'
|
|
82
|
+
// multimodal content part in content
|
|
83
|
+
| 'content_part'
|
|
80
84
|
// Search or Grounding
|
|
81
85
|
| 'grounding'
|
|
82
86
|
// stop signal
|
|
@@ -91,6 +95,21 @@ export interface StreamProtocolChunk {
|
|
|
91
95
|
| 'data';
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Stream content part chunk data for multimodal support
|
|
100
|
+
*/
|
|
101
|
+
export interface StreamPartChunkData {
|
|
102
|
+
content: string;
|
|
103
|
+
// whether this part is in reasoning or regular content
|
|
104
|
+
inReasoning: boolean;
|
|
105
|
+
// image MIME type
|
|
106
|
+
mimeType?: string;
|
|
107
|
+
// text content or base64 image data
|
|
108
|
+
partType: 'text' | 'image';
|
|
109
|
+
// Optional signature for reasoning verification (Google Gemini feature)
|
|
110
|
+
thoughtSignature?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
94
113
|
export interface StreamToolCallChunkData {
|
|
95
114
|
function?: {
|
|
96
115
|
arguments?: string;
|
|
@@ -26,14 +26,40 @@ export interface ChatCitationItem {
|
|
|
26
26
|
url: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Message content part types for multimodal content support
|
|
31
|
+
*/
|
|
32
|
+
export interface MessageContentPartText {
|
|
33
|
+
text: string;
|
|
34
|
+
thoughtSignature?: string;
|
|
35
|
+
type: 'text';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MessageContentPartImage {
|
|
39
|
+
image: string;
|
|
40
|
+
thoughtSignature?: string;
|
|
41
|
+
type: 'image';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type MessageContentPart = MessageContentPartText | MessageContentPartImage;
|
|
45
|
+
|
|
29
46
|
export interface ModelReasoning {
|
|
47
|
+
/**
|
|
48
|
+
* Reasoning content, can be plain string or serialized JSON array of MessageContentPart[]
|
|
49
|
+
*/
|
|
30
50
|
content?: string;
|
|
31
51
|
duration?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Flag indicating if content is multimodal (serialized MessageContentPart[])
|
|
54
|
+
*/
|
|
55
|
+
isMultimodal?: boolean;
|
|
32
56
|
signature?: string;
|
|
57
|
+
tempDisplayContent?: MessageContentPart[];
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
export const ModelReasoningSchema = z.object({
|
|
36
61
|
content: z.string().optional(),
|
|
37
62
|
duration: z.number().optional(),
|
|
63
|
+
isMultimodal: z.boolean().optional(),
|
|
38
64
|
signature: z.string().optional(),
|
|
39
65
|
});
|
|
@@ -78,6 +78,7 @@ export const ModelPerformanceSchema = z.object({
|
|
|
78
78
|
export const MessageMetadataSchema = ModelUsageSchema.merge(ModelPerformanceSchema).extend({
|
|
79
79
|
collapsed: z.boolean().optional(),
|
|
80
80
|
inspectExpanded: z.boolean().optional(),
|
|
81
|
+
isMultimodal: z.boolean().optional(),
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
export interface ModelUsage extends ModelTokensUsage {
|
|
@@ -123,4 +124,10 @@ export interface MessageMetadata extends ModelUsage, ModelPerformance {
|
|
|
123
124
|
compare?: boolean;
|
|
124
125
|
usage?: ModelUsage;
|
|
125
126
|
performance?: ModelPerformance;
|
|
127
|
+
/**
|
|
128
|
+
* Flag indicating if message content is multimodal (serialized MessageContentPart[])
|
|
129
|
+
*/
|
|
130
|
+
isMultimodal?: boolean;
|
|
131
|
+
// message content is multimodal, display content in the streaming, won't save to db
|
|
132
|
+
tempDisplayContent?: string;
|
|
126
133
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MessageContentPart } from '@lobechat/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serialize message content parts to JSON string for storage
|
|
5
|
+
*/
|
|
6
|
+
export function serializePartsForStorage(parts: MessageContentPart[]): string {
|
|
7
|
+
return JSON.stringify(parts);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Deserialize content string to message content parts
|
|
12
|
+
* Returns null if content is not valid JSON array of parts
|
|
13
|
+
*/
|
|
14
|
+
export function deserializeParts(content: string): MessageContentPart[] | null {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(content);
|
|
17
|
+
// Validate it's an array with valid part structure
|
|
18
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed[0]?.type) {
|
|
19
|
+
return parsed as MessageContentPart[];
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Not JSON, treat as plain text
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
@@ -4,7 +4,7 @@ import { createStyles } from 'antd-style';
|
|
|
4
4
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
5
5
|
import { AtomIcon } from 'lucide-react';
|
|
6
6
|
import { rgba } from 'polished';
|
|
7
|
-
import { CSSProperties, RefObject, memo, useEffect, useRef, useState } from 'react';
|
|
7
|
+
import { CSSProperties, ReactNode, RefObject, memo, useEffect, useRef, useState } from 'react';
|
|
8
8
|
import { useTranslation } from 'react-i18next';
|
|
9
9
|
import { Flexbox } from 'react-layout-kit';
|
|
10
10
|
|
|
@@ -76,7 +76,7 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
|
76
76
|
|
|
77
77
|
interface ThinkingProps {
|
|
78
78
|
citations?: ChatCitationItem[];
|
|
79
|
-
content?: string;
|
|
79
|
+
content?: string | ReactNode;
|
|
80
80
|
duration?: number;
|
|
81
81
|
style?: CSSProperties;
|
|
82
82
|
thinking?: boolean;
|
|
@@ -158,7 +158,7 @@ const Thinking = memo<ThinkingProps>((props) => {
|
|
|
158
158
|
</Flexbox>
|
|
159
159
|
)}
|
|
160
160
|
<Flexbox gap={4} horizontal>
|
|
161
|
-
{showDetail && content && (
|
|
161
|
+
{showDetail && content && typeof content === 'string' && (
|
|
162
162
|
<div
|
|
163
163
|
onClick={(event) => {
|
|
164
164
|
event.stopPropagation();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { deserializeParts } from '@lobechat/utils';
|
|
2
|
+
import { Markdown, MarkdownProps } from '@lobehub/ui';
|
|
3
|
+
import { memo } from 'react';
|
|
4
|
+
|
|
5
|
+
import BubblesLoading from '@/components/BubblesLoading';
|
|
6
|
+
import { LOADING_FLAT } from '@/const/message';
|
|
7
|
+
import { RichContentRenderer } from '@/features/ChatList/components/RichContentRenderer';
|
|
8
|
+
import { normalizeThinkTags, processWithArtifact } from '@/features/ChatList/utils/markdown';
|
|
9
|
+
|
|
10
|
+
const MessageContent = memo<{
|
|
11
|
+
addIdOnDOM?: boolean;
|
|
12
|
+
content: string;
|
|
13
|
+
hasImages?: boolean;
|
|
14
|
+
isMultimodal?: boolean;
|
|
15
|
+
isToolCallGenerating?: boolean;
|
|
16
|
+
markdownProps?: Omit<MarkdownProps, 'className' | 'style' | 'children'>;
|
|
17
|
+
tempDisplayContent?: string;
|
|
18
|
+
}>(
|
|
19
|
+
({
|
|
20
|
+
markdownProps,
|
|
21
|
+
content,
|
|
22
|
+
isToolCallGenerating,
|
|
23
|
+
hasImages,
|
|
24
|
+
isMultimodal,
|
|
25
|
+
tempDisplayContent,
|
|
26
|
+
}) => {
|
|
27
|
+
const message = normalizeThinkTags(processWithArtifact(content));
|
|
28
|
+
if (isToolCallGenerating) return;
|
|
29
|
+
|
|
30
|
+
if ((!content && !hasImages) || content === LOADING_FLAT) return <BubblesLoading />;
|
|
31
|
+
|
|
32
|
+
const contentParts = isMultimodal ? deserializeParts(tempDisplayContent || content) : null;
|
|
33
|
+
|
|
34
|
+
return contentParts ? (
|
|
35
|
+
<RichContentRenderer parts={contentParts} />
|
|
36
|
+
) : (
|
|
37
|
+
<Markdown {...markdownProps} variant={'chat'}>
|
|
38
|
+
{message}
|
|
39
|
+
</Markdown>
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export default MessageContent;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { LOADING_FLAT } from '@lobechat/const';
|
|
2
|
+
import { UIChatMessage } from '@lobechat/types';
|
|
3
|
+
import { MarkdownProps } from '@lobehub/ui';
|
|
4
|
+
import { ReactNode, memo } from 'react';
|
|
5
|
+
import { Flexbox } from 'react-layout-kit';
|
|
6
|
+
|
|
7
|
+
import { useChatStore } from '@/store/chat';
|
|
8
|
+
import { aiChatSelectors, messageStateSelectors } from '@/store/chat/selectors';
|
|
9
|
+
|
|
10
|
+
import { DefaultMessage } from '../Default';
|
|
11
|
+
import ImageFileListViewer from '../User/ImageFileListViewer';
|
|
12
|
+
import { CollapsedMessage } from './CollapsedMessage';
|
|
13
|
+
import MessageContent from './DisplayContent';
|
|
14
|
+
import FileChunks from './FileChunks';
|
|
15
|
+
import IntentUnderstanding from './IntentUnderstanding';
|
|
16
|
+
import Reasoning from './Reasoning';
|
|
17
|
+
import SearchGrounding from './SearchGrounding';
|
|
18
|
+
|
|
19
|
+
export const AssistantMessageBody = memo<
|
|
20
|
+
UIChatMessage & {
|
|
21
|
+
editableContent: ReactNode;
|
|
22
|
+
markdownProps?: Omit<MarkdownProps, 'className' | 'style' | 'children'>;
|
|
23
|
+
}
|
|
24
|
+
>(
|
|
25
|
+
({
|
|
26
|
+
id,
|
|
27
|
+
tools,
|
|
28
|
+
content,
|
|
29
|
+
chunksList,
|
|
30
|
+
search,
|
|
31
|
+
imageList,
|
|
32
|
+
metadata,
|
|
33
|
+
editableContent,
|
|
34
|
+
markdownProps,
|
|
35
|
+
...props
|
|
36
|
+
}) => {
|
|
37
|
+
const [editing, generating, isCollapsed] = useChatStore((s) => [
|
|
38
|
+
messageStateSelectors.isMessageEditing(id)(s),
|
|
39
|
+
messageStateSelectors.isMessageGenerating(id)(s),
|
|
40
|
+
messageStateSelectors.isMessageCollapsed(id)(s),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
|
|
44
|
+
|
|
45
|
+
const isReasoning = useChatStore(aiChatSelectors.isMessageInReasoning(id));
|
|
46
|
+
|
|
47
|
+
const isIntentUnderstanding = useChatStore(aiChatSelectors.isIntentUnderstanding(id));
|
|
48
|
+
|
|
49
|
+
const showSearch = !!search && !!search.citations?.length;
|
|
50
|
+
const showImageItems = !!imageList && imageList.length > 0;
|
|
51
|
+
|
|
52
|
+
// remove \n to avoid empty content
|
|
53
|
+
// refs: https://github.com/lobehub/lobe-chat/pull/6153
|
|
54
|
+
const showReasoning =
|
|
55
|
+
(!!props.reasoning && props.reasoning.content?.trim() !== '') ||
|
|
56
|
+
(!props.reasoning && isReasoning);
|
|
57
|
+
|
|
58
|
+
const showFileChunks = !!chunksList && chunksList.length > 0;
|
|
59
|
+
|
|
60
|
+
if (editing)
|
|
61
|
+
return (
|
|
62
|
+
<DefaultMessage
|
|
63
|
+
content={content}
|
|
64
|
+
editableContent={editableContent}
|
|
65
|
+
id={id}
|
|
66
|
+
isToolCallGenerating={isToolCallGenerating}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (isCollapsed) return <CollapsedMessage content={content} id={id} />;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Flexbox gap={8} id={id}>
|
|
75
|
+
{showSearch && (
|
|
76
|
+
<SearchGrounding citations={search?.citations} searchQueries={search?.searchQueries} />
|
|
77
|
+
)}
|
|
78
|
+
{showFileChunks && <FileChunks data={chunksList} />}
|
|
79
|
+
{showReasoning && <Reasoning {...props.reasoning} id={id} />}
|
|
80
|
+
{isIntentUnderstanding ? (
|
|
81
|
+
<IntentUnderstanding />
|
|
82
|
+
) : (
|
|
83
|
+
<MessageContent
|
|
84
|
+
content={content}
|
|
85
|
+
hasImages={showImageItems}
|
|
86
|
+
isMultimodal={metadata?.isMultimodal}
|
|
87
|
+
isToolCallGenerating={isToolCallGenerating}
|
|
88
|
+
markdownProps={markdownProps}
|
|
89
|
+
tempDisplayContent={metadata?.tempDisplayContent}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
{showImageItems && <ImageFileListViewer items={imageList} />}
|
|
93
|
+
</Flexbox>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { MessageContentPart } from '@lobechat/types';
|
|
2
|
+
import { deserializeParts } from '@lobechat/utils';
|
|
1
3
|
import { memo } from 'react';
|
|
2
4
|
|
|
3
5
|
import Thinking from '@/components/Thinking';
|
|
@@ -6,24 +8,35 @@ import { aiChatSelectors } from '@/store/chat/selectors';
|
|
|
6
8
|
import { useUserStore } from '@/store/user';
|
|
7
9
|
import { userGeneralSettingsSelectors } from '@/store/user/selectors';
|
|
8
10
|
|
|
11
|
+
import { RichContentRenderer } from '../../../components/RichContentRenderer';
|
|
12
|
+
|
|
9
13
|
interface ReasoningProps {
|
|
10
14
|
content?: string;
|
|
11
15
|
duration?: number;
|
|
12
16
|
id: string;
|
|
17
|
+
isMultimodal?: boolean;
|
|
18
|
+
tempDisplayContent?: MessageContentPart[];
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
const Reasoning = memo<ReasoningProps>(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
21
|
+
const Reasoning = memo<ReasoningProps>(
|
|
22
|
+
({ content = '', duration, id, isMultimodal, tempDisplayContent }) => {
|
|
23
|
+
const isReasoning = useChatStore(aiChatSelectors.isMessageInReasoning(id));
|
|
24
|
+
const transitionMode = useUserStore(userGeneralSettingsSelectors.transitionMode);
|
|
25
|
+
|
|
26
|
+
const parts = tempDisplayContent || deserializeParts(content);
|
|
27
|
+
|
|
28
|
+
// If parts are provided, render multimodal content
|
|
29
|
+
const thinkingContent = isMultimodal && parts ? <RichContentRenderer parts={parts} /> : content;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Thinking
|
|
33
|
+
content={thinkingContent}
|
|
34
|
+
duration={duration}
|
|
35
|
+
thinking={isReasoning}
|
|
36
|
+
thinkingAnimated={transitionMode === 'fadeIn' && isReasoning}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
28
41
|
|
|
29
42
|
export default Reasoning;
|
|
@@ -33,7 +33,7 @@ import { useDoubleClickEdit } from '../../hooks/useDoubleClickEdit';
|
|
|
33
33
|
import { normalizeThinkTags, processWithArtifact } from '../../utils/markdown';
|
|
34
34
|
import { AssistantActionsBar } from './Actions';
|
|
35
35
|
import { AssistantMessageExtra } from './Extra';
|
|
36
|
-
import {
|
|
36
|
+
import { AssistantMessageBody } from './MessageBody';
|
|
37
37
|
|
|
38
38
|
const rehypePlugins = markdownElements.map((element) => element.rehypePlugin).filter(Boolean);
|
|
39
39
|
const remarkPlugins = markdownElements.map((element) => element.remarkPlugin).filter(Boolean);
|
|
@@ -75,7 +75,7 @@ export const useStyles = createStyles(
|
|
|
75
75
|
justify-content: ${placement === 'left' ? 'flex-end' : 'flex-start'};
|
|
76
76
|
`,
|
|
77
77
|
editing &&
|
|
78
|
-
|
|
78
|
+
css`
|
|
79
79
|
pointer-events: none !important;
|
|
80
80
|
opacity: 0 !important;
|
|
81
81
|
`,
|
|
@@ -84,11 +84,9 @@ export const useStyles = createStyles(
|
|
|
84
84
|
variant === 'docs' && rawContainerStylish,
|
|
85
85
|
css`
|
|
86
86
|
position: relative;
|
|
87
|
-
|
|
88
87
|
width: 100%;
|
|
89
88
|
max-width: 100vw;
|
|
90
89
|
padding-block: 24px 12px;
|
|
91
|
-
padding-inline: 12px;
|
|
92
90
|
|
|
93
91
|
@supports (content-visibility: auto) {
|
|
94
92
|
contain-intrinsic-size: auto 100lvh;
|
|
@@ -305,9 +303,13 @@ const AssistantMessage = memo<AssistantMessageProps>(
|
|
|
305
303
|
|
|
306
304
|
const renderMessage = useCallback(
|
|
307
305
|
(editableContent: ReactNode) => (
|
|
308
|
-
<
|
|
306
|
+
<AssistantMessageBody
|
|
307
|
+
{...item}
|
|
308
|
+
editableContent={editableContent}
|
|
309
|
+
markdownProps={markdownProps}
|
|
310
|
+
/>
|
|
309
311
|
),
|
|
310
|
-
[item],
|
|
312
|
+
[item, markdownProps],
|
|
311
313
|
);
|
|
312
314
|
const errorMessage = <ErrorMessageExtra data={item} />;
|
|
313
315
|
|
|
@@ -6,25 +6,22 @@ import { LOADING_FLAT } from '@/const/message';
|
|
|
6
6
|
import { useChatStore } from '@/store/chat';
|
|
7
7
|
import { messageStateSelectors } from '@/store/chat/selectors';
|
|
8
8
|
|
|
9
|
-
export const MessageContentClassName = 'msg_content_flag'
|
|
9
|
+
export const MessageContentClassName = 'msg_content_flag';
|
|
10
10
|
|
|
11
11
|
export const DefaultMessage = memo<
|
|
12
12
|
UIChatMessage & {
|
|
13
13
|
addIdOnDOM?: boolean;
|
|
14
14
|
editableContent: ReactNode;
|
|
15
|
+
hasImages?: boolean;
|
|
15
16
|
isToolCallGenerating?: boolean;
|
|
16
17
|
}
|
|
17
|
-
>(({ id, editableContent, content, isToolCallGenerating, addIdOnDOM = true }) => {
|
|
18
|
+
>(({ id, editableContent, content, isToolCallGenerating, addIdOnDOM = true, hasImages }) => {
|
|
18
19
|
const editing = useChatStore(messageStateSelectors.isMessageEditing(id));
|
|
19
20
|
|
|
20
21
|
if (isToolCallGenerating) return;
|
|
21
22
|
|
|
22
|
-
if (!content) return <BubblesLoading />;
|
|
23
|
+
if (!content && !hasImages) return <BubblesLoading />;
|
|
23
24
|
if (content === LOADING_FLAT && !editing) return <BubblesLoading />;
|
|
24
25
|
|
|
25
26
|
return <div id={addIdOnDOM ? id : undefined}>{editableContent}</div>;
|
|
26
27
|
});
|
|
27
|
-
|
|
28
|
-
export const DefaultBelowMessage = memo<UIChatMessage>(() => {
|
|
29
|
-
return null;
|
|
30
|
-
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Image, Markdown } from '@lobehub/ui';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { Flexbox } from 'react-layout-kit';
|
|
4
|
+
|
|
5
|
+
import { MessageContentPart } from '@/types/index';
|
|
6
|
+
|
|
7
|
+
interface RichContentRendererProps {
|
|
8
|
+
parts: MessageContentPart[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const RichContentRenderer = memo<RichContentRendererProps>(({ parts }) => {
|
|
12
|
+
return (
|
|
13
|
+
<Flexbox gap={8}>
|
|
14
|
+
{parts.map((part, index) => {
|
|
15
|
+
if (part.type === 'text') {
|
|
16
|
+
return (
|
|
17
|
+
<Markdown key={index} variant="chat">
|
|
18
|
+
{part.text}
|
|
19
|
+
</Markdown>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (part.type === 'image') {
|
|
24
|
+
return (
|
|
25
|
+
<Image key={index} src={part.image} style={{ borderRadius: 8, maxWidth: '100%' }} />
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return null;
|
|
30
|
+
})}
|
|
31
|
+
</Flexbox>
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
RichContentRenderer.displayName = 'RichContentRenderer';
|