Skip to content

Commit 625503d

Browse files
mintdartReynardoEW
andauthored
virtualize chat history (#2272)
* virtualize chat history * rm space * refactor --------- Co-authored-by: reynardoew <[email protected]>
1 parent ff2db4b commit 625503d

File tree

3 files changed

+78
-21
lines changed

3 files changed

+78
-21
lines changed

src/containers/LlamaAI/components/ChatHistorySidebar.tsx

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useMemo, useRef } from 'react'
2+
import { useVirtualizer } from '@tanstack/react-virtual'
23
import { Icon } from '~/components/Icon'
34
import { LoadingSpinner } from '~/components/Loaders'
45
import { 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+
2934
export 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 dont 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>

src/containers/LlamaAI/components/SessionItem.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ interface SessionItemProps {
1515
isActive: boolean
1616
onSessionSelect: (sessionId: string, data: { conversationHistory: any[]; pagination?: any }) => void
1717
handleSidebarToggle: () => void
18+
style: React.CSSProperties
1819
}
1920

20-
export function SessionItem({ session, isActive, onSessionSelect, handleSidebarToggle }: SessionItemProps) {
21+
export function SessionItem({ session, isActive, onSessionSelect, handleSidebarToggle, style }: SessionItemProps) {
2122
const router = useRouter()
2223
const { authorizedFetch } = useAuthContext()
2324
const { deleteSession, updateSessionTitle, isRestoringSession, isDeletingSession, isUpdatingTitle } = useChatHistory()
@@ -95,6 +96,7 @@ export function SessionItem({ session, isActive, onSessionSelect, handleSidebarT
9596
ref={formRef}
9697
onSubmit={handleSave}
9798
className="group relative -mx-1.5 flex items-center gap-0.5 rounded-sm text-xs hover:bg-[#f7f7f7] data-[active=true]:bg-(--old-blue) data-[active=true]:text-white dark:hover:bg-[#222324]"
99+
style={style}
98100
>
99101
<input
100102
type="text"
@@ -135,6 +137,7 @@ export function SessionItem({ session, isActive, onSessionSelect, handleSidebarT
135137
<div
136138
data-active={isActive}
137139
className="group relative -mx-1.5 flex items-center rounded-sm text-xs focus-within:bg-[#f7f7f7] hover:bg-[#f7f7f7] data-[active=true]:bg-(--old-blue) data-[active=true]:text-white dark:focus-within:bg-[#222324] dark:hover:bg-[#222324]"
140+
style={style}
138141
>
139142
<a
140143
href={`/ai/${session.sessionId}`}

src/containers/LlamaAI/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,7 @@ export function LlamaAI({ initialSessionId, sharedSession, readOnly = false, sho
10751075
<div className="mx-auto flex h-full w-full max-w-3xl flex-col gap-2.5">
10761076
<div className="mt-[100px] flex shrink-0 flex-col items-center justify-center gap-2.5 max-lg:mt-[50px]">
10771077
<img src="/icons/llama-ai.svg" alt="LlamaAI" className="object-contain" width={64} height={77} />
1078-
<h1 className="text-center text-2xl font-semibold">What can I help you with ?</h1>
1078+
<h1 className="text-center text-2xl font-semibold">What can I help you with?</h1>
10791079
</div>
10801080
{!readOnly && (
10811081
<>
@@ -1242,7 +1242,7 @@ export function LlamaAI({ initialSessionId, sharedSession, readOnly = false, sho
12421242
) : (
12431243
<div className="mt-[100px] flex flex-col items-center justify-center gap-2.5">
12441244
<img src="/icons/llama-ai.svg" alt="LlamaAI" className="object-contain" width={64} height={77} />
1245-
<h1 className="text-center text-2xl font-semibold">What can I help you with ?</h1>
1245+
<h1 className="text-center text-2xl font-semibold">What can I help you with?</h1>
12461246
</div>
12471247
)}
12481248
</div>

0 commit comments

Comments
 (0)