@lobehub/lobehub 2.0.0-next.374 → 2.0.0-next.375
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 +13 -13
- package/locales/zh-CN/chat.json +14 -14
- package/package.json +1 -1
- package/packages/builtin-tool-group-agent-builder/src/ExecutionRuntime/index.ts +31 -9
- package/packages/builtin-tool-group-agent-builder/src/client/Inspector/InviteAgent/index.tsx +28 -12
- package/packages/builtin-tool-group-agent-builder/src/client/Inspector/RemoveAgent/index.tsx +28 -12
- package/packages/builtin-tool-group-agent-builder/src/types.ts +8 -0
- package/packages/context-engine/src/engine/tools/ToolsEngine.ts +16 -10
- package/packages/context-engine/src/engine/tools/__tests__/ToolsEngine.test.ts +142 -0
- package/packages/context-engine/src/engine/tools/types.ts +6 -0
- package/src/features/Conversation/ChatItem/ChatItem.tsx +4 -7
- package/src/features/Conversation/ChatItem/style.ts +0 -3
- package/src/features/Conversation/ChatItem/type.ts +4 -1
- package/src/features/Conversation/Messages/Assistant/index.tsx +6 -2
- package/src/features/Conversation/Messages/AssistantGroup/index.tsx +3 -3
- package/src/features/Conversation/Messages/Supervisor/index.tsx +6 -2
- package/src/features/Conversation/Messages/Task/index.tsx +6 -2
- package/src/features/Conversation/Messages/components/useNewScreen.test.ts +331 -0
- package/src/features/Conversation/Messages/components/useNewScreen.ts +80 -5
- package/src/locales/default/chat.ts +13 -13
- package/src/services/chat/chat.test.ts +125 -45
- package/src/services/chat/index.ts +10 -14
- package/src/services/chat/mecha/agentConfigResolver.ts +8 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +29 -0
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +27 -19
|
@@ -39,7 +39,7 @@ const ChatItem = memo<ChatItemProps>(
|
|
|
39
39
|
disabled = false,
|
|
40
40
|
id,
|
|
41
41
|
style,
|
|
42
|
-
|
|
42
|
+
newScreenMinHeight,
|
|
43
43
|
...rest
|
|
44
44
|
}) => {
|
|
45
45
|
const isUser = placement === 'right';
|
|
@@ -63,15 +63,12 @@ const ChatItem = memo<ChatItemProps>(
|
|
|
63
63
|
return (
|
|
64
64
|
<Flexbox
|
|
65
65
|
align={isUser ? 'flex-end' : 'flex-start'}
|
|
66
|
-
className={cx(
|
|
67
|
-
|
|
68
|
-
styles.container,
|
|
69
|
-
newScreen && styles.newScreen,
|
|
70
|
-
className,
|
|
71
|
-
)}
|
|
66
|
+
className={cx('message-wrapper', styles.container, className)}
|
|
67
|
+
data-message-id={id}
|
|
72
68
|
gap={8}
|
|
73
69
|
paddingBlock={8}
|
|
74
70
|
style={{
|
|
71
|
+
minHeight: newScreenMinHeight,
|
|
75
72
|
paddingInlineStart: isUser ? 36 : 0,
|
|
76
73
|
...style,
|
|
77
74
|
}}
|
|
@@ -35,7 +35,10 @@ export interface ChatItemProps extends Omit<FlexboxProps, 'children' | 'onChange
|
|
|
35
35
|
*/
|
|
36
36
|
message?: ReactNode;
|
|
37
37
|
messageExtra?: ReactNode;
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Dynamic min-height for new screen effect, e.g. "calc(100dvh - 350px)"
|
|
40
|
+
*/
|
|
41
|
+
newScreenMinHeight?: string;
|
|
39
42
|
onAvatarClick?: () => void;
|
|
40
43
|
onDoubleClick?: DivProps['onDoubleClick'];
|
|
41
44
|
/**
|
|
@@ -58,7 +58,11 @@ const AssistantMessage = memo<AssistantMessageProps>(
|
|
|
58
58
|
const editing = useConversationStore(messageStateSelectors.isMessageEditing(id));
|
|
59
59
|
const generating = useConversationStore(messageStateSelectors.isMessageGenerating(id));
|
|
60
60
|
const creating = useConversationStore(messageStateSelectors.isMessageCreating(id));
|
|
61
|
-
const
|
|
61
|
+
const { minHeight } = useNewScreen({
|
|
62
|
+
creating: creating || generating,
|
|
63
|
+
isLatestItem,
|
|
64
|
+
messageId: id,
|
|
65
|
+
});
|
|
62
66
|
|
|
63
67
|
const errorContent = useErrorContent(error);
|
|
64
68
|
|
|
@@ -114,7 +118,7 @@ const AssistantMessage = memo<AssistantMessageProps>(
|
|
|
114
118
|
usage={usage! || metadata}
|
|
115
119
|
/>
|
|
116
120
|
}
|
|
117
|
-
|
|
121
|
+
newScreenMinHeight={minHeight}
|
|
118
122
|
onDoubleClick={onDoubleClick}
|
|
119
123
|
onMouseEnter={onMouseEnter}
|
|
120
124
|
placement={'left'}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import type { AssistantContentBlock } from '@lobechat/types';
|
|
4
4
|
import isEqual from 'fast-deep-equal';
|
|
5
|
-
import dynamic from '@/libs/next/dynamic';
|
|
6
5
|
import { type MouseEventHandler, Suspense, memo, useCallback, useMemo } from 'react';
|
|
7
6
|
|
|
8
7
|
import { MESSAGE_ACTION_BAR_PORTAL_ATTRIBUTES } from '@/const/messageActionPortal';
|
|
9
8
|
import { ChatItem } from '@/features/Conversation/ChatItem';
|
|
10
9
|
import { useNewScreen } from '@/features/Conversation/Messages/components/useNewScreen';
|
|
11
10
|
import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
|
|
11
|
+
import dynamic from '@/libs/next/dynamic';
|
|
12
12
|
import { useAgentStore } from '@/store/agent';
|
|
13
13
|
import { builtinAgentSelectors } from '@/store/agent/selectors';
|
|
14
14
|
import { useGlobalStore } from '@/store/global';
|
|
@@ -68,7 +68,7 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
|
|
|
68
68
|
// Get editing state from ConversationStore
|
|
69
69
|
const editing = useConversationStore(messageStateSelectors.isMessageEditing(contentId || ''));
|
|
70
70
|
const creating = useConversationStore(messageStateSelectors.isMessageCreating(id));
|
|
71
|
-
const
|
|
71
|
+
const { minHeight } = useNewScreen({ creating, isLatestItem, messageId: id });
|
|
72
72
|
|
|
73
73
|
const setMessageItemActionElementPortialContext = useSetMessageItemActionElementPortialContext();
|
|
74
74
|
const setMessageItemActionTypeContext = useSetMessageItemActionTypeContext();
|
|
@@ -113,7 +113,7 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
|
|
|
113
113
|
)
|
|
114
114
|
}
|
|
115
115
|
avatar={avatar}
|
|
116
|
-
|
|
116
|
+
newScreenMinHeight={minHeight}
|
|
117
117
|
onAvatarClick={onAvatarClick}
|
|
118
118
|
onMouseEnter={onMouseEnter}
|
|
119
119
|
placement={'left'}
|
|
@@ -56,7 +56,11 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
|
|
|
56
56
|
// Get editing state from ConversationStore
|
|
57
57
|
const creating = useConversationStore(messageStateSelectors.isMessageCreating(id));
|
|
58
58
|
const generating = useConversationStore(messageStateSelectors.isMessageGenerating(id));
|
|
59
|
-
const
|
|
59
|
+
const { minHeight } = useNewScreen({
|
|
60
|
+
creating: creating || generating,
|
|
61
|
+
isLatestItem,
|
|
62
|
+
messageId: id,
|
|
63
|
+
});
|
|
60
64
|
|
|
61
65
|
const setMessageItemActionElementPortialContext = useSetMessageItemActionElementPortialContext();
|
|
62
66
|
const setMessageItemActionTypeContext = useSetMessageItemActionTypeContext();
|
|
@@ -98,7 +102,7 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
|
|
|
98
102
|
memberAvatars={memberAvatars}
|
|
99
103
|
/>
|
|
100
104
|
)}
|
|
101
|
-
|
|
105
|
+
newScreenMinHeight={minHeight}
|
|
102
106
|
onMouseEnter={onMouseEnter}
|
|
103
107
|
placement={'left'}
|
|
104
108
|
showTitle
|
|
@@ -44,7 +44,11 @@ const TaskMessage = memo<TaskMessageProps>(({ id, index, disableEditing, isLates
|
|
|
44
44
|
const editing = useConversationStore(messageStateSelectors.isMessageEditing(id));
|
|
45
45
|
const generating = useConversationStore(messageStateSelectors.isMessageGenerating(id));
|
|
46
46
|
const creating = useConversationStore(messageStateSelectors.isMessageCreating(id));
|
|
47
|
-
const
|
|
47
|
+
const { minHeight } = useNewScreen({
|
|
48
|
+
creating: generating || creating,
|
|
49
|
+
isLatestItem,
|
|
50
|
+
messageId: id,
|
|
51
|
+
});
|
|
48
52
|
|
|
49
53
|
const errorContent = useErrorContent(error);
|
|
50
54
|
|
|
@@ -84,7 +88,7 @@ const TaskMessage = memo<TaskMessageProps>(({ id, index, disableEditing, isLates
|
|
|
84
88
|
id={id}
|
|
85
89
|
loading={generating}
|
|
86
90
|
message={message}
|
|
87
|
-
|
|
91
|
+
newScreenMinHeight={minHeight}
|
|
88
92
|
onAvatarClick={onAvatarClick}
|
|
89
93
|
onDoubleClick={onDoubleClick}
|
|
90
94
|
placement={'left'}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { useNewScreen } from './useNewScreen';
|
|
5
|
+
|
|
6
|
+
// Mock useConversationStore
|
|
7
|
+
const mockGetViewportSize = vi.fn();
|
|
8
|
+
vi.mock('../../store', () => ({
|
|
9
|
+
useConversationStore: vi.fn((selector) =>
|
|
10
|
+
selector({
|
|
11
|
+
virtuaScrollMethods: {
|
|
12
|
+
getViewportSize: mockGetViewportSize,
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe('useNewScreen', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
mockGetViewportSize.mockReturnValue(800);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('when not the latest item', () => {
|
|
25
|
+
it('should return undefined minHeight', () => {
|
|
26
|
+
const { result } = renderHook(() =>
|
|
27
|
+
useNewScreen({
|
|
28
|
+
creating: true,
|
|
29
|
+
isLatestItem: false,
|
|
30
|
+
messageId: 'msg-1',
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(result.current.minHeight).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should clear minHeight when becoming not the latest item', () => {
|
|
38
|
+
const { result, rerender } = renderHook(
|
|
39
|
+
({ isLatestItem }) =>
|
|
40
|
+
useNewScreen({
|
|
41
|
+
creating: true,
|
|
42
|
+
isLatestItem,
|
|
43
|
+
messageId: 'msg-1',
|
|
44
|
+
}),
|
|
45
|
+
{
|
|
46
|
+
initialProps: { isLatestItem: true },
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Initially, it will use fallback since no DOM elements exist
|
|
51
|
+
expect(result.current.minHeight).toBeDefined();
|
|
52
|
+
|
|
53
|
+
// When it's no longer the latest item
|
|
54
|
+
rerender({ isLatestItem: false });
|
|
55
|
+
|
|
56
|
+
expect(result.current.minHeight).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('when latest item but not creating', () => {
|
|
61
|
+
it('should not update minHeight (preserve existing value)', () => {
|
|
62
|
+
const { result, rerender } = renderHook(
|
|
63
|
+
({ creating }) =>
|
|
64
|
+
useNewScreen({
|
|
65
|
+
creating,
|
|
66
|
+
isLatestItem: true,
|
|
67
|
+
messageId: 'msg-1',
|
|
68
|
+
}),
|
|
69
|
+
{
|
|
70
|
+
initialProps: { creating: true },
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Initially sets minHeight (fallback)
|
|
75
|
+
const initialMinHeight = result.current.minHeight;
|
|
76
|
+
expect(initialMinHeight).toBeDefined();
|
|
77
|
+
|
|
78
|
+
// When creating ends, should preserve the minHeight
|
|
79
|
+
rerender({ creating: false });
|
|
80
|
+
|
|
81
|
+
expect(result.current.minHeight).toBe(initialMinHeight);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('when latest item and creating', () => {
|
|
86
|
+
it('should calculate minHeight based on viewport and previous message height', () => {
|
|
87
|
+
// Setup DOM mocks
|
|
88
|
+
const mockPrevMessageEl = {
|
|
89
|
+
getBoundingClientRect: () => ({ height: 150 }),
|
|
90
|
+
};
|
|
91
|
+
const mockPrevWrapper = {
|
|
92
|
+
querySelector: vi.fn().mockReturnValue(mockPrevMessageEl),
|
|
93
|
+
getBoundingClientRect: () => ({ height: 150 }),
|
|
94
|
+
};
|
|
95
|
+
const mockCurrentWrapper = {
|
|
96
|
+
dataset: { index: '1' },
|
|
97
|
+
};
|
|
98
|
+
const mockMessageEl = {
|
|
99
|
+
closest: vi.fn().mockReturnValue(mockCurrentWrapper),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
103
|
+
if (selector === '[data-message-id="msg-1"]') {
|
|
104
|
+
return mockMessageEl as unknown as Element;
|
|
105
|
+
}
|
|
106
|
+
if (selector === '[data-index="0"]') {
|
|
107
|
+
return mockPrevWrapper as unknown as Element;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
mockGetViewportSize.mockReturnValue(800);
|
|
113
|
+
|
|
114
|
+
const { result } = renderHook(() =>
|
|
115
|
+
useNewScreen({
|
|
116
|
+
creating: true,
|
|
117
|
+
isLatestItem: true,
|
|
118
|
+
messageId: 'msg-1',
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// minHeight = viewportHeight - prevHeight - EXTRA_PADDING = 800 - 150 - 0 = 650
|
|
123
|
+
expect(result.current.minHeight).toBe('650px');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should use fallback height when previous message element not found', () => {
|
|
127
|
+
// Setup DOM mocks - no previous element
|
|
128
|
+
const mockCurrentWrapper = {
|
|
129
|
+
dataset: { index: '0' },
|
|
130
|
+
querySelector: vi.fn().mockReturnValue(null),
|
|
131
|
+
};
|
|
132
|
+
const mockMessageEl = {
|
|
133
|
+
closest: vi.fn().mockReturnValue(mockCurrentWrapper),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
137
|
+
if (selector === '[data-message-id="msg-1"]') {
|
|
138
|
+
return mockMessageEl as unknown as Element;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
mockGetViewportSize.mockReturnValue(800);
|
|
144
|
+
|
|
145
|
+
const { result } = renderHook(() =>
|
|
146
|
+
useNewScreen({
|
|
147
|
+
creating: true,
|
|
148
|
+
isLatestItem: true,
|
|
149
|
+
messageId: 'msg-1',
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// fallback: viewportHeight - DEFAULT_USER_MESSAGE_HEIGHT - EXTRA_PADDING = 800 - 200 - 0 = 600
|
|
154
|
+
expect(result.current.minHeight).toBe('600px');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should return undefined when calculated height is less than or equal to 0', () => {
|
|
158
|
+
// Setup DOM mocks with very large previous message
|
|
159
|
+
const mockPrevMessageEl = {
|
|
160
|
+
getBoundingClientRect: () => ({ height: 900 }), // Larger than viewport
|
|
161
|
+
};
|
|
162
|
+
const mockPrevWrapper = {
|
|
163
|
+
querySelector: vi.fn().mockReturnValue(mockPrevMessageEl),
|
|
164
|
+
getBoundingClientRect: () => ({ height: 900 }),
|
|
165
|
+
};
|
|
166
|
+
const mockCurrentWrapper = {
|
|
167
|
+
dataset: { index: '1' },
|
|
168
|
+
};
|
|
169
|
+
const mockMessageEl = {
|
|
170
|
+
closest: vi.fn().mockReturnValue(mockCurrentWrapper),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
174
|
+
if (selector === '[data-message-id="msg-1"]') {
|
|
175
|
+
return mockMessageEl as unknown as Element;
|
|
176
|
+
}
|
|
177
|
+
if (selector === '[data-index="0"]') {
|
|
178
|
+
return mockPrevWrapper as unknown as Element;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
mockGetViewportSize.mockReturnValue(800);
|
|
184
|
+
|
|
185
|
+
const { result } = renderHook(() =>
|
|
186
|
+
useNewScreen({
|
|
187
|
+
creating: true,
|
|
188
|
+
isLatestItem: true,
|
|
189
|
+
messageId: 'msg-1',
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// minHeight = 800 - 900 - 0 = -100, should be undefined
|
|
194
|
+
expect(result.current.minHeight).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should use window.innerHeight when virtuaScrollMethods is not available', () => {
|
|
198
|
+
// Reset mock to return undefined for virtuaScrollMethods
|
|
199
|
+
vi.mocked(mockGetViewportSize).mockReturnValue(undefined as unknown as number);
|
|
200
|
+
|
|
201
|
+
// Mock window.innerHeight
|
|
202
|
+
Object.defineProperty(window, 'innerHeight', { value: 768, writable: true });
|
|
203
|
+
|
|
204
|
+
// Setup DOM mocks - no previous element (fallback case)
|
|
205
|
+
const mockCurrentWrapper = {
|
|
206
|
+
dataset: { index: '0' },
|
|
207
|
+
querySelector: vi.fn().mockReturnValue(null),
|
|
208
|
+
};
|
|
209
|
+
const mockMessageEl = {
|
|
210
|
+
closest: vi.fn().mockReturnValue(mockCurrentWrapper),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
214
|
+
if (selector === '[data-message-id="msg-1"]') {
|
|
215
|
+
return mockMessageEl as unknown as Element;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const { result } = renderHook(() =>
|
|
221
|
+
useNewScreen({
|
|
222
|
+
creating: true,
|
|
223
|
+
isLatestItem: true,
|
|
224
|
+
messageId: 'msg-1',
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// fallback: window.innerHeight - DEFAULT_USER_MESSAGE_HEIGHT = 768 - 200 = 568
|
|
229
|
+
expect(result.current.minHeight).toBe('568px');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('edge cases', () => {
|
|
234
|
+
it('should handle message element not found', () => {
|
|
235
|
+
vi.spyOn(document, 'querySelector').mockReturnValue(null);
|
|
236
|
+
|
|
237
|
+
const { result } = renderHook(() =>
|
|
238
|
+
useNewScreen({
|
|
239
|
+
creating: true,
|
|
240
|
+
isLatestItem: true,
|
|
241
|
+
messageId: 'non-existent',
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Should use fallback
|
|
246
|
+
expect(result.current.minHeight).toBeDefined();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should handle negative prevIndex', () => {
|
|
250
|
+
const mockCurrentWrapper = {
|
|
251
|
+
dataset: { index: '0' }, // First item, prevIndex would be -1
|
|
252
|
+
querySelector: vi.fn().mockReturnValue(null),
|
|
253
|
+
};
|
|
254
|
+
const mockMessageEl = {
|
|
255
|
+
closest: vi.fn().mockReturnValue(mockCurrentWrapper),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
259
|
+
if (selector === '[data-message-id="msg-1"]') {
|
|
260
|
+
return mockMessageEl as unknown as Element;
|
|
261
|
+
}
|
|
262
|
+
// Should not query for [data-index="-1"]
|
|
263
|
+
if (selector === '[data-index="-1"]') {
|
|
264
|
+
throw new Error('Should not query for negative index');
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
mockGetViewportSize.mockReturnValue(800);
|
|
270
|
+
|
|
271
|
+
const { result } = renderHook(() =>
|
|
272
|
+
useNewScreen({
|
|
273
|
+
creating: true,
|
|
274
|
+
isLatestItem: true,
|
|
275
|
+
messageId: 'msg-1',
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Should use fallback without throwing
|
|
280
|
+
expect(result.current.minHeight).toBe('600px');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should recalculate when messageId changes', () => {
|
|
284
|
+
const mockPrevMessageEl = {
|
|
285
|
+
getBoundingClientRect: () => ({ height: 150 }),
|
|
286
|
+
};
|
|
287
|
+
const mockPrevWrapper = {
|
|
288
|
+
querySelector: vi.fn().mockReturnValue(mockPrevMessageEl),
|
|
289
|
+
getBoundingClientRect: () => ({ height: 150 }),
|
|
290
|
+
};
|
|
291
|
+
const mockCurrentWrapper = {
|
|
292
|
+
dataset: { index: '1' },
|
|
293
|
+
};
|
|
294
|
+
const mockMessageEl = {
|
|
295
|
+
closest: vi.fn().mockReturnValue(mockCurrentWrapper),
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
|
|
299
|
+
if (selector.includes('data-message-id')) {
|
|
300
|
+
return mockMessageEl as unknown as Element;
|
|
301
|
+
}
|
|
302
|
+
if (selector === '[data-index="0"]') {
|
|
303
|
+
return mockPrevWrapper as unknown as Element;
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
mockGetViewportSize.mockReturnValue(800);
|
|
309
|
+
|
|
310
|
+
const { result, rerender } = renderHook(
|
|
311
|
+
({ messageId }) =>
|
|
312
|
+
useNewScreen({
|
|
313
|
+
creating: true,
|
|
314
|
+
isLatestItem: true,
|
|
315
|
+
messageId,
|
|
316
|
+
}),
|
|
317
|
+
{
|
|
318
|
+
initialProps: { messageId: 'msg-1' },
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(result.current.minHeight).toBe('650px');
|
|
323
|
+
|
|
324
|
+
// Change messageId - should recalculate
|
|
325
|
+
rerender({ messageId: 'msg-2' });
|
|
326
|
+
|
|
327
|
+
// Still the same value since mocks return same values
|
|
328
|
+
expect(result.current.minHeight).toBe('650px');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -1,16 +1,91 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
1
2
|
import { useEffect, useState } from 'react';
|
|
2
3
|
|
|
4
|
+
import { useConversationStore } from '../../store';
|
|
5
|
+
|
|
6
|
+
const log = debug('lobe-render:Conversation:newScreen');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extra padding if needed
|
|
10
|
+
*/
|
|
11
|
+
const EXTRA_PADDING = 0;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default user message height (fallback)
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_USER_MESSAGE_HEIGHT = 200;
|
|
17
|
+
|
|
3
18
|
export const useNewScreen = ({
|
|
4
19
|
isLatestItem,
|
|
5
20
|
creating,
|
|
21
|
+
messageId,
|
|
6
22
|
}: {
|
|
7
23
|
creating?: boolean;
|
|
8
24
|
isLatestItem?: boolean;
|
|
25
|
+
messageId: string;
|
|
9
26
|
}) => {
|
|
10
|
-
const [
|
|
27
|
+
const [minHeight, setMinHeight] = useState<string | undefined>(undefined);
|
|
28
|
+
const virtuaScrollMethods = useConversationStore((s) => s.virtuaScrollMethods);
|
|
29
|
+
|
|
11
30
|
useEffect(() => {
|
|
12
|
-
|
|
13
|
-
if (isLatestItem
|
|
14
|
-
|
|
15
|
-
|
|
31
|
+
// Clear minHeight when no longer the latest item
|
|
32
|
+
if (!isLatestItem) {
|
|
33
|
+
setMinHeight(undefined);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Only calculate and set minHeight when creating, preserve after creating ends
|
|
38
|
+
if (!creating) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Find current message element by data-message-id
|
|
43
|
+
const messageEl = document.querySelector(`[data-message-id="${messageId}"]`);
|
|
44
|
+
// Find VList item container (has data-index attribute)
|
|
45
|
+
const currentWrapper = messageEl?.closest('[data-index]') as HTMLElement | null;
|
|
46
|
+
// Get current index
|
|
47
|
+
const currentIndex = currentWrapper?.dataset.index;
|
|
48
|
+
|
|
49
|
+
// Find previous VList item by data-index (avoid sibling not existing due to virtualization)
|
|
50
|
+
const prevIndex = currentIndex ? Number(currentIndex) - 1 : -1;
|
|
51
|
+
const prevWrapper =
|
|
52
|
+
prevIndex >= 0 ? document.querySelector(`[data-index="${prevIndex}"]`) : null;
|
|
53
|
+
// Get previous message's .message-wrapper
|
|
54
|
+
const prevMessageEl = prevWrapper?.querySelector('.message-wrapper');
|
|
55
|
+
|
|
56
|
+
// Get real viewport height from VList
|
|
57
|
+
const viewportHeight = virtuaScrollMethods?.getViewportSize?.() || window.innerHeight;
|
|
58
|
+
|
|
59
|
+
if (prevMessageEl) {
|
|
60
|
+
const prevHeight = prevMessageEl.getBoundingClientRect().height;
|
|
61
|
+
|
|
62
|
+
// Goal: userMessage at top, so assistantMinHeight = viewportHeight - userMessageHeight
|
|
63
|
+
const calculatedHeight = viewportHeight - prevHeight - EXTRA_PADDING;
|
|
64
|
+
|
|
65
|
+
log(
|
|
66
|
+
'calculate minHeight: messageId=%s, index=%s, viewportHeight=%d, prevHeight=%d, result=%d',
|
|
67
|
+
messageId,
|
|
68
|
+
currentIndex,
|
|
69
|
+
viewportHeight,
|
|
70
|
+
prevHeight,
|
|
71
|
+
calculatedHeight,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Don't set minHeight if calculated height <= 0
|
|
75
|
+
setMinHeight(calculatedHeight > 0 ? `${calculatedHeight}px` : undefined);
|
|
76
|
+
} else {
|
|
77
|
+
// Fallback: use default value
|
|
78
|
+
const fallbackHeight = viewportHeight - DEFAULT_USER_MESSAGE_HEIGHT - EXTRA_PADDING;
|
|
79
|
+
log(
|
|
80
|
+
'fallback minHeight: messageId=%s, viewportHeight=%d, fallbackHeight=%d',
|
|
81
|
+
messageId,
|
|
82
|
+
viewportHeight,
|
|
83
|
+
fallbackHeight,
|
|
84
|
+
);
|
|
85
|
+
// Don't set minHeight if calculated height <= 0
|
|
86
|
+
setMinHeight(fallbackHeight > 0 ? `${fallbackHeight}px` : undefined);
|
|
87
|
+
}
|
|
88
|
+
}, [isLatestItem, creating, messageId, virtuaScrollMethods]);
|
|
89
|
+
|
|
90
|
+
return { minHeight };
|
|
16
91
|
};
|
|
@@ -48,7 +48,7 @@ export default {
|
|
|
48
48
|
'confirmRemoveChatGroupItemAlert':
|
|
49
49
|
'This Group will be deleted. Group-specific assistants will also be deleted, while external assistants will not be affected.',
|
|
50
50
|
'confirmRemoveGroupItemAlert':
|
|
51
|
-
'You are about to delete this
|
|
51
|
+
'You are about to delete this category. After deletion, its agents will be moved to the default list. Please confirm your action.',
|
|
52
52
|
'confirmRemoveGroupSuccess': 'Group deleted successfully',
|
|
53
53
|
'confirmRemoveSessionItemAlert':
|
|
54
54
|
'You are about to delete this agent. Once deleted, it cannot be retrieved. Please confirm your action.',
|
|
@@ -267,29 +267,29 @@ export default {
|
|
|
267
267
|
'searchAgents': 'Search agents...',
|
|
268
268
|
'selectedAgents': 'Selected agents',
|
|
269
269
|
'sendPlaceholder': 'Ask, create, or start a task, <hotkey><hotkey/>',
|
|
270
|
-
'sessionGroup.config': '
|
|
270
|
+
'sessionGroup.config': 'Category Management',
|
|
271
271
|
'sessionGroup.confirmRemoveGroupAlert':
|
|
272
|
-
'This
|
|
272
|
+
'This category is about to be deleted. After deletion, the agents in this category will be moved to the default list. Please confirm your operation.',
|
|
273
273
|
'sessionGroup.createAgentSuccess': 'Agent created successfully',
|
|
274
|
-
'sessionGroup.createGroup': 'Add New
|
|
275
|
-
'sessionGroup.createGroupFailed': 'Failed to create
|
|
276
|
-
'sessionGroup.createGroupSuccess': '
|
|
274
|
+
'sessionGroup.createGroup': 'Add New Category',
|
|
275
|
+
'sessionGroup.createGroupFailed': 'Failed to create category',
|
|
276
|
+
'sessionGroup.createGroupSuccess': 'Category created successfully',
|
|
277
277
|
'sessionGroup.createSuccess': 'Created successfully',
|
|
278
278
|
'sessionGroup.creatingAgent': 'Creating agent...',
|
|
279
|
-
'sessionGroup.groupName': '
|
|
280
|
-
'sessionGroup.inputPlaceholder': 'Please enter
|
|
281
|
-
'sessionGroup.moveGroup': 'Move to
|
|
282
|
-
'sessionGroup.newGroup': 'New
|
|
279
|
+
'sessionGroup.groupName': 'Category Name',
|
|
280
|
+
'sessionGroup.inputPlaceholder': 'Please enter category name...',
|
|
281
|
+
'sessionGroup.moveGroup': 'Move to Category',
|
|
282
|
+
'sessionGroup.newGroup': 'New Category',
|
|
283
283
|
'sessionGroup.noAvailableAgents': 'No available agents',
|
|
284
284
|
'sessionGroup.noMatchingAgents': 'No matching agents found',
|
|
285
285
|
'sessionGroup.noSelectedAgents': 'Please select agents',
|
|
286
|
-
'sessionGroup.rename': 'Rename
|
|
286
|
+
'sessionGroup.rename': 'Rename Category',
|
|
287
287
|
'sessionGroup.renameSuccess': 'Renamed successfully',
|
|
288
288
|
'sessionGroup.searchAgents': 'Search agents',
|
|
289
289
|
'sessionGroup.selectedAgents': 'Selected agents ({{count}})',
|
|
290
290
|
'sessionGroup.sortSuccess': 'Reorder successful',
|
|
291
|
-
'sessionGroup.sorting': '
|
|
292
|
-
'sessionGroup.tooLong': '
|
|
291
|
+
'sessionGroup.sorting': 'Category sorting updating...',
|
|
292
|
+
'sessionGroup.tooLong': 'Category name length should be between 1-20',
|
|
293
293
|
'shareModal.copy': 'Copy',
|
|
294
294
|
'shareModal.copyLink': 'Copy Link',
|
|
295
295
|
'shareModal.copyLinkSuccess': 'Link copied',
|