Compare commits
2 Commits
a912e4d24c
...
efef926e45
Author | SHA1 | Date | |
---|---|---|---|
efef926e45 | |||
4f7b2f3e6a |
@ -125,7 +125,6 @@ services:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: ollama
|
||||
#image: ollama
|
||||
container_name: ollama
|
||||
restart: "always"
|
||||
env_file:
|
||||
|
@ -24,12 +24,7 @@ import { Candidate, ChatMessage, ChatMessageBase, ChatMessageUser, ChatSession,
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
import { toCamelCase } from 'types/conversion';
|
||||
|
||||
|
||||
interface Job {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
import { Job } from 'types/types';
|
||||
|
||||
interface JobAnalysisProps extends BackstoryPageProps {
|
||||
job: Job;
|
||||
@ -48,7 +43,6 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
} = props
|
||||
const { apiClient } = useAuth();
|
||||
const theme = useTheme();
|
||||
const [jobRequirements, setJobRequirements] = useState<JobRequirements | null>(null);
|
||||
const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]);
|
||||
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
|
||||
const [creatingSession, setCreatingSession] = useState<boolean>(false);
|
||||
@ -89,7 +83,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
|
||||
// Fetch initial requirements
|
||||
useEffect(() => {
|
||||
if (!job.description || !requirementsSession || loadingRequirements || jobRequirements) {
|
||||
if (!job.description || !requirementsSession || loadingRequirements) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -101,22 +95,36 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
onMessage: (msg: ChatMessage) => {
|
||||
console.log(`onMessage: ${msg.type}`, msg);
|
||||
if (msg.type === "response") {
|
||||
const incoming: any = toCamelCase<JobRequirements>(JSON.parse(msg.content || ''));
|
||||
const requirements: { requirement: string, domain: string }[] = ['technicalSkills', 'experienceRequirements'].flatMap((domain) => {
|
||||
return ['required', 'preferred'].flatMap((level) => {
|
||||
return incoming[domain][level].map((s: string) => { return { requirement: s, domain: domain }; });
|
||||
})
|
||||
});
|
||||
['softSkills', 'experience', 'education', 'certifications', 'preferredAttributes'].forEach(domain => {
|
||||
if (incoming[domain]) {
|
||||
incoming[domain].forEach((s: string) => requirements.push({ requirement: s, domain: domain }));
|
||||
}
|
||||
});
|
||||
const job: Job = toCamelCase<Job>(JSON.parse(msg.content || ''));
|
||||
const requirements: { requirement: string, domain: string }[] = [];
|
||||
if (job.requirements?.technicalSkills) {
|
||||
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)' }));
|
||||
}
|
||||
if (job.requirements?.experienceRequirements) {
|
||||
job.requirements.experienceRequirements.required?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (required)' }));
|
||||
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 => ({
|
||||
requirement: req.requirement,
|
||||
domain: req.domain,
|
||||
status: 'pending' as const,
|
||||
status: 'waiting' as const,
|
||||
matchScore: 0,
|
||||
assessment: '',
|
||||
description: '',
|
||||
@ -168,6 +176,12 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
// Process requirements one by one
|
||||
for (let i = 0; i < requirements.length; i++) {
|
||||
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 skillMatch = result.skillMatch;
|
||||
let matchScore: number = 0;
|
||||
@ -177,7 +191,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
case "WEAK": matchScore = 50; 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);
|
||||
}
|
||||
const match: SkillMatch = {
|
||||
@ -234,7 +248,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
|
||||
// Get icon based on status
|
||||
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 (score >= 70) return <CheckCircleIcon color="success" />;
|
||||
if (score >= 40) return <WarningIcon color="warning" />;
|
||||
|
@ -23,52 +23,51 @@ import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext';
|
||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
|
||||
type NavigationLinkType = {
|
||||
name: string;
|
||||
label: ReactElement<any> | string;
|
||||
path: string;
|
||||
icon?: ReactElement<any>;
|
||||
label?: ReactElement<any>;
|
||||
icon?: ReactElement<any>;
|
||||
};
|
||||
|
||||
const DefaultNavItems: NavigationLinkType[] = [
|
||||
{ name: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
||||
{ name: 'Docs', path: '/docs', icon: <InfoIcon /> },
|
||||
// { name: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
|
||||
// { name: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
|
||||
// { name: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
|
||||
// { name: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
|
||||
{ label: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
||||
{ label: 'Docs', path: '/docs', icon: <InfoIcon /> },
|
||||
// { label: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
|
||||
// { label: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
|
||||
// { label: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
|
||||
// { label: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
|
||||
];
|
||||
|
||||
const ViewerNavItems: NavigationLinkType[] = [
|
||||
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||
];
|
||||
|
||||
const CandidateNavItems : NavigationLinkType[]= [
|
||||
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||
{ name: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon /> },
|
||||
{ name: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> },
|
||||
// { name: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> },
|
||||
// { name: 'Dashboard', icon: <DashboardIcon />, path: '/candidate/dashboard' },
|
||||
// { name: 'Profile', icon: <PersonIcon />, path: '/candidate/profile' },
|
||||
// { name: 'Backstory', icon: <HistoryIcon />, path: '/candidate/backstory' },
|
||||
// { name: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes' },
|
||||
// { name: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/candidate/qa-setup' },
|
||||
// { name: 'Analytics', icon: <BarChartIcon />, path: '/candidate/analytics' },
|
||||
// { name: 'Settings', icon: <SettingsIcon />, path: '/candidate/settings' },
|
||||
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||
{ label: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon /> },
|
||||
{ label: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> },
|
||||
// { label: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> },
|
||||
// { label: 'Dashboard', icon: <DashboardIcon />, path: '/candidate/dashboard' },
|
||||
// { label: 'Profile', icon: <PersonIcon />, path: '/candidate/profile' },
|
||||
// { label: 'Backstory', icon: <HistoryIcon />, path: '/candidate/backstory' },
|
||||
// { label: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes' },
|
||||
// { label: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/candidate/qa-setup' },
|
||||
// { label: 'Analytics', icon: <BarChartIcon />, path: '/candidate/analytics' },
|
||||
// { label: 'Settings', icon: <SettingsIcon />, path: '/candidate/settings' },
|
||||
];
|
||||
|
||||
const EmployerNavItems: NavigationLinkType[] = [
|
||||
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||
{ name: 'Job Analysis', path: '/employer/job-analysis', icon: <WorkIcon /> },
|
||||
{ name: 'Resume Builder', path: '/employer/resume-builder', icon: <WorkIcon /> },
|
||||
{ name: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: <WorkIcon /> },
|
||||
{ name: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
||||
// { name: 'Dashboard', icon: <DashboardIcon />, path: '/employer/dashboard' },
|
||||
// { name: 'Search', icon: <SearchIcon />, path: '/employer/search' },
|
||||
// { name: 'Saved', icon: <BookmarkIcon />, path: '/employer/saved' },
|
||||
// { name: 'Jobs', icon: <WorkIcon />, path: '/employer/jobs' },
|
||||
// { name: 'Company', icon: <BusinessIcon />, path: '/employer/company' },
|
||||
// { name: 'Analytics', icon: <BarChartIcon />, path: '/employer/analytics' },
|
||||
// { name: 'Settings', icon: <SettingsIcon />, path: '/employer/settings' },
|
||||
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||
{ label: 'Job Analysis', path: '/employer/job-analysis', icon: <WorkIcon /> },
|
||||
{ label: 'Resume Builder', path: '/employer/resume-builder', icon: <WorkIcon /> },
|
||||
{ label: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: <WorkIcon /> },
|
||||
{ label: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
||||
// { label: 'Dashboard', icon: <DashboardIcon />, path: '/employer/dashboard' },
|
||||
// { label: 'Search', icon: <SearchIcon />, path: '/employer/search' },
|
||||
// { label: 'Saved', icon: <BookmarkIcon />, path: '/employer/saved' },
|
||||
// { label: 'Jobs', icon: <WorkIcon />, path: '/employer/jobs' },
|
||||
// { label: 'Company', icon: <BusinessIcon />, path: '/employer/company' },
|
||||
// { label: 'Analytics', icon: <BarChartIcon />, path: '/employer/analytics' },
|
||||
// { label: 'Settings', icon: <SettingsIcon />, path: '/employer/settings' },
|
||||
];
|
||||
|
||||
// Navigation links based on user type
|
||||
|
@ -170,7 +170,7 @@ interface HeaderProps {
|
||||
navigationLinks: NavigationLinkType[];
|
||||
currentPath: string;
|
||||
sessionId?: string | null;
|
||||
setSnack: SetSnackType,
|
||||
setSnack: SetSnackType;
|
||||
}
|
||||
|
||||
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 navLinks : NavigationLinkType[] = [
|
||||
{name: "Home", path: "/", label: <BackstoryLogo/>},
|
||||
const mainNavSections: NavigationLinkType[] = [
|
||||
{ path: '/', label: <BackstoryLogo /> },
|
||||
...navigationLinks
|
||||
];
|
||||
|
||||
// State for page navigation
|
||||
const [ currentTab, setCurrentTab ] = useState<string>("/");
|
||||
// State for page navigation - only for main navigation
|
||||
const [currentTab, setCurrentTab] = useState<string | false>("/");
|
||||
const [userMenuTab, setUserMenuTab] = useState<string>("");
|
||||
|
||||
// State for mobile drawer
|
||||
@ -206,6 +206,26 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
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
|
||||
const userMenuItems = [
|
||||
{
|
||||
@ -244,15 +264,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const parts = location.pathname.split('/');
|
||||
let tab = '/';
|
||||
if (parts.length > 1) {
|
||||
tab = `/${parts[1]}`;
|
||||
const mainSection = getTailSection(location.pathname);
|
||||
|
||||
// Only update if the section is different from current tab
|
||||
if (mainSection !== currentTab) {
|
||||
setCurrentTab(mainSection); // mainSection is either a string or false
|
||||
}
|
||||
if (tab !== currentTab) {
|
||||
setCurrentTab(tab);
|
||||
}
|
||||
}, [location, currentTab]);
|
||||
}, [location.pathname, currentTab]);
|
||||
|
||||
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setUserMenuAnchor(event.currentTarget);
|
||||
@ -274,28 +292,34 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: string | false) => {
|
||||
if (newValue !== false) {
|
||||
setCurrentTab(newValue);
|
||||
navigate(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
// Render desktop navigation links
|
||||
const renderNavLinks = () => {
|
||||
return (
|
||||
<Tabs value={currentTab} onChange={(e, newValue) => setCurrentTab(newValue)}
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onChange={handleTabChange}
|
||||
indicatorColor="secondary"
|
||||
textColor="inherit"
|
||||
variant="fullWidth"
|
||||
allowScrollButtonsMobile
|
||||
aria-label="Backstory navigation"
|
||||
>
|
||||
{navLinks.map((link) => (
|
||||
<Tab
|
||||
sx={{
|
||||
minWidth: link.path === '/' ? "max-content" : "auto",
|
||||
}}
|
||||
key={link.name}
|
||||
value={link.path}
|
||||
label={link.label ? link.label : link.name}
|
||||
onClick={() => {
|
||||
navigate(link.path);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{mainNavSections.map((section) => (
|
||||
<Tab
|
||||
sx={{
|
||||
minWidth: section.path === '/' ? "max-content" : "auto",
|
||||
}}
|
||||
key={section.path}
|
||||
value={section.path}
|
||||
label={section.label}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
@ -308,33 +332,21 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
<MobileMenuTabs
|
||||
orientation="vertical"
|
||||
value={currentTab}
|
||||
onChange={(e, newValue) => setCurrentTab(newValue)}
|
||||
onChange={handleTabChange}
|
||||
>
|
||||
{navLinks.map((link) => (
|
||||
{mainNavSections.map((section) => (
|
||||
<Tab
|
||||
key={link.name}
|
||||
value={link.path}
|
||||
key={section.path || '/'}
|
||||
value={section.path}
|
||||
label={
|
||||
<MenuItemBox>
|
||||
{link.path === '/' ? (
|
||||
<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>
|
||||
{section.label}
|
||||
</MenuItemBox>
|
||||
}
|
||||
onClick={(e) => {
|
||||
handleDrawerToggle();
|
||||
setCurrentTab(link.path);
|
||||
navigate(link.path);
|
||||
setCurrentTab(section.path);
|
||||
navigate(section.path);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@ -458,8 +470,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
>
|
||||
<Container maxWidth="xl">
|
||||
<Toolbar disableGutters>
|
||||
{/* Logo Section */}
|
||||
|
||||
{/* Navigation Links - Desktop */}
|
||||
<NavLinksContainer>
|
||||
{renderNavLinks()}
|
||||
@ -482,22 +492,22 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{sessionId && <CopyBubble
|
||||
tooltip="Copy link"
|
||||
color="inherit"
|
||||
aria-label="copy link"
|
||||
edge="end"
|
||||
sx={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
opacity: 1,
|
||||
bgcolor: 'inherit',
|
||||
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
|
||||
}}
|
||||
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
|
||||
onClick={() => { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }}
|
||||
size="large"
|
||||
/>}
|
||||
{sessionId && <CopyBubble
|
||||
tooltip="Copy link"
|
||||
color="inherit"
|
||||
aria-label="copy link"
|
||||
edge="end"
|
||||
sx={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
opacity: 1,
|
||||
bgcolor: 'inherit',
|
||||
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
|
||||
}}
|
||||
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
|
||||
onClick={() => { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }}
|
||||
size="large"
|
||||
/>}
|
||||
</UserActionsContainer>
|
||||
|
||||
{/* Mobile Navigation Drawer */}
|
||||
@ -507,18 +517,16 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile
|
||||
keepMounted: true,
|
||||
}}
|
||||
>
|
||||
{renderDrawerContent()}
|
||||
</MobileDrawer>
|
||||
</Toolbar>
|
||||
</Container>
|
||||
<Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} />
|
||||
<Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} />
|
||||
</StyledAppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
Header
|
||||
};
|
||||
export { Header };
|
@ -1,18 +1,45 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
||||
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 {
|
||||
selectedCandidate: Types.Candidate | null;
|
||||
selectedJob: Types.Job | null;
|
||||
selectedEmployer: Types.Employer | null;
|
||||
// Add more global state as needed:
|
||||
// currentView: string;
|
||||
// filters: Record<string, any>;
|
||||
// searchQuery: string;
|
||||
routeState: RouteState;
|
||||
isInitializing: boolean;
|
||||
}
|
||||
|
||||
export interface AppStateActions {
|
||||
@ -20,22 +47,214 @@ export interface AppStateActions {
|
||||
setSelectedJob: (job: Types.Job | null) => void;
|
||||
setSelectedEmployer: (employer: Types.Employer | null) => 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;
|
||||
|
||||
// ============================
|
||||
// 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 {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { apiClient } = useAuth();
|
||||
|
||||
// Entity state
|
||||
const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null);
|
||||
const [selectedJob, setSelectedJobState] = useState<Types.Job | 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) => {
|
||||
setSelectedCandidateState(candidate);
|
||||
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
|
||||
|
||||
if (candidate) {
|
||||
console.log('Selected candidate:', candidate);
|
||||
@ -46,6 +265,7 @@ export function useAppStateLogic(): AppStateContextType {
|
||||
|
||||
const setSelectedJob = useCallback((job: Types.Job | null) => {
|
||||
setSelectedJobState(job);
|
||||
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null);
|
||||
|
||||
if (job) {
|
||||
console.log('Selected job:', job);
|
||||
@ -56,6 +276,7 @@ export function useAppStateLogic(): AppStateContextType {
|
||||
|
||||
const setSelectedEmployer = useCallback((employer: Types.Employer | null) => {
|
||||
setSelectedEmployerState(employer);
|
||||
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, employer?.id || null);
|
||||
|
||||
if (employer) {
|
||||
console.log('Selected employer:', employer);
|
||||
@ -68,17 +289,91 @@ export function useAppStateLogic(): AppStateContextType {
|
||||
setSelectedCandidateState(null);
|
||||
setSelectedJobState(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');
|
||||
}, []);
|
||||
|
||||
// ============================
|
||||
// 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 {
|
||||
selectedCandidate,
|
||||
selectedJob,
|
||||
selectedEmployer,
|
||||
routeState,
|
||||
isInitializing,
|
||||
setSelectedCandidate,
|
||||
setSelectedJob,
|
||||
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() {
|
||||
const { selectedCandidate, setSelectedCandidate } = useAppState();
|
||||
return { selectedCandidate, setSelectedCandidate };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook specifically for job selection
|
||||
*/
|
||||
export function useSelectedJob() {
|
||||
const { selectedJob, setSelectedJob } = useAppState();
|
||||
return { selectedJob, setSelectedJob };
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook specifically for employer selection
|
||||
*/
|
||||
export function useSelectedEmployer() {
|
||||
const { selectedEmployer, setSelectedEmployer } = useAppState();
|
||||
return { selectedEmployer, setSelectedEmployer };
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Development Utilities
|
||||
// ============================
|
||||
export function useRouteState() {
|
||||
const {
|
||||
routeState,
|
||||
setActiveTab,
|
||||
setFilters,
|
||||
setSidebarCollapsed,
|
||||
restoreLastRoute,
|
||||
clearRouteState
|
||||
} = useAppState();
|
||||
|
||||
/**
|
||||
* Debug utility to log current app state (development only)
|
||||
*/
|
||||
export function useAppStateDebug() {
|
||||
const appState = useAppState();
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.group('🔍 App State Debug');
|
||||
console.log('Selected Candidate:', appState.selectedCandidate);
|
||||
console.log('Selected Job:', appState.selectedJob);
|
||||
console.log('Selected Employer:', appState.selectedEmployer);
|
||||
console.groupEnd();
|
||||
}
|
||||
}, [appState.selectedCandidate, appState.selectedJob, appState.selectedEmployer]);
|
||||
|
||||
return appState;
|
||||
return {
|
||||
routeState,
|
||||
setActiveTab,
|
||||
setFilters,
|
||||
setSidebarCollapsed,
|
||||
restoreLastRoute,
|
||||
clearRouteState
|
||||
};
|
||||
}
|
||||
|
||||
export function useAppInitializing() {
|
||||
const { isInitializing } = useAppState();
|
||||
return isInitializing;
|
||||
}
|
@ -34,7 +34,6 @@ const CandidateListingPage = (props: BackstoryPageProps) => {
|
||||
}
|
||||
return result;
|
||||
});
|
||||
console.log(candidates);
|
||||
setCandidates(candidates);
|
||||
} catch (err) {
|
||||
setSnack("" + err);
|
||||
|
@ -46,6 +46,7 @@ import { ComingSoon } from 'components/ui/ComingSoon';
|
||||
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const theme = useTheme();
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
||||
const { setSnack, submitQuery } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
@ -53,6 +54,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [jobDescription, setJobDescription] = useState('');
|
||||
const [jobTitle, setJobTitle] = useState('');
|
||||
const [company, setCompany] = useState('');
|
||||
const [jobLocation, setJobLocation] = useState('');
|
||||
const [analysisStarted, setAnalysisStarted] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -60,6 +62,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
const { apiClient } = useAuth();
|
||||
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
||||
|
||||
const user_type = user?.userType || 'guest';
|
||||
const user_id = user?.id || '';
|
||||
|
||||
useEffect(() => {
|
||||
if (candidates !== null || selectedCandidate) {
|
||||
return;
|
||||
@ -96,7 +101,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
|
||||
// Steps in our process
|
||||
const steps = [
|
||||
{ index: 1, label: 'Job Description', icon: <WorkIcon /> },
|
||||
{ index: 1, label: 'Job Selection', icon: <WorkIcon /> },
|
||||
{ index: 2, label: 'AI Analysis', icon: <WorkIcon /> },
|
||||
{ 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 /> })
|
||||
}
|
||||
|
||||
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
|
||||
const handleNext = () => {
|
||||
if (activeStep === 0 && !selectedCandidate) {
|
||||
@ -230,7 +117,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
}
|
||||
|
||||
if (activeStep === 1) {
|
||||
if ((/*(extraInfo && !jobTitle) || */!jobDescription)) {
|
||||
if (!jobDescription) {
|
||||
setError('Please provide job description before continuing.');
|
||||
return;
|
||||
}
|
||||
@ -338,12 +225,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
</Paper>
|
||||
);
|
||||
|
||||
const extraInfo = false;
|
||||
|
||||
// Render function for the job description step
|
||||
const renderJobDescription = () => (
|
||||
<Paper elevation={3} sx={{ p: 3, mt: 3, mb: 4, borderRadius: 2 }}>
|
||||
{extraInfo && <>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Enter Job Details
|
||||
</Typography>
|
||||
@ -361,6 +245,18 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
/>
|
||||
</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 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
@ -371,23 +267,21 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2, mb: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ mr: 2 }}>
|
||||
Job Description
|
||||
Job Selection
|
||||
</Typography>
|
||||
{extraInfo && <Button
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<FileUploadIcon />}
|
||||
size="small"
|
||||
onClick={() => setOpenUploadDialog(true)}
|
||||
>
|
||||
Upload
|
||||
</Button>}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
@ -419,7 +313,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{selectedCandidate && (
|
||||
<JobMatchAnalysis
|
||||
job={{ title: jobTitle, description: jobDescription }}
|
||||
job={{ title: jobTitle, description: jobDescription, company: company, ownerId: user_id, ownerType: user_type }}
|
||||
candidate={selectedCandidate}
|
||||
{...backstoryProps}
|
||||
/>
|
||||
@ -434,14 +328,14 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
);
|
||||
|
||||
// If no user is logged in, show message
|
||||
if (!user) {
|
||||
if (!user?.id) {
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Paper elevation={3} sx={{ p: 4, mt: 4, textAlign: 'center' }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Please log in to access candidate analysis
|
||||
</Typography>
|
||||
<Button variant="contained" color="primary" sx={{ mt: 2 }}>
|
||||
<Button variant="contained" onClick={() => { navigate('/login'); }} color="primary" sx={{ mt: 2 }}>
|
||||
Log In
|
||||
</Button>
|
||||
</Paper>
|
||||
|
@ -24,9 +24,9 @@ const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProp
|
||||
if (candidate?.username === username || !username) {
|
||||
return;
|
||||
}
|
||||
const getCandidate = async (username: string) => {
|
||||
const getCandidate = async (reference: string) => {
|
||||
try {
|
||||
const result : Candidate = await apiClient.getCandidate(username);
|
||||
const result: Candidate = await apiClient.getCandidate(reference);
|
||||
setCandidate(result);
|
||||
navigate('/chat');
|
||||
} catch {
|
||||
|
@ -506,8 +506,9 @@ class ApiClient {
|
||||
// Candidate Methods with Date Conversion
|
||||
// ============================
|
||||
|
||||
async getCandidate(username: string): Promise<Types.Candidate> {
|
||||
const response = await fetch(`${this.baseUrl}/candidates/${username}`, {
|
||||
// reference can be candidateId, username, or email
|
||||
async getCandidate(reference: string): Promise<Types.Candidate> {
|
||||
const response = await fetch(`${this.baseUrl}/candidates/${reference}`, {
|
||||
headers: this.defaultHeaders
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Generated TypeScript types from Pydantic models
|
||||
// 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
|
||||
|
||||
// ============================
|
||||
@ -577,26 +577,13 @@ export interface InterviewSchedule {
|
||||
|
||||
export interface Job {
|
||||
id?: string;
|
||||
title: string;
|
||||
ownerId: string;
|
||||
ownerType: "candidate" | "employer" | "guest";
|
||||
title?: string;
|
||||
summary?: string;
|
||||
company?: string;
|
||||
description: string;
|
||||
responsibilities: Array<string>;
|
||||
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;
|
||||
requirements?: JobRequirements;
|
||||
}
|
||||
|
||||
export interface JobApplication {
|
||||
@ -615,6 +602,31 @@ export interface JobApplication {
|
||||
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 {
|
||||
success: boolean;
|
||||
data?: Array<Job>;
|
||||
@ -1224,23 +1236,6 @@ export function convertInterviewScheduleFromApi(data: any): InterviewSchedule {
|
||||
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
|
||||
* Date fields: appliedDate, updatedDate
|
||||
@ -1256,6 +1251,23 @@ export function convertJobApplicationFromApi(data: any): JobApplication {
|
||||
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
|
||||
* Date fields: timestamp
|
||||
@ -1378,10 +1390,10 @@ export function convertFromApi<T>(data: any, modelType: string): T {
|
||||
return convertInterviewFeedbackFromApi(data) as T;
|
||||
case 'InterviewSchedule':
|
||||
return convertInterviewScheduleFromApi(data) as T;
|
||||
case 'Job':
|
||||
return convertJobFromApi(data) as T;
|
||||
case 'JobApplication':
|
||||
return convertJobApplicationFromApi(data) as T;
|
||||
case 'JobFull':
|
||||
return convertJobFullFromApi(data) as T;
|
||||
case 'MessageReaction':
|
||||
return convertMessageReactionFromApi(data) as T;
|
||||
case 'RAGConfiguration':
|
||||
|
@ -25,7 +25,7 @@ from models import Candidate
|
||||
|
||||
_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.
|
||||
|
||||
@ -39,16 +39,17 @@ def get_or_create_agent(agent_type: str, prometheus_collector: CollectorRegistry
|
||||
Raises:
|
||||
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
|
||||
for agent in _agents:
|
||||
if agent.agent_type == agent_type:
|
||||
return agent
|
||||
# Check if a global (non-user) agent with the given agent_type already exists
|
||||
if not user:
|
||||
for agent in _agents:
|
||||
if agent.agent_type == agent_type:
|
||||
return agent
|
||||
|
||||
# Find the matching subclass
|
||||
for agent_cls in Agent.__subclasses__():
|
||||
if agent_cls.model_fields["agent_type"].default == agent_type:
|
||||
# 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
|
||||
_agents.append(agent)
|
||||
return agent
|
||||
|
@ -40,13 +40,17 @@ class JobRequirementsAgent(Agent):
|
||||
## INSTRUCTIONS:
|
||||
|
||||
1. Analyze ONLY the job description provided.
|
||||
2. Extract and categorize all requirements and preferences.
|
||||
3. DO NOT consider any candidate information - this is a pure job analysis task.
|
||||
2. Extract company information, job title, and all requirements.
|
||||
3. Extract and categorize all requirements and preferences.
|
||||
4. DO NOT consider any candidate information - this is a pure job analysis task.
|
||||
|
||||
## OUTPUT FORMAT:
|
||||
|
||||
```json
|
||||
{
|
||||
"company_name": "Company Name",
|
||||
"job_title": "Job Title",
|
||||
"job_summary": "Brief summary of the job",
|
||||
"job_requirements": {
|
||||
"technical_skills": {
|
||||
"required": ["skill1", "skill2"],
|
||||
@ -133,9 +137,15 @@ class JobRequirementsAgent(Agent):
|
||||
json_str = self.extract_json_from_text(generated_message.content)
|
||||
job_requirements : JobRequirements | None = None
|
||||
job_requirements_data = ""
|
||||
company_name = ""
|
||||
job_summary = ""
|
||||
job_title = ""
|
||||
try:
|
||||
job_requirements_data = json.loads(json_str)
|
||||
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)
|
||||
if not job_requirements:
|
||||
raise ValueError("Job requirements data is empty or invalid.")
|
||||
@ -160,7 +170,13 @@ class JobRequirementsAgent(Agent):
|
||||
return
|
||||
status_message.status = ChatStatusType.DONE
|
||||
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
|
||||
|
||||
logger.info(f"✅ Job requirements analysis completed successfully.")
|
||||
|
@ -168,7 +168,7 @@ JSON RESPONSE:"""
|
||||
|
||||
user_message.content = prompt
|
||||
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:
|
||||
status_message.status = ChatStatusType.ERROR
|
||||
status_message.content = skill_assessment.content
|
||||
|
@ -20,11 +20,13 @@ from logger import logger
|
||||
import agents as agents
|
||||
from models import (Tunables, CandidateQuestion, ChatMessageUser, ChatMessage, RagEntry, ChatMessageType, ChatMessageMetaData, ChatStatusType, Candidate, ChatContextType)
|
||||
from llm_manager import llm_manager
|
||||
from agents.base import Agent
|
||||
|
||||
class CandidateEntity(Candidate):
|
||||
model_config = {"arbitrary_types_allowed": True} # Allow ChromaDBFileWatcher, etc
|
||||
|
||||
# Internal instance members
|
||||
CandidateEntity__agents: List[Agent] = []
|
||||
CandidateEntity__observer: Optional[Any] = Field(default=None, exclude=True)
|
||||
CandidateEntity__file_watcher: Optional[ChromaDBFileWatcher] = Field(default=None, exclude=True)
|
||||
CandidateEntity__prometheus_collector: Optional[CollectorRegistry] = Field(
|
||||
@ -63,7 +65,7 @@ class CandidateEntity(Candidate):
|
||||
# Check if file exists
|
||||
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.
|
||||
|
||||
@ -74,11 +76,17 @@ class CandidateEntity(Candidate):
|
||||
Returns:
|
||||
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(
|
||||
agent_type=agent_type,
|
||||
user=self,
|
||||
prometheus_collector=self.prometheus_collector,
|
||||
**kwargs)
|
||||
prometheus_collector=self.prometheus_collector
|
||||
)
|
||||
|
||||
# Wrapper properties that map into file_watcher
|
||||
@property
|
||||
|
@ -64,13 +64,13 @@ import agents
|
||||
# =============================
|
||||
from models import (
|
||||
# API
|
||||
LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
|
||||
Job, LoginRequest, CreateCandidateRequest, CreateEmployerRequest,
|
||||
|
||||
# User models
|
||||
Candidate, Employer, BaseUserWithType, BaseUser, Guest, Authentication, AuthResponse, CandidateAI,
|
||||
|
||||
# Job models
|
||||
Job, JobApplication, ApplicationStatus,
|
||||
JobFull, JobApplication, ApplicationStatus,
|
||||
|
||||
# Chat models
|
||||
ChatSession, ChatMessage, ChatContext, ChatQuery, ChatStatusType, ChatMessageBase, ChatMessageUser, ChatSenderType, ChatMessageType, ChatContextType,
|
||||
@ -2609,28 +2609,24 @@ async def confirm_password_reset(
|
||||
# ============================
|
||||
|
||||
@api_router.post("/jobs")
|
||||
async def create_job(
|
||||
async def create_candidate_job(
|
||||
job_data: Dict[str, Any] = Body(...),
|
||||
current_user = Depends(get_current_user),
|
||||
database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Create a new job"""
|
||||
is_employer = isinstance(current_user, Employer)
|
||||
|
||||
try:
|
||||
# Verify user is an employer
|
||||
if not isinstance(current_user, Employer):
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content=create_error_response("FORBIDDEN", "Only employers can create jobs")
|
||||
)
|
||||
if is_employer:
|
||||
job = JobFull.model_validate(job_data)
|
||||
else:
|
||||
job = Job.model_validate(job_data)
|
||||
|
||||
# Add required fields
|
||||
job_data["id"] = str(uuid.uuid4())
|
||||
job_data["datePosted"] = datetime.now(UTC).isoformat()
|
||||
job_data["views"] = 0
|
||||
job_data["applicationCount"] = 0
|
||||
job_data["employerId"] = current_user.id
|
||||
job.id = str(uuid.uuid4())
|
||||
job.owner_id = current_user.id
|
||||
|
||||
job = Job.model_validate(job_data)
|
||||
await database.set_job(job.id, job.model_dump())
|
||||
|
||||
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
|
||||
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(
|
||||
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))
|
||||
)
|
||||
|
||||
@api_router.get("/candidates/{username}")
|
||||
# reference can be candidateId, username, or email
|
||||
@api_router.get("/candidates/{reference}")
|
||||
async def get_candidate(
|
||||
username: str = Path(...),
|
||||
reference: str = Path(...),
|
||||
database: RedisDatabase = Depends(get_database)
|
||||
):
|
||||
"""Get a candidate by username"""
|
||||
try:
|
||||
# Normalize reference to lowercase for case-insensitive search
|
||||
query_lower = reference.lower()
|
||||
|
||||
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
|
||||
query_lower = username.lower()
|
||||
candidate_data = None
|
||||
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
|
||||
candidates_list = [
|
||||
c for c in candidates_list
|
||||
if (query_lower == c.email.lower() or
|
||||
query_lower == c.username.lower())
|
||||
]
|
||||
|
||||
if not len(candidates_list):
|
||||
if not candidate_data:
|
||||
logger.warning(f"⚠️ Candidate not found for reference: {reference}")
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
logger.info(f"🔍 Running skill match for candidate {candidate.id} against requirement: {requirement}")
|
||||
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)
|
||||
if not agent:
|
||||
return JSONResponse(
|
||||
|
@ -108,17 +108,6 @@ class SkillMatch(BaseModel):
|
||||
"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):
|
||||
ERROR = "error"
|
||||
@ -650,14 +639,32 @@ class AuthResponse(BaseModel):
|
||||
"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):
|
||||
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
|
||||
responsibilities: List[str]
|
||||
requirements: List[str]
|
||||
preferred_skills: Optional[List[str]] = Field(None, alias="preferredSkills")
|
||||
employer_id: str = Field(..., alias="employerId")
|
||||
requirements: Optional[JobRequirements]
|
||||
model_config = {
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
|
||||
class JobFull(Job):
|
||||
location: Location
|
||||
salary_range: Optional[SalaryRange] = Field(None, alias="salaryRange")
|
||||
employment_type: EmploymentType = Field(..., alias="employmentType")
|
||||
@ -672,9 +679,6 @@ class Job(BaseModel):
|
||||
featured_until: Optional[datetime] = Field(None, alias="featuredUntil")
|
||||
views: int = 0
|
||||
application_count: int = Field(0, alias="applicationCount")
|
||||
model_config = {
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
|
||||
class InterviewFeedback(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||
@ -1061,4 +1065,4 @@ Candidate.update_forward_refs()
|
||||
Employer.update_forward_refs()
|
||||
ChatSession.update_forward_refs()
|
||||
JobApplication.update_forward_refs()
|
||||
Job.update_forward_refs()
|
||||
JobFull.update_forward_refs()
|
Loading…
x
Reference in New Issue
Block a user