diff --git a/orchestrator/src/client/components/DiscoveredPanel.tsx b/orchestrator/src/client/components/DiscoveredPanel.tsx deleted file mode 100644 index 80f8cc3..0000000 --- a/orchestrator/src/client/components/DiscoveredPanel.tsx +++ /dev/null @@ -1,699 +0,0 @@ -/** - * DiscoveredPanel - Two-mode triage workspace for Discovered jobs. - * - * Mode A: Decide (default) - Quick assessment to Skip or Tailor - * Mode B: Tailor - Draft tailoring data before moving to Ready - * - * Moving to Ready generates the PDF using the current tailored draft. - */ - -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { - AlertTriangle, - ArrowLeft, - Calendar, - Check, - ChevronDown, - ChevronUp, - DollarSign, - ExternalLink, - Loader2, - MapPin, - Sparkles, - XCircle, -} from "lucide-react"; -import { toast } from "sonner"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Separator } from "@/components/ui/separator"; -import { cn } from "@/lib/utils"; -import { formatDate } from "../lib/dateUtils"; -import * as api from "../api"; -import { FitAssessment } from "."; -import type { Job, ResumeProjectCatalogItem } from "../../shared/types"; - -// ───────────────────────────────────────────────────────────────────────────── -// Types -// ───────────────────────────────────────────────────────────────────────────── - -type PanelMode = "decide" | "tailor"; - -interface DiscoveredPanelProps { - job: Job | null; - onJobUpdated: () => void | Promise; - onJobMoved: (jobId: string) => void; -} - -// ───────────────────────────────────────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────────────────────────────────────── - -const stripHtml = (value: string) => - value - .replace(/<[^>]*>/g, " ") - .replace(/\s+/g, " ") - .trim(); - -const sourceLabel: Record = { - gradcracker: "Gradcracker", - indeed: "Indeed", - linkedin: "LinkedIn", - ukvisajobs: "UK Visa Jobs", - manual: "Manual", -}; - -// ───────────────────────────────────────────────────────────────────────────── -// Decide Mode Panel -// ───────────────────────────────────────────────────────────────────────────── - -interface DecideModeProps { - job: Job; - onTailor: () => void; - onSkip: () => void; - isSkipping: boolean; -} - -const DecideMode: React.FC = ({ - job, - onTailor, - onSkip, - isSkipping, -}) => { - const [showDescription, setShowDescription] = useState(false); - const deadline = formatDate(job.deadline); - const jobLink = job.applicationLink || job.jobUrl; - - const description = useMemo(() => { - if (!job.jobDescription) return "No description available."; - const jd = job.jobDescription; - if (jd.includes("<") && jd.includes(">")) return stripHtml(jd); - return jd; - }, [job.jobDescription]); - - return ( -
- {/* Header */} -
-
-
-

- {job.title} -

-

- {job.employer} -

