Improved doc loading

This commit is contained in:
James Ketr 2025-06-08 12:53:38 -07:00
parent 82df3758cd
commit 588b1d9b61
12 changed files with 100 additions and 1833 deletions

View File

@ -337,10 +337,22 @@ const JobCreator = (props: JobCreator) => {
onSave ? onSave(job) : setSelectedJob(job);
};
const handleExtractRequirements = () => {
// Implement requirements extraction logic here
setIsProcessing(true);
// This would call your API to extract requirements from the job description
const handleExtractRequirements = async () => {
try {
setIsProcessing(true);
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,13 +67,23 @@ 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}
</Typography>
}
{job.summary && <Typography variant="body2">
<strong>Summary:</strong> {job.summary}
<strong>Summary:</strong> {job.summary}
</Typography>
}
</>}

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

@ -1,277 +0,0 @@
import React 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,
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';
interface DashboardProps extends BackstoryPageProps {
userName?: string;
profileCompletion?: number;
}
const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps) => {
const navigate = useNavigate();
const { setSnack } = props;
const { user, isLoading, isInitializing, isAuthenticated } = 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 || !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,
}}
>
<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>
{/* Main Content */}
<Box sx={{ flex: 1, p: 3 }}>
{/* Welcome Section */}
<Box sx={{ mb: 4 }}>
<Typography variant="h4" sx={{ mb: 2, fontWeight: 'bold' }}>
Welcome back, {user.firstName}!
</Typography>
<Box sx={{ mb: 2 }}>
<Typography variant="body1" sx={{ mb: 1 }}>
Your profile is {profileCompletion}% complete
</Typography>
<LinearProgress
variant="determinate"
value={profileCompletion}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: '#e0e0e0',
'& .MuiLinearProgress-bar': {
backgroundColor: '#4caf50',
},
}}
/>
</Box>
<Button
variant="contained"
color="primary"
sx={{ mt: 1 }}
>
Complete Your Profile
</Button>
</Box>
{/* Cards Grid */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Top Row */}
<Box sx={{ display: 'flex', gap: 3 }}>
{/* Resume Builder Card */}
<Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Resume Builder
</Typography>
<Typography variant="body2" sx={{ mb: 1, color: '#666' }}>
3 custom resumes
</Typography>
<Typography variant="body2" sx={{ mb: 3, color: '#666' }}>
Last created: May 15, 2025
</Typography>
<Button
variant="outlined"
startIcon={<AddIcon />}
fullWidth
>
Create New
</Button>
</CardContent>
</Card>
{/* Recent Activity Card */}
<Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Recent Activity
</Typography>
<Stack spacing={1} sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<VisibilityIcon sx={{ fontSize: 16, color: '#666' }} />
<Typography variant="body2">5 profile views</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<DownloadIcon sx={{ fontSize: 16, color: '#666' }} />
<Typography variant="body2">2 resume downloads</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ContactMailIcon sx={{ fontSize: 16, color: '#666' }} />
<Typography variant="body2">1 direct contact</Typography>
</Box>
</Stack>
<Button
variant="outlined"
fullWidth
>
View All Activity
</Button>
</CardContent>
</Card>
</Box>
{/* Bottom Row */}
<Box sx={{ display: 'flex', gap: 3 }}>
{/* Complete Your Backstory Card */}
<Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Complete Your Backstory
</Typography>
<Stack spacing={1} sx={{ mb: 3 }}>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
Add projects
</Typography>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
Detail skills
</Typography>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
Work history
</Typography>
</Stack>
<Button
variant="outlined"
startIcon={<EditIcon />}
fullWidth
>
Edit Backstory
</Button>
</CardContent>
</Card>
{/* Improvement Suggestions Card */}
<Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Improvement Suggestions
</Typography>
<Stack spacing={1} sx={{ mb: 3 }}>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
Add certifications
</Typography>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
Enhance your project details
</Typography>
</Stack>
<Button
variant="outlined"
startIcon={<TipsIcon />}
fullWidth
>
View All Tips
</Button>
</CardContent>
</Card>
</Box>
</Box>
</Box>
</Box>
);
};
export { CandidateDashboardPage };

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

