mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-12 17:51:53 +08:00
feat: process upload in background
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user