Compare commits

..

2 Commits

Author SHA1 Message Date
18863a23d9 Added missing files 2025-06-08 12:53:53 -07:00
588b1d9b61 Improved doc loading 2025-06-08 12:53:38 -07:00
13 changed files with 295 additions and 726 deletions

View File

@ -337,10 +337,22 @@ const JobCreator = (props: JobCreator) => {
onSave ? onSave(job) : setSelectedJob(job);
};
const handleExtractRequirements = () => {
// Implement requirements extraction logic here
const handleExtractRequirements = async () => {
try {
setIsProcessing(true);
// This would call your API to extract requirements from the job description
const controller = apiClient.createJobFromDescription(jobDescription, jobStatusHandlers);
const job = await controller.promise;
if (!job) {
setIsProcessing(false);
return;
}
console.log(`Job id: ${job.id}`);
} catch (error) {
console.error(error);
setSnack('Failed to upload document', 'error');
setIsProcessing(false);
}
setIsProcessing(false);
};
const renderJobCreation = () => {

View File

@ -1,541 +0,0 @@
import React, { useState, useEffect, useRef, JSX } from 'react';
import {
Box,
Button,
Typography,
Paper,
TextField,
Grid,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
IconButton,
useTheme,
useMediaQuery,
Chip,
Divider,
Card,
CardContent,
CardHeader,
LinearProgress,
Stack,
Alert
} from '@mui/material';
import {
SyncAlt,
Favorite,
Settings,
Info,
Search,
AutoFixHigh,
Image,
Psychology,
Build,
CloudUpload,
Description,
Business,
LocationOn,
Work,
CheckCircle,
Star
} from '@mui/icons-material';
import { styled } from '@mui/material/styles';
import DescriptionIcon from '@mui/icons-material/Description';
import FileUploadIcon from '@mui/icons-material/FileUpload';
import { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
import { BackstoryElementProps } from './BackstoryTab';
import { LoginRequired } from 'components/ui/LoginRequired';
import * as Types from 'types/types';
const VisuallyHiddenInput = styled('input')({
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: 1,
overflow: 'hidden',
position: 'absolute',
bottom: 0,
left: 0,
whiteSpace: 'nowrap',
width: 1,
});
const UploadBox = styled(Box)(({ theme }) => ({
border: `2px dashed ${theme.palette.primary.main}`,
borderRadius: theme.shape.borderRadius * 2,
padding: theme.spacing(4),
textAlign: 'center',
backgroundColor: theme.palette.action.hover,
transition: 'all 0.3s ease',
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.action.selected,
borderColor: theme.palette.primary.dark,
},
}));
const StatusBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
padding: theme.spacing(1, 2),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
minHeight: 48,
}));
const getIcon = (type: Types.ApiActivityType) => {
switch (type) {
case 'converting':
return <SyncAlt color="primary" />;
case 'heartbeat':
return <Favorite color="error" />;
case 'system':
return <Settings color="action" />;
case 'info':
return <Info color="info" />;
case 'searching':
return <Search color="primary" />;
case 'generating':
return <AutoFixHigh color="secondary" />;
case 'generating_image':
return <Image color="primary" />;
case 'thinking':
return <Psychology color="secondary" />;
case 'tooling':
return <Build color="action" />;
default:
return <Info color="action" />;
}
};
const JobManagement = (props: BackstoryElementProps) => {
const { user, apiClient } = useAuth();
const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack, submitQuery } = props;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [jobDescription, setJobDescription] = useState<string>('');
const [jobRequirements, setJobRequirements] = useState<Types.JobRequirements | null>(null);
const [jobTitle, setJobTitle] = useState<string>('');
const [company, setCompany] = useState<string>('');
const [summary, setSummary] = useState<string>('');
const [jobStatus, setJobStatus] = useState<string>('');
const [jobStatusIcon, setJobStatusIcon] = useState<JSX.Element>(<></>);
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null);
if (!user?.id) {
return (
<LoginRequired asset="candidate analysis" />
);
}
const jobStatusHandlers = {
onStatus: (status: Types.ChatMessageStatus) => {
console.log('status:', status.content);
setJobStatusIcon(getIcon(status.activity));
setJobStatus(status.content);
},
onMessage: (job: Types.Job) => {
console.log('onMessage - job', job);
setCompany(job.company || '');
setJobDescription(job.description);
setSummary(job.summary || '');
setJobTitle(job.title || '');
setJobRequirements(job.requirements || null);
setJobStatusIcon(<></>);
setJobStatus('');
},
onError: (error: Types.ChatMessageError) => {
console.log('onError', error);
setSnack(error.content, "error");
setIsProcessing(false);
},
onComplete: () => {
setJobStatusIcon(<></>);
setJobStatus('');
setIsProcessing(false);
}
};
const documentStatusHandlers = {
...jobStatusHandlers,
onMessage: (document: Types.DocumentMessage) => {
if ('document' in document) {
console.log('onMessage - document', document);
setJobDescription(document.content || '');
} else if ('requirements' in document) {
console.log('onMessage - document (as job)', document);
jobStatusHandlers.onMessage(document);
}
setJobStatusIcon(<></>);
setJobStatus('');
}
};
const handleJobUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
let docType: Types.DocumentType | null = null;
switch (fileExtension.substring(1)) {
case "pdf":
docType = "pdf";
break;
case "docx":
docType = "docx";
break;
case "md":
docType = "markdown";
break;
case "txt":
docType = "txt";
break;
}
if (!docType) {
setSnack('Invalid file type. Please upload .txt, .md, .docx, or .pdf files only.', 'error');
return;
}
try {
setIsProcessing(true);
setJobDescription('');
setJobTitle('');
setJobRequirements(null);
setSummary('');
const controller = apiClient.createJobFromFile(file, jobStatusHandlers);
const job = await controller.promise;
if (!job) {
return;
}
console.log(`Job id: ${job.id}`);
e.target.value = '';
} catch (error) {
console.error(error);
setSnack('Failed to upload document', 'error');
setIsProcessing(false);
}
}
};
const handleUploadClick = () => {
fileInputRef.current?.click();
};
const renderRequirementSection = (title: string, items: string[] | undefined, icon: JSX.Element, required = false) => {
if (!items || items.length === 0) return null;
return (
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}>
{icon}
<Typography variant="subtitle1" sx={{ ml: 1, fontWeight: 600 }}>
{title}
</Typography>
{required && <Chip label="Required" size="small" color="error" sx={{ ml: 1 }} />}
</Box>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{items.map((item, index) => (
<Chip
key={index}
label={item}
variant="outlined"
size="small"
sx={{ mb: 1 }}
/>
))}
</Stack>
</Box>
);
};
const renderJobRequirements = () => {
if (!jobRequirements) return null;
return (
<Card elevation={2} sx={{ mt: 3 }}>
<CardHeader
title="Job Requirements Analysis"
avatar={<CheckCircle color="success" />}
sx={{ pb: 1 }}
/>
<CardContent sx={{ pt: 0 }}>
{renderRequirementSection(
"Technical Skills (Required)",
jobRequirements.technicalSkills.required,
<Build color="primary" />,
true
)}
{renderRequirementSection(
"Technical Skills (Preferred)",
jobRequirements.technicalSkills.preferred,
<Build color="action" />
)}
{renderRequirementSection(
"Experience Requirements (Required)",
jobRequirements.experienceRequirements.required,
<Work color="primary" />,
true
)}
{renderRequirementSection(
"Experience Requirements (Preferred)",
jobRequirements.experienceRequirements.preferred,
<Work color="action" />
)}
{renderRequirementSection(
"Soft Skills",
jobRequirements.softSkills,
<Psychology color="secondary" />
)}
{renderRequirementSection(
"Experience",
jobRequirements.experience,
<Star color="warning" />
)}
{renderRequirementSection(
"Education",
jobRequirements.education,
<Description color="info" />
)}
{renderRequirementSection(
"Certifications",
jobRequirements.certifications,
<CheckCircle color="success" />
)}
{renderRequirementSection(
"Preferred Attributes",
jobRequirements.preferredAttributes,
<Star color="secondary" />
)}
</CardContent>
</Card>
);
};
const handleSave = async () => {
const newJob: Types.Job = {
ownerId: user?.id || '',
ownerType: 'candidate',
description: jobDescription,
company: company,
summary: summary,
title: jobTitle,
requirements: jobRequirements || undefined
};
setIsProcessing(true);
const job = await apiClient.createJob(newJob);
setIsProcessing(false);
setSelectedJob(job);
};
const handleExtractRequirements = () => {
// Implement requirements extraction logic here
setIsProcessing(true);
// This would call your API to extract requirements from the job description
};
const renderJobCreation = () => {
if (!user) {
return <Box>You must be logged in</Box>;
}
return (
<Box sx={{
mx: 'auto', p: { xs: 2, sm: 3 },
}}>
{/* Upload Section */}
<Card elevation={3} sx={{ mb: 4 }}>
<CardHeader
title="Job Information"
subheader="Upload a job description or enter details manually"
avatar={<Work color="primary" />}
/>
<CardContent>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 6 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center' }}>
<CloudUpload sx={{ mr: 1 }} />
Upload Job Description
</Typography>
<UploadBox onClick={handleUploadClick}>
<CloudUpload sx={{ fontSize: 48, color: 'primary.main', mb: 2 }} />
<Typography variant="h6" gutterBottom>
Drop your job description here
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Supported formats: PDF, DOCX, TXT, MD
</Typography>
<Button
variant="contained"
startIcon={<FileUploadIcon />}
disabled={isProcessing}
// onClick={handleUploadClick}
>
Choose File
</Button>
</UploadBox>
<VisuallyHiddenInput
ref={fileInputRef}
type="file"
accept=".txt,.md,.docx,.pdf"
onChange={handleJobUpload}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center' }}>
<Description sx={{ mr: 1 }} />
Or Enter Manually
</Typography>
<TextField
fullWidth
multiline
rows={isMobile ? 8 : 12}
placeholder="Paste or type the job description here..."
variant="outlined"
value={jobDescription}
onChange={(e) => setJobDescription(e.target.value)}
disabled={isProcessing}
sx={{ mb: 2 }}
/>
{jobRequirements === null && jobDescription && (
<Button
variant="outlined"
onClick={handleExtractRequirements}
startIcon={<AutoFixHigh />}
disabled={isProcessing}
fullWidth={isMobile}
>
Extract Requirements
</Button>
)}
</Grid>
</Grid>
{(jobStatus || isProcessing) && (
<Box sx={{ mt: 3 }}>
<StatusBox>
{jobStatusIcon}
<Typography variant="body2" sx={{ ml: 1 }}>
{jobStatus || 'Processing...'}
</Typography>
</StatusBox>
{isProcessing && <LinearProgress sx={{ mt: 1 }} />}
</Box>
)}
</CardContent>
</Card>
{/* Job Details Section */}
<Card elevation={3} sx={{ mb: 4 }}>
<CardHeader
title="Job Details"
subheader="Enter specific information about the position"
avatar={<Business color="primary" />}
/>
<CardContent>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
fullWidth
label="Job Title"
variant="outlined"
value={jobTitle}
onChange={(e) => setJobTitle(e.target.value)}
required
disabled={isProcessing}
InputProps={{
startAdornment: <Work sx={{ mr: 1, color: 'text.secondary' }} />
}}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<TextField
fullWidth
label="Company"
variant="outlined"
value={company}
onChange={(e) => setCompany(e.target.value)}
required
disabled={isProcessing}
InputProps={{
startAdornment: <Business sx={{ mr: 1, color: 'text.secondary' }} />
}}
/>
</Grid>
{/* <Grid size={{ xs: 12, md: 6 }}>
<TextField
fullWidth
label="Job Location"
variant="outlined"
value={jobLocation}
onChange={(e) => setJobLocation(e.target.value)}
disabled={isProcessing}
InputProps={{
startAdornment: <LocationOn sx={{ mr: 1, color: 'text.secondary' }} />
}}
/>
</Grid> */}
<Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-end', height: '100%' }}>
<Button
variant="contained"
onClick={handleSave}
disabled={!jobTitle || !company || !jobDescription || isProcessing}
fullWidth={isMobile}
size="large"
startIcon={<CheckCircle />}
>
Save Job
</Button>
</Box>
</Grid>
</Grid>
</CardContent>
</Card>
{/* Job Summary */}
{summary !== '' &&
<Card elevation={2} sx={{ mt: 3 }}>
<CardHeader
title="Job Summary"
avatar={<CheckCircle color="success" />}
sx={{ pb: 1 }}
/>
<CardContent sx={{ pt: 0 }}>
{summary}
</CardContent>
</Card>
}
{/* Requirements Display */}
{renderJobRequirements()}
</Box>
);
};
return (
<Box className="JobManagement"
sx={{
background: "white",
p: 0,
}}>
{selectedJob === null && renderJobCreation()}
</Box>
);
};
export { JobManagement };

