-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add chat functionality and update UI (#16)
* feat: add getFileMessages procedure to retrieve file messages * feat: add `infinite query` limit constant * feat: add `--turbo` in dev script * feat: add ExtendedMessage type to message.ts * feat: add Message component to handle chat messages * feat: add pricing page * refactor: variable name in trpc/index.ts * refactor: refactor Messages component and update Prisma initialization * feat: update page title and add blue space icon * feat: add `react-markdown` library * feat: update Messages component props * feat: add date formatting and markdown rendering to Message component * fix: the UI layout issues and update dependencies * feat: add @mantine/hooks dependency * refactor: the Message component to use React.forwardRef * feat: add message sending functionality and update chat context * feat: add loading message when AI is thinking * feat: add intersection observer to fetch next page of messages when scrolling to the bottom
- Loading branch information
1 parent
e67b587
commit f089eba
Showing
15 changed files
with
945 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function PricingPage() { | ||
return <div>Pricing Page</div>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import * as React from 'react'; | ||
import { format } from 'date-fns'; | ||
import ReactMarkdown from 'react-markdown'; | ||
|
||
import { ExtendedMessage } from '@/types/message'; | ||
import { cn } from '@/lib/utils'; | ||
import { Icons } from '@/components/icons'; | ||
|
||
interface MessageProps { | ||
message: ExtendedMessage; | ||
isNextMessageSamePerson: boolean; | ||
} | ||
|
||
export const Message = React.forwardRef<HTMLDivElement, MessageProps>( | ||
({ message, isNextMessageSamePerson }, ref) => { | ||
return ( | ||
<div | ||
ref={ref} | ||
className={cn('flex items-end', { | ||
'justify-end': message.isUserMessage, | ||
})} | ||
> | ||
<div | ||
className={cn('relative flex h-6 w-6 aspect-square items-center justify-center', { | ||
'order-2 bg-zinc-800 rounded-sm': message.isUserMessage, | ||
'order-1 bg-zinc-800 rounded-sm': !message.isUserMessage, | ||
invisible: isNextMessageSamePerson, | ||
})} | ||
> | ||
{message.isUserMessage ? ( | ||
<Icons.user className="fill-zinc-200 text-zinc-200 h-3/4 w-3/4" /> | ||
) : ( | ||
<Icons.bot className="text-white h-3/4 w-3/4" /> | ||
)} | ||
</div> | ||
<div | ||
className={cn('flex flex-col space-y-2 text-base max-w-md mx-2', { | ||
'order-1 items-end': message.isUserMessage, | ||
'order-2 items-start': !message.isUserMessage, | ||
})} | ||
> | ||
<div | ||
className={cn('px-4 py-2 rounded-lg inline-block', { | ||
'bg-gray-800 text-white': message.isUserMessage, | ||
'bg-gray-200 text-gray-900': !message.isUserMessage, | ||
'rounded-br-none': !isNextMessageSamePerson && message.isUserMessage, | ||
'rounded-bl-none': !!isNextMessageSamePerson && !message.isUserMessage, | ||
})} | ||
> | ||
{typeof message.text === 'string' ? ( | ||
<ReactMarkdown | ||
className={cn('prose', { | ||
'text-zinc-50': message.isUserMessage, | ||
})} | ||
> | ||
{message.text} | ||
</ReactMarkdown> | ||
) : ( | ||
message.text | ||
)} | ||
{message.id !== 'loading-message' ? ( | ||
<div | ||
className={cn('text-xs select-none mt-2 w-full text-right', { | ||
'text-zinc-500': !message.isUserMessage, | ||
'text-white': message.isUserMessage, | ||
})} | ||
> | ||
{format(new Date(message.createdAt), 'HH:mm')} | ||
</div> | ||
) : null} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}, | ||
); | ||
|
||
Message.displayName = 'Message'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,93 @@ | ||
export function Messages() { | ||
return <div>messages</div>; | ||
import * as React from 'react'; | ||
import { useContext } from 'react'; | ||
import { useIntersection } from '@mantine/hooks'; | ||
import Skeleton from 'react-loading-skeleton'; | ||
|
||
import { INFINIT_QUERY_LIMIT } from '@/config/infinite-query'; | ||
import { ChatContext } from '@/components/chat/chat-context'; | ||
import { Message } from '@/components/chat/message'; | ||
import { Icons } from '@/components/icons'; | ||
import { trpc } from '@/app/_trpc/client'; | ||
|
||
interface MessagesProps { | ||
fileId: string; | ||
} | ||
|
||
export function Messages({ fileId }: MessagesProps) { | ||
const { isLoading: isAiThinking } = useContext(ChatContext); | ||
const { data, isLoading, fetchNextPage } = trpc.getFileMessages.useInfiniteQuery( | ||
{ | ||
fileId, | ||
limit: INFINIT_QUERY_LIMIT, | ||
}, | ||
{ | ||
getNextPageParam: (lastPage) => lastPage?.nextCursor, | ||
keepPreviousData: true, | ||
}, | ||
); | ||
const messages = data?.pages.flatMap((page) => page.messages); | ||
const loadingMessage = { | ||
createdAt: new Date().toISOString(), | ||
id: 'loading-message', | ||
isUserMessage: false, | ||
text: ( | ||
<span className="flex h=full items-center justify-center"> | ||
<Icons.loader2 className="h-4 w-4 animate-spin" /> | ||
</span> | ||
), | ||
}; | ||
const combinedMessages = [...(isAiThinking ? [loadingMessage] : []), ...(messages ?? [])]; | ||
const lastMessageRef = React.useRef<HTMLDivElement>(null); | ||
const { ref, entry } = useIntersection({ | ||
root: lastMessageRef.current, | ||
threshold: 1, | ||
}); | ||
|
||
React.useEffect(() => { | ||
if (entry?.isIntersecting) { | ||
fetchNextPage(); | ||
} | ||
}, [entry, fetchNextPage]); | ||
|
||
return ( | ||
<div className="flex max-h-[calc(100vh-3.5rem-7rem)] border-zinc-200 flex-1 flex-col-reverse gap-4 p-3 overflow-y-auto scrollbar-thumb-black scrollbar-thumb-rounded scrollbar-track-black-lighter scrollbar-w-2 scrolling-touch"> | ||
{combinedMessages && combinedMessages.length > 0 ? ( | ||
combinedMessages.map((message, index) => { | ||
const isNextMessageSamePerson = | ||
combinedMessages[index - 1]?.isUserMessage === combinedMessages[index]?.isUserMessage; | ||
|
||
if (index === combinedMessages.length - 1) { | ||
return ( | ||
<Message | ||
ref={ref} | ||
key={message.id} | ||
isNextMessageSamePerson={isNextMessageSamePerson} | ||
message={message} | ||
/> | ||
); | ||
} else | ||
return ( | ||
<Message | ||
key={message.id} | ||
isNextMessageSamePerson={isNextMessageSamePerson} | ||
message={message} | ||
/> | ||
); | ||
}) | ||
) : isLoading ? ( | ||
<div className="w-full flex flex-col gap-2"> | ||
<Skeleton className="h-16" /> | ||
<Skeleton className="h-16" /> | ||
<Skeleton className="h-16" /> | ||
<Skeleton className="h-16" /> | ||
</div> | ||
) : ( | ||
<div className="flex-1 flex flex-col items-center justify-center gap-2"> | ||
<Icons.messageSquare className="h-8 w-8 text-blue-500" /> | ||
<h3 className="font-semibold text-xl">You're all set!</h3> | ||
<p className="text-zinc-500 text-sm">Ask your first question to get started.</p> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
} |
Oops, something went wrong.