can regenerate pdfs, because sometimes the ai gets it wrong, and we need to help it out
This commit is contained in:
parent
deb30efa44
commit
8a236c0be9
@ -116,8 +116,10 @@ export const App: React.FC = () => {
|
|||||||
const handleProcess = async (jobId: string) => {
|
const handleProcess = async (jobId: string) => {
|
||||||
try {
|
try {
|
||||||
setProcessingJobId(jobId);
|
setProcessingJobId(jobId);
|
||||||
await api.processJob(jobId);
|
const job = jobs.find((item) => item.id === jobId);
|
||||||
toast.success("Resume generated successfully");
|
const force = job?.status === "ready";
|
||||||
|
await api.processJob(jobId, { force });
|
||||||
|
toast.success(force ? "Resume regenerated successfully" : "Resume generated successfully");
|
||||||
await loadJobs();
|
await loadJobs();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to process job";
|
const message = error instanceof Error ? error.message : "Failed to process job";
|
||||||
|
|||||||
@ -56,8 +56,9 @@ export async function updateJob(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processJob(id: string): Promise<Job> {
|
export async function processJob(id: string, options?: { force?: boolean }): Promise<Job> {
|
||||||
return fetchApi<Job>(`/jobs/${id}/process`, {
|
const query = options?.force ? '?force=1' : '';
|
||||||
|
return fetchApi<Job>(`/jobs/${id}/process${query}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export const JobCard: React.FC<JobCardProps> = ({
|
|||||||
|
|
||||||
const hasPdf = !!job.pdfPath;
|
const hasPdf = !!job.pdfPath;
|
||||||
const canApply = job.status === "ready";
|
const canApply = job.status === "ready";
|
||||||
const canProcess = job.status === "discovered";
|
const canProcess = ["discovered", "ready"].includes(job.status);
|
||||||
const canReject = ["discovered", "ready"].includes(job.status);
|
const canReject = ["discovered", "ready"].includes(job.status);
|
||||||
|
|
||||||
const jobLink = job.applicationLink || job.jobUrl;
|
const jobLink = job.applicationLink || job.jobUrl;
|
||||||
@ -200,7 +200,7 @@ export const JobCard: React.FC<JobCardProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<RefreshCcw className="mr-2 h-4 w-4" />
|
<RefreshCcw className="mr-2 h-4 w-4" />
|
||||||
Generate Resume
|
{job.status === "ready" ? "Regenerate PDF" : "Generate Resume"}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -215,7 +215,7 @@ export const JobTable: React.FC<JobTableProps> = ({
|
|||||||
const pdfHref = `/pdfs/resume_${job.id}.pdf`;
|
const pdfHref = `/pdfs/resume_${job.id}.pdf`;
|
||||||
|
|
||||||
const canApply = job.status === "ready";
|
const canApply = job.status === "ready";
|
||||||
const canProcess = job.status === "discovered";
|
const canProcess = ["discovered", "ready"].includes(job.status);
|
||||||
const canReject = ["discovered", "ready"].includes(job.status);
|
const canReject = ["discovered", "ready"].includes(job.status);
|
||||||
const isProcessing = processingJobId === job.id;
|
const isProcessing = processingJobId === job.id;
|
||||||
const isSelected = selectedJobIds.has(job.id);
|
const isSelected = selectedJobIds.has(job.id);
|
||||||
@ -240,7 +240,7 @@ export const JobTable: React.FC<JobTableProps> = ({
|
|||||||
asChild
|
asChild
|
||||||
variant="link"
|
variant="link"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-auto justify-start p-0 text-left leading-snug whitespace-normal break-words"
|
className="h-auto justify-start p-0 text-left leading-snug whitespace-normal wrap-break-word"
|
||||||
>
|
>
|
||||||
<a href={jobLink} target="_blank" rel="noopener noreferrer">
|
<a href={jobLink} target="_blank" rel="noopener noreferrer">
|
||||||
{job.title}
|
{job.title}
|
||||||
@ -248,7 +248,7 @@ export const JobTable: React.FC<JobTableProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="align-middle whitespace-normal break-words">
|
<TableCell className="align-middle whitespace-normal wrap-break-word">
|
||||||
{job.employer}
|
{job.employer}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
@ -258,7 +258,7 @@ export const JobTable: React.FC<JobTableProps> = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="align-middle whitespace-normal break-words text-muted-foreground">
|
<TableCell className="align-middle whitespace-normal wrap-break-word text-muted-foreground">
|
||||||
{job.location || "—"}
|
{job.location || "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
@ -331,7 +331,11 @@ export const JobTable: React.FC<JobTableProps> = ({
|
|||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
>
|
>
|
||||||
<RefreshCcw className="mr-2 h-4 w-4" />
|
<RefreshCcw className="mr-2 h-4 w-4" />
|
||||||
{isProcessing ? "Processing..." : "Generate Resume"}
|
{isProcessing
|
||||||
|
? "Processing..."
|
||||||
|
: job.status === "ready"
|
||||||
|
? "Regenerate PDF"
|
||||||
|
: "Generate Resume"}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -133,7 +133,10 @@ apiRouter.patch('/jobs/:id', async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
apiRouter.post('/jobs/:id/process', async (req: Request, res: Response) => {
|
apiRouter.post('/jobs/:id/process', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const result = await processJob(req.params.id);
|
const forceRaw = req.query.force as string | undefined;
|
||||||
|
const force = forceRaw === '1' || forceRaw === 'true';
|
||||||
|
|
||||||
|
const result = await processJob(req.params.id, { force });
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return res.status(400).json({ success: false, error: result.error });
|
return res.status(400).json({ success: false, error: result.error });
|
||||||
|
|||||||
@ -279,7 +279,10 @@ export async function runPipeline(config: Partial<PipelineConfig> = {}): Promise
|
|||||||
/**
|
/**
|
||||||
* Process a single job (for manual processing).
|
* Process a single job (for manual processing).
|
||||||
*/
|
*/
|
||||||
export async function processJob(jobId: string): Promise<{
|
export async function processJob(
|
||||||
|
jobId: string,
|
||||||
|
options?: { force?: boolean }
|
||||||
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
@ -290,14 +293,31 @@ export async function processJob(jobId: string): Promise<{
|
|||||||
if (!job) {
|
if (!job) {
|
||||||
return { success: false, error: 'Job not found' };
|
return { success: false, error: 'Job not found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (job.status !== 'discovered' && job.status !== 'ready') {
|
||||||
|
return { success: false, error: `Job cannot be processed from status: ${job.status}` };
|
||||||
|
}
|
||||||
|
|
||||||
const profile = await loadProfile(DEFAULT_PROFILE_PATH);
|
const profile = await loadProfile(DEFAULT_PROFILE_PATH);
|
||||||
|
|
||||||
// Mark as processing
|
// Mark as processing
|
||||||
await jobsRepo.updateJob(job.id, { status: 'processing' });
|
await jobsRepo.updateJob(job.id, { status: 'processing' });
|
||||||
|
|
||||||
|
// Re-score job suitability (AI)
|
||||||
|
// If forcing, always recompute; otherwise compute if missing.
|
||||||
|
if (options?.force || job.suitabilityScore == null || !job.suitabilityReason) {
|
||||||
|
const suitability = await scoreJobSuitability(job, profile);
|
||||||
|
await jobsRepo.updateJob(job.id, {
|
||||||
|
suitabilityScore: suitability.score,
|
||||||
|
suitabilityReason: suitability.reason,
|
||||||
|
});
|
||||||
|
job.suitabilityScore = suitability.score;
|
||||||
|
job.suitabilityReason = suitability.reason;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate summary if not already done
|
// Generate summary (AI)
|
||||||
if (!job.tailoredSummary) {
|
// If forcing, always recompute; otherwise compute if missing.
|
||||||
|
if (options?.force || !job.tailoredSummary) {
|
||||||
console.log(' Generating summary...');
|
console.log(' Generating summary...');
|
||||||
const summaryResult = await generateSummary(
|
const summaryResult = await generateSummary(
|
||||||
job.jobDescription || '',
|
job.jobDescription || '',
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
import { readFile, writeFile, mkdir, access, unlink } from 'fs/promises';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
import { getSetting } from '../repositories/settings.js';
|
import { getSetting } from '../repositories/settings.js';
|
||||||
@ -101,6 +101,13 @@ export async function generatePdf(
|
|||||||
// Generate PDF using Python script - output directly to our data folder
|
// Generate PDF using Python script - output directly to our data folder
|
||||||
const outputFilename = `resume_${jobId}.pdf`;
|
const outputFilename = `resume_${jobId}.pdf`;
|
||||||
const outputPath = join(OUTPUT_DIR, outputFilename);
|
const outputPath = join(OUTPUT_DIR, outputFilename);
|
||||||
|
|
||||||
|
// Ensure regeneration overwrites the old file if it exists.
|
||||||
|
try {
|
||||||
|
await unlink(outputPath);
|
||||||
|
} catch {
|
||||||
|
// Ignore if it doesn't exist or cannot be removed.
|
||||||
|
}
|
||||||
|
|
||||||
await runPythonPdfGenerator(tempResumePath, outputFilename, OUTPUT_DIR);
|
await runPythonPdfGenerator(tempResumePath, outputFilename, OUTPUT_DIR);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user