Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions app/api/generate/cover-letter-from-resume/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { NextResponse } from "next/server";

type Body = {
resume?: string;
jobDescription?: string;
tone?: string;
};

function buildPrompt(resume: string, jobDescription: string, tone?: string) {
const t = tone && tone.trim().length > 0 ? tone.trim() : "professional";
return [
"Write a tailored cover letter using the following resume and job description.",
"Goals:",
"- Mirror the resume’s tone, keywords, and achievements.",
"- Align with the job description’s requirements and terminology.",
"- Keep it concise, one page, clear sections, and easy to scan.",
"- Use the same voice and tense as the resume.",
"- Avoid repeating the resume verbatim; synthesize and contextualize.",
`Tone: ${t}`,
"",
"Resume:",
"-----",
resume,
"-----",
"",
"Job Description:",
"-----",
jobDescription,
"-----",
"",
"Return only the final cover letter text."
].join("\n");
}

async function callGemini(prompt: string): Promise<string> {
const gemini: any = await import("../../../../lib/gemini").catch(() => ({}));
let output = "";
try {
if (typeof gemini.generateText === "function") {
const r = await gemini.generateText(prompt);
if (typeof r === "string" && r.trim().length > 0) return r;
if (r && typeof r.text === "function") return r.text();
if (r && typeof r.text === "string") return r.text;
}
} catch {}
try {
const model = gemini.model || gemini.gemini || gemini.client || gemini.default || gemini;
if (model && typeof model.generateContent === "function") {
const r = await model.generateContent(prompt as any);
if (r?.response?.text && typeof r.response.text === "function") return r.response.text();
if (r?.response?.candidates?.[0]?.content?.parts?.[0]?.text) return r.response.candidates[0].content.parts[0].text;
if (typeof r?.text === "function") return r.text();
if (typeof r?.text === "string") return r.text;
}
} catch {}
throw new Error("Gemini client unavailable or incompatible");
}

export async function POST(req: Request) {
try {
const body: Body = await req.json().catch(() => ({}));
const resume = typeof body.resume === "string" ? body.resume.trim() : "";
const jobDescription = typeof body.jobDescription === "string" ? body.jobDescription.trim() : "";
const tone = typeof body.tone === "string" ? body.tone : undefined;
if (!resume || !jobDescription) {
return NextResponse.json({ error: "Missing resume or jobDescription" }, { status: 400 });
}
const prompt = buildPrompt(resume, jobDescription, tone);
const text = await callGemini(prompt);
return NextResponse.json({ text });
} catch (e: any) {
return NextResponse.json({ error: e?.message || "Unexpected error" }, { status: 500 });
}
}

11 changes: 11 additions & 0 deletions app/letter/cover-letter-from-resume/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import CoverLetterFromResume from "../../../components/letter/CoverLetterFromResume";

export default function Page() {
return (
<main style={{ maxWidth: 880, margin: "0 auto", padding: 24 }}>
<h1>Cover Letter from Resume</h1>
<p>Create a tailored cover letter using your resume and a job description.</p>
<CoverLetterFromResume />
</main>
);
}
88 changes: 88 additions & 0 deletions components/letter/CoverLetterFromResume.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
\"use client\";

import { useState } from "react";

export default function CoverLetterFromResume() {
const [resume, setResume] = useState(\"\"");
const [jobDescription, setJobDescription] = useState(\"\"");
const [tone, setTone] = useState(\"professional\");
const [output, setOutput] = useState(\"\"");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(\"\"");

async function generate() {
setError(\"\");
setOutput(\"\"");
if (!resume.trim() || !jobDescription.trim()) {
setError(\"Please provide both resume and job description\");
return;
}
setLoading(true);
try {
const res = await fetch(\"/api/generate/cover-letter-from-resume\", {
method: \"POST\",
headers: { \"Content-Type\": \"application/json\" },
body: JSON.stringify({ resume, jobDescription, tone }),
});
const data = await res.json();
if (!res.ok) {
setError(data?.error || \"Failed to generate cover letter\");
} else {
setOutput(data?.text || \"\");
}
} catch (e: any) {
setError(e?.message || \"Failed to generate cover letter\");
} finally {
setLoading(false);
}
}

return (
<div style={{ display: \"grid\", gap: 16 }}>
<div style={{ display: \"grid\", gap: 8 }}>
<label>Resume</label>
<textarea
value={resume}
onChange={(e) => setResume(e.target.value)}
rows={10}
placeholder=\"Paste your resume content\"
style={{ width: \"100%\" }}
/>
</div>
<div style={{ display: \"grid\", gap: 8 }}>
<label>Job Description</label>
<textarea
value={jobDescription}
onChange={(e) => setJobDescription(e.target.value)}
rows={10}
placeholder=\"Paste the job description\"
style={{ width: \"100%\" }}
/>
</div>
<div style={{ display: \"grid\", gap: 8 }}>
<label>Tone</label>
<input
value={tone}
onChange={(e) => setTone(e.target.value)}
placeholder=\"e.g., professional, friendly\"
style={{ width: \"100%\" }}
/>
</div>
<button onClick={generate} disabled={loading} style={{ padding: 10 }}>
{loading ? \"Generating...\" : \"Generate Cover Letter\"}
</button>
{error ? <div style={{ color: \"red\" }}>{error}</div> : null}
<div style={{ display: \"grid\", gap: 8 }}>
<label>Output</label>
<textarea
value={output}
readOnly
rows={16}
placeholder=\"The generated cover letter will appear here\"
style={{ width: \"100%\" }}
/>
</div>
</div>
);
}