allowing tailoring of resume before being manually built

This commit is contained in:
DaKheera47
2026-01-06 23:06:19 +00:00
parent 4e17371990
commit cd5ec25c3b
12 changed files with 417 additions and 68 deletions
+1
View File
@@ -174,6 +174,7 @@ export const App: React.FC = () => {
onApply={handleApply}
onReject={handleReject}
onProcess={handleProcess}
onUpdate={loadJobs}
processingJobId={processingJobId}
/>
</main>
+19 -1
View File
@@ -63,6 +63,19 @@ export async function processJob(id: string, options?: { force?: boolean }): Pro
});
}
export async function summarizeJob(id: string, options?: { force?: boolean }): Promise<Job> {
const query = options?.force ? '?force=1' : '';
return fetchApi<Job>(`/jobs/${id}/summarize${query}`, {
method: 'POST',
});
}
export async function generateJobPdf(id: string): Promise<Job> {
return fetchApi<Job>(`/jobs/${id}/generate-pdf`, {
method: 'POST',
});
}
export async function markAsApplied(id: string): Promise<Job> {
return fetchApi<Job>(`/jobs/${id}/apply`, {
method: 'POST',
@@ -95,11 +108,16 @@ export async function runPipeline(config?: {
});
}
// Settings API
// Settings & Profile API
export async function getSettings(): Promise<AppSettings> {
return fetchApi<AppSettings>('/settings');
}
export async function getProfileProjects(): Promise<ResumeProjectCatalogItem[]> {
return fetchApi<ResumeProjectCatalogItem[]>('/profile/projects');
}
export async function updateSettings(update: {
model?: string | null
pipelineWebhookUrl?: string | null
@@ -26,12 +26,14 @@ import { cn } from "@/lib/utils";
import type { Job, JobStatus, JobSource } from "../../shared/types";
import { JobCard } from "./JobCard";
import { JobTable, type JobSort } from "./JobTable";
import { TailoringEditor } from "./TailoringEditor";
interface JobListProps {
jobs: Job[];
onApply: (id: string) => void | Promise<void>;
onReject: (id: string) => void | Promise<void>;
onProcess: (id: string) => void | Promise<void>;
onUpdate: () => void | Promise<void>;
processingJobId: string | null;
}
@@ -186,6 +188,7 @@ export const JobList: React.FC<JobListProps> = ({
onApply,
onReject,
onProcess,
onUpdate,
processingJobId,
}) => {
const [activeTab, setActiveTab] = useState<FilterTab>("ready");
@@ -408,6 +411,8 @@ export const JobList: React.FC<JobListProps> = ({
onHighlightChange={setHighlightedJobId}
/>
<TailoringEditor job={highlightedJob} onUpdate={onUpdate} />
<Card>
<CardHeader className="space-y-1">
<CardTitle className="text-base">Job description</CardTitle>
@@ -0,0 +1,181 @@
import React, { useEffect, useState } from "react";
import { Check, Loader2, Sparkles, FileText, AlertTriangle } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import * as api from "../api";
import type { Job, ResumeProjectCatalogItem } from "../../shared/types";
interface TailoringEditorProps {
job: Job;
onUpdate: () => void | Promise<void>;
}
export const TailoringEditor: React.FC<TailoringEditorProps> = ({ job, onUpdate }) => {
const [catalog, setCatalog] = useState<ResumeProjectCatalogItem[]>([]);
const [summary, setSummary] = useState(job.tailoredSummary || "");
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [isSummarizing, setIsSummarizing] = useState(false);
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
// Load project catalog
api.getProfileProjects().then(setCatalog).catch(console.error);
// Set initial selection
if (job.selectedProjectIds) {
setSelectedIds(new Set(job.selectedProjectIds.split(',').filter(Boolean)));
}
}, [job.selectedProjectIds]);
useEffect(() => {
setSummary(job.tailoredSummary || "");
}, [job.tailoredSummary]);
const handleToggleProject = (id: string) => {
const next = new Set(selectedIds);
if (next.has(id)) next.delete(id);
else next.add(id);
setSelectedIds(next);
};
const handleSave = async () => {
try {
setIsSaving(true);
await api.updateJob(job.id, {
tailoredSummary: summary,
selectedProjectIds: Array.from(selectedIds).join(','),
});
toast.success("Changes saved");
await onUpdate();
} catch (error) {
toast.error("Failed to save changes");
} finally {
setIsSaving(false);
}
};
const handleSummarize = async () => {
try {
setIsSummarizing(true);
const updatedJob = await api.summarizeJob(job.id, { force: true });
setSummary(updatedJob.tailoredSummary || "");
if (updatedJob.selectedProjectIds) {
setSelectedIds(new Set(updatedJob.selectedProjectIds.split(',').filter(Boolean)));
}
toast.success("AI Summary & Projects generated");
await onUpdate();
} catch (error) {
toast.error("AI summarization failed");
} finally {
setIsSummarizing(false);
}
};
const handleGeneratePdf = async () => {
try {
setIsGeneratingPdf(true);
// Save current state first to ensure PDF uses latest
await api.updateJob(job.id, {
tailoredSummary: summary,
selectedProjectIds: Array.from(selectedIds).join(','),
});
await api.generateJobPdf(job.id);
toast.success("Resume PDF generated");
await onUpdate();
} catch (error) {
toast.error("PDF generation failed");
} finally {
setIsGeneratingPdf(false);
}
};
const maxProjects = 3; // Example limit, could come from settings
const tooManyProjects = selectedIds.size > maxProjects;
return (
<Card className="border-primary/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-lg font-bold">Tailoring Editor</CardTitle>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={handleSummarize}
disabled={isSummarizing || isGeneratingPdf || isSaving}
>
{isSummarizing ? <Loader2 className="h-4 w-4 animate-spin" /> : <Sparkles className="mr-2 h-4 w-4" />}
AI Summarize
</Button>
<Button
size="sm"
onClick={handleGeneratePdf}
disabled={isSummarizing || isGeneratingPdf || isSaving || !summary}
>
{isGeneratingPdf ? <Loader2 className="h-4 w-4 animate-spin" /> : <FileText className="mr-2 h-4 w-4" />}
Generate PDF
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Tailored Summary</label>
<textarea
className="w-full min-h-[120px] rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={summary}
onChange={(e) => setSummary(e.target.value)}
placeholder="AI will generate this, or you can write your own..."
/>
</div>
<Separator />
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">Selected Projects</label>
{tooManyProjects && (
<span className="flex items-center gap-1 text-xs text-amber-600 font-medium">
<AlertTriangle className="h-3 w-3" />
Warning: More than {maxProjects} projects might make the resume too long.
</span>
)}
</div>
<div className="grid gap-2 max-h-[300px] overflow-auto pr-2">
{catalog.map((project) => (
<div
key={project.id}
className="flex items-start gap-3 rounded-lg border p-3 text-sm transition-colors hover:bg-muted/50"
>
<Checkbox
id={`project-${project.id}`}
checked={selectedIds.has(project.id)}
onCheckedChange={() => handleToggleProject(project.id)}
className="mt-1"
/>
<label
htmlFor={`project-${project.id}`}
className="flex flex-1 flex-col gap-1 cursor-pointer"
>
<span className="font-semibold">{project.name}</span>
<span className="text-xs text-muted-foreground line-clamp-2">{project.description}</span>
</label>
</div>
))}
</div>
</div>
<div className="flex justify-end border-t pt-4">
<Button variant="ghost" size="sm" onClick={handleSave} disabled={isSaving}>
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Check className="mr-2 h-4 w-4" />}
Save Selection
</Button>
</div>
</CardContent>
</Card>
);
};
@@ -6,3 +6,5 @@ export { JobCard } from './JobCard';
export { JobTable } from './JobTable';
export { JobList } from './JobList';
export { PipelineProgress } from './PipelineProgress';
export { TailoringEditor } from './TailoringEditor';