@lobehub/lobehub 2.0.0-next.65 → 2.0.0-next.67
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/.github/workflows/claude-translator.yml +1 -0
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/chat.json +3 -0
- package/locales/bg-BG/chat.json +3 -0
- package/locales/de-DE/chat.json +3 -0
- package/locales/en-US/chat.json +3 -0
- package/locales/es-ES/chat.json +3 -0
- package/locales/fa-IR/chat.json +3 -0
- package/locales/fr-FR/chat.json +3 -0
- package/locales/it-IT/chat.json +3 -0
- package/locales/ja-JP/chat.json +3 -0
- package/locales/ko-KR/chat.json +3 -0
- package/locales/nl-NL/chat.json +3 -0
- package/locales/pl-PL/chat.json +3 -0
- package/locales/pt-BR/chat.json +3 -0
- package/locales/ru-RU/chat.json +3 -0
- package/locales/tr-TR/chat.json +3 -0
- package/locales/vi-VN/chat.json +3 -0
- package/locales/zh-CN/chat.json +3 -0
- package/locales/zh-TW/chat.json +3 -0
- package/package.json +7 -6
- package/packages/conversation-flow/src/__tests__/fixtures/index.ts +4 -8
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/{assistant-with-tools.json → assistantGroup/assistant-with-tools.json} +2 -1
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistantGroup/index.ts +8 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/index.ts +2 -4
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/{assistant-with-tools.json → assistantGroup/assistant-with-tools.json} +8 -8
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistantGroup/index.ts +8 -0
- package/packages/conversation-flow/src/__tests__/parse.test.ts +6 -6
- package/packages/conversation-flow/src/parse.ts +45 -1
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +64 -0
- package/packages/database/package.json +2 -2
- package/packages/obervability-otel/package.json +1 -1
- package/packages/types/src/message/common/metadata.ts +8 -1
- package/packages/types/src/message/ui/chat.ts +1 -0
- package/src/app/(backend)/market/agent/[[...segments]]/route.ts +1 -1
- package/src/app/(backend)/market/oidc/[[...segments]]/route.ts +1 -1
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/ChatItem/index.tsx +1 -0
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatMinimap/index.tsx +21 -28
- package/src/app/market-auth-callback/layout.tsx +27 -3
- package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +2 -2
- package/src/features/ChatItem/style.ts +4 -0
- package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +18 -4
- package/src/features/Conversation/Messages/Assistant/CollapsedMessage.tsx +37 -0
- package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +16 -9
- package/src/features/Conversation/Messages/Assistant/index.tsx +329 -230
- package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +31 -9
- package/src/features/Conversation/Messages/Group/CollapsedMessage.tsx +37 -0
- package/src/features/Conversation/Messages/Group/{GroupChildren.tsx → Group.tsx} +18 -4
- package/src/features/Conversation/Messages/Group/GroupItem.tsx +3 -5
- package/src/features/Conversation/Messages/Group/index.tsx +84 -19
- package/src/features/Conversation/Messages/User/Actions/ActionsBar.tsx +3 -3
- package/src/features/Conversation/Messages/index.tsx +24 -8
- package/src/features/Conversation/components/VirtualizedList/VirtuosoContext.ts +13 -13
- package/src/features/Conversation/components/VirtualizedList/index.tsx +92 -58
- package/src/features/Conversation/components/WideScreenContainer/index.tsx +10 -6
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +14 -0
- package/src/features/Conversation/hooks/useDoubleClickEdit.ts +3 -3
- package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +1 -1
- package/src/libs/mcp/__tests__/index.test.ts +6 -6
- package/src/locales/default/chat.ts +3 -0
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +9 -1
- package/src/store/chat/slices/message/actions/publicApi.ts +17 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -1
- package/src/store/chat/slices/message/selectors/messageState.ts +7 -0
- package/src/store/chat/slices/translate/action.test.ts +26 -32
- package/src/store/chat/slices/translate/action.ts +3 -3
- /package/packages/conversation-flow/src/__tests__/fixtures/inputs/{complex-scenario.json → assistantGroup/tools-with-branches.json} +0 -0
- /package/packages/conversation-flow/src/__tests__/fixtures/outputs/{complex-scenario.json → assistantGroup/tools-with-branches.json} +0 -0
- /package/src/features/Conversation/Messages/Group/{GroupContext.tsx → GroupContext.ts} +0 -0
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import isEqual from 'fast-deep-equal';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
forwardRef,
|
|
7
|
-
memo,
|
|
8
|
-
useCallback,
|
|
9
|
-
useEffect,
|
|
10
|
-
useMemo,
|
|
11
|
-
useRef,
|
|
12
|
-
useState,
|
|
13
|
-
} from 'react';
|
|
14
|
-
import { Flexbox } from 'react-layout-kit';
|
|
15
|
-
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
|
4
|
+
import { ReactNode, memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { VList, VListHandle } from 'virtua';
|
|
16
6
|
|
|
17
7
|
import WideScreenContainer from '@/features/Conversation/components/WideScreenContainer';
|
|
18
8
|
import { useChatStore } from '@/store/chat';
|
|
@@ -20,11 +10,7 @@ import { displayMessageSelectors } from '@/store/chat/selectors';
|
|
|
20
10
|
|
|
21
11
|
import AutoScroll from '../AutoScroll';
|
|
22
12
|
import SkeletonList from '../SkeletonList';
|
|
23
|
-
import {
|
|
24
|
-
VirtuosoContext,
|
|
25
|
-
resetVirtuosoVisibleItems,
|
|
26
|
-
setVirtuosoGlobalRef,
|
|
27
|
-
} from './VirtuosoContext';
|
|
13
|
+
import { VirtuaContext, resetVirtuaVisibleItems, setVirtuaGlobalRef } from './VirtuosoContext';
|
|
28
14
|
|
|
29
15
|
interface VirtualizedListProps {
|
|
30
16
|
dataSource: string[];
|
|
@@ -32,78 +18,124 @@ interface VirtualizedListProps {
|
|
|
32
18
|
mobile?: boolean;
|
|
33
19
|
}
|
|
34
20
|
|
|
35
|
-
const List = forwardRef(({ ...props }, ref) => {
|
|
36
|
-
return (
|
|
37
|
-
<Flexbox>
|
|
38
|
-
<WideScreenContainer id={'chatlist-list'} ref={ref} {...props} />
|
|
39
|
-
</Flexbox>
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
21
|
const VirtualizedList = memo<VirtualizedListProps>(({ mobile, dataSource, itemContent }) => {
|
|
44
|
-
const
|
|
22
|
+
const virtuaRef = useRef<VListHandle>(null);
|
|
45
23
|
const prevDataLengthRef = useRef(dataSource.length);
|
|
46
24
|
const [atBottom, setAtBottom] = useState(true);
|
|
47
25
|
const [isScrolling, setIsScrolling] = useState(false);
|
|
26
|
+
// eslint-disable-next-line no-undef
|
|
27
|
+
const scrollEndTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
48
28
|
|
|
49
29
|
const [isFirstLoading, isCurrentChatLoaded] = useChatStore((s) => [
|
|
50
30
|
displayMessageSelectors.currentChatLoadingState(s),
|
|
51
31
|
displayMessageSelectors.isCurrentDisplayChatLoaded(s),
|
|
52
32
|
]);
|
|
53
33
|
|
|
54
|
-
const
|
|
55
|
-
|
|
34
|
+
const atBottomThreshold = 200 * (mobile ? 2 : 1);
|
|
35
|
+
|
|
36
|
+
// Check if at bottom based on scroll position
|
|
37
|
+
const checkAtBottom = useCallback(() => {
|
|
38
|
+
const ref = virtuaRef.current;
|
|
39
|
+
if (!ref) return false;
|
|
40
|
+
|
|
41
|
+
const scrollOffset = ref.scrollOffset;
|
|
42
|
+
const scrollSize = ref.scrollSize;
|
|
43
|
+
const viewportSize = ref.viewportSize;
|
|
44
|
+
|
|
45
|
+
return scrollSize - scrollOffset - viewportSize <= atBottomThreshold;
|
|
46
|
+
}, [atBottomThreshold]);
|
|
47
|
+
|
|
48
|
+
// Handle scroll events
|
|
49
|
+
const handleScroll = useCallback(() => {
|
|
50
|
+
setIsScrolling(true);
|
|
51
|
+
|
|
52
|
+
// Check if at bottom
|
|
53
|
+
const isAtBottom = checkAtBottom();
|
|
54
|
+
setAtBottom(isAtBottom);
|
|
55
|
+
|
|
56
|
+
// Clear existing timer
|
|
57
|
+
if (scrollEndTimerRef.current) {
|
|
58
|
+
clearTimeout(scrollEndTimerRef.current);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Set new timer for scroll end
|
|
62
|
+
scrollEndTimerRef.current = setTimeout(() => {
|
|
63
|
+
setIsScrolling(false);
|
|
64
|
+
}, 150);
|
|
65
|
+
}, [checkAtBottom]);
|
|
66
|
+
|
|
67
|
+
const handleScrollEnd = useCallback(() => {
|
|
68
|
+
setIsScrolling(false);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
// Auto scroll to bottom when new messages arrive
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const shouldScroll = dataSource.length > prevDataLengthRef.current;
|
|
56
74
|
prevDataLengthRef.current = dataSource.length;
|
|
57
|
-
|
|
75
|
+
|
|
76
|
+
if (shouldScroll && virtuaRef.current) {
|
|
77
|
+
virtuaRef.current.scrollToIndex(dataSource.length - 2, { align: 'start', smooth: true });
|
|
78
|
+
}
|
|
58
79
|
}, [dataSource.length]);
|
|
59
80
|
|
|
60
81
|
const scrollToBottom = useCallback(
|
|
61
82
|
(behavior: 'auto' | 'smooth' = 'smooth') => {
|
|
62
83
|
if (atBottom) return;
|
|
63
|
-
if (!
|
|
64
|
-
|
|
84
|
+
if (!virtuaRef.current) return;
|
|
85
|
+
virtuaRef.current.scrollToIndex(dataSource.length - 1, {
|
|
86
|
+
align: 'end',
|
|
87
|
+
smooth: behavior === 'smooth',
|
|
88
|
+
});
|
|
65
89
|
},
|
|
66
|
-
[atBottom],
|
|
90
|
+
[atBottom, dataSource.length],
|
|
67
91
|
);
|
|
68
92
|
|
|
69
|
-
const components = useMemo(() => ({ List }), []);
|
|
70
|
-
const computeItemKey = useCallback((index: number, item: string) => item, []);
|
|
71
|
-
|
|
72
93
|
useEffect(() => {
|
|
73
|
-
|
|
94
|
+
setVirtuaGlobalRef(virtuaRef);
|
|
74
95
|
|
|
75
96
|
return () => {
|
|
76
|
-
|
|
97
|
+
setVirtuaGlobalRef(null);
|
|
77
98
|
};
|
|
78
|
-
}, [
|
|
99
|
+
}, [virtuaRef]);
|
|
79
100
|
|
|
80
101
|
useEffect(() => {
|
|
81
102
|
return () => {
|
|
82
|
-
|
|
103
|
+
resetVirtuaVisibleItems();
|
|
104
|
+
if (scrollEndTimerRef.current) {
|
|
105
|
+
clearTimeout(scrollEndTimerRef.current);
|
|
106
|
+
}
|
|
83
107
|
};
|
|
84
108
|
}, []);
|
|
85
109
|
|
|
86
|
-
//
|
|
87
|
-
|
|
110
|
+
// Scroll to bottom on initial render
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (virtuaRef.current && dataSource.length > 0) {
|
|
113
|
+
virtuaRef.current.scrollToIndex(dataSource.length - 1, { align: 'end' });
|
|
114
|
+
}
|
|
115
|
+
}, [isCurrentChatLoaded]);
|
|
88
116
|
|
|
89
117
|
// first time loading or not loaded
|
|
90
118
|
if (isFirstLoading || !isCurrentChatLoaded) return <SkeletonList mobile={mobile} />;
|
|
91
119
|
|
|
92
120
|
return (
|
|
93
|
-
<
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
components={components}
|
|
98
|
-
computeItemKey={computeItemKey}
|
|
121
|
+
<VirtuaContext value={virtuaRef}>
|
|
122
|
+
<VList
|
|
123
|
+
// bufferSize should be 2 times the height of the window
|
|
124
|
+
bufferSize={typeof window !== 'undefined' ? window.innerHeight : 0}
|
|
99
125
|
data={dataSource}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
onScroll={handleScroll}
|
|
127
|
+
onScrollEnd={handleScrollEnd}
|
|
128
|
+
ref={virtuaRef}
|
|
129
|
+
reverse
|
|
130
|
+
style={{ height: '100%' }}
|
|
131
|
+
>
|
|
132
|
+
{(data, index) => (
|
|
133
|
+
<WideScreenContainer key={data} style={{ position: 'relative' }}>
|
|
134
|
+
{itemContent(index, data, { virtuaRef })}
|
|
135
|
+
</WideScreenContainer>
|
|
136
|
+
)}
|
|
137
|
+
</VList>
|
|
138
|
+
|
|
107
139
|
<WideScreenContainer
|
|
108
140
|
onChange={() => {
|
|
109
141
|
if (!atBottom) return;
|
|
@@ -117,21 +149,23 @@ const VirtualizedList = memo<VirtualizedListProps>(({ mobile, dataSource, itemCo
|
|
|
117
149
|
atBottom={atBottom}
|
|
118
150
|
isScrolling={isScrolling}
|
|
119
151
|
onScrollToBottom={(type) => {
|
|
120
|
-
const
|
|
152
|
+
const virtua = virtuaRef.current;
|
|
153
|
+
if (!virtua) return;
|
|
154
|
+
|
|
121
155
|
switch (type) {
|
|
122
156
|
case 'auto': {
|
|
123
|
-
|
|
157
|
+
virtua.scrollToIndex(dataSource.length - 1, { align: 'end' });
|
|
124
158
|
break;
|
|
125
159
|
}
|
|
126
160
|
case 'click': {
|
|
127
|
-
|
|
161
|
+
virtua.scrollToIndex(dataSource.length - 1, { align: 'end', smooth: true });
|
|
128
162
|
break;
|
|
129
163
|
}
|
|
130
164
|
}
|
|
131
165
|
}}
|
|
132
166
|
/>
|
|
133
167
|
</WideScreenContainer>
|
|
134
|
-
</
|
|
168
|
+
</VirtuaContext>
|
|
135
169
|
);
|
|
136
170
|
}, isEqual);
|
|
137
171
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { createStyles } from 'antd-style';
|
|
4
|
+
import isEqual from 'fast-deep-equal';
|
|
4
5
|
import { memo, useEffect } from 'react';
|
|
5
6
|
import { Flexbox, FlexboxProps } from 'react-layout-kit';
|
|
6
7
|
|
|
@@ -32,15 +33,18 @@ const WideScreenContainer = memo<WideScreenContainerProps>(
|
|
|
32
33
|
}, [wideScreen]);
|
|
33
34
|
|
|
34
35
|
return (
|
|
35
|
-
<Flexbox
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
<Flexbox width={'100%'}>
|
|
37
|
+
<Flexbox
|
|
38
|
+
className={cx(styles.container, className)}
|
|
39
|
+
width={wideScreen ? '100%' : `min(${CONVERSATION_MIN_WIDTH}px, 100%)`}
|
|
40
|
+
{...rest}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</Flexbox>
|
|
41
44
|
</Flexbox>
|
|
42
45
|
);
|
|
43
46
|
},
|
|
47
|
+
isEqual,
|
|
44
48
|
);
|
|
45
49
|
|
|
46
50
|
export default WideScreenContainer;
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
DownloadIcon,
|
|
7
7
|
Edit,
|
|
8
8
|
LanguagesIcon,
|
|
9
|
+
ListChevronsDownUp,
|
|
10
|
+
ListChevronsUpDown,
|
|
9
11
|
ListRestart,
|
|
10
12
|
Play,
|
|
11
13
|
RotateCcw,
|
|
@@ -27,12 +29,14 @@ const translateStyle = css`
|
|
|
27
29
|
|
|
28
30
|
interface ChatListActionsBar {
|
|
29
31
|
branching: ActionIconGroupItemType;
|
|
32
|
+
collapse: ActionIconGroupItemType;
|
|
30
33
|
continueGeneration: ActionIconGroupItemType;
|
|
31
34
|
copy: ActionIconGroupItemType;
|
|
32
35
|
del: ActionIconGroupItemType;
|
|
33
36
|
delAndRegenerate: ActionIconGroupItemType;
|
|
34
37
|
divider: { type: 'divider' };
|
|
35
38
|
edit: ActionIconGroupItemType;
|
|
39
|
+
expand: ActionIconGroupItemType;
|
|
36
40
|
export: ActionIconGroupItemType;
|
|
37
41
|
regenerate: ActionIconGroupItemType;
|
|
38
42
|
share: ActionIconGroupItemType;
|
|
@@ -58,6 +62,11 @@ export const useChatListActionsBar = ({
|
|
|
58
62
|
key: 'branching',
|
|
59
63
|
label: t('branching'),
|
|
60
64
|
},
|
|
65
|
+
collapse: {
|
|
66
|
+
icon: ListChevronsDownUp,
|
|
67
|
+
key: 'collapse',
|
|
68
|
+
label: t('messageAction.collapse', { ns: 'chat' }),
|
|
69
|
+
},
|
|
61
70
|
continueGeneration: {
|
|
62
71
|
disabled: isContinuing,
|
|
63
72
|
icon: ArrowDownFromLine,
|
|
@@ -93,6 +102,11 @@ export const useChatListActionsBar = ({
|
|
|
93
102
|
key: 'edit',
|
|
94
103
|
label: t('edit'),
|
|
95
104
|
},
|
|
105
|
+
expand: {
|
|
106
|
+
icon: ListChevronsUpDown,
|
|
107
|
+
key: 'expand',
|
|
108
|
+
label: t('messageAction.expand', { ns: 'chat' }),
|
|
109
|
+
},
|
|
96
110
|
export: {
|
|
97
111
|
icon: DownloadIcon,
|
|
98
112
|
key: 'export',
|
|
@@ -2,7 +2,7 @@ import { MouseEventHandler, use, useCallback } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { useChatStore } from '@/store/chat';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { VirtuaContext } from '../components/VirtualizedList/VirtuosoContext';
|
|
6
6
|
|
|
7
7
|
interface UseDoubleClickEditProps {
|
|
8
8
|
disableEditing?: boolean;
|
|
@@ -20,7 +20,7 @@ export const useDoubleClickEdit = ({
|
|
|
20
20
|
index,
|
|
21
21
|
}: UseDoubleClickEditProps) => {
|
|
22
22
|
const [toggleMessageEditing] = useChatStore((s) => [s.toggleMessageEditing]);
|
|
23
|
-
const
|
|
23
|
+
const virtuaRef = use(VirtuaContext);
|
|
24
24
|
|
|
25
25
|
return useCallback<MouseEventHandler<HTMLDivElement>>(
|
|
26
26
|
(e) => {
|
|
@@ -35,7 +35,7 @@ export const useDoubleClickEdit = ({
|
|
|
35
35
|
|
|
36
36
|
toggleMessageEditing(id, true);
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
virtuaRef?.current?.scrollToIndex(index, { align: 'start' });
|
|
39
39
|
},
|
|
40
40
|
[role, disableEditing],
|
|
41
41
|
);
|
|
@@ -106,7 +106,7 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
|
|
|
106
106
|
// 初始化 OIDC 客户端(仅在客户端)
|
|
107
107
|
useEffect(() => {
|
|
108
108
|
if (typeof window !== 'undefined') {
|
|
109
|
-
const baseUrl = process.env.NEXT_PUBLIC_MARKET_BASE_URL || '
|
|
109
|
+
const baseUrl = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
|
|
110
110
|
const desktopRedirectUri = new URL(MARKET_OIDC_ENDPOINTS.desktopCallback, baseUrl).toString();
|
|
111
111
|
|
|
112
112
|
// 桌面端使用 Market 手动维护的 Web 回调,Web 端使用当前域名
|
|
@@ -21,16 +21,16 @@ describe('MCPClient', () => {
|
|
|
21
21
|
await mcpClient.initialize();
|
|
22
22
|
// Add a small delay to allow the server process to fully start (optional, but can help)
|
|
23
23
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
24
|
-
});
|
|
24
|
+
}, 30000);
|
|
25
25
|
|
|
26
26
|
afterEach(async () => {
|
|
27
27
|
// Assume SDK client/transport handles process termination gracefully
|
|
28
28
|
// If processes leak, more explicit cleanup might be needed here
|
|
29
|
-
});
|
|
29
|
+
}, 30000);
|
|
30
30
|
|
|
31
31
|
it('should create and initialize an instance with stdio transport', () => {
|
|
32
32
|
expect(mcpClient).toBeInstanceOf(MCPClient);
|
|
33
|
-
});
|
|
33
|
+
}, 30000);
|
|
34
34
|
|
|
35
35
|
it('should list tools via stdio', async () => {
|
|
36
36
|
const result = await mcpClient.listTools();
|
|
@@ -40,7 +40,7 @@ describe('MCPClient', () => {
|
|
|
40
40
|
|
|
41
41
|
// Expect the tools defined in mock-sdk-server.ts
|
|
42
42
|
expect(result).toMatchSnapshot();
|
|
43
|
-
});
|
|
43
|
+
}, 30000);
|
|
44
44
|
|
|
45
45
|
it('should call the "echo" tool via stdio', async () => {
|
|
46
46
|
const toolName = 'echo';
|
|
@@ -52,7 +52,7 @@ describe('MCPClient', () => {
|
|
|
52
52
|
|
|
53
53
|
const result = await mcpClient.callTool(toolName, toolArgs);
|
|
54
54
|
expect(result).toEqual(expectedResult);
|
|
55
|
-
});
|
|
55
|
+
}, 30000);
|
|
56
56
|
|
|
57
57
|
it('should call the "add" tool via stdio', async () => {
|
|
58
58
|
const toolName = 'add';
|
|
@@ -62,7 +62,7 @@ describe('MCPClient', () => {
|
|
|
62
62
|
expect(result).toEqual({
|
|
63
63
|
content: [{ type: 'text', text: 'The sum is: 12' }],
|
|
64
64
|
});
|
|
65
|
-
});
|
|
65
|
+
}, 30000);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
// Error Handling tests remain the same...
|
|
@@ -18,6 +18,7 @@ export default {
|
|
|
18
18
|
availableAgents: '可用助手',
|
|
19
19
|
backToBottom: '跳转至当前',
|
|
20
20
|
chatList: {
|
|
21
|
+
expandMessage: '展开消息',
|
|
21
22
|
longMessageDetail: '查看详情',
|
|
22
23
|
},
|
|
23
24
|
clearCurrentMessages: '清空当前会话消息',
|
|
@@ -188,9 +189,11 @@ export default {
|
|
|
188
189
|
},
|
|
189
190
|
|
|
190
191
|
messageAction: {
|
|
192
|
+
collapse: '收起消息',
|
|
191
193
|
continueGeneration: '继续生成',
|
|
192
194
|
delAndRegenerate: '删除并重新生成',
|
|
193
195
|
deleteDisabledByThreads: '存在子话题,不能删除',
|
|
196
|
+
expand: '展开消息',
|
|
194
197
|
regenerate: '重新生成',
|
|
195
198
|
},
|
|
196
199
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
|
2
2
|
// Disable the auto sort key eslint rule to make the code more logic and readable
|
|
3
|
-
import { DEFAULT_AGENT_CHAT_CONFIG, INBOX_SESSION_ID } from '@lobechat/const';
|
|
3
|
+
import { DEFAULT_AGENT_CHAT_CONFIG, INBOX_SESSION_ID, LOADING_FLAT } from '@lobechat/const';
|
|
4
4
|
import {
|
|
5
5
|
ChatImageItem,
|
|
6
6
|
ChatVideoItem,
|
|
@@ -124,6 +124,14 @@ export const conversationLifecycle: StateCreator<
|
|
|
124
124
|
imageList: tempImages.length > 0 ? tempImages : undefined,
|
|
125
125
|
videoList: tempVideos.length > 0 ? tempVideos : undefined,
|
|
126
126
|
});
|
|
127
|
+
get().optimisticCreateTmpMessage({
|
|
128
|
+
content: LOADING_FLAT,
|
|
129
|
+
role: 'assistant',
|
|
130
|
+
sessionId: activeId,
|
|
131
|
+
// if there is activeTopicId,then add topicId to message
|
|
132
|
+
topicId: activeTopicId,
|
|
133
|
+
threadId: activeThreadId,
|
|
134
|
+
});
|
|
127
135
|
get().internal_toggleMessageLoading(true, tempId);
|
|
128
136
|
|
|
129
137
|
const operationKey = messageMapKey(activeId, activeTopicId);
|
|
@@ -43,6 +43,10 @@ export interface MessagePublicApiAction {
|
|
|
43
43
|
updateMessageInput: (message: string) => void;
|
|
44
44
|
modifyMessageContent: (id: string, content: string) => Promise<void>;
|
|
45
45
|
toggleMessageEditing: (id: string, editing: boolean) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Toggle message collapsed state
|
|
48
|
+
*/
|
|
49
|
+
toggleMessageCollapsed: (id: string, collapsed?: boolean) => Promise<void>;
|
|
46
50
|
|
|
47
51
|
// ===== Others ===== //
|
|
48
52
|
copyMessage: (id: string, content: string) => Promise<void>;
|
|
@@ -241,4 +245,17 @@ export const messagePublicApi: StateCreator<
|
|
|
241
245
|
|
|
242
246
|
await get().optimisticUpdateMessageContent(id, content);
|
|
243
247
|
},
|
|
248
|
+
|
|
249
|
+
toggleMessageCollapsed: async (id, collapsed) => {
|
|
250
|
+
const message = displayMessageSelectors.getDisplayMessageById(id)(get());
|
|
251
|
+
if (!message) return;
|
|
252
|
+
|
|
253
|
+
// 如果没有传入 collapsed,则取反当前状态
|
|
254
|
+
const nextCollapsed = collapsed ?? !message.metadata?.collapsed;
|
|
255
|
+
|
|
256
|
+
// 直接调用现有的 optimisticUpdateMessageMetadata
|
|
257
|
+
await get().optimisticUpdateMessageMetadata(id, {
|
|
258
|
+
collapsed: nextCollapsed,
|
|
259
|
+
});
|
|
260
|
+
},
|
|
244
261
|
});
|
|
@@ -85,7 +85,7 @@ const activeDisplayMessages = (s: ChatStoreState): UIChatMessage[] => {
|
|
|
85
85
|
/**
|
|
86
86
|
* Get display message by ID (searches in messagesMap including assistantGroup children)
|
|
87
87
|
*/
|
|
88
|
-
const getDisplayMessageById = (id: string) => (s: ChatStoreState) =>
|
|
88
|
+
export const getDisplayMessageById = (id: string) => (s: ChatStoreState) =>
|
|
89
89
|
chatHelpers.getMessageById(activeDisplayMessages(s), id);
|
|
90
90
|
|
|
91
91
|
const lastDisplayMessageId = (s: ChatStoreState) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChatStoreState } from '../../../initialState';
|
|
2
2
|
import { mainDisplayChatIDs } from './chat';
|
|
3
3
|
import { getDbMessageByToolCallId } from './dbMessage';
|
|
4
|
+
import { getDisplayMessageById } from './displayMessage';
|
|
4
5
|
|
|
5
6
|
const isMessageEditing = (id: string) => (s: ChatStoreState) => s.messageEditingIds.includes(id);
|
|
6
7
|
const isMessageLoading = (id: string) => (s: ChatStoreState) => s.messageLoadingIds.includes(id);
|
|
@@ -13,6 +14,11 @@ const isMessageInRAGFlow = (id: string) => (s: ChatStoreState) =>
|
|
|
13
14
|
const isMessageInChatReasoning = (id: string) => (s: ChatStoreState) =>
|
|
14
15
|
s.reasoningLoadingIds.includes(id);
|
|
15
16
|
|
|
17
|
+
const isMessageCollapsed = (id: string) => (s: ChatStoreState) => {
|
|
18
|
+
const message = getDisplayMessageById(id)(s);
|
|
19
|
+
return message?.metadata?.collapsed ?? false;
|
|
20
|
+
};
|
|
21
|
+
|
|
16
22
|
const isPluginApiInvoking = (id: string) => (s: ChatStoreState) =>
|
|
17
23
|
s.pluginApiLoadingIds.includes(id);
|
|
18
24
|
|
|
@@ -71,6 +77,7 @@ export const messageStateSelectors = {
|
|
|
71
77
|
isHasMessageLoading,
|
|
72
78
|
isInRAGFlow,
|
|
73
79
|
isInToolsCalling,
|
|
80
|
+
isMessageCollapsed,
|
|
74
81
|
isMessageContinuing,
|
|
75
82
|
isMessageEditing,
|
|
76
83
|
isMessageGenerating,
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { chainLangDetect } from '@lobechat/prompts';
|
|
2
|
-
import { chainTranslate } from '@lobechat/prompts';
|
|
3
1
|
import { act, renderHook } from '@testing-library/react';
|
|
4
2
|
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
3
|
|
|
@@ -9,7 +7,7 @@ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
|
|
9
7
|
|
|
10
8
|
import { useChatStore } from '../../store';
|
|
11
9
|
|
|
12
|
-
// Mock messageService
|
|
10
|
+
// Mock messageService and chatService
|
|
13
11
|
vi.mock('@/services/message', () => ({
|
|
14
12
|
messageService: {
|
|
15
13
|
updateMessageTTS: vi.fn(),
|
|
@@ -24,27 +22,20 @@ vi.mock('@/services/chat', () => ({
|
|
|
24
22
|
},
|
|
25
23
|
}));
|
|
26
24
|
|
|
27
|
-
vi.mock('@/
|
|
28
|
-
|
|
29
|
-
}))
|
|
30
|
-
|
|
31
|
-
vi.mock('@/chains/translate', () => ({
|
|
32
|
-
chainTranslate: vi.fn(),
|
|
25
|
+
vi.mock('@/store/user', () => ({
|
|
26
|
+
useUserStore: {
|
|
27
|
+
getState: vi.fn(() => ({})),
|
|
28
|
+
},
|
|
33
29
|
}));
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
vi.mock('@/store/user/selectors', () => ({
|
|
32
|
+
systemAgentSelectors: {
|
|
33
|
+
translation: vi.fn(() => ({})),
|
|
34
|
+
},
|
|
38
35
|
}));
|
|
39
36
|
|
|
40
37
|
beforeEach(() => {
|
|
41
38
|
vi.clearAllMocks();
|
|
42
|
-
useChatStore.setState(
|
|
43
|
-
{
|
|
44
|
-
// ... 初始状态
|
|
45
|
-
},
|
|
46
|
-
false,
|
|
47
|
-
);
|
|
48
39
|
});
|
|
49
40
|
|
|
50
41
|
afterEach(() => {
|
|
@@ -53,26 +44,26 @@ afterEach(() => {
|
|
|
53
44
|
|
|
54
45
|
describe('ChatEnhanceAction', () => {
|
|
55
46
|
describe('translateMessage', () => {
|
|
56
|
-
it('should translate a message to the target language
|
|
57
|
-
const { result } = renderHook(() => useChatStore());
|
|
47
|
+
it('should translate a message to the target language', async () => {
|
|
58
48
|
const messageId = 'message-id';
|
|
59
49
|
const targetLang = 'zh-CN';
|
|
60
50
|
const messageContent = 'Hello World';
|
|
61
51
|
const detectedLang = 'en-US';
|
|
52
|
+
const translatedText = '你好世界';
|
|
62
53
|
|
|
54
|
+
// Setup initial state
|
|
63
55
|
act(() => {
|
|
64
56
|
useChatStore.setState({
|
|
65
57
|
activeId: 'session',
|
|
66
|
-
|
|
58
|
+
dbMessagesMap: {
|
|
67
59
|
[messageMapKey('session')]: [
|
|
68
60
|
{
|
|
69
61
|
id: messageId,
|
|
70
62
|
content: messageContent,
|
|
71
63
|
createdAt: Date.now(),
|
|
72
64
|
updatedAt: Date.now(),
|
|
73
|
-
role: '
|
|
74
|
-
sessionId: '
|
|
75
|
-
topicId: 'test',
|
|
65
|
+
role: 'assistant',
|
|
66
|
+
sessionId: 'session',
|
|
76
67
|
meta: {},
|
|
77
68
|
},
|
|
78
69
|
],
|
|
@@ -80,21 +71,24 @@ describe('ChatEnhanceAction', () => {
|
|
|
80
71
|
});
|
|
81
72
|
});
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
if (params === chainTranslate(messageContent, targetLang)) {
|
|
88
|
-
return Promise.resolve('Hola Mundo');
|
|
89
|
-
}
|
|
90
|
-
return Promise.resolve(undefined);
|
|
74
|
+
// First call for language detection
|
|
75
|
+
(chatService.fetchPresetTaskResult as Mock).mockImplementationOnce(async ({ onFinish }) => {
|
|
76
|
+
if (onFinish) await onFinish(detectedLang);
|
|
91
77
|
});
|
|
92
78
|
|
|
79
|
+
// Second call for translation
|
|
80
|
+
(chatService.fetchPresetTaskResult as Mock).mockImplementationOnce(async ({ onFinish }) => {
|
|
81
|
+
if (onFinish) await onFinish(translatedText);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const { result } = renderHook(() => useChatStore());
|
|
85
|
+
|
|
93
86
|
await act(async () => {
|
|
94
87
|
await result.current.translateMessage(messageId, targetLang);
|
|
95
88
|
});
|
|
96
89
|
|
|
97
90
|
expect(messageService.updateMessageTranslate).toHaveBeenCalled();
|
|
91
|
+
expect(chatService.fetchPresetTaskResult).toHaveBeenCalledTimes(2);
|
|
98
92
|
});
|
|
99
93
|
});
|
|
100
94
|
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { chainLangDetect, chainTranslate } from '@lobechat/prompts';
|
|
2
2
|
import { ChatTranslate, TraceNameMap, TracePayload } from '@lobechat/types';
|
|
3
|
+
import { merge } from '@lobechat/utils';
|
|
3
4
|
import { produce } from 'immer';
|
|
4
5
|
import { StateCreator } from 'zustand/vanilla';
|
|
5
6
|
|
|
6
7
|
import { supportLocales } from '@/locales/resources';
|
|
7
8
|
import { chatService } from '@/services/chat';
|
|
8
9
|
import { messageService } from '@/services/message';
|
|
9
|
-
import {
|
|
10
|
+
import { dbMessageSelectors } from '@/store/chat/selectors';
|
|
10
11
|
import { ChatStore } from '@/store/chat/store';
|
|
11
12
|
import { useUserStore } from '@/store/user';
|
|
12
13
|
import { systemAgentSelectors } from '@/store/user/selectors';
|
|
13
|
-
import { merge } from '@/utils/merge';
|
|
14
14
|
import { setNamespace } from '@/utils/storeDebug';
|
|
15
15
|
|
|
16
16
|
const n = setNamespace('enhance');
|
|
@@ -43,7 +43,7 @@ export const chatTranslate: StateCreator<
|
|
|
43
43
|
translateMessage: async (id, targetLang) => {
|
|
44
44
|
const { internal_toggleChatLoading, updateMessageTranslate, internal_dispatchMessage } = get();
|
|
45
45
|
|
|
46
|
-
const message =
|
|
46
|
+
const message = dbMessageSelectors.getDbMessageById(id)(get());
|
|
47
47
|
if (!message) return;
|
|
48
48
|
|
|
49
49
|
// Get current agent for translation
|
|
File without changes
|
|
File without changes
|
|
File without changes
|