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

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 (55) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/en-US/chat.json +6 -1
  4. package/locales/zh-CN/chat.json +5 -0
  5. package/package.json +1 -1
  6. package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
  7. package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
  8. package/packages/agent-runtime/src/types/instruction.ts +46 -2
  9. package/packages/builtin-tool-gtd/src/const.ts +1 -0
  10. package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
  11. package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
  12. package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
  13. package/packages/builtin-tool-gtd/src/types.ts +55 -33
  14. package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
  15. package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
  16. package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
  17. package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
  18. package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
  19. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
  20. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
  21. package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
  22. package/packages/database/src/models/message.ts +8 -1
  23. package/packages/database/src/models/thread.ts +1 -1
  24. package/packages/types/src/message/ui/chat.ts +2 -0
  25. package/packages/types/src/topic/thread.ts +20 -0
  26. package/src/components/StreamingMarkdown/index.tsx +10 -43
  27. package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
  28. package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
  29. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
  30. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
  31. package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
  32. package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
  33. package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
  34. package/src/features/Conversation/Messages/Task/index.tsx +11 -6
  35. package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
  36. package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
  37. package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
  38. package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
  39. package/src/features/Conversation/components/Thinking/index.tsx +9 -30
  40. package/src/features/Conversation/store/slices/data/action.ts +2 -3
  41. package/src/features/NavPanel/components/BackButton.tsx +10 -13
  42. package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
  43. package/src/hooks/useAutoScroll.ts +117 -0
  44. package/src/locales/default/chat.ts +6 -1
  45. package/src/server/routers/lambda/aiAgent.ts +239 -1
  46. package/src/server/routers/lambda/thread.ts +2 -0
  47. package/src/server/services/message/__tests__/index.test.ts +37 -0
  48. package/src/server/services/message/index.ts +6 -1
  49. package/src/services/aiAgent.ts +51 -0
  50. package/src/store/chat/agents/createAgentExecutors.ts +714 -12
  51. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
  52. package/src/store/chat/slices/message/actions/query.ts +33 -1
  53. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
  54. package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
  55. package/src/store/chat/slices/operation/types.ts +4 -0
