diff --git a/orchestrator/src/client/components/DiscoveredPanel.tsx b/orchestrator/src/client/components/DiscoveredPanel.tsx index 21fe0aa..02d5325 100644 --- a/orchestrator/src/client/components/DiscoveredPanel.tsx +++ b/orchestrator/src/client/components/DiscoveredPanel.tsx @@ -1,9 +1,9 @@ /** * 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. */ @@ -61,16 +61,51 @@ const formatDate = (dateStr: string | null) => { } }; -const getScoreLabel = (score: number | null): { label: string; color: string; description: string } => { - if (score == null) return { label: "Unscored", color: "text-muted-foreground", description: "No AI assessment yet" }; - if (score >= 80) return { label: "Excellent fit", color: "text-emerald-400", description: "Strong match for your profile" }; - if (score >= 65) return { label: "Good fit", color: "text-emerald-400/80", description: "Solid match worth considering" }; - if (score >= 50) return { label: "Possible fit", color: "text-amber-400", description: "Some relevant aspects" }; - if (score >= 35) return { label: "Weak fit", color: "text-orange-400", description: "Limited alignment" }; - return { label: "Poor fit", color: "text-rose-400", description: "May not be worth pursuing" }; +const getScoreLabel = ( + score: number | null +): { label: string; color: string; description: string } => { + if (score == null) + return { + label: "Unscored", + color: "text-muted-foreground", + description: "No AI assessment yet", + }; + if (score >= 80) + return { + label: "Excellent fit", + color: "text-emerald-400", + description: "Strong match for your profile", + }; + if (score >= 65) + return { + label: "Good fit", + color: "text-emerald-400/80", + description: "Solid match worth considering", + }; + if (score >= 50) + return { + label: "Possible fit", + color: "text-amber-400", + description: "Some relevant aspects", + }; + if (score >= 35) + return { + label: "Weak fit", + color: "text-orange-400", + description: "Limited alignment", + }; + return { + label: "Poor fit", + color: "text-rose-400", + description: "May not be worth pursuing", + }; }; -const stripHtml = (value: string) => value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); +const stripHtml = (value: string) => + value + .replace(/<[^>]*>/g, " ") + .replace(/\s+/g, " ") + .trim(); const sourceLabel: Record = { gradcracker: "Gradcracker", @@ -88,43 +123,15 @@ interface FitSummaryProps { } const FitSummary: React.FC = ({ job }) => { - const scoreInfo = getScoreLabel(job.suitabilityScore); - return ( -
- {/* Score badge with context */} -
-
= 50 - ? "border-emerald-500/30 bg-emerald-500/10" - : job.suitabilityScore != null - ? "border-amber-500/30 bg-amber-500/10" - : "border-border/50 bg-muted/20" - )}> - {job.suitabilityScore != null && ( - - {job.suitabilityScore} - - )} -
-
- {scoreInfo.label} -
-
- {scoreInfo.description} -
-
-
-
- +
{/* AI Assessment */} {job.suitabilityReason && ( -
-
+
+
AI Assessment
-

+

{job.suitabilityReason}

@@ -132,11 +139,11 @@ const FitSummary: React.FC = ({ job }) => { {/* No assessment fallback */} {!job.suitabilityReason && !job.suitabilityScore && ( -
-

+

+

No AI assessment available yet.

-

+

Review the job description to decide if you want to tailor.

@@ -165,7 +172,8 @@ const DecideMode: React.FC = ({ const [showDescription, setShowDescription] = useState(false); const deadline = formatDate(job.deadline); const jobLink = job.applicationLink || job.jobUrl; - + const scoreInfo = getScoreLabel(job.suitabilityScore); + const description = useMemo(() => { if (!job.jobDescription) return "No description available."; const jd = job.jobDescription; @@ -174,68 +182,88 @@ const DecideMode: React.FC = ({ }, [job.jobDescription]); return ( -
+
{/* Header */} -
-
-
-

+
+
+
+

{job.title}

-

{job.employer}

+

+ {job.employer} +

+
+ +
+ + {sourceLabel[job.source]} + + {job.suitabilityScore != null && ( +
+ + {job.suitabilityScore}% + +
+ )}
- - {sourceLabel[job.source]} -
{/* Metadata row */} -
+
{job.location && ( - - + + {job.location} )} {deadline && ( - - + + {deadline} )} {job.salary && ( - - + + {job.salary} )}
- + {/* Fit Summary - the core content */} -
+
{/* Collapsible full description */} -
+
- + {showDescription && ( -
-

+

+

{description}

@@ -243,45 +271,45 @@ const DecideMode: React.FC = ({
- + {/* Actions - clear hierarchy */} -
+
{/* External link - tertiary */} -
+ {/* Primary/Secondary actions */} -
+
@@ -315,7 +343,9 @@ const TailorMode: React.FC = ({ }); const [isGenerating, setIsGenerating] = useState(false); const [isSaving, setIsSaving] = useState(false); - const [draftStatus, setDraftStatus] = useState<"unsaved" | "saving" | "saved">("saved"); + const [draftStatus, setDraftStatus] = useState< + "unsaved" | "saving" | "saved" + >("saved"); // Load project catalog useEffect(() => { @@ -386,7 +416,9 @@ const TailorMode: React.FC = ({ const updatedJob = await api.summarizeJob(job.id, { force: true }); setSummary(updatedJob.tailoredSummary || ""); if (updatedJob.selectedProjectIds) { - setSelectedIds(new Set(updatedJob.selectedProjectIds.split(",").filter(Boolean))); + setSelectedIds( + new Set(updatedJob.selectedProjectIds.split(",").filter(Boolean)) + ); } setDraftStatus("saved"); // AI response is saved server-side toast.success("Draft generated with AI", { @@ -426,108 +458,110 @@ const TailorMode: React.FC = ({ 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 + 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?
-
+
+ Need help getting started? +
+
AI can draft a summary and select projects for you
{/* Tailored Summary */} -
-