From 4a7a72812f2e0f9fb0c693348787fb0abfb1eff4 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Tue, 8 Jul 2025 13:48:47 -0700 Subject: [PATCH] Do not use cached skills if content updated Add candidate/job route to job-analysis --- Dockerfile | 3 ++ frontend/src/config/navigationConfig.tsx | 2 +- frontend/src/pages/JobAnalysisPage.tsx | 68 ++++++++++++++++++++++++ src/backend/routes/candidates.py | 18 +++++++ 4 files changed, 90 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7a67dc6..bcdb5f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -199,6 +199,9 @@ RUN pip install openapi-python-client # QR code generator RUN pip install pyqrcode pypng +# Anthropic and other backends +RUN pip install anthropic pydantic_ai + # Automatic type conversion pydantic -> typescript RUN pip install pydantic typing-inspect jinja2 RUN apt-get update \ diff --git a/frontend/src/config/navigationConfig.tsx b/frontend/src/config/navigationConfig.tsx index 2f0d0ec..e21b1fd 100644 --- a/frontend/src/config/navigationConfig.tsx +++ b/frontend/src/config/navigationConfig.tsx @@ -52,7 +52,7 @@ export const navigationConfig: NavigationConfig = { { id: 'job-analysis', label: 'Job Analysis', - path: '/job-analysis', + path: '/job-analysis/:candidateId?/:jobId?/:stepId?', variant: 'fullWidth', icon: , component: , diff --git a/frontend/src/pages/JobAnalysisPage.tsx b/frontend/src/pages/JobAnalysisPage.tsx index 49c1723..49f83aa 100644 --- a/frontend/src/pages/JobAnalysisPage.tsx +++ b/frontend/src/pages/JobAnalysisPage.tsx @@ -29,6 +29,7 @@ import { JobCreator } from 'components/JobCreator'; import { LoginRestricted } from 'components/ui/LoginRestricted'; import { ResumeGenerator } from 'components/ResumeGenerator'; import { JobsView } from 'components/ui/JobsView'; +import { Navigate, useNavigate, useParams } from 'react-router-dom'; function WorkAddIcon(): JSX.Element { return ( @@ -78,7 +79,13 @@ const capitalize = (str: string): string => { // Main component const JobAnalysisPage: React.FC = () => { const theme = useTheme(); + const navigate = useNavigate(); const { user, guest, apiClient } = useAuth(); + const { candidateId, jobId, stepId } = useParams<{ + candidateId?: string; + jobId?: string; + stepId?: string; + }>(); const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate(); const { selectedJob, setSelectedJob } = useSelectedJob(); @@ -95,6 +102,61 @@ const JobAnalysisPage: React.FC = () => { const [activeStep, setActiveStep] = useState(user === null ? 0 : 1); const maxStep = 4; + useEffect(() => { + if (jobId && candidateId && stepId && activeStep !== parseFloat(stepId)) { + setActiveStep(parseFloat(stepId)); + } + }, [jobId, candidateId, activeStep, stepId]); + + useEffect(() => { + let routeCandidateId = candidateId || ''; + let routeJobId = jobId || ''; + const routeStepId = stepId ? `/${stepId}` : ''; + + if (routeCandidateId && routeCandidateId !== selectedCandidate?.id) { + apiClient + .getCandidate(routeCandidateId) + .then((candidate: Candidate | null) => { + if (candidate) { + setSelectedCandidate(candidate); + routeCandidateId = candidate.id || ''; + } else { + setError('Candidate not found.'); + } + }) + .catch((err: Error) => { + console.error('Error fetching candidate:', err); + setError('Failed to fetch candidate information.'); + }); + } else { + routeCandidateId = selectedCandidate?.id || ''; + } + + if (routeJobId && routeJobId !== selectedJob?.id) { + apiClient + .getJob(routeJobId) + .then((job: Job | null) => { + if (job) { + setSelectedJob(job); + routeJobId = job.id || ''; + } else { + setError('Job not found.'); + } + }) + .catch((err: Error) => { + console.error('Error fetching job:', err); + setError('Failed to fetch job information.'); + }); + } else { + routeJobId = selectedJob?.id || ''; + } + if (routeCandidateId && routeJobId) { + navigate(`/job-analysis/${routeCandidateId}/${routeJobId}${routeStepId}`, { + replace: true, + }); + } + }, [candidateId, jobId, setSelectedCandidate, setSelectedJob]); + useEffect(() => { if (selectedCandidate === null && user !== null) { apiClient @@ -209,6 +271,12 @@ const JobAnalysisPage: React.FC = () => { return; } setActiveStep(step); + if (selectedCandidate?.id && selectedJob?.id) { + const routeStep = step ? `/${step.toString()}` : ''; + navigate(`/job-analysis/${selectedCandidate.id}/${selectedJob.id}${routeStep}`, { + replace: true, + }); + } }; const onCandidateSelect = (candidate: Candidate): void => { diff --git a/src/backend/routes/candidates.py b/src/backend/routes/candidates.py index e7edd40..1dec917 100644 --- a/src/backend/routes/candidates.py +++ b/src/backend/routes/candidates.py @@ -1482,6 +1482,24 @@ async def post_job_analysis( continue cache_key = get_skill_cache_key(candidate.id, skill) assessment: SkillAssessment | None = await database.get_cached_skill_match(cache_key) + # Determine if we need to regenerate the assessment + if assessment: + # Get the latest RAG data update time for the current user + user_rag_update_time = await database.get_user_rag_update_time(candidate.id) + + updated = assessment.updated_at + # Check if cached result is still valid + # Regenerate if user's RAG data was updated after cache date + if user_rag_update_time and user_rag_update_time >= updated: + logger.info(f"🔄 Out-of-date cached entry for {candidate.username} skill {assessment.skill}") + assessment = None + else: + logger.info( + f"✅ Using cached skill match for {candidate.username} skill {assessment.skill}: {cache_key}" + ) + else: + logger.info(f"💾 No cached skill match data: {cache_key}, {candidate.id}, {skill}") + if not assessment: logger.info(f"💾 No cached skill match data: {cache_key}, {candidate.id}, {skill}") continue