@lobehub/lobehub 2.0.0-next.327 → 2.0.0-next.329

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 (83) hide show
  1. package/.env.example +0 -3
  2. package/.env.example.development +0 -3
  3. package/CHANGELOG.md +58 -0
  4. package/Dockerfile +1 -2
  5. package/changelog/v1.json +18 -0
  6. package/docs/self-hosting/advanced/auth.mdx +5 -6
  7. package/docs/self-hosting/advanced/auth.zh-CN.mdx +5 -6
  8. package/docs/self-hosting/environment-variables/auth.mdx +0 -7
  9. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +0 -7
  10. package/locales/en-US/chat.json +6 -1
  11. package/locales/en-US/discover.json +1 -0
  12. package/locales/zh-CN/chat.json +5 -0
  13. package/locales/zh-CN/discover.json +1 -0
  14. package/package.json +1 -1
  15. package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
  16. package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
  17. package/packages/agent-runtime/src/types/instruction.ts +46 -2
  18. package/packages/builtin-tool-gtd/src/const.ts +1 -0
  19. package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
  20. package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
  21. package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
  22. package/packages/builtin-tool-gtd/src/types.ts +55 -33
  23. package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
  24. package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
  25. package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
  26. package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
  27. package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
  28. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
  29. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
  30. package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
  31. package/packages/database/src/models/message.ts +8 -1
  32. package/packages/database/src/models/thread.ts +1 -1
  33. package/packages/types/src/message/ui/chat.ts +2 -0
  34. package/packages/types/src/topic/thread.ts +20 -0
  35. package/scripts/prebuild.mts +2 -2
  36. package/src/app/[variants]/(main)/community/(list)/agent/features/List/Item.tsx +1 -0
  37. package/src/components/StreamingMarkdown/index.tsx +10 -43
  38. package/src/envs/__tests__/app.test.ts +81 -0
  39. package/src/envs/app.ts +14 -2
  40. package/src/envs/auth.test.ts +0 -13
  41. package/src/envs/auth.ts +0 -41
  42. package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
  43. package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
  44. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
  45. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
  46. package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
  47. package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
  48. package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
  49. package/src/features/Conversation/Messages/Task/index.tsx +11 -6
  50. package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
  51. package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
  52. package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
  53. package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
  54. package/src/features/Conversation/components/Thinking/index.tsx +9 -30
  55. package/src/features/Conversation/store/slices/data/action.ts +2 -3
  56. package/src/features/NavPanel/components/BackButton.tsx +10 -13
  57. package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
  58. package/src/hooks/useAutoScroll.ts +117 -0
  59. package/src/libs/better-auth/auth-client.ts +0 -9
  60. package/src/libs/better-auth/define-config.ts +13 -12
  61. package/src/libs/better-auth/sso/index.ts +2 -1
  62. package/src/libs/better-auth/utils/config.ts +2 -2
  63. package/src/libs/next/proxy/define-config.ts +4 -6
  64. package/src/locales/default/chat.ts +6 -1
  65. package/src/locales/default/discover.ts +2 -0
  66. package/src/server/routers/lambda/__tests__/integration/topic.integration.test.ts +74 -0
  67. package/src/server/routers/lambda/aiAgent.ts +239 -1
  68. package/src/server/routers/lambda/thread.ts +2 -0
  69. package/src/server/routers/lambda/topic.ts +6 -0
  70. package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +4 -1
  71. package/src/server/services/agentRuntime/AgentRuntimeService.ts +2 -1
  72. package/src/server/services/message/__tests__/index.test.ts +37 -0
  73. package/src/server/services/message/index.ts +6 -1
  74. package/src/services/aiAgent.ts +51 -0
  75. package/src/services/topic/index.ts +4 -0
  76. package/src/store/chat/agents/createAgentExecutors.ts +714 -12
  77. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
  78. package/src/store/chat/slices/message/actions/query.ts +33 -1
  79. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
  80. package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
  81. package/src/store/chat/slices/operation/types.ts +4 -0
  82. package/src/store/chat/slices/topic/action.test.ts +2 -1
  83. package/src/store/chat/slices/topic/action.ts +1 -1
