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