Improved doc loading
This commit is contained in:
parent
82df3758cd
commit
588b1d9b61
@ -337,10 +337,22 @@ const JobCreator = (props: JobCreator) => {
|
|||||||
onSave ? onSave(job) : setSelectedJob(job);
|
onSave ? onSave(job) : setSelectedJob(job);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExtractRequirements = () => {
|
const handleExtractRequirements = async () => {
|
||||||
// Implement requirements extraction logic here
|
try {
|
||||||
setIsProcessing(true);
|
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 = () => {
|
const renderJobCreation = () => {
|
||||||
|
@ -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 };
|
|
@ -47,7 +47,7 @@ const CandidateNavItems : NavigationLinkType[]= [
|
|||||||
{ label: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> },
|
{ label: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> },
|
||||||
// { label: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> },
|
// { label: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> },
|
||||||
// { label: 'Dashboard', icon: <DashboardIcon />, path: '/candidate/dashboard' },
|
// { 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: 'Backstory', icon: <HistoryIcon />, path: '/candidate/backstory' },
|
||||||
// { label: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes' },
|
// { label: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes' },
|
||||||
// { label: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/candidate/qa-setup' },
|
// { label: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/candidate/qa-setup' },
|
||||||
|
@ -18,9 +18,8 @@ import { JobAnalysisPage } from 'pages/JobAnalysisPage';
|
|||||||
import { GenerateCandidate } from "pages/GenerateCandidate";
|
import { GenerateCandidate } from "pages/GenerateCandidate";
|
||||||
import { ControlsPage } from 'pages/ControlsPage';
|
import { ControlsPage } from 'pages/ControlsPage';
|
||||||
import { LoginPage } from "pages/LoginPage";
|
import { LoginPage } from "pages/LoginPage";
|
||||||
import { CandidateDashboardPage } from "pages/CandidateDashboardPage"
|
import { CandidateDashboardPage } from "pages/candidate/Dashboard"
|
||||||
import { EmailVerificationPage } from "components/EmailVerificationComponents";
|
import { EmailVerificationPage } from "components/EmailVerificationComponents";
|
||||||
import { CandidateProfilePage } from "pages/candidate/Profile";
|
|
||||||
import { JobMatchAnalysis } from "components/JobMatchAnalysis";
|
import { JobMatchAnalysis } from "components/JobMatchAnalysis";
|
||||||
|
|
||||||
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
|
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
|
||||||
@ -69,8 +68,8 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNod
|
|||||||
|
|
||||||
if (user.userType === 'candidate') {
|
if (user.userType === 'candidate') {
|
||||||
routes.splice(-1, 0, ...[
|
routes.splice(-1, 0, ...[
|
||||||
<Route key={`${index++}`} path="/candidate/dashboard" element={<BetaPage><CandidateDashboardPage {...backstoryProps} /></BetaPage>} />,
|
<Route key={`${index++}`} path="/candidate/dashboard" element={<CandidateDashboardPage {...backstoryProps} />} />,
|
||||||
<Route key={`${index++}`} path="/candidate/profile" element={<CandidateProfilePage {...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/job-analysis" element={<JobAnalysisPage {...backstoryProps} />} />,
|
||||||
<Route key={`${index++}`} path="/candidate/backstory" element={<BackstoryPage />} />,
|
<Route key={`${index++}`} path="/candidate/backstory" element={<BackstoryPage />} />,
|
||||||
<Route key={`${index++}`} path="/candidate/resumes" element={<ResumesPage />} />,
|
<Route key={`${index++}`} path="/candidate/resumes" element={<ResumesPage />} />,
|
||||||
|
@ -232,7 +232,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
id: 'profile',
|
id: 'profile',
|
||||||
label: 'Profile',
|
label: 'Profile',
|
||||||
icon: <Person fontSize="small" />,
|
icon: <Person fontSize="small" />,
|
||||||
action: () => navigate(`/${user?.userType}/profile`)
|
action: () => navigate(`/${user?.userType}/dashboard/profile`)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dashboard',
|
id: 'dashboard',
|
||||||
|
@ -67,6 +67,16 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
|||||||
<strong>Location:</strong> {job.location.city}, {job.location.state || job.location.country}
|
<strong>Location:</strong> {job.location.city}, {job.location.state || job.location.country}
|
||||||
</Typography>
|
</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 &&
|
{job.company &&
|
||||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
<strong>Company:</strong> {job.company}
|
<strong>Company:</strong> {job.company}
|
||||||
|
@ -87,6 +87,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
|
width: "100%",
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
bgcolor: theme.palette.background.default,
|
bgcolor: theme.palette.background.default,
|
||||||
|
@ -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 };
|
|
@ -9,37 +9,26 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
useTheme,
|
useTheme,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Container,
|
|
||||||
Grid,
|
|
||||||
Alert,
|
Alert,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Divider,
|
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Person,
|
|
||||||
PersonAdd,
|
|
||||||
AccountCircle,
|
|
||||||
Add,
|
Add,
|
||||||
WorkOutline,
|
WorkOutline,
|
||||||
AddCircle,
|
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
import WorkIcon from '@mui/icons-material/Work';
|
import WorkIcon from '@mui/icons-material/Work';
|
||||||
import AssessmentIcon from '@mui/icons-material/Assessment';
|
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||||
import { JobMatchAnalysis } from 'components/JobMatchAnalysis';
|
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 { useNavigate } from 'react-router-dom';
|
||||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
import { useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
||||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
import { ComingSoon } from 'components/ui/ComingSoon';
|
||||||
import { JobManagement } from 'components/JobManagement';
|
|
||||||
import { LoginRequired } from 'components/ui/LoginRequired';
|
import { LoginRequired } from 'components/ui/LoginRequired';
|
||||||
import { Scrollable } from 'components/Scrollable';
|
import { Scrollable } from 'components/Scrollable';
|
||||||
import { CandidatePicker } from 'components/ui/CandidatePicker';
|
import { CandidatePicker } from 'components/ui/CandidatePicker';
|
||||||
|
@ -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 };
|
|
@ -25,16 +25,9 @@ import {
|
|||||||
|
|
||||||
// Import generated date conversion functions
|
// Import generated date conversion functions
|
||||||
import {
|
import {
|
||||||
// convertCandidateFromApi,
|
|
||||||
// convertEmployerFromApi,
|
|
||||||
// convertJobFromApi,
|
|
||||||
// convertJobApplicationFromApi,
|
|
||||||
// convertChatSessionFromApi,
|
|
||||||
convertChatMessageFromApi,
|
|
||||||
convertFromApi,
|
convertFromApi,
|
||||||
convertArrayFromApi
|
convertArrayFromApi
|
||||||
} from 'types/types';
|
} from 'types/types';
|
||||||
import { json } from 'stream/consumers';
|
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Streaming Types and Interfaces
|
// Streaming Types and Interfaces
|
||||||
@ -180,7 +173,7 @@ class ApiClient {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const apiResponse = parsePaginatedResponse<T>(data);
|
const apiResponse = parsePaginatedResponse<T>(data);
|
||||||
const extractedData = extractApiData(apiResponse);
|
const extractedData = extractApiData(apiResponse);
|
||||||
|
console.log("extracted", extractedData);
|
||||||
// Apply model-specific date conversion to array items if modelType is provided
|
// Apply model-specific date conversion to array items if modelType is provided
|
||||||
if (modelType && extractedData.data) {
|
if (modelType && extractedData.data) {
|
||||||
return {
|
return {
|
||||||
@ -632,6 +625,11 @@ class ApiClient {
|
|||||||
// Job Methods with Date Conversion
|
// 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> {
|
async createJob(job: Omit<Types.Job, 'id' | 'datePosted' | 'views' | 'applicationCount'>): Promise<Types.Job> {
|
||||||
const body = JSON.stringify(formatApiRequest(job));
|
const body = JSON.stringify(formatApiRequest(job));
|
||||||
const response = await fetch(`${this.baseUrl}/jobs`, {
|
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
|
// Can't do a simple += as typescript thinks .content might not be there
|
||||||
streamingMessage.content = (streamingMessage?.content || '') + streaming.content;
|
streamingMessage.content = (streamingMessage?.content || '') + streaming.content;
|
||||||
// Update timestamp to latest
|
// Update timestamp to latest
|
||||||
streamingMessage.timestamp = streamingMessage.timestamp;
|
streamingMessage.timestamp = streaming.timestamp;
|
||||||
}
|
}
|
||||||
options.onStreaming?.(streamingMessage);
|
options.onStreaming?.(streamingMessage);
|
||||||
break;
|
break;
|
||||||
|
@ -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")
|
@api_router.post("/jobs/upload")
|
||||||
async def create_job_from_file(
|
async def create_job_from_file(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user