View File

@ -47,7 +47,7 @@ const CandidateNavItems : NavigationLinkType[]= [
{ 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: 'Profile', icon: <PersonIcon />, path: '/candidate/dashboard/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' },

View File

@ -18,9 +18,8 @@ import { JobAnalysisPage } from 'pages/JobAnalysisPage';
import { GenerateCandidate } from "pages/GenerateCandidate";
import { ControlsPage } from 'pages/ControlsPage';
import { LoginPage } from "pages/LoginPage";
import { CandidateDashboardPage } from "pages/CandidateDashboardPage"
import { CandidateDashboardPage } from "pages/candidate/Dashboard"
import { EmailVerificationPage } from "components/EmailVerificationComponents";
import { CandidateProfilePage } from "pages/candidate/Profile";
import { JobMatchAnalysis } from "components/JobMatchAnalysis";
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
@ -69,8 +68,8 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNod
if (user.userType === 'candidate') {
routes.splice(-1, 0, ...[
<Route key={`${index++}`} path="/candidate/dashboard" element={<BetaPage><CandidateDashboardPage {...backstoryProps} /></BetaPage>} />,
<Route key={`${index++}`} path="/candidate/profile" element={<CandidateProfilePage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/candidate/dashboard" element={<CandidateDashboardPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/candidate/dashboard/:subPage" element={<CandidateDashboardPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/candidate/job-analysis" element={<JobAnalysisPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/candidate/backstory" element={<BackstoryPage />} />,
<Route key={`${index++}`} path="/candidate/resumes" element={<ResumesPage />} />,

View File

@ -232,7 +232,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
id: 'profile',
label: 'Profile',
icon: <Person fontSize="small" />,
action: () => navigate(`/${user?.userType}/profile`)
action: () => navigate(`/${user?.userType}/dashboard/profile`)
},
{
id: 'dashboard',

View File

@ -67,6 +67,16 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<strong>Location:</strong> {job.location.city}, {job.location.state || job.location.country}
</Typography>
}
{job.title &&
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>Title:</strong> {job.title}
</Typography>
}
{/* {job.datePosted &&
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>Posted:</strong> {job.datePosted.toISOString()}
</Typography>
} */}
{job.company &&
<Typography variant="body2" sx={{ mb: 1 }}>
<strong>Company:</strong> {job.company}

View File

@ -87,6 +87,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
<Box
sx={{
minHeight: '100%',
width: "100%",
position: 'relative',
overflow: 'hidden',
bgcolor: theme.palette.background.default,

View File

@ -9,37 +9,26 @@ import {
Paper,
useTheme,
Snackbar,
Container,
Grid,
Alert,
Tabs,
Tab,
Card,
CardContent,
Divider,
Avatar,
Badge,
} from '@mui/material';
import {
Person,
PersonAdd,
AccountCircle,
Add,
WorkOutline,
AddCircle,
} from '@mui/icons-material';
import PersonIcon from '@mui/icons-material/Person';
import WorkIcon from '@mui/icons-material/Work';
import AssessmentIcon from '@mui/icons-material/Assessment';
import { JobMatchAnalysis } from 'components/JobMatchAnalysis';
import { Candidate, Job, JobFull } from "types/types";
import { Candidate, Job } from "types/types";
import { useNavigate } from 'react-router-dom';
import { BackstoryPageProps } from 'components/BackstoryTab';
import { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
import { CandidateInfo } from 'components/ui/CandidateInfo';
import { ComingSoon } from 'components/ui/ComingSoon';
import { JobManagement } from 'components/JobManagement';
import { LoginRequired } from 'components/ui/LoginRequired';
import { Scrollable } from 'components/Scrollable';
import { CandidatePicker } from 'components/ui/CandidatePicker';

View File

@ -0,0 +1,138 @@
import React, {useState} from 'react';
import {
Box,
Card,
CardContent,
Typography,
Button,
LinearProgress,
List,
ListItem,
ListItemIcon,
ListItemText,
ListItemButton,
Divider,
Chip,
Stack
} from '@mui/material';
import {
Dashboard as DashboardIcon,
Person as PersonIcon,
Work as WorkIcon,
Article as ArticleIcon,
Description as DescriptionIcon,
Quiz as QuizIcon,
Analytics as AnalyticsIcon,
Settings as SettingsIcon,
Add as AddIcon,
Visibility as VisibilityIcon,
Download as DownloadIcon,
ContactMail as ContactMailIcon,
Edit as EditIcon,
TipsAndUpdates as TipsIcon,
SettingsBackupRestore,
BubbleChart
} from '@mui/icons-material';
import { useAuth } from 'hooks/AuthContext';
import { LoadingPage } from 'pages/LoadingPage';
import { LoginRequired } from 'pages/LoginRequired';
import { BackstoryPageProps } from 'components/BackstoryTab';
import { Navigate, useNavigate, useParams } from 'react-router-dom';
import { BetaPage } from 'pages/BetaPage';
import { map } from 'lodash';
import { CandidateDashboard } from 'pages/candidate/dashboard/Dashboard';
import { CandidateProfile } from 'pages/candidate/dashboard/Profile';
import { VectorVisualizer } from 'components/VectorVisualizer';
import { DocumentManager } from 'components/DocumentManager';
import { JobPicker } from 'components/ui/JobPicker';
interface DashboardProps extends BackstoryPageProps {
userName?: string;
profileCompletion?: number;
}
const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps) => {
const navigate = useNavigate();
const { subPage = 'dashboard' } = useParams();
const [activeTab, setActiveTab] = useState<string>(subPage);
const { user, isLoading, isInitializing, isAuthenticated } = useAuth();
const profileCompletion = 75;
const { setSnack, submitQuery } = props;
const backstoryProps = { setSnack, submitQuery };
const sidebarItems = [
{ text: 'Dashboard', icon: <DashboardIcon />,path: '/', element: <CandidateDashboard {...backstoryProps}/> },
{ text: 'Profile', icon: <PersonIcon />,path: '/profile', element: <CandidateProfile {...backstoryProps}/> },
{ text: 'Jobs', icon: <WorkIcon />,path: '/jobs', element: <JobPicker {...backstoryProps}/> },
{ text: 'Resumes', icon: <DescriptionIcon />,path: '/resumes', element: <BetaPage><Box>Candidate resumes page</Box></BetaPage> },
{ text: 'Content', icon: <BubbleChart />, path: '/rag', element: <Box sx={{display: "flex", width: "100%", flexDirection: "column"}}><VectorVisualizer {...backstoryProps} /><DocumentManager {...backstoryProps} /></Box>},
{ text: 'Q&A Setup', icon: <QuizIcon />,path: '/q-a-setup', element: <BetaPage><Box>Candidate q&a setup page</Box></BetaPage> },
{ text: 'Analytics', icon: <AnalyticsIcon />,path: '/analytics', element: <BetaPage><Box>Candidate analytics page</Box></BetaPage> },
{ text: 'Settings', icon: <SettingsIcon />,path: '/settings', element: <BetaPage><Box>Candidate settings page</Box></BetaPage> },
]
if (isLoading || isInitializing) {
return (<LoadingPage {...props}/>);
}
if (!user || !isAuthenticated) {
return (<LoginRequired {...props}/>);
}
if (user.userType !== 'candidate') {
setSnack(`The page you were on is only available for candidates (you are a ${user.userType}`, 'warning');
navigate('/');
return (<></>);
}
return (
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundColor: '#f5f5f5' }}>
{/* Sidebar */}
<Box
sx={{
width: 250,
backgroundColor: 'white',
borderRight: '1px solid #e0e0e0',
p: 2,
}}
>
<List>
{sidebarItems.map((item, index) => (
<ListItem key={index} disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
sx={{
borderRadius: 1,
backgroundColor: (item.text.toLowerCase() === activeTab )? '#e3f2fd' : 'transparent',
color: (item.text.toLowerCase() === activeTab ) ? '#1976d2' : '#666',
'&:hover': {
backgroundColor: (item.text.toLowerCase() === activeTab ) ? '#e3f2fd' : '#f5f5f5',
},
}}
onClick={()=>{setActiveTab(item.text.toLowerCase()); navigate(`/candidate/dashboard/${item.text.toLowerCase()}`);}}
>
<ListItemIcon sx={{ color: 'inherit', minWidth: 40 }}>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.text}
primaryTypographyProps={{
fontSize: '0.9rem',
fontWeight: (item.text === activeTab ) ? 600 : 400,
}}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Box>
{/* Main Content */}
<Box sx={{ flex: 1, p: 3 }}>
{ sidebarItems.map(item =>
<Box sx={{display: (item.text.toLowerCase() === activeTab) ? "flex": "none", width: "100%"}}>{item.element}</Box>
)}
</Box>
</Box>
);
};
export { CandidateDashboardPage };

View File

@ -6,114 +6,45 @@ import {
Typography,
Button,
LinearProgress,
List,
ListItem,
ListItemIcon,
ListItemText,
ListItemButton,
Divider,
Chip,
Stack
} from '@mui/material';
import {
Dashboard as DashboardIcon,
Person as PersonIcon,
Article as ArticleIcon,
Description as DescriptionIcon,
Quiz as QuizIcon,
Analytics as AnalyticsIcon,
Settings as SettingsIcon,
Add as AddIcon,
Visibility as VisibilityIcon,
Download as DownloadIcon,
ContactMail as ContactMailIcon,
Edit as EditIcon,
TipsAndUpdates as TipsIcon,
SettingsBackupRestore
} from '@mui/icons-material';
import { useAuth } from 'hooks/AuthContext';
import { LoadingPage } from './LoadingPage';
import { LoginRequired } from './LoginRequired';
import { BackstoryPageProps } from 'components/BackstoryTab';
import { Navigate, useNavigate } from 'react-router-dom';
import { LoginRequired } from 'pages/LoginRequired';
import { BackstoryElementProps } from 'components/BackstoryTab';
import { useNavigate } from 'react-router-dom';
import { ComingSoon } from 'components/ui/ComingSoon';
interface DashboardProps extends BackstoryPageProps {
userName?: string;
profileCompletion?: number;
}
interface CandidateDashboardProps extends BackstoryElementProps {
};
const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps) => {
const CandidateDashboard = (props: CandidateDashboardProps) => {
const { setSnack, submitQuery } = props;
const backstoryProps = { setSnack, submitQuery };
const navigate = useNavigate();
const { setSnack } = props;
const { user, isLoading, isInitializing, isAuthenticated } = useAuth();
const { user } = useAuth();
const profileCompletion = 75;
const sidebarItems = [
{ icon: <DashboardIcon />, text: 'Dashboard', active: true },
{ icon: <PersonIcon />, text: 'Profile', active: false },
{ icon: <ArticleIcon />, text: 'Backstory', active: false },
{ icon: <DescriptionIcon />, text: 'Resumes', active: false },
{ icon: <QuizIcon />, text: 'Q&A Setup', active: false },
{ icon: <AnalyticsIcon />, text: 'Analytics', active: false },
{ icon: <SettingsIcon />, text: 'Settings', active: false },
];
if (isLoading || isInitializing) {
return (<LoadingPage {...props}/>);
if (!user) {
return <LoginRequired {...backstoryProps}/>;
}
if (!user || !isAuthenticated) {
return (<LoginRequired {...props}/>);
}
if (user.userType !== 'candidate') {
if (user?.userType !== 'candidate') {
setSnack(`The page you were on is only available for candidates (you are a ${user.userType}`, 'warning');
navigate('/');
return (<></>);
}
return (
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundColor: '#f5f5f5' }}>
{/* Sidebar */}
<Box
sx={{
width: 250,
backgroundColor: 'white',
borderRight: '1px solid #e0e0e0',
p: 2,
}}
>
<Typography variant="h6" sx={{ mb: 3, fontWeight: 'bold', color: '#1976d2' }}>
JobPortal
</Typography>
<List>
{sidebarItems.map((item, index) => (
<ListItem key={index} disablePadding sx={{ mb: 0.5 }}>
<ListItemButton
sx={{
borderRadius: 1,
backgroundColor: item.active ? '#e3f2fd' : 'transparent',
color: item.active ? '#1976d2' : '#666',
'&:hover': {
backgroundColor: item.active ? '#e3f2fd' : '#f5f5f5',
},
}}
>
<ListItemIcon sx={{ color: 'inherit', minWidth: 40 }}>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.text}
primaryTypographyProps={{
fontSize: '0.9rem',
fontWeight: item.active ? 600 : 400,
}}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Box>
return (<>
{/* Main Content */}
<ComingSoon>
<Box sx={{ flex: 1, p: 3 }}>
{/* Welcome Section */}
<Box sx={{ mb: 4 }}>
@ -143,6 +74,7 @@ const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps)
variant="contained"
color="primary"
sx={{ mt: 1 }}
onClick={(e) => {e.stopPropagation(); navigate('/candidate/dashboard/profile'); }}
>
Complete Your Profile
</Button>
@ -270,8 +202,9 @@ const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps)
</Box>
</Box>
</Box>
</Box>
</ComingSoon>
</>
);
};
export { CandidateDashboardPage };
export { CandidateDashboard };

View File

@ -17,13 +17,7 @@ import {
Alert,
Card,
CardContent,
CardActions,
Chip,
Divider,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
Dialog,
DialogTitle,
DialogContent,
@ -37,7 +31,6 @@ import {
} from '@mui/material';
import { styled } from '@mui/material/styles';
import {
CloudUpload,
PhotoCamera,
Edit,
Save,
@ -46,21 +39,17 @@ import {
Delete,
Work,
School,
Language,
EmojiEvents,
LocationOn,
Phone,
Email,
AccountCircle,
BubbleChart
} from '@mui/icons-material';
import { useTheme } from '@mui/material/styles';
import { useAuth } from "hooks/AuthContext";
import * as Types from 'types/types';
import { ComingSoon } from 'components/ui/ComingSoon';
import { VectorVisualizer } from 'components/VectorVisualizer';
import { BackstoryPageProps } from 'components/BackstoryTab';
import { DocumentManager } from 'components/DocumentManager';
// Styled components
const VisuallyHiddenInput = styled('input')({
@ -105,9 +94,8 @@ function TabPanel(props: TabPanelProps) {
);
}
const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const { setSnack, submitQuery } = props;
const backstoryProps = { setSnack, submitQuery };
const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const { setSnack } = props;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const { user, updateUserData, apiClient } = useAuth();
@ -136,9 +124,6 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
// Dialog states
const [skillDialog, setSkillDialog] = useState(false);
const [experienceDialog, setExperienceDialog] = useState(false);
const [educationDialog, setEducationDialog] = useState(false);
const [languageDialog, setLanguageDialog] = useState(false);
const [certificationDialog, setCertificationDialog] = useState(false);
// New item states
const [newSkill, setNewSkill] = useState<Partial<Types.Skill>>({
@ -156,22 +141,6 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
skills: [],
location: { city: '', country: '' }
});
const [newEducation, setNewEducation] = useState<Partial<Types.Education>>({
institution: '',
degree: '',
fieldOfStudy: '',
startDate: new Date(),
isCurrent: false
});
const [newLanguage, setNewLanguage] = useState<Partial<Types.Language>>({
language: '',
proficiency: 'basic'
});
const [newCertification, setNewCertification] = useState<Partial<Types.Certification>>({
name: '',
issuingOrganization: '',
issueDate: new Date()
});
useEffect(() => {
if (candidate) {
@ -229,11 +198,7 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
if (candidate.id) {
const updatedCandidate = await apiClient.updateCandidate(candidate.id, formData);
updateUserData(updatedCandidate);
setSnackbar({
open: true,
message: 'Profile updated successfully!',
severity: 'success'
});
setSnack('Profile updated successfully!');
toggleEditMode(section);
}
} catch (error) {
@ -662,9 +627,38 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
</Box>
);
// Resume Tab
const renderResume = () => (
<DocumentManager {...backstoryProps} />
const renderEducation = () => (
<Box>
<Box sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
alignItems: { xs: 'stretch', sm: 'center' },
mb: { xs: 2, sm: 3 },
gap: { xs: 1, sm: 0 }
}}>
<Typography variant={isMobile ? "subtitle1" : "h6"}>Education</Typography>
<Button
variant="outlined"
startIcon={<Add />}
fullWidth={isMobile}
size={isMobile ? "small" : "medium"}
>
Add Education
</Button>
</Box>
{(!formData.experience || formData.experience.length === 0) && (
<Typography variant="body2" color="text.secondary" sx={{
textAlign: 'center',
py: { xs: 2, sm: 4 },
fontSize: { xs: '0.8rem', sm: '0.875rem' }
}}>
No work experience added yet. Click "Add Experience" to get started.
</Typography>
)}
</Box>
);
return (
@ -715,16 +709,6 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
icon={<School sx={{ fontSize: { xs: 18, sm: 24 } }} />}
iconPosition={isMobile ? "top" : "start"}
/>
<Tab
label="Docs"
icon={<CloudUpload sx={{ fontSize: { xs: 18, sm: 24 } }} />}
iconPosition={isMobile ? "top" : "start"}
/>
<Tab
label="RAG"
icon={<BubbleChart sx={{ fontSize: { xs: 18, sm: 24 } }} />}
iconPosition={isMobile ? "top" : "start"}
/>
</Tabs>
</Box>
@ -741,20 +725,7 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
</TabPanel>
<TabPanel value={tabValue} index={3}>
<ComingSoon>
<Typography variant="h6">Education (Coming Soon)</Typography>
<Typography variant="body2" color="text.secondary">
Education management will be available in a future update.
</Typography>
</ComingSoon>
</TabPanel>
<TabPanel value={tabValue} index={4}>
{renderResume()}
</TabPanel>
<TabPanel value={tabValue} index={5}>
<VectorVisualizer {...backstoryProps} />
<ComingSoon>{renderEducation()}</ComingSoon>
</TabPanel>
</Paper>
@ -980,4 +951,4 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
);
};
export { CandidateProfilePage };
export { CandidateProfile };

View File

@ -25,16 +25,9 @@ import {
// Import generated date conversion functions
import {
// convertCandidateFromApi,
// convertEmployerFromApi,
// convertJobFromApi,
// convertJobApplicationFromApi,
// convertChatSessionFromApi,
convertChatMessageFromApi,
convertFromApi,
convertArrayFromApi
} from 'types/types';
import { json } from 'stream/consumers';
// ============================
// Streaming Types and Interfaces
@ -180,7 +173,7 @@ class ApiClient {
const data = await response.json();
const apiResponse = parsePaginatedResponse<T>(data);
const extractedData = extractApiData(apiResponse);
console.log("extracted", extractedData);
// Apply model-specific date conversion to array items if modelType is provided
if (modelType && extractedData.data) {
return {
@ -632,6 +625,11 @@ class ApiClient {
// Job Methods with Date Conversion
// ============================
createJobFromDescription(job_description: string, streamingOptions?: StreamingOptions<Types.Job>): StreamingResponse<Types.Job> {
const body = JSON.stringify(job_description);
return this.streamify<Types.Job>('/jobs/from-content', body, streamingOptions);
}
async createJob(job: Omit<Types.Job, 'id' | 'datePosted' | 'views' | 'applicationCount'>): Promise<Types.Job> {
const body = JSON.stringify(formatApiRequest(job));
const response = await fetch(`${this.baseUrl}/jobs`, {
@ -1087,7 +1085,7 @@ class ApiClient {
// Can't do a simple += as typescript thinks .content might not be there
streamingMessage.content = (streamingMessage?.content || '') + streaming.content;
// Update timestamp to latest
streamingMessage.timestamp = streamingMessage.timestamp;
streamingMessage.timestamp = streaming.timestamp;
}
options.onStreaming?.(streamingMessage);
break;

View File

@ -2823,6 +2823,65 @@ async def create_candidate_job(
)
@api_router.post("/jobs/from-content")
async def create_job_from_description(
content: str = Body(...),
current_user = Depends(get_current_user),
database: RedisDatabase = Depends(get_database)
):
"""Upload a document for the current candidate"""
async def content_stream_generator(content):
# Verify user is a candidate
if current_user.user_type != "candidate":
logger.warning(f"⚠️ Unauthorized upload attempt by user type: {current_user.user_type}")
error_message = ChatMessageError(
session_id=MOCK_UUID, # No session ID for document uploads
content="Only candidates can upload documents"
)
yield error_message
return
logger.info(f"📁 Received file content: size='{len(content)} bytes'")
async for message in create_job_from_content(database=database, current_user=current_user, content=content):
yield message
return
try:
async def to_json(method):
try:
async for message in method:
json_data = message.model_dump(mode='json', by_alias=True)
json_str = json.dumps(json_data)
yield f"data: {json_str}\n\n".encode("utf-8")
except Exception as e:
logger.error(backstory_traceback.format_exc())
logger.error(f"Error in to_json conversion: {e}")
return
return StreamingResponse(
to_json(content_stream_generator(content)),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache, no-store, must-revalidate",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # Nginx
"X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": "*", # Adjust for your CORS needs
"Transfer-Encoding": "chunked",
},
)
except Exception as e:
logger.error(backstory_traceback.format_exc())
logger.error(f"❌ Document upload error: {e}")
return StreamingResponse(
iter([ChatMessageError(
session_id=MOCK_UUID, # No session ID for document uploads
content="Failed to upload document"
)]),
media_type="text/event-stream"
)
@api_router.post("/jobs/upload")
async def create_job_from_file(
file: UploadFile = File(...),