Eliminate all synthetic resume hallucinations, integrate OpenRouter small-model support, and strip all emojis site-wide
This commit is contained in:
parent
7f0ae931d5
commit
9205b88458
16 changed files with 194 additions and 221 deletions
|
|
@ -35,6 +35,8 @@ services:
|
|||
NEXTAUTH_SECRET: "${NEXTAUTH_SECRET:-jobsboard-secret-key-for-auth-sessions}"
|
||||
NEXTAUTH_URL: "${NEXTAUTH_URL:-http://localhost:3000}"
|
||||
NODE_ENV: "production"
|
||||
OPENROUTER_API_KEY: "${OPENROUTER_API_KEY:-}"
|
||||
OPENROUTER_MODEL: "${OPENROUTER_MODEL:-meta-llama/llama-3.2-3b-instruct}"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ export default function JobAlertsPage() {
|
|||
disabled={testing}
|
||||
className="px-3 py-1.5 bg-zinc-900 text-white text-xs font-medium rounded hover:bg-zinc-800 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{testing ? "Processing Alerts..." : "⚡ Run Alert Matching Check Now"}
|
||||
{testing ? "Processing Alerts..." : " Run Alert Matching Check Now"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -294,10 +294,10 @@ export default function JobAlertsPage() {
|
|||
</div>
|
||||
|
||||
<div className="text-xs text-zinc-500 flex flex-wrap gap-x-3 gap-y-1">
|
||||
{s.keywords && <span>🔍 "{s.keywords}"</span>}
|
||||
{s.location && <span>📍 {s.location}</span>}
|
||||
{s.department && <span>🏢 {s.department}</span>}
|
||||
{s.minSalary && <span>💰 Min ${s.minSalary.toLocaleString()}</span>}
|
||||
{s.keywords && <span> "{s.keywords}"</span>}
|
||||
{s.location && <span> {s.location}</span>}
|
||||
{s.department && <span> {s.department}</span>}
|
||||
{s.minSalary && <span> Min ${s.minSalary.toLocaleString()}</span>}
|
||||
</div>
|
||||
|
||||
{s.lastSentAt && (
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export async function PATCH(req: Request) {
|
|||
await createNotification({
|
||||
userId: claim.claimantId,
|
||||
type: "COMPANY_CLAIM_STATUS",
|
||||
title: "Company Claim Approved! 🎉",
|
||||
title: "Company Claim Approved! ",
|
||||
message: `Your ownership claim for ${claim.company.name} has been verified and approved. You are now the official Owner.`,
|
||||
link: `/employer/ats`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,8 +99,8 @@ export async function POST(req: Request) {
|
|||
(j) =>
|
||||
`<li style="margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #f1f5f9;">
|
||||
<strong><a href="${j.jobUrl || `${baseUrl}/jobs/${j.id}`}" target="_blank" style="color: #2563eb; text-decoration: none; font-size: 16px;">${j.title}</a></strong>
|
||||
<div style="color: #334155; font-size: 14px; margin-top: 4px;">🏢 ${j.company} • 📍 ${j.location} ${j.isRemote ? '• 🌐 Remote' : ''}</div>
|
||||
${j.salaryMin ? `<div style="color: #059669; font-size: 13px; margin-top: 2px;">💵 $${j.salaryMin.toLocaleString()}${j.salaryMax ? ` - $${j.salaryMax.toLocaleString()}` : '+'}</div>` : ''}
|
||||
<div style="color: #334155; font-size: 14px; margin-top: 4px;"> ${j.company} • ${j.location} ${j.isRemote ? '• Remote' : ''}</div>
|
||||
${j.salaryMin ? `<div style="color: #059669; font-size: 13px; margin-top: 2px;"> $${j.salaryMin.toLocaleString()}${j.salaryMax ? ` - $${j.salaryMax.toLocaleString()}` : '+'}</div>` : ''}
|
||||
</li>`
|
||||
)
|
||||
.join("");
|
||||
|
|
@ -109,7 +109,7 @@ export async function POST(req: Request) {
|
|||
|
||||
const emailResult = await sendEmail({
|
||||
to: search.user.email,
|
||||
subject: `🎯 ${matchingJobs.length} New Job Match${matchingJobs.length > 1 ? "es" : ""} for "${searchTitle}"`,
|
||||
subject: ` ${matchingJobs.length} New Job Match${matchingJobs.length > 1 ? "es" : ""} for "${searchTitle}"`,
|
||||
html: `
|
||||
<div style="font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 0 auto; color: #0f172a; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden;">
|
||||
<div style="background-color: #2563eb; color: #ffffff; padding: 20px; text-align: center;">
|
||||
|
|
@ -179,7 +179,7 @@ export async function POST(req: Request) {
|
|||
|
||||
await sendEmail({
|
||||
to: alert.user.email,
|
||||
subject: `🎯 ${matchingJobs.length} New Job Match${matchingJobs.length > 1 ? "es" : ""} for "${alert.titleQuery || "your search"}"`,
|
||||
subject: ` ${matchingJobs.length} New Job Match${matchingJobs.length > 1 ? "es" : ""} for "${alert.titleQuery || "your search"}"`,
|
||||
html: `<div style="font-family: sans-serif;"><h2>Your Job Alert</h2><ul>${jobListHtml}</ul></div>`,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export async function POST(req: Request) {
|
|||
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "🔒 JobsBoard: Reset Your Password",
|
||||
subject: " JobsBoard: Reset Your Password",
|
||||
html: `
|
||||
<div style="font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e2e8f0; border-radius: 8px;">
|
||||
<h2 style="margin-top: 0; color: #0f172a;">Password Reset Request</h2>
|
||||
|
|
|
|||
|
|
@ -2,119 +2,77 @@ import { NextResponse } from "next/server";
|
|||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
|
||||
// Action verb enhancement bank
|
||||
const ACTIVE_VERBS_BY_DOMAIN: Record<string, string[]> = {
|
||||
engineering: ["Architected", "Engineered", "Developed", "Spearheaded", "Optimized", "Refactored", "Automated", "Deployed", "Orchestrated", "Implemented"],
|
||||
systems: ["Administered", "Configured", "Monitored", "Diagnosed", "Maintained", "Hardened", "Virtualize", "Migrated", "Resolved", "Provisioned"],
|
||||
leadership: ["Directed", "Supervised", "Coordinated", "Championed", "Mentored", "Facilitated", "Delivered", "Accelerated", "Strengthened"],
|
||||
general: ["Delivered", "Constructed", "Streamlined", "Executed", "Formulated", "Analyzed", "Pioneered", "Standardized"],
|
||||
};
|
||||
/**
|
||||
* TRUTH-GROUNDED RESUME ENHANCER & OPTIMIZER
|
||||
*
|
||||
* Rules:
|
||||
* 1. NEVER invent facts, employers, companies, degrees, dates, metrics, or technologies not in input.
|
||||
* 2. ONLY polish grammar, replace passive phrasing with active professional verbs, and improve syntax.
|
||||
* 3. Preserve the candidate's exact domain, authentic role, and truthfulness.
|
||||
* 4. Pluggable: Uses OpenRouter small models (e.g. meta-llama/llama-3.2-3b-instruct) when key is supplied, with fallback to strict deterministic grammatical polish.
|
||||
*/
|
||||
|
||||
function polishBullet(bullet: string, positionTitle: string): string {
|
||||
// Active verb replacements for weak starting phrases that preserve original meaning
|
||||
const WEAK_PHRASE_MAPPINGS: Array<[RegExp, string]> = [
|
||||
[/^\s*(worked on|working on|was working on)\b/i, "Developed and maintained"],
|
||||
[/^\s*(worked with|working with)\b/i, "Collaborated with"],
|
||||
[/^\s*(responsible for|was responsible for|held responsibility for)\b/i, "Managed and executed"],
|
||||
[/^\s*(helped with|helped out with|assisted with|assisted in)\b/i, "Assisted with and contributed to"],
|
||||
[/^\s*(helped|assisted)\b/i, "Collaborated on"],
|
||||
[/^\s*(handled|handling)\b/i, "Managed and coordinated"],
|
||||
[/^\s*(made|making|created|creating)\b/i, "Developed and produced"],
|
||||
[/^\s*(did|doing|performed|performing)\b/i, "Executed and completed"],
|
||||
[/^\s*(used|using|utilized|utilizing)\b/i, "Applied and utilized"],
|
||||
[/^\s*(maintained|maintaining)\b/i, "Maintained and supported"],
|
||||
[/^\s*(tested|testing)\b/i, "Tested and verified"],
|
||||
[/^\s*(supported|supporting)\b/i, "Supported and resolved"],
|
||||
[/^\s*(fixed|fixing)\b/i, "Diagnosed and resolved"],
|
||||
[/^\s*(built|building)\b/i, "Constructed and implemented"],
|
||||
[/^\s*(learned|learning)\b/i, "Acquired proficiency in"],
|
||||
[/^\s*(talked to|communicated with)\b/i, "Liaised and coordinated with"],
|
||||
[/^\s*(in charge of)\b/i, "Directed operations for"],
|
||||
];
|
||||
|
||||
function polishBulletStrict(bullet: string): string {
|
||||
if (!bullet || typeof bullet !== "string") return "";
|
||||
let clean = bullet.trim().replace(/^[•·\-\*–—\d.)\s]+/, "").trim();
|
||||
if (clean.length < 5) return clean;
|
||||
if (clean.length < 4) return clean;
|
||||
|
||||
const lower = clean.toLowerCase();
|
||||
|
||||
// 1. Specific domain-specific transformations
|
||||
if (lower.includes("customer service") || lower.includes("front-line") || lower.includes("client relations")) {
|
||||
return "Delivered high-volume client engagement and operational support, maintaining 99%+ satisfaction and strict service delivery standards.";
|
||||
}
|
||||
|
||||
if (lower.includes("homelab") || lower.includes("bare metal") || lower.includes("virtualization")) {
|
||||
return "Architected and administered self-hosted virtualization environments, configuring automated backups, containerized services, and secure network segmentation.";
|
||||
}
|
||||
|
||||
if (lower.includes("troubleshoot") || lower.includes("help desk") || lower.includes("tier 1") || lower.includes("tier 2")) {
|
||||
return "Diagnosed and resolved technical support escalations across operating systems, enterprise hardware, and network infrastructure with 98% first-touch SLA compliance.";
|
||||
}
|
||||
|
||||
// 2. Active verb replacements for weak starting phrases
|
||||
const weakReplacements: Array<[RegExp, string]> = [
|
||||
[/^\s*(worked on|working on|worked with|was working on)/i, "Engineered and deployed"],
|
||||
[/^\s*(responsible for|was responsible for|held responsibility for)/i, "Directed and executed"],
|
||||
[/^\s*(helped with|helped out with|assisted with|assisted in)/i, "Streamlined and collaborated on"],
|
||||
[/^\s*(helped|assisted)/i, "Spearheaded and facilitated"],
|
||||
[/^\s*(handled|handling)/i, "Managed and optimized"],
|
||||
[/^\s*(made|making|created|creating)/i, "Constructed and deployed"],
|
||||
[/^\s*(did|doing|performed|performing)/i, "Executed and delivered"],
|
||||
[/^\s*(used|using|utilized|utilizing)/i, "Leveraged and applied"],
|
||||
[/^\s*(managed|managing)/i, "Orchestrated and managed"],
|
||||
[/^\s*(maintained|maintaining)/i, "Maintained and safeguarded"],
|
||||
[/^\s*(tested|testing)/i, "Tested and validated"],
|
||||
[/^\s*(supported|supporting)/i, "Supported and optimized"],
|
||||
[/^\s*(fixed|fixing)/i, "Diagnosed and resolved"],
|
||||
[/^\s*(built|building)/i, "Architected and delivered"],
|
||||
[/^\s*(learned|learning)/i, "Mastered and adopted"],
|
||||
[/^\s*(talked to|communicated with)/i, "Consulted and liaised with"],
|
||||
];
|
||||
|
||||
let replaced = false;
|
||||
for (const [pattern, replacement] of weakReplacements) {
|
||||
// 1. Replace weak lead phrases while preserving the rest of the sentence verbatim
|
||||
for (const [pattern, replacement] of WEAK_PHRASE_MAPPINGS) {
|
||||
if (pattern.test(clean)) {
|
||||
clean = clean.replace(pattern, replacement);
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If no weak phrase was replaced, ensure it starts with an active verb or metric
|
||||
if (!replaced) {
|
||||
const firstWord = clean.split(" ")[0].toLowerCase();
|
||||
const commonVerbs = ["engineered", "architected", "developed", "managed", "delivered", "deployed", "spearheaded", "administered", "configured", "optimized", "diagnosed", "executed", "collaborated"];
|
||||
const startsWithActive = commonVerbs.some(v => firstWord.startsWith(v));
|
||||
// 2. Ensure first letter is capitalized
|
||||
clean = clean.charAt(0).toUpperCase() + clean.slice(1);
|
||||
|
||||
if (!startsWithActive) {
|
||||
if (positionTitle.toLowerCase().includes("engineer") || positionTitle.toLowerCase().includes("developer")) {
|
||||
clean = `Engineered solutions to ${clean.charAt(0).toLowerCase() + clean.slice(1)}`;
|
||||
} else if (positionTitle.toLowerCase().includes("support") || positionTitle.toLowerCase().includes("admin")) {
|
||||
clean = `Administered operations to ${clean.charAt(0).toLowerCase() + clean.slice(1)}`;
|
||||
} else {
|
||||
clean = `Delivered key deliverables to ${clean.charAt(0).toLowerCase() + clean.slice(1)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Ensure trailing punctuation
|
||||
// 3. Ensure proper sentence termination
|
||||
if (!clean.endsWith(".") && !clean.endsWith("!") && !clean.endsWith("?")) {
|
||||
clean = clean + ".";
|
||||
}
|
||||
|
||||
// 5. Capitalize first letter
|
||||
clean = clean.charAt(0).toUpperCase() + clean.slice(1);
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
function polishSummary(title: string, currentSummary: string, skills: string[]): string {
|
||||
const skillsSnippet = Array.isArray(skills) && skills.length > 0 ? skills.slice(0, 5).join(", ") : "";
|
||||
const lowerSummary = (currentSummary || "").toLowerCase();
|
||||
const lowerTitle = (title || "").toLowerCase();
|
||||
|
||||
if (lowerSummary.includes("it support") || lowerTitle.includes("support") || lowerTitle.includes("admin")) {
|
||||
return `Dedicated ${title || "IT Systems Specialist"} with demonstrated expertise administering server infrastructure, identity management, and technical troubleshooting${skillsSnippet ? ` across ${skillsSnippet}` : ""}. Proven track record of resolving complex escalations, automating operational workflows, and ensuring enterprise system reliability.`;
|
||||
function polishSummaryStrict(currentSummary: string, title?: string): string {
|
||||
if (!currentSummary || currentSummary.trim().length === 0) {
|
||||
return title ? `Professional with experience in ${title}.` : "";
|
||||
}
|
||||
|
||||
if (lowerSummary.includes("software") || lowerTitle.includes("engineer") || lowerTitle.includes("developer")) {
|
||||
return `Results-driven ${title || "Software Systems Engineer"} with extensive experience architecting high-throughput applications, distributed microservices, and database systems${skillsSnippet ? ` utilizing ${skillsSnippet}` : ""}. Proven history of reducing API latency, maintaining 99.9%+ availability, and delivering mission-critical web platforms.`;
|
||||
let text = currentSummary.trim();
|
||||
text = text.replace(/^(hi,?\s*i('?m| am)|i am an?|i'm an?)\s+/i, "");
|
||||
text = text.charAt(0).toUpperCase() + text.slice(1);
|
||||
if (!text.endsWith(".") && !text.endsWith("!") && !text.endsWith("?")) {
|
||||
text = text + ".";
|
||||
}
|
||||
|
||||
if (lowerTitle.includes("design") || lowerSummary.includes("design") || lowerTitle.includes("ui") || lowerTitle.includes("ux")) {
|
||||
return `Creative and strategic ${title || "UI/UX Designer"} specializing in cohesive digital design systems, user-centric interfaces, and interactive prototyping${skillsSnippet ? ` with expertise in ${skillsSnippet}` : ""}. Proven record of translating complex product workflows into intuitive user experiences.`;
|
||||
}
|
||||
|
||||
if (currentSummary && currentSummary.length > 60) {
|
||||
let enhanced = currentSummary.trim();
|
||||
if (!enhanced.endsWith(".")) enhanced += ".";
|
||||
return `Results-oriented ${title || "Professional"} with proven expertise in operational execution and technical problem solving. ${enhanced}`;
|
||||
}
|
||||
|
||||
return `Results-driven ${title || "Professional"} with hands-on experience in technical problem solving, workflow optimization, and team deliverables${skillsSnippet ? ` specializing in ${skillsSnippet}` : ""}. Proven track record of maintaining high operational quality and delivering resilient outcomes.`;
|
||||
return text;
|
||||
}
|
||||
|
||||
// Optional LLM enhancement if user has OPENAI or OPENROUTER configured
|
||||
async function optimizeWithLLM(payload: any) {
|
||||
// OpenRouter / Small Model Integration (Guaranteed anti-hallucination prompt)
|
||||
async function optimizeWithOpenRouter(payload: { profile: any; work: any[]; skills: string[] }) {
|
||||
const apiKey = process.env.OPENROUTER_API_KEY || process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) return null;
|
||||
|
||||
|
|
@ -122,40 +80,67 @@ async function optimizeWithLLM(payload: any) {
|
|||
const endpoint = isRouter
|
||||
? "https://openrouter.ai/api/v1/chat/completions"
|
||||
: `${process.env.OPENAI_BASE_URL || "https://api.openai.com/v1"}/chat/completions`;
|
||||
const model = process.env.OPENROUTER_MODEL || process.env.OPENAI_MODEL || (isRouter ? "google/gemini-2.5-flash" : "gpt-4o-mini");
|
||||
|
||||
const model =
|
||||
process.env.OPENROUTER_MODEL ||
|
||||
process.env.OPENAI_MODEL ||
|
||||
(isRouter ? "meta-llama/llama-3.2-3b-instruct" : "gpt-4o-mini");
|
||||
|
||||
const systemPrompt = `You are an accurate, truth-grounded ATS resume editor.
|
||||
CRITICAL INTEGRITY INSTRUCTIONS:
|
||||
1. NEVER invent, fabricate, exaggerate, or assume any facts, tools, employers, degrees, metrics, dates, or certifications.
|
||||
2. Only rewrite the candidate's existing text to improve grammar, conciseness, and use strong action verbs.
|
||||
3. Keep the exact same company names, titles, and real duties.
|
||||
4. If a bullet point is short or simple, polish its grammar; DO NOT add fake percentages, fake dollar amounts, or unmentioned technologies.
|
||||
5. Return strictly a JSON object with this format:
|
||||
{
|
||||
"profile": { "summary": "polished summary" },
|
||||
"work": [
|
||||
{ "position": "...", "company": "...", "summary": "...", "highlights": ["polished bullet 1", "polished bullet 2"] }
|
||||
]
|
||||
}`;
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 12000);
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
...(isRouter ? { "HTTP-Referer": "https://directwire.io", "X-Title": "DirectWire Resume Optimizer" } : {}),
|
||||
},
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
temperature: 0.1,
|
||||
max_tokens: 1800,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"You are an expert executive resume optimizer and ATS specialist. Transform the candidate's summary and work bullets into impactful, quantifiable, action-verb driven bullets that score 95+ on Workday and Greenhouse ATS scans. Return strictly JSON matching: { profile: { ...profile, summary: string }, work: [{ ...work[i], highlights: string[] }] }",
|
||||
},
|
||||
{ role: "system", content: systemPrompt },
|
||||
{
|
||||
role: "user",
|
||||
content: `Optimize this resume content:\n\n${JSON.stringify(payload)}`,
|
||||
content: `Polish and improve the grammar/action-verbs of this resume while keeping ALL facts 100% true:\n\n${JSON.stringify(payload)}`,
|
||||
},
|
||||
],
|
||||
response_format: { type: "json_object" },
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
const json = await res.json();
|
||||
const content = json.choices?.[0]?.message?.content;
|
||||
if (!content) return null;
|
||||
clearTimeout(timeout);
|
||||
|
||||
return JSON.parse(content);
|
||||
} catch (err) {
|
||||
console.error("LLM optimize fallback triggered:", err);
|
||||
if (!res.ok) {
|
||||
console.warn("OpenRouter API returned non-200:", res.status, await res.text());
|
||||
return null;
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
const rawContent = json.choices?.[0]?.message?.content;
|
||||
if (!rawContent) return null;
|
||||
|
||||
return JSON.parse(rawContent);
|
||||
} catch (err: any) {
|
||||
console.warn("OpenRouter enhancement skipped or timed out, using deterministic polish:", err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -164,80 +149,79 @@ export async function POST(req: Request) {
|
|||
try {
|
||||
const { profile, work, skills, education, projects } = await req.json();
|
||||
|
||||
const candidateTitle = profile?.title || (work?.[0]?.position) || "Technical Professional";
|
||||
const candidateSkills = Array.isArray(skills) && skills.length > 0 ? skills : ["Problem Solving", "Communication", "System Architecture"];
|
||||
const candidateProfile = profile || {};
|
||||
const candidateWork = Array.isArray(work) ? work : [];
|
||||
const candidateSkills = Array.isArray(skills) ? skills : [];
|
||||
const candidateEducation = Array.isArray(education) ? education : [];
|
||||
const candidateProjects = Array.isArray(projects) ? projects : [];
|
||||
|
||||
// 1. Try OpenRouter / Small model if API key is provided
|
||||
let aiResult = await optimizeWithOpenRouter({
|
||||
profile: candidateProfile,
|
||||
work: candidateWork,
|
||||
skills: candidateSkills,
|
||||
});
|
||||
|
||||
if (aiResult?.profile?.summary || Array.isArray(aiResult?.work)) {
|
||||
const mergedWork = candidateWork.map((originalWork: any, idx: number) => {
|
||||
const aiWork = aiResult.work?.[idx];
|
||||
if (!aiWork) return originalWork;
|
||||
return {
|
||||
...originalWork,
|
||||
summary: aiWork.summary || originalWork.summary || "",
|
||||
highlights: Array.isArray(aiWork.highlights) && aiWork.highlights.length > 0
|
||||
? aiWork.highlights.map((h: string) => polishBulletStrict(h))
|
||||
: (originalWork.highlights || []).map((h: string) => polishBulletStrict(h)),
|
||||
};
|
||||
});
|
||||
|
||||
// 1. Try external AI if configured
|
||||
let aiOptimized = await optimizeWithLLM({ profile, work, skills });
|
||||
if (aiOptimized?.profile && Array.isArray(aiOptimized?.work)) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
profile: {
|
||||
...profile,
|
||||
...aiOptimized.profile,
|
||||
...candidateProfile,
|
||||
summary: aiResult.profile?.summary || polishSummaryStrict(candidateProfile.summary, candidateProfile.title),
|
||||
},
|
||||
work: aiOptimized.work,
|
||||
work: mergedWork,
|
||||
skills: candidateSkills,
|
||||
education: education || [],
|
||||
projects: projects || [],
|
||||
education: candidateEducation,
|
||||
projects: candidateProjects,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 2. High-precision deterministic local polish engine (Zero external cost, instant, 100% reliable)
|
||||
const enhancedProfile = {
|
||||
...profile,
|
||||
title: candidateTitle,
|
||||
summary: polishSummary(candidateTitle, profile?.summary || "", candidateSkills),
|
||||
// 2. Truthful deterministic local polish engine (Zero hallucination guarantee)
|
||||
const polishedWork = candidateWork.map((w: any) => {
|
||||
const rawHighlights = Array.isArray(w.highlights)
|
||||
? w.highlights
|
||||
: (w.highlights ? [w.highlights] : []);
|
||||
|
||||
const polishedBullets = rawHighlights
|
||||
.map((h: string) => polishBulletStrict(h))
|
||||
.filter((h: string) => h.length > 0);
|
||||
|
||||
return {
|
||||
...w,
|
||||
position: w.position || "",
|
||||
company: w.company || "",
|
||||
summary: w.summary ? polishBulletStrict(w.summary) : "",
|
||||
highlights: polishedBullets,
|
||||
};
|
||||
});
|
||||
|
||||
const polishedProfile = {
|
||||
...candidateProfile,
|
||||
summary: polishSummaryStrict(candidateProfile.summary, candidateProfile.title),
|
||||
};
|
||||
|
||||
const enhancedWork = Array.isArray(work) && work.length > 0
|
||||
? work.map((w: any) => {
|
||||
const pos = w.position || candidateTitle || "Role";
|
||||
const rawHighlights = Array.isArray(w.highlights)
|
||||
? w.highlights
|
||||
: (w.highlights ? [w.highlights] : []);
|
||||
|
||||
let polishedHighlights = rawHighlights
|
||||
.map((h: string) => polishBullet(h, pos))
|
||||
.filter((h: string) => h.length > 5);
|
||||
|
||||
if (polishedHighlights.length === 0) {
|
||||
polishedHighlights = [
|
||||
`Directed core operational and technical duties as ${pos}, maintaining 100% SLA compliance.`,
|
||||
`Automated routine workflows and collaborated across teams to resolve technical blockers efficiently.`
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
...w,
|
||||
position: pos,
|
||||
company: w.company || "Company",
|
||||
summary: w.summary ? polishBullet(w.summary, pos) : "",
|
||||
highlights: polishedHighlights,
|
||||
};
|
||||
})
|
||||
: [
|
||||
{
|
||||
position: candidateTitle,
|
||||
company: "Enterprise Technology Inc.",
|
||||
summary: "Directed technical operations and core deliverables.",
|
||||
highlights: [
|
||||
`Delivered scalable system components as ${candidateTitle}, ensuring 99.9% uptime and high quality delivery.`,
|
||||
"Collaborated cross-functionally to streamline operational workflows and reduce incident resolution time by 30%."
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
profile: enhancedProfile,
|
||||
work: enhancedWork,
|
||||
profile: polishedProfile,
|
||||
work: polishedWork,
|
||||
skills: candidateSkills,
|
||||
education: education || [],
|
||||
projects: projects || [],
|
||||
education: candidateEducation,
|
||||
projects: candidateProjects,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
|
@ -245,4 +229,3 @@ export async function POST(req: Request) {
|
|||
return NextResponse.json({ error: error.message || "Failed to optimize resume" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -328,21 +328,9 @@ function parseResumeFull(rawText: string) {
|
|||
location: location || "",
|
||||
summary: summary || "",
|
||||
},
|
||||
skills: allDetectedSkills.length > 0 ? allDetectedSkills : ["Problem Solving", "Communication", "Documentation"],
|
||||
work: parsedWork.length > 0 ? parsedWork : [
|
||||
{
|
||||
position: headline || "Professional Experience",
|
||||
company: "Previous Employer",
|
||||
summary: "Provided high-quality technical and operational contributions.",
|
||||
highlights: [
|
||||
"Executed core project responsibilities and operational workflows with 100% adherence to standards.",
|
||||
"Collaborated cross-functionally to diagnose technical roadblocks and implement sustainable improvements."
|
||||
]
|
||||
}
|
||||
],
|
||||
education: parsedEducation.length > 0 ? parsedEducation : [
|
||||
{ degree: "Technical Certification / Degree", institution: "Accredited Institution" }
|
||||
],
|
||||
skills: allDetectedSkills.length > 0 ? allDetectedSkills : [],
|
||||
work: parsedWork,
|
||||
education: parsedEducation,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -280,10 +280,10 @@ export default function ApplicationsPage() {
|
|||
<div className="text-xs text-zinc-600 space-x-2">
|
||||
<span className="font-semibold text-zinc-800">{job.company}</span>
|
||||
<span>•</span>
|
||||
<span>📍 {job.location}</span>
|
||||
{job.isRemote && <span className="text-emerald-700 font-medium">• 🌐 Remote</span>}
|
||||
<span> {job.location}</span>
|
||||
{job.isRemote && <span className="text-emerald-700 font-medium">• Remote</span>}
|
||||
{job.salaryMin && (
|
||||
<span className="text-emerald-700 font-medium">• 💰 ${job.salaryMin.toLocaleString()}</span>
|
||||
<span className="text-emerald-700 font-medium">• ${job.salaryMin.toLocaleString()}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -342,7 +342,7 @@ export default function ApplicationsPage() {
|
|||
{isExpanded && (
|
||||
<div className="bg-zinc-50 p-3 rounded-md border border-zinc-200 text-xs space-y-2 mt-2">
|
||||
<h4 className="font-bold text-zinc-900 text-xs flex items-center justify-between border-b border-zinc-200 pb-1.5">
|
||||
<span>⏱️ Application Status Audit Trail</span>
|
||||
<span> Application Status Audit Trail</span>
|
||||
<span className="text-[10px] font-normal text-zinc-500 font-mono">Real-time status updates</span>
|
||||
</h4>
|
||||
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ export default function CompanyDetailPage() {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 text-xs text-zinc-500 mt-1">
|
||||
{company.location && <span>📍 {company.location}</span>}
|
||||
{company.location && <span> {company.location}</span>}
|
||||
{company.website && (
|
||||
<a
|
||||
href={company.website}
|
||||
|
|
@ -260,7 +260,7 @@ export default function CompanyDetailPage() {
|
|||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline flex items-center space-x-1 font-medium"
|
||||
>
|
||||
<span>🌐 Website</span>
|
||||
<span> Website</span>
|
||||
</a>
|
||||
)}
|
||||
{company.verificationStatus === "UNCLAIMED" && (
|
||||
|
|
@ -353,7 +353,7 @@ export default function CompanyDetailPage() {
|
|||
{job.title}
|
||||
</Link>
|
||||
<div className="flex items-center space-x-3 text-xs text-zinc-500 mt-1">
|
||||
<span>📍 {job.location}</span>
|
||||
<span> {job.location}</span>
|
||||
{job.isRemote && (
|
||||
<span className="bg-emerald-50 text-emerald-700 px-1.5 py-0.5 rounded font-mono text-[10px]">
|
||||
Remote
|
||||
|
|
@ -371,7 +371,7 @@ export default function CompanyDetailPage() {
|
|||
onClick={() => setApplyingJob(job)}
|
||||
className="px-3.5 py-1.5 bg-zinc-900 hover:bg-zinc-800 text-white text-xs font-bold rounded transition-colors"
|
||||
>
|
||||
⚡ Quick Apply
|
||||
Quick Apply
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -558,7 +558,7 @@ export default function CompanyDetailPage() {
|
|||
onClick={() => setReportingReviewId(rev.id)}
|
||||
className="text-[10px] text-zinc-400 hover:text-red-600 font-medium"
|
||||
>
|
||||
🚩 Report
|
||||
Report
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -398,7 +398,7 @@ export default function EmployerATSPage() {
|
|||
key={t.id}
|
||||
className="text-[9px] font-mono font-bold bg-amber-50 text-amber-800 border border-amber-200 px-1.5 py-0.5 rounded"
|
||||
>
|
||||
🏷️ {t.tag}
|
||||
{t.tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -500,7 +500,7 @@ export default function EmployerATSPage() {
|
|||
{/* Snapshot Banner */}
|
||||
{snap && (
|
||||
<div className="bg-emerald-50 text-emerald-900 border border-emerald-200 p-3 rounded-md text-xs">
|
||||
🔒 <strong>Frozen Candidate Snapshot:</strong> Captured on{" "}
|
||||
<strong>Frozen Candidate Snapshot:</strong> Captured on{" "}
|
||||
{new Date(snap.submittedAt || selectedApp.createdAt).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -524,7 +524,7 @@ export default function EmployerATSPage() {
|
|||
{cand.location && (
|
||||
<div>
|
||||
<span className="font-semibold text-zinc-500 block text-[10px] uppercase font-mono">Location</span>
|
||||
<span className="font-medium text-zinc-800">📍 {cand.location}</span>
|
||||
<span className="font-medium text-zinc-800"> {cand.location}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -538,7 +538,7 @@ export default function EmployerATSPage() {
|
|||
key={t.id}
|
||||
className="inline-flex items-center space-x-1 text-xs font-mono font-bold bg-amber-50 text-amber-800 border border-amber-200 px-2 py-1 rounded"
|
||||
>
|
||||
<span>🏷️ {t.tag}</span>
|
||||
<span> {t.tag}</span>
|
||||
<button
|
||||
onClick={() => handleRemoveTag(selectedApp.id, t.tag)}
|
||||
className="hover:text-red-700 font-bold ml-1"
|
||||
|
|
@ -638,7 +638,7 @@ export default function EmployerATSPage() {
|
|||
<div className="border-t border-zinc-100 pt-3">
|
||||
<h4 className="font-bold text-zinc-900 mb-1">Attached Resume Document</h4>
|
||||
<div className="bg-zinc-50 p-3 rounded-md border border-zinc-200 flex justify-between items-center">
|
||||
<span className="font-semibold text-zinc-800">📄 {resume.title}</span>
|
||||
<span className="font-semibold text-zinc-800"> {resume.title}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-[10px] font-mono text-emerald-700 bg-emerald-50 border border-emerald-200 px-2 py-0.5 rounded font-bold">
|
||||
Verified Attached
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export default function CandidateSearchPage() {
|
|||
|
||||
{errorMsg && (
|
||||
<div className="p-4 bg-red-50 border border-red-200 text-red-700 text-xs rounded-md flex justify-between items-center">
|
||||
<span>⚠️ {errorMsg}</span>
|
||||
<span> {errorMsg}</span>
|
||||
<button
|
||||
onClick={searchCandidates}
|
||||
className="px-3 py-1 bg-red-600 text-white rounded font-bold hover:bg-red-700 transition-colors"
|
||||
|
|
@ -246,7 +246,7 @@ export default function CandidateSearchPage() {
|
|||
{cand.user.name || "Anonymous Candidate"}
|
||||
</h3>
|
||||
<p className="text-xs font-medium text-zinc-600">
|
||||
{cand.headline || "Job Candidate"} {cand.location && `• 📍 ${cand.location}`}
|
||||
{cand.headline || "Job Candidate"} {cand.location && `• ${cand.location}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ export default function CandidateSearchPage() {
|
|||
href={`mailto:${cand.user.email}?subject=Opportunity%20via%20JobsBoard`}
|
||||
className="px-3 py-1.5 bg-zinc-900 text-white text-xs font-semibold rounded hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
✉ Contact Candidate
|
||||
Contact Candidate
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ export default function ProfileBuilderPage() {
|
|||
</div>
|
||||
|
||||
<label className="cursor-pointer bg-zinc-100 hover:bg-zinc-200 border border-zinc-300 text-zinc-800 text-xs font-semibold px-3 py-1.5 rounded transition-colors inline-block">
|
||||
{parsing ? "Parsing Resume..." : "✨ Auto-Fill from Resume PDF/docx"}
|
||||
{parsing ? "Parsing Resume..." : " Auto-Fill from Resume PDF/docx"}
|
||||
<input type="file" accept=".pdf,.docx,.txt" onChange={handleResumeUpload} className="hidden" />
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -228,7 +228,7 @@ export default function ProfileBuilderPage() {
|
|||
{/* Privacy & Employer Search Visibility Section */}
|
||||
<div className="bg-white p-5 rounded-md border border-zinc-200 shadow-sm space-y-3">
|
||||
<h2 className="text-sm font-bold text-zinc-900 flex items-center space-x-2">
|
||||
<span>🔒 Privacy & Employer Search Visibility</span>
|
||||
<span> Privacy & Employer Search Visibility</span>
|
||||
</h2>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 p-3.5 rounded text-xs text-blue-900 space-y-2">
|
||||
|
|
|
|||
|
|
@ -184,11 +184,11 @@ export default function SavedJobsPage() {
|
|||
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-zinc-600">
|
||||
<span className="font-medium text-zinc-800">{job.company}</span>
|
||||
<span>📍 {job.location}</span>
|
||||
{job.department && <span>🏢 {job.department}</span>}
|
||||
<span> {job.location}</span>
|
||||
{job.department && <span> {job.department}</span>}
|
||||
{job.salaryMin && (
|
||||
<span className="text-emerald-700 font-medium">
|
||||
💰 ${job.salaryMin.toLocaleString()}
|
||||
${job.salaryMin.toLocaleString()}
|
||||
{job.salaryMax ? ` - $${job.salaryMax.toLocaleString()}` : "+"}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export function NotificationPreferences() {
|
|||
<div className="bg-white p-5 rounded-md border border-zinc-200 shadow-sm space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold text-zinc-900">🔔 Notification Preferences</h2>
|
||||
<h2 className="text-sm font-bold text-zinc-900"> Notification Preferences</h2>
|
||||
<p className="text-xs text-zinc-500 mt-0.5">
|
||||
Configure how and when you receive status updates and job alert notifications.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export function OneClickApplyModal({ job, onClose, onSuccess }: OneClickApplyMod
|
|||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="bg-zinc-50 p-4 rounded-md border border-zinc-200 text-xs space-y-2">
|
||||
<div className="flex justify-between items-center pb-2 border-b border-zinc-200 font-bold text-zinc-800">
|
||||
<span>📄 Application Snapshot Summary</span>
|
||||
<span> Application Snapshot Summary</span>
|
||||
<span className="text-[10px] font-mono bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded font-semibold">
|
||||
Profile Verified
|
||||
</span>
|
||||
|
|
@ -156,20 +156,20 @@ export function OneClickApplyModal({ job, onClose, onSuccess }: OneClickApplyMod
|
|||
)}
|
||||
|
||||
<div className="text-zinc-500 space-y-1 text-[11px]">
|
||||
{profileData?.location && <div>📍 Location: {profileData.location}</div>}
|
||||
{profileData?.location && <div> Location: {profileData.location}</div>}
|
||||
{profileData?.skills?.length > 0 && (
|
||||
<div>
|
||||
💡 Skills:{" "}
|
||||
Skills:{" "}
|
||||
{profileData.skills.slice(0, 4).map((s: any) => s.name).join(", ")}
|
||||
</div>
|
||||
)}
|
||||
{activeResume ? (
|
||||
<div className="text-blue-700 font-medium">
|
||||
📎 Active Resume: {activeResume.title}
|
||||
Active Resume: {activeResume.title}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-zinc-600">
|
||||
📋 Profile details will be submitted directly to employer.
|
||||
Profile details will be submitted directly to employer.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -439,9 +439,9 @@ export function ResumeEditor() {
|
|||
onClick={handleOptimize}
|
||||
disabled={optimizing}
|
||||
className="bg-emerald-800 hover:bg-emerald-900 text-white font-mono text-xs font-semibold px-4 py-2 border border-emerald-950 shadow-[2px_2px_0px_#064e3b] transition-all disabled:opacity-50"
|
||||
title="Uses active verb transformer to enhance bullets & summary"
|
||||
title="Polishes bullet points and grammar with truth-grounded ATS active verbs"
|
||||
>
|
||||
{optimizing ? "Polishing Bullets..." : "✨ Optimize Resume"}
|
||||
{optimizing ? "Polishing Bullets..." : "Enhance & Polish"}
|
||||
</button>
|
||||
|
||||
<label className="inline-flex items-center space-x-2 font-mono text-xs text-stone-700 cursor-pointer select-none bg-stone-50 border border-stone-300 px-3 py-2">
|
||||
|
|
@ -451,7 +451,7 @@ export function ResumeEditor() {
|
|||
onChange={(e) => setIsActiveForMatching(e.target.checked)}
|
||||
className="rounded text-stone-900 focus:ring-stone-900 h-3.5 w-3.5"
|
||||
/>
|
||||
<span>Active for Job Matching</span>
|
||||
<span>Active for Matching</span>
|
||||
</label>
|
||||
|
||||
<a
|
||||
|
|
@ -460,7 +460,7 @@ export function ResumeEditor() {
|
|||
download
|
||||
className="bg-stone-900 hover:bg-stone-800 text-stone-100 font-mono text-xs font-semibold px-3.5 py-2 border border-stone-950 shadow-[2px_2px_0px_#1c1917] transition-all inline-flex items-center space-x-1"
|
||||
>
|
||||
<span>📥 Download PDF</span>
|
||||
<span>Download PDF</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
|
|
@ -468,7 +468,7 @@ export function ResumeEditor() {
|
|||
disabled={saving}
|
||||
className="bg-amber-100 hover:bg-amber-200 text-stone-900 font-mono text-xs font-semibold px-4 py-2 border border-stone-400 shadow-[2px_2px_0px_#78350f] transition-all"
|
||||
>
|
||||
{savedSuccess ? "✓ Saved" : saving ? "Saving..." : "Save Changes"}
|
||||
{savedSuccess ? "Saved" : saving ? "Saving..." : "Save Changes"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue