Skip to content

Commit cd104ca

Browse files
committed
rengenerate resume, highlight results in bullet points
1 parent 56f8953 commit cd104ca

11 files changed

Lines changed: 604 additions & 20 deletions

File tree

scratch-test-summary-real.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import fs from 'node:fs';
2+
for (const line of fs.readFileSync('.env', 'utf-8').split('\n')) {
3+
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
4+
if (m) process.env[m[1]] = m[2];
5+
}
6+
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
7+
import { generateText, tool } from 'ai';
8+
import { z } from 'zod';
9+
import { PROFESSIONAL_SUMMARY_GENERATOR_MESSAGE } from './src/lib/prompts';
10+
11+
const aiClient = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY, baseURL: 'https://openrouter.ai/api/v1' })('deepseek/deepseek-v3.2:nitro');
12+
13+
async function run(label: string, work_experience: any[]) {
14+
const profile = {
15+
work_experience,
16+
skills: [{ category: 'Languages', skills: ['TypeScript', 'React', 'Node.js'] }],
17+
projects: [],
18+
certifications: [],
19+
};
20+
21+
const job = {
22+
position_title: 'Software Engineer',
23+
company_name: 'Beta Inc',
24+
description: 'Looking for a software engineer to join our team.',
25+
keywords: ['TypeScript', 'React'],
26+
};
27+
28+
const profileBlob = JSON.stringify(profile, null, 2);
29+
const jobBlob = `Position: ${job.position_title}\nCompany: ${job.company_name}\nKeywords: ${job.keywords.join(', ')}\nDescription:\n${job.description}`;
30+
31+
const { text } = await generateText({
32+
model: aiClient,
33+
system: PROFESSIONAL_SUMMARY_GENERATOR_MESSAGE.content as string,
34+
prompt: `CANDIDATE PROFILE (source of truth — only reference skills/experience present here):
35+
${profileBlob}
36+
37+
TARGET JOB:
38+
${jobBlob}
39+
40+
Call getCurrentDate first, then compute the candidate's real years of experience and the seniority-adjusted opener title per your instructions. Write the professional summary paragraph now. 3-4 sentences, 60-90 words, plain text only.`,
41+
tools: {
42+
getCurrentDate: tool({
43+
description:
44+
'Returns today\'s real-world date. Call this before computing years of experience or resolving "Present" in work_experience date ranges, since your training data may be outdated.',
45+
parameters: z.object({}),
46+
execute: async () => new Date().toISOString().split('T')[0],
47+
}),
48+
},
49+
maxSteps: 3,
50+
});
51+
52+
console.log(`\n=== ${label} ===`);
53+
console.log('SUMMARY:', text.trim());
54+
}
55+
56+
async function main() {
57+
await run('Real resume (expect 5+ years)', [
58+
{ company: 'Acuity Health', position: 'Software Engineer II', location: 'Spring Hill, TN', date: 'Jul 2024 - Present', description: ['Led development of admin portal'], technologies: ['Next.js', 'React'] },
59+
{ company: 'George Mason University', position: 'Software Engineer', location: 'Fairfax, VA', date: 'Feb 2024 - Dec 2024', description: ['Built geospatial workflow tool'], technologies: ['React', 'D3.js'] },
60+
{ company: 'Phenom', position: 'Software Development Engineer', location: 'Hyderabad, India', date: 'Sep 2020 - Dec 2022', description: ['Built no-code form builder'], technologies: ['React', 'TypeScript'] },
61+
{ company: 'Carelon Global Solutions', position: 'Associate Software Engineer', location: 'Hyderabad, India', date: 'Jun 2020 - Aug 2020', description: ['Built LMS'], technologies: ['Angular'] },
62+
]);
63+
64+
await run('Overlap only, no gap (expect 3+ years, not 6+)', [
65+
{ company: 'A', position: 'Engineer', location: 'X', date: 'Jan 2023 - Present', description: ['x'], technologies: [] },
66+
{ company: 'B', position: 'Consultant', location: 'X', date: 'Jun 2023 - Dec 2024', description: ['x'], technologies: [] },
67+
]);
68+
69+
await run('Gap only, no overlap (expect 4+ years, not 5+)', [
70+
{ company: 'A', position: 'Engineer', location: 'X', date: 'Jan 2020 - Dec 2021', description: ['x'], technologies: [] },
71+
{ company: 'B', position: 'Engineer', location: 'X', date: 'Jan 2023 - Dec 2024', description: ['x'], technologies: [] },
72+
]);
73+
}
74+
75+
main().catch((e) => { console.error(e); process.exit(1); });

