pdf generation fix

This commit is contained in:
DaKheera47 2025-12-11 23:15:20 +00:00
parent 9905353a49
commit 302fadb494
8 changed files with 149 additions and 23 deletions

View File

@ -21,6 +21,7 @@ export const App: React.FC = () => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isPipelineRunning, setIsPipelineRunning] = useState(false); const [isPipelineRunning, setIsPipelineRunning] = useState(false);
const [processingJobId, setProcessingJobId] = useState<string | null>(null); const [processingJobId, setProcessingJobId] = useState<string | null>(null);
const [isProcessingAll, setIsProcessingAll] = useState(false);
const [toasts, setToasts] = useState<Toast[]>([]); const [toasts, setToasts] = useState<Toast[]>([]);
// Toast helpers // Toast helpers
@ -147,6 +148,31 @@ export const App: React.FC = () => {
} }
}; };
// Process all discovered jobs
const handleProcessAll = async () => {
try {
setIsProcessingAll(true);
const result = await api.processAllDiscovered();
addToast(`Processing ${result.count} jobs in background...`, 'info');
// Poll for completion
const pollInterval = setInterval(async () => {
await loadJobs();
const currentStats = await api.getJobs();
const stillDiscovered = currentStats.byStatus.discovered + currentStats.byStatus.processing;
if (stillDiscovered === 0) {
clearInterval(pollInterval);
setIsProcessingAll(false);
addToast('All jobs processed!', 'success');
}
}, 3000);
} catch (error) {
setIsProcessingAll(false);
const message = error instanceof Error ? error.message : 'Failed to process jobs';
addToast(message, 'error');
}
};
return ( return (
<> <>
<Header <Header
@ -167,7 +193,9 @@ export const App: React.FC = () => {
onApply={handleApply} onApply={handleApply}
onReject={handleReject} onReject={handleReject}
onProcess={handleProcess} onProcess={handleProcess}
onProcessAll={handleProcessAll}
processingJobId={processingJobId} processingJobId={processingJobId}
isProcessingAll={isProcessingAll}
/> />
</main> </main>

View File

@ -104,3 +104,16 @@ export async function clearDatabase(): Promise<{
method: 'DELETE', method: 'DELETE',
}); });
} }
// Bulk operations
export async function processAllDiscovered(): Promise<{
message: string;
count: number;
}> {
return fetchApi<{
message: string;
count: number;
}>('/jobs/process-discovered', {
method: 'POST',
});
}

View File

