@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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/locales/en-US/chat.json +6 -1
- package/locales/zh-CN/chat.json +5 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
- package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
- package/packages/agent-runtime/src/types/instruction.ts +46 -2
- package/packages/builtin-tool-gtd/src/const.ts +1 -0
- package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
- package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
- package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
- package/packages/builtin-tool-gtd/src/types.ts +55 -33
- package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
- package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
- package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
- package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
- package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
- package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
- package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
- package/packages/database/src/models/message.ts +8 -1
- package/packages/database/src/models/thread.ts +1 -1
- package/packages/types/src/message/ui/chat.ts +2 -0
- package/packages/types/src/topic/thread.ts +20 -0
- package/src/components/StreamingMarkdown/index.tsx +10 -43
- package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
- package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
- package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
- package/src/features/Conversation/Messages/Task/index.tsx +11 -6
- package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
- package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
- package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
- package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
- package/src/features/Conversation/components/Thinking/index.tsx +9 -30
- package/src/features/Conversation/store/slices/data/action.ts +2 -3
- package/src/features/NavPanel/components/BackButton.tsx +10 -13
- package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
- package/src/hooks/useAutoScroll.ts +117 -0
- package/src/locales/default/chat.ts +6 -1
- package/src/server/routers/lambda/aiAgent.ts +239 -1
- package/src/server/routers/lambda/thread.ts +2 -0
- package/src/server/services/message/__tests__/index.test.ts +37 -0
- package/src/server/services/message/index.ts +6 -1
- package/src/services/aiAgent.ts +51 -0
- package/src/store/chat/agents/createAgentExecutors.ts +714 -12
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
- package/src/store/chat/slices/message/actions/query.ts +33 -1
- package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
- 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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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,
|
|
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 = <
|
|
31
|
+
icon = <NeuralNetworkLoading size={16} />;
|
|
31
32
|
} else {
|
|
32
33
|
return null;
|
|
33
34
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
return messageService.getMessages(context);
|
|
114
|
-
},
|
|
113
|
+
() => messageService.getMessages(context),
|
|
115
114
|
{
|
|
116
115
|
onData: (data) => {
|
|
117
116
|
if (!data) return;
|