11import { useEffect , useMemo , useRef } from 'react'
2+ import { useVirtualizer } from '@tanstack/react-virtual'
23import { Icon } from '~/components/Icon'
34import { LoadingSpinner } from '~/components/Loaders'
45import { Tooltip } from '~/components/Tooltip'
@@ -26,6 +27,10 @@ function getGroupName(lastActivity: string) {
2627 : 'Older'
2728}
2829
30+ type VirtualItem =
31+ | { type : 'header' ; groupName : string ; isFirst : boolean }
32+ | { type : 'session' ; session : ChatSession ; groupName : string }
33+
2934export function ChatHistorySidebar ( {
3035 handleSidebarToggle,
3136 currentSessionId,
@@ -36,6 +41,7 @@ export function ChatHistorySidebar({
3641 const { user } = useAuthContext ( )
3742 const { sessions, isLoading } = useChatHistory ( )
3843 const sidebarRef = useRef < HTMLDivElement > ( null )
44+ const scrollContainerRef = useRef < HTMLDivElement > ( null )
3945
4046 const groupedSessions = useMemo ( ( ) => {
4147 return Object . entries (
@@ -49,6 +55,30 @@ export function ChatHistorySidebar({
4955 >
5056 } , [ sessions ] )
5157
58+ const virtualItems = useMemo ( ( ) => {
59+ const items : VirtualItem [ ] = [ ]
60+ groupedSessions . forEach ( ( [ groupName , groupSessions ] , groupIndex ) => {
61+ items . push ( { type : 'header' , groupName, isFirst : groupIndex === 0 } )
62+ groupSessions . forEach ( ( session ) => {
63+ items . push ( { type : 'session' , session, groupName } )
64+ } )
65+ } )
66+ return items
67+ } , [ groupedSessions ] )
68+
69+ const virtualizer = useVirtualizer ( {
70+ count : virtualItems . length ,
71+ getScrollElement : ( ) => scrollContainerRef . current ,
72+ estimateSize : ( index ) => {
73+ const item = virtualItems [ index ]
74+ if ( item . type === 'header' ) {
75+ return item . isFirst ? 20 : 32 // First header has less padding
76+ }
77+ return 32 // Session item height
78+ } ,
79+ overscan : 5
80+ } )
81+
5282 useEffect ( ( ) => {
5383 const handleClickOutside = ( event : MouseEvent ) => {
5484 // Check if event.target is a Node and if click is outside the sidebar
@@ -91,32 +121,56 @@ export function ChatHistorySidebar({
91121 </ button >
92122 </ div >
93123
94- < div className = "thin-scrollbar flex-1 overflow-auto p-4 pt-0" >
124+ < div ref = { scrollContainerRef } className = "thin-scrollbar flex-1 overflow-auto p-4 pt-0" >
95125 { isLoading ? (
96126 < div className = "flex items-center justify-center rounded-sm border border-dashed border-[#666]/50 p-4 text-center text-xs text-[#666] dark:border-[#919296]/50 dark:text-[#919296]" >
97127 < LoadingSpinner size = { 12 } />
98128 </ div >
99129 ) : sessions . length === 0 ? (
100130 < p className = "rounded-sm border border-dashed border-[#666]/50 p-4 text-center text-xs text-[#666] dark:border-[#919296]/50 dark:text-[#919296]" >
101- You don’ t have any chats yet
131+ You don' t have any chats yet
102132 </ p >
103133 ) : (
104- < >
105- { groupedSessions . map ( ( [ groupName , sessions ] ) => (
106- < div key = { groupName } className = "group/parent flex flex-col gap-0.5" >
107- < h2 className = "pt-2.5 text-xs text-[#666] group-first/parent:pt-0 dark:text-[#919296]" > { groupName } </ h2 >
108- { sessions . map ( ( session ) => (
109- < SessionItem
110- key = { `${ session . sessionId } -${ session . isPublic } -${ session . lastActivity } ` }
111- session = { session }
112- isActive = { session . sessionId === currentSessionId }
113- onSessionSelect = { onSessionSelect }
114- handleSidebarToggle = { handleSidebarToggle }
115- />
116- ) ) }
117- </ div >
118- ) ) }
119- </ >
134+ < div
135+ style = { {
136+ height : `${ virtualizer . getTotalSize ( ) } px` ,
137+ width : '100%' ,
138+ position : 'relative'
139+ } }
140+ >
141+ { virtualizer . getVirtualItems ( ) . map ( ( virtualItem ) => {
142+ const item = virtualItems [ virtualItem . index ]
143+ const style = {
144+ position : 'absolute' as const ,
145+ top : 0 ,
146+ left : 0 ,
147+ width : '100%' ,
148+ height : `${ virtualItem . size } px` ,
149+ transform : `translateY(${ virtualItem . start } px)`
150+ }
151+
152+ if ( item . type === 'header' ) {
153+ return (
154+ < div key = { `header-${ item . groupName } ` } style = { style } >
155+ < h2 className = { `text-xs text-[#666] dark:text-[#919296] ${ item . isFirst ? 'pt-0' : 'pt-2.5' } ` } >
156+ { item . groupName }
157+ </ h2 >
158+ </ div >
159+ )
160+ }
161+
162+ return (
163+ < SessionItem
164+ key = { `session-${ item . session . sessionId } -${ item . session . isPublic } -${ item . session . lastActivity } ` }
165+ session = { item . session }
166+ isActive = { item . session . sessionId === currentSessionId }
167+ onSessionSelect = { onSessionSelect }
168+ handleSidebarToggle = { handleSidebarToggle }
169+ style = { style }
170+ />
171+ )
172+ } ) }
173+ </ div >
120174 ) }
121175 </ div >
122176 </ div >
0 commit comments