@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.
- package/.env.example +0 -3
- package/.env.example.development +0 -3
- package/CHANGELOG.md +58 -0
- package/Dockerfile +1 -2
- package/changelog/v1.json +18 -0
- package/docs/self-hosting/advanced/auth.mdx +5 -6
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +5 -6
- package/docs/self-hosting/environment-variables/auth.mdx +0 -7
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +0 -7
- package/locales/en-US/chat.json +6 -1
- package/locales/en-US/discover.json +1 -0
- package/locales/zh-CN/chat.json +5 -0
- package/locales/zh-CN/discover.json +1 -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/scripts/prebuild.mts +2 -2
- package/src/app/[variants]/(main)/community/(list)/agent/features/List/Item.tsx +1 -0
- package/src/components/StreamingMarkdown/index.tsx +10 -43
- package/src/envs/__tests__/app.test.ts +81 -0
- package/src/envs/app.ts +14 -2
- package/src/envs/auth.test.ts +0 -13
- package/src/envs/auth.ts +0 -41
- 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/libs/better-auth/auth-client.ts +0 -9
- package/src/libs/better-auth/define-config.ts +13 -12
- package/src/libs/better-auth/sso/index.ts +2 -1
- package/src/libs/better-auth/utils/config.ts +2 -2
- package/src/libs/next/proxy/define-config.ts +4 -6
- package/src/locales/default/chat.ts +6 -1
- package/src/locales/default/discover.ts +2 -0
- package/src/server/routers/lambda/__tests__/integration/topic.integration.test.ts +74 -0
- package/src/server/routers/lambda/aiAgent.ts +239 -1
- package/src/server/routers/lambda/thread.ts +2 -0
- package/src/server/routers/lambda/topic.ts +6 -0
- package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +4 -1
- package/src/server/services/agentRuntime/AgentRuntimeService.ts +2 -1
- 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/services/topic/index.ts +4 -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
- package/src/store/chat/slices/topic/action.test.ts +2 -1
- package/src/store/chat/slices/topic/action.ts +1 -1
|
@@ -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;
|
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
import { ActionIcon, type ActionIconProps } from '@lobehub/ui';
|
|
2
2
|
import { ChevronLeftIcon } from 'lucide-react';
|
|
3
3
|
import { memo } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { Link } from 'react-router-dom';
|
|
5
5
|
|
|
6
6
|
import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
|
7
7
|
|
|
8
8
|
export const BACK_BUTTON_ID = 'lobe-back-button';
|
|
9
9
|
|
|
10
10
|
const BackButton = memo<ActionIconProps & { to?: string }>(({ to = '/', onClick, ...rest }) => {
|
|
11
|
-
const navigate = useNavigate();
|
|
12
|
-
|
|
13
11
|
return (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/>
|
|
12
|
+
// @ts-expect-error
|
|
13
|
+
<Link onClick={onClick} to={to}>
|
|
14
|
+
<ActionIcon
|
|
15
|
+
icon={ChevronLeftIcon}
|
|
16
|
+
id={BACK_BUTTON_ID}
|
|
17
|
+
size={DESKTOP_HEADER_ICON_SIZE}
|
|
18
|
+
{...rest}
|
|
19
|
+
/>
|
|
20
|
+
</Link>
|
|
24
21
|
);
|
|
25
22
|
});
|
|
26
23
|
|
|
@@ -13,6 +13,7 @@ import { systemStatusSelectors } from '@/store/global/selectors';
|
|
|
13
13
|
import { isMacOS } from '@/utils/platform';
|
|
14
14
|
|
|
15
15
|
import { useNavPanelSizeChangeHandler } from '../hooks/useNavPanel';
|
|
16
|
+
import { BACK_BUTTON_ID } from './BackButton';
|
|
16
17
|
|
|
17
18
|
const motionVariants = {
|
|
18
19
|
animate: { opacity: 1, x: 0 },
|
|
@@ -76,6 +77,9 @@ const draggableStyles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
76
77
|
opacity,
|
|
77
78
|
width 0.2s ${cssVar.motionEaseOut};
|
|
78
79
|
}
|
|
80
|
+
#${BACK_BUTTON_ID} {
|
|
81
|
+
width: 24px !important;
|
|
82
|
+
}
|
|
79
83
|
|
|
80
84
|
&:hover {
|
|
81
85
|
#${TOGGLE_BUTTON_ID} {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UseAutoScrollOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Dependencies that trigger auto-scroll when changed
|
|
6
|
+
*/
|
|
7
|
+
deps?: unknown[];
|
|
8
|
+
/**
|
|
9
|
+
* Whether auto-scroll is enabled (e.g., only when streaming/executing)
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Distance threshold from bottom to consider "near bottom" (in pixels)
|
|
15
|
+
* @default 20
|
|
16
|
+
*/
|
|
17
|
+
threshold?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface UseAutoScrollReturn<T extends HTMLElement> {
|
|
21
|
+
/**
|
|
22
|
+
* Callback to handle scroll events, attach to onScroll
|
|
23
|
+
*/
|
|
24
|
+
handleScroll: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Ref to attach to the scrollable container
|
|
27
|
+
*/
|
|
28
|
+
ref: RefObject<T | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Reset the scroll lock state (e.g., when new content starts)
|
|
31
|
+
*/
|
|
32
|
+
resetScrollLock: () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Whether user has scrolled away from bottom (scroll lock active)
|
|
35
|
+
*/
|
|
36
|
+
userHasScrolled: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hook for auto-scrolling content with user scroll detection
|
|
41
|
+
*
|
|
42
|
+
* Features:
|
|
43
|
+
* - Auto-scrolls to bottom when dependencies change
|
|
44
|
+
* - Detects when user scrolls away from bottom and stops auto-scrolling
|
|
45
|
+
* - Provides reset function for when new content starts
|
|
46
|
+
* - Ignores scroll events triggered by auto-scroll itself
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* const { ref, handleScroll } = useAutoScroll<HTMLDivElement>({
|
|
51
|
+
* deps: [content],
|
|
52
|
+
* enabled: isStreaming,
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* return (
|
|
56
|
+
* <ScrollShadow ref={ref} onScroll={handleScroll}>
|
|
57
|
+
* {content}
|
|
58
|
+
* </ScrollShadow>
|
|
59
|
+
* );
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function useAutoScroll<T extends HTMLElement = HTMLDivElement>(
|
|
63
|
+
options: UseAutoScrollOptions = {},
|
|
64
|
+
): UseAutoScrollReturn<T> {
|
|
65
|
+
const { deps = [], enabled = true, threshold = 20 } = options;
|
|
66
|
+
|
|
67
|
+
const ref = useRef<T | null>(null);
|
|
68
|
+
const [userHasScrolled, setUserHasScrolled] = useState(false);
|
|
69
|
+
const isAutoScrollingRef = useRef(false);
|
|
70
|
+
|
|
71
|
+
// Handle user scroll detection
|
|
72
|
+
const handleScroll = useCallback(() => {
|
|
73
|
+
// Ignore scroll events triggered by auto-scroll
|
|
74
|
+
if (isAutoScrollingRef.current) return;
|
|
75
|
+
|
|
76
|
+
const container = ref.current;
|
|
77
|
+
if (!container) return;
|
|
78
|
+
|
|
79
|
+
// Check if user scrolled away from bottom
|
|
80
|
+
const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
81
|
+
const isAtBottom = distanceToBottom < threshold;
|
|
82
|
+
|
|
83
|
+
// If user scrolled up, stop auto-scrolling
|
|
84
|
+
if (!isAtBottom) {
|
|
85
|
+
setUserHasScrolled(true);
|
|
86
|
+
}
|
|
87
|
+
}, [threshold]);
|
|
88
|
+
|
|
89
|
+
// Reset scroll lock state
|
|
90
|
+
const resetScrollLock = useCallback(() => {
|
|
91
|
+
setUserHasScrolled(false);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
// Auto scroll to bottom when deps change (unless user has scrolled or disabled)
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!enabled || userHasScrolled) return;
|
|
97
|
+
|
|
98
|
+
const container = ref.current;
|
|
99
|
+
if (!container) return;
|
|
100
|
+
|
|
101
|
+
isAutoScrollingRef.current = true;
|
|
102
|
+
requestAnimationFrame(() => {
|
|
103
|
+
container.scrollTop = container.scrollHeight;
|
|
104
|
+
// Reset the flag after scroll completes
|
|
105
|
+
requestAnimationFrame(() => {
|
|
106
|
+
isAutoScrollingRef.current = false;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}, [enabled, userHasScrolled, ...deps]);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
handleScroll,
|
|
113
|
+
ref,
|
|
114
|
+
resetScrollLock,
|
|
115
|
+
userHasScrolled,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -7,9 +7,6 @@ import {
|
|
|
7
7
|
import { createAuthClient } from 'better-auth/react';
|
|
8
8
|
|
|
9
9
|
import type { auth } from '@/auth';
|
|
10
|
-
import { getAuthConfig } from '@/envs/auth';
|
|
11
|
-
|
|
12
|
-
const { NEXT_PUBLIC_AUTH_URL } = getAuthConfig();
|
|
13
10
|
|
|
14
11
|
export const {
|
|
15
12
|
linkSocial,
|
|
@@ -24,12 +21,6 @@ export const {
|
|
|
24
21
|
unlinkAccount,
|
|
25
22
|
useSession,
|
|
26
23
|
} = createAuthClient({
|
|
27
|
-
/** The base URL of the server (optional if you're using the same domain) */
|
|
28
|
-
...(NEXT_PUBLIC_AUTH_URL
|
|
29
|
-
? {
|
|
30
|
-
baseURL: NEXT_PUBLIC_AUTH_URL,
|
|
31
|
-
}
|
|
32
|
-
: {}),
|
|
33
24
|
plugins: [
|
|
34
25
|
adminClient(),
|
|
35
26
|
inferAdditionalFields<typeof auth>(),
|
|
@@ -14,6 +14,7 @@ import { admin, emailOTP, genericOAuth, magicLink } from 'better-auth/plugins';
|
|
|
14
14
|
import { type BetterAuthPlugin } from 'better-auth/types';
|
|
15
15
|
|
|
16
16
|
import { businessEmailValidator } from '@/business/server/better-auth';
|
|
17
|
+
import { appEnv } from '@/envs/app';
|
|
17
18
|
import { authEnv } from '@/envs/auth';
|
|
18
19
|
import {
|
|
19
20
|
getMagicLinkEmailTemplate,
|
|
@@ -32,13 +33,13 @@ import { UserService } from '@/server/services/user';
|
|
|
32
33
|
const VERIFICATION_LINK_EXPIRES_IN = 3600;
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
|
-
* Safely extract hostname from
|
|
36
|
-
* Returns undefined if
|
|
36
|
+
* Safely extract hostname from APP_URL for passkey rpID.
|
|
37
|
+
* Returns undefined if APP_URL is not set (e.g., in e2e tests).
|
|
37
38
|
*/
|
|
38
39
|
const getPasskeyRpID = (): string | undefined => {
|
|
39
|
-
if (!
|
|
40
|
+
if (!appEnv.APP_URL) return undefined;
|
|
40
41
|
try {
|
|
41
|
-
return new URL(
|
|
42
|
+
return new URL(appEnv.APP_URL).hostname;
|
|
42
43
|
} catch {
|
|
43
44
|
return undefined;
|
|
44
45
|
}
|
|
@@ -46,14 +47,15 @@ const getPasskeyRpID = (): string | undefined => {
|
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Get passkey origins array.
|
|
49
|
-
* Returns undefined if
|
|
50
|
+
* Returns undefined if APP_URL is not set (e.g., in e2e tests).
|
|
50
51
|
*/
|
|
51
52
|
const getPasskeyOrigins = (): string[] | undefined => {
|
|
52
|
-
if (!
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
if (!appEnv.APP_URL) return undefined;
|
|
54
|
+
try {
|
|
55
|
+
return [new URL(appEnv.APP_URL).origin];
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
57
59
|
};
|
|
58
60
|
const MAGIC_LINK_EXPIRES_IN = 900;
|
|
59
61
|
// OTP expiration time (in seconds) - 5 minutes for mobile OTP verification
|
|
@@ -81,8 +83,7 @@ export function defineConfig(customOptions: CustomBetterAuthOptions) {
|
|
|
81
83
|
},
|
|
82
84
|
},
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
baseURL: authEnv.NEXT_PUBLIC_AUTH_URL,
|
|
86
|
+
baseURL: appEnv.APP_URL,
|
|
86
87
|
secret: authEnv.AUTH_SECRET,
|
|
87
88
|
trustedOrigins: getTrustedOrigins(enabledSSOProviders),
|
|
88
89
|
|