pdf generation fix
This commit is contained in:
parent
9905353a49
commit
302fadb494
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -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,24 +45,49 @@ 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)' }}>
|
||||||
{tabs.map(tab => {
|
<div style={{ display: 'flex', gap: 'var(--space-2)', flex: 1 }}>
|
||||||
const count = tab.statuses.length === 0
|
{tabs.map(tab => {
|
||||||
? jobs.length
|
const count = tab.statuses.length === 0
|
||||||
: jobs.filter(j => tab.statuses.includes(j.status)).length;
|
? jobs.length
|
||||||
|
: jobs.filter(j => tab.statuses.includes(j.status)).length;
|
||||||
return (
|
|
||||||
<button
|
return (
|
||||||
key={tab.id}
|
<button
|
||||||
className={`tab ${activeTab === tab.id ? 'active' : ''}`}
|
key={tab.id}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
className={`tab ${activeTab === tab.id ? 'active' : ''}`}
|
||||||
>
|
onClick={() => setActiveTab(tab.id)}
|
||||||
{tab.label} ({count})
|
>
|
||||||
</button>
|
{tab.label} ({count})
|
||||||
);
|
</button>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
|
</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>
|
</div>
|
||||||
|
|
||||||
{filteredJobs.length === 0 ? (
|
{filteredJobs.length === 0 ? (
|
||||||
|
|||||||
@ -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
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
8
resume-generator/.gitignore
vendored
8
resume-generator/.gitignore
vendored
@ -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/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user