feat: clear chat button and scroll to bottom

This commit is contained in:
Arik Chakma
2025-06-02 22:46:52 +06:00
parent 2a0d94c162
commit 6924408b1a
4 changed files with 117 additions and 30 deletions

1
.astro/types.d.ts vendored
View File

@@ -1 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

View File

@@ -1,6 +1,12 @@
import './AIChat.css';
import { FileUpIcon, PersonStandingIcon, SendIcon } from 'lucide-react';
import { useCallback, useRef, useState } from 'react';
import {
ArrowDownIcon,
FileUpIcon,
PersonStandingIcon,
SendIcon,
TrashIcon,
} from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import AutogrowTextarea from 'react-textarea-autosize';
import { QuickHelpPrompts } from './QuickHelpPrompts';
@@ -33,6 +39,11 @@ export function AIChat() {
useState(false);
const [isUploadResumeModalOpen, setIsUploadResumeModalOpen] = useState(false);
const [showScrollToBottomButton, setShowScrollToBottomButton] =
useState(false);
const scrollableContainerRef = useRef<HTMLDivElement>(null);
const chatContainerRef = useRef<HTMLDivElement>(null);
const textareaMessageRef = useRef<HTMLTextAreaElement>(null);
const { data: tokenUsage, isLoading } = useQuery(
@@ -83,8 +94,13 @@ export function AIChat() {
};
const scrollToBottom = useCallback(() => {
window.scrollTo({
top: document.body.scrollHeight,
const scrollableContainer = scrollableContainerRef?.current;
if (!scrollableContainer) {
return;
}
scrollableContainer.scrollTo({
top: scrollableContainer.scrollHeight,
behavior: 'smooth',
});
}, []);
@@ -180,12 +196,58 @@ export function AIChat() {
queryClient,
);
useEffect(() => {
const scrollableContainer = scrollableContainerRef.current;
const chatContainer = chatContainerRef.current;
if (!scrollableContainer || !chatContainer) {
return;
}
const abortController = new AbortController();
let timeoutId: NodeJS.Timeout;
const debouncedHandleScroll = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
const paddingBottom = parseInt(
getComputedStyle(scrollableContainer).paddingBottom,
);
const distanceFromBottom =
scrollableContainer.scrollHeight -
// scroll from the top + the container height
(scrollableContainer.scrollTop + scrollableContainer.clientHeight) -
paddingBottom;
setShowScrollToBottomButton(distanceFromBottom > 130);
}, 100);
};
debouncedHandleScroll();
scrollableContainer.addEventListener('scroll', debouncedHandleScroll, {
signal: abortController.signal,
});
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
abortController.abort();
};
}, [aiChatHistory]);
const shouldShowQuickHelpPrompts =
message.length === 0 && aiChatHistory.length === 0;
return (
<div className="ai-chat relative flex min-h-screen w-full flex-col gap-2 overflow-y-auto bg-gray-100">
<div className="relative mx-auto w-full max-w-2xl grow px-4 pb-55">
<div
className="ai-chat relative flex min-h-screen w-full flex-col gap-2 overflow-y-auto bg-gray-100 pb-55"
ref={scrollableContainerRef}
>
<div className="relative mx-auto w-full max-w-2xl grow px-4">
{shouldShowQuickHelpPrompts && (
<QuickHelpPrompts
onQuickActionClick={(action) => {
@@ -223,25 +285,49 @@ export function AIChat() {
/>
)}
<div className="pointer-events-none fixed right-0 bottom-0 left-0 mx-auto w-full max-w-3xl px-4 lg:left-[var(--ai-sidebar-width)]">
<div className="mb-2 flex items-center gap-2">
<QuickActionButton
icon={PersonStandingIcon}
label="Personalized Response"
onClick={() => setIsPersonalizedResponseFormOpen(true)}
/>
<QuickActionButton
icon={FileUpIcon}
label={
isUploading
? 'Processing...'
: userResume?.fileName
? 'Upload New Resume'
: 'Upload Resume'
}
onClick={() => setIsUploadResumeModalOpen(true)}
isLoading={isUploading}
/>
<div
className="pointer-events-none fixed right-0 bottom-0 left-0 mx-auto w-full max-w-3xl px-4 lg:left-[var(--ai-sidebar-width)]"
ref={chatContainerRef}
>
<div className="mb-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<QuickActionButton
icon={PersonStandingIcon}
label="Personalized Response"
onClick={() => setIsPersonalizedResponseFormOpen(true)}
/>
<QuickActionButton
icon={FileUpIcon}
label={
isUploading
? 'Processing...'
: userResume?.fileName
? 'Upload New Resume'
: 'Upload Resume'
}
onClick={() => setIsUploadResumeModalOpen(true)}
isLoading={isUploading}
/>
</div>
<div className="flex items-center gap-2">
{aiChatHistory.length > 0 && (
<QuickActionButton
icon={TrashIcon}
label="Clear Chat"
onClick={() => {
setAiChatHistory([]);
}}
/>
)}
{showScrollToBottomButton && (
<QuickActionButton
icon={ArrowDownIcon}
label="Scroll to Bottom"
onClick={scrollToBottom}
/>
)}
</div>
</div>
<form

View File

@@ -18,9 +18,9 @@ export const ChatHistory = memo((props: ChatHistoryProps) => {
const { chatHistory, isStreamingMessage, streamedMessageHtml } = props;
return (
<div className="flex flex-col">
<div className="flex grow flex-col">
<div className="relative flex grow flex-col justify-end">
<div className="flex flex-col justify-end gap-14 py-5">
<div className="flex grow flex-col justify-end gap-14 py-5">
{chatHistory.map((chat, index) => {
return (
<Fragment key={`chat-${index}`}>
@@ -83,8 +83,8 @@ export const AIChatCard = memo((props: AIChatCardProps) => {
>
<div
className={cn(
'flex items-start gap-2.5 rounded-lg',
role === 'user' ? 'bg-gray-200 p-3 max-w-[70%]' : '',
'flex max-w-full items-start gap-2.5 rounded-lg',
role === 'user' ? 'max-w-[70%] bg-gray-200 p-3' : '',
)}
>
<div

View File

@@ -3,7 +3,7 @@ import { cn } from '../../lib/classname';
type QuickActionButtonProps = {
icon?: LucideIcon;
label: string;
label?: string;
onClick?: () => void;
className?: string;
isLoading?: boolean;