initial commit

This commit is contained in:
DaKheera47
2026-01-25 11:50:04 +00:00
parent e83cff7a12
commit 35365b5fca
9 changed files with 421 additions and 3 deletions
+6
View File
@@ -85,6 +85,12 @@ export async function processJob(id: string, options?: { force?: boolean }): Pro
});
}
export async function rescoreJob(id: string): Promise<Job> {
return fetchApi<Job>(`/jobs/${id}/rescore`, {
method: 'POST',
});
}
export async function summarizeJob(id: string, options?: { force?: boolean }): Promise<Job> {
const query = options?.force ? '?force=1' : '';
return fetchApi<Job>(`/jobs/${id}/summarize${query}`, {
@@ -0,0 +1,140 @@
import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { ReadyPanel } from "./ReadyPanel";
import type { Job } from "../../shared/types";
import * as api from "../api";
vi.mock("@/components/ui/dropdown-menu", () => {
return {
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div role="menu">{children}</div>,
DropdownMenuItem: ({
children,
onSelect,
...props
}: {
children: React.ReactNode;
onSelect?: () => void;
}) => (
<button type="button" role="menuitem" onClick={() => onSelect?.()} {...props}>
{children}
</button>
),
DropdownMenuSeparator: () => <div role="separator" />,
};
});
vi.mock("../hooks/useProfile", () => ({
useProfile: () => ({ personName: "Test" }),
}));
vi.mock("../hooks/useSettings", () => ({
useSettings: () => ({ showSponsorInfo: false }),
}));
vi.mock("../api", () => ({
rescoreJob: vi.fn(),
getResumeProjectsCatalog: vi.fn().mockResolvedValue([]),
markAsApplied: vi.fn(),
generateJobPdf: vi.fn(),
checkSponsor: vi.fn(),
skipJob: vi.fn(),
updateJob: vi.fn(),
}));
vi.mock("sonner", () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
message: vi.fn(),
},
}));
const createJob = (overrides: Partial<Job> = {}): Job => ({
id: "job-1",
source: "linkedin",
sourceJobId: null,
jobUrlDirect: null,
datePosted: null,
title: "Backend Engineer",
employer: "Acme",
employerUrl: null,
jobUrl: "https://example.com/job",
applicationLink: "https://example.com/apply",
disciplines: null,
deadline: "2025-02-01",
salary: "GBP 50k",
location: "London",
degreeRequired: null,
starting: null,
jobDescription: "Build APIs",
status: "ready",
suitabilityScore: 82,
suitabilityReason: "Strong fit",
tailoredSummary: null,
tailoredHeadline: null,
tailoredSkills: null,
selectedProjectIds: null,
pdfPath: null,
notionPageId: null,
sponsorMatchScore: null,
sponsorMatchNames: null,
jobType: null,
salarySource: null,
salaryInterval: null,
salaryMinAmount: null,
salaryMaxAmount: null,
salaryCurrency: null,
isRemote: null,
jobLevel: null,
jobFunction: null,
listingType: null,
emails: null,
companyIndustry: null,
companyLogo: null,
companyUrlDirect: null,
companyAddresses: null,
companyNumEmployees: null,
companyRevenue: null,
companyDescription: null,
skills: null,
experienceRange: null,
companyRating: null,
companyReviewsCount: null,
vacancyCount: null,
workFromHomeType: null,
discoveredAt: "2025-01-01T00:00:00Z",
processedAt: null,
appliedAt: null,
createdAt: "2025-01-01T00:00:00Z",
updatedAt: "2025-01-02T00:00:00Z",
...overrides,
});
describe("ReadyPanel", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("re-runs the fit assessment from the menu", async () => {
const onJobUpdated = vi.fn().mockResolvedValue(undefined);
const job = createJob();
vi.mocked(api.rescoreJob).mockResolvedValue(job as Job);
render(
<ReadyPanel
job={job}
onJobUpdated={onJobUpdated}
onJobMoved={vi.fn()}
/>
);
fireEvent.click(screen.getByRole("menuitem", { name: /re-run fit assessment/i }));
await waitFor(() => expect(api.rescoreJob).toHaveBeenCalledWith("job-1"));
expect(onJobUpdated).toHaveBeenCalled();
});
});
@@ -67,6 +67,7 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
const [mode, setMode] = useState<PanelMode>("ready");
const [isMarkingApplied, setIsMarkingApplied] = useState(false);
const [isRegenerating, setIsRegenerating] = useState(false);
const [isRescoring, setIsRescoring] = useState(false);
const [catalog, setCatalog] = useState<ResumeProjectCatalogItem[]>([]);
const [recentlyApplied, setRecentlyApplied] = useState<{
jobId: string;
@@ -181,6 +182,22 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
}
}, [job, onJobUpdated]);
const handleRescore = useCallback(async () => {
if (!job) return;
try {
setIsRescoring(true);
await api.rescoreJob(job.id);
toast.success("Fit assessment updated");
await onJobUpdated();
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to re-run fit assessment";
toast.error(message);
} finally {
setIsRescoring(false);
}
}, [job, onJobUpdated]);
const handleSkip = useCallback(async () => {
if (!job) return;
@@ -385,6 +402,14 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
{isRegenerating ? "Regenerating..." : "Regenerate PDF"}
</DropdownMenuItem>
<DropdownMenuItem
onSelect={handleRescore}
disabled={isRescoring}
>
<RefreshCcw className={cn("mr-2 h-4 w-4", isRescoring && "animate-spin")} />
{isRescoring ? "Re-scoring..." : "Re-run fit assessment"}
</DropdownMenuItem>
<DropdownMenuSeparator />
{/* Utility actions */}
@@ -1,7 +1,13 @@
import React, { useMemo, useState } from "react";
import { ExternalLink, Loader2, Sparkles, XCircle } from "lucide-react";
import { ChevronUp, ExternalLink, Loader2, RefreshCcw, Sparkles, XCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { FitAssessment, JobHeader, TailoredSummary } from "..";
@@ -14,6 +20,8 @@ interface DecideModeProps {
onTailor: () => void;
onSkip: () => void;
isSkipping: boolean;
onRescore: () => void;
isRescoring: boolean;
onCheckSponsor?: () => Promise<void>;
}
@@ -22,6 +30,8 @@ export const DecideMode: React.FC<DecideModeProps> = ({
onTailor,
onSkip,
isSkipping,
onRescore,
isRescoring,
onCheckSponsor,
}) => {
const [showDescription, setShowDescription] = useState(false);
@@ -87,7 +97,26 @@ export const DecideMode: React.FC<DecideModeProps> = ({
<Separator className='opacity-40' />
<div className='pt-6 pb-2'>
<div className='pt-4 pb-2 space-y-4'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="w-full h-8 gap-2 text-xs text-muted-foreground hover:text-foreground justify-center"
>
More actions
<ChevronUp className="h-3 w-3 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center" className="w-56">
<DropdownMenuItem onSelect={onRescore} disabled={isRescoring}>
<RefreshCcw className={isRescoring ? "mr-2 h-4 w-4 animate-spin" : "mr-2 h-4 w-4"} />
{isRescoring ? "Re-scoring..." : "Re-run fit assessment"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{jobLink ? (
<div className='flex justify-center'>
<a
@@ -105,4 +134,3 @@ export const DecideMode: React.FC<DecideModeProps> = ({
</div>
);
};
@@ -0,0 +1,133 @@
import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { DiscoveredPanel } from "./DiscoveredPanel";
import type { Job } from "../../../shared/types";
import * as api from "../../api";
vi.mock("@/components/ui/dropdown-menu", () => {
return {
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div role="menu">{children}</div>,
DropdownMenuItem: ({
children,
onSelect,
...props
}: {
children: React.ReactNode;
onSelect?: () => void;
}) => (
<button type="button" role="menuitem" onClick={() => onSelect?.()} {...props}>
{children}
</button>
),
DropdownMenuSeparator: () => <div role="separator" />,
};
});
vi.mock("../../hooks/useSettings", () => ({
useSettings: () => ({ showSponsorInfo: false }),
}));
vi.mock("../../api", () => ({
rescoreJob: vi.fn(),
skipJob: vi.fn(),
processJob: vi.fn(),
checkSponsor: vi.fn(),
}));
vi.mock("sonner", () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
message: vi.fn(),
},
}));
const createJob = (overrides: Partial<Job> = {}): Job => ({
id: "job-2",
source: "linkedin",
sourceJobId: null,
jobUrlDirect: null,
datePosted: null,
title: "Backend Engineer",
employer: "Acme",
employerUrl: null,
jobUrl: "https://example.com/job",
applicationLink: "https://example.com/apply",
disciplines: null,
deadline: null,
salary: null,
location: "London",
degreeRequired: null,
starting: null,
jobDescription: "Build APIs",
status: "discovered",
suitabilityScore: 55,
suitabilityReason: "Ok fit",
tailoredSummary: null,
tailoredHeadline: null,
tailoredSkills: null,
selectedProjectIds: null,
pdfPath: null,
notionPageId: null,
sponsorMatchScore: null,
sponsorMatchNames: null,
jobType: null,
salarySource: null,
salaryInterval: null,
salaryMinAmount: null,
salaryMaxAmount: null,
salaryCurrency: null,
isRemote: null,
jobLevel: null,
jobFunction: null,
listingType: null,
emails: null,
companyIndustry: null,
companyLogo: null,
companyUrlDirect: null,
companyAddresses: null,
companyNumEmployees: null,
companyRevenue: null,
companyDescription: null,
skills: null,
experienceRange: null,
companyRating: null,
companyReviewsCount: null,
vacancyCount: null,
workFromHomeType: null,
discoveredAt: "2025-01-01T00:00:00Z",
processedAt: null,
appliedAt: null,
createdAt: "2025-01-01T00:00:00Z",
updatedAt: "2025-01-02T00:00:00Z",
...overrides,
});
describe("DiscoveredPanel", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("re-runs the fit assessment from the menu", async () => {
const onJobUpdated = vi.fn().mockResolvedValue(undefined);
const job = createJob();
vi.mocked(api.rescoreJob).mockResolvedValue(job as Job);
render(
<DiscoveredPanel
job={job}
onJobUpdated={onJobUpdated}
onJobMoved={vi.fn()}
/>
);
fireEvent.click(screen.getByRole("menuitem", { name: /re-run fit assessment/i }));
await waitFor(() => expect(api.rescoreJob).toHaveBeenCalledWith("job-2"));
expect(onJobUpdated).toHaveBeenCalled();
});
});
@@ -24,11 +24,13 @@ export const DiscoveredPanel: React.FC<DiscoveredPanelProps> = ({
const [mode, setMode] = useState<PanelMode>("decide");
const [isSkipping, setIsSkipping] = useState(false);
const [isFinalizing, setIsFinalizing] = useState(false);
const [isRescoring, setIsRescoring] = useState(false);
useEffect(() => {
setMode("decide");
setIsSkipping(false);
setIsFinalizing(false);
setIsRescoring(false);
}, [job?.id]);
const handleSkip = async () => {
@@ -69,6 +71,22 @@ export const DiscoveredPanel: React.FC<DiscoveredPanelProps> = ({
}
};
const handleRescore = async () => {
if (!job) return;
try {
setIsRescoring(true);
await api.rescoreJob(job.id);
toast.success("Fit assessment updated");
await onJobUpdated();
} catch (error) {
const message =
error instanceof Error ? error.message : "Failed to re-run fit assessment";
toast.error(message);
} finally {
setIsRescoring(false);
}
};
if (!job) {
return <EmptyState />;
}
@@ -85,6 +103,8 @@ export const DiscoveredPanel: React.FC<DiscoveredPanelProps> = ({
onTailor={() => setMode("tailor")}
onSkip={handleSkip}
isSkipping={isSkipping}
onRescore={handleRescore}
isRescoring={isRescoring}
onCheckSponsor={async () => {
await api.checkSponsor(job.id);
await onJobUpdated();