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

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 (70) hide show
  1. package/.github/workflows/claude-translator.yml +1 -0
  2. package/CHANGELOG.md +50 -0
  3. package/changelog/v1.json +18 -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 +7 -6
  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/[variants]/(main)/chat/components/conversation/features/ChatList/ChatItem/index.tsx +1 -0
  39. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatMinimap/index.tsx +21 -28
  40. package/src/app/market-auth-callback/layout.tsx +27 -3
  41. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +2 -2
  42. package/src/features/ChatItem/style.ts +4 -0
  43. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +18 -4
  44. package/src/features/Conversation/Messages/Assistant/CollapsedMessage.tsx +37 -0
  45. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +16 -9
  46. package/src/features/Conversation/Messages/Assistant/index.tsx +329 -230
  47. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +31 -9
  48. package/src/features/Conversation/Messages/Group/CollapsedMessage.tsx +37 -0
  49. package/src/features/Conversation/Messages/Group/{GroupChildren.tsx → Group.tsx} +18 -4
  50. package/src/features/Conversation/Messages/Group/GroupItem.tsx +3 -5
  51. package/src/features/Conversation/Messages/Group/index.tsx +84 -19
  52. package/src/features/Conversation/Messages/User/Actions/ActionsBar.tsx +3 -3
  53. package/src/features/Conversation/Messages/index.tsx +24 -8
  54. package/src/features/Conversation/components/VirtualizedList/VirtuosoContext.ts +13 -13
  55. package/src/features/Conversation/components/VirtualizedList/index.tsx +92 -58
  56. package/src/features/Conversation/components/WideScreenContainer/index.tsx +10 -6
  57. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +14 -0
  58. package/src/features/Conversation/hooks/useDoubleClickEdit.ts +3 -3
  59. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +1 -1
  60. package/src/libs/mcp/__tests__/index.test.ts +6 -6
  61. package/src/locales/default/chat.ts +3 -0
  62. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +9 -1
  63. package/src/store/chat/slices/message/actions/publicApi.ts +17 -0
  64. package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -1
  65. package/src/store/chat/slices/message/selectors/messageState.ts +7 -0
  66. package/src/store/chat/slices/translate/action.test.ts +26 -32
  67. package/src/store/chat/slices/translate/action.ts +3 -3
  68. /package/packages/conversation-flow/src/__tests__/fixtures/inputs/{complex-scenario.json → assistantGroup/tools-with-branches.json} +0 -0
  69. /package/packages/conversation-flow/src/__tests__/fixtures/outputs/{complex-scenario.json → assistantGroup/tools-with-branches.json} +0 -0
  70. /package/src/features/Conversation/Messages/Group/{GroupContext.tsx → GroupContext.ts} +0 -0
