@lobehub/lobehub 2.0.0-next.65 → 2.0.0-next.66

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 (58) hide show
  1. package/.github/workflows/claude-translator.yml +1 -0
  2. package/CHANGELOG.md +25 -0
  3. package/changelog/v1.json +9 -0
  4. package/locales/ar/chat.json +3 -0
  5. package/locales/bg-BG/chat.json +3 -0
  6. package/locales/de-DE/chat.json +3 -0
  7. package/locales/en-US/chat.json +3 -0
  8. package/locales/es-ES/chat.json +3 -0
  9. package/locales/fa-IR/chat.json +3 -0
  10. package/locales/fr-FR/chat.json +3 -0
  11. package/locales/it-IT/chat.json +3 -0
  12. package/locales/ja-JP/chat.json +3 -0
  13. package/locales/ko-KR/chat.json +3 -0
  14. package/locales/nl-NL/chat.json +3 -0
  15. package/locales/pl-PL/chat.json +3 -0
  16. package/locales/pt-BR/chat.json +3 -0
  17. package/locales/ru-RU/chat.json +3 -0
  18. package/locales/tr-TR/chat.json +3 -0
  19. package/locales/vi-VN/chat.json +3 -0
  20. package/locales/zh-CN/chat.json +3 -0
  21. package/locales/zh-TW/chat.json +3 -0
  22. package/package.json +5 -5
  23. package/packages/conversation-flow/src/__tests__/fixtures/index.ts +4 -8
  24. package/packages/conversation-flow/src/__tests__/fixtures/inputs/{assistant-with-tools.json → assistantGroup/assistant-with-tools.json} +2 -1
  25. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistantGroup/index.ts +8 -0
  26. package/packages/conversation-flow/src/__tests__/fixtures/inputs/index.ts +2 -4
  27. package/packages/conversation-flow/src/__tests__/fixtures/outputs/{assistant-with-tools.json → assistantGroup/assistant-with-tools.json} +8 -8
  28. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistantGroup/index.ts +8 -0
  29. package/packages/conversation-flow/src/__tests__/parse.test.ts +6 -6
  30. package/packages/conversation-flow/src/parse.ts +45 -1
  31. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +64 -0
  32. package/packages/database/package.json +2 -2
  33. package/packages/obervability-otel/package.json +1 -1
  34. package/packages/types/src/message/common/metadata.ts +8 -1
  35. package/packages/types/src/message/ui/chat.ts +1 -0
  36. package/src/app/(backend)/market/agent/[[...segments]]/route.ts +1 -1
  37. package/src/app/(backend)/market/oidc/[[...segments]]/route.ts +1 -1
  38. package/src/app/market-auth-callback/layout.tsx +27 -3
  39. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +2 -2
  40. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +15 -1
  41. package/src/features/Conversation/Messages/Assistant/CollapsedMessage.tsx +37 -0
  42. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +16 -9
  43. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +28 -6
  44. package/src/features/Conversation/Messages/Group/CollapsedMessage.tsx +37 -0
  45. package/src/features/Conversation/Messages/Group/{GroupChildren.tsx → Group.tsx} +18 -4
  46. package/src/features/Conversation/Messages/Group/index.tsx +4 -6
  47. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +14 -0
  48. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +1 -1
  49. package/src/libs/mcp/__tests__/index.test.ts +6 -6
  50. package/src/locales/default/chat.ts +3 -0
  51. package/src/store/chat/slices/message/actions/publicApi.ts +17 -0
  52. package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -1
  53. package/src/store/chat/slices/message/selectors/messageState.ts +7 -0
  54. package/src/store/chat/slices/translate/action.test.ts +26 -32
  55. package/src/store/chat/slices/translate/action.ts +3 -3
  56. /package/packages/conversation-flow/src/__tests__/fixtures/inputs/{complex-scenario.json → assistantGroup/tools-with-branches.json} +0 -0
  57. /package/packages/conversation-flow/src/__tests__/fixtures/outputs/{complex-scenario.json → assistantGroup/tools-with-branches.json} +0 -0
  58. /package/src/features/Conversation/Messages/Group/{GroupContext.tsx → GroupContext.ts} +0 -0