scratch-test-summary.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import fs from 'node:fs';
2+
for (const line of fs.readFileSync('.env', 'utf-8').split('\n')) {
3+
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
4+
if (m) process.env[m[1]] = m[2];
5+
}
6+
import { generateProfessionalSummary } from './src/utils/actions/resumes/ai';
7+
8+
async function main() {
9+
const profile = {
10+
work_experience: [
11+
{
12+
company: 'Acme Corp',
13+
position: 'Software Engineer',
14+
location: 'Remote',
15+
date: 'Jan 2019 - Present',
16+
description: ['Built scalable backend services', 'Led migration to microservices'],
17+
technologies: ['TypeScript', 'Node.js', 'AWS'],
18+
},
19+
],
20+
skills: [{ category: 'Languages', skills: ['TypeScript', 'Python'] }],
21+
projects: [],
22+
education: [],
23+
certifications: [],
24+
} as any;
25+
26+
const job = {
27+
position_title: 'Junior Software Engineer',
28+
company_name: 'Beta Inc',
29+
description: 'Looking for a junior software engineer to join our team.',
30+
keywords: ['TypeScript', 'React'],
31+
};
32+
33+
const summary = await generateProfessionalSummary({ profile, job });
34+
console.log('SUMMARY:', summary);
35+
}
36+
37+
main().catch((e) => { console.error(e); process.exit(1); });

src/components/resume/editor/actions/resume-editor-actions.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
'use client';
22

3-
import { Resume } from "@/lib/types";
3+
import { Job, Resume } from "@/lib/types";
44
import { Button } from "@/components/ui/button";
5-
import { Download, Loader2, Save } from "lucide-react";
5+
import { Download, Loader2, RefreshCw, Save } from "lucide-react";
66
import { toast } from "@/hooks/use-toast";
77
import { pdf } from '@react-pdf/renderer';
88
import { TextImport } from "../../text-import";
9+
import { RegenerateResumeDialog } from "../dialogs/regenerate-resume-dialog";
910
import { ResumePDFDocument } from "../preview/resume-pdf-document";
1011
import { CoverLetterPDFDocument } from "@/components/cover-letter/cover-letter-pdf-document";
1112
import { generateResumeDocx } from "@/lib/docx/resume-docx";
@@ -21,21 +22,29 @@ import { useState } from "react";
2122

2223
interface ResumeEditorActionsProps {
2324
onResumeChange: (field: keyof Resume, value: Resume[keyof Resume]) => void;
25+
/** The job this resume is tailored to, if any. Required to offer regeneration. */
26+
job?: Job | null;
2427
}
2528

2629
export function ResumeEditorActions({
27-
onResumeChange
30+
onResumeChange,
31+
job
2832
}: ResumeEditorActionsProps) {
2933
const resume = useResumeEditorStore((s) => s.resume);
3034
const isSaving = useResumeEditorStore((s) => s.isSaving);
3135
const hasUnsavedChanges = useResumeEditorStore((s) => s.hasUnsavedChanges);
3236
const setSaving = useResumeEditorStore((s) => s.setSaving);
3337
const markSaved = useResumeEditorStore((s) => s.markSaved);
38+
const replaceResume = useResumeEditorStore((s) => s.replaceResume);
3439
const [downloadOptions, setDownloadOptions] = useState({
3540
resume: true,
3641
coverLetter: true
3742
});
3843
const [downloadFormat, setDownloadFormat] = useState<'pdf' | 'word'>('pdf');
44+
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
45+
46+
// Regeneration re-tailors against a linked job, so it only applies to tailored resumes.
47+
const canRegenerate = !resume.is_base_resume && !!resume.job_id && !!job;
3948

4049
// Save Resume
4150
const handleSave = async () => {
@@ -127,7 +136,11 @@ export function ResumeEditorActions({
127136
{saveStatus}
128137
</span>
129138
</div>
130-
<div className="grid grid-cols-3 gap-2">
139+
<div className={cn(
140+
"grid gap-2",
141+
// Four buttons are too cramped in a narrow editor panel, so wrap to two rows.
142+
canRegenerate ? "grid-cols-2 @md:grid-cols-4" : "grid-cols-3"
143+
)}>
131144
{/* Text Import Button */}
132145
<TextImport
133146
resume={resume}
@@ -275,7 +288,28 @@ export function ResumeEditorActions({
275288
</>
276289
)}
277290
</Button>
291+
292+
{/* Regenerate Button (tailored resumes with a linked job only) */}
293+
{canRegenerate && (
294+
<Button
295+
onClick={() => setShowRegenerateDialog(true)}
296+
className={actionButtonClasses}
297+
>
298+
<RefreshCw className="mr-1.5 h-3.5 w-3.5" />
299+
Regenerate
300+
</Button>
301+
)}
278302
</div>
303+
304+
{canRegenerate && job && (
305+
<RegenerateResumeDialog
306+
open={showRegenerateDialog}
307+
onOpenChange={setShowRegenerateDialog}
308+
resume={resume}
309+
job={job}
310+
onRegenerated={replaceResume}
311+
/>
312+
)}
279313
</div>
280314
);
281-
}
315+
}

0 commit comments

Comments
 (0)