@lobehub/lobehub 2.0.0-next.44 → 2.0.0-next.46

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 (29) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/model-runtime/src/providers/azureai/index.ts +34 -2
  5. package/packages/utils/src/server/__tests__/response.test.ts +79 -0
  6. package/packages/utils/src/server/index.ts +1 -0
  7. package/packages/utils/src/server/response.ts +110 -0
  8. package/src/app/(backend)/webapi/stt/openai/route.ts +0 -2
  9. package/src/app/(backend)/webapi/tts/edge/route.ts +8 -2
  10. package/src/app/(backend)/webapi/tts/microsoft/route.ts +8 -2
  11. package/src/app/(backend)/webapi/tts/openai/route.ts +15 -3
  12. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/ChatItem/Thread.tsx +1 -1
  13. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/ChatItem/index.tsx +9 -16
  14. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +3 -5
  15. package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -3
  16. package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +8 -5
  17. package/src/features/Conversation/Messages/Assistant/index.tsx +29 -15
  18. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +3 -5
  19. package/src/features/Conversation/Messages/Group/index.tsx +12 -20
  20. package/src/features/Conversation/Messages/Supervisor/index.tsx +14 -5
  21. package/src/features/Conversation/Messages/User/index.tsx +14 -8
  22. package/src/features/Conversation/Messages/index.tsx +16 -26
  23. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +7 -6
  24. package/src/features/Conversation/components/Extras/Usage/UsageDetail/tokens.ts +2 -5
  25. package/src/features/Conversation/components/Extras/Usage/index.tsx +13 -6
  26. package/src/server/modules/ContentChunk/index.test.ts +372 -0
  27. package/src/server/utils/createSpeechResponse.ts +55 -0
  28. package/src/app/(backend)/webapi/chat/azureai/route.test.ts +0 -25
  29. package/src/app/(backend)/webapi/chat/azureai/route.ts +0 -6
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.46](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.45...v2.0.0-next.46)
6
+
7
+ <sup>Released on **2025-11-11**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Fix thread display.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: Fix thread display, closes [#10153](https://github.com/lobehub/lobe-chat/issues/10153) ([8fda83e](https://github.com/lobehub/lobe-chat/commit/8fda83e))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 2.0.0-next.45](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.44...v2.0.0-next.45)
31
+
32
+ <sup>Released on **2025-11-10**</sup>
33
+
34
+ #### ♻ Code Refactoring
35
+
36
+ - **misc**: Edge to node runtime.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### Code refactoring
44
+
45
+ - **misc**: Edge to node runtime, closes [#10149](https://github.com/lobehub/lobe-chat/issues/10149) ([2f4c25d](https://github.com/lobehub/lobe-chat/commit/2f4c25d))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ## [Version 2.0.0-next.44](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.43...v2.0.0-next.44)
6
56
 
7
57
  <sup>Released on **2025-11-10**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Fix thread display."
6
+ ]
7
+ },
8
+ "date": "2025-11-11",
9
+ "version": "2.0.0-next.46"
10
+ },
11
+ {
12
+ "children": {
13
+ "improvements": [
14
+ "Edge to node runtime."
15
+ ]
16
+ },
17
+ "date": "2025-11-10",
18
+ "version": "2.0.0-next.45"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.44",
3
+ "version": "2.0.0-next.46",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -1,6 +1,7 @@
1
1
  import createClient, { ModelClient } from '@azure-rest/ai-inference';
2
2
  import { AzureKeyCredential } from '@azure/core-auth';
3
3
  import { ModelProvider } from 'model-bank';
4
+ import type { Readable as NodeReadable } from 'node:stream';
4
5
  import OpenAI from 'openai';
5
6
 
6
7
  import { systemToUserModels } from '../../const/models';
@@ -64,9 +65,40 @@ export class LobeAzureAI implements LobeRuntimeAI {
64
65
  });
65
66
 
