@lobehub/chat 1.135.3 → 1.135.4

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 (116) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/changelog/v1.json +9 -0
  3. package/package.json +2 -2
  4. package/packages/const/src/index.ts +1 -0
  5. package/packages/const/src/settings/index.ts +1 -0
  6. package/packages/model-bank/src/aiModels/aihubmix.ts +25 -0
  7. package/packages/model-bank/src/aiModels/nvidia.ts +17 -1
  8. package/packages/model-bank/src/aiModels/openai.ts +80 -1
  9. package/packages/model-runtime/src/const/models.ts +2 -0
  10. package/packages/model-runtime/src/providers/openai/index.ts +15 -11
  11. package/packages/model-runtime/src/providers/openrouter/index.ts +7 -2
  12. package/packages/model-runtime/src/providers/openrouter/type.ts +4 -2
  13. package/packages/model-runtime/src/providers/vercelaigateway/index.ts +7 -0
  14. package/packages/model-runtime/src/utils/modelParse.ts +31 -3
  15. package/packages/types/src/aiProvider.ts +1 -2
  16. package/packages/types/src/discover/models.ts +3 -2
  17. package/packages/types/src/discover/providers.ts +3 -2
  18. package/packages/types/src/message/chat.ts +13 -0
  19. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx +1 -5
  20. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx +1 -1
  21. package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/Form.tsx +1 -2
  22. package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +1 -4
  23. package/src/app/[variants]/(main)/settings/provider/features/ModelList/SortModelModal/ListItem.tsx +1 -2
  24. package/src/app/[variants]/(main)/settings/provider/features/ModelList/SortModelModal/index.tsx +1 -1
  25. package/src/{components → features}/ChatItem/ChatItem.tsx +5 -35
  26. package/src/{components → features}/ChatItem/components/MessageContent.tsx +27 -7
  27. package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/LobeArtifact/Render/index.tsx +1 -1
  28. package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/remarkPlugins/getNodeContent.test.ts +1 -1
  29. package/src/features/Conversation/{Actions → Messages/Assistant/Actions}/Error.tsx +1 -1
  30. package/src/features/Conversation/{components/ChatItem/ActionsBar.tsx → Messages/Assistant/Actions/index.tsx} +71 -44
  31. package/src/features/Conversation/Messages/Assistant/Block.tsx +63 -0
  32. package/src/features/Conversation/{Extras/Assistant.test.tsx → Messages/Assistant/Extra/index.test.tsx} +36 -31
  33. package/src/features/Conversation/{Extras/Assistant.tsx → Messages/Assistant/Extra/index.tsx} +13 -7
  34. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +102 -0
  35. package/src/features/Conversation/Messages/Assistant/index.tsx +235 -84
  36. package/src/features/Conversation/Messages/User/Actions.tsx +153 -0
  37. package/src/features/Conversation/Messages/User/BelowMessage.tsx +7 -2
  38. package/src/features/Conversation/{Extras/User.tsx → Messages/User/Extra.tsx} +9 -7
  39. package/src/features/Conversation/Messages/User/MessageContent.tsx +31 -0
  40. package/src/features/Conversation/Messages/User/index.tsx +127 -24
  41. package/src/features/Conversation/Messages/index.tsx +152 -0
  42. package/src/features/Conversation/{Extras → components/Extras}/Usage/UsageDetail/ModelCard.tsx +4 -3
  43. package/src/features/Conversation/{Extras → components/Extras}/Usage/UsageDetail/pricing.ts +3 -2
  44. package/src/features/Conversation/{Extras → components/Extras}/Usage/UsageDetail/tokens.test.ts +2 -3
  45. package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/ShareImage/Preview.tsx +1 -1
  46. package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/ShareText/index.tsx +1 -1
  47. package/src/features/Conversation/components/VirtualizedList/index.tsx +1 -0
  48. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +29 -1
  49. package/src/features/Conversation/hooks/useDoubleClickEdit.ts +42 -0
  50. package/src/features/Conversation/index.ts +1 -1
  51. package/src/features/Conversation/types/{index.tsx → index.ts} +0 -7
  52. package/src/features/Portal/Thread/Chat/ChatItem.tsx +0 -7
  53. package/src/hooks/useUserAvatar.test.ts +129 -0
  54. package/src/hooks/useUserAvatar.ts +19 -0
  55. package/src/server/routers/lambda/aiModel.ts +7 -8
  56. package/src/store/user/slices/settings/selectors/settings.ts +6 -5
  57. package/src/features/ChatItem/index.tsx +0 -58
  58. package/src/features/Conversation/Actions/Assistant.tsx +0 -68
  59. package/src/features/Conversation/Actions/Fallback.tsx +0 -19
  60. package/src/features/Conversation/Actions/Tool.tsx +0 -33
  61. package/src/features/Conversation/Actions/User.tsx +0 -39
  62. package/src/features/Conversation/Actions/customAction.ts +0 -37
  63. package/src/features/Conversation/Actions/index.ts +0 -14
  64. package/src/features/Conversation/Extras/index.ts +0 -8
  65. package/src/features/Conversation/Extras/type.ts +0 -5
  66. package/src/features/Conversation/Messages/index.ts +0 -45
  67. package/src/features/Conversation/components/ChatItem/index.tsx +0 -358
  68. /package/src/{components → features}/ChatItem/components/Actions.tsx +0 -0
  69. /package/src/{components → features}/ChatItem/components/Avatar.tsx +0 -0
  70. /package/src/{components → features}/ChatItem/components/BorderSpacing.tsx +0 -0
  71. /package/src/{components → features}/ChatItem/components/ErrorContent.tsx +0 -0
  72. /package/src/{components → features}/ChatItem/components/Loading.tsx +0 -0
  73. /package/src/{components → features}/ChatItem/components/Title.tsx +0 -0
  74. /package/src/{components → features}/ChatItem/index.ts +0 -0
  75. /package/src/{components → features}/ChatItem/style.ts +0 -0
  76. /package/src/{components → features}/ChatItem/type.ts +0 -0
  77. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/LobeArtifact/Render/Icon.tsx +0 -0
  78. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/LobeArtifact/index.ts +0 -0
  79. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/LobeArtifact/rehypePlugin.test.ts +0 -0
  80. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/LobeArtifact/rehypePlugin.ts +0 -0
  81. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/LobeThinking/Render.tsx +0 -0
  82. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/LobeThinking/index.ts +0 -0
  83. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/LocalFile/Render/index.tsx +0 -0
  84. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/LocalFile/index.ts +0 -0
  85. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/Thinking/Render.tsx +0 -0
  86. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/Thinking/index.ts +0 -0
  87. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/index.ts +0 -0
  88. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/remarkPlugins/__snapshots__/createRemarkSelfClosingTagPlugin.test.ts.snap +0 -0
  89. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/remarkPlugins/createRemarkCustomTagPlugin.ts +0 -0
  90. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +0 -0
  91. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +0 -0
  92. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/remarkPlugins/getNodeContent.ts +0 -0
  93. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/type.ts +0 -0
  94. /package/src/features/Conversation/{components/MarkdownElements → MarkdownElements}/utils.ts +0 -0
  95. /package/src/features/Conversation/{Extras → components/Extras}/ExtraContainer.tsx +0 -0
  96. /package/src/features/Conversation/{Extras → components/Extras}/TTS/FilePlayer.tsx +0 -0
  97. /package/src/features/Conversation/{Extras → components/Extras}/TTS/InitPlayer.tsx +0 -0
  98. /package/src/features/Conversation/{Extras → components/Extras}/TTS/Player.tsx +0 -0
  99. /package/src/features/Conversation/{Extras → components/Extras}/TTS/index.tsx +0 -0
  100. /package/src/features/Conversation/{Extras → components/Extras}/Translate.tsx +0 -0
  101. /package/src/features/Conversation/{Extras → components/Extras}/Usage/UsageDetail/TokenProgress.tsx +0 -0
  102. /package/src/features/Conversation/{Extras → components/Extras}/Usage/UsageDetail/index.tsx +0 -0
  103. /package/src/features/Conversation/{Extras → components/Extras}/Usage/UsageDetail/tokens.ts +0 -0
  104. /package/src/features/Conversation/{Extras → components/Extras}/Usage/index.tsx +0 -0
  105. /package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/ShareImage/index.tsx +0 -0
  106. /package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/ShareImage/style.ts +0 -0
  107. /package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/ShareImage/type.ts +0 -0
  108. /package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/ShareText/Preview.tsx +0 -0
  109. /package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/ShareText/template.test.ts +0 -0
  110. /package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/ShareText/template.ts +0 -0
  111. /package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/ShareText/type.ts +0 -0
  112. /package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/index.tsx +0 -0
  113. /package/src/features/Conversation/components/{ChatItem/ShareMessageModal → ShareMessageModal}/style.ts +0 -0
  114. /package/src/features/Conversation/{components/ChatItem → context}/InPortalThreadContext.ts +0 -0
  115. /package/src/features/Conversation/{components/ChatItem/utils.test.ts → utils.test.ts} +0 -0
  116. /package/src/features/Conversation/{components/ChatItem/utils.ts → utils.ts} +0 -0