@ -1,983 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
Container,
Grid,
Paper,
TextField,
Typography,
Avatar,
IconButton,
Tabs,
Tab,
useMediaQuery,
CircularProgress,
Snackbar,
Alert,
Card,
CardContent,
CardActions,
Chip,
Divider,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
MenuItem,
Select,
FormControl,
InputLabel,
Switch,
FormControlLabel
} from '@mui/material';
import { styled } from '@mui/material/styles';
import {
CloudUpload,
PhotoCamera,
Edit,
Save,
Cancel,
Add,
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')({
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: 1,
overflow: 'hidden',
position: 'absolute',
bottom: 0,
left: 0,
whiteSpace: 'nowrap',
width: 1,
});
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`profile-tabpanel-${index}`}
aria-labelledby={`profile-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{
p: { xs: 1, sm: 3 },
maxWidth: '100%',
overflow: 'hidden'
}}>
{children}
</Box>
)}
</div>
);
}
const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const { setSnack, submitQuery } = props;
const backstoryProps = { setSnack, submitQuery };
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const { user, updateUserData, apiClient } = useAuth();
// Check if user is a candidate
const candidate = user?.userType === 'candidate' ? user as Types.Candidate : null;
// State management
const [tabValue, setTabValue] = useState(0);
const [editMode, setEditMode] = useState<{ [key: string]: boolean }>({});
const [loading, setLoading] = useState(false);
const [snackbar, setSnackbar] = useState<{
open: boolean;
message: string;
severity: "success" | "error" | "info" | "warning";
}>({
open: false,
message: '',
severity: 'success'
});
// Form data state
const [formData, setFormData] = useState<Partial<Types.Candidate>>({});
const [profileImage, setProfileImage] = useState<string | null>(null);
// 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>>({
name: '',
category: '',
level: 'beginner',
yearsOfExperience: 0
});
const [newExperience, setNewExperience] = useState<Partial<Types.WorkExperience>>({
companyName: '',
position: '',
startDate: new Date(),
isCurrent: false,
description: '',
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) {
setFormData(candidate);
setProfileImage(candidate.profileImage || null);
}
}, [candidate]);
if (!candidate) {
return (
<Container maxWidth="md" sx={{ mt: 4 }}>
<Alert severity="error">
Access denied. This page is only available for candidates.
</Alert>
</Container>
);
}
// Handle tab change
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
// Handle form input changes
const handleInputChange = (field: string, value: any) => {
setFormData({
...formData,
[field]: value,
});
};
// Handle profile image upload
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
if (await apiClient.uploadCandidateProfile(e.target.files[0])) {
candidate.profileImage = 'profile.' + e.target.files[0].name.replace(/^.*\./, '');
console.log(`Set profile image to: ${candidate.profileImage}`);
updateUserData(candidate);
}
}
};
// Toggle edit mode for a section
const toggleEditMode = (section: string) => {
setEditMode({
...editMode,
[section]: !editMode[section]
});
};
// Save changes
const handleSave = async (section: string) => {
setLoading(true);
try {
if (candidate.id) {
const updatedCandidate = await apiClient.updateCandidate(candidate.id, formData);
updateUserData(updatedCandidate);
setSnackbar({
open: true,
message: 'Profile updated successfully!',
severity: 'success'
});
toggleEditMode(section);
}
} catch (error) {
setSnackbar({
open: true,
message: 'Failed to update profile. Please try again.',
severity: 'error'
});
} finally {
setLoading(false);
}
};
// Cancel edit
const handleCancel = (section: string) => {
setFormData(candidate);
toggleEditMode(section);
};
// Add new skill
const handleAddSkill = () => {
if (newSkill.name && newSkill.category) {
const updatedSkills = [...(formData.skills || []), newSkill as Types.Skill];
setFormData({ ...formData, skills: updatedSkills });
setNewSkill({ name: '', category: '', level: 'beginner', yearsOfExperience: 0 });
setSkillDialog(false);
}
};
// Remove skill
const handleRemoveSkill = (index: number) => {
const updatedSkills = (formData.skills || []).filter((_, i) => i !== index);
setFormData({ ...formData, skills: updatedSkills });
};
// Add new work experience
const handleAddExperience = () => {
if (newExperience.companyName && newExperience.position) {
const updatedExperience = [...(formData.experience || []), newExperience as Types.WorkExperience];
setFormData({ ...formData, experience: updatedExperience });
setNewExperience({
companyName: '',
position: '',
startDate: new Date(),
isCurrent: false,
description: '',
skills: [],
location: { city: '', country: '' }
});
setExperienceDialog(false);
}
};
// Remove work experience
const handleRemoveExperience = (index: number) => {
const updatedExperience = (formData.experience || []).filter((_, i) => i !== index);
setFormData({ ...formData, experience: updatedExperience });
};
// Basic Information Tab
const renderBasicInfo = () => (
<Box sx={{ display: "flex", flexDirection: "column", "& .entry": { flexDirection: "column", fontSize: "0.9rem", display: "flex", mt: 1 }, "& .title": { display: "flex", fontWeight: "bold" } }}>
<Box sx={{ textAlign: 'center', mb: { xs: 1, sm: 2 } }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Avatar
src={profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''}
sx={{
width: { xs: 80, sm: 120 },
height: { xs: 80, sm: 120 },
mb: { xs: 1, sm: 2 },
border: `2px solid ${theme.palette.primary.main}`
}}
>
{!profileImage && <AccountCircle sx={{ fontSize: { xs: 50, sm: 80 } }} />}
</Avatar>
{editMode.basic && (
<>
<IconButton
color="primary"
aria-label="upload picture"
component="label"
size={isMobile ? "small" : "medium"}
>
<PhotoCamera />
<VisuallyHiddenInput
type="file"
accept="image/*"
onChange={handleImageUpload}
/>
</IconButton>
<Typography variant="caption" color="textSecondary" sx={{ textAlign: 'center', fontSize: { xs: '0.7rem', sm: '0.75rem' } }}>
Update profile photo
</Typography>
</>
)}
</Box>
</Box>
<Box className="entry">
{editMode.basic ? (
<TextField
fullWidth
label="First Name"
value={formData.firstName || ''}
onChange={(e) => handleInputChange('firstName', e.target.value)}
variant="outlined"
/>
) : (<>
<Box className="title">First Name</Box>
<Box className="value">{candidate.firstName}</Box>
</>)}
</Box>
<Box className="entry">
{editMode.basic ? (
<TextField
fullWidth
label="Last Name"
value={formData.lastName || ''}
onChange={(e) => handleInputChange('lastName', e.target.value)}
variant="outlined"
/>
) : (<>
<Box className="title">Last Name</Box>
<Box className="value">{candidate.lastName}</Box>
</>)}
</Box>
<Box className="entry">
{(false && editMode.basic) ? (
<TextField
fullWidth
label="Email"
type="email"
value={formData.email || ''}
onChange={(e) => handleInputChange('email', e.target.value)}
variant="outlined"
/>
) : (<>
<Box className="title"><Email sx={{ mr: 1, verticalAlign: 'middle' }} />
Email</Box>
<Box className="value">{candidate.email}</Box>
</>
)}
</Box>
<Box className="entry">
{editMode.basic ? (
<TextField
fullWidth
label="Phone"
value={formData.phone || ''}
onChange={(e) => handleInputChange('phone', e.target.value)}
variant="outlined"
/>
) : (<>
<Box className="title"><Phone sx={{ mr: 1, verticalAlign: 'middle' }} />
Phone</Box>
<Box className="value">{candidate.phone || 'Not provided'}</Box>
</>
)}
</Box>
<Box className="entry">
{editMode.basic ? (
<TextField
fullWidth
multiline
rows={3}
label="Professional Summary"
value={formData.description || ''}
onChange={(e) => handleInputChange('description', e.target.value)}
variant="outlined"
/>
) : (<>
<Box className="title">Professional Summary</Box>
<Box className="value">{candidate.description || 'No summary provided'}</Box>
</>)}
</Box>
<Box className="entry">
{false && editMode.basic ? (
<TextField
fullWidth
label="Location"
value={formData.location?.city || ''}
onChange={(e) => handleInputChange('location', {
...formData.location,
city: e.target.value
})}
variant="outlined"
placeholder="City, State, Country"
/>
) : (<><Box className="title">
<LocationOn sx={{ mr: 1, verticalAlign: 'middle' }} />
Location</Box>
<Box className="value">{candidate.location?.city || 'Not specified'} {candidate.location?.country || ''}</Box>
</>
)}
</Box>
<Box className="entry">
<Box sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'flex-end',
gap: 2,
mt: { xs: 2, sm: 0 }
}}>
{editMode.basic ? (
<>
<Button
variant="outlined"
onClick={() => handleCancel('basic')}
startIcon={<Cancel />}
fullWidth={isMobile}
>
Cancel
</Button>
<Button
variant="contained"
onClick={() => handleSave('basic')}
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <Save />}
disabled={loading}
fullWidth={isMobile}
>
Save
</Button>
</>
) : (
<Button
variant="outlined"
onClick={() => toggleEditMode('basic')}
startIcon={<Edit />}
fullWidth={isMobile}
>
Edit Info
</Button>
)}
</Box>
</Box>
</Box >
);
// Skills Tab
const renderSkills = () => (
<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"}>Skills & Expertise</Typography>
<Button
variant="outlined"
startIcon={<Add />}
onClick={() => setSkillDialog(true)}
fullWidth={isMobile}
size={isMobile ? "small" : "medium"}
>
Add Skill
</Button>
</Box>
<Grid container spacing={{ xs: 1, sm: 2 }} sx={{ maxWidth: '100%' }}>
{(formData.skills || []).map((skill, index) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<Card variant="outlined" sx={{ height: '100%' }}>
<CardContent sx={{ p: { xs: 1.5, sm: 3 } }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant={isMobile ? "subtitle2" : "h6"} component="div" sx={{
fontSize: { xs: '0.9rem', sm: '1.25rem' },
wordBreak: 'break-word'
}}>
{skill.name}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{
wordBreak: 'break-word',
fontSize: { xs: '0.75rem', sm: '0.875rem' }
}}>
{skill.category}
</Typography>
<Chip
size="small"
label={skill.level}
color="primary"
variant="outlined"
sx={{
mt: 1,
fontSize: { xs: '0.65rem', sm: '0.75rem' },
height: { xs: 20, sm: 24 }
}}
/>
{skill.yearsOfExperience && (
<Typography variant="caption" display="block" sx={{ fontSize: { xs: '0.65rem', sm: '0.75rem' } }}>
{skill.yearsOfExperience} years experience
</Typography>
)}
</Box>
<IconButton
size="small"
onClick={() => handleRemoveSkill(index)}
color="error"
sx={{ ml: 1 }}
>
<Delete sx={{ fontSize: { xs: 16, sm: 20 } }} />
</IconButton>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
{(!formData.skills || formData.skills.length === 0) && (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 4 }}>
No skills added yet. Click "Add Skill" to get started.
</Typography>
)}
</Box>
);
// Experience Tab
const renderExperience = () => (
<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"}>Work Experience</Typography>
<Button
variant="outlined"
startIcon={<Add />}
onClick={() => setExperienceDialog(true)}
fullWidth={isMobile}
size={isMobile ? "small" : "medium"}
>
Add Experience
</Button>
</Box>
{(formData.experience || []).map((exp, index) => (
<Card key={index} sx={{ mb: { xs: 1.5, sm: 2 }, overflow: 'hidden' }}>
<CardContent sx={{ p: { xs: 1.5, sm: 3 } }}>
<Box sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: { xs: 1, sm: 0 }
}}>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant={isMobile ? "subtitle1" : "h6"} component="div" sx={{
fontSize: { xs: '1rem', sm: '1.25rem' },
wordBreak: 'break-word'
}}>
{exp.position}
</Typography>
<Typography variant="subtitle1" color="primary" sx={{
wordBreak: 'break-word',
fontSize: { xs: '0.9rem', sm: '1rem' }
}}>
{exp.companyName}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.8rem', sm: '0.875rem' } }}>
{exp.startDate?.toLocaleDateString()} - {exp.isCurrent ? 'Present' : exp.endDate?.toLocaleDateString()}
</Typography>
<Typography variant="body2" sx={{
mt: 1,
wordBreak: 'break-word',
fontSize: { xs: '0.8rem', sm: '0.875rem' }
}}>
{exp.description}
</Typography>
{exp.skills && exp.skills.length > 0 && (
<Box sx={{ mt: { xs: 1, sm: 2 } }}>
{exp.skills.map((skill, skillIndex) => (
<Chip
key={skillIndex}
label={skill}
size="small"
sx={{
mr: 0.5,
mb: 0.5,
fontSize: { xs: '0.65rem', sm: '0.75rem' },
height: { xs: 20, sm: 24 }
}}
/>
))}
</Box>
)}
</Box>
<IconButton
onClick={() => handleRemoveExperience(index)}
color="error"
size="small"
sx={{
alignSelf: { xs: 'flex-end', sm: 'flex-start' },
ml: { sm: 1 }
}}
>
<Delete sx={{ fontSize: { xs: 16, sm: 20 } }} />
</IconButton>
</Box>
</CardContent>
</Card>
))}
{(!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>
);
// Resume Tab
const renderResume = () => (
<DocumentManager {...backstoryProps} />
);
return (
<Container maxWidth="lg" sx={{
mt: { xs: 1, sm: 4 },
mb: { xs: 1, sm: 4 },
px: { xs: 0.5, sm: 3 }
}}>
<Paper elevation={3} sx={{
overflow: 'hidden',
mx: { xs: 0, sm: 0 }
}}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={tabValue}
onChange={handleTabChange}
variant="scrollable"
scrollButtons="auto"
allowScrollButtonsMobile
sx={{
'& .MuiTabs-flexContainer': {
justifyContent: isMobile ? 'flex-start' : 'center'
},
'& .MuiTab-root': {
fontSize: { xs: '0.75rem', sm: '0.875rem' },
minWidth: { xs: 60, sm: 120 },
padding: { xs: '6px 8px', sm: '12px 16px' }
}
}}
>
<Tab
label={isMobile ? "Info" : "Basic Info"}
icon={<AccountCircle sx={{ fontSize: { xs: 18, sm: 24 } }} />}
iconPosition={isMobile ? "top" : "start"}
/>
<Tab
label="Skills"
icon={<EmojiEvents sx={{ fontSize: { xs: 18, sm: 24 } }} />}
iconPosition={isMobile ? "top" : "start"}
/>
<Tab
label={isMobile ? "Work" : "Experience"}
icon={<Work sx={{ fontSize: { xs: 18, sm: 24 } }} />}
iconPosition={isMobile ? "top" : "start"}
/>
<Tab
label={isMobile ? "Edu" : "Education"}
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>
<TabPanel value={tabValue} index={0}>
{renderBasicInfo()}
</TabPanel>
<TabPanel value={tabValue} index={1}>
<ComingSoon>{renderSkills()}</ComingSoon>
</TabPanel>
<TabPanel value={tabValue} index={2}>
<ComingSoon>{renderExperience()}</ComingSoon>
</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} />
</TabPanel>
</Paper>
{/* Add Skill Dialog */}
<Dialog
open={skillDialog}
onClose={() => setSkillDialog(false)}
maxWidth="sm"
fullWidth
fullScreen={isMobile}
PaperProps={{
sx: {
...(isMobile && {
margin: 0,
width: '100%',
height: '100%',
maxHeight: '100%'
})
}
}}
>
<DialogTitle sx={{ pb: { xs: 1, sm: 2 } }}>Add New Skill</DialogTitle>
<DialogContent
sx={{
overflow: 'auto',
pt: { xs: 1, sm: 2 }
}}
>
<Grid container spacing={{ xs: 1.5, sm: 2 }} sx={{ mt: 0.5, maxWidth: '100%' }}>
<Grid size={{ xs: 12 }}>
<TextField
fullWidth
label="Skill Name"
value={newSkill.name || ''}
onChange={(e) => setNewSkill({ ...newSkill, name: e.target.value })}
size={isMobile ? "small" : "medium"}
/>
</Grid>
<Grid size={{ xs: 12 }}>
<TextField
fullWidth
label="Category"
value={newSkill.category || ''}
onChange={(e) => setNewSkill({ ...newSkill, category: e.target.value })}
placeholder="e.g., Programming, Design, Marketing"
size={isMobile ? "small" : "medium"}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<FormControl fullWidth size={isMobile ? "small" : "medium"}>
<InputLabel>Proficiency Level</InputLabel>
<Select
value={newSkill.level || 'beginner'}
onChange={(e) => setNewSkill({ ...newSkill, level: e.target.value as Types.SkillLevel })}
label="Proficiency Level"
>
<MenuItem value="beginner">Beginner</MenuItem>
<MenuItem value="intermediate">Intermediate</MenuItem>
<MenuItem value="advanced">Advanced</MenuItem>
<MenuItem value="expert">Expert</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<TextField
fullWidth
type="number"
label="Years of Experience"
value={newSkill.yearsOfExperience || 0}
onChange={(e) => setNewSkill({ ...newSkill, yearsOfExperience: parseInt(e.target.value) || 0 })}
size={isMobile ? "small" : "medium"}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{
p: { xs: 1.5, sm: 3 },
flexDirection: { xs: 'column', sm: 'row' },
gap: { xs: 1, sm: 0 }
}}>
<Button
onClick={() => setSkillDialog(false)}
fullWidth={isMobile}
size={isMobile ? "small" : "medium"}
>
Cancel
</Button>
<Button
onClick={handleAddSkill}
variant="contained"
fullWidth={isMobile}
size={isMobile ? "small" : "medium"}
>
Add Skill
</Button>
</DialogActions>
</Dialog>
{/* Add Experience Dialog */}
<Dialog
open={experienceDialog}
onClose={() => setExperienceDialog(false)}
maxWidth="md"
fullWidth
fullScreen={isMobile}
PaperProps={{
sx: {
...(isMobile && {
margin: 0,
width: '100%',
height: '100%',
maxHeight: '100%'
})
}
}}
>
<DialogTitle sx={{ pb: { xs: 1, sm: 2 } }}>Add Work Experience</DialogTitle>
<DialogContent
sx={{
overflow: 'auto',
pt: { xs: 1, sm: 2 }
}}
>
<Grid container spacing={{ xs: 1.5, sm: 2 }} sx={{ mt: 0.5, maxWidth: '100%' }}>
<Grid size={{ xs: 12, sm: 6 }}>
<TextField
fullWidth
label="Company Name"
value={newExperience.companyName || ''}
onChange={(e) => setNewExperience({ ...newExperience, companyName: e.target.value })}
size={isMobile ? "small" : "medium"}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<TextField
fullWidth
label="Position/Title"
value={newExperience.position || ''}
onChange={(e) => setNewExperience({ ...newExperience, position: e.target.value })}
size={isMobile ? "small" : "medium"}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<TextField
fullWidth
type="date"
label="Start Date"
value={newExperience.startDate?.toISOString().split('T')[0] || ''}
onChange={(e) => setNewExperience({ ...newExperience, startDate: new Date(e.target.value) })}
InputLabelProps={{ shrink: true }}
size={isMobile ? "small" : "medium"}
/>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<FormControlLabel
control={
<Switch
checked={newExperience.isCurrent || false}
onChange={(e) => setNewExperience({ ...newExperience, isCurrent: e.target.checked })}
size={isMobile ? "small" : "medium"}
/>
}
label="Currently working here"
sx={{
'& .MuiFormControlLabel-label': {
fontSize: { xs: '0.875rem', sm: '1rem' }
}
}}
/>
</Grid>
<Grid size={{ xs: 12 }}>
<TextField
fullWidth
multiline
rows={isMobile ? 3 : 4}
label="Job Description"
value={newExperience.description || ''}
onChange={(e) => setNewExperience({ ...newExperience, description: e.target.value })}
placeholder="Describe your responsibilities and achievements..."
size={isMobile ? "small" : "medium"}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{
p: { xs: 1.5, sm: 3 },
flexDirection: { xs: 'column', sm: 'row' },
gap: { xs: 1, sm: 0 }
}}>
<Button
onClick={() => setExperienceDialog(false)}
fullWidth={isMobile}
size={isMobile ? "small" : "medium"}
>
Cancel
</Button>
<Button
onClick={handleAddExperience}
variant="contained"
fullWidth={isMobile}
size={isMobile ? "small" : "medium"}
>
Add Experience
</Button>
</DialogActions>
</Dialog>
{/* Snackbar for notifications */}
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={() => setSnackbar({ ...snackbar, open: false })}
>
<Alert
onClose={() => setSnackbar({ ...snackbar, open: false })}
severity={snackbar.severity}
sx={{ width: '100%' }}
>
{snackbar.message}
</Alert>
</Snackbar>
</Container>
);
};
export { CandidateProfilePage };

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(...),