@@ -32,7 +32,7 @@ export class ThreadModel {
32
32
  // @ts-ignore
33
33
  const [result] = await this.db
34
34
  .insert(threads)
35
- .values({ ...params, status: ThreadStatus.Active, userId: this.userId })
35
+ .values({ status: ThreadStatus.Active, ...params, userId: this.userId })
36
36
  .onConflictDoNothing()
37
37
  .returning();
38
38
 
@@ -64,6 +64,8 @@ interface UIMessageBranch {
64
64
  * Retrieved from the associated Thread via sourceMessageId
65
65
  */
66
66
  export interface TaskDetail {
67
+ /** Whether this task runs in client mode (local execution) */
68
+ clientMode?: boolean;
67
69
  /** Task completion time (ISO string) */
68
70
  completedAt?: string;
69
71
  /** Execution duration in milliseconds */
@@ -23,6 +23,8 @@ export enum ThreadStatus {
23
23
  * Metadata for Thread, used for agent task execution
24
24
  */
25
25
  export interface ThreadMetadata {
26
+ /** Whether this thread runs in client mode (local execution) */
27
+ clientMode?: boolean;
26
28
  /** Task completion time */
27
29
  completedAt?: string;
28
30
  /** Execution duration in milliseconds */
@@ -68,16 +70,34 @@ export interface CreateThreadParams {
68
70
  agentId?: string;
69
71
  /** Group ID for group chat context */
70
72
  groupId?: string;
73
+ /** Initial metadata for the thread */
74
+ metadata?: ThreadMetadata;
71
75
  parentThreadId?: string;
72
76
  sourceMessageId?: string;
77
+ /** Initial status (defaults to Active) */
78
+ status?: ThreadStatus;
73
79
  title?: string;
74
80
  topicId: string;
75
81
  type: IThreadType;
76
82
  }
77
83
 
84
+ export const threadMetadataSchema = z.object({
85
+ clientMode: z.boolean().optional(),
86
+ completedAt: z.string().optional(),
87
+ duration: z.number().optional(),
88
+ error: z.any().optional(),
89
+ operationId: z.string().optional(),
90
+ startedAt: z.string().optional(),
91
+ totalCost: z.number().optional(),
92
+ totalMessages: z.number().optional(),
93
+ totalTokens: z.number().optional(),
94
+ totalToolCalls: z.number().optional(),
95
+ });
96
+
78
97
  export const createThreadSchema = z.object({
79
98
  agentId: z.string().optional(),
80
99
  groupId: z.string().optional(),
100
+ metadata: threadMetadataSchema.optional(),
81
101
  parentThreadId: z.string().optional(),
82
102
  sourceMessageId: z.string().optional(),
83
103
  title: z.string().optional(),
@@ -51,10 +51,10 @@ const printEnvInfo = () => {
51
51
 
52
52
  // Auth-related env vars
53
53
  console.log('\n Auth Environment Variables:');
54
- console.log(` NEXT_PUBLIC_AUTH_URL: ${process.env.NEXT_PUBLIC_AUTH_URL ?? '(not set)'}`);
55
- console.log(` NEXTAUTH_URL: ${process.env.NEXTAUTH_URL ?? '(not set)'}`);
56
54
  console.log(` APP_URL: ${process.env.APP_URL ?? '(not set)'}`);
57
55
  console.log(` VERCEL_URL: ${process.env.VERCEL_URL ?? '(not set)'}`);
56
+ console.log(` VERCEL_BRANCH_URL: ${process.env.VERCEL_BRANCH_URL ?? '(not set)'}`);
57
+ console.log(` VERCEL_PROJECT_PRODUCTION_URL: ${process.env.VERCEL_PROJECT_PRODUCTION_URL ?? '(not set)'}`);
58
58
  console.log(` AUTH_EMAIL_VERIFICATION: ${process.env.AUTH_EMAIL_VERIFICATION ?? '(not set)'}`);
59
59
  console.log(` ENABLE_MAGIC_LINK: ${process.env.ENABLE_MAGIC_LINK ?? '(not set)'}`);
60
60
  console.log(` AUTH_SECRET: ${process.env.AUTH_SECRET ? '✓ set' : '✗ not set'}`);
@@ -139,6 +139,7 @@ const AssistantItem = memo<DiscoverAssistantItem>(
139
139
  horizontal
140
140
  justify={'space-between'}
141
141
  padding={16}
142
+ style={{ paddingRight: isGroupAgent ? 80 : 16 }}
142
143
  width={'100%'}
143
144
  >
144
145
  <Flexbox
@@ -2,7 +2,9 @@
2
2
 
3
3
  import { Markdown, ScrollShadow } from '@lobehub/ui';
4
4
  import { createStaticStyles } from 'antd-style';
5
- import { memo, useCallback, useEffect, useRef, useState } from 'react';
5
+ import { type RefObject, memo, useEffect } from 'react';
6
+
7
+ import { useAutoScroll } from '@/hooks/useAutoScroll';
6
8
 
7
9
  const styles = createStaticStyles(({ css }) => ({
8
10
  container: css`
@@ -19,51 +21,16 @@ interface StreamingMarkdownProps {
19
21
  }
20
22
 
21
23
  const StreamingMarkdown = memo<StreamingMarkdownProps>(({ children, maxHeight = 400 }) => {
22
- const containerRef = useRef<HTMLDivElement>(null);
23
- const [userHasScrolled, setUserHasScrolled] = useState(false);
24
- const isAutoScrollingRef = useRef(false);
25
-
26
- // Handle user scroll detection
27
- const handleScroll = useCallback(() => {
28
- // Ignore scroll events triggered by auto-scroll
29
- if (isAutoScrollingRef.current) return;
30
-
31
- const container = containerRef.current;
32
- if (!container) return;
33
-
34
- // Check if user scrolled away from bottom
35
- const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
36
- const isAtBottom = distanceToBottom < 20;
37
-
38
- // If user scrolled up, stop auto-scrolling
39
- if (!isAtBottom) {
40
- setUserHasScrolled(true);
41
- }
42
- }, []);
43
-
44
- // Auto scroll to bottom when content changes (unless user has scrolled)
45
- useEffect(() => {
46
- if (userHasScrolled) return;
47
-
48
- const container = containerRef.current;
49
- if (!container) return;
50
-
51
- isAutoScrollingRef.current = true;
52
- requestAnimationFrame(() => {
53
- container.scrollTop = container.scrollHeight;
54
- // Reset the flag after scroll completes
55
- requestAnimationFrame(() => {
56
- isAutoScrollingRef.current = false;
57
- });
58
- });
59
- }, [children, userHasScrolled]);
24
+ const { ref, handleScroll, resetScrollLock } = useAutoScroll<HTMLDivElement>({
25
+ deps: [children],
26
+ });
60
27
 
61
- // Reset userHasScrolled when content is cleared (new stream starts)
28
+ // Reset scroll lock when content is cleared (new stream starts)
62
29
  useEffect(() => {
63
30
  if (!children) {
64
- setUserHasScrolled(false);
31
+ resetScrollLock();
65
32
  }
66
- }, [children]);
33
+ }, [children, resetScrollLock]);
67
34
 
68
35
  if (!children) return null;
69
36
 
@@ -72,7 +39,7 @@ const StreamingMarkdown = memo<StreamingMarkdownProps>(({ children, maxHeight =
72
39
  className={styles.container}
73
40
  offset={12}
74
41
  onScroll={handleScroll}
75
- ref={containerRef}
42
+ ref={ref as RefObject<HTMLDivElement>}
76
43
  size={12}
77
44
  style={{ maxHeight }}
78
45
  >
@@ -82,3 +82,84 @@ describe('getServerConfig', () => {
82
82
  });
83
83
  });
84
84
  });
85
+
86
+ describe('APP_URL fallback', () => {
87
+ beforeEach(() => {
88
+ vi.resetModules();
89
+ // Clean up all related env vars
90
+ delete process.env.APP_URL;
91
+ delete process.env.VERCEL;
92
+ delete process.env.VERCEL_ENV;
93
+ delete process.env.VERCEL_URL;
94
+ delete process.env.VERCEL_BRANCH_URL;
95
+ delete process.env.VERCEL_PROJECT_PRODUCTION_URL;
96
+ });
97
+
98
+ it('should use APP_URL when explicitly set', async () => {
99
+ process.env.APP_URL = 'https://custom-app.com';
100
+ process.env.VERCEL = '1';
101
+
102
+ const { getAppConfig } = await import('../app');
103
+ const config = getAppConfig();
104
+ expect(config.APP_URL).toBe('https://custom-app.com');
105
+ });
106
+
107
+ describe('Vercel environment', () => {
108
+ it('should use VERCEL_PROJECT_PRODUCTION_URL in production', async () => {
109
+ process.env.VERCEL = '1';
110
+ process.env.VERCEL_ENV = 'production';
111
+ process.env.VERCEL_PROJECT_PRODUCTION_URL = 'lobechat.vercel.app';
112
+ process.env.VERCEL_BRANCH_URL = 'lobechat-git-main-org.vercel.app';
113
+ process.env.VERCEL_URL = 'lobechat-abc123.vercel.app';
114
+
115
+ const { getAppConfig } = await import('../app');
116
+ const config = getAppConfig();
117
+ expect(config.APP_URL).toBe('https://lobechat.vercel.app');
118
+ });
119
+
120
+ it('should use VERCEL_BRANCH_URL in preview environment', async () => {
121
+ process.env.VERCEL = '1';
122
+ process.env.VERCEL_ENV = 'preview';
123
+ process.env.VERCEL_BRANCH_URL = 'lobechat-git-feature-org.vercel.app';
124
+ process.env.VERCEL_URL = 'lobechat-abc123.vercel.app';
125
+
126
+ const { getAppConfig } = await import('../app');
127
+ const config = getAppConfig();
128
+ expect(config.APP_URL).toBe('https://lobechat-git-feature-org.vercel.app');
129
+ });
130
+
131
+ it('should fallback to VERCEL_URL when VERCEL_BRANCH_URL is not set', async () => {
132
+ process.env.VERCEL = '1';
133
+ process.env.VERCEL_ENV = 'preview';
134
+ process.env.VERCEL_URL = 'lobechat-abc123.vercel.app';
135
+
136
+ const { getAppConfig } = await import('../app');
137
+ const config = getAppConfig();
138
+ expect(config.APP_URL).toBe('https://lobechat-abc123.vercel.app');
139
+ });
140
+ });
141
+
142
+ describe('local environment', () => {
143
+ it('should use localhost:3010 in development', async () => {
144
+
145
+ vi.stubEnv('NODE_ENV', 'development');
146
+
147
+ const { getAppConfig } = await import('../app');
148
+ const config = getAppConfig();
149
+ expect(config.APP_URL).toBe('http://localhost:3010');
150
+
151
+
152
+ });
153
+
154
+ it('should use localhost:3210 in non-development', async () => {
155
+
156
+ vi.stubEnv('NODE_ENV', 'test');
157
+
158
+ const { getAppConfig } = await import('../app');
159
+ const config = getAppConfig();
160
+ expect(config.APP_URL).toBe('http://localhost:3210');
161
+
162
+
163
+ });
164
+ });
165
+ });
package/src/envs/app.ts CHANGED
@@ -12,12 +12,24 @@ declare global {
12
12
  }
13
13
  const isInVercel = process.env.VERCEL === '1';
14
14
 
15
- const vercelUrl = `https://${process.env.VERCEL_URL}`;
15
+ // Vercel URL fallback order (by stability):
16
+ // 1. VERCEL_PROJECT_PRODUCTION_URL - project level, most stable
17
+ // 2. VERCEL_BRANCH_URL - branch level, stable across deployments on same branch
18
+ // 3. VERCEL_URL - deployment level, changes every deployment
19
+ const getVercelUrl = () => {
20
+ if (process.env.VERCEL_ENV === 'production' && process.env.VERCEL_PROJECT_PRODUCTION_URL) {
21
+ return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
22
+ }
23
+ if (process.env.VERCEL_BRANCH_URL) {
24
+ return `https://${process.env.VERCEL_BRANCH_URL}`;
25
+ }
26
+ return `https://${process.env.VERCEL_URL}`;
27
+ };
16
28
 
17
29
  const APP_URL = process.env.APP_URL
18
30
  ? process.env.APP_URL
19
31
  : isInVercel
20
- ? vercelUrl
32
+ ? getVercelUrl()
21
33
  : process.env.NODE_ENV === 'development'
22
34
  ? 'http://localhost:3010'
23
35
  : 'http://localhost:3210';
@@ -44,17 +44,4 @@ describe('getAuthConfig fallbacks', () => {
44
44
 
45
45
  expect(config.AUTH_SECRET).toBe('nextauth-secret');
46
46
  });
47
-
48
- it('should fall back to NEXTAUTH_URL origin when NEXT_PUBLIC_AUTH_URL is empty string', () => {
49
- process.env.NEXT_PUBLIC_AUTH_URL = '';
50
- process.env.NEXTAUTH_URL = 'https://example.com/api/auth';
51
-
52
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
53
- // @ts-expect-error - allow overriding for test
54
- globalThis.window = undefined;
55
-
56
- const config = getAuthConfig();
57
-
58
- expect(config.NEXT_PUBLIC_AUTH_URL).toBe('https://example.com');
59
- });
60
47
  });
package/src/envs/auth.ts CHANGED
@@ -2,43 +2,6 @@
2
2
  import { createEnv } from '@t3-oss/env-nextjs';
3
3
  import { z } from 'zod';
4
4
 
5
- /**
6
- * Resolve public auth URL with compatibility fallbacks for NextAuth and Vercel deployments.
7
- */
8
- const resolvePublicAuthUrl = () => {
9
- if (process.env.NEXT_PUBLIC_AUTH_URL) return process.env.NEXT_PUBLIC_AUTH_URL;
10
-
11
- if (process.env.NEXTAUTH_URL) {
12
- try {
13
- return new URL(process.env.NEXTAUTH_URL).origin;
14
- } catch {
15
- // ignore invalid NEXTAUTH_URL
16
- }
17
- }
18
-
19
- if (process.env.APP_URL) {
20
- try {
21
- return new URL(process.env.APP_URL).origin;
22
- } catch {
23
- // ignore invalid APP_URL
24
- }
25
- }
26
-
27
- if (process.env.VERCEL_URL) {
28
- try {
29
- const normalizedVercelUrl = process.env.VERCEL_URL.startsWith('http')
30
- ? process.env.VERCEL_URL
31
- : `https://${process.env.VERCEL_URL}`;
32
-
33
- return new URL(normalizedVercelUrl).origin;
34
- } catch {
35
- // ignore invalid Vercel URL
36
- }
37
- }
38
-
39
- return undefined;
40
- };
41
-
42
5
  declare global {
43
6
  // eslint-disable-next-line @typescript-eslint/no-namespace
44
7
  namespace NodeJS {
@@ -50,7 +13,6 @@ declare global {
50
13
 
51
14
  // ===== Auth (shared by Better Auth / Next Auth) ===== //
52
15
  AUTH_SECRET?: string;
53
- NEXT_PUBLIC_AUTH_URL?: string;
54
16
  AUTH_EMAIL_VERIFICATION?: string;
55
17
  ENABLE_MAGIC_LINK?: string;
56
18
  AUTH_SSO_PROVIDERS?: string;
@@ -180,7 +142,6 @@ export const getAuthConfig = () => {
180
142
 
181
143
  // ---------------------------------- better auth ----------------------------------
182
144
  NEXT_PUBLIC_ENABLE_BETTER_AUTH: z.boolean().optional(),
183
- NEXT_PUBLIC_AUTH_URL: z.string().optional(),
184
145
 
185
146
  // ---------------------------------- next auth ----------------------------------
186
147
  NEXT_PUBLIC_ENABLE_NEXT_AUTH: z.boolean().optional(),
@@ -310,8 +271,6 @@ export const getAuthConfig = () => {
310
271
 
311
272
  // ---------------------------------- better auth ----------------------------------
312
273
  NEXT_PUBLIC_ENABLE_BETTER_AUTH: process.env.NEXT_PUBLIC_ENABLE_BETTER_AUTH === '1',
313
- // Fallback to NEXTAUTH_URL origin or Vercel deployment domain for seamless migration from next-auth
314
- NEXT_PUBLIC_AUTH_URL: resolvePublicAuthUrl(),
315
274
  // Fallback to NEXT_PUBLIC_* for seamless migration
316
275
  AUTH_EMAIL_VERIFICATION:
317
276
  process.env.AUTH_EMAIL_VERIFICATION === '1' ||
@@ -28,8 +28,6 @@ const MessageContent = memo<ContentBlockProps>(({ content, hasTools, id }) => {
28
28
  if (!content && !hasTools) return <ContentLoading id={id} />;
29
29
 
30
30
  if (content === LOADING_FLAT) {
31
- if (hasTools) return null;
32
-
33
31
  return <ContentLoading id={id} />;
34
32
  }
35
33
 
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import { type AssistantContentBlock } from '@lobechat/types';
4
+ import { Accordion, AccordionItem, Block, Flexbox, Icon, Text } from '@lobehub/ui';
5
+ import { cssVar } from 'antd-style';
6
+ import { Workflow } from 'lucide-react';
7
+ import { memo, useMemo } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+
10
+ import ContentBlock from '../../AssistantGroup/components/ContentBlock';
11
+ import { formatDuration } from '../../Tasks/shared/utils';
12
+ import Usage from '../../components/Extras/Usage';
13
+
14
+ interface CompletedStateProps {
15
+ assistantId: string;
16
+ blocks: AssistantContentBlock[];
17
+ duration?: number;
18
+ model?: string;
19
+ provider?: string;
20
+ totalCost?: number;
21
+ totalTokens?: number;
22
+ totalToolCalls?: number;
23
+ }
24
+
25
+ const CompletedState = memo<CompletedStateProps>(
26
+ ({ blocks, assistantId, duration, totalToolCalls, model, provider, totalTokens, totalCost }) => {
27
+ const { t } = useTranslation('chat');
28
+
29
+ // Split blocks: intermediate steps (all but last) and final result (last)
30
+ const { intermediateBlocks, finalBlock } = useMemo(() => {
31
+ if (blocks.length === 0) return { finalBlock: null, intermediateBlocks: [] };
32
+ if (blocks.length === 1) return { finalBlock: blocks[0], intermediateBlocks: [] };
33
+
34
+ return {
35
+ finalBlock: blocks.at(-1)!,
36
+ intermediateBlocks: blocks.slice(0, -1),
37
+ };
38
+ }, [blocks]);
39
+
40
+ if (!finalBlock) return null;
41
+
42
+ const title = (
43
+ <Flexbox align="center" gap={8} horizontal>
44
+ <Block
45
+ align="center"
46
+ flex="none"
47
+ gap={4}
48
+ height={24}
49
+ horizontal
50
+ justify="center"
51
+ style={{ fontSize: 12 }}
52
+ variant="outlined"
53
+ width={24}
54
+ >
55
+ <Icon color={cssVar.colorTextSecondary} icon={Workflow} />
56
+ </Block>
57
+ <Flexbox align="center" gap={4} horizontal>
58
+ <Text as="span" type="secondary" weight={500}>
59
+ {totalToolCalls}
60
+ </Text>
61
+ <Text as="span" type="secondary">
62
+ {t('task.metrics.toolCallsShort')}
63
+ </Text>
64
+ {/* Duration display */}
65
+ {duration && (
66
+ <Text as="span" type="secondary">
67
+ {t('task.metrics.duration', { duration: formatDuration(duration) })}
68
+ </Text>
69
+ )}
70
+ </Flexbox>
71
+ </Flexbox>
72
+ );
73
+
74
+ return (
75
+ <Flexbox gap={8}>
76
+ {/* Intermediate steps - collapsed by default */}
77
+ {intermediateBlocks.length > 0 && (
78
+ <Accordion defaultExpandedKeys={[]} gap={8}>
79
+ <AccordionItem itemKey="intermediate" paddingBlock={4} paddingInline={4} title={title}>
80
+ <Flexbox gap={8} paddingInline={4} style={{ marginTop: 8 }}>
81
+ {intermediateBlocks.map((block) => (
82
+ <ContentBlock
83
+ {...block}
84
+ assistantId={assistantId}
85
+ disableEditing
86
+ key={block.id}
87
+ />
88
+ ))}
89
+ </Flexbox>
90
+ </AccordionItem>
91
+ </Accordion>
92
+ )}
93
+
94
+ {/* Final result - always visible */}
95
+ <ContentBlock {...finalBlock} assistantId={assistantId} disableEditing />
96
+
97
+ {/* Usage display */}
98
+ {model && provider && (
99
+ <Usage model={model} provider={provider} usage={{ cost: totalCost, totalTokens }} />
100
+ )}
101
+ </Flexbox>
102
+ );
103
+ },
104
+ );
105
+
106
+ CompletedState.displayName = 'ClientCompletedState';
107
+
108
+ export default CompletedState;
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import { Flexbox, Text } from '@lobehub/ui';
4
+ import { createStaticStyles, keyframes } from 'antd-style';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
9
+ import { shinyTextStyles } from '@/styles';
10
+
11
+ const shimmer = keyframes`
12
+ 0% {
13
+ transform: translateX(-100%);
14
+ }
15
+
16
+ 100% {
17
+ transform: translateX(100%);
18
+ }
19
+ `;
20
+
21
+ const styles = createStaticStyles(({ css, cssVar }) => ({
22
+ container: css`
23
+ padding-block: 12px;
24
+ `,
25
+ progress: css`
26
+ position: relative;
27
+
28
+ overflow: hidden;
29
+
30
+ height: 3px;
31
+ border-radius: 2px;
32
+
33
+ background: ${cssVar.colorFillSecondary};
34
+ `,
35
+ progressShimmer: css`
36
+ position: absolute;
37
+ inset-block-start: 0;
38
+ inset-inline-start: 0;
39
+
40
+ width: 100%;
41
+ height: 100%;
42
+
43
+ background: linear-gradient(90deg, transparent, ${cssVar.colorPrimaryBgHover}, transparent);
44
+
45
+ animation: ${shimmer} 2s infinite;
46
+ `,
47
+ }));
48
+
49
+ const InitializingState = memo(() => {
50
+ const { t } = useTranslation('chat');
51
+
52
+ return (
53
+ <Flexbox className={styles.container} gap={12}>
54
+ <Flexbox align="center" gap={8} horizontal>
55
+ <NeuralNetworkLoading size={14} />
56
+ <Text className={shinyTextStyles.shinyText} weight={500}>
57
+ {t('task.status.initializing')}
58
+ </Text>
59
+ </Flexbox>
60
+ </Flexbox>
61
+ );
62
+ });
63
+
64
+ InitializingState.displayName = 'InitializingState';
65
+
66
+ export default InitializingState;
@@ -0,0 +1,63 @@
1
+ import { Accordion, AccordionItem, Block, Flexbox, Icon, Markdown, Text } from '@lobehub/ui';
2
+ import { cssVar } from 'antd-style';
3
+ import { ScrollText } from 'lucide-react';
4
+ import { memo, useEffect, useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ interface InstructionAccordionProps {
8
+ childrenCount: number;
9
+ instruction: string;
10
+ }
11
+
12
+ const InstructionAccordion = memo<InstructionAccordionProps>(({ instruction, childrenCount }) => {
13
+ const { t } = useTranslation('chat');
14
+
15
+ // Auto-collapse instruction when children count exceeds threshold
16
+ const [expandedKeys, setExpandedKeys] = useState<string[]>(['instruction']);
17
+
18
+ useEffect(() => {
19
+ if (childrenCount > 1) {
20
+ setExpandedKeys([]);
21
+ }
22
+ }, [childrenCount > 1]);
23
+
24
+ return (
25
+ <Accordion
26
+ expandedKeys={expandedKeys}
27
+ gap={8}
28
+ onExpandedChange={(keys) => setExpandedKeys(keys as string[])}
29
+ >
30
+ <AccordionItem
31
+ itemKey="instruction"
32
+ paddingBlock={4}
33
+ paddingInline={4}
34
+ title={
35
+ <Flexbox align="center" gap={8} horizontal>
36
+ <Block
37
+ align="center"
38
+ flex="none"
39
+ gap={4}
40
+ height={24}
41
+ horizontal
42
+ justify="center"
43
+ style={{ fontSize: 12 }}
44
+ variant="outlined"
45
+ width={24}
46
+ >
47
+ <Icon color={cssVar.colorTextSecondary} icon={ScrollText} />
48
+ </Block>
49
+ <Text as="span" type="secondary">
50
+ {t('task.instruction')}
51
+ </Text>
52
+ </Flexbox>
53
+ }
54
+ >
55
+ <Block padding={12} style={{ marginBlock: 8, maxHeight: 300, overflow: 'auto' }} variant={'outlined'}>
56
+ <Markdown variant={'chat'}>{instruction}</Markdown>
57
+ </Block>
58
+ </AccordionItem>
59
+ </Accordion>
60
+ );
61
+ });
62
+
63
+ export default InstructionAccordion;