@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.
@@ -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 className={styles.container} gap={mobile ? 6 : 12}>
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 { VirtuosoContext } from '../../../components/VirtualizedList/VirtuosoContext';
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 virtuosoRef = use(VirtuosoContext);
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
- virtuosoRef?.current?.scrollIntoView({ align: 'start', behavior: 'auto', index });
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
- removeVirtuosoVisibleItem,
11
- upsertVirtuosoVisibleItem,
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
- upsertVirtuosoVisibleItem(index, {
91
+ upsertVirtuaVisibleItem(index, {
90
92
  bottom,
91
93
  ratio: entry.intersectionRatio,
92
94
  top,
93
95
  });
94
96
  } else {
95
- removeVirtuosoVisibleItem(index);
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
- removeVirtuosoVisibleItem(index);
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 <AssistantMessage disableEditing={disableEditing} id={id} index={index} />;
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 <GroupMessage disableEditing={disableEditing} id={id} index={index} />;
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 { VirtuosoHandle } from 'react-virtuoso';
2
+ import { VListHandle } from 'virtua';
3
3
 
4
- export const VirtuosoContext = createContext<RefObject<VirtuosoHandle | null> | null>(null);
4
+ export const VirtuaContext = createContext<RefObject<VListHandle | null> | null>(null);
5
5
 
6
- type VirtuosoRef = RefObject<VirtuosoHandle | null> | null;
6
+ type VirtuaRef = RefObject<VListHandle | null> | null;
7
7
 
8
- let currentVirtuosoRef: VirtuosoRef = null;
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 setVirtuosoGlobalRef = (ref: VirtuosoRef) => {
49
- currentVirtuosoRef = ref;
48
+ export const setVirtuaGlobalRef = (ref: VirtuaRef) => {
49
+ currentVirtuaRef = ref;
50
50
  refListeners.forEach((listener) => listener());
51
51
  };
52
52
 
53
- export const getVirtuosoGlobalRef = () => currentVirtuosoRef;
53
+ export const getVirtuaGlobalRef = () => currentVirtuaRef;
54
54
 
55
- export const subscribeVirtuosoGlobalRef = (listener: () => void) => {
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 upsertVirtuosoVisibleItem = (
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 removeVirtuosoVisibleItem = (index: number) => {
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 resetVirtuosoVisibleItems = () => {
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 getVirtuosoActiveIndex = () => currentActiveIndex;
84
+ export const getVirtuaActiveIndex = () => currentActiveIndex;
85
85
 
86
- export const subscribeVirtuosoActiveIndex = (listener: () => void) => {
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
- ReactNode,
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 virtuosoRef = useRef<VirtuosoHandle>(null);
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 getFollowOutput = useCallback(() => {
55
- const newFollowOutput = dataSource.length > prevDataLengthRef.current ? 'auto' : false;
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
- return newFollowOutput;
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 (!virtuosoRef.current) return;
64
- virtuosoRef.current.scrollToIndex({ align: 'end', behavior, index: 'LAST' });
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
- setVirtuosoGlobalRef(virtuosoRef);
94
+ setVirtuaGlobalRef(virtuaRef);
74
95
 
75
96
  return () => {
76
- setVirtuosoGlobalRef(null);
97
+ setVirtuaGlobalRef(null);
77
98
  };
78
- }, [virtuosoRef]);
99
+ }, [virtuaRef]);
79
100
 
80
101
  useEffect(() => {
81
102
  return () => {
82
- resetVirtuosoVisibleItems();
103
+ resetVirtuaVisibleItems();
104
+ if (scrollEndTimerRef.current) {
105
+ clearTimeout(scrollEndTimerRef.current);
106
+ }
83
107
  };
84
108
  }, []);
85
109
 
86
- // overscan should be 2 times the height of the window
87
- const overscan = typeof window !== 'undefined' ? window.innerHeight * 2 : 0;
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
- <VirtuosoContext value={virtuosoRef}>
94
- <Virtuoso
95
- atBottomStateChange={setAtBottom}
96
- atBottomThreshold={200 * (mobile ? 2 : 1)}
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
- followOutput={getFollowOutput}
101
- increaseViewportBy={overscan}
102
- initialTopMostItemIndex={dataSource?.length - 1}
103
- isScrolling={setIsScrolling}
104
- itemContent={itemContent}
105
- ref={virtuosoRef}
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 virtuoso = virtuosoRef.current;
152
+ const virtua = virtuaRef.current;
153
+ if (!virtua) return;
154
+
121
155
  switch (type) {
122
156
  case 'auto': {
123
- virtuoso?.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' });
157
+ virtua.scrollToIndex(dataSource.length - 1, { align: 'end' });
124
158
  break;
125
159
  }
126
160
  case 'click': {
127
- virtuoso?.scrollToIndex({ align: 'end', behavior: 'smooth', index: 'LAST' });
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
- </VirtuosoContext>
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
- className={cx(styles.container, className)}
37
- width={wideScreen ? '100%' : `min(${CONVERSATION_MIN_WIDTH}px, 100%)`}
38
- {...rest}
39
- >
40
- {children}
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 { VirtuosoContext } from '../components/VirtualizedList/VirtuosoContext';
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 virtuosoRef = use(VirtuosoContext);
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
- virtuosoRef?.current?.scrollIntoView({ align: 'start', behavior: 'auto', index });
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);