Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: useScroll hook #376

Merged
merged 11 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/masterbots.ai/app/api/payment/intent/route.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NextRequest } from 'next/server';
import type { NextRequest } from 'next/server';
import Stripe from 'stripe';

// Initialize Stripe with your secret key
Expand Down
4 changes: 4 additions & 0 deletions apps/masterbots.ai/components/routes/chat/chat-accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ export const ChatAccordion = ({
}: {
className?: string
children: React.ReactNode[]


defaultState?: boolean


triggerClass?: string
contentClass?: string
onToggle?: (isOpen: boolean) => void
Expand Down
166 changes: 101 additions & 65 deletions apps/masterbots.ai/components/routes/chat/chat-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ChatMessage } from '@/components/routes/chat/chat-message'
import { SharedAccordion } from '@/components/shared/shared-accordion'
import { ShortMessage } from '@/components/shared/short-message'
import { Separator } from '@/components/ui/separator'
import { useScroll } from '@/lib/hooks/use-scroll'
import { useMBScroll } from '@/lib/hooks/use-mb-scroll'
import { useThread } from '@/lib/hooks/use-thread'
import { cn, createMessagePairs } from '@/lib/utils'
import type { Message } from 'ai'
Expand All @@ -22,7 +22,6 @@ export interface ChatList {
chatTitleClass?: string
chatArrowClass?: string
containerRef?: React.RefObject<HTMLDivElement>
isNearBottom?: boolean
isLoadingMessages?: boolean
sendMessageFn?: (message: string) => void
}
Expand All @@ -40,33 +39,42 @@ export function ChatList({
chatContentClass,
chatTitleClass,
chatArrowClass,
containerRef,
sendMessageFn,
isNearBottom,
containerRef: externalContainerRef,
sendMessageFn
}: ChatList) {
const [pairs, setPairs] = React.useState<MessagePair[]>([])
const [previousConversationPairs, setPreviousConversationPairs] = React.useState<MessagePair[]>([])
const [previousConversationPairs, setPreviousConversationPairs] =
React.useState<MessagePair[]>([])
const { isNewResponse, activeThread } = useThread()
const localContainerRef = useRef<HTMLDivElement>(null)
const effectiveContainerRef = containerRef || localContainerRef
const chatMessages = (messages || activeThread?.messages || [])
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
const previousChatMessages = (activeThread?.thread?.messages || [])
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
const chatListRef = useRef<HTMLDivElement>(null)
const messageContainerRef = useRef<HTMLDivElement>(null)

useScroll({
//? Uses the external ref if provided, otherwise it uses our internal refs
const effectiveContainerRef = externalContainerRef || chatListRef
const effectiveThreadRef = messageContainerRef

const chatMessages = (messages || activeThread?.messages || []).sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)
const previousChatMessages = (activeThread?.thread?.messages || []).sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)

const { isNearBottom } = useMBScroll({
containerRef: effectiveContainerRef,
threadRef: effectiveContainerRef,
threadRef: effectiveThreadRef,
isNewContent: isNewResponse,
hasMore: false,
isLast: true,
loading: isLoadingMessages,
loadMore: () => { }
loadMore: () => {}
})

useEffect(() => {
if (chatMessages?.length) {
const prePairs: MessagePair[] = createMessagePairs(chatMessages) as MessagePair[]
const prePairs: MessagePair[] = createMessagePairs(
chatMessages
) as MessagePair[]
setPairs(prevPairs => {
if (!isEqual(prevPairs, prePairs)) {
return prePairs
Expand All @@ -76,10 +84,12 @@ export function ChatList({
}
}, [chatMessages])

// biome-ignore lint/correctness/useExhaustiveDependencies: adding functions to array dep is not needed
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => {
if (previousChatMessages?.length) {
const prePairs: MessagePair[] = createMessagePairs(previousChatMessages) as MessagePair[]
const prePairs: MessagePair[] = createMessagePairs(
previousChatMessages
) as MessagePair[]
setPreviousConversationPairs(prevPairs => {
if (!isEqual(prevPairs, prePairs)) {
return prePairs
Expand All @@ -98,20 +108,22 @@ export function ChatList({
<div
ref={effectiveContainerRef}
className={cn(
'relative max-w-3xl px-4 mx-auto',
'relative max-w-3xl px-4 mx-auto overflow-auto scrollbar-thin',
className,
{ 'flex flex-col gap-3': isThread }
)}
>
<MessagePairs
pairs={pairs}
previousPairs={previousConversationPairs}
isThread={isThread}
chatTitleClass={chatTitleClass}
chatArrowClass={chatArrowClass}
chatContentClass={chatContentClass}
sendMessageFn={sendMessageFn}
/>
<div ref={effectiveThreadRef} className="min-h-full">
<MessagePairs
pairs={pairs}
previousPairs={previousConversationPairs}
isThread={isThread}
chatTitleClass={chatTitleClass}
chatArrowClass={chatArrowClass}
chatContentClass={chatContentClass}
sendMessageFn={sendMessageFn}
/>
</div>
</div>
)
}
Expand All @@ -123,7 +135,7 @@ function MessagePairs({
chatTitleClass,
chatArrowClass,
chatContentClass,
sendMessageFn,
sendMessageFn
}: {
pairs: MessagePair[]
previousPairs: MessagePair[]
Expand Down Expand Up @@ -153,8 +165,8 @@ function MessagePairs({
sendMessageFn={sendMessageFn}
/>
))}
{(previousPairs.length > 0 && pairs.length > 0) && (
<Separator className="relative mt-6 -bottom-1.5 h-1.5 z-[2] rounded-sm bg-iron dark:bg-mirage"/>
{previousPairs.length > 0 && pairs.length > 0 && (
<Separator className="relative mt-6 -bottom-1.5 h-1.5 z-[2] rounded-sm bg-iron dark:bg-mirage" />
)}
{pairs.map((pair: MessagePair, key: number, pairsArray) => (
<MessagePairAccordion
Expand All @@ -174,7 +186,15 @@ function MessagePairs({
)
}

export function MessagePairAccordion({ pair, isThread, index, arrayLength, isNewResponse, type, ...props }: {
export function MessagePairAccordion({
pair,
isThread,
index,
arrayLength,
isNewResponse,
type,
...props
}: {
pair: MessagePair
isThread: boolean
index: number
Expand All @@ -191,20 +211,27 @@ export function MessagePairAccordion({ pair, isThread, index, arrayLength, isNew
return (
<SharedAccordion
key={`${pair.userMessage.createdAt}-${pair.chatGptMessage[0]?.id ?? 'pending'}`}
defaultState={index === 0 || index === arrayLength - 1 || (index === arrayLength - 2 && isNewResponse)}
defaultState={
index === 0 ||
index === arrayLength - 1 ||
(index === arrayLength - 2 && isNewResponse)
}
className={cn(
{ relative: isThread },
// Add subtle background tint and left border for previous messages
isPrevious && 'bg-accent/25 rounded-[8px] border-l-accent/20',
// Adds subtle background tint and left border for previous messages
isPrevious && 'bg-accent/25 rounded-[8px] border-l-accent/20'
)}
triggerClass={cn(
'py-[0.4375rem]',
{
'sticky top-0 md:-top-10 z-[1] px-3 [&[data-state=open]]:rounded-t-[8px]': isThread,
'sticky top-0 md:-top-10 z-[1] px-3 [&[data-state=open]]:rounded-t-[8px]':
isThread,
'px-[calc(32px-0.25rem)]': !isThread,
'hidden': !isThread && index === 0,// Style differences for previous vs current messages
'dark:bg-[#1d283a9a] bg-iron !border-l-[transparent] [&[data-state=open]]:!bg-gray-400/50 dark:[&[data-state=open]]:!bg-mirage': !isPrevious,
'bg-accent/15 dark:bg-accent/15 hover:bg-accent/30 hover:dark:bg-accent/30 border-l-accent/20 dark:border-l-accent/20 [&[data-state=open]]:!bg-accent/25 dark:[&[data-state=open]]:!bg-accent/25': isPrevious,
hidden: !isThread && index === 0, // Style differences for previous vs current messages
'dark:bg-[#1d283a9a] bg-iron !border-l-[transparent] [&[data-state=open]]:!bg-gray-400/50 dark:[&[data-state=open]]:!bg-mirage':
!isPrevious,
'bg-accent/15 dark:bg-accent/15 hover:bg-accent/30 hover:dark:bg-accent/30 border-l-accent/20 dark:border-l-accent/20 [&[data-state=open]]:!bg-accent/25 dark:[&[data-state=open]]:!bg-accent/25':
isPrevious
},
props.chatTitleClass
)}
Expand All @@ -219,14 +246,13 @@ export function MessagePairAccordion({ pair, isThread, index, arrayLength, isNew
variant="chat"
>
{/* Thread Title with indicator for previous messages */}
{!isThread && index === 0 ? '' : (
{!isThread && index === 0 ? (
''
) : (
<div
className={cn(
'flex items-start gap-2',
{
'[&_div]:text-sm': isPrevious,
}
)}
className={cn('flex items-start gap-2', {
'[&_div]:text-sm': isPrevious
})}
>
<ChatMessage actionRequired={false} message={pair.userMessage} />
</div>
Expand All @@ -238,17 +264,21 @@ export function MessagePairAccordion({ pair, isThread, index, arrayLength, isNew
<div className="flex-1 px-1 space-y-2 overflow-hidden text-left">
<ShortMessage content={pair.chatGptMessage[0]?.content} />
</div>
) : ''}
) : (
''
)}
</div>
) : <></>}
) : (
<></>
)}

{/* Thread Content */}
<div
className={cn(
'mx-4 md:mx-[46px] px-1 py-4 border-transparent dark:border-x-mirage border-x-gray-300 border h-full',
{
{
'!border-[transparent]': !isThread && index === 0,
'[&>div>div>div_*]:!text-xs': isPrevious,
'[&>div>div>div_*]:!text-xs': isPrevious
},
props.chatContentClass
)}
Expand All @@ -258,26 +288,32 @@ export function MessagePairAccordion({ pair, isThread, index, arrayLength, isNew
<span className="absolute top-1 -left-5 px-1.5 py-0.5 text-[10px] font-medium rounded-md bg-accent text-accent-foreground">
Previous Thread
</span>
<div className="opacity-50 pb-3 overflow-hidden text-sm mt-4">
Continued from <b>&ldquo;{pair.userMessage.content.trim()}&rdquo;</b> thread{activeThread?.thread?.user?.username ? `, by ${activeThread?.thread?.user?.username}.` : '.'}
<div className="pb-3 mt-4 overflow-hidden text-sm opacity-50">
Continued from{' '}
<b>&ldquo;{pair.userMessage.content.trim()}&rdquo;</b> thread
{activeThread?.thread?.user?.username
? `, by ${activeThread?.thread?.user?.username}.`
: '.'}
</div>
</>
) : ''}
) : (
''
)}
<ChatLoadingState />
{pair.chatGptMessage.length > 0
? pair.chatGptMessage.map((message) => (
<ChatMessage
key={message.id}
actionRequired={false}
message={message}
sendMessageFromResponse={props.sendMessageFn}
/>
))
? pair.chatGptMessage.map(message => (
<ChatMessage
key={message.id}
actionRequired={false}
message={message}
sendMessageFromResponse={props.sendMessageFn}
/>
))
: ''}
</div>
</SharedAccordion>
);
};
)
}

export function ChatLoadingState() {
const { activeTool, loadingState } = useThread()
Expand All @@ -295,7 +331,7 @@ export function ChatLoadingState() {
switch (activeTool?.toolName) {
case 'webSearch':
return (
<div className='flex items-center w-full h-20 gap-4 opacity-65'>
<div className="flex items-center w-full h-20 gap-4 opacity-65">
<GlobeIcon className="relative size-6 animate-bounce top-2" />
<p className="flex flex-col gap-1 leading-none">
<span>
Expand Down Expand Up @@ -323,4 +359,4 @@ export function ChatLoadingState() {
</div>
)
}
}
}
Loading