@@ -22,9 +22,9 @@ describe('parse', () => {
22
22
 
23
23
  describe('Tool Usage', () => {
24
24
  it('should parse assistant with tools correctly', () => {
25
- const result = parse(inputs.assistantWithTools);
25
+ const result = parse(inputs.assistantGroup.assistantWithTools);
26
26
 
27
- expect(serializeParseResult(result)).toEqual(outputs.assistantWithTools);
27
+ expect(serializeParseResult(result)).toEqual(outputs.assistantGroup.assistantWithTools);
28
28
  });
29
29
 
30
30
  it('should include follow-up messages after assistant chain', () => {
@@ -99,11 +99,11 @@ describe('parse', () => {
99
99
  });
100
100
  });
101
101
 
102
- describe('Complex Scenarios', () => {
103
- it('should handle complex mixed scenarios correctly', () => {
104
- const result = parse(inputs.complexScenario);
102
+ describe('Assistant Group Scenarios', () => {
103
+ it('should handle tools with assistant branches correctly', () => {
104
+ const result = parse(inputs.assistantGroup.toolsWithBranches);
105
105
 
106
- expect(serializeParseResult(result)).toEqual(outputs.complexScenario);
106
+ expect(serializeParseResult(result)).toEqual(outputs.assistantGroup.toolsWithBranches);
107
107
  });
108
108
  });
109
109
 
@@ -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[]) => {
@@ -75,6 +75,7 @@ const MainChatItem = memo<ThreadChatItemProps>(({ id, index }) => {
75
75
  endRender={showThread && <Thread id={id} placement={placement} />}
76
76
  id={id}
77
77
  index={index}
78
+ isLatestItem={isLatestItem}
78
79
  />
79
80
  {isLatestItem && <SupervisorThinkingTag />}
80
81
  </>
@@ -4,6 +4,7 @@ import { Icon } from '@lobehub/ui';
4
4
  import { Popover, Tooltip } from 'antd';
5
5
  import { createStyles, useTheme } from 'antd-style';
6
6
  import debug from 'debug';
7
+ import isEqual from 'fast-deep-equal';
7
8
  import { ChevronDown, ChevronUp } from 'lucide-react';
8
9
  import { markdownToTxt } from 'markdown-to-txt';
9
10
  import { memo, useCallback, useMemo, useState, useSyncExternalStore } from 'react';
@@ -11,13 +12,13 @@ import { useTranslation } from 'react-i18next';
11
12
  import { Flexbox } from 'react-layout-kit';
12
13
 
13
14
  import {
14
- getVirtuosoActiveIndex,
15
- getVirtuosoGlobalRef,
16
- subscribeVirtuosoActiveIndex,
17
- subscribeVirtuosoGlobalRef,
15
+ getVirtuaActiveIndex,
16
+ getVirtuaGlobalRef,
17
+ subscribeVirtuaActiveIndex,
18
+ subscribeVirtuaGlobalRef,
18
19
  } from '@/features/Conversation/components/VirtualizedList/VirtuosoContext';
19
20
  import { useChatStore } from '@/store/chat';
20
- import { chatSelectors } from '@/store/chat/selectors';
21
+ import { displayMessageSelectors } from '@/store/chat/selectors';
21
22
 
22
23
  const log = debug('lobe-react:chat-minimap');
23
24
 
@@ -194,21 +195,17 @@ interface MinimapIndicator {
194
195
  width: number;
195
196
  }
196
197
 
197
- const ChatMinimap = () => {
198
+ const ChatMinimap = memo(() => {
198
199
  const { t } = useTranslation('chat');
199
200
  const { styles, cx } = useStyles();
200
201
  const [isHovered, setIsHovered] = useState(false);
201
- const virtuosoRef = useSyncExternalStore(
202
- subscribeVirtuosoGlobalRef,
203
- getVirtuosoGlobalRef,
204
- () => null,
205
- );
202
+ const virtuaRef = useSyncExternalStore(subscribeVirtuaGlobalRef, getVirtuaGlobalRef, () => null);
206
203
  const activeIndex = useSyncExternalStore(
207
- subscribeVirtuosoActiveIndex,
208
- getVirtuosoActiveIndex,
204
+ subscribeVirtuaActiveIndex,
205
+ getVirtuaActiveIndex,
209
206
  () => null,
210
207
  );
211
- const messages = useChatStore(chatSelectors.mainDisplayChats);
208
+ const messages = useChatStore(displayMessageSelectors.mainAIChats, isEqual);
212
209
 
213
210
  const theme = useTheme();
214
211
 
@@ -247,20 +244,19 @@ const ChatMinimap = () => {
247
244
 
248
245
  const handleJump = useCallback(
249
246
  (virtIndex: number) => {
250
- virtuosoRef?.current?.scrollToIndex({
247
+ virtuaRef?.current?.scrollToIndex(virtIndex, {
251
248
  align: 'start',
252
- behavior: 'smooth',
253
- index: virtIndex,
254
249
  // The current index detection will be off by 1, so we need to add 1 here
255
250
  offset: 6,
251
+ smooth: true,
256
252
  });
257
253
  },
258
- [virtuosoRef],
254
+ [virtuaRef],
259
255
  );
260
256
 
261
257
  const handleStep = useCallback(
262
258
  (direction: 'prev' | 'next') => {
263
- const ref = virtuosoRef?.current;
259
+ const ref = virtuaRef?.current;
264
260
  if (!ref || indicators.length === 0) return;
265
261
 
266
262
  let targetPosition: number;
@@ -303,14 +299,13 @@ const ChatMinimap = () => {
303
299
 
304
300
  if (!targetIndicator) return;
305
301
 
306
- ref.scrollToIndex({
302
+ ref.scrollToIndex(targetIndicator.virtuosoIndex, {
307
303
  align: 'start',
308
- behavior: 'smooth',
309
- index: targetIndicator.virtuosoIndex,
310
304
  offset: 6,
305
+ smooth: true,
311
306
  });
312
307
  },
313
- [activeIndex, activeIndicatorPosition, indicators, virtuosoRef],
308
+ [activeIndex, activeIndicatorPosition, indicators, virtuaRef],
314
309
  );
315
310
 
316
311
  if (indicators.length <= MIN_MESSAGES) return null;
@@ -353,9 +348,7 @@ const ChatMinimap = () => {
353
348
  aria-label={t('minimap.jumpToMessage', { index: position + 1 })}
354
349
  className={styles.indicator}
355
350
  onClick={() => handleJump(virtuosoIndex)}
356
- style={{
357
- width,
358
- }}
351
+ style={{ width }}
359
352
  type={'button'}
360
353
  >
361
354
  <div
@@ -382,6 +375,6 @@ const ChatMinimap = () => {
382
375
  </Flexbox>
383
376
  </Flexbox>
384
377
  );
385
- };
378
+ });
386
379
 
387
- export default memo(ChatMinimap);
380
+ export default ChatMinimap;
@@ -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
 
@@ -86,6 +86,10 @@ export const useStyles = createStyles(
86
86
  padding-block: 24px 12px;
87
87
  padding-inline: 12px;
88
88
 
89
+ @supports (content-visibility: auto) {
90
+ contain-intrinsic-size: auto 100lvh;
91
+ }
92
+
89
93
  time {
90
94
  display: inline-block;
91
95
  white-space: nowrap;
@@ -6,7 +6,7 @@ import { memo, use, useCallback, useContext, useMemo, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import ShareMessageModal from '@/features/Conversation/components/ShareMessageModal';
9
- import { VirtuosoContext } from '@/features/Conversation/components/VirtualizedList/VirtuosoContext';
9
+ import { VirtuaContext } from '@/features/Conversation/components/VirtualizedList/VirtuosoContext';
10
10
  import { useChatStore } from '@/store/chat';
11
11
  import { messageStateSelectors, threadSelectors } from '@/store/chat/selectors';
12
12
  import { useSessionStore } from '@/store/session';
@@ -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,9 +87,10 @@ 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
- const virtuosoRef = use(VirtuosoContext);
93
+ const virtuaRef = use(VirtuaContext);
89
94
 
90
95
  const onActionClick = useCallback(
91
96
  async (action: ActionIconGroupEvent) => {
@@ -93,7 +98,7 @@ export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, inde
93
98
  case 'edit': {
94
99
  toggleMessageEditing(id, true);
95
100
 
96
- virtuosoRef?.current?.scrollIntoView({ align: 'start', behavior: 'auto', index });
101
+ virtuaRef?.current?.scrollToIndex(index, { align: 'start' });
97
102
  }
98
103
  }
99
104
  if (!data) return;
@@ -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} />