66
67
  if (enableStreaming) {
67
- const stream = await response.asBrowserStream();
68
+ const unifiedStream = await (async () => {
69
+ if (typeof window === 'undefined') {
70
+ /**
71
+ * In Node.js the SDK exposes a Node readable stream, so we convert it to a Web ReadableStream
72
+ * to reuse the same streaming pipeline used by Edge/browser runtimes.
73
+ */
74
+ const streamModule = await import('node:stream');
75
+ const Readable = streamModule.Readable ?? streamModule.default.Readable;
76
+
77
+ if (!Readable) throw new Error('node:stream module missing Readable export');
78
+ if (typeof Readable.toWeb !== 'function')
79
+ throw new Error('Readable.toWeb is not a function');
80
+
81
+ const nodeResponse = await response.asNodeStream();
82
+ const nodeStream = nodeResponse.body;
83
+
84
+ if (!nodeStream) {
85
+ throw new Error('Azure AI response body is empty');
86
+ }
87
+
88
+ return Readable.toWeb(nodeStream as unknown as NodeReadable) as ReadableStream;
89
+ }
90
+
91
+ const browserResponse = await response.asBrowserStream();
92
+ const browserStream = browserResponse.body;
93
+
94
+ if (!browserStream) {
95
+ throw new Error('Azure AI response body is empty');
96
+ }
97
+
98
+ return browserStream;
99
+ })();
68
100
 
69
- const [prod, debug] = stream.body!.tee();
101
+ const [prod, debug] = unifiedStream.tee();
70
102
 
71
103
  if (process.env.DEBUG_AZURE_AI_CHAT_COMPLETION === '1') {
72
104
  debugStream(debug).catch(console.error);
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createNodeResponse } from '../response';
4
+
5
+ describe('createNodeResponse', () => {
6
+ it('wraps successful Response with default headers', async () => {
7
+ const upstream = new Response('audio-chunk', {
8
+ headers: {
9
+ 'x-source': 'sdk',
10
+ },
11
+ status: 201,
12
+ statusText: 'Created',
13
+ });
14
+ upstream.headers.delete('content-type');
15
+
16
+ const result = await createNodeResponse(() => Promise.resolve(upstream), {
17
+ success: {
18
+ cacheControl: 'no-store',
19
+ defaultContentType: 'audio/mpeg',
20
+ },
21
+ });
22
+
23
+ expect(await result.text()).toBe('audio-chunk');
24
+ expect(result.status).toBe(201);
25
+ expect(result.headers.get('x-source')).toBe('sdk');
26
+ expect(result.headers.get('content-type')).toBe('audio/mpeg');
27
+ expect(result.headers.get('cache-control')).toBe('no-store');
28
+ });
29
+
30
+ it('delegates to onInvalidResponse when payload is not Response-like', async () => {
31
+ const fallback = new Response('invalid', { status: 500 });
32
+
33
+ const result = await createNodeResponse(() => Promise.resolve({} as any), {
34
+ onInvalidResponse: () => fallback,
35
+ });
36
+
37
+ expect(result).toBe(fallback);
38
+ });
39
+
40
+ it('normalizes thrown Response-like errors via error options', async () => {
41
+ const upstreamError = new Response(JSON.stringify({ error: 'boom' }), {
42
+ status: 429,
43
+ statusText: 'Too Many Requests',
44
+ });
45
+ upstreamError.headers.delete('content-type');
46
+
47
+ const result = await createNodeResponse(
48
+ async () => {
49
+ throw upstreamError;
50
+ },
51
+ {
52
+ error: {
53
+ cacheControl: 'no-store',
54
+ defaultContentType: 'application/json',
55
+ },
56
+ },
57
+ );
58
+
59
+ expect(result.status).toBe(429);
60
+ expect(result.headers.get('content-type')).toBe('application/json');
61
+ expect(result.headers.get('cache-control')).toBe('no-store');
62
+ expect(await result.json()).toEqual({ error: 'boom' });
63
+ });
64
+
65
+ it('delegates to onNonResponseError for unexpected exceptions', async () => {
66
+ const fallback = new Response('fallback', { status: 500 });
67
+
68
+ const result = await createNodeResponse(
69
+ async () => {
70
+ throw new Error('unexpected');
71
+ },
72
+ {
73
+ onNonResponseError: () => fallback,
74
+ },
75
+ );
76
+
77
+ expect(result).toBe(fallback);
78
+ });
79
+ });
@@ -1,5 +1,6 @@
1
1
  export * from './auth';
2
2
  export * from './correctOIDCUrl';
3
3
  export * from './geo';
4
+ export * from './response';
4
5
  export * from './responsive';
5
6
  export * from './xor';
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Options for normalizing a Response so it can be consumed by the platform runtime.
3
+ */
4
+ export interface EnsureNodeResponseOptions {
5
+ /**
6
+ * Force update the cache-control header, usually to disable caching for APIs.
7
+ */
8
+ cacheControl?: string;
9
+ /**
10
+ * Sets a default content-type header when the original Response omitted it.
11
+ */
12
+ defaultContentType?: string;
13
+ /**
14
+ * Force buffering even if a readable body stream exists.
15
+ */
16
+ forceBuffering?: boolean;
17
+ }
18
+
19
+ /**
20
+ * Checks whether a value structurally matches the minimal Response interface.
21
+ */
22
+ export const isResponseLike = (value: unknown): value is Response => {
23
+ if (typeof value !== 'object' || value === null) return false;
24
+
25
+ const candidate = value as Partial<Response>;
26
+
27
+ return (
28
+ typeof candidate.arrayBuffer === 'function' &&
29
+ !!candidate.headers &&
30
+ typeof (candidate.headers as Headers).get === 'function' &&
31
+ typeof candidate.status === 'number' &&
32
+ typeof candidate.statusText === 'string'
33
+ );
34
+ };
35
+
36
+ /**
37
+ * Re-wraps an arbitrary Response-like object into the platform Response implementation.
38
+ *
39
+ * This is required because some SDKs (e.g., OpenAI) ship their own Response shim
40
+ * that is not recognized by Next.js when running in the Node.js runtime.
41
+ */
42
+ export const ensureNodeResponse = async (
43
+ source: Response,
44
+ options: EnsureNodeResponseOptions = {},
45
+ ) => {
46
+ const headers = new Headers(source.headers);
47
+
48
+ if (options.defaultContentType && !headers.has('content-type')) {
49
+ headers.set('content-type', options.defaultContentType);
50
+ }
51
+
52
+ if (options.cacheControl) {
53
+ headers.set('cache-control', options.cacheControl);
54
+ }
55
+
56
+ const body = !options.forceBuffering && source.body ? source.body : await source.arrayBuffer();
57
+
58
+ return new Response(body, {
59
+ headers,
60
+ status: source.status,
61
+ statusText: source.statusText,
62
+ });
63
+ };
64
+
65
+ export interface CreateNodeResponseOptions {
66
+ /**
67
+ * Options applied when a Response-like error is thrown.
68
+ */
69
+ error?: EnsureNodeResponseOptions;
70
+ /**
71
+ * Callback when the resolved value is not Response-like.
72
+ */
73
+ onInvalidResponse?: (payload: unknown) => Response;
74
+ /**
75
+ * Callback when a non-Response error is thrown.
76
+ */
77
+ onNonResponseError?: (error: unknown) => Response;
78
+ /**
79
+ * Options applied when the resolved Response is normalized.
80
+ */
81
+ success?: EnsureNodeResponseOptions;
82
+ }
83
+
84
+ /**
85
+ * Runs a response factory and ensures every exit path returns a platform Response.
86
+ */
87
+ export const createNodeResponse = async <T>(
88
+ responseCreator: () => Promise<T>,
89
+ options: CreateNodeResponseOptions = {},
90
+ ) => {
91
+ try {
92
+ const response = await responseCreator();
93
+
94
+ if (!isResponseLike(response)) {
95
+ if (options.onInvalidResponse) return options.onInvalidResponse(response);
96
+
97
+ throw new Error('Expected a Response-like object from responseCreator.');
98
+ }
99
+
100
+ return ensureNodeResponse(response, options.success);
101
+ } catch (error) {
102
+ if (isResponseLike(error)) {
103
+ return ensureNodeResponse(error, options.error);
104
+ }
105
+
106
+ if (options.onNonResponseError) return options.onNonResponseError(error);
107
+
108
+ throw error;
109
+ }
110
+ };
@@ -3,8 +3,6 @@ import { createOpenaiAudioTranscriptions } from '@lobehub/tts/server';
3
3
 
4
4
  import { createBizOpenAI } from '@/app/(backend)/_deprecated/createBizOpenAI';
5
5
 
6
- export const runtime = 'edge';
7
-
8
6
  export const preferredRegion = [
9
7
  'arn1',
10
8
  'bom1',
@@ -1,9 +1,15 @@
1
1
  import { EdgeSpeechPayload, EdgeSpeechTTS } from '@lobehub/tts';
2
2
 
3
- export const runtime = 'edge';
3
+ import { createSpeechResponse } from '@/server/utils/createSpeechResponse';
4
4
 
5
5
  export const POST = async (req: Request) => {
6
6
  const payload = (await req.json()) as EdgeSpeechPayload;
7
7
 
8
- return await EdgeSpeechTTS.createRequest({ payload });
8
+ return createSpeechResponse(() => EdgeSpeechTTS.createRequest({ payload }), {
9
+ logTag: 'webapi/tts/edge',
10
+ messages: {
11
+ failure: 'Failed to synthesize speech',
12
+ invalid: 'Unexpected payload from Edge speech API',
13
+ },
14
+ });
9
15
  };
@@ -1,9 +1,15 @@
1
1
  import { MicrosoftSpeechPayload, MicrosoftSpeechTTS } from '@lobehub/tts';
2
2
 
3
- export const runtime = 'edge';
3
+ import { createSpeechResponse } from '@/server/utils/createSpeechResponse';
4
4
 
5
5
  export const POST = async (req: Request) => {
6
6
  const payload = (await req.json()) as MicrosoftSpeechPayload;
7
7
 
8
- return await MicrosoftSpeechTTS.createRequest({ payload });
8
+ return createSpeechResponse(() => MicrosoftSpeechTTS.createRequest({ payload }), {
9
+ logTag: 'webapi/tts/microsoft',
10
+ messages: {
11
+ failure: 'Failed to synthesize speech',
12
+ invalid: 'Unexpected payload from Microsoft speech API',
13
+ },
14
+ });
9
15
  };
@@ -2,8 +2,7 @@ import { OpenAITTSPayload } from '@lobehub/tts';
2
2
  import { createOpenaiAudioSpeech } from '@lobehub/tts/server';
3
3
 
4
4
  import { createBizOpenAI } from '@/app/(backend)/_deprecated/createBizOpenAI';
5
-
6
- export const runtime = 'edge';
5
+ import { createSpeechResponse } from '@/server/utils/createSpeechResponse';
7
6
 
8
7
  export const preferredRegion = [
9
8
  'arn1',
@@ -34,5 +33,18 @@ export const POST = async (req: Request) => {
34
33
  // if resOrOpenAI is a Response, it means there is an error,just return it
35
34
  if (openaiOrErrResponse instanceof Response) return openaiOrErrResponse;
36
35
 
37
- return await createOpenaiAudioSpeech({ openai: openaiOrErrResponse as any, payload });
36
+ return createSpeechResponse(
37
+ () =>
38
+ createOpenaiAudioSpeech({
39
+ openai: openaiOrErrResponse as any,
40
+ payload,
41
+ }),
42
+ {
43
+ logTag: 'webapi/tts/openai',
44
+ messages: {
45
+ failure: 'Failed to synthesize speech',
46
+ invalid: 'Unexpected payload from OpenAI TTS',
47
+ },
48
+ },
49
+ );
38
50
  };
@@ -39,7 +39,7 @@ const Thread = memo<ThreadProps>(({ id, placement, style }) => {
39
39
  direction={placement === 'end' ? 'horizontal-reverse' : 'horizontal'}
40
40
  gap={12}
41
41
  paddingInline={16}
42
- style={{ paddingBottom: 16, ...style }}
42
+ style={{ marginTop: -12, paddingBottom: 16, ...style }}
43
43
  >
44
44
  <div style={{ width: 40 }} />
45
45
  <Flexbox className={styles.container} gap={4} padding={4} style={{ width: 'fit-content' }}>
@@ -1,13 +1,13 @@
1
1
  import { createStyles } from 'antd-style';
2
2
  import React, { memo } from 'react';
3
3
 
4
- import SupervisorThinkingTag from '@/app/[variants]/(main)/chat/components/conversation/features/ChatList/ChatItem/OrchestratorThinking';
5
4
  import { ChatItem } from '@/features/Conversation';
6
5
  import { useAgentStore } from '@/store/agent';
7
6
  import { agentChatConfigSelectors } from '@/store/agent/selectors';
8
7
  import { useChatStore } from '@/store/chat';
9
- import { chatSelectors, threadSelectors } from '@/store/chat/selectors';
8
+ import { displayMessageSelectors, threadSelectors } from '@/store/chat/selectors';
10
9
 
10
+ import SupervisorThinkingTag from './OrchestratorThinking';
11
11
  import Thread from './Thread';
12
12
 
13
13
  const useStyles = createStyles(({ css, token, isDarkMode }) => {
@@ -26,15 +26,16 @@ const useStyles = createStyles(({ css, token, isDarkMode }) => {
26
26
  content: '';
27
27
 
28
28
  position: absolute;
29
- inset-block: 56px 50px;
29
+ inset-block-end: 60px;
30
30
 
31
- width: 32px;
31
+ width: 38px;
32
+ height: 53px;
32
33
  border-block-end: 2px solid ${borderColor};
33
34
  }
34
35
  `,
35
36
  start: css`
36
37
  &::after {
37
- inset-inline-start: 36px;
38
+ inset-inline-start: 30px;
38
39
  border-inline-start: 2px solid ${borderColor};
39
40
  border-end-start-radius: 8px;
40
41
  }
@@ -52,7 +53,7 @@ const MainChatItem = memo<ThreadChatItemProps>(({ id, index }) => {
52
53
 
53
54
  const [showThread, historyLength] = useChatStore((s) => [
54
55
  threadSelectors.hasThreadBySourceMsgId(id)(s),
55
- chatSelectors.mainDisplayChatIDs(s).length,
56
+ displayMessageSelectors.mainDisplayChatIDs(s).length,
56
57
  ]);
57
58
 
58
59
  const [displayMode, enableHistoryDivider] = useAgentStore((s) => [
@@ -60,7 +61,7 @@ const MainChatItem = memo<ThreadChatItemProps>(({ id, index }) => {
60
61
  agentChatConfigSelectors.enableHistoryDivider(historyLength, index)(s),
61
62
  ]);
62
63
 
63
- const userRole = useChatStore((s) => chatSelectors.getMessageById(id)(s)?.role);
64
+ const userRole = useChatStore((s) => displayMessageSelectors.getDisplayMessageById(id)(s)?.role);
64
65
 
65
66
  const placement = displayMode === 'chat' && userRole === 'user' ? 'end' : 'start';
66
67
 
@@ -71,15 +72,7 @@ const MainChatItem = memo<ThreadChatItemProps>(({ id, index }) => {
71
72
  <ChatItem
72
73
  className={showThread ? cx(styles.line, styles[placement]) : ''}
73
74
  enableHistoryDivider={enableHistoryDivider}
74
- endRender={
75
- showThread && (
76
- <Thread
77
- id={id}
78
- placement={placement}
79
- style={{ marginTop: displayMode === 'docs' ? 12 : undefined }}
80
- />
81
- )
82
- }
75
+ endRender={showThread && <Thread id={id} placement={placement} />}
83
76
  id={id}
84
77
  index={index}
85
78
  />
@@ -53,11 +53,9 @@ export const AssistantActionsBar = memo<AssistantActionsProps>(({ id, data, inde
53
53
  const items = useMemo(() => {
54
54
  if (hasTools) return [delAndRegenerate, copy];
55
55
 
56
- return [
57
- edit,
58
- copy,
59
- // inThread || isGroupSession ? null : branching
60
- ].filter(Boolean) as ActionIconGroupItemType[];
56
+ return [edit, copy, inThread || isGroupSession ? null : branching].filter(
57
+ Boolean,
58
+ ) as ActionIconGroupItemType[];
61
59
  }, [inThread, hasTools, isGroupSession, delAndRegenerate, copy, edit, branching]);
62
60
 
63
61
  const { t } = useTranslation('common');
@@ -24,7 +24,7 @@ vi.mock('@/store/chat', () => ({
24
24
  useChatStore: vi.fn(),
25
25
  }));
26
26
 
27
- const mockData: UIChatMessage = {
27
+ const mockData = {
28
28
  content: 'test-content',
29
29
  createdAt: 0,
30
30
  id: 'abc',
@@ -53,8 +53,8 @@ describe('AssistantMessageExtra', () => {
53
53
  expect(screen.queryByText('Translate Component')).toBeNull();
54
54
  });
55
55
 
56
- it('should render Usage component if extra.model exists', async () => {
57
- render(<AssistantMessageExtra {...mockData} extra={{ model: 'gpt-4', provider: 'openai' }} />);
56
+ it('should render Usage component if model prop exists', async () => {
57
+ render(<AssistantMessageExtra {...mockData} model="gpt-4" provider="openai" />);
58
58
 
59
59
  expect(screen.getByText('Usage Component')).toBeInTheDocument();
60
60
  });
@@ -1,4 +1,4 @@
1
- import { type MessageMetadata } from '@lobechat/types';
1
+ import { ModelPerformance, ModelUsage } from '@lobechat/types';
2
2
  import { memo } from 'react';
3
3
  import { Flexbox } from 'react-layout-kit';
4
4
 
@@ -14,18 +14,21 @@ interface AssistantMessageExtraProps {
14
14
  content: string;
15
15
  extra?: any;
16
16
  id: string;
17
- metadata?: MessageMetadata | null;
17
+ model?: string;
18
+ performance?: ModelPerformance;
19
+ provider?: string;
18
20
  tools?: any[];
21
+ usage?: ModelUsage;
19
22
  }
20
23
 
21
24
  export const AssistantMessageExtra = memo<AssistantMessageExtraProps>(
22
- ({ extra, id, content, metadata, tools }) => {
25
+ ({ extra, id, content, performance, usage, tools, provider, model }) => {
23
26
  const loading = useChatStore(messageStateSelectors.isMessageGenerating(id));
24
27
 
25
28
  return (
26
29
  <Flexbox gap={8} style={{ marginTop: !!tools?.length ? 8 : 4 }}>
27
- {content !== LOADING_FLAT && extra?.model && (
28
- <Usage metadata={metadata || {}} model={extra?.model} provider={extra.provider!} />
30
+ {content !== LOADING_FLAT && model && (
31
+ <Usage model={model} performance={performance} provider={provider!} usage={usage} />
29
32
  )}
30
33
  <>
31
34
  {!!extra?.tts && (
@@ -20,7 +20,7 @@ import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
20
20
  import { useAgentStore } from '@/store/agent';
21
21
  import { agentChatConfigSelectors } from '@/store/agent/selectors';
22
22
  import { useChatStore } from '@/store/chat';
23
- import { messageStateSelectors } from '@/store/chat/slices/message/selectors';
23
+ import { displayMessageSelectors, messageStateSelectors } from '@/store/chat/selectors';
24
24
  import { chatGroupSelectors, useChatGroupStore } from '@/store/chatGroup';
25
25
  import { useGlobalStore } from '@/store/global';
26
26
  import { useSessionStore } from '@/store/session';
@@ -48,28 +48,38 @@ const isHtmlCode = (content: string, language: string) => {
48
48
  };
49
49
  const MOBILE_AVATAR_SIZE = 32;
50
50
 
51
- interface AssistantMessageProps extends UIChatMessage {
51
+ interface AssistantMessageProps {
52
52
  disableEditing?: boolean;
53
+ id: string;
53
54
  index: number;
54
- showTitle?: boolean;
55
55
  }
56
- const AssistantMessage = memo<AssistantMessageProps>((props) => {
56
+
57
+ const AssistantMessage = memo<AssistantMessageProps>(({ id, index, disableEditing }) => {
58
+ const item = useChatStore(
59
+ displayMessageSelectors.getDisplayMessageById(id),
60
+ isEqual,
61
+ ) as UIChatMessage;
62
+
57
63
  const {
58
64
  error,
59
- showTitle,
60
- id,
61
65
  role,
62
66
  search,
63
- disableEditing,
64
- index,
65
67
  content,
66
68
  createdAt,
67
69
  tools,
68
70
  extra,
69
- metadata,
71
+ model,
72
+ provider,
70
73
  meta,
71
74
  targetId,
72
- } = props;
75
+ groupId,
76
+ performance,
77
+ usage,
78
+ metadata,
79
+ } = item;
80
+
81
+ const showTitle = !!groupId;
82
+
73
83
  const avatar = meta;
74
84
  const { t } = useTranslation('chat');
75
85
  const { mobile } = useResponsive();
@@ -199,11 +209,12 @@ const AssistantMessage = memo<AssistantMessageProps>((props) => {
199
209
 
200
210
  const renderMessage = useCallback(
201
211
  (editableContent: ReactNode) => (
202
- <AssistantMessageContent {...props} editableContent={editableContent} />
212
+ <AssistantMessageContent {...item} editableContent={editableContent} />
203
213
  ),
204
- [props],
214
+ [item],
205
215
  );
206
- const errorMessage = <ErrorMessageExtra data={props} />;
216
+ const errorMessage = <ErrorMessageExtra data={item} />;
217
+
207
218
  return (
208
219
  <Flexbox className={styles.container} gap={mobile ? 6 : 12}>
209
220
  <Flexbox gap={4} horizontal>
@@ -254,8 +265,11 @@ const AssistantMessage = memo<AssistantMessageProps>((props) => {
254
265
  content={content}
255
266
  extra={extra}
256
267
  id={id}
257
- metadata={metadata}
268
+ model={model!}
269
+ performance={performance! || metadata}
270
+ provider={provider!}
258
271
  tools={tools}
272
+ usage={usage! || metadata}
259
273
  />
260
274
  </>
261
275
  }
@@ -268,7 +282,7 @@ const AssistantMessage = memo<AssistantMessageProps>((props) => {
268
282
  </Flexbox>
269
283
  {!disableEditing && !editing && (
270
284
  <Flexbox align={'flex-start'} className={styles.actions} role="menubar">
271
- <AssistantActionsBar data={props} id={id} index={index} />
285
+ <AssistantActionsBar data={item} id={id} index={index} />
272
286
  </Flexbox>
273
287
  )}
274
288
  </Flexbox>
@@ -46,11 +46,9 @@ const WithContentId = memo<GroupActionsProps>(({ id, data, index, contentBlock }
46
46
  const items = useMemo(() => {
47
47
  if (hasTools) return [delAndRegenerate, copy];
48
48
 
49
- return [
50
- edit,
51
- copy,
52
- // inThread || isGroupSession ? null : branching
53
- ].filter(Boolean) as ActionIconGroupItemType[];
49
+ return [edit, copy, inThread || isGroupSession ? null : branching].filter(
50
+ Boolean,
51
+ ) as ActionIconGroupItemType[];
54
52
  }, [inThread, hasTools, isGroupSession, delAndRegenerate, copy, edit, branching]);
55
53
 
56
54
  const { t } = useTranslation('common');