-
- -
- - {sourceLabel[job.source]} - -
-
- - {/* Metadata row */} -
- {job.location && ( - - - {job.location} - - )} - {deadline && ( - - - {deadline} - - )} - {job.salary && ( - - - {job.salary} - - )} -
- - {/* Primary/Secondary actions */} -
- - -
-
- - - - {/* Fit Summary - the core content */} -
- - - {/* Collapsible full description */} -
- - - {showDescription && ( -
-

- {description} -

-
- )} -
-
- - - - {/* Actions - clear hierarchy */} -
- {/* External link - tertiary */} - -
-
- ); -}; - -// ───────────────────────────────────────────────────────────────────────────── -// Tailor Mode Panel -// ───────────────────────────────────────────────────────────────────────────── - -interface TailorModeProps { - job: Job; - onBack: () => void; - onFinalize: () => void; - isFinalizing: boolean; -} - -const TailorMode: React.FC = ({ - job, - onBack, - onFinalize, - isFinalizing, -}) => { - const [catalog, setCatalog] = useState([]); - const [summary, setSummary] = useState(job.tailoredSummary || ""); - const [jobDescription, setJobDescription] = useState(job.jobDescription || ""); - const [selectedIds, setSelectedIds] = useState>(() => { - const saved = job.selectedProjectIds?.split(",").filter(Boolean) ?? []; - return new Set(saved); - }); - const [isGenerating, setIsGenerating] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [draftStatus, setDraftStatus] = useState< - "unsaved" | "saving" | "saved" - >("saved"); - const [showDescription, setShowDescription] = useState(false); - - // Load project catalog - useEffect(() => { - api.getProfileProjects().then(setCatalog).catch(console.error); - }, []); - - // Reset form when job changes - useEffect(() => { - setSummary(job.tailoredSummary || ""); - setJobDescription(job.jobDescription || ""); - const saved = job.selectedProjectIds?.split(",").filter(Boolean) ?? []; - setSelectedIds(new Set(saved)); - setDraftStatus("saved"); - }, [job.id, job.tailoredSummary, job.selectedProjectIds, job.jobDescription]); - - // Track unsaved changes - const savedSummary = job.tailoredSummary || ""; - const savedDescription = job.jobDescription || ""; - const savedIds = useMemo(() => { - const saved = job.selectedProjectIds?.split(",").filter(Boolean) ?? []; - return new Set(saved); - }, [job.selectedProjectIds]); - - const hasChanges = useMemo(() => { - if (summary !== savedSummary) return true; - if (jobDescription !== savedDescription) return true; - if (selectedIds.size !== savedIds.size) return true; - for (const id of selectedIds) { - if (!savedIds.has(id)) return true; - } - return false; - }, [summary, savedSummary, jobDescription, savedDescription, selectedIds, savedIds]); - - // Update draft status when changes are made - useEffect(() => { - if (hasChanges && draftStatus === "saved") { - setDraftStatus("unsaved"); - } - }, [hasChanges, draftStatus]); - - // Auto-save draft (debounced) - useEffect(() => { - if (!hasChanges || draftStatus !== "unsaved") return; - - const timeout = setTimeout(async () => { - try { - setDraftStatus("saving"); - await api.updateJob(job.id, { - tailoredSummary: summary, - jobDescription: jobDescription, - selectedProjectIds: Array.from(selectedIds).join(","), - }); - setDraftStatus("saved"); - } catch { - setDraftStatus("unsaved"); - } - }, 1500); - - return () => clearTimeout(timeout); - }, [summary, jobDescription, selectedIds, hasChanges, draftStatus, job.id]); - - const handleToggleProject = (id: string) => { - const next = new Set(selectedIds); - if (next.has(id)) next.delete(id); - else next.add(id); - setSelectedIds(next); - }; - - const handleGenerateWithAI = async () => { - try { - setIsGenerating(true); - - // Save any pending changes first so AI uses the latest description - if (hasChanges) { - await api.updateJob(job.id, { - tailoredSummary: summary, - jobDescription: jobDescription, - selectedProjectIds: Array.from(selectedIds).join(","), - }); - } - - 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)) - ); - } - setDraftStatus("saved"); // AI response is saved server-side - toast.success("Draft generated with AI", { - description: "Review and edit before finalizing.", - }); - } catch { - toast.error("Failed to generate AI draft"); - } finally { - setIsGenerating(false); - } - }; - - const handleFinalize = async () => { - // Save any pending changes first - if (hasChanges) { - try { - setIsSaving(true); - await api.updateJob(job.id, { - tailoredSummary: summary, - jobDescription: jobDescription, - selectedProjectIds: Array.from(selectedIds).join(","), - }); - } catch { - toast.error("Failed to save draft before finalizing"); - setIsSaving(false); - return; - } finally { - setIsSaving(false); - } - } - - // Now finalize (which generates PDF and moves to Ready) - onFinalize(); - }; - - const maxProjects = 3; - const tooManyProjects = selectedIds.size > maxProjects; - const canFinalize = summary.trim().length > 0 && selectedIds.size > 0; - - return ( -
- {/* Header with back navigation */} -
- - - {/* Draft status indicator */} -
- {draftStatus === "saving" && ( - <> - - Saving... - - )} - {draftStatus === "saved" && !hasChanges && ( - <> - - Saved - - )} - {draftStatus === "unsaved" && ( - Unsaved changes - )} -
-
- - {/* Draft framing */} -
-
-
- - Draft tailoring for this role - -
-

- Edit below, then finalize to generate your PDF and move to Ready. -

-
- - {/* Scrollable content */} -
- {/* AI Generate option */} -
-
-
- Need help getting started? -
-
- AI can draft a summary and select projects for you -
-
- -
- - {/* Job Description - collapsible */} -
- - - {showDescription && ( -
- -