feat: process upload in background

This commit is contained in:
Arik Chakma
2025-06-02 11:06:29 +06:00
parent afc230153e
commit 1f03185a10
5 changed files with 51 additions and 53 deletions

View File

@@ -8,7 +8,7 @@ import { QuickActionButton } from './QuickActionButton';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import type { AIChatHistoryType } from '../GenerateCourse/AICourseLessonChat';
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing';
import { useToast } from '../../hooks/use-toast';
@@ -19,6 +19,8 @@ import { PersonalizedResponseForm } from './PersonalizedResponseForm';
import { userPersonaOptions } from '../../queries/user-persona';
import { UploadResumeModal } from './UploadResumeModal';
import { userResumeOptions } from '../../queries/user-resume';
import { httpPost } from '../../lib/http';
import { httpPost as queryHttpPost } from '../../lib/query-http';
export function AIChat() {
const toast = useToast();
@@ -159,6 +161,26 @@ export function AIChat() {
setIsStreamingMessage(false);
};
const { mutate: uploadResume, isPending: isUploading } = useMutation(
{
mutationFn: (formData: FormData) => {
return queryHttpPost('/v1-upload-resume', formData);
},
onSuccess: () => {
toast.success('Resume uploaded successfully');
setIsUploadResumeModalOpen(false);
queryClient.invalidateQueries(userResumeOptions());
},
onError: (error) => {
toast.error(error?.message || 'Failed to upload resume');
},
onMutate: () => {
setIsUploadResumeModalOpen(false);
},
},
queryClient,
);
const shouldShowQuickHelpPrompts =
message.length === 0 && aiChatHistory.length === 0;
@@ -197,6 +219,8 @@ export function AIChat() {
<UploadResumeModal
onClose={() => setIsUploadResumeModalOpen(false)}
userResume={userResume}
isUploading={isUploading}
uploadResume={uploadResume}
/>
)}
@@ -209,8 +233,15 @@ export function AIChat() {
/>
<QuickActionButton
icon={FileUpIcon}
label="Upload Resume"
label={
isUploading
? 'Uploading...'
: userResume?.fileName
? 'Upload New Resume'
: 'Upload Resume'
}
onClick={() => setIsUploadResumeModalOpen(true)}
isLoading={isUploading}
/>
</div>

View File

@@ -1,4 +1,4 @@
import type { LucideIcon } from 'lucide-react';
import { Loader2Icon, type LucideIcon } from 'lucide-react';
import { cn } from '../../lib/classname';
type QuickActionButtonProps = {
@@ -6,20 +6,23 @@ type QuickActionButtonProps = {
label: string;
onClick?: () => void;
className?: string;
isLoading?: boolean;
};
export function QuickActionButton(props: QuickActionButtonProps) {
const { icon: Icon, label, onClick, className } = props;
const { icon: Icon, label, onClick, className, isLoading } = props;
return (
<button
className={cn(
'pointer-events-auto flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border border-gray-200 bg-white px-2 py-1.5 text-sm text-gray-500 hover:bg-gray-100 hover:text-black',
'pointer-events-auto flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border border-gray-200 bg-white px-2 py-1.5 text-sm text-gray-500 hover:bg-gray-100 hover:text-black disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
onClick={onClick}
disabled={isLoading}
>
{Icon && <Icon className="size-4" />}
{Icon && !isLoading && <Icon className="size-4" />}
{isLoading && Icon && <Loader2Icon className="size-4 animate-spin" />}
{label}
</button>
);

View File

@@ -22,10 +22,17 @@ type OnDrop<T extends File = File> = (
type UploadResumeModalProps = {
userResume?: UserResumeDocument;
onClose: () => void;
isUploading: boolean;
uploadResume: (formData: FormData) => void;
};
export function UploadResumeModal(props: UploadResumeModalProps) {
const { onClose, userResume: defaultUserResume } = props;
const {
onClose,
userResume: defaultUserResume,
isUploading,
uploadResume,
} = props;
const toast = useToast();
const [file, setFile] = useState<File | null>(
@@ -49,22 +56,6 @@ export function UploadResumeModal(props: UploadResumeModalProps) {
maxSize: 5 * 1024 * 1024, // 5MB
});
const { mutate: uploadResume, isPending: isUploading } = useMutation(
{
mutationFn: (formData: FormData) => {
return httpPost('/v1-upload-resume', formData);
},
onSuccess: () => {
onClose();
toast.success('Resume uploaded successfully');
},
onError: (error) => {
toast.error(error?.message || 'Failed to upload resume');
},
},
queryClient,
);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

View File

@@ -1,27 +1,12 @@
---
import { CheckSubscriptionVerification } from '../../../components/Billing/CheckSubscriptionVerification';
import { RoadmapAIChat } from '../../../components/RoadmapAIChat/RoadmapAIChat';
import SkeletonLayout from '../../../layouts/SkeletonLayout.astro';
import { AITutorLayout } from '../../../components/AITutor/AITutorLayout';
type Props = {
roadmapId: string;
};
const { roadmapId } = Astro.params as Props;
import { AIChat } from '../../../components/AIChat/AIChat';
---
<SkeletonLayout
title='Roadmap AI Chat'
title='AI Chat'
noIndex={true}
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.'
>
<AITutorLayout
activeTab='chat'
wrapperClassName='flex-row p-0 lg:p-0 overflow-hidden'
client:load
>
<h1>Roadmap AI Chat</h1>
<CheckSubscriptionVerification client:load />
</AITutorLayout>
<AIChat client:load />
</SkeletonLayout>

View File

@@ -1,12 +0,0 @@
---
import SkeletonLayout from '../layouts/SkeletonLayout.astro';
import { AIChat } from '../components/AIChat/AIChat';
---
<SkeletonLayout
title='AI Chat'
noIndex={true}
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.'
>
<AIChat client:load />
</SkeletonLayout>