Compare commits

...

2 Commits

Author SHA1 Message Date
efef926e45 Fixed tab errors 2025-06-04 11:11:42 -07:00
4f7b2f3e6a Refresh maintains selected entities 2025-06-04 10:31:26 -07:00
16 changed files with 660 additions and 410 deletions

View File

@ -125,7 +125,6 @@ services:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
target: ollama target: ollama
#image: ollama
container_name: ollama container_name: ollama
restart: "always" restart: "always"
env_file: env_file:

View File

@ -24,12 +24,7 @@ import { Candidate, ChatMessage, ChatMessageBase, ChatMessageUser, ChatSession,
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { BackstoryPageProps } from './BackstoryTab'; import { BackstoryPageProps } from './BackstoryTab';
import { toCamelCase } from 'types/conversion'; import { toCamelCase } from 'types/conversion';
import { Job } from 'types/types';
interface Job {
title: string;
description: string;
}
interface JobAnalysisProps extends BackstoryPageProps { interface JobAnalysisProps extends BackstoryPageProps {
job: Job; job: Job;
@ -48,7 +43,6 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
} = props } = props
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const theme = useTheme(); const theme = useTheme();
const [jobRequirements, setJobRequirements] = useState<JobRequirements | null>(null);
const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]); const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]);
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]); const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
const [creatingSession, setCreatingSession] = useState<boolean>(false); const [creatingSession, setCreatingSession] = useState<boolean>(false);
@ -89,7 +83,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
// Fetch initial requirements // Fetch initial requirements
useEffect(() => { useEffect(() => {
if (!job.description || !requirementsSession || loadingRequirements || jobRequirements) { if (!job.description || !requirementsSession || loadingRequirements) {
return; return;
} }
@ -101,22 +95,36 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
onMessage: (msg: ChatMessage) => { onMessage: (msg: ChatMessage) => {
console.log(`onMessage: ${msg.type}`, msg); console.log(`onMessage: ${msg.type}`, msg);
if (msg.type === "response") { if (msg.type === "response") {
const incoming: any = toCamelCase<JobRequirements>(JSON.parse(msg.content || '')); const job: Job = toCamelCase<Job>(JSON.parse(msg.content || ''));
const requirements: { requirement: string, domain: string }[] = ['technicalSkills', 'experienceRequirements'].flatMap((domain) => { const requirements: { requirement: string, domain: string }[] = [];
return ['required', 'preferred'].flatMap((level) => { if (job.requirements?.technicalSkills) {
return incoming[domain][level].map((s: string) => { return { requirement: s, domain: domain }; }); job.requirements.technicalSkills.required?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (required)' }));
}) job.requirements.technicalSkills.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (preferred)' }));
}); }
['softSkills', 'experience', 'education', 'certifications', 'preferredAttributes'].forEach(domain => { if (job.requirements?.experienceRequirements) {
if (incoming[domain]) { job.requirements.experienceRequirements.required?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (required)' }));
incoming[domain].forEach((s: string) => requirements.push({ requirement: s, domain: domain })); job.requirements.experienceRequirements.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (preferred)' }));
}
if (job.requirements?.softSkills) {
job.requirements.softSkills.forEach(req => requirements.push({ requirement: req, domain: 'Soft Skills' }));
}
if (job.requirements?.experience) {
job.requirements.experience.forEach(req => requirements.push({ requirement: req, domain: 'Experience' }));
}
if (job.requirements?.education) {
job.requirements.education.forEach(req => requirements.push({ requirement: req, domain: 'Education' }));
}
if (job.requirements?.certifications) {
job.requirements.certifications.forEach(req => requirements.push({ requirement: req, domain: 'Certifications' }));
}
if (job.requirements?.preferredAttributes) {
job.requirements.preferredAttributes.forEach(req => requirements.push({ requirement: req, domain: 'Preferred Attributes' }));
} }
});
const initialSkillMatches = requirements.map(req => ({ const initialSkillMatches = requirements.map(req => ({
requirement: req.requirement, requirement: req.requirement,
domain: req.domain, domain: req.domain,
status: 'pending' as const, status: 'waiting' as const,
matchScore: 0, matchScore: 0,
assessment: '', assessment: '',
description: '', description: '',
@ -168,6 +176,12 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
// Process requirements one by one // Process requirements one by one
for (let i = 0; i < requirements.length; i++) { for (let i = 0; i < requirements.length; i++) {
try { try {
setSkillMatches(prev => {
const updated = [...prev];
updated[i] = { ...updated[i], status: 'pending' };
return updated;
});
const result: any = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i].requirement); const result: any = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i].requirement);
const skillMatch = result.skillMatch; const skillMatch = result.skillMatch;
let matchScore: number = 0; let matchScore: number = 0;
@ -177,7 +191,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
case "WEAK": matchScore = 50; break; case "WEAK": matchScore = 50; break;
case "NONE": matchScore = 0; break; case "NONE": matchScore = 0; break;
} }
if (skillMatch.evidenceStrength == "NONE" && skillMatch.citations.length > 3) { if (skillMatch.evidenceStrength == "NONE" && skillMatch.citations && skillMatch.citations.length > 3) {
matchScore = Math.min(skillMatch.citations.length * 8, 40); matchScore = Math.min(skillMatch.citations.length * 8, 40);
} }
const match: SkillMatch = { const match: SkillMatch = {
@ -234,7 +248,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
// Get icon based on status // Get icon based on status
const getStatusIcon = (status: string, score: number) => { const getStatusIcon = (status: string, score: number) => {
if (status === 'pending') return <PendingIcon />; if (status === 'pending' || status === 'waiting') return <PendingIcon />;
if (status === 'error') return <ErrorIcon color="error" />; if (status === 'error') return <ErrorIcon color="error" />;
if (score >= 70) return <CheckCircleIcon color="success" />; if (score >= 70) return <CheckCircleIcon color="success" />;
if (score >= 40) return <WarningIcon color="warning" />; if (score >= 40) return <WarningIcon color="warning" />;

View File

@ -23,52 +23,51 @@ import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext';
import { useSelectedCandidate } from 'hooks/GlobalContext'; import { useSelectedCandidate } from 'hooks/GlobalContext';
type NavigationLinkType = { type NavigationLinkType = {
name: string; label: ReactElement<any> | string;
path: string; path: string;
icon?: ReactElement<any>; icon?: ReactElement<any>;
label?: ReactElement<any>;
}; };
const DefaultNavItems: NavigationLinkType[] = [ const DefaultNavItems: NavigationLinkType[] = [
{ name: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> }, { label: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
{ name: 'Docs', path: '/docs', icon: <InfoIcon /> }, { label: 'Docs', path: '/docs', icon: <InfoIcon /> },
// { name: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> }, // { label: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
// { name: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> }, // { label: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
// { name: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> }, // { label: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
// { name: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> }, // { label: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
]; ];
const ViewerNavItems: NavigationLinkType[] = [ const ViewerNavItems: NavigationLinkType[] = [
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> }, { label: 'Chat', path: '/chat', icon: <ChatIcon /> },
]; ];
const CandidateNavItems : NavigationLinkType[]= [ const CandidateNavItems : NavigationLinkType[]= [
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> }, { label: 'Chat', path: '/chat', icon: <ChatIcon /> },
{ name: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon /> }, { label: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon /> },
{ name: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> }, { label: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> },
// { name: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> }, // { label: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> },
// { name: 'Dashboard', icon: <DashboardIcon />, path: '/candidate/dashboard' }, // { label: 'Dashboard', icon: <DashboardIcon />, path: '/candidate/dashboard' },
// { name: 'Profile', icon: <PersonIcon />, path: '/candidate/profile' }, // { label: 'Profile', icon: <PersonIcon />, path: '/candidate/profile' },
// { name: 'Backstory', icon: <HistoryIcon />, path: '/candidate/backstory' }, // { label: 'Backstory', icon: <HistoryIcon />, path: '/candidate/backstory' },
// { name: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes' }, // { label: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes' },
// { name: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/candidate/qa-setup' }, // { label: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/candidate/qa-setup' },
// { name: 'Analytics', icon: <BarChartIcon />, path: '/candidate/analytics' }, // { label: 'Analytics', icon: <BarChartIcon />, path: '/candidate/analytics' },
// { name: 'Settings', icon: <SettingsIcon />, path: '/candidate/settings' }, // { label: 'Settings', icon: <SettingsIcon />, path: '/candidate/settings' },
]; ];
const EmployerNavItems: NavigationLinkType[] = [ const EmployerNavItems: NavigationLinkType[] = [
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> }, { label: 'Chat', path: '/chat', icon: <ChatIcon /> },
{ name: 'Job Analysis', path: '/employer/job-analysis', icon: <WorkIcon /> }, { label: 'Job Analysis', path: '/employer/job-analysis', icon: <WorkIcon /> },
{ name: 'Resume Builder', path: '/employer/resume-builder', icon: <WorkIcon /> }, { label: 'Resume Builder', path: '/employer/resume-builder', icon: <WorkIcon /> },
{ name: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: <WorkIcon /> }, { label: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: <WorkIcon /> },
{ name: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> }, { label: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
// { name: 'Dashboard', icon: <DashboardIcon />, path: '/employer/dashboard' }, // { label: 'Dashboard', icon: <DashboardIcon />, path: '/employer/dashboard' },
// { name: 'Search', icon: <SearchIcon />, path: '/employer/search' }, // { label: 'Search', icon: <SearchIcon />, path: '/employer/search' },
// { name: 'Saved', icon: <BookmarkIcon />, path: '/employer/saved' }, // { label: 'Saved', icon: <BookmarkIcon />, path: '/employer/saved' },
// { name: 'Jobs', icon: <WorkIcon />, path: '/employer/jobs' }, // { label: 'Jobs', icon: <WorkIcon />, path: '/employer/jobs' },
// { name: 'Company', icon: <BusinessIcon />, path: '/employer/company' }, // { label: 'Company', icon: <BusinessIcon />, path: '/employer/company' },
// { name: 'Analytics', icon: <BarChartIcon />, path: '/employer/analytics' }, // { label: 'Analytics', icon: <BarChartIcon />, path: '/employer/analytics' },
// { name: 'Settings', icon: <SettingsIcon />, path: '/employer/settings' }, // { label: 'Settings', icon: <SettingsIcon />, path: '/employer/settings' },
]; ];
// Navigation links based on user type // Navigation links based on user type

View File

@ -170,7 +170,7 @@ interface HeaderProps {
navigationLinks: NavigationLinkType[]; navigationLinks: NavigationLinkType[];
currentPath: string; currentPath: string;
sessionId?: string | null; sessionId?: string | null;
setSnack: SetSnackType, setSnack: SetSnackType;
} }
const Header: React.FC<HeaderProps> = (props: HeaderProps) => { const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
@ -190,13 +190,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const name = (user?.firstName || user?.email || ''); const name = (user?.firstName || user?.email || '');
const navLinks : NavigationLinkType[] = [ const mainNavSections: NavigationLinkType[] = [
{name: "Home", path: "/", label: <BackstoryLogo/>}, { path: '/', label: <BackstoryLogo /> },
...navigationLinks ...navigationLinks
]; ];
// State for page navigation // State for page navigation - only for main navigation
const [ currentTab, setCurrentTab ] = useState<string>("/"); const [currentTab, setCurrentTab] = useState<string | false>("/");
const [userMenuTab, setUserMenuTab] = useState<string>(""); const [userMenuTab, setUserMenuTab] = useState<string>("");
// State for mobile drawer // State for mobile drawer
@ -206,6 +206,26 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null); const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
const userMenuOpen = Boolean(userMenuAnchor); const userMenuOpen = Boolean(userMenuAnchor);
// Helper function to determine which action page we're in
const getTailSection = (pathname: string): string | false => {
// Handle home page
if (pathname === '/') return '/';
// Split path and check against main sections
const segments = pathname.split('/').filter(Boolean);
if (segments.length === 0) return '/';
const lastSegment = `/${segments[segments.length - 1]}`;
// Check if this matches any of our main navigation sections
const matchingSection = mainNavSections.find(section =>
section.path === lastSegment ||
(section.path !== '/' && pathname.endsWith(section.path))
);
return matchingSection ? matchingSection.path : false; // Return false for routes that shouldn't show in main nav
};
// User menu items // User menu items
const userMenuItems = [ const userMenuItems = [
{ {
@ -244,15 +264,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
]; ];
useEffect(() => { useEffect(() => {
const parts = location.pathname.split('/'); const mainSection = getTailSection(location.pathname);
let tab = '/';
if (parts.length > 1) { // Only update if the section is different from current tab
tab = `/${parts[1]}`; if (mainSection !== currentTab) {
setCurrentTab(mainSection); // mainSection is either a string or false
} }
if (tab !== currentTab) { }, [location.pathname, currentTab]);
setCurrentTab(tab);
}
}, [location, currentTab]);
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => { const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setUserMenuAnchor(event.currentTarget); setUserMenuAnchor(event.currentTarget);
@ -274,27 +292,33 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
setMobileOpen(!mobileOpen); setMobileOpen(!mobileOpen);
}; };
const handleTabChange = (event: React.SyntheticEvent, newValue: string | false) => {
if (newValue !== false) {
setCurrentTab(newValue);
navigate(newValue);
}
};
// Render desktop navigation links // Render desktop navigation links
const renderNavLinks = () => { const renderNavLinks = () => {
return ( return (
<Tabs value={currentTab} onChange={(e, newValue) => setCurrentTab(newValue)} <Tabs
value={currentTab}
onChange={handleTabChange}
indicatorColor="secondary" indicatorColor="secondary"
textColor="inherit" textColor="inherit"
variant="fullWidth" variant="fullWidth"
allowScrollButtonsMobile allowScrollButtonsMobile
aria-label="Backstory navigation" aria-label="Backstory navigation"
> >
{navLinks.map((link) => ( {mainNavSections.map((section) => (
<Tab <Tab
sx={{ sx={{
minWidth: link.path === '/' ? "max-content" : "auto", minWidth: section.path === '/' ? "max-content" : "auto",
}}
key={link.name}
value={link.path}
label={link.label ? link.label : link.name}
onClick={() => {
navigate(link.path);
}} }}
key={section.path}
value={section.path}
label={section.label}
/> />
))} ))}
</Tabs> </Tabs>
@ -308,33 +332,21 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<MobileMenuTabs <MobileMenuTabs
orientation="vertical" orientation="vertical"
value={currentTab} value={currentTab}
onChange={(e, newValue) => setCurrentTab(newValue)} onChange={handleTabChange}
> >
{navLinks.map((link) => ( {mainNavSections.map((section) => (
<Tab <Tab
key={link.name} key={section.path || '/'}
value={link.path} value={section.path}
label={ label={
<MenuItemBox> <MenuItemBox>
{link.path === '/' ? ( {section.label}
<Avatar
sx={{ width: 20, height: 20 }}
variant="rounded"
alt="Backstory logo"
src="/logo192.png"
/>
) : (
link.icon && link.icon
)}
<Typography variant="body2">
{link.path === '/' ? 'Backstory' : (link.label && typeof link.label === 'object' ? link.name : (link.label || link.name))}
</Typography>
</MenuItemBox> </MenuItemBox>
} }
onClick={(e) => { onClick={(e) => {
handleDrawerToggle(); handleDrawerToggle();
setCurrentTab(link.path); setCurrentTab(section.path);
navigate(link.path); navigate(section.path);
}} }}
/> />
))} ))}
@ -458,8 +470,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
> >
<Container maxWidth="xl"> <Container maxWidth="xl">
<Toolbar disableGutters> <Toolbar disableGutters>
{/* Logo Section */}
{/* Navigation Links - Desktop */} {/* Navigation Links - Desktop */}
<NavLinksContainer> <NavLinksContainer>
{renderNavLinks()} {renderNavLinks()}
@ -507,7 +517,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
open={mobileOpen} open={mobileOpen}
onClose={handleDrawerToggle} onClose={handleDrawerToggle}
ModalProps={{ ModalProps={{
keepMounted: true, // Better open performance on mobile keepMounted: true,
}} }}
> >
{renderDrawerContent()} {renderDrawerContent()}
@ -519,6 +529,4 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
); );
}; };
export { export { Header };
Header
};

View File

@ -1,18 +1,45 @@
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'; import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
import * as Types from 'types/types'; import * as Types from 'types/types';
// Assuming you're using React Router
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from 'hooks/AuthContext';
// ============================ // ============================
// App State Interface // Storage Keys
// ============================
const STORAGE_KEYS = {
SELECTED_CANDIDATE_ID: 'selectedCandidateId',
SELECTED_JOB_ID: 'selectedJobId',
SELECTED_EMPLOYER_ID: 'selectedEmployerId',
LAST_ROUTE: 'lastVisitedRoute',
ROUTE_STATE: 'routeState',
ACTIVE_TAB: 'activeTab',
APPLIED_FILTERS: 'appliedFilters',
SIDEBAR_COLLAPSED: 'sidebarCollapsed'
} as const;
// ============================
// Route State Interface
// ============================
interface RouteState {
lastRoute: string | null;
activeTab: string | null;
appliedFilters: Record<string, any>;
sidebarCollapsed: boolean;
}
// ============================
// Enhanced App State Interface
// ============================ // ============================
export interface AppState { export interface AppState {
selectedCandidate: Types.Candidate | null; selectedCandidate: Types.Candidate | null;
selectedJob: Types.Job | null; selectedJob: Types.Job | null;
selectedEmployer: Types.Employer | null; selectedEmployer: Types.Employer | null;
// Add more global state as needed: routeState: RouteState;
// currentView: string; isInitializing: boolean;
// filters: Record<string, any>;
// searchQuery: string;
} }
export interface AppStateActions { export interface AppStateActions {
@ -20,22 +47,214 @@ export interface AppStateActions {
setSelectedJob: (job: Types.Job | null) => void; setSelectedJob: (job: Types.Job | null) => void;
setSelectedEmployer: (employer: Types.Employer | null) => void; setSelectedEmployer: (employer: Types.Employer | null) => void;
clearSelections: () => void; clearSelections: () => void;
// Future actions can be added here
// Route management
saveCurrentRoute: () => void;
restoreLastRoute: () => void;
setActiveTab: (tab: string) => void;
setFilters: (filters: Record<string, any>) => void;
setSidebarCollapsed: (collapsed: boolean) => void;
clearRouteState: () => void;
} }
export type AppStateContextType = AppState & AppStateActions; export type AppStateContextType = AppState & AppStateActions;
// ============================ // ============================
// App State Hook // Storage Utilities
// ============================
function getStoredId(key: string): string | null {
try {
return localStorage.getItem(key);
} catch (error) {
console.warn('Failed to read from localStorage:', error);
return null;
}
}
function setStoredId(key: string, id: string | null): void {
try {
if (id) {
localStorage.setItem(key, id);
} else {
localStorage.removeItem(key);
}
} catch (error) {
console.warn('Failed to write to localStorage:', error);
}
}
function getStoredObject<T>(key: string, defaultValue: T): T {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
} catch (error) {
console.warn(`Failed to read ${key} from localStorage:`, error);
return defaultValue;
}
}
function setStoredObject(key: string, value: any): void {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn(`Failed to write ${key} to localStorage:`, error);
}
}
// ============================
// Route State Management
// ============================
function getInitialRouteState(): RouteState {
return {
lastRoute: getStoredId(STORAGE_KEYS.LAST_ROUTE),
activeTab: getStoredId(STORAGE_KEYS.ACTIVE_TAB),
appliedFilters: getStoredObject(STORAGE_KEYS.APPLIED_FILTERS, {}),
sidebarCollapsed: getStoredObject(STORAGE_KEYS.SIDEBAR_COLLAPSED, false)
};
}
function persistRouteState(routeState: RouteState): void {
setStoredId(STORAGE_KEYS.LAST_ROUTE, routeState.lastRoute);
setStoredId(STORAGE_KEYS.ACTIVE_TAB, routeState.activeTab);
setStoredObject(STORAGE_KEYS.APPLIED_FILTERS, routeState.appliedFilters);
setStoredObject(STORAGE_KEYS.SIDEBAR_COLLAPSED, routeState.sidebarCollapsed);
}
// ============================
// Enhanced App State Hook
// ============================ // ============================
export function useAppStateLogic(): AppStateContextType { export function useAppStateLogic(): AppStateContextType {
const location = useLocation();
const navigate = useNavigate();
const { apiClient } = useAuth();
// Entity state
const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null); const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null);
const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null); const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null);
const [selectedEmployer, setSelectedEmployerState] = useState<Types.Employer | null>(null); const [selectedEmployer, setSelectedEmployerState] = useState<Types.Employer | null>(null);
const [isInitializing, setIsInitializing] = useState<boolean>(true);
// Route state
const [routeState, setRouteStateState] = useState<RouteState>(getInitialRouteState);
// ============================
// Initialization Effect
// ============================
useEffect(() => {
const initializeFromStorage = async () => {
setIsInitializing(true);
try {
// Get stored entity IDs
const candidateId = getStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID);
const jobId = getStoredId(STORAGE_KEYS.SELECTED_JOB_ID);
const employerId = getStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID);
// Restore entities in parallel if IDs exist
const promises: Promise<void>[] = [];
if (candidateId) {
promises.push(
(async () => {
try {
const candidate = await apiClient.getCandidate(candidateId);
if (candidate) {
setSelectedCandidateState(candidate);
console.log('Restored candidate from storage:', candidate);
} else {
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
console.log('Candidate not found, cleared from storage');
}
} catch (error) {
console.warn('Failed to restore candidate:', error);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
}
})()
);
}
if (jobId) {
promises.push(
(async () => {
try {
const job = await apiClient.getJob(jobId);
if (job) {
setSelectedJobState(job);
console.log('Restored job from storage:', job);
} else {
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
console.log('Job not found, cleared from storage');
}
} catch (error) {
console.warn('Failed to restore job:', error);
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
}
})()
);
}
if (employerId) {
promises.push(
(async () => {
try {
const employer = await apiClient.getEmployer(employerId);
if (employer) {
setSelectedEmployerState(employer);
console.log('Restored employer from storage:', employer);
} else {
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
console.log('Employer not found, cleared from storage');
}
} catch (error) {
console.warn('Failed to restore employer:', error);
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
}
})()
);
}
// Wait for all restoration attempts to complete
await Promise.all(promises);
} catch (error) {
console.error('Error during app state initialization:', error);
} finally {
setIsInitializing(false);
}
};
initializeFromStorage();
}, []);
// ============================
// Auto-save current route
// ============================
useEffect(() => {
// Don't save certain routes (login, register, etc.)
const excludedRoutes = ['/login', '/register', '/verify-email', '/reset-password'];
const shouldSaveRoute = !excludedRoutes.some(route => location.pathname.startsWith(route));
if (shouldSaveRoute && !isInitializing) {
setRouteStateState(prev => {
const newState = { ...prev, lastRoute: location.pathname };
persistRouteState(newState);
return newState;
});
}
}, [location.pathname, isInitializing]);
// ============================
// Entity State Setters with Persistence
// ============================
const setSelectedCandidate = useCallback((candidate: Types.Candidate | null) => { const setSelectedCandidate = useCallback((candidate: Types.Candidate | null) => {
setSelectedCandidateState(candidate); setSelectedCandidateState(candidate);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
if (candidate) { if (candidate) {
console.log('Selected candidate:', candidate); console.log('Selected candidate:', candidate);
@ -46,6 +265,7 @@ export function useAppStateLogic(): AppStateContextType {
const setSelectedJob = useCallback((job: Types.Job | null) => { const setSelectedJob = useCallback((job: Types.Job | null) => {
setSelectedJobState(job); setSelectedJobState(job);
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null);
if (job) { if (job) {
console.log('Selected job:', job); console.log('Selected job:', job);
@ -56,6 +276,7 @@ export function useAppStateLogic(): AppStateContextType {
const setSelectedEmployer = useCallback((employer: Types.Employer | null) => { const setSelectedEmployer = useCallback((employer: Types.Employer | null) => {
setSelectedEmployerState(employer); setSelectedEmployerState(employer);
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, employer?.id || null);
if (employer) { if (employer) {
console.log('Selected employer:', employer); console.log('Selected employer:', employer);
@ -68,17 +289,91 @@ export function useAppStateLogic(): AppStateContextType {
setSelectedCandidateState(null); setSelectedCandidateState(null);
setSelectedJobState(null); setSelectedJobState(null);
setSelectedEmployerState(null); setSelectedEmployerState(null);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
console.log('Cleared all selections'); console.log('Cleared all selections');
}, []); }, []);
// ============================
// Route State Actions
// ============================
const saveCurrentRoute = useCallback(() => {
setRouteStateState(prev => {
const newState = { ...prev, lastRoute: location.pathname };
persistRouteState(newState);
return newState;
});
}, [location.pathname]);
const restoreLastRoute = useCallback(() => {
if (routeState.lastRoute && routeState.lastRoute !== location.pathname) {
navigate(routeState.lastRoute);
}
}, [routeState.lastRoute, location.pathname, navigate]);
const setActiveTab = useCallback((tab: string) => {
setRouteStateState(prev => {
const newState = { ...prev, activeTab: tab };
persistRouteState(newState);
return newState;
});
}, []);
const setFilters = useCallback((filters: Record<string, any>) => {
setRouteStateState(prev => {
const newState = { ...prev, appliedFilters: filters };
persistRouteState(newState);
return newState;
});
}, []);
const setSidebarCollapsed = useCallback((collapsed: boolean) => {
setRouteStateState(prev => {
const newState = { ...prev, sidebarCollapsed: collapsed };
persistRouteState(newState);
return newState;
});
}, []);
const clearRouteState = useCallback(() => {
const clearedState: RouteState = {
lastRoute: null,
activeTab: null,
appliedFilters: {},
sidebarCollapsed: false
};
setRouteStateState(clearedState);
// Clear from localStorage
localStorage.removeItem(STORAGE_KEYS.LAST_ROUTE);
localStorage.removeItem(STORAGE_KEYS.ACTIVE_TAB);
localStorage.removeItem(STORAGE_KEYS.APPLIED_FILTERS);
localStorage.removeItem(STORAGE_KEYS.SIDEBAR_COLLAPSED);
console.log('Cleared all route state');
}, []);
return { return {
selectedCandidate, selectedCandidate,
selectedJob, selectedJob,
selectedEmployer, selectedEmployer,
routeState,
isInitializing,
setSelectedCandidate, setSelectedCandidate,
setSelectedJob, setSelectedJob,
setSelectedEmployer, setSelectedEmployer,
clearSelections clearSelections,
saveCurrentRoute,
restoreLastRoute,
setActiveTab,
setFilters,
setSidebarCollapsed,
clearRouteState
}; };
} }
@ -107,53 +402,45 @@ export function useAppState() {
} }
// ============================ // ============================
// Convenience Hooks for Specific State // Convenience Hooks
// ============================ // ============================
/**
* Hook specifically for candidate selection
* Useful when a component only cares about candidate state
*/
export function useSelectedCandidate() { export function useSelectedCandidate() {
const { selectedCandidate, setSelectedCandidate } = useAppState(); const { selectedCandidate, setSelectedCandidate } = useAppState();
return { selectedCandidate, setSelectedCandidate }; return { selectedCandidate, setSelectedCandidate };
} }
/**
* Hook specifically for job selection
*/
export function useSelectedJob() { export function useSelectedJob() {
const { selectedJob, setSelectedJob } = useAppState(); const { selectedJob, setSelectedJob } = useAppState();
return { selectedJob, setSelectedJob }; return { selectedJob, setSelectedJob };
} }
/**
* Hook specifically for employer selection
*/
export function useSelectedEmployer() { export function useSelectedEmployer() {
const { selectedEmployer, setSelectedEmployer } = useAppState(); const { selectedEmployer, setSelectedEmployer } = useAppState();
return { selectedEmployer, setSelectedEmployer }; return { selectedEmployer, setSelectedEmployer };
} }
// ============================ export function useRouteState() {
// Development Utilities const {
// ============================ routeState,
setActiveTab,
setFilters,
setSidebarCollapsed,
restoreLastRoute,
clearRouteState
} = useAppState();
/** return {
* Debug utility to log current app state (development only) routeState,
*/ setActiveTab,
export function useAppStateDebug() { setFilters,
const appState = useAppState(); setSidebarCollapsed,
restoreLastRoute,
useEffect(() => { clearRouteState
if (process.env.NODE_ENV === 'development') { };
console.group('🔍 App State Debug'); }
console.log('Selected Candidate:', appState.selectedCandidate);
console.log('Selected Job:', appState.selectedJob); export function useAppInitializing() {
console.log('Selected Employer:', appState.selectedEmployer); const { isInitializing } = useAppState();
console.groupEnd(); return isInitializing;
}
}, [appState.selectedCandidate, appState.selectedJob, appState.selectedEmployer]);
return appState;
} }

View File

@ -34,7 +34,6 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
} }
return result; return result;
}); });
console.log(candidates);
setCandidates(candidates); setCandidates(candidates);
} catch (err) { } catch (err) {
setSnack("" + err); setSnack("" + err);

View File

@ -46,6 +46,7 @@ import { ComingSoon } from 'components/ui/ComingSoon';
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => { const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const theme = useTheme(); const theme = useTheme();
const { user } = useAuth(); const { user } = useAuth();
const navigate = useNavigate();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate() const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
const { setSnack, submitQuery } = props; const { setSnack, submitQuery } = props;
const backstoryProps = { setSnack, submitQuery }; const backstoryProps = { setSnack, submitQuery };
@ -53,6 +54,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
const [activeStep, setActiveStep] = useState(0); const [activeStep, setActiveStep] = useState(0);
const [jobDescription, setJobDescription] = useState(''); const [jobDescription, setJobDescription] = useState('');
const [jobTitle, setJobTitle] = useState(''); const [jobTitle, setJobTitle] = useState('');
const [company, setCompany] = useState('');
const [jobLocation, setJobLocation] = useState(''); const [jobLocation, setJobLocation] = useState('');
const [analysisStarted, setAnalysisStarted] = useState(false); const [analysisStarted, setAnalysisStarted] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -60,6 +62,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const [candidates, setCandidates] = useState<Candidate[] | null>(null); const [candidates, setCandidates] = useState<Candidate[] | null>(null);
const user_type = user?.userType || 'guest';
const user_id = user?.id || '';
useEffect(() => { useEffect(() => {
if (candidates !== null || selectedCandidate) { if (candidates !== null || selectedCandidate) {
return; return;
@ -96,7 +101,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
// Steps in our process // Steps in our process
const steps = [ const steps = [
{ index: 1, label: 'Job Description', icon: <WorkIcon /> }, { index: 1, label: 'Job Selection', icon: <WorkIcon /> },
{ index: 2, label: 'AI Analysis', icon: <WorkIcon /> }, { index: 2, label: 'AI Analysis', icon: <WorkIcon /> },
{ index: 3, label: 'Generated Resume', icon: <AssessmentIcon /> } { index: 3, label: 'Generated Resume', icon: <AssessmentIcon /> }
]; ];
@ -104,124 +109,6 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
steps.unshift({ index: 0, label: 'Select Candidate', icon: <PersonIcon /> }) steps.unshift({ index: 0, label: 'Select Candidate', icon: <PersonIcon /> })
} }
const fetchMatchForRequirement = async (requirement: string): Promise<any> => {
// Create different mock responses based on the requirement
const mockResponses: Record<string, any> = {
"5+ years of React development experience": {
requirement: "5+ years of React development experience",
status: "complete",
matchScore: 85,
assessment: "The candidate demonstrates extensive React experience spanning over 6 years, with a strong portfolio of complex applications and deep understanding of React's component lifecycle and hooks.",
citations: [
{
text: "Led frontend development team of 5 engineers to rebuild our customer portal using React and TypeScript, resulting in 40% improved performance and 30% reduction in bugs.",
source: "Resume, Work Experience",
relevance: 95
},
{
text: "Developed and maintained reusable React component library used across 12 different products within the organization.",
source: "Resume, Work Experience",
relevance: 90
},
{
text: "I've been working with React since 2017, building everything from small widgets to enterprise applications.",
source: "Cover Letter",
relevance: 85
}
]
},
"Strong TypeScript skills": {
requirement: "Strong TypeScript skills",
status: "complete",
matchScore: 90,
assessment: "The candidate shows excellent TypeScript proficiency through their work history and personal projects. They have implemented complex type systems and demonstrate an understanding of advanced TypeScript features.",
citations: [
{
text: "Converted a legacy JavaScript codebase of 100,000+ lines to TypeScript, implementing strict type checking and reducing runtime errors by 70%.",
source: "Resume, Projects",
relevance: 98
},
{
text: "Created comprehensive TypeScript interfaces for our GraphQL API, ensuring type safety across the entire application stack.",
source: "Resume, Technical Skills",
relevance: 95
}
]
},
"Experience with RESTful APIs": {
requirement: "Experience with RESTful APIs",
status: "complete",
matchScore: 75,
assessment: "The candidate has good experience with RESTful APIs, having both consumed and designed them. They understand REST principles but have less documented experience with API versioning and caching strategies.",
citations: [
{
text: "Designed and implemented a RESTful API serving over 1M requests daily with a focus on performance and scalability.",
source: "Resume, Technical Projects",
relevance: 85
},
{
text: "Worked extensively with third-party APIs including Stripe, Twilio, and Salesforce to integrate payment processing and communication features.",
source: "Resume, Work Experience",
relevance: 70
}
]
},
"Knowledge of state management solutions (Redux, Context API)": {
requirement: "Knowledge of state management solutions (Redux, Context API)",
status: "complete",
matchScore: 65,
assessment: "The candidate has moderate experience with state management, primarily using Redux. There is less evidence of Context API usage, which could indicate a knowledge gap in more modern React state management approaches.",
citations: [
{
text: "Implemented Redux for global state management in an e-commerce application, handling complex state logic for cart, user preferences, and product filtering.",
source: "Resume, Skills",
relevance: 80
},
{
text: "My experience includes working with state management libraries like Redux and MobX.",
source: "Cover Letter",
relevance: 60
}
]
},
"Experience with CI/CD pipelines": {
requirement: "Experience with CI/CD pipelines",
status: "complete",
matchScore: 40,
assessment: "The candidate shows limited experience with CI/CD pipelines. While they mention some exposure to Jenkins and GitLab CI, there is insufficient evidence of setting up or maintaining comprehensive CI/CD workflows.",
citations: [
{
text: "Familiar with CI/CD tools including Jenkins and GitLab CI.",
source: "Resume, Skills",
relevance: 40
}
]
},
"Cloud platform experience (AWS, Azure, GCP)": {
requirement: "Cloud platform experience (AWS, Azure, GCP)",
status: "complete",
matchScore: 30,
assessment: "The candidate demonstrates minimal experience with cloud platforms. There is a brief mention of AWS S3 and Lambda, but no substantial evidence of deeper cloud architecture knowledge or experience with Azure or GCP.",
citations: [
{
text: "Used AWS S3 for file storage and Lambda for image processing in a photo sharing application.",
source: "Resume, Projects",
relevance: 35
}
]
}
};
// Return a promise that resolves with the mock data after a delay
return new Promise((resolve) => {
// Different requirements resolve at different speeds to simulate real-world analysis
const delay = Math.random() * 5000 + 2000; // 2-7 seconds
setTimeout(() => {
resolve(mockResponses[requirement]);
}, delay);
});
};
// Navigation handlers // Navigation handlers
const handleNext = () => { const handleNext = () => {
if (activeStep === 0 && !selectedCandidate) { if (activeStep === 0 && !selectedCandidate) {
@ -230,7 +117,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
} }
if (activeStep === 1) { if (activeStep === 1) {
if ((/*(extraInfo && !jobTitle) || */!jobDescription)) { if (!jobDescription) {
setError('Please provide job description before continuing.'); setError('Please provide job description before continuing.');
return; return;
} }
@ -338,12 +225,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
</Paper> </Paper>
); );
const extraInfo = false;
// Render function for the job description step // Render function for the job description step
const renderJobDescription = () => ( const renderJobDescription = () => (
<Paper elevation={3} sx={{ p: 3, mt: 3, mb: 4, borderRadius: 2 }}> <Paper elevation={3} sx={{ p: 3, mt: 3, mb: 4, borderRadius: 2 }}>
{extraInfo && <>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
Enter Job Details Enter Job Details
</Typography> </Typography>
@ -361,6 +245,18 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
fullWidth
label="Company"
variant="outlined"
value={company}
onChange={(e) => setCompany(e.target.value)}
required
margin="normal"
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<TextField <TextField
fullWidth fullWidth
@ -372,22 +268,20 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
/> />
</Grid> </Grid>
</Grid> </Grid>
</>
}
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2, mb: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', mt: 2, mb: 1 }}>
<Typography variant="subtitle1" sx={{ mr: 2 }}> <Typography variant="subtitle1" sx={{ mr: 2 }}>
Job Description Job Selection
</Typography> </Typography>
{extraInfo && <Button <Button
variant="outlined" variant="outlined"
startIcon={<FileUploadIcon />} startIcon={<FileUploadIcon />}
size="small" size="small"
onClick={() => setOpenUploadDialog(true)} onClick={() => setOpenUploadDialog(true)}
> >
Upload Upload
</Button>} </Button>
</Box> </Box>
<TextField <TextField
@ -419,7 +313,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
{selectedCandidate && ( {selectedCandidate && (
<JobMatchAnalysis <JobMatchAnalysis
job={{ title: jobTitle, description: jobDescription }} job={{ title: jobTitle, description: jobDescription, company: company, ownerId: user_id, ownerType: user_type }}
candidate={selectedCandidate} candidate={selectedCandidate}
{...backstoryProps} {...backstoryProps}
/> />
@ -434,14 +328,14 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
); );
// If no user is logged in, show message // If no user is logged in, show message
if (!user) { if (!user?.id) {
return ( return (
<Container maxWidth="md"> <Container maxWidth="md">
<Paper elevation={3} sx={{ p: 4, mt: 4, textAlign: 'center' }}> <Paper elevation={3} sx={{ p: 4, mt: 4, textAlign: 'center' }}>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
Please log in to access candidate analysis Please log in to access candidate analysis
</Typography> </Typography>
<Button variant="contained" color="primary" sx={{ mt: 2 }}> <Button variant="contained" onClick={() => { navigate('/login'); }} color="primary" sx={{ mt: 2 }}>
Log In Log In
</Button> </Button>
</Paper> </Paper>

View File

@ -24,9 +24,9 @@ const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProp
if (candidate?.username === username || !username) { if (candidate?.username === username || !username) {
return; return;
} }
const getCandidate = async (username: string) => { const getCandidate = async (reference: string) => {
try { try {
const result : Candidate = await apiClient.getCandidate(username); const result: Candidate = await apiClient.getCandidate(reference);
setCandidate(result); setCandidate(result);
navigate('/chat'); navigate('/chat');
} catch { } catch {

View File

@ -506,8 +506,9 @@ class ApiClient {
// Candidate Methods with Date Conversion // Candidate Methods with Date Conversion
// ============================ // ============================
async getCandidate(username: string): Promise<Types.Candidate> { // reference can be candidateId, username, or email
const response = await fetch(`${this.baseUrl}/candidates/${username}`, { async getCandidate(reference: string): Promise<Types.Candidate> {
const response = await fetch(`${this.baseUrl}/candidates/${reference}`, {
headers: this.defaultHeaders headers: this.defaultHeaders
}); });

View File

@ -1,6 +1,6 @@
// Generated TypeScript types from Pydantic models // Generated TypeScript types from Pydantic models
// Source: src/backend/models.py // Source: src/backend/models.py
// Generated on: 2025-06-04T05:16:43.020718 // Generated on: 2025-06-04T17:02:08.242818
// DO NOT EDIT MANUALLY - This file is auto-generated // DO NOT EDIT MANUALLY - This file is auto-generated
// ============================ // ============================
@ -577,26 +577,13 @@ export interface InterviewSchedule {
export interface Job { export interface Job {
id?: string; id?: string;
title: string; ownerId: string;
ownerType: "candidate" | "employer" | "guest";
title?: string;
summary?: string;
company?: string;
description: string; description: string;
responsibilities: Array<string>; requirements?: JobRequirements;
requirements: Array<string>;
preferredSkills?: Array<string>;
employerId: string;
location: Location;
salaryRange?: SalaryRange;
employmentType: "full-time" | "part-time" | "contract" | "internship" | "freelance";
datePosted: Date;
applicationDeadline?: Date;
isActive: boolean;
applicants?: Array<JobApplication>;
department?: string;
reportsTo?: string;
benefits?: Array<string>;
visaSponsorship?: boolean;
featuredUntil?: Date;
views: number;
applicationCount: number;
} }
export interface JobApplication { export interface JobApplication {
@ -615,6 +602,31 @@ export interface JobApplication {
decision?: ApplicationDecision; decision?: ApplicationDecision;
} }
export interface JobFull {
id?: string;
ownerId: string;
ownerType: "candidate" | "employer" | "guest";
title?: string;
summary?: string;
company?: string;
description: string;
requirements?: JobRequirements;
location: Location;
salaryRange?: SalaryRange;
employmentType: "full-time" | "part-time" | "contract" | "internship" | "freelance";
datePosted: Date;
applicationDeadline?: Date;
isActive: boolean;
applicants?: Array<JobApplication>;
department?: string;
reportsTo?: string;
benefits?: Array<string>;
visaSponsorship?: boolean;
featuredUntil?: Date;
views: number;
applicationCount: number;
}
export interface JobListResponse { export interface JobListResponse {
success: boolean; success: boolean;
data?: Array<Job>; data?: Array<Job>;
@ -1224,23 +1236,6 @@ export function convertInterviewScheduleFromApi(data: any): InterviewSchedule {
endDate: new Date(data.endDate), endDate: new Date(data.endDate),
}; };
} }
/**
* Convert Job from API response, parsing date fields
* Date fields: datePosted, applicationDeadline, featuredUntil
*/
export function convertJobFromApi(data: any): Job {
if (!data) return data;
return {
...data,
// Convert datePosted from ISO string to Date
datePosted: new Date(data.datePosted),
// Convert applicationDeadline from ISO string to Date
applicationDeadline: data.applicationDeadline ? new Date(data.applicationDeadline) : undefined,
// Convert featuredUntil from ISO string to Date
featuredUntil: data.featuredUntil ? new Date(data.featuredUntil) : undefined,
};
}
/** /**
* Convert JobApplication from API response, parsing date fields * Convert JobApplication from API response, parsing date fields
* Date fields: appliedDate, updatedDate * Date fields: appliedDate, updatedDate
@ -1256,6 +1251,23 @@ export function convertJobApplicationFromApi(data: any): JobApplication {
updatedDate: new Date(data.updatedDate), updatedDate: new Date(data.updatedDate),
}; };
} }
/**
* Convert JobFull from API response, parsing date fields
* Date fields: datePosted, applicationDeadline, featuredUntil
*/
export function convertJobFullFromApi(data: any): JobFull {
if (!data) return data;
return {
...data,
// Convert datePosted from ISO string to Date
datePosted: new Date(data.datePosted),
// Convert applicationDeadline from ISO string to Date
applicationDeadline: data.applicationDeadline ? new Date(data.applicationDeadline) : undefined,
// Convert featuredUntil from ISO string to Date
featuredUntil: data.featuredUntil ? new Date(data.featuredUntil) : undefined,
};
}
/** /**
* Convert MessageReaction from API response, parsing date fields * Convert MessageReaction from API response, parsing date fields
* Date fields: timestamp * Date fields: timestamp
@ -1378,10 +1390,10 @@ export function convertFromApi<T>(data: any, modelType: string): T {
return convertInterviewFeedbackFromApi(data) as T; return convertInterviewFeedbackFromApi(data) as T;
case 'InterviewSchedule': case 'InterviewSchedule':
return convertInterviewScheduleFromApi(data) as T; return convertInterviewScheduleFromApi(data) as T;
case 'Job':
return convertJobFromApi(data) as T;
case 'JobApplication': case 'JobApplication':
return convertJobApplicationFromApi(data) as T; return convertJobApplicationFromApi(data) as T;
case 'JobFull':
return convertJobFullFromApi(data) as T;
case 'MessageReaction': case 'MessageReaction':
return convertMessageReactionFromApi(data) as T; return convertMessageReactionFromApi(data) as T;
case 'RAGConfiguration': case 'RAGConfiguration':

View File

@ -25,7 +25,7 @@ from models import Candidate
_agents: List[Agent] = [] _agents: List[Agent] = []
def get_or_create_agent(agent_type: str, prometheus_collector: CollectorRegistry, user: Optional[Candidate]=None, **kwargs) -> Agent: def get_or_create_agent(agent_type: str, prometheus_collector: CollectorRegistry, user: Optional[Candidate]=None) -> Agent:
""" """
Get or create and append a new agent of the specified type, ensuring only one agent per type exists. Get or create and append a new agent of the specified type, ensuring only one agent per type exists.
@ -39,7 +39,8 @@ def get_or_create_agent(agent_type: str, prometheus_collector: CollectorRegistry
Raises: Raises:
ValueError: If no matching agent type is found or if a agent of this type already exists. ValueError: If no matching agent type is found or if a agent of this type already exists.
""" """
# Check if a agent with the given agent_type already exists # Check if a global (non-user) agent with the given agent_type already exists
if not user:
for agent in _agents: for agent in _agents:
if agent.agent_type == agent_type: if agent.agent_type == agent_type:
return agent return agent
@ -48,7 +49,7 @@ def get_or_create_agent(agent_type: str, prometheus_collector: CollectorRegistry
for agent_cls in Agent.__subclasses__(): for agent_cls in Agent.__subclasses__():
if agent_cls.model_fields["agent_type"].default == agent_type: if agent_cls.model_fields["agent_type"].default == agent_type:
# Create the agent instance with provided kwargs # Create the agent instance with provided kwargs
agent = agent_cls(agent_type=agent_type, user=user, prometheus_collector=prometheus_collector, **kwargs) agent = agent_cls(agent_type=agent_type, user=user, prometheus_collector=prometheus_collector)
# if agent.agent_persist: # If an agent is not set to persist, do not add it to the list # if agent.agent_persist: # If an agent is not set to persist, do not add it to the list
_agents.append(agent) _agents.append(agent)
return agent return agent

View File

@ -40,13 +40,17 @@ class JobRequirementsAgent(Agent):
## INSTRUCTIONS: ## INSTRUCTIONS:
1. Analyze ONLY the job description provided. 1. Analyze ONLY the job description provided.
2. Extract and categorize all requirements and preferences. 2. Extract company information, job title, and all requirements.
3. DO NOT consider any candidate information - this is a pure job analysis task. 3. Extract and categorize all requirements and preferences.
4. DO NOT consider any candidate information - this is a pure job analysis task.
## OUTPUT FORMAT: ## OUTPUT FORMAT:
```json ```json
{ {
"company_name": "Company Name",
"job_title": "Job Title",
"job_summary": "Brief summary of the job",
"job_requirements": { "job_requirements": {
"technical_skills": { "technical_skills": {
"required": ["skill1", "skill2"], "required": ["skill1", "skill2"],
@ -133,9 +137,15 @@ class JobRequirementsAgent(Agent):
json_str = self.extract_json_from_text(generated_message.content) json_str = self.extract_json_from_text(generated_message.content)
job_requirements : JobRequirements | None = None job_requirements : JobRequirements | None = None
job_requirements_data = "" job_requirements_data = ""
company_name = ""
job_summary = ""
job_title = ""
try: try:
job_requirements_data = json.loads(json_str) job_requirements_data = json.loads(json_str)
job_requirements_data = job_requirements_data.get("job_requirements", None) job_requirements_data = job_requirements_data.get("job_requirements", None)
job_title = job_requirements_data.get("job_title", "")
company_name = job_requirements_data.get("company_name", "")
job_summary = job_requirements_data.get("job_summary", "")
job_requirements = JobRequirements.model_validate(job_requirements_data) job_requirements = JobRequirements.model_validate(job_requirements_data)
if not job_requirements: if not job_requirements:
raise ValueError("Job requirements data is empty or invalid.") raise ValueError("Job requirements data is empty or invalid.")
@ -160,7 +170,13 @@ class JobRequirementsAgent(Agent):
return return
status_message.status = ChatStatusType.DONE status_message.status = ChatStatusType.DONE
status_message.type = ChatMessageType.RESPONSE status_message.type = ChatMessageType.RESPONSE
status_message.content = json.dumps(job_requirements.model_dump(mode="json", exclude_unset=True)) job_data = {
"company": company_name,
"title": job_title,
"summary": job_summary,
"requirements": job_requirements.model_dump(mode="json", exclude_unset=True)
}
status_message.content = json.dumps(job_data)
yield status_message yield status_message
logger.info(f"✅ Job requirements analysis completed successfully.") logger.info(f"✅ Job requirements analysis completed successfully.")

View File

@ -168,7 +168,7 @@ JSON RESPONSE:"""
user_message.content = prompt user_message.content = prompt
skill_assessment = None skill_assessment = None
async for skill_assessment in self.llm_one_shot(llm=llm, model=model, user_message=user_message, system_prompt=system_prompt, temperature=0.1): async for skill_assessment in self.llm_one_shot(llm=llm, model=model, user_message=user_message, system_prompt=system_prompt, temperature=0.7):
if skill_assessment.status == ChatStatusType.ERROR: if skill_assessment.status == ChatStatusType.ERROR:
status_message.status = ChatStatusType.ERROR status_message.status = ChatStatusType.ERROR
status_message.content = skill_assessment.content status_message.content = skill_assessment.content

View File

@ -20,11 +20,13 @@ from logger import logger
import agents as agents import agents as agents
from models import (Tunables, CandidateQuestion, ChatMessageUser, ChatMessage, RagEntry, ChatMessageType, ChatMessageMetaData, ChatStatusType, Candidate, ChatContextType) from models import (Tunables, CandidateQuestion, ChatMessageUser, ChatMessage, RagEntry, ChatMessageType, ChatMessageMetaData, ChatStatusType, Candidate, ChatContextType)
from llm_manager import llm_manager from llm_manager import llm_manager
from agents.base import Agent
class CandidateEntity(Candidate): class CandidateEntity(Candidate):
model_config = {"arbitrary_types_allowed": True} # Allow ChromaDBFileWatcher, etc model_config = {"arbitrary_types_allowed": True} # Allow ChromaDBFileWatcher, etc
# Internal instance members # Internal instance members
CandidateEntity__agents: List[Agent] = []
CandidateEntity__observer: Optional[Any] = Field(default=None, exclude=True) CandidateEntity__observer: Optional[Any] = Field(default=None, exclude=True)
CandidateEntity__file_watcher: Optional[ChromaDBFileWatcher] = Field(default=None, exclude=True) CandidateEntity__file_watcher: Optional[ChromaDBFileWatcher] = Field(default=None, exclude=True)
CandidateEntity__prometheus_collector: Optional[CollectorRegistry] = Field( CandidateEntity__prometheus_collector: Optional[CollectorRegistry] = Field(
@ -63,7 +65,7 @@ class CandidateEntity(Candidate):
# Check if file exists # Check if file exists
return user_info_path.is_file() return user_info_path.is_file()
def get_or_create_agent(self, agent_type: ChatContextType, **kwargs) -> agents.Agent: def get_or_create_agent(self, agent_type: ChatContextType) -> agents.Agent:
""" """
Get or create an agent of the specified type for this candidate. Get or create an agent of the specified type for this candidate.
@ -74,11 +76,17 @@ class CandidateEntity(Candidate):
Returns: Returns:
The created agent instance. The created agent instance.
""" """
# Only instantiate one agent of each type per user
for agent in self.CandidateEntity__agents:
if agent.agent_type == agent_type:
return agent
return agents.get_or_create_agent( return agents.get_or_create_agent(
agent_type=agent_type, agent_type=agent_type,
user=self, user=self,
prometheus_collector=self.prometheus_collector, prometheus_collector=self.prometheus_collector
**kwargs) )
# Wrapper properties that map into file_watcher # Wrapper properties that map into file_watcher
@property @property

View File

@ -64,13 +64,13 @@ import agents
# ============================= # =============================
from models import ( from models import (
# API # API
LoginRequest, CreateCandidateRequest, CreateEmployerRequest, Job, LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
# User models # User models
Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse, CandidateAI, Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse, CandidateAI,
# Job models # Job models
Job, JobApplication, ApplicationStatus, JobFull, JobApplication, ApplicationStatus,
# Chat models # Chat models
ChatSession, ChatMessage, ChatContext, ChatQuery, ChatStatusType, ChatMessageBase, ChatMessageUser, ChatSenderType, ChatMessageType, ChatContextType, ChatSession, ChatMessage, ChatContext, ChatQuery, ChatStatusType, ChatMessageBase, ChatMessageUser, ChatSenderType, ChatMessageType, ChatContextType,
@ -2609,28 +2609,24 @@ async def confirm_password_reset(
# ============================ # ============================
@api_router.post("/jobs") @api_router.post("/jobs")
async def create_job( async def create_candidate_job(
job_data: Dict[str, Any] = Body(...), job_data: Dict[str, Any] = Body(...),
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
database: RedisDatabase = Depends(get_database) database: RedisDatabase = Depends(get_database)
): ):
"""Create a new job""" """Create a new job"""
is_employer = isinstance(current_user, Employer)
try: try:
# Verify user is an employer if is_employer:
if not isinstance(current_user, Employer): job = JobFull.model_validate(job_data)
return JSONResponse( else:
status_code=403, job = Job.model_validate(job_data)
content=create_error_response("FORBIDDEN", "Only employers can create jobs")
)
# Add required fields # Add required fields
job_data["id"] = str(uuid.uuid4()) job.id = str(uuid.uuid4())
job_data["datePosted"] = datetime.now(UTC).isoformat() job.owner_id = current_user.id
job_data["views"] = 0
job_data["applicationCount"] = 0
job_data["employerId"] = current_user.id
job = Job.model_validate(job_data)
await database.set_job(job.id, job.model_dump()) await database.set_job(job.id, job.model_dump())
return create_success_response(job.model_dump(by_alias=True, exclude_unset=True)) return create_success_response(job.model_dump(by_alias=True, exclude_unset=True))
@ -2687,7 +2683,12 @@ async def get_jobs(
# Get all jobs from Redis # Get all jobs from Redis
all_jobs_data = await database.get_all_jobs() all_jobs_data = await database.get_all_jobs()
jobs_list = [Job.model_validate(data) for data in all_jobs_data.values() if data.get("is_active", True)] jobs_list = []
for job in all_jobs_data.values():
if job.get("user_type") == "employer":
jobs_list.append(JobFull.model_validate(job))
else:
jobs_list.append(Job.model_validate(job))
paginated_jobs, total = filter_and_paginate( paginated_jobs, total = filter_and_paginate(
jobs_list, page, limit, sortBy, sortOrder, filter_dict jobs_list, page, limit, sortBy, sortOrder, filter_dict
@ -2822,33 +2823,40 @@ async def post_candidate_rag_search(
content=create_error_response("SUMMARY_ERROR", str(e)) content=create_error_response("SUMMARY_ERROR", str(e))
) )
@api_router.get("/candidates/{username}") # reference can be candidateId, username, or email
@api_router.get("/candidates/{reference}")
async def get_candidate( async def get_candidate(
username: str = Path(...), reference: str = Path(...),
database: RedisDatabase = Depends(get_database) database: RedisDatabase = Depends(get_database)
): ):
"""Get a candidate by username""" """Get a candidate by username"""
try: try:
# Normalize reference to lowercase for case-insensitive search
query_lower = reference.lower()
all_candidates_data = await database.get_all_candidates() all_candidates_data = await database.get_all_candidates()
candidates_list = [Candidate.model_validate(data) for data in all_candidates_data.values()] if not all_candidates_data:
logger.warning(f"⚠️ No candidates found in database")
return JSONResponse(
status_code=404,
content=create_error_response("NOT_FOUND", "No candidates found")
)
# Normalize username to lowercase for case-insensitive search candidate_data = None
query_lower = username.lower() for candidate in all_candidates_data.values():
if (candidate.get("id", "").lower() == query_lower or
candidate.get("username", "").lower() == query_lower or
candidate.get("email", "").lower() == query_lower):
candidate_data = candidate
break
# Filter by search query if not candidate_data:
candidates_list = [ logger.warning(f"⚠️ Candidate not found for reference: {reference}")
c for c in candidates_list
if (query_lower == c.email.lower() or
query_lower == c.username.lower())
]
if not len(candidates_list):
return JSONResponse( return JSONResponse(
status_code=404, status_code=404,
content=create_error_response("NOT_FOUND", "Candidate not found") content=create_error_response("NOT_FOUND", "Candidate not found")
) )
candidate_data = candidates_list[0]
candidate = Candidate.model_validate(candidate_data) if not candidate_data.get("is_AI") else CandidateAI.model_validate(candidate_data) candidate = Candidate.model_validate(candidate_data) if not candidate_data.get("is_AI") else CandidateAI.model_validate(candidate_data)
return create_success_response(candidate.model_dump(by_alias=True, exclude_unset=True)) return create_success_response(candidate.model_dump(by_alias=True, exclude_unset=True))
@ -3368,8 +3376,8 @@ async def get_candidate_skill_match(
candidate = Candidate.model_validate(candidate_data) candidate = Candidate.model_validate(candidate_data)
logger.info(f"🔍 Running skill match for candidate {candidate.id} against requirement: {requirement}")
async with entities.get_candidate_entity(candidate=candidate) as candidate_entity: async with entities.get_candidate_entity(candidate=candidate) as candidate_entity:
logger.info(f"🔍 Running skill match for candidate {candidate_entity.username} against requirement: {requirement}")
agent = candidate_entity.get_or_create_agent(agent_type=ChatContextType.SKILL_MATCH) agent = candidate_entity.get_or_create_agent(agent_type=ChatContextType.SKILL_MATCH)
if not agent: if not agent:
return JSONResponse( return JSONResponse(

View File

@ -108,17 +108,6 @@ class SkillMatch(BaseModel):
"populate_by_name": True # Allow both field names and aliases "populate_by_name": True # Allow both field names and aliases
} }
class JobRequirements(BaseModel):
technical_skills: Requirements = Field(..., alias="technicalSkills")
experience_requirements: Requirements = Field(..., alias="experienceRequirements")
soft_skills: Optional[List[str]] = Field(default_factory=list, alias="softSkills")
experience: Optional[List[str]] = []
education: Optional[List[str]] = []
certifications: Optional[List[str]] = []
preferred_attributes: Optional[List[str]] = Field(None, alias="preferredAttributes")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class ChatMessageType(str, Enum): class ChatMessageType(str, Enum):
ERROR = "error" ERROR = "error"
@ -650,14 +639,32 @@ class AuthResponse(BaseModel):
"populate_by_name": True # Allow both field names and aliases "populate_by_name": True # Allow both field names and aliases
} }
class JobRequirements(BaseModel):
technical_skills: Requirements = Field(..., alias="technicalSkills")
experience_requirements: Requirements = Field(..., alias="experienceRequirements")
soft_skills: Optional[List[str]] = Field(default_factory=list, alias="softSkills")
experience: Optional[List[str]] = []
education: Optional[List[str]] = []
certifications: Optional[List[str]] = []
preferred_attributes: Optional[List[str]] = Field(None, alias="preferredAttributes")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class Job(BaseModel): class Job(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4())) id: str = Field(default_factory=lambda: str(uuid.uuid4()))
title: str owner_id: str = Field(..., alias="ownerId")
owner_type: UserType = Field(..., alias="ownerType")
title: Optional[str]
summary: Optional[str]
company: Optional[str]
description: str description: str
responsibilities: List[str] requirements: Optional[JobRequirements]
requirements: List[str] model_config = {
preferred_skills: Optional[List[str]] = Field(None, alias="preferredSkills") "populate_by_name": True # Allow both field names and aliases
employer_id: str = Field(..., alias="employerId") }
class JobFull(Job):
location: Location location: Location
salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange") salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange")
employment_type: EmploymentType = Field(..., alias="employmentType") employment_type: EmploymentType = Field(..., alias="employmentType")
@ -672,9 +679,6 @@ class Job(BaseModel):
featured_until: Optional[datetime] = Field(None, alias="featuredUntil") featured_until: Optional[datetime] = Field(None, alias="featuredUntil")
views: int = 0 views: int = 0
application_count: int = Field(0, alias="applicationCount") application_count: int = Field(0, alias="applicationCount")
model_config = {
"populate_by_name": True # Allow both field names and aliases
}
class InterviewFeedback(BaseModel): class InterviewFeedback(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4())) id: str = Field(default_factory=lambda: str(uuid.uuid4()))
@ -1061,4 +1065,4 @@ Candidate.update_forward_refs()
Employer.update_forward_refs() Employer.update_forward_refs()
ChatSession.update_forward_refs() ChatSession.update_forward_refs()
JobApplication.update_forward_refs() JobApplication.update_forward_refs()
Job.update_forward_refs() JobFull.update_forward_refs()