UI intagration into header top section

This commit is contained in:
DaKheera47 2026-01-20 22:40:43 +00:00
parent 30385cfe24
commit 29b259352f
7 changed files with 60 additions and 37 deletions

View File

@ -46,13 +46,13 @@ const ScoreMeter: React.FC<{ score: number | null }> = ({ score }) => {
); );
}; };
interface SponsorBadgeProps { interface SponsorPillProps {
score: number | null; score: number | null;
names: string | null; names: string | null;
onCheck?: () => Promise<void>; onCheck?: () => Promise<void>;
} }
const SponsorBadge: React.FC<SponsorBadgeProps> = ({ score, names, onCheck }) => { const SponsorPill: React.FC<SponsorPillProps> = ({ score, names, onCheck }) => {
const [isChecking, setIsChecking] = useState(false); const [isChecking, setIsChecking] = useState(false);
const parsedNames = useMemo(() => { const parsedNames = useMemo(() => {
@ -78,12 +78,12 @@ const SponsorBadge: React.FC<SponsorBadgeProps> = ({ score, names, onCheck }) =>
if (score == null && onCheck) { if (score == null && onCheck) {
return ( return (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip delayDuration={0}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
className="h-5 px-1.5 text-[9px] font-medium text-muted-foreground hover:text-foreground" className="h-5 px-1.5 text-[9px] font-medium text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
onClick={handleCheck} onClick={handleCheck}
disabled={isChecking} disabled={isChecking}
> >
@ -92,7 +92,7 @@ const SponsorBadge: React.FC<SponsorBadgeProps> = ({ score, names, onCheck }) =>
) : ( ) : (
<Search className="h-2.5 w-2.5" /> <Search className="h-2.5 w-2.5" />
)} )}
<span className="ml-0.5">{isChecking ? "Checking..." : "Check Visa"}</span> <span>{isChecking ? "Checking..." : "Check Visa"}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side="top">
@ -103,34 +103,30 @@ const SponsorBadge: React.FC<SponsorBadgeProps> = ({ score, names, onCheck }) =>
); );
} }
// If no score (and no callback), or error in score, show nothing
if (score == null) { if (score == null) {
return null; return null;
} }
const isFound = score >= 95; const canSponsor = score >= 95;
const tooltipContent = isFound const label = canSponsor ? "Can Sponsor" : "Unsure if Sponsor";
? `Confirmed Visa Sponsor (${score}% match: ${parsedNames.join(", ")})` const dotClass = canSponsor ? "bg-emerald-500" : "bg-slate-500";
: `Sponsor Not Found (${score}% match${parsedNames.length > 0 ? `: ${parsedNames.join(", ")}` : ""})`; const tooltipContent = canSponsor
? `${score}% match`
: `Closest: ${parsedNames.join(", ")} (${score}% match)`;
return ( return (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip delayDuration={0}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span <span className="inline-flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground/80 cursor-help">
className={cn( <span className={cn("h-1.5 w-1.5 rounded-full opacity-80", dotClass)} />
"inline-flex items-center gap-1 rounded-full border px-1 py-1 text-[9px] font-semibold uppercase tracking-wide cursor-help transition-colors", {label}
isFound
? "border-emerald-500/40 bg-emerald-500/15 text-emerald-400"
: "border-slate-500/40 bg-slate-500/15 text-slate-400"
)}
>
<Shield className={cn("h-3 w-3", isFound ? "fill-emerald-400/20" : "")} />
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" className="max-w-xs"> <TooltipContent side="top" className="max-w-xs">
<p className="text-xs font-medium">{isFound ? "Found" : "Not Found"}</p> {canSponsor && <p className="text-xs font-medium">{parsedNames.join(", ")}</p>}
<p className="text-[10px] opacity-80 mt-1">{tooltipContent}</p> {!canSponsor && <p className="text-xs font-medium">Unsure if sponsor</p>}
<p className="opacity-80 mt-1 text-xs">{tooltipContent}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@ -148,13 +144,6 @@ export const JobHeader: React.FC<JobHeaderProps> = ({ job, className, showSponso
<div className="truncate text-base font-semibold text-foreground/90">{job.title}</div> <div className="truncate text-base font-semibold text-foreground/90">{job.title}</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground"> <div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>{job.employer}</span> <span>{job.employer}</span>
{showSponsorInfo && (
<SponsorBadge
score={job.sponsorMatchScore}
names={job.sponsorMatchNames}
onCheck={onCheckSponsor}
/>
)}
</div> </div>
</div> </div>
<Badge variant="outline" className="text-[10px] uppercase tracking-wide text-muted-foreground border-border/50"> <Badge variant="outline" className="text-[10px] uppercase tracking-wide text-muted-foreground border-border/50">
@ -186,7 +175,16 @@ export const JobHeader: React.FC<JobHeaderProps> = ({ job, className, showSponso
{/* Status and score: single line, subdued */} {/* Status and score: single line, subdued */}
<div className="flex items-center justify-between gap-2 py-1 border-y border-border/30"> <div className="flex items-center justify-between gap-2 py-1 border-y border-border/30">
<StatusPill status={job.status} /> <div className="flex items-center gap-4">
<StatusPill status={job.status} />
{showSponsorInfo && (
<SponsorPill
score={job.sponsorMatchScore}
names={job.sponsorMatchNames}
onCheck={onCheckSponsor}
/>
)}
</div>
<ScoreMeter score={job.suitabilityScore} /> <ScoreMeter score={job.suitabilityScore} />
</div> </div>
</div> </div>

View File

@ -50,6 +50,7 @@ interface ReadyPanelProps {
onJobMoved: (jobId: string) => void; onJobMoved: (jobId: string) => void;
onEditTailoring: () => void; onEditTailoring: () => void;
onEditDescription: () => void; onEditDescription: () => void;
showSponsorInfo?: boolean;
} }
const safeFilenamePart = (value: string | null | undefined) => const safeFilenamePart = (value: string | null | undefined) =>
@ -61,6 +62,7 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
onJobMoved, onJobMoved,
onEditTailoring, onEditTailoring,
onEditDescription, onEditDescription,
showSponsorInfo,
}) => { }) => {
const [isMarkingApplied, setIsMarkingApplied] = useState(false); const [isMarkingApplied, setIsMarkingApplied] = useState(false);
const [isRegenerating, setIsRegenerating] = useState(false); const [isRegenerating, setIsRegenerating] = useState(false);
@ -222,6 +224,7 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
await api.checkSponsor(job.id); await api.checkSponsor(job.id);
await onJobUpdated(); await onJobUpdated();
}} }}
showSponsorInfo={showSponsorInfo}
/> />
{/* {/*

View File

@ -15,6 +15,7 @@ interface DecideModeProps {
onSkip: () => void; onSkip: () => void;
isSkipping: boolean; isSkipping: boolean;
onCheckSponsor?: () => Promise<void>; onCheckSponsor?: () => Promise<void>;
showSponsorInfo?: boolean;
} }
export const DecideMode: React.FC<DecideModeProps> = ({ export const DecideMode: React.FC<DecideModeProps> = ({
@ -23,6 +24,7 @@ export const DecideMode: React.FC<DecideModeProps> = ({
onSkip, onSkip,
isSkipping, isSkipping,
onCheckSponsor, onCheckSponsor,
showSponsorInfo,
}) => { }) => {
const [showDescription, setShowDescription] = useState(false); const [showDescription, setShowDescription] = useState(false);
const jobLink = job.applicationLink || job.jobUrl; const jobLink = job.applicationLink || job.jobUrl;
@ -35,7 +37,11 @@ export const DecideMode: React.FC<DecideModeProps> = ({
return ( return (
<div className='flex flex-col h-full'> <div className='flex flex-col h-full'>
<div className='space-y-4 pb-4'> <div className='space-y-4 pb-4'>
<JobHeader job={job} onCheckSponsor={onCheckSponsor} /> <JobHeader
job={job}
onCheckSponsor={onCheckSponsor}
showSponsorInfo={showSponsorInfo}
/>
<div className='flex flex-col gap-2.5 pt-2 sm:flex-row'> <div className='flex flex-col gap-2.5 pt-2 sm:flex-row'>
<Button <Button

View File

@ -14,12 +14,14 @@ interface DiscoveredPanelProps {
job: Job | null; job: Job | null;
onJobUpdated: () => void | Promise<void>; onJobUpdated: () => void | Promise<void>;
onJobMoved: (jobId: string) => void; onJobMoved: (jobId: string) => void;
showSponsorInfo?: boolean;
} }
export const DiscoveredPanel: React.FC<DiscoveredPanelProps> = ({ export const DiscoveredPanel: React.FC<DiscoveredPanelProps> = ({
job, job,
onJobUpdated, onJobUpdated,
onJobMoved, onJobMoved,
showSponsorInfo,
}) => { }) => {
const [mode, setMode] = useState<PanelMode>("decide"); const [mode, setMode] = useState<PanelMode>("decide");
const [isSkipping, setIsSkipping] = useState(false); const [isSkipping, setIsSkipping] = useState(false);
@ -89,6 +91,7 @@ export const DiscoveredPanel: React.FC<DiscoveredPanelProps> = ({
await api.checkSponsor(job.id); await api.checkSponsor(job.id);
await onJobUpdated(); await onJobUpdated();
}} }}
showSponsorInfo={showSponsorInfo}
/> />
) : ( ) : (
<TailorMode <TailorMode

View File

@ -122,7 +122,9 @@ export const OrchestratorPage: React.FC = () => {
}; };
const { pipelineSources, setPipelineSources, toggleSource } = usePipelineSources(); const { pipelineSources, setPipelineSources, toggleSource } = usePipelineSources();
const { jobs, stats, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs } = useOrchestratorData(); const { jobs, stats, settings, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs } = useOrchestratorData();
const showSponsorInfo = settings?.showSponsorInfo ?? true;
const activeJobs = useFilteredJobs(jobs, activeTab, sourceFilter, searchQuery, sort); const activeJobs = useFilteredJobs(jobs, activeTab, sourceFilter, searchQuery, sort);
const counts = useMemo(() => getJobCounts(jobs), [jobs]); const counts = useMemo(() => getJobCounts(jobs), [jobs]);
@ -276,6 +278,7 @@ export const OrchestratorPage: React.FC = () => {
onSelectJobId={handleSelectJobId} onSelectJobId={handleSelectJobId}
onJobUpdated={loadJobs} onJobUpdated={loadJobs}
onSetActiveTab={setActiveTab} onSetActiveTab={setActiveTab}
showSponsorInfo={showSponsorInfo}
/> />
</div> </div>
)} )}

View File

@ -41,6 +41,7 @@ interface JobDetailPanelProps {
onSelectJobId: (jobId: string | null) => void; onSelectJobId: (jobId: string | null) => void;
onJobUpdated: () => Promise<void>; onJobUpdated: () => Promise<void>;
onSetActiveTab: (tab: FilterTab) => void; onSetActiveTab: (tab: FilterTab) => void;
showSponsorInfo?: boolean;
} }
export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
@ -50,6 +51,7 @@ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
onSelectJobId, onSelectJobId,
onJobUpdated, onJobUpdated,
onSetActiveTab, onSetActiveTab,
showSponsorInfo,
}) => { }) => {
const [detailTab, setDetailTab] = useState<"overview" | "tailoring" | "description">("overview"); const [detailTab, setDetailTab] = useState<"overview" | "tailoring" | "description">("overview");
const [isEditingDescription, setIsEditingDescription] = useState(false); const [isEditingDescription, setIsEditingDescription] = useState(false);
@ -233,6 +235,7 @@ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
job={selectedJob} job={selectedJob}
onJobUpdated={onJobUpdated} onJobUpdated={onJobUpdated}
onJobMoved={handleJobMoved} onJobMoved={handleJobMoved}
showSponsorInfo={showSponsorInfo}
/> />
); );
} }
@ -254,6 +257,7 @@ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
setIsEditingDescription(true); setIsEditingDescription(true);
}, 50); }, 50);
}} }}
showSponsorInfo={showSponsorInfo}
/> />
); );
} }
@ -275,6 +279,7 @@ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
await api.checkSponsor(selectedJob.id); await api.checkSponsor(selectedJob.id);
await onJobUpdated(); await onJobUpdated();
}} }}
showSponsorInfo={showSponsorInfo}
/> />
<div className="flex flex-wrap items-center gap-1.5"> <div className="flex flex-wrap items-center gap-1.5">

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import type { Job, JobStatus } from "../../../shared/types"; import type { Job, JobStatus, AppSettings } from "../../../shared/types";
import * as api from "../../api"; import * as api from "../../api";
const initialStats: Record<JobStatus, number> = { const initialStats: Record<JobStatus, number> = {
@ -16,15 +16,20 @@ const initialStats: Record<JobStatus, number> = {
export const useOrchestratorData = () => { export const useOrchestratorData = () => {
const [jobs, setJobs] = useState<Job[]>([]); const [jobs, setJobs] = useState<Job[]>([]);
const [stats, setStats] = useState<Record<JobStatus, number>>(initialStats); const [stats, setStats] = useState<Record<JobStatus, number>>(initialStats);
const [settings, setSettings] = useState<AppSettings | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isPipelineRunning, setIsPipelineRunning] = useState(false); const [isPipelineRunning, setIsPipelineRunning] = useState(false);
const loadJobs = useCallback(async () => { const loadJobs = useCallback(async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const data = await api.getJobs(); const [jobsData, settingsData] = await Promise.all([
setJobs(data.jobs); api.getJobs(),
setStats(data.byStatus); api.getSettings(),
]);
setJobs(jobsData.jobs);
setStats(jobsData.byStatus);
setSettings(settingsData);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Failed to load jobs"; const message = error instanceof Error ? error.message : "Failed to load jobs";
toast.error(message); toast.error(message);
@ -54,5 +59,5 @@ export const useOrchestratorData = () => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [loadJobs, checkPipelineStatus]); }, [loadJobs, checkPipelineStatus]);
return { jobs, stats, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs, checkPipelineStatus }; return { jobs, stats, settings, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs, checkPipelineStatus };
}; };