@ -114,15 +114,28 @@ export const JobCard: React.FC<JobCardProps> = ({
View Job View Job
</a> </a>
{/* View PDF in browser */}
{hasPdf && (
<a
href={`/pdfs/resume_${job.id}.pdf`}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost"
>
<ExternalLinkIcon size={16} />
View PDF
</a>
)}
{/* Download PDF */} {/* Download PDF */}
{hasPdf && ( {hasPdf && (
<a <a
href={`/pdfs/resume_${job.id}.pdf`} href={`/pdfs/resume_${job.id}.pdf`}
download download={`resume_${job.employer.replace(/[^a-z0-9]/gi, '_')}_${job.title.replace(/[^a-z0-9]/gi, '_')}.pdf`}
className="btn btn-ghost" className="btn btn-ghost"
> >
<DownloadIcon size={16} /> <DownloadIcon size={16} />
Download PDF Download
</a> </a>
)} )}

View File

@ -5,13 +5,16 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import type { Job, JobStatus } from '../../shared/types'; import type { Job, JobStatus } from '../../shared/types';
import { JobCard } from './JobCard'; import { JobCard } from './JobCard';
import { RefreshIcon } from './Icons';
interface JobListProps { interface JobListProps {
jobs: Job[]; jobs: Job[];
onApply: (id: string) => void; onApply: (id: string) => void;
onReject: (id: string) => void; onReject: (id: string) => void;
onProcess: (id: string) => void; onProcess: (id: string) => void;
onProcessAll: () => void;
processingJobId: string | null; processingJobId: string | null;
isProcessingAll: boolean;
} }
type FilterTab = 'ready' | 'discovered' | 'applied' | 'all'; type FilterTab = 'ready' | 'discovered' | 'applied' | 'all';
@ -28,7 +31,9 @@ export const JobList: React.FC<JobListProps> = ({
onApply, onApply,
onReject, onReject,
onProcess, onProcess,
onProcessAll,
processingJobId, processingJobId,
isProcessingAll,
}) => { }) => {
const [activeTab, setActiveTab] = useState<FilterTab>('ready'); const [activeTab, setActiveTab] = useState<FilterTab>('ready');
@ -40,9 +45,12 @@ export const JobList: React.FC<JobListProps> = ({
return jobs.filter(job => tab.statuses.includes(job.status)); return jobs.filter(job => tab.statuses.includes(job.status));
}, [jobs, activeTab]); }, [jobs, activeTab]);
const discoveredCount = jobs.filter(j => j.status === 'discovered').length;
return ( return (
<div> <div>
<div className="tabs"> <div className="tabs" style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)' }}>
<div style={{ display: 'flex', gap: 'var(--space-2)', flex: 1 }}>
{tabs.map(tab => { {tabs.map(tab => {
const count = tab.statuses.length === 0 const count = tab.statuses.length === 0
? jobs.length ? jobs.length
@ -60,6 +68,28 @@ export const JobList: React.FC<JobListProps> = ({
})} })}
</div> </div>
{activeTab === 'discovered' && discoveredCount > 0 && (
<button
className="btn btn-primary"
onClick={onProcessAll}
disabled={isProcessingAll}
style={{ marginLeft: 'auto' }}
>
{isProcessingAll ? (
<>
<div className="spinner" />
Processing...
</>
) : (
<>
<RefreshIcon size={16} />
Process All ({discoveredCount})
</>
)}
</button>
)}
</div>
{filteredJobs.length === 0 ? ( {filteredJobs.length === 0 ? (
<div className="empty-state"> <div className="empty-state">
<div className="empty-state-icon">📭</div> <div className="empty-state-icon">📭</div>

View File

@ -170,6 +170,39 @@ apiRouter.post('/jobs/:id/reject', async (req: Request, res: Response) => {
} }
}); });
/**
* POST /api/jobs/process-discovered - Process all discovered jobs (generate PDFs)
*/
apiRouter.post('/jobs/process-discovered', async (req: Request, res: Response) => {
try {
const discoveredJobs = await jobsRepo.getAllJobs(['discovered']);
// Process each job in background
const processInBackground = async () => {
for (const job of discoveredJobs.filter(j => j.status === 'discovered')) {
try {
await processJob(job.id);
} catch (error) {
console.error(`Failed to process job ${job.id}:`, error);
}
}
};
processInBackground().catch(console.error);
res.json({
success: true,
data: {
message: `Processing ${discoveredJobs.length} jobs`,
count: discoveredJobs.length,
}
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ success: false, error: message });
}
});
// ============================================================================ // ============================================================================
// Pipeline API // Pipeline API
// ============================================================================ // ============================================================================

View File

@ -97,11 +97,10 @@ async function runPythonPdfGenerator(
outputFilename: string outputFilename: string
): Promise<void> { ): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Note: This calls the Python script with the JSON path // Use the virtual environment's Python
// The Python script needs to be modified to accept these args const pythonPath = join(RESUME_GEN_DIR, '.venv', 'bin', 'python');
// For now, we'll use environment variables
const child = spawn('python3', ['rxresume_automation.py'], { const child = spawn(pythonPath, ['rxresume_automation.py'], {
cwd: RESUME_GEN_DIR, cwd: RESUME_GEN_DIR,
env: { env: {
...process.env, ...process.env,

View File

@ -18,6 +18,10 @@ export default defineConfig({
target: 'http://localhost:3001', target: 'http://localhost:3001',
changeOrigin: true, changeOrigin: true,
}, },
'/pdfs': {
target: 'http://localhost:3001',
changeOrigin: true,
},
}, },
}, },
build: { build: {

View File

@ -1,2 +1,8 @@
# any json files that start with "temp_" # Temp JSON files (used by orchestrator)
temp_*.json temp_*.json
# Python virtual environment
.venv/
# Generated resumes
resumes/