@@ -40,9 +40,53 @@ export function parse(messages: Message[], messageGroups?: MessageGroupMetadata[
40
40
  const flatList = transformer.flatten(messages);
41
41
 
42
42
  // Convert messageMap from Map to plain object for serialization
43
+ // Clean up metadata for assistant messages with tools
43
44
  const messageMapObj: Record<string, Message> = {};
45
+ const usagePerformanceFields = new Set([
46
+ 'acceptedPredictionTokens',
47
+ 'cost',
48
+ 'duration',
49
+ 'inputAudioTokens',
50
+ 'inputCacheMissTokens',
51
+ 'inputCachedTokens',
52
+ 'inputCitationTokens',
53
+ 'inputImageTokens',
54
+ 'inputTextTokens',
55
+ 'inputWriteCacheTokens',
56
+ 'latency',
57
+ 'outputAudioTokens',
58
+ 'outputImageTokens',
59
+ 'outputReasoningTokens',
60
+ 'outputTextTokens',
61
+ 'rejectedPredictionTokens',
62
+ 'totalInputTokens',
63
+ 'totalOutputTokens',
64
+ 'totalTokens',
65
+ 'tps',
66
+ 'ttft',
67
+ ]);
68
+
44
69
  helperMaps.messageMap.forEach((message, id) => {
45
- messageMapObj[id] = message;
70
+ // For assistant messages with tools, clean metadata to keep only usage/performance fields
71
+ if (
72
+ message.role === 'assistant' &&
73
+ message.tools &&
74
+ message.tools.length > 0 &&
75
+ message.metadata
76
+ ) {
77
+ const cleanedMetadata: Record<string, any> = {};
78
+ Object.entries(message.metadata).forEach(([key, value]) => {
79
+ if (usagePerformanceFields.has(key)) {
80
+ cleanedMetadata[key] = value;
81
+ }
82
+ });
83
+ messageMapObj[id] = {
84
+ ...message,
85
+ metadata: Object.keys(cleanedMetadata).length > 0 ? cleanedMetadata : undefined,
86
+ };
87
+ } else {
88
+ messageMapObj[id] = message;
89
+ }
46
90
  });
47
91
 
48
92
  return {
@@ -445,6 +445,40 @@ export class FlatListBuilder {
445
445
  const msgUsage = assistant.usage || metaUsage;
446
446
  const msgPerformance = assistant.performance || metaPerformance;
447
447
 
448
+ // Extract non-usage/performance metadata fields
449
+ const otherMetadata: Record<string, any> = {};
450
+ if (assistant.metadata) {
451
+ const usagePerformanceFields = new Set([
452
+ 'acceptedPredictionTokens',
453
+ 'cost',
454
+ 'duration',
455
+ 'inputAudioTokens',
456
+ 'inputCacheMissTokens',
457
+ 'inputCachedTokens',
458
+ 'inputCitationTokens',
459
+ 'inputImageTokens',
460
+ 'inputTextTokens',
461
+ 'inputWriteCacheTokens',
462
+ 'latency',
463
+ 'outputAudioTokens',
464
+ 'outputImageTokens',
465
+ 'outputReasoningTokens',
466
+ 'outputTextTokens',
467
+ 'rejectedPredictionTokens',
468
+ 'totalInputTokens',
469
+ 'totalOutputTokens',
470
+ 'totalTokens',
471
+ 'tps',
472
+ 'ttft',
473
+ ]);
474
+
475
+ Object.entries(assistant.metadata).forEach(([key, value]) => {
476
+ if (!usagePerformanceFields.has(key)) {
477
+ otherMetadata[key] = value;
478
+ }
479
+ });
480
+ }
481
+
448
482
  const childBlock: AssistantContentBlock = {
449
483
  content: assistant.content || '',
450
484
  id: assistant.id,
@@ -457,12 +491,37 @@ export class FlatListBuilder {
457
491
  if (assistant.reasoning) childBlock.reasoning = assistant.reasoning;
458
492
  if (toolsWithResults.length > 0) childBlock.tools = toolsWithResults;
459
493
  if (msgUsage) childBlock.usage = msgUsage;
494
+ if (Object.keys(otherMetadata).length > 0) {
495
+ childBlock.metadata = otherMetadata;
496
+ }
460
497
 
461
498
  children.push(childBlock);
462
499
  }
463
500
 
464
501
  const aggregated = this.messageTransformer.aggregateMetadata(children);
465
502
 
503
+ // Collect all non-usage/performance metadata from all children
504
+ const groupMetadata: Record<string, any> = {};
505
+ children.forEach((child) => {
506
+ if ((child as any).metadata) {
507
+ Object.assign(groupMetadata, (child as any).metadata);
508
+ }
509
+ });
510
+
511
+ // If there's group-level metadata, apply it to first child and remove from others
512
+ if (Object.keys(groupMetadata).length > 0 && children.length > 0) {
513
+ // Ensure first child has the group metadata
514
+ if (!(children[0] as any).metadata) {
515
+ (children[0] as any).metadata = {};
516
+ }
517
+ Object.assign((children[0] as any).metadata, groupMetadata);
518
+
519
+ // Remove metadata from subsequent children (keep only in first child)
520
+ for (let i = 1; i < children.length; i++) {
521
+ delete (children[i] as any).metadata;
522
+ }
523
+ }
524
+
466
525
  const result: Message = {
467
526
  ...firstAssistant,
468
527
  children,
@@ -480,6 +539,11 @@ export class FlatListBuilder {
480
539
  if (aggregated.performance) result.performance = aggregated.performance;
481
540
  if (aggregated.usage) result.usage = aggregated.usage;
482
541
 
542
+ // Add group-level metadata if it exists
543
+ if (Object.keys(groupMetadata).length > 0) {
544
+ result.metadata = groupMetadata;
545
+ }
546
+
483
547
  return result;
484
548
  }
485
549
 
@@ -23,9 +23,9 @@
23
23
  },
24
24
  "peerDependencies": {
25
25
  "@electric-sql/pglite": "^0.2.17",
26
- "dayjs": ">=1.11.18",
26
+ "dayjs": ">=1.11.19",
27
27
  "drizzle-orm": ">=0.44.7",
28
- "nanoid": ">=5.1.5",
28
+ "nanoid": ">=5.1.6",
29
29
  "pg": ">=8.16.3"
30
30
  }
31
31
  }
@@ -20,6 +20,6 @@
20
20
  "@opentelemetry/sdk-node": "^0.207.0",
21
21
  "@opentelemetry/sdk-trace-node": "^2.0.1",
22
22
  "@opentelemetry/semantic-conventions": "^1.36.0",
23
- "@vercel/otel": "^1.13.0"
23
+ "@vercel/otel": "^2.1.0"
24
24
  }
25
25
  }
@@ -75,7 +75,9 @@ export const ModelPerformanceSchema = z.object({
75
75
  latency: z.number().optional(),
76
76
  });
77
77
 
78
- export const MessageMetadataSchema = ModelUsageSchema.merge(ModelPerformanceSchema);
78
+ export const MessageMetadataSchema = ModelUsageSchema.merge(ModelPerformanceSchema).extend({
79
+ collapsed: z.boolean().optional(),
80
+ });
79
81
 
80
82
  export interface ModelUsage extends ModelTokensUsage {
81
83
  /**
@@ -106,5 +108,10 @@ export interface ModelPerformance {
106
108
  export interface MessageMetadata extends ModelUsage, ModelPerformance {
107
109
  activeBranchIndex?: number;
108
110
  activeColumn?: boolean;
111
+ /**
112
+ * 消息折叠状态
113
+ * true: 折叠, false/undefined: 展开
114
+ */
115
+ collapsed?: boolean;
109
116
  compare?: boolean;
110
117
  }
@@ -40,6 +40,7 @@ export interface AssistantContentBlock {
40
40
  error?: ChatMessageError | null;
41
41
  id: string;
42
42
  imageList?: ChatImageItem[];
43
+ metadata?: Record<string, any>;
43
44
  performance?: ModelPerformance;
44
45
  reasoning?: ModelReasoning;
45
46
  tools?: ChatToolPayloadWithResult[];
@@ -7,7 +7,7 @@ type RouteContext = {
7
7
  }>;
8
8
  };
9
9
 
10
- const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'http://127.0.0.1:8787';
10
+ const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
11
11
 
12
12
  const extractAccessToken = (req: NextRequest) => {
13
13
  const authorization = req.headers.get('authorization');
@@ -7,7 +7,7 @@ type RouteContext = {
7
7
  }>;
8
8
  };
9
9
 
10
- const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'http://127.0.0.1:8787';
10
+ const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
11
11
  const ALLOWED_ENDPOINTS = new Set(['handoff', 'token', 'userinfo']);
12
12
 
13
13
  const ensureEndpoint = (segments?: string[]) => {
@@ -1,13 +1,37 @@
1
+ import { cookies, headers } from 'next/headers';
1
2
  import { ReactNode } from 'react';
3
+ import { isRtlLang } from 'rtl-detect';
4
+ import { NuqsAdapter } from 'nuqs/adapters/next/app';
5
+
6
+
7
+ import { DEFAULT_LANG, LOBE_LOCALE_COOKIE } from '@/const/locale';
8
+ import GlobalLayout from '@/layout/GlobalProvider';
9
+ import { Locales } from '@/locales/resources';
10
+ import { parseBrowserLanguage } from '@/utils/locale';
2
11
 
3
12
  interface RootLayoutProps {
4
13
  children: ReactNode;
5
14
  }
6
15
 
7
- const RootLayout = ({ children }: RootLayoutProps) => {
16
+ const RootLayout = async ({ children }: RootLayoutProps) => {
17
+ // 获取 locale:优先级为 cookie > 浏览器语言 > 默认语言
18
+ const cookieStore = await cookies();
19
+ const headersList = await headers();
20
+ const cookieLocale = cookieStore.get(LOBE_LOCALE_COOKIE)?.value as Locales | undefined;
21
+ const browserLanguage = parseBrowserLanguage(headersList, DEFAULT_LANG);
22
+ const locale = (cookieLocale || browserLanguage || DEFAULT_LANG) as Locales;
23
+
24
+ const direction = isRtlLang(locale) ? 'rtl' : 'ltr';
25
+
8
26
  return (
9
- <html lang="en" suppressHydrationWarning>
10
- <body>{children}</body>
27
+ <html dir={direction} lang={locale} suppressHydrationWarning>
28
+ <body>
29
+ <NuqsAdapter>
30
+ <GlobalLayout appearance="auto" isMobile={false} locale={locale}>
31
+ {children}
32
+ </GlobalLayout>
33
+ </NuqsAdapter>
34
+ </body>
11
35
  </html>
12
36
  );
13
37
  };
@@ -13,7 +13,7 @@ import { useTokenCount } from '@/hooks/useTokenCount';
13
13
  import { useAgentStore } from '@/store/agent';
14
14
  import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
15
15
  import { useChatStore } from '@/store/chat';
16
- import { chatSelectors, topicSelectors } from '@/store/chat/selectors';
16
+ import { dbMessageSelectors, topicSelectors } from '@/store/chat/selectors';
17
17
  import { useToolStore } from '@/store/tool';
18
18
  import { toolSelectors } from '@/store/tool/selectors';
19
19
 
@@ -77,7 +77,7 @@ const Token = memo<TokenTagProps>(({ total: messageString }) => {
77
77
  const inputTokenCount = useTokenCount(input);
78
78
 
79
79
  const chatsString = useMemo(() => {
80
- const chats = chatSelectors.mainAIChatsWithHistoryConfig(useChatStore.getState());
80
+ const chats = dbMessageSelectors.activeDbMessages(useChatStore.getState());
81
81
  return chats.map((chat) => chat.content).join('');
82
82
  }, [messageString, historyCount, enableHistoryCount]);
83
83
 
@@ -23,10 +23,11 @@ interface AssistantActionsProps {
23
23
  }
24
24
  export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, index }) => {
25
25
  const { error, tools } = data;
26
- const [isThreadMode, hasThread, isRegenerating] = useChatStore((s) => [
26
+ const [isThreadMode, hasThread, isRegenerating, isCollapsed] = useChatStore((s) => [
27
27
  !!s.activeThreadId,
28
28
  threadSelectors.hasThreadBySourceMsgId(id)(s),
29
29
  messageStateSelectors.isMessageRegenerating(id)(s),
30
+ messageStateSelectors.isMessageCollapsed(id)(s),
30
31
  ]);
31
32
  const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
32
33
  const [showShareModal, setShareModal] = useState(false);
@@ -43,6 +44,8 @@ export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, inde
43
44
  share,
44
45
  tts,
45
46
  translate,
47
+ collapse,
48
+ expand,
46
49
  } = useChatListActionsBar({ hasThread, isRegenerating });
47
50
 
48
51
  const hasTools = !!tools;
@@ -72,6 +75,7 @@ export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, inde
72
75
  resendThreadMessage,
73
76
  delAndResendThreadMessage,
74
77
  toggleMessageEditing,
78
+ toggleMessageCollapsed,
75
79
  ] = useChatStore((s) => [
76
80
  s.deleteMessage,
77
81
  s.regenerateAssistantMessage,
@@ -83,6 +87,7 @@ export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, inde
83
87
  s.resendThreadMessage,
84
88
  s.delAndResendThreadMessage,
85
89
  s.toggleMessageEditing,
90
+ s.toggleMessageCollapsed,
86
91
  ]);
87
92
  const { message } = App.useApp();
88
93
  const virtuosoRef = use(VirtuosoContext);
@@ -142,6 +147,12 @@ export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, inde
142
147
  break;
143
148
  }
144
149
 
150
+ case 'collapse':
151
+ case 'expand': {
152
+ toggleMessageCollapsed(id);
153
+ break;
154
+ }
155
+
145
156
  // case 'export': {
146
157
  // setModal(true);
147
158
  // break;
@@ -166,6 +177,8 @@ export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, inde
166
177
 
167
178
  if (error) return <ErrorActionsBar onActionClick={onActionClick} />;
168
179
 
180
+ const collapseAction = isCollapsed ? expand : collapse;
181
+
169
182
  return (
170
183
  <>
171
184
  <ActionIconGroup
@@ -174,6 +187,7 @@ export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, inde
174
187
  items: [
175
188
  edit,
176
189
  copy,
190
+ collapseAction,
177
191
  divider,
178
192
  tts,
179
193
  translate,
@@ -0,0 +1,37 @@
1
+ import { Button, Markdown, MaskShadow } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import { useChatStore } from '@/store/chat';
7
+
8
+ interface CollapsedMessageProps {
9
+ content: string;
10
+ id: string;
11
+ }
12
+
13
+ export const CollapsedMessage = memo<CollapsedMessageProps>(({ id, content }) => {
14
+ const { t } = useTranslation('chat');
15
+ const toggleMessageCollapsed = useChatStore((s) => s.toggleMessageCollapsed);
16
+
17
+ return (
18
+ <Flexbox>
19
+ <MaskShadow>
20
+ <Markdown variant={'chat'}>{content?.slice(0, 100)}</Markdown>
21
+ </MaskShadow>
22
+ <Flexbox padding={4}>
23
+ <Button
24
+ block
25
+ color={'default'}
26
+ onClick={() => {
27
+ toggleMessageCollapsed(id, false);
28
+ }}
29
+ size={'small'}
30
+ variant={'filled'}
31
+ >
32
+ {t('chatList.expandMessage')}
33
+ </Button>
34
+ </Flexbox>
35
+ </Flexbox>
36
+ );
37
+ });
@@ -3,6 +3,7 @@ import { UIChatMessage } from '@lobechat/types';
3
3
  import { ReactNode, memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
+ import { CollapsedMessage } from '@/features/Conversation/Messages/Assistant/CollapsedMessage';
6
7
  import { useChatStore } from '@/store/chat';
7
8
  import { aiChatSelectors, messageStateSelectors } from '@/store/chat/selectors';
8
9
 
@@ -18,9 +19,10 @@ export const AssistantMessageContent = memo<
18
19
  editableContent: ReactNode;
19
20
  }
20
21
  >(({ id, tools, content, chunksList, search, imageList, ...props }) => {
21
- const [editing, generating] = useChatStore((s) => [
22
+ const [editing, generating, isCollapsed] = useChatStore((s) => [
22
23
  messageStateSelectors.isMessageEditing(id)(s),
23
24
  messageStateSelectors.isMessageGenerating(id)(s),
25
+ messageStateSelectors.isMessageCollapsed(id)(s),
24
26
  ]);
25
27
 
26
28
  const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
@@ -40,14 +42,19 @@ export const AssistantMessageContent = memo<
40
42
 
41
43
  const showFileChunks = !!chunksList && chunksList.length > 0;
42
44
 
43
- return editing ? (
44
- <DefaultMessage
45
- content={content}
46
- id={id}
47
- isToolCallGenerating={isToolCallGenerating}
48
- {...props}
49
- />
50
- ) : (
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 (
51
58
  <Flexbox gap={8} id={id}>
52
59
  {showSearch && (
53
60
  <SearchGrounding citations={search?.citations} searchQueries={search?.searchQueries} />
@@ -24,19 +24,30 @@ interface GroupActionsProps {
24
24
 
25
25
  const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }) => {
26
26
  const { tools } = data;
27
- const [isThreadMode, hasThread, isRegenerating] = useChatStore((s) => [
27
+ const [isThreadMode, hasThread, isRegenerating, isCollapsed] = useChatStore((s) => [
28
28
  !!s.activeThreadId,
29
29
  threadSelectors.hasThreadBySourceMsgId(id)(s),
30
30
  messageStateSelectors.isMessageRegenerating(id)(s),
31
+ messageStateSelectors.isMessageCollapsed(id)(s),
31
32
  ]);
32
33
  const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
33
34
  const [showShareModal, setShareModal] = useState(false);
34
35
 
35
- const { edit, delAndRegenerate, regenerate, copy, divider, del, branching, share } =
36
- useChatListActionsBar({
37
- hasThread,
38
- isRegenerating,
39
- });
36
+ const {
37
+ edit,
38
+ delAndRegenerate,
39
+ regenerate,
40
+ copy,
41
+ divider,
42
+ del,
43
+ branching,
44
+ share,
45
+ expand,
46
+ collapse,
47
+ } = useChatListActionsBar({
48
+ hasThread,
49
+ isRegenerating,
50
+ });
40
51
 
41
52
  const hasTools = !!tools;
42
53
 
@@ -64,6 +75,7 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
64
75
  resendThreadMessage,
65
76
  delAndResendThreadMessage,
66
77
  toggleMessageEditing,
78
+ toggleMessageCollapsed,
67
79
  ] = useChatStore((s) => [
68
80
  s.deleteMessage,
69
81
  s.regenerateAssistantMessage,
@@ -74,6 +86,7 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
74
86
  s.resendThreadMessage,
75
87
  s.delAndResendThreadMessage,
76
88
  s.toggleMessageEditing,
89
+ s.toggleMessageCollapsed,
77
90
  ]);
78
91
  const { message } = App.useApp();
79
92
  const virtuosoRef = use(VirtuosoContext);
@@ -133,6 +146,12 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
133
146
  setShareModal(true);
134
147
  break;
135
148
  }
149
+
150
+ case 'collapse':
151
+ case 'expand': {
152
+ toggleMessageCollapsed(id);
153
+ break;
154
+ }
136
155
  }
137
156
 
138
157
  if (action.keyPath.at(-1) === 'translate') {
@@ -146,6 +165,8 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
146
165
  [data, topic],
147
166
  );
148
167
 
168
+ const collapseAction = isCollapsed ? expand : collapse;
169
+
149
170
  return (
150
171
  <>
151
172
  <ActionIconGroup
@@ -154,6 +175,7 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
154
175
  items: [
155
176
  edit,
156
177
  copy,
178
+ collapseAction,
157
179
  divider,
158
180
  share,
159
181
  divider,
@@ -0,0 +1,37 @@
1
+ import { Button, Markdown, MaskShadow } from '@lobehub/ui';
2
+ import { memo } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import { useChatStore } from '@/store/chat';
7
+
8
+ interface CollapsedMessageProps {
9
+ content: string;
10
+ id: string;
11
+ }
12
+
13
+ export const CollapsedMessage = memo<CollapsedMessageProps>(({ id, content }) => {
14
+ const { t } = useTranslation('chat');
15
+ const toggleMessageCollapsed = useChatStore((s) => s.toggleMessageCollapsed);
16
+
17
+ return (
18
+ <Flexbox>
19
+ <MaskShadow>
20
+ <Markdown variant={'chat'}>{content?.slice(0, 300)}</Markdown>
21
+ </MaskShadow>
22
+ <Flexbox padding={4}>
23
+ <Button
24
+ block
25
+ color={'default'}
26
+ onClick={() => {
27
+ toggleMessageCollapsed(id, false);
28
+ }}
29
+ size={'small'}
30
+ variant={'filled'}
31
+ >
32
+ {t('chatList.expandMessage')}
33
+ </Button>
34
+ </Flexbox>
35
+ </Flexbox>
36
+ );
37
+ });
@@ -4,6 +4,10 @@ import isEqual from 'fast-deep-equal';
4
4
  import { memo, useMemo } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
+ import { CollapsedMessage } from '@/features/Conversation/Messages/Group/CollapsedMessage';
8
+ import { useChatStore } from '@/store/chat';
9
+ import { messageStateSelectors } from '@/store/chat/slices/message/selectors';
10
+
7
11
  import { GroupMessageContext } from './GroupContext';
8
12
  import GroupItem from './GroupItem';
9
13
 
@@ -19,18 +23,28 @@ const useStyles = createStyles(({ css }) => {
19
23
 
20
24
  interface GroupChildrenProps {
21
25
  blocks: AssistantContentBlock[];
26
+ content?: string;
22
27
  contentId?: string;
23
28
  disableEditing?: boolean;
24
29
  id: string;
25
30
  messageIndex: number;
26
31
  }
27
32
 
28
- const GroupChildren = memo<GroupChildrenProps>(
29
- ({ blocks, contentId, disableEditing, messageIndex, id }) => {
33
+ const Group = memo<GroupChildrenProps>(
34
+ ({ blocks, contentId, disableEditing, messageIndex, id, content }) => {
30
35
  const { styles } = useStyles();
31
-
36
+ const [isCollapsed] = useChatStore((s) => [messageStateSelectors.isMessageCollapsed(id)(s)]);
32
37
  const contextValue = useMemo(() => ({ assistantGroupId: id }), [id]);
33
38
 
39
+ if (isCollapsed) {
40
+ return (
41
+ content && (
42
+ <Flexbox>
43
+ <CollapsedMessage content={content} id={id} />
44
+ </Flexbox>
45
+ )
46
+ );
47
+ }
34
48
  return (
35
49
  <GroupMessageContext value={contextValue}>
36
50
  <Flexbox className={styles.container} gap={8}>
@@ -53,4 +67,4 @@ const GroupChildren = memo<GroupChildrenProps>(
53
67
  isEqual,
54
68
  );
55
69
 
56
- export default GroupChildren;
70
+ export default Group;
@@ -10,22 +10,19 @@ import Avatar from '@/features/ChatItem/components/Avatar';
10
10
  import BorderSpacing from '@/features/ChatItem/components/BorderSpacing';
11
11
  import Title from '@/features/ChatItem/components/Title';
12
12
  import { useStyles } from '@/features/ChatItem/style';
13
- import GroupChildren from '@/features/Conversation/Messages/Group/GroupChildren';
14
13
  import Usage from '@/features/Conversation/components/Extras/Usage';
15
14
  import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
16
15
  import { useAgentStore } from '@/store/agent';
17
16
  import { agentChatConfigSelectors } from '@/store/agent/selectors';
18
17
  import { useChatStore } from '@/store/chat';
19
- import {
20
- displayMessageSelectors,
21
- messageStateSelectors,
22
- } from '@/store/chat/slices/message/selectors';
18
+ import { displayMessageSelectors, messageStateSelectors } from '@/store/chat/selectors';
23
19
  import { useGlobalStore } from '@/store/global';
24
20
  import { useSessionStore } from '@/store/session';
25
21
  import { sessionSelectors } from '@/store/session/selectors';
26
22
 
27
23
  import { GroupActionsBar } from './Actions';
28
24
  import EditState from './EditState';
25
+ import Group from './Group';
29
26
 
30
27
  const MOBILE_AVATAR_SIZE = 32;
31
28
 
@@ -111,8 +108,9 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing }) =>
111
108
  width={'100%'}
112
109
  >
113
110
  {children && children.length > 0 && (
114
- <GroupChildren
111
+ <Group
115
112
  blocks={children}
113
+ content={lastAssistantMsg?.content}
116
114
  contentId={contentId}
117
115
  disableEditing={disableEditing}
118
116
  id={id}