@@ -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;
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+
3
+ import { type AssistantContentBlock } from '@lobechat/types';
4
+ import { Block, Flexbox, ScrollShadow, Text } from '@lobehub/ui';
5
+ import { createStaticStyles } from 'antd-style';
6
+ import { type RefObject, memo, useEffect, useMemo, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
10
+ import AnimatedNumber from '@/features/Conversation/Messages/components/Extras/Usage/UsageDetail/AnimatedNumber';
11
+ import { useAutoScroll } from '@/hooks/useAutoScroll';
12
+
13
+ import ContentBlock from '../../AssistantGroup/components/ContentBlock';
14
+ import { accumulateUsage, formatElapsedTime } from '../../Tasks/shared/utils';
15
+ import Usage from '../../components/Extras/Usage';
16
+
17
+ const styles = createStaticStyles(({ css }) => ({
18
+ contentScroll: css`
19
+ max-height: min(50vh, 300px);
20
+ `,
21
+ }));
22
+
23
+ interface ProcessingStateProps {
24
+ assistantId: string;
25
+ blocks: AssistantContentBlock[];
26
+ model?: string;
27
+ provider?: string;
28
+ startTime?: number;
29
+ }
30
+
31
+ const ProcessingState = memo<ProcessingStateProps>(
32
+ ({ blocks, assistantId, startTime, model, provider }) => {
33
+ const { t } = useTranslation('chat');
34
+ const [elapsedTime, setElapsedTime] = useState(0);
35
+ const { ref, handleScroll } = useAutoScroll<HTMLDivElement>({
36
+ deps: [blocks],
37
+ enabled: true,
38
+ });
39
+
40
+ const totalToolCalls = useMemo(
41
+ () => blocks.reduce((sum, block) => sum + (block.tools?.length || 0), 0),
42
+ [blocks],
43
+ );
44
+
45
+ // Accumulate usage from all blocks
46
+ const accumulatedUsage = useMemo(() => accumulateUsage(blocks), [blocks]);
47
+
48
+ // Calculate initial elapsed time
49
+ useEffect(() => {
50
+ if (startTime) {
51
+ setElapsedTime(Math.max(0, Date.now() - startTime));
52
+ }
53
+ }, [startTime]);
54
+
55
+ // Timer for updating elapsed time every second
56
+ useEffect(() => {
57
+ if (!startTime) return;
58
+
59
+ const timer = setInterval(() => {
60
+ setElapsedTime(Math.max(0, Date.now() - startTime));
61
+ }, 1000);
62
+
63
+ return () => clearInterval(timer);
64
+ }, [startTime]);
65
+
66
+ return (
67
+ <Flexbox gap={8}>
68
+ <Flexbox align="center" gap={8} horizontal paddingInline={4}>
69
+ <Block
70
+ align="center"
71
+ flex="none"
72
+ gap={4}
73
+ height={24}
74
+ horizontal
75
+ justify="center"
76
+ style={{ fontSize: 12 }}
77
+ variant="outlined"
78
+ width={24}
79
+ >
80
+ <NeuralNetworkLoading size={16} />
81
+ </Block>
82
+ <Flexbox align="center" gap={4} horizontal>
83
+ <Text as="span" type="secondary" weight={500}>
84
+ <AnimatedNumber
85
+ duration={500}
86
+ formatter={(v) => Math.round(v).toString()}
87
+ value={totalToolCalls}
88
+ />
89
+ </Text>
90
+ <Text as="span" type="secondary">
91
+ {t('task.metrics.toolCallsShort')}
92
+ </Text>
93
+ {startTime && (
94
+ <Text as="span" type="secondary">
95
+ ({formatElapsedTime(elapsedTime)})
96
+ </Text>
97
+ )}
98
+ </Flexbox>
99
+ </Flexbox>
100
+ <ScrollShadow
101
+ className={styles.contentScroll}
102
+ offset={12}
103
+ onScroll={handleScroll}
104
+ ref={ref as RefObject<HTMLDivElement>}
105
+ size={8}
106
+ >
107
+ <Flexbox gap={8}>
108
+ {blocks.map((block) => (
109
+ <ContentBlock {...block} assistantId={assistantId} disableEditing key={block.id} />
110
+ ))}
111
+ </Flexbox>
112
+ </ScrollShadow>
113
+
114
+ {/* Usage display */}
115
+ {model && provider && <Usage model={model} provider={provider} usage={accumulatedUsage} />}
116
+ </Flexbox>
117
+ );
118
+ },
119
+ );
120
+
121
+ ProcessingState.displayName = 'ClientProcessingState';
122
+
123
+ export default ProcessingState;
@@ -0,0 +1,106 @@
1
+ 'use client';
2
+
3
+ import { type TaskDetail, ThreadStatus } from '@lobechat/types';
4
+ import { Flexbox } from '@lobehub/ui';
5
+ import { memo, useMemo } from 'react';
6
+
7
+ import BubblesLoading from '@/components/BubblesLoading';
8
+ import { useChatStore } from '@/store/chat';
9
+ import { displayMessageSelectors } from '@/store/chat/selectors';
10
+ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
11
+
12
+ import CompletedState from './CompletedState';
13
+ import InitializingState from './InitializingState';
14
+ import InstructionAccordion from './InstructionAccordion';
15
+ import ProcessingState from './ProcessingState';
16
+
17
+ interface ClientTaskDetailProps {
18
+ content?: string;
19
+ messageId: string;
20
+ taskDetail?: TaskDetail;
21
+ }
22
+
23
+ const ClientTaskDetail = memo<ClientTaskDetailProps>(({ taskDetail }) => {
24
+ const threadId = taskDetail?.threadId;
25
+ const isExecuting = taskDetail?.status === ThreadStatus.Processing;
26
+
27
+ const [activeAgentId, activeTopicId, useFetchMessages] = useChatStore((s) => [
28
+ s.activeAgentId,
29
+ s.activeTopicId,
30
+ s.useFetchMessages,
31
+ ]);
32
+
33
+ const threadContext = useMemo(
34
+ () => ({
35
+ agentId: activeAgentId,
36
+ scope: 'thread' as const,
37
+ threadId,
38
+ topicId: activeTopicId,
39
+ }),
40
+ [activeAgentId, activeTopicId, threadId],
41
+ );
42
+
43
+ const threadMessageKey = useMemo(
44
+ () => (threadId ? messageMapKey(threadContext) : null),
45
+ [threadId],
46
+ );
47
+
48
+ // Fetch thread messages (skip when executing - messages come from real-time updates)
49
+ useFetchMessages(threadContext, isExecuting);
50
+
51
+ // Get thread messages from store using selector
52
+ const threadMessages = useChatStore((s) =>
53
+ threadMessageKey
54
+ ? displayMessageSelectors.getDisplayMessagesByKey(threadMessageKey)(s)
55
+ : undefined,
56
+ );
57
+
58
+ if (!threadMessages) return <BubblesLoading />;
59
+
60
+ // Find the assistantGroup message which contains the children blocks
61
+ const assistantGroupMessage = threadMessages.find((item) => item.role === 'assistantGroup');
62
+ const instruction = threadMessages.find((item) => item.role === 'user')?.content;
63
+ const childrenCount = assistantGroupMessage?.children?.length ?? 0;
64
+
65
+ // Get model/provider from assistantGroup message
66
+ const model = assistantGroupMessage?.model;
67
+ const provider = assistantGroupMessage?.provider;
68
+
69
+ // Initializing state: no status yet (task just created, waiting for client execution)
70
+ if (threadMessages.length === 0 || !assistantGroupMessage?.children || childrenCount === 0) {
71
+ return <InitializingState />;
72
+ }
73
+
74
+ return (
75
+ <Flexbox gap={4}>
76
+ {instruction && (
77
+ <InstructionAccordion childrenCount={childrenCount} instruction={instruction} />
78
+ )}
79
+
80
+ {isExecuting ? (
81
+ <ProcessingState
82
+ assistantId={assistantGroupMessage.id}
83
+ blocks={assistantGroupMessage.children}
84
+ model={model ?? undefined}
85
+ provider={provider ?? undefined}
86
+ startTime={assistantGroupMessage.createdAt}
87
+ />
88
+ ) : (
89
+ <CompletedState
90
+ assistantId={assistantGroupMessage.id}
91
+ blocks={assistantGroupMessage.children}
92
+ duration={taskDetail?.duration}
93
+ model={model ?? undefined}
94
+ provider={provider ?? undefined}
95
+ totalCost={taskDetail?.totalCost}
96
+ totalTokens={taskDetail?.totalTokens}
97
+ totalToolCalls={taskDetail?.totalToolCalls}
98
+ />
99
+ )}
100
+ </Flexbox>
101
+ );
102
+ });
103
+
104
+ ClientTaskDetail.displayName = 'ClientClientTaskDetail';
105
+
106
+ export default ClientTaskDetail;
@@ -17,6 +17,7 @@ interface TaskDetailPanelProps {
17
17
  }
18
18
 
19
19
  const TaskDetailPanel = memo<TaskDetailPanelProps>(({ taskDetail, content, messageId }) => {
20
+ // Default: server-side task execution
20
21
  return <StatusContent content={content} messageId={messageId} taskDetail={taskDetail} />;
21
22
  });
22
23
 
@@ -19,6 +19,7 @@ import { useAgentMeta, useDoubleClickEdit } from '../../hooks';
19
19
  import { dataSelectors, messageStateSelectors, useConversationStore } from '../../store';
20
20
  import { normalizeThinkTags, processWithArtifact } from '../../utils/markdown';
21
21
  import { AssistantActionsBar } from './Actions';
22
+ import ClientTaskDetail from './ClientTaskDetail';
22
23
  import TaskDetailPanel from './TaskDetailPanel';
23
24
 
24
25
  interface TaskMessageProps {
@@ -91,12 +92,16 @@ const TaskMessage = memo<TaskMessageProps>(({ id, index, disableEditing, isLates
91
92
  time={createdAt}
92
93
  titleAddon={<Tag>{t('task.subtask')}</Tag>}
93
94
  >
94
- <TaskDetailPanel
95
- content={content}
96
- instruction={metadata?.instruction}
97
- messageId={id}
98
- taskDetail={taskDetail}
99
- />
95
+ {taskDetail?.clientMode ? (
96
+ <ClientTaskDetail messageId={id} taskDetail={taskDetail} />
97
+ ) : (
98
+ <TaskDetailPanel
99
+ content={content}
100
+ instruction={metadata?.instruction}
101
+ messageId={id}
102
+ taskDetail={taskDetail}
103
+ />
104
+ )}
100
105
  </ChatItem>
101
106
  );
102
107
  }, isEqual);
@@ -2,9 +2,10 @@
2
2
 
3
3
  import { Block, Flexbox, Icon, Text } from '@lobehub/ui';
4
4
  import { cssVar } from 'antd-style';
5
- import { ListChecksIcon, Loader2, XIcon } from 'lucide-react';
5
+ import { ListChecksIcon, XIcon } from 'lucide-react';
6
6
  import { memo } from 'react';
7
7
 
8
+ import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
8
9
  import { ThreadStatus } from '@/types/index';
9
10
 
10
11
  import { isProcessingStatus } from '../shared';
@@ -27,7 +28,7 @@ const TaskStatusIndicator = memo<{ status?: ThreadStatus }>(({ status }) => {
27
28
  } else if (isError) {
28
29
  icon = <Icon color={cssVar.colorError} icon={XIcon} />;
29
30
  } else if (isProcessing || isInitializing) {
30
- icon = <Icon color={cssVar.colorTextDescription} icon={Loader2} spin />;
31
+ icon = <NeuralNetworkLoading size={16} />;
31
32
  } else {
32
33
  return null;
33
34
  }
@@ -57,10 +57,6 @@ const InitializingState = memo(() => {
57
57
  {t('task.status.initializing')}
58
58
  </Text>
59
59
  </Flexbox>
60
-
61
- <div className={styles.progress}>
62
- <div className={styles.progressShimmer} />
63
- </div>
64
60
  </Flexbox>
65
61
  );
66
62
  });
@@ -1,4 +1,9 @@
1
- import { type TaskCurrentActivity, ThreadStatus } from '@/types/index';
1
+ import {
2
+ type AssistantContentBlock,
3
+ type ModelUsage,
4
+ type TaskCurrentActivity,
5
+ ThreadStatus,
6
+ } from '@lobechat/types';
2
7
 
3
8
  /**
4
9
  * Format duration in milliseconds to human-readable string
@@ -68,3 +73,19 @@ export const isProcessingStatus = (status?: ThreadStatus): boolean => {
68
73
  status === ThreadStatus.Todo
69
74
  );
70
75
  };
76
+
77
+ /**
78
+ * Accumulate usage from all blocks
79
+ */
80
+ export const accumulateUsage = (blocks: AssistantContentBlock[]): ModelUsage => {
81
+ return blocks.reduce((acc, block) => {
82
+ const usage = block.usage;
83
+ if (!usage) return acc;
84
+ return {
85
+ cost: (acc.cost || 0) + (usage.cost || 0),
86
+ totalInputTokens: (acc.totalInputTokens || 0) + (usage.totalInputTokens || 0),
87
+ totalOutputTokens: (acc.totalOutputTokens || 0) + (usage.totalOutputTokens || 0),
88
+ totalTokens: (acc.totalTokens || 0) + (usage.totalTokens || 0),
89
+ };
90
+ }, {} as ModelUsage);
91
+ };
@@ -18,7 +18,7 @@ interface ContentLoadingProps {
18
18
  const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
19
19
  const { t } = useTranslation('chat');
20
20
  const runningOp = useChatStore(operationSelectors.getDeepestRunningOperationByMessage(id));
21
- console.log('runningOp', runningOp);
21
+
22
22
  const [elapsedSeconds, setElapsedSeconds] = useState(0);
23
23
  const [startTime, setStartTime] = useState(runningOp?.metadata?.startTime);
24
24
 
@@ -6,11 +6,11 @@ import {
6
6
  type RefObject,
7
7
  memo,
8
8
  useEffect,
9
- useRef,
10
9
  useState,
11
10
  } from 'react';
12
11
 
13
12
  import MarkdownMessage from '@/features/Conversation/Markdown';
13
+ import { useAutoScroll } from '@/hooks/useAutoScroll';
14
14
  import { type ChatCitationItem } from '@/types/index';
15
15
 
16
16
  import Title from './Title';
@@ -40,39 +40,17 @@ interface ThinkingProps {
40
40
  const Thinking = memo<ThinkingProps>((props) => {
41
41
  const { content, duration, thinking, citations, thinkingAnimated } = props;
42
42
  const [showDetail, setShowDetail] = useState(false);
43
- const contentRef = useRef<HTMLDivElement | null>(null);
43
+
44
+ const { ref, handleScroll } = useAutoScroll<HTMLDivElement>({
45
+ deps: [content, showDetail],
46
+ enabled: thinking && showDetail,
47
+ threshold: 120,
48
+ });
44
49
 
45
50
  useEffect(() => {
46
51
  setShowDetail(!!thinking);
47
52
  }, [thinking]);
48
53
 
49
- // 当内容变更且正在思考时,如果用户接近底部则自动滚动到底部
50
- useEffect(() => {
51
- if (!thinking || !showDetail) return;
52
- const container = contentRef.current;
53
- if (!container) return;
54
-
55
- // 仅当用户接近底部时才自动滚动,避免打断用户查看上方内容
56
- const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
57
- const isNearBottom = distanceToBottom < 120;
58
-
59
- if (isNearBottom) {
60
- requestAnimationFrame(() => {
61
- container.scrollTop = container.scrollHeight;
62
- });
63
- }
64
- }, [content, thinking, showDetail]);
65
-
66
- // 展开时滚动到底部,便于查看最新内容
67
- useEffect(() => {
68
- if (!showDetail) return;
69
- const container = contentRef.current;
70
- if (!container) return;
71
- requestAnimationFrame(() => {
72
- container.scrollTop = container.scrollHeight;
73
- });
74
- }, [showDetail]);
75
-
76
54
  return (
77
55
  <Accordion
78
56
  expandedKeys={showDetail ? ['thinking'] : []}
@@ -88,7 +66,8 @@ const Thinking = memo<ThinkingProps>((props) => {
88
66
  <ScrollShadow
89
67
  className={styles.contentScroll}
90
68
  offset={12}
91
- ref={contentRef as unknown as RefObject<HTMLDivElement>}
69
+ onScroll={handleScroll}
70
+ ref={ref as RefObject<HTMLDivElement>}
92
71
  size={12}
93
72
  >
94
73
  {typeof content === 'string' ? (
@@ -106,12 +106,11 @@ export const dataSlice: StateCreator<
106
106
  // Also skip fetch when topicId is null (new conversation state) - there's no server data,
107
107
  // only local optimistic updates. Fetching would return empty array and overwrite local data.
108
108
  const shouldFetch = !skipFetch && !!context.agentId && !!context.topicId;
109
+
109
110
  return useClientDataSWRWithSync<UIChatMessage[]>(
110
111
  shouldFetch ? ['CONVERSATION_FETCH_MESSAGES', context] : null,
111
112
 
112
- async () => {
113
- return messageService.getMessages(context);
114
- },
113
+ () => messageService.getMessages(context),
115
114
  {
116
115
  onData: (data) => {
117
116
  if (!data) return;