import { AlertTriangle, Check, FileText, Loader2, Sparkles, } from "lucide-react"; import type React from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Separator } from "@/components/ui/separator"; import type { Job, ResumeProjectCatalogItem } from "../../shared/types"; import * as api from "../api"; interface TailoringEditorProps { job: Job; onUpdate: () => void | Promise; onDirtyChange?: (isDirty: boolean) => void; onRegisterSave?: (save: () => Promise) => void; onBeforeGenerate?: () => boolean | Promise; } export const TailoringEditor: React.FC = ({ job, onUpdate, onDirtyChange, onRegisterSave, onBeforeGenerate, }) => { const [catalog, setCatalog] = useState([]); const [summary, setSummary] = useState(job.tailoredSummary || ""); const [jobDescription, setJobDescription] = useState( job.jobDescription || "", ); const [selectedIds, setSelectedIds] = useState>(new Set()); const [isSummarizing, setIsSummarizing] = useState(false); const [isGeneratingPdf, setIsGeneratingPdf] = useState(false); const [isSaving, setIsSaving] = useState(false); const savedSelectedIds = useMemo(() => { const saved = job.selectedProjectIds?.split(",").filter(Boolean) ?? []; return new Set(saved); }, [job.selectedProjectIds]); const hasSelectionDiff = useMemo(() => { if (selectedIds.size !== savedSelectedIds.size) return true; for (const id of selectedIds) { if (!savedSelectedIds.has(id)) return true; } return false; }, [selectedIds, savedSelectedIds]); const isDirty = summary !== (job.tailoredSummary || "") || jobDescription !== (job.jobDescription || "") || hasSelectionDiff; useEffect(() => { onDirtyChange?.(isDirty); }, [isDirty, onDirtyChange]); useEffect(() => { // Load project catalog api.getResumeProjectsCatalog().then(setCatalog).catch(console.error); // Set initial selection if (job.selectedProjectIds) { setSelectedIds( new Set(job.selectedProjectIds.split(",").filter(Boolean)), ); } setJobDescription(job.jobDescription || ""); }, [job.selectedProjectIds, job.jobDescription]); useEffect(() => { setSummary(job.tailoredSummary || ""); }, [job.tailoredSummary]); const saveChanges = useCallback( async ({ showToast = true }: { showToast?: boolean } = {}) => { try { setIsSaving(true); await api.updateJob(job.id, { tailoredSummary: summary, jobDescription: jobDescription, selectedProjectIds: Array.from(selectedIds).join(","), }); if (showToast) toast.success("Changes saved"); await onUpdate(); } catch (error) { if (showToast) toast.error("Failed to save changes"); throw error; } finally { setIsSaving(false); } }, [job.id, onUpdate, selectedIds, summary, jobDescription], ); useEffect(() => { onRegisterSave?.(() => saveChanges({ showToast: false })); }, [onRegisterSave, saveChanges]); const handleToggleProject = (id: string) => { const next = new Set(selectedIds); if (next.has(id)) next.delete(id); else next.add(id); setSelectedIds(next); }; const handleSave = async () => { try { await saveChanges(); } catch { // Toast handled in saveChanges } }; const handleSummarize = async () => { try { setIsSummarizing(true); // Save changes first so AI uses latest description if (isDirty) { await saveChanges({ showToast: false }); } const updatedJob = await api.summarizeJob(job.id, { force: true }); setSummary(updatedJob.tailoredSummary || ""); setJobDescription(updatedJob.jobDescription || ""); if (updatedJob.selectedProjectIds) { setSelectedIds( new Set(updatedJob.selectedProjectIds.split(",").filter(Boolean)), ); } toast.success("AI Summary & Projects generated"); await onUpdate(); } catch (_error) { toast.error("AI summarization failed"); } finally { setIsSummarizing(false); } }; const handleGeneratePdf = async () => { try { const shouldProceed = onBeforeGenerate ? await onBeforeGenerate() : true; if (shouldProceed === false) return; setIsGeneratingPdf(true); // Save current state first to ensure PDF uses latest await saveChanges({ showToast: false }); await api.generateJobPdf(job.id); toast.success("Resume PDF generated"); await onUpdate(); } catch (_error) { toast.error("PDF generation failed"); } finally { setIsGeneratingPdf(false); } }; const maxProjects = 3; // Example limit, could come from settings const tooManyProjects = selectedIds.size > maxProjects; return (

Editor