@@ -1,15 +1,20 @@
1
1
  import { MarkdownProps } from '@lobehub/ui';
2
2
  import { EditableMessage } from '@lobehub/ui/chat';
3
3
  import { useResponsive } from 'antd-style';
4
- import { type ReactNode, memo } from 'react';
4
+ import { type ReactNode, memo, useMemo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
5
6
  import { Flexbox } from 'react-layout-kit';
6
7
 
8
+ import { useChatStore } from '@/store/chat';
9
+ import { useUserStore } from '@/store/user';
10
+ import { userGeneralSettingsSelectors } from '@/store/user/selectors';
11
+
7
12
  import { useStyles } from '../style';
8
13
  import { ChatItemProps } from '../type';
9
14
 
10
15
  export interface MessageContentProps {
11
16
  editing?: ChatItemProps['editing'];
12
- fontSize?: number;
17
+ id: string;
13
18
  markdownProps?: Omit<MarkdownProps, 'className' | 'style' | 'children'>;
14
19
  message?: ReactNode;
15
20
  messageExtra?: ChatItemProps['messageExtra'];
@@ -19,16 +24,13 @@ export interface MessageContentProps {
19
24
  placement?: ChatItemProps['placement'];
20
25
  primary?: ChatItemProps['primary'];
21
26
  renderMessage?: ChatItemProps['renderMessage'];
22
- text?: ChatItemProps['text'];
23
27
  variant?: ChatItemProps['variant'];
24
28
  }
25
29
 
26
30
  const MessageContent = memo<MessageContentProps>(
27
31
  ({
28
32
  editing,
29
- onChange,
30
- onEditingChange,
31
- text,
33
+ id,
32
34
  message,
33
35
  placement,
34
36
  messageExtra,
@@ -36,11 +38,29 @@ const MessageContent = memo<MessageContentProps>(
36
38
  variant,
37
39
  primary,
38
40
  onDoubleClick,
39
- fontSize,
40
41
  markdownProps,
41
42
  }) => {
43
+ const { t } = useTranslation('common');
42
44
  const { cx, styles } = useStyles({ editing, placement, primary, variant });
45
+ const fontSize = useUserStore(userGeneralSettingsSelectors.fontSize);
43
46
  const { mobile } = useResponsive();
47
+ const text = useMemo(
48
+ () => ({
49
+ cancel: t('cancel'),
50
+ confirm: t('ok'),
51
+ edit: t('edit'),
52
+ }),
53
+ [],
54
+ );
55
+
56
+ const [toggleMessageEditing, updateMessageContent] = useChatStore((s) => [
57
+ s.toggleMessageEditing,
58
+ s.modifyMessageContent,
59
+ ]);
60
+ const onChange = (value: string) => {
61
+ updateMessageContent(id, value);
62
+ };
63
+ const onEditingChange = (edit: boolean) => toggleMessageEditing(id, edit);
44
64
 
45
65
  const content = (
46
66
  <EditableMessage
@@ -10,7 +10,7 @@ import { useChatStore } from '@/store/chat';
10
10
  import { chatPortalSelectors, chatSelectors } from '@/store/chat/selectors';
11
11
  import { dotLoading } from '@/styles/loading';
12
12
 
13
- import { InPortalThreadContext } from '../../../ChatItem/InPortalThreadContext';
13
+ import { InPortalThreadContext } from '../../../context/InPortalThreadContext';
14
14
  import { MarkdownElementProps } from '../../type';
15
15
  import ArtifactIcon from './Icon';
16
16
 
@@ -2,7 +2,7 @@ import { toMarkdown } from 'mdast-util-to-markdown';
2
2
  import { Parent } from 'unist';
3
3
  import { expect } from 'vitest';
4
4
 
5
- import { treeNodeToString } from '@/features/Conversation/components/MarkdownElements/remarkPlugins/getNodeContent';
5
+ import { treeNodeToString } from '@/features/Conversation/MarkdownElements/remarkPlugins/getNodeContent';
6
6
 
7
7
  describe('treeNodeToString', () => {
8
8
  it('with latex', () => {
@@ -2,7 +2,7 @@ import { ActionIconGroup } from '@lobehub/ui';
2
2
  import type { ChatActionsBarProps } from '@lobehub/ui/chat';
3
3
  import { memo } from 'react';
4
4
 
5
- import { useChatListActionsBar } from '../hooks/useChatListActionsBar';
5
+ import { useChatListActionsBar } from '../../../hooks/useChatListActionsBar';
6
6
 
7
7
  export const ErrorActionsBar = memo<ChatActionsBarProps>(({ onActionClick }) => {
8
8
  const { regenerate, copy, edit, del, divider } = useChatListActionsBar();
@@ -1,43 +1,57 @@
1
- import { ActionIconGroup, type ActionIconGroupEvent, type ActionIconGroupProps } from '@lobehub/ui';
1
+ import { ActionIconGroup, type ActionIconGroupEvent, ActionIconGroupItemType } from '@lobehub/ui';
2
2
  import { App } from 'antd';
3
- import isEqual from 'fast-deep-equal';
4
3
  import { useSearchParams } from 'next/navigation';
5
- import { memo, use, useCallback, useState } from 'react';
4
+ import { memo, use, useCallback, useContext, useMemo, useState } from 'react';
6
5
  import { useTranslation } from 'react-i18next';
7
6
 
7
+ import ShareMessageModal from '@/features/Conversation/components/ShareMessageModal';
8
8
  import { VirtuosoContext } from '@/features/Conversation/components/VirtualizedList/VirtuosoContext';
9
9
  import { useChatStore } from '@/store/chat';
10
- import { chatSelectors } from '@/store/chat/selectors';
11
- import { MessageRoleType } from '@/types/message';
10
+ import { threadSelectors } from '@/store/chat/selectors';
11
+ import { ChatMessage } from '@/types/message';
12
12
 
13
- import { renderActions } from '../../Actions';
14
- import { useChatListActionsBar } from '../../hooks/useChatListActionsBar';
15
- import ShareMessageModal from './ShareMessageModal';
13
+ import { InPortalThreadContext } from '../../../context/InPortalThreadContext';
14
+ import { useChatListActionsBar } from '../../../hooks/useChatListActionsBar';
15
+ import { ErrorActionsBar } from './Error';
16
16
 
17
- export type ActionsBarProps = ActionIconGroupProps;
18
-
19
- const ActionsBar = memo<ActionsBarProps>((props) => {
20
- const { regenerate, edit, copy, divider, del } = useChatListActionsBar();
21
-
22
- return (
23
- <ActionIconGroup
24
- items={[regenerate, edit]}
25
- menu={{
26
- items: [edit, copy, regenerate, divider, del],
27
- }}
28
- {...props}
29
- />
30
- );
31
- });
32
-
33
- interface ActionsProps {
17
+ interface AssistantActionsProps {
18
+ data: ChatMessage;
34
19
  id: string;
35
- inPortalThread?: boolean;
36
20
  index: number;
37
21
  }
22
+ export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, index }) => {
23
+ const { error, tools } = data;
24
+ const [isThreadMode, hasThread] = useChatStore((s) => [
25
+ !!s.activeThreadId,
26
+ threadSelectors.hasThreadBySourceMsgId(id)(s),
27
+ ]);
28
+ const [showShareModal, setShareModal] = useState(false);
29
+
30
+ const {
31
+ regenerate,
32
+ edit,
33
+ delAndRegenerate,
34
+ copy,
35
+ divider,
36
+ del,
37
+ branching,
38
+ // export: exportPDF,
39
+ share,
40
+ tts,
41
+ translate,
42
+ } = useChatListActionsBar({ hasThread });
43
+
44
+ const hasTools = !!tools;
45
+
46
+ const inPortalThread = useContext(InPortalThreadContext);
47
+ const inThread = isThreadMode || inPortalThread;
48
+
49
+ const items = useMemo(() => {
50
+ if (hasTools) return [delAndRegenerate, copy];
51
+
52
+ return [edit, copy, inThread ? null : branching].filter(Boolean) as ActionIconGroupItemType[];
53
+ }, [inThread, hasTools]);
38
54
 
39
- const Actions = memo<ActionsProps>(({ id, inPortalThread, index }) => {
40
- const item = useChatStore(chatSelectors.getMessageById(id), isEqual);
41
55
  const { t } = useTranslation('common');
42
56
  const searchParams = useSearchParams();
43
57
  const topic = searchParams.get('topic');
@@ -67,9 +81,7 @@ const Actions = memo<ActionsProps>(({ id, inPortalThread, index }) => {
67
81
  const { message } = App.useApp();
68
82
  const virtuosoRef = use(VirtuosoContext);
69
83
 
70
- const [showShareModal, setShareModal] = useState(false);
71
-
72
- const handleActionClick = useCallback(
84
+ const onActionClick = useCallback(
73
85
  async (action: ActionIconGroupEvent) => {
74
86
  switch (action.key) {
75
87
  case 'edit': {
@@ -78,11 +90,11 @@ const Actions = memo<ActionsProps>(({ id, inPortalThread, index }) => {
78
90
  virtuosoRef?.current?.scrollIntoView({ align: 'start', behavior: 'auto', index });
79
91
  }
80
92
  }
81
- if (!item) return;
93
+ if (!data) return;
82
94
 
83
95
  switch (action.key) {
84
96
  case 'copy': {
85
- await copyMessage(id, item.content);
97
+ await copyMessage(id, data.content);
86
98
  message.success(t('copySuccess', { defaultValue: 'Copy Success' }));
87
99
  break;
88
100
  }
@@ -106,7 +118,7 @@ const Actions = memo<ActionsProps>(({ id, inPortalThread, index }) => {
106
118
  } else regenerateMessage(id);
107
119
 
108
120
  // if this message is an error message, we need to delete it
109
- if (item.error) deleteMessage(id);
121
+ if (data.error) deleteMessage(id);
110
122
  break;
111
123
  }
112
124
 
@@ -136,28 +148,45 @@ const Actions = memo<ActionsProps>(({ id, inPortalThread, index }) => {
136
148
  }
137
149
 
138
150
  if (action.keyPath.at(-1) === 'translate') {
139
- // click the menu item with translate item, the result is:
151
+ // click the menu data with translate data, the result is:
140
152
  // key: 'en-US'
141
153
  // keyPath: ['en-US','translate']
142
154
  const lang = action.keyPath[0];
143
155
  translateMessage(id, lang);
144
156
  }
145
157
  },
146
- [item],
158
+ [data],
147
159
  );
148
160
 
149
- const RenderFunction = renderActions[(item?.role || '') as MessageRoleType] ?? ActionsBar;
150
-
151
- if (!item) return null;
161
+ if (error) return <ErrorActionsBar onActionClick={onActionClick} />;
152
162
 
153
163
  return (
154
164
  <>
155
- <RenderFunction {...item} onActionClick={handleActionClick} />
165
+ <ActionIconGroup
166
+ items={items}
167
+ menu={{
168
+ items: [
169
+ edit,
170
+ copy,
171
+ divider,
172
+ tts,
173
+ translate,
174
+ divider,
175
+ share,
176
+ // exportPDF,
177
+ divider,
178
+ regenerate,
179
+ delAndRegenerate,
180
+ del,
181
+ ],
182
+ }}
183
+ onActionClick={onActionClick}
184
+ />
156
185
  {/*{showModal && (*/}
157
- {/* <ExportPreview content={item.content} onClose={() => setModal(false)} open={showModal} />*/}
186
+ {/* <ExportPreview content={data.content} onClose={() => setModal(false)} open={showModal} />*/}
158
187
  {/*)}*/}
159
188
  <ShareMessageModal
160
- message={item}
189
+ message={data!}
161
190
  onCancel={() => {
162
191
  setShareModal(false);
163
192
  }}
@@ -166,5 +195,3 @@ const Actions = memo<ActionsProps>(({ id, inPortalThread, index }) => {
166
195
  </>
167
196
  );
168
197
  });
169
-
170
- export default Actions;
@@ -0,0 +1,63 @@
1
+ import { Markdown } from '@lobehub/ui';
2
+ import { ReactNode, memo } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ import { LOADING_FLAT } from '@/const/message';
6
+ import ImageFileListViewer from '@/features/Conversation/Messages/User/ImageFileListViewer';
7
+ import { useChatStore } from '@/store/chat';
8
+ import { chatSelectors } from '@/store/chat/selectors';
9
+ import { AssistantContentBlock } from '@/types/message';
10
+
11
+ import { DefaultMessage } from '../Default';
12
+ import Tool from './Tool';
13
+
14
+ interface AssistantBlockProps extends AssistantContentBlock {
15
+ editableContent: ReactNode;
16
+ }
17
+ export const AssistantBlock = memo<AssistantBlockProps>(
18
+ ({ id, tools, content, imageList, ...props }) => {
19
+ const editing = useChatStore(chatSelectors.isMessageEditing(id));
20
+ const generating = useChatStore(chatSelectors.isMessageGenerating(id));
21
+
22
+ console.log(id, content, props);
23
+ const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
24
+
25
+ const showImageItems = !!imageList && imageList.length > 0;
26
+
27
+ if (tools && tools.length > 0)
28
+ return (
29
+ <Flexbox gap={8}>
30
+ {tools.map((toolCall, index) => (
31
+ <Tool
32
+ apiName={toolCall.apiName}
33
+ arguments={toolCall.arguments}
34
+ id={toolCall.id}
35
+ identifier={toolCall.identifier}
36
+ index={index}
37
+ key={toolCall.id}
38
+ messageId={id}
39
+ payload={toolCall}
40
+ type={toolCall.type}
41
+ />
42
+ ))}
43
+ </Flexbox>
44
+ );
45
+
46
+ if (editing)
47
+ return (
48
+ <DefaultMessage
49
+ content={content}
50
+ id={id}
51
+ isToolCallGenerating={isToolCallGenerating}
52
+ {...(props as any)}
53
+ />
54
+ );
55
+
56
+ return (
57
+ <Flexbox gap={8} id={id}>
58
+ <Markdown variant={'chat'}>{content}</Markdown>
59
+ {showImageItems && <ImageFileListViewer items={imageList} />}
60
+ </Flexbox>
61
+ );
62
+ },
63
+ );
@@ -1,28 +1,27 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
- import { useAgentStore } from '@/store/agent';
5
- import { agentSelectors } from '@/store/agent/selectors';
4
+ import { useChatStore } from '@/store/chat';
6
5
  import { ChatMessage } from '@/types/message';
7
6
 
8
- import { AssistantMessageExtra } from './Assistant';
7
+ import { AssistantMessageExtra } from './index';
8
+
9
+ vi.mock('zustand/traditional');
9
10
 
10
11
  // Mock TTS and Translate components
11
- vi.mock('./TTS', () => ({
12
+ vi.mock('@/features/Conversation/components/Extras/TTS', () => ({
12
13
  default: vi.fn(() => <div>TTS Component</div>),
13
14
  }));
14
- vi.mock('./Translate', () => ({
15
+ vi.mock('@/features/Conversation/components/Extras/Translate', () => ({
15
16
  default: vi.fn(() => <div>Translate Component</div>),
16
17
  }));
17
-
18
- // Mock dependencies
19
- vi.mock('@/store/agent', () => ({
20
- useAgentStore: vi.fn(),
18
+ vi.mock('@/features/Conversation/components/Extras/Usage', () => ({
19
+ default: vi.fn(() => <div>Usage Component</div>),
21
20
  }));
22
- vi.mock('@/store/agent/selectors', () => ({
23
- agentSelectors: {
24
- currentAgentModel: vi.fn(),
25
- },
21
+
22
+ // Mock store
23
+ vi.mock('@/store/chat', () => ({
24
+ useChatStore: vi.fn(),
26
25
  }));
27
26
 
28
27
  const mockData: ChatMessage = {
@@ -36,37 +35,38 @@ const mockData: ChatMessage = {
36
35
 
37
36
  describe('AssistantMessageExtra', () => {
38
37
  beforeEach(() => {
39
- // Set default mock return values
40
- (useAgentStore as unknown as Mock).mockImplementation(() => ({
41
- chatLoadingId: null,
42
- }));
43
- (agentSelectors.currentAgentModel as Mock).mockReturnValue('defaultModel');
38
+ // Mock useChatStore to return false for loading state
39
+ (useChatStore as unknown as Mock).mockReturnValue(false);
44
40
  });
45
41
 
46
42
  it('should not render content if extra is undefined', async () => {
47
43
  render(<AssistantMessageExtra {...mockData} />);
48
- expect(screen.queryByText('defaultModel')).toBeNull();
44
+ expect(screen.queryByText('Usage Component')).toBeNull();
49
45
  expect(screen.queryByText('TTS Component')).toBeNull();
50
46
  expect(screen.queryByText('Translate Component')).toBeNull();
51
47
  });
52
48
 
53
49
  it('should not render content if extra is defined but does not contain fromModel, tts, or translate', async () => {
54
- render(<AssistantMessageExtra {...mockData} />);
55
- expect(screen.queryByText('defaultModel')).toBeNull();
50
+ render(<AssistantMessageExtra {...mockData} extra={{}} />);
51
+ expect(screen.queryByText('Usage Component')).toBeNull();
56
52
  expect(screen.queryByText('TTS Component')).toBeNull();
57
53
  expect(screen.queryByText('Translate Component')).toBeNull();
58
54
  });
59
55
 
60
- it('should render Tag component if extra.fromModel exists and does not match the current model', async () => {
61
- render(<AssistantMessageExtra {...mockData} extra={{ fromModel: 'otherModel' }} />);
56
+ it('should render Usage component if extra.fromModel exists', async () => {
57
+ render(
58
+ <AssistantMessageExtra
59
+ {...mockData}
60
+ extra={{ fromModel: 'gpt-4', fromProvider: 'openai' }}
61
+ />,
62
+ );
62
63
 
63
- expect(screen.getByText('otherModel')).toBeInTheDocument();
64
+ expect(screen.getByText('Usage Component')).toBeInTheDocument();
64
65
  });
65
66
 
66
- it('should render TTS component if extra.fromModel and extra.tts coexist', async () => {
67
- render(<AssistantMessageExtra {...mockData} extra={{ fromModel: 'otherModel', tts: {} }} />);
67
+ it('should render TTS component if extra.tts exists', async () => {
68
+ render(<AssistantMessageExtra {...mockData} extra={{ tts: {} }} />);
68
69
 
69
- expect(screen.getByText('otherModel')).toBeInTheDocument();
70
70
  expect(screen.getByText('TTS Component')).toBeInTheDocument();
71
71
  });
72
72
 
@@ -75,10 +75,15 @@ describe('AssistantMessageExtra', () => {
75
75
  expect(screen.getByText('Translate Component')).toBeInTheDocument();
76
76
  });
77
77
 
78
- it('should receive the correct loading attribute if loading is true for TTS and Translate components', async () => {
79
- (useAgentStore as unknown as Mock).mockImplementation(() => ({
80
- chatLoadingId: 'test-id',
81
- }));
78
+ it('should render both TTS and Translate components when both exist in extra', async () => {
79
+ render(<AssistantMessageExtra {...mockData} extra={{ translate: { to: 'abc' }, tts: {} }} />);
80
+ expect(screen.getByText('TTS Component')).toBeInTheDocument();
81
+ expect(screen.getByText('Translate Component')).toBeInTheDocument();
82
+ });
83
+
84
+ it('should pass loading state to TTS and Translate components', async () => {
85
+ (useChatStore as unknown as Mock).mockReturnValue(true);
86
+
82
87
  render(<AssistantMessageExtra {...mockData} extra={{ translate: { to: 'abc' }, tts: {} }} />);
83
88
  expect(screen.getByText('TTS Component')).toBeInTheDocument();
84
89
  expect(screen.getByText('Translate Component')).toBeInTheDocument();
@@ -2,17 +2,23 @@ import { memo } from 'react';
2
2
  import { Flexbox } from 'react-layout-kit';
3
3
 
4
4
  import { LOADING_FLAT } from '@/const/message';
5
+ import ExtraContainer from '@/features/Conversation/components/Extras/ExtraContainer';
6
+ import TTS from '@/features/Conversation/components/Extras/TTS';
7
+ import Translate from '@/features/Conversation/components/Extras/Translate';
8
+ import Usage from '@/features/Conversation/components/Extras/Usage';
5
9
  import { useChatStore } from '@/store/chat';
6
10
  import { chatSelectors } from '@/store/chat/selectors';
7
- import { ChatMessage } from '@/types/message';
11
+ import { type MessageMetadata } from '@/types/message';
8
12
 
9
- import { RenderMessageExtra } from '../types';
10
- import ExtraContainer from './ExtraContainer';
11
- import TTS from './TTS';
12
- import Translate from './Translate';
13
- import Usage from './Usage';
13
+ interface AssistantMessageExtraProps {
14
+ content: string;
15
+ extra?: any;
16
+ id: string;
17
+ metadata?: MessageMetadata | null;
18
+ tools?: any[];
19
+ }
14
20
 
15
- export const AssistantMessageExtra: RenderMessageExtra = memo<ChatMessage>(
21
+ export const AssistantMessageExtra = memo<AssistantMessageExtraProps>(
16
22
  ({ extra, id, content, metadata, tools }) => {
17
23
  const loading = useChatStore(chatSelectors.isMessageGenerating(id));
18
24
 
@@ -0,0 +1,102 @@
1
+ import { ReactNode, memo } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import { LOADING_FLAT } from '@/const/message';
5
+ import { AssistantBlock } from '@/features/Conversation/Messages/Assistant/Block';
6
+ import ImageFileListViewer from '@/features/Conversation/Messages/User/ImageFileListViewer';
7
+ import VideoFileListViewer from '@/features/Conversation/Messages/User/VideoFileListViewer';
8
+ import { useChatStore } from '@/store/chat';
9
+ import { aiChatSelectors, chatSelectors } from '@/store/chat/selectors';
10
+ import { ChatMessage } from '@/types/message';
11
+
12
+ import { DefaultMessage } from '../Default';
13
+ import FileChunks from './FileChunks';
14
+ import IntentUnderstanding from './IntentUnderstanding';
15
+ import Reasoning from './Reasoning';
16
+ import SearchGrounding from './SearchGrounding';
17
+ import Tool from './Tool';
18
+
19
+ export const AssistantMessageContent = memo<
20
+ ChatMessage & {
21
+ editableContent: ReactNode;
22
+ }
23
+ >(({ id, tools, content, chunksList, search, imageList, videoList, children, ...props }) => {
24
+ const editing = useChatStore(chatSelectors.isMessageEditing(id));
25
+ const generating = useChatStore(chatSelectors.isMessageGenerating(id));
26
+
27
+ const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
28
+
29
+ const isReasoning = useChatStore(aiChatSelectors.isMessageInReasoning(id));
30
+
31
+ const isIntentUnderstanding = useChatStore(aiChatSelectors.isIntentUnderstanding(id));
32
+
33
+ const showSearch = !!search && !!search.citations?.length;
34
+ const showImageItems = !!imageList && imageList.length > 0;
35
+ const showVideoItems = !!videoList && videoList.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 (children && children?.length > 0)
46
+ return (
47
+ <Flexbox gap={8}>
48
+ {children.map((item) => (
49
+ <AssistantBlock key={item.id} {...item} editableContent={props.editableContent} />
50
+ ))}
51
+ </Flexbox>
52
+ );
53
+
54
+ return editing ? (
55
+ <DefaultMessage
56
+ content={content}
57
+ id={id}
58
+ isToolCallGenerating={isToolCallGenerating}
59
+ {...props}
60
+ />
61
+ ) : (
62
+ <Flexbox gap={8} id={id}>
63
+ {showSearch && (
64
+ <SearchGrounding citations={search?.citations} searchQueries={search?.searchQueries} />
65
+ )}
66
+ {showFileChunks && <FileChunks data={chunksList} />}
67
+ {showReasoning && <Reasoning {...props.reasoning} id={id} />}
68
+ {isIntentUnderstanding ? (
69
+ <IntentUnderstanding />
70
+ ) : (
71
+ content && (
72
+ <DefaultMessage
73
+ addIdOnDOM={false}
74
+ content={content}
75
+ id={id}
76
+ isToolCallGenerating={isToolCallGenerating}
77
+ {...props}
78
+ />
79
+ )
80
+ )}
81
+ {showImageItems && <ImageFileListViewer items={imageList} />}
82
+ {showVideoItems && <VideoFileListViewer items={videoList} />}
83
+ {tools && (
84
+ <Flexbox gap={8}>
85
+ {tools.map((toolCall, index) => (
86
+ <Tool
87
+ apiName={toolCall.apiName}
88
+ arguments={toolCall.arguments}
89
+ id={toolCall.id}
90
+ identifier={toolCall.identifier}
91
+ index={index}
92
+ key={toolCall.id}
93
+ messageId={id}
94
+ payload={toolCall}
95
+ type={toolCall.type}
96
+ />
97
+ ))}
98
+ </Flexbox>
99
+ )}
100
+ </Flexbox>
101
+ );
102
+ });