fit assessment UI
This commit is contained in:
parent
bdae9d13cc
commit
d9187acb37
@ -30,6 +30,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import * as api from "../api";
|
import * as api from "../api";
|
||||||
|
import { FitAssessment } from ".";
|
||||||
import type { Job, ResumeProjectCatalogItem } from "../../shared/types";
|
import type { Job, ResumeProjectCatalogItem } from "../../shared/types";
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -61,46 +62,6 @@ 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 stripHtml = (value: string) =>
|
const stripHtml = (value: string) =>
|
||||||
value
|
value
|
||||||
.replace(/<[^>]*>/g, " ")
|
.replace(/<[^>]*>/g, " ")
|
||||||
@ -114,44 +75,6 @@ const sourceLabel: Record<Job["source"], string> = {
|
|||||||
ukvisajobs: "UK Visa Jobs",
|
ukvisajobs: "UK Visa Jobs",
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Fit Summary Component (for Decide mode)
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
interface FitSummaryProps {
|
|
||||||
job: Job;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FitSummary: React.FC<FitSummaryProps> = ({ job }) => {
|
|
||||||
return (
|
|
||||||
<div className='space-y-3'>
|
|
||||||
{/* AI Assessment */}
|
|
||||||
{job.suitabilityReason && (
|
|
||||||
<div className='rounded-lg border border-border/40 bg-muted/10 p-3'>
|
|
||||||
<div className='text-[10px] uppercase tracking-wide text-muted-foreground/70 mb-1.5'>
|
|
||||||
AI Assessment
|
|
||||||
</div>
|
|
||||||
<p className='text-sm text-foreground/80 leading-relaxed'>
|
|
||||||
{job.suitabilityReason}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* No assessment fallback */}
|
|
||||||
{!job.suitabilityReason && !job.suitabilityScore && (
|
|
||||||
<div className='rounded-lg border border-border/40 bg-muted/10 p-3 text-center'>
|
|
||||||
<p className='text-sm text-muted-foreground'>
|
|
||||||
No AI assessment available yet.
|
|
||||||
</p>
|
|
||||||
<p className='text-xs text-muted-foreground/70 mt-1'>
|
|
||||||
Review the job description to decide if you want to tailor.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Decide Mode Panel
|
// Decide Mode Panel
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -172,7 +95,6 @@ const DecideMode: React.FC<DecideModeProps> = ({
|
|||||||
const [showDescription, setShowDescription] = useState(false);
|
const [showDescription, setShowDescription] = useState(false);
|
||||||
const deadline = formatDate(job.deadline);
|
const deadline = formatDate(job.deadline);
|
||||||
const jobLink = job.applicationLink || job.jobUrl;
|
const jobLink = job.applicationLink || job.jobUrl;
|
||||||
const scoreInfo = getScoreLabel(job.suitabilityScore);
|
|
||||||
|
|
||||||
const description = useMemo(() => {
|
const description = useMemo(() => {
|
||||||
if (!job.jobDescription) return "No description available.";
|
if (!job.jobDescription) return "No description available.";
|
||||||
@ -195,25 +117,13 @@ const DecideMode: React.FC<DecideModeProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center space-y-2">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<Badge
|
<Badge
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='text-[10px] uppercase tracking-wide text-muted-foreground border-border/50 shrink-0'
|
className='text-[10px] uppercase tracking-wide text-muted-foreground border-border/50 shrink-0'
|
||||||
>
|
>
|
||||||
{sourceLabel[job.source]}
|
{sourceLabel[job.source]}
|
||||||
</Badge>
|
</Badge>
|
||||||
{job.suitabilityScore != null && (
|
|
||||||
<div className='flex justify-end'>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-xl font-bold tabular-nums",
|
|
||||||
scoreInfo.color
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{job.suitabilityScore}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -244,7 +154,7 @@ const DecideMode: React.FC<DecideModeProps> = ({
|
|||||||
|
|
||||||
{/* Fit Summary - the core content */}
|
{/* Fit Summary - the core content */}
|
||||||
<div className='flex-1 py-4 space-y-4 overflow-y-auto'>
|
<div className='flex-1 py-4 space-y-4 overflow-y-auto'>
|
||||||
<FitSummary job={job} />
|
<FitAssessment job={job} />
|
||||||
|
|
||||||
{/* Collapsible full description */}
|
{/* Collapsible full description */}
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
|
|||||||
30
orchestrator/src/client/components/FitAssessment.tsx
Normal file
30
orchestrator/src/client/components/FitAssessment.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Sparkles } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { Job } from "../../shared/types";
|
||||||
|
|
||||||
|
interface FitAssessmentProps {
|
||||||
|
job: Job;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FitAssessment: React.FC<FitAssessmentProps> = ({
|
||||||
|
job,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
if (!job.suitabilityReason) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("space-y-3", className)}>
|
||||||
|
<div className="rounded-lg border border-primary/20 bg-primary/5 px-3 py-2.5">
|
||||||
|
<div className="text-[11px] font-medium uppercase tracking-wide text-primary/70 mb-1.5 flex items-center gap-1.5">
|
||||||
|
<Sparkles className="h-3 w-3" />
|
||||||
|
Fit Assessment
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-foreground/90 leading-relaxed font-medium">
|
||||||
|
{job.suitabilityReason}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -22,7 +22,6 @@ import {
|
|||||||
Briefcase,
|
Briefcase,
|
||||||
Building2,
|
Building2,
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
Sparkles,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@ -43,6 +42,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { copyTextToClipboard, formatJobForWebhook } from "@client/lib/jobCopy";
|
import { copyTextToClipboard, formatJobForWebhook } from "@client/lib/jobCopy";
|
||||||
import * as api from "../api";
|
import * as api from "../api";
|
||||||
|
import { FitAssessment } from ".";
|
||||||
import type { Job, ResumeProjectCatalogItem } from "../../shared/types";
|
import type { Job, ResumeProjectCatalogItem } from "../../shared/types";
|
||||||
|
|
||||||
interface ReadyPanelProps {
|
interface ReadyPanelProps {
|
||||||
@ -275,18 +275,7 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
|
|||||||
<div className="flex-1 py-4 space-y-4">
|
<div className="flex-1 py-4 space-y-4">
|
||||||
{/* Job identity - confirm this is the right role */}
|
{/* Job identity - confirm this is the right role */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* AI Suitability Reasoning - Why you're a fit */}
|
<FitAssessment job={job} />
|
||||||
{job.suitabilityReason && (
|
|
||||||
<div className="rounded-lg border border-primary/20 bg-primary/5 px-3 py-2.5">
|
|
||||||
<div className="text-[11px] font-medium uppercase tracking-wide text-primary/70 mb-1.5 flex items-center gap-1.5">
|
|
||||||
<Sparkles className="h-3 w-3" />
|
|
||||||
Fit Assessment
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-foreground/90 leading-relaxed font-medium">
|
|
||||||
{job.suitabilityReason}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tailored summary snippet - shows what's in the PDF */}
|
{/* Tailored summary snippet - shows what's in the PDF */}
|
||||||
{tailoredSummary && (
|
{tailoredSummary && (
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export { Header } from './Header';
|
|||||||
export { Stats } from './Stats';
|
export { Stats } from './Stats';
|
||||||
export { StatusBadge } from './StatusBadge';
|
export { StatusBadge } from './StatusBadge';
|
||||||
export { ScoreIndicator } from './ScoreIndicator';
|
export { ScoreIndicator } from './ScoreIndicator';
|
||||||
|
export { FitAssessment } from './FitAssessment';
|
||||||
export { JobCard } from './JobCard';
|
export { JobCard } from './JobCard';
|
||||||
export { JobTable } from './JobTable';
|
export { JobTable } from './JobTable';
|
||||||
export { JobList } from './JobList';
|
export { JobList } from './JobList';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user