Skip to content

Commit af0d5b5

Browse files
committed
Fix resume score reliability and privacy logging
1 parent 9b3b53e commit af0d5b5

File tree

2 files changed

+127
-43
lines changed

2 files changed

+127
-43
lines changed

src/components/resume/editor/panels/resume-score-panel.tsx

Lines changed: 127 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
77
import { Button } from "@/components/ui/button";
88
import { RefreshCw, TrendingUp, Target, Award } from "lucide-react";
99
import { cn } from "@/lib/utils";
10-
import { useState, useEffect } from "react";
10+
import { useState, useEffect, useMemo } from "react";
1111
import { generateResumeScore } from "@/utils/actions/resumes/actions";
1212
import { Resume, Job as JobType } from "@/lib/types";
13-
import { ApiKey } from "@/utils/ai-tools";
13+
import { useApiKeys, useDefaultModel } from "@/hooks/use-api-keys";
14+
import { toast } from "@/hooks/use-toast";
1415

1516
export interface ResumeScoreMetrics {
1617
overallScore: {
@@ -95,8 +96,15 @@ interface ResumeScorePanelProps {
9596
}
9697

9798
const LOCAL_STORAGE_KEY = 'resumelm-resume-scores';
99+
const MODEL_STORAGE_KEY = 'resumelm-default-model';
98100
const MAX_SCORES = 10;
99101

102+
interface StoredScoreEntry {
103+
score: ResumeScoreMetrics;
104+
signature: string;
105+
generatedAt: string;
106+
}
107+
100108
// Helper function to convert camelCase to readable labels
101109
function camelCaseToReadable(text: string): string {
102110
return text
@@ -106,83 +114,163 @@ function camelCaseToReadable(text: string): string {
106114
.replace(/^./, str => str.toUpperCase());
107115
}
108116

109-
function getStoredScores(resumeId: string): ResumeScoreMetrics | null {
117+
function getModelFromStorage(): string {
118+
if (typeof window === "undefined") return "";
119+
return localStorage.getItem(MODEL_STORAGE_KEY) ?? "";
120+
}
121+
122+
function getResumeForScoring(resume: Resume) {
123+
return {
124+
...resume,
125+
section_configs: undefined,
126+
section_order: undefined
127+
};
128+
}
129+
130+
function getJobForScoring(job?: JobType | null) {
131+
if (!job) return null;
132+
133+
return {
134+
...job,
135+
employment_type: job.employment_type || undefined
136+
};
137+
}
138+
139+
function hashContent(content: string): string {
140+
let hash = 2166136261;
141+
for (let i = 0; i < content.length; i += 1) {
142+
hash ^= content.charCodeAt(i);
143+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
144+
}
145+
146+
return (hash >>> 0).toString(16).padStart(8, "0");
147+
}
148+
149+
function createScoreSignature(resume: Resume, job: JobType | null | undefined, model: string): string {
150+
const payload = {
151+
resume: getResumeForScoring(resume),
152+
job: getJobForScoring(job),
153+
model
154+
};
155+
156+
return hashContent(JSON.stringify(payload));
157+
}
158+
159+
function parseStoredScoreEntry(raw: unknown): StoredScoreEntry | null {
160+
if (!raw || typeof raw !== "object") return null;
161+
162+
const candidate = raw as Record<string, unknown>;
163+
if (typeof candidate.signature !== "string") return null;
164+
if (!candidate.score || typeof candidate.score !== "object") return null;
165+
166+
return {
167+
score: candidate.score as ResumeScoreMetrics,
168+
signature: candidate.signature,
169+
generatedAt: typeof candidate.generatedAt === "string" ? candidate.generatedAt : ""
170+
};
171+
}
172+
173+
function getStoredScores(resumeId: string, signature: string): ResumeScoreMetrics | null {
174+
if (typeof window === "undefined") return null;
175+
110176
try {
111177
const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
112178
if (!stored) return null;
113179

114-
const scores = new Map(JSON.parse(stored));
115-
return scores.get(resumeId) as ResumeScoreMetrics | null;
180+
const scores = new Map<string, unknown>(JSON.parse(stored));
181+
const storedEntry = parseStoredScoreEntry(scores.get(resumeId));
182+
183+
if (!storedEntry) return null;
184+
if (storedEntry.signature !== signature) return null;
185+
186+
return storedEntry.score;
116187
} catch (error) {
117188
console.error('Error reading stored scores:', error);
118189
return null;
119190
}
120191
}
121192

122-
function updateStoredScores(resumeId: string, score: ResumeScoreMetrics) {
193+
function updateStoredScores(resumeId: string, entry: StoredScoreEntry) {
194+
if (typeof window === "undefined") return;
195+
123196
try {
124197
const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
125-
const scores = stored ? new Map(JSON.parse(stored)) : new Map();
198+
const scores = stored ? new Map<string, StoredScoreEntry>(JSON.parse(stored)) : new Map<string, StoredScoreEntry>();
199+
200+
if (scores.has(resumeId)) {
201+
scores.delete(resumeId);
202+
}
126203

127204
// Maintain only MAX_SCORES entries
128205
if (scores.size >= MAX_SCORES) {
129206
const oldestKey = scores.keys().next().value;
130-
scores.delete(oldestKey);
207+
if (oldestKey) {
208+
scores.delete(oldestKey);
209+
}
131210
}
132211

133-
scores.set(resumeId, score);
212+
scores.set(resumeId, entry);
134213
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(Array.from(scores)));
135214
} catch (error) {
136215
console.error('Error storing score:', error);
137216
}
138217
}
139218

140219
export default function ResumeScorePanel({ resume, job }: ResumeScorePanelProps) {
220+
const { apiKeys } = useApiKeys();
221+
const { defaultModel } = useDefaultModel();
222+
const selectedModel = useMemo(() => defaultModel || getModelFromStorage(), [defaultModel]);
223+
const scoreSignature = useMemo(
224+
() => createScoreSignature(resume, job, selectedModel),
225+
[resume, job, selectedModel]
226+
);
141227
const [isCalculating, setIsCalculating] = useState(false);
142228
const [scoreData, setScoreData] = useState<ResumeScoreMetrics | null>(() => {
143-
// Initialize with stored score if available
144-
return getStoredScores(resume.id);
229+
return getStoredScores(resume.id, scoreSignature);
145230
});
146231

147-
// Add useEffect for initial load
148232
useEffect(() => {
149-
const storedScore = getStoredScores(resume.id);
150-
if (storedScore) {
151-
setScoreData(storedScore);
152-
}
153-
}, [resume.id]);
233+
const storedScore = getStoredScores(resume.id, scoreSignature);
234+
setScoreData(storedScore);
235+
}, [resume.id, scoreSignature]);
154236

155237
const handleRecalculate = async () => {
238+
if (!selectedModel) {
239+
toast({
240+
title: "Model required",
241+
description: "Select an AI model before generating a resume score.",
242+
variant: "destructive",
243+
});
244+
return;
245+
}
246+
156247
setIsCalculating(true);
157248
try {
158-
const MODEL_STORAGE_KEY = 'resumelm-default-model';
159-
// const LOCAL_STORAGE_KEY = 'resumelm-api-keys';
160-
161-
const selectedModel = localStorage.getItem(MODEL_STORAGE_KEY);
162-
// const storedKeys = localStorage.getItem(LOCAL_STORAGE_KEY);
163-
const apiKeys: string[] = [];
164-
165-
// Convert job type to match the expected schema
166-
const jobForScoring = job ? {
167-
...job,
168-
employment_type: job.employment_type || undefined
169-
} : null;
170-
171249
// Call the generateResumeScore action with current resume
172-
const newScore = await generateResumeScore({
173-
...resume,
174-
section_configs: undefined,
175-
section_order: undefined
176-
}, jobForScoring, {
177-
model: selectedModel || '',
178-
apiKeys: apiKeys as unknown as ApiKey[]
250+
const newScore = await generateResumeScore(getResumeForScoring(resume), getJobForScoring(job), {
251+
model: selectedModel,
252+
apiKeys,
179253
});
180254

181255
// Update state and storage
182-
setScoreData(newScore as ResumeScoreMetrics);
183-
updateStoredScores(resume.id, newScore as ResumeScoreMetrics);
256+
const scoreMetrics = newScore as ResumeScoreMetrics;
257+
setScoreData(scoreMetrics);
258+
updateStoredScores(resume.id, {
259+
score: scoreMetrics,
260+
signature: scoreSignature,
261+
generatedAt: new Date().toISOString()
262+
});
184263
} catch (error) {
185264
console.error("Error generating score:", error);
265+
const description = error instanceof Error
266+
? `${error.message} Check your model selection and API keys in Settings, then try again.`
267+
: "Failed to generate resume score. Check your model selection and API keys in Settings, then try again.";
268+
269+
toast({
270+
title: "Resume score failed",
271+
description,
272+
variant: "destructive",
273+
});
186274
} finally {
187275
setIsCalculating(false);
188276
}

src/utils/actions/resumes/actions.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,10 +469,6 @@ export async function generateResumeScore(
469469

470470
const isTailoredResume = job && !resume.is_base_resume;
471471

472-
console.log("RESUME IS", resume);
473-
console.log("JOB IS", job);
474-
console.log("IS TAILORED RESUME", isTailoredResume);
475-
476472
try {
477473
let prompt = `
478474
Generate a comprehensive score for this resume: ${JSON.stringify(resume)}

0 commit comments

Comments
 (0)