@lobehub/lobehub 2.0.0-next.66 → 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/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +3 -2
- 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/features/ChatItem/style.ts +4 -0
- package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +3 -3
- package/src/features/Conversation/Messages/Assistant/index.tsx +329 -230
- package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +3 -3
- package/src/features/Conversation/Messages/Group/GroupItem.tsx +3 -5
- package/src/features/Conversation/Messages/Group/index.tsx +80 -13
- 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/useDoubleClickEdit.ts +3 -3
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +9 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { UIChatMessage } from '@lobechat/types';
|
|
4
|
-
import { useResponsive } from 'antd-style';
|
|
4
|
+
import { createStyles, useResponsive } from 'antd-style';
|
|
5
5
|
import isEqual from 'fast-deep-equal';
|
|
6
6
|
import { memo, useCallback } from 'react';
|
|
7
7
|
import { Flexbox } from 'react-layout-kit';
|
|
@@ -9,7 +9,6 @@ import { Flexbox } from 'react-layout-kit';
|
|
|
9
9
|
import Avatar from '@/features/ChatItem/components/Avatar';
|
|
10
10
|
import BorderSpacing from '@/features/ChatItem/components/BorderSpacing';
|
|
11
11
|
import Title from '@/features/ChatItem/components/Title';
|
|
12
|
-
import { useStyles } from '@/features/ChatItem/style';
|
|
13
12
|
import Usage from '@/features/Conversation/components/Extras/Usage';
|
|
14
13
|
import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
|
|
15
14
|
import { useAgentStore } from '@/store/agent';
|
|
@@ -26,13 +25,85 @@ import Group from './Group';
|
|
|
26
25
|
|
|
27
26
|
const MOBILE_AVATAR_SIZE = 32;
|
|
28
27
|
|
|
28
|
+
const useStyles = createStyles(
|
|
29
|
+
({ cx, css, token, responsive }, { variant }: { variant?: 'bubble' | 'docs' }) => {
|
|
30
|
+
const rawContainerStylish = css`
|
|
31
|
+
margin-block-end: -16px;
|
|
32
|
+
transition: background-color 100ms ${token.motionEaseOut};
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
actions: css`
|
|
37
|
+
flex: none;
|
|
38
|
+
align-self: flex-end;
|
|
39
|
+
justify-content: flex-end;
|
|
40
|
+
`,
|
|
41
|
+
container: cx(
|
|
42
|
+
variant === 'docs' && rawContainerStylish,
|
|
43
|
+
css`
|
|
44
|
+
position: relative;
|
|
45
|
+
|
|
46
|
+
width: 100%;
|
|
47
|
+
max-width: 100vw;
|
|
48
|
+
padding-block: 24px 12px;
|
|
49
|
+
padding-inline: 12px;
|
|
50
|
+
|
|
51
|
+
@supports (content-visibility: auto) {
|
|
52
|
+
contain-intrinsic-size: auto 100lvh;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
time {
|
|
56
|
+
display: inline-block;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
div[role='menubar'] {
|
|
61
|
+
display: flex;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
time,
|
|
65
|
+
div[role='menubar'] {
|
|
66
|
+
pointer-events: none;
|
|
67
|
+
opacity: 0;
|
|
68
|
+
transition: opacity 200ms ${token.motionEaseOut};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&:hover {
|
|
72
|
+
time,
|
|
73
|
+
div[role='menubar'] {
|
|
74
|
+
pointer-events: unset;
|
|
75
|
+
opacity: 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
${responsive.mobile} {
|
|
80
|
+
padding-block-start: ${variant === 'docs' ? '16px' : '12px'};
|
|
81
|
+
padding-inline: 8px;
|
|
82
|
+
}
|
|
83
|
+
`,
|
|
84
|
+
),
|
|
85
|
+
messageContent: css`
|
|
86
|
+
position: relative;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
width: 100%;
|
|
89
|
+
max-width: 100%;
|
|
90
|
+
|
|
91
|
+
${responsive.mobile} {
|
|
92
|
+
flex-direction: column !important;
|
|
93
|
+
}
|
|
94
|
+
`,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
29
99
|
interface GroupMessageProps {
|
|
30
100
|
disableEditing?: boolean;
|
|
31
101
|
id: string;
|
|
32
102
|
index: number;
|
|
103
|
+
isLatestItem?: boolean;
|
|
33
104
|
}
|
|
34
105
|
|
|
35
|
-
const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing }) => {
|
|
106
|
+
const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLatestItem }) => {
|
|
36
107
|
const item = useChatStore(
|
|
37
108
|
displayMessageSelectors.getDisplayMessageById(id),
|
|
38
109
|
isEqual,
|
|
@@ -45,15 +116,7 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing }) =>
|
|
|
45
116
|
const type = useAgentStore(agentChatConfigSelectors.displayMode);
|
|
46
117
|
const variant = type === 'chat' ? 'bubble' : 'docs';
|
|
47
118
|
|
|
48
|
-
const { styles } = useStyles({
|
|
49
|
-
editing: false,
|
|
50
|
-
placement,
|
|
51
|
-
primary: false,
|
|
52
|
-
showTitle: true,
|
|
53
|
-
time: createdAt,
|
|
54
|
-
title: avatar.title,
|
|
55
|
-
variant,
|
|
56
|
-
});
|
|
119
|
+
const { styles } = useStyles({ variant });
|
|
57
120
|
|
|
58
121
|
const [isInbox] = useSessionStore((s) => [sessionSelectors.isInboxSession(s)]);
|
|
59
122
|
const [toggleSystemRole] = useGlobalStore((s) => [s.toggleSystemRole]);
|
|
@@ -79,7 +142,11 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing }) =>
|
|
|
79
142
|
}, [isInbox]);
|
|
80
143
|
|
|
81
144
|
return (
|
|
82
|
-
<Flexbox
|
|
145
|
+
<Flexbox
|
|
146
|
+
className={styles.container}
|
|
147
|
+
gap={mobile ? 6 : 12}
|
|
148
|
+
style={isLatestItem ? { minHeight: 'calc(-300px + 100dvh)' } : undefined}
|
|
149
|
+
>
|
|
83
150
|
<Flexbox gap={4} horizontal>
|
|
84
151
|
<Avatar
|
|
85
152
|
alt={avatar.title || 'avatar'}
|
|
@@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
10
10
|
import { useChatStore } from '@/store/chat';
|
|
11
11
|
import { messageStateSelectors, threadSelectors } from '@/store/chat/selectors';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { VirtuaContext } from '../../../components/VirtualizedList/VirtuosoContext';
|
|
14
14
|
import { InPortalThreadContext } from '../../../context/InPortalThreadContext';
|
|
15
15
|
import { useChatListActionsBar } from '../../../hooks/useChatListActionsBar';
|
|
16
16
|
|
|
@@ -76,7 +76,7 @@ export const UserActionsBar = memo<UserActionsProps>(({ id, data, index }) => {
|
|
|
76
76
|
|
|
77
77
|
// remove line breaks in artifact tag to make the ast transform easier
|
|
78
78
|
|
|
79
|
-
const
|
|
79
|
+
const virtuaRef = use(VirtuaContext);
|
|
80
80
|
|
|
81
81
|
const onActionClick = useCallback(
|
|
82
82
|
async (action: ActionIconGroupEvent) => {
|
|
@@ -84,7 +84,7 @@ export const UserActionsBar = memo<UserActionsProps>(({ id, data, index }) => {
|
|
|
84
84
|
case 'edit': {
|
|
85
85
|
toggleMessageEditing(id, true);
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
virtuaRef?.current?.scrollToIndex(index, { align: 'start' });
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -7,8 +7,8 @@ import { ReactNode, memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
|
7
7
|
import { Flexbox } from 'react-layout-kit';
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
removeVirtuaVisibleItem,
|
|
11
|
+
upsertVirtuaVisibleItem,
|
|
12
12
|
} from '@/features/Conversation/components/VirtualizedList/VirtuosoContext';
|
|
13
13
|
import { getChatStoreState, useChatStore } from '@/store/chat';
|
|
14
14
|
import { displayMessageSelectors, messageStateSelectors } from '@/store/chat/selectors';
|
|
@@ -42,6 +42,7 @@ export interface ChatListItemProps {
|
|
|
42
42
|
id: string;
|
|
43
43
|
inPortalThread?: boolean;
|
|
44
44
|
index: number;
|
|
45
|
+
isLatestItem?: boolean;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const Item = memo<ChatListItemProps>(
|
|
@@ -53,6 +54,7 @@ const Item = memo<ChatListItemProps>(
|
|
|
53
54
|
disableEditing,
|
|
54
55
|
inPortalThread = false,
|
|
55
56
|
index,
|
|
57
|
+
isLatestItem,
|
|
56
58
|
}) => {
|
|
57
59
|
const { styles, cx } = useStyles();
|
|
58
60
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -86,13 +88,13 @@ const Item = memo<ChatListItemProps>(
|
|
|
86
88
|
if (entry.isIntersecting) {
|
|
87
89
|
const { bottom, top } = entry.intersectionRect;
|
|
88
90
|
|
|
89
|
-
|
|
91
|
+
upsertVirtuaVisibleItem(index, {
|
|
90
92
|
bottom,
|
|
91
93
|
ratio: entry.intersectionRatio,
|
|
92
94
|
top,
|
|
93
95
|
});
|
|
94
96
|
} else {
|
|
95
|
-
|
|
97
|
+
removeVirtuaVisibleItem(index);
|
|
96
98
|
}
|
|
97
99
|
});
|
|
98
100
|
}, options);
|
|
@@ -101,7 +103,7 @@ const Item = memo<ChatListItemProps>(
|
|
|
101
103
|
|
|
102
104
|
return () => {
|
|
103
105
|
observer.disconnect();
|
|
104
|
-
|
|
106
|
+
removeVirtuaVisibleItem(index);
|
|
105
107
|
};
|
|
106
108
|
}, [index]);
|
|
107
109
|
|
|
@@ -127,11 +129,25 @@ const Item = memo<ChatListItemProps>(
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
case 'assistant': {
|
|
130
|
-
return
|
|
132
|
+
return (
|
|
133
|
+
<AssistantMessage
|
|
134
|
+
disableEditing={disableEditing}
|
|
135
|
+
id={id}
|
|
136
|
+
index={index}
|
|
137
|
+
isLatestItem={isLatestItem}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
131
140
|
}
|
|
132
141
|
|
|
133
142
|
case 'assistantGroup': {
|
|
134
|
-
return
|
|
143
|
+
return (
|
|
144
|
+
<GroupMessage
|
|
145
|
+
disableEditing={disableEditing}
|
|
146
|
+
id={id}
|
|
147
|
+
index={index}
|
|
148
|
+
isLatestItem={isLatestItem}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
135
151
|
}
|
|
136
152
|
|
|
137
153
|
case 'tool': {
|
|
@@ -144,7 +160,7 @@ const Item = memo<ChatListItemProps>(
|
|
|
144
160
|
}
|
|
145
161
|
|
|
146
162
|
return null;
|
|
147
|
-
}, [role, disableEditing, id, index]);
|
|
163
|
+
}, [role, disableEditing, id, index, isLatestItem]);
|
|
148
164
|
|
|
149
165
|
if (!role) return;
|
|
150
166
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { RefObject, createContext } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { VListHandle } from 'virtua';
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const VirtuaContext = createContext<RefObject<VListHandle | null> | null>(null);
|
|
5
5
|
|
|
6
|
-
type
|
|
6
|
+
type VirtuaRef = RefObject<VListHandle | null> | null;
|
|
7
7
|
|
|
8
|
-
let
|
|
8
|
+
let currentVirtuaRef: VirtuaRef = null;
|
|
9
9
|
const refListeners = new Set<() => void>();
|
|
10
10
|
|
|
11
11
|
const visibleItems = new Map<number, { bottom: number; ratio: number; top: number }>();
|
|
@@ -45,14 +45,14 @@ const recalculateActiveIndex = () => {
|
|
|
45
45
|
notifyActiveIndex(candidate ?? null);
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
export const
|
|
49
|
-
|
|
48
|
+
export const setVirtuaGlobalRef = (ref: VirtuaRef) => {
|
|
49
|
+
currentVirtuaRef = ref;
|
|
50
50
|
refListeners.forEach((listener) => listener());
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
export const
|
|
53
|
+
export const getVirtuaGlobalRef = () => currentVirtuaRef;
|
|
54
54
|
|
|
55
|
-
export const
|
|
55
|
+
export const subscribeVirtuaGlobalRef = (listener: () => void) => {
|
|
56
56
|
refListeners.add(listener);
|
|
57
57
|
|
|
58
58
|
return () => {
|
|
@@ -60,7 +60,7 @@ export const subscribeVirtuosoGlobalRef = (listener: () => void) => {
|
|
|
60
60
|
};
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
export const
|
|
63
|
+
export const upsertVirtuaVisibleItem = (
|
|
64
64
|
index: number,
|
|
65
65
|
metrics: { bottom: number; ratio: number; top: number },
|
|
66
66
|
) => {
|
|
@@ -68,22 +68,22 @@ export const upsertVirtuosoVisibleItem = (
|
|
|
68
68
|
recalculateActiveIndex();
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
export const
|
|
71
|
+
export const removeVirtuaVisibleItem = (index: number) => {
|
|
72
72
|
if (!visibleItems.delete(index)) return;
|
|
73
73
|
|
|
74
74
|
recalculateActiveIndex();
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
export const
|
|
77
|
+
export const resetVirtuaVisibleItems = () => {
|
|
78
78
|
if (visibleItems.size === 0 && currentActiveIndex === null) return;
|
|
79
79
|
|
|
80
80
|
visibleItems.clear();
|
|
81
81
|
notifyActiveIndex(null);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
export const
|
|
84
|
+
export const getVirtuaActiveIndex = () => currentActiveIndex;
|
|
85
85
|
|
|
86
|
-
export const
|
|
86
|
+
export const subscribeVirtuaActiveIndex = (listener: () => void) => {
|
|
87
87
|
activeIndexListeners.add(listener);
|
|
88
88
|
|
|
89
89
|
return () => {
|
|
@@ -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;
|
|
@@ -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
|
);
|
|
@@ -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);
|