@@ -7,10 +7,11 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
77import { Button } from "@/components/ui/button" ;
88import { RefreshCw , TrendingUp , Target , Award } from "lucide-react" ;
99import { cn } from "@/lib/utils" ;
10- import { useState , useEffect } from "react" ;
10+ import { useState , useEffect , useMemo } from "react" ;
1111import { generateResumeScore } from "@/utils/actions/resumes/actions" ;
1212import { 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
1516export interface ResumeScoreMetrics {
1617 overallScore : {
@@ -95,8 +96,15 @@ interface ResumeScorePanelProps {
9596}
9697
9798const LOCAL_STORAGE_KEY = 'resumelm-resume-scores' ;
99+ const MODEL_STORAGE_KEY = 'resumelm-default-model' ;
98100const 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
101109function 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
140219export 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 }
0 commit comments