Compare commits
2 Commits
82df3758cd
...
18863a23d9
Author | SHA1 | Date | |
---|---|---|---|
18863a23d9 | |||
588b1d9b61 |
@ -337,10 +337,22 @@ const JobCreator = (props: JobCreator) => {
|
||||
onSave ? onSave(job) : setSelectedJob(job);
|
||||
};
|
||||
|
||||
const handleExtractRequirements = () => {
|
||||
// Implement requirements extraction logic here
|
||||
const handleExtractRequirements = async () => {
|
||||
try {
|
||||
setIsProcessing(true);
|
||||
// This would call your API to extract requirements from the job description
|
||||
const controller = apiClient.createJobFromDescription(jobDescription, jobStatusHandlers);
|
||||
const job = await controller.promise;
|
||||
if (!job) {
|
||||
setIsProcessing(false);
|
||||
return;
|
||||
}
|
||||
console.log(`Job id: ${job.id}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setSnack('Failed to upload document', 'error');
|
||||
setIsProcessing(false);
|
||||
}
|
||||
setIsProcessing(false);
|
||||
};
|
||||
|
||||
const renderJobCreation = () => {
|
||||
|
@ -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: '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' },
|
||||
|
@ -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 />} />,
|
||||
|
@ -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',
|
||||
|
@ -67,6 +67,16 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
<strong>Location:</strong> {job.location.city}, {job.location.state || job.location.country}
|
||||
</Typography>
|
||||
}
|
||||
{job.title &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Title:</strong> {job.title}
|
||||
</Typography>
|
||||
}
|
||||
{/* {job.datePosted &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Posted:</strong> {job.datePosted.toISOString()}
|
||||
</Typography>
|
||||
} */}
|
||||
{job.company &&
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
<strong>Company:</strong> {job.company}
|
||||
|
@ -87,6 +87,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100%',
|
||||
width: "100%",
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
bgcolor: theme.palette.background.default,
|
||||
|
@ -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';
|
||||
|
138
frontend/src/pages/candidate/Dashboard.tsx
Normal file
138
frontend/src/pages/candidate/Dashboard.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import React, {useState} from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Button,
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListItemButton,
|
||||
Divider,
|
||||
Chip,
|
||||
Stack
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Dashboard as DashboardIcon,
|
||||
Person as PersonIcon,
|
||||
Work as WorkIcon,
|
||||
Article as ArticleIcon,
|
||||
Description as DescriptionIcon,
|
||||
Quiz as QuizIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
Settings as SettingsIcon,
|
||||
Add as AddIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
Download as DownloadIcon,
|
||||
ContactMail as ContactMailIcon,
|
||||
Edit as EditIcon,
|
||||
TipsAndUpdates as TipsIcon,
|
||||
SettingsBackupRestore,
|
||||
BubbleChart
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { LoadingPage } from 'pages/LoadingPage';
|
||||
import { LoginRequired } from 'pages/LoginRequired';
|
||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router-dom';
|
||||
import { BetaPage } from 'pages/BetaPage';
|
||||
import { map } from 'lodash';
|
||||
|
||||
import { CandidateDashboard } from 'pages/candidate/dashboard/Dashboard';
|
||||
import { CandidateProfile } from 'pages/candidate/dashboard/Profile';
|
||||
import { VectorVisualizer } from 'components/VectorVisualizer';
|
||||
import { DocumentManager } from 'components/DocumentManager';
|
||||
import { JobPicker } from 'components/ui/JobPicker';
|
||||
|
||||
interface DashboardProps extends BackstoryPageProps {
|
||||
userName?: string;
|
||||
profileCompletion?: number;
|
||||
}
|
||||
|
||||
const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { subPage = 'dashboard' } = useParams();
|
||||
const [activeTab, setActiveTab] = useState<string>(subPage);
|
||||
const { user, isLoading, isInitializing, isAuthenticated } = useAuth();
|
||||
const profileCompletion = 75;
|
||||
const { setSnack, submitQuery } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
|
||||
const sidebarItems = [
|
||||
{ text: 'Dashboard', icon: <DashboardIcon />,path: '/', element: <CandidateDashboard {...backstoryProps}/> },
|
||||
{ text: 'Profile', icon: <PersonIcon />,path: '/profile', element: <CandidateProfile {...backstoryProps}/> },
|
||||
{ text: 'Jobs', icon: <WorkIcon />,path: '/jobs', element: <JobPicker {...backstoryProps}/> },
|
||||
{ text: 'Resumes', icon: <DescriptionIcon />,path: '/resumes', element: <BetaPage><Box>Candidate resumes page</Box></BetaPage> },
|
||||
{ text: 'Content', icon: <BubbleChart />, path: '/rag', element: <Box sx={{display: "flex", width: "100%", flexDirection: "column"}}><VectorVisualizer {...backstoryProps} /><DocumentManager {...backstoryProps} /></Box>},
|
||||
{ text: 'Q&A Setup', icon: <QuizIcon />,path: '/q-a-setup', element: <BetaPage><Box>Candidate q&a setup page</Box></BetaPage> },
|
||||
{ text: 'Analytics', icon: <AnalyticsIcon />,path: '/analytics', element: <BetaPage><Box>Candidate analytics page</Box></BetaPage> },
|
||||
{ text: 'Settings', icon: <SettingsIcon />,path: '/settings', element: <BetaPage><Box>Candidate settings page</Box></BetaPage> },
|
||||
]
|
||||
|
||||
if (isLoading || isInitializing) {
|
||||
return (<LoadingPage {...props}/>);
|
||||
}
|
||||
if (!user || !isAuthenticated) {
|
||||
return (<LoginRequired {...props}/>);
|
||||
}
|
||||
if (user.userType !== 'candidate') {
|
||||
setSnack(`The page you were on is only available for candidates (you are a ${user.userType}`, 'warning');
|
||||
navigate('/');
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundColor: '#f5f5f5' }}>
|
||||
{/* Sidebar */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 250,
|
||||
backgroundColor: 'white',
|
||||
borderRight: '1px solid #e0e0e0',
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<List>
|
||||
{sidebarItems.map((item, index) => (
|
||||
<ListItem key={index} disablePadding sx={{ mb: 0.5 }}>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
backgroundColor: (item.text.toLowerCase() === activeTab )? '#e3f2fd' : 'transparent',
|
||||
color: (item.text.toLowerCase() === activeTab ) ? '#1976d2' : '#666',
|
||||
'&:hover': {
|
||||
backgroundColor: (item.text.toLowerCase() === activeTab ) ? '#e3f2fd' : '#f5f5f5',
|
||||
},
|
||||
}}
|
||||
onClick={()=>{setActiveTab(item.text.toLowerCase()); navigate(`/candidate/dashboard/${item.text.toLowerCase()}`);}}
|
||||
>
|
||||
<ListItemIcon sx={{ color: 'inherit', minWidth: 40 }}>
|
||||
{item.icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={item.text}
|
||||
primaryTypographyProps={{
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: (item.text === activeTab ) ? 600 : 400,
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
{/* Main Content */}
|
||||
<Box sx={{ flex: 1, p: 3 }}>
|
||||
{ sidebarItems.map(item =>
|
||||
<Box sx={{display: (item.text.toLowerCase() === activeTab) ? "flex": "none", width: "100%"}}>{item.element}</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export { CandidateDashboardPage };
|
@ -6,114 +6,45 @@ import {
|
||||
Typography,
|
||||
Button,
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
ListItemButton,
|
||||
Divider,
|
||||
Chip,
|
||||
Stack
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Dashboard as DashboardIcon,
|
||||
Person as PersonIcon,
|
||||
Article as ArticleIcon,
|
||||
Description as DescriptionIcon,
|
||||
Quiz as QuizIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
Settings as SettingsIcon,
|
||||
Add as AddIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
Download as DownloadIcon,
|
||||
ContactMail as ContactMailIcon,
|
||||
Edit as EditIcon,
|
||||
TipsAndUpdates as TipsIcon,
|
||||
SettingsBackupRestore
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { LoadingPage } from './LoadingPage';
|
||||
import { LoginRequired } from './LoginRequired';
|
||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
import { LoginRequired } from 'pages/LoginRequired';
|
||||
import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
||||
|
||||
interface DashboardProps extends BackstoryPageProps {
|
||||
userName?: string;
|
||||
profileCompletion?: number;
|
||||
}
|
||||
interface CandidateDashboardProps extends BackstoryElementProps {
|
||||
};
|
||||
|
||||
const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps) => {
|
||||
const CandidateDashboard = (props: CandidateDashboardProps) => {
|
||||
const { setSnack, submitQuery } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
const navigate = useNavigate();
|
||||
const { setSnack } = props;
|
||||
const { user, isLoading, isInitializing, isAuthenticated } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const profileCompletion = 75;
|
||||
const sidebarItems = [
|
||||
{ icon: <DashboardIcon />, text: 'Dashboard', active: true },
|
||||
{ icon: <PersonIcon />, text: 'Profile', active: false },
|
||||
{ icon: <ArticleIcon />, text: 'Backstory', active: false },
|
||||
{ icon: <DescriptionIcon />, text: 'Resumes', active: false },
|
||||
{ icon: <QuizIcon />, text: 'Q&A Setup', active: false },
|
||||
{ icon: <AnalyticsIcon />, text: 'Analytics', active: false },
|
||||
{ icon: <SettingsIcon />, text: 'Settings', active: false },
|
||||
];
|
||||
|
||||
if (isLoading || isInitializing) {
|
||||
return (<LoadingPage {...props}/>);
|
||||
if (!user) {
|
||||
return <LoginRequired {...backstoryProps}/>;
|
||||
}
|
||||
if (!user || !isAuthenticated) {
|
||||
return (<LoginRequired {...props}/>);
|
||||
}
|
||||
if (user.userType !== 'candidate') {
|
||||
|
||||
if (user?.userType !== 'candidate') {
|
||||
setSnack(`The page you were on is only available for candidates (you are a ${user.userType}`, 'warning');
|
||||
navigate('/');
|
||||
return (<></>);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundColor: '#f5f5f5' }}>
|
||||
{/* Sidebar */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 250,
|
||||
backgroundColor: 'white',
|
||||
borderRight: '1px solid #e0e0e0',
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ mb: 3, fontWeight: 'bold', color: '#1976d2' }}>
|
||||
JobPortal
|
||||
</Typography>
|
||||
|
||||
<List>
|
||||
{sidebarItems.map((item, index) => (
|
||||
<ListItem key={index} disablePadding sx={{ mb: 0.5 }}>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
backgroundColor: item.active ? '#e3f2fd' : 'transparent',
|
||||
color: item.active ? '#1976d2' : '#666',
|
||||
'&:hover': {
|
||||
backgroundColor: item.active ? '#e3f2fd' : '#f5f5f5',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ color: 'inherit', minWidth: 40 }}>
|
||||
{item.icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={item.text}
|
||||
primaryTypographyProps={{
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: item.active ? 600 : 400,
|
||||
}}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
return (<>
|
||||
{/* Main Content */}
|
||||
<ComingSoon>
|
||||
<Box sx={{ flex: 1, p: 3 }}>
|
||||
{/* Welcome Section */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
@ -143,6 +74,7 @@ const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps)
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 1 }}
|
||||
onClick={(e) => {e.stopPropagation(); navigate('/candidate/dashboard/profile'); }}
|
||||
>
|
||||
Complete Your Profile
|
||||
</Button>
|
||||
@ -270,8 +202,9 @@ const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps)
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</ComingSoon>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { CandidateDashboardPage };
|
||||
export { CandidateDashboard };
|
@ -17,13 +17,7 @@ import {
|
||||
Alert,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
Chip,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
@ -37,7 +31,6 @@ import {
|
||||
} from '@mui/material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import {
|
||||
CloudUpload,
|
||||
PhotoCamera,
|
||||
Edit,
|
||||
Save,
|
||||
@ -46,21 +39,17 @@ import {
|
||||
Delete,
|
||||
Work,
|
||||
School,
|
||||
Language,
|
||||
EmojiEvents,
|
||||
LocationOn,
|
||||
Phone,
|
||||
Email,
|
||||
AccountCircle,
|
||||
BubbleChart
|
||||
} from '@mui/icons-material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useAuth } from "hooks/AuthContext";
|
||||
import * as Types from 'types/types';
|
||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
||||
import { VectorVisualizer } from 'components/VectorVisualizer';
|
||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||
import { DocumentManager } from 'components/DocumentManager';
|
||||
|
||||
// Styled components
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
@ -105,9 +94,8 @@ function TabPanel(props: TabPanelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const { setSnack, submitQuery } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const { setSnack } = props;
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const { user, updateUserData, apiClient } = useAuth();
|
||||
@ -136,9 +124,6 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
|
||||
// Dialog states
|
||||
const [skillDialog, setSkillDialog] = useState(false);
|
||||
const [experienceDialog, setExperienceDialog] = useState(false);
|
||||
const [educationDialog, setEducationDialog] = useState(false);
|
||||
const [languageDialog, setLanguageDialog] = useState(false);
|
||||
const [certificationDialog, setCertificationDialog] = useState(false);
|
||||
|
||||
// New item states
|
||||
const [newSkill, setNewSkill] = useState<Partial<Types.Skill>>({
|
||||
@ -156,22 +141,6 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
|
||||
skills: [],
|
||||
location: { city: '', country: '' }
|
||||
});
|
||||
const [newEducation, setNewEducation] = useState<Partial<Types.Education>>({
|
||||
institution: '',
|
||||
degree: '',
|
||||
fieldOfStudy: '',
|
||||
startDate: new Date(),
|
||||
isCurrent: false
|
||||
});
|
||||
const [newLanguage, setNewLanguage] = useState<Partial<Types.Language>>({
|
||||
language: '',
|
||||
proficiency: 'basic'
|
||||
});
|
||||
const [newCertification, setNewCertification] = useState<Partial<Types.Certification>>({
|
||||
name: '',
|
||||
issuingOrganization: '',
|
||||
issueDate: new Date()
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (candidate) {
|
||||
@ -229,11 +198,7 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
|
||||
if (candidate.id) {
|
||||
const updatedCandidate = await apiClient.updateCandidate(candidate.id, formData);
|
||||
updateUserData(updatedCandidate);
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: 'Profile updated successfully!',
|
||||
severity: 'success'
|
||||
});
|
||||
setSnack('Profile updated successfully!');
|
||||
toggleEditMode(section);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -662,9 +627,38 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
|
||||
</Box>
|
||||
);
|
||||
|
||||
// Resume Tab
|
||||
const renderResume = () => (
|
||||
<DocumentManager {...backstoryProps} />
|
||||
|
||||
const renderEducation = () => (
|
||||
<Box>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'stretch', sm: 'center' },
|
||||
mb: { xs: 2, sm: 3 },
|
||||
gap: { xs: 1, sm: 0 }
|
||||
}}>
|
||||
<Typography variant={isMobile ? "subtitle1" : "h6"}>Education</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Add />}
|
||||
fullWidth={isMobile}
|
||||
size={isMobile ? "small" : "medium"}
|
||||
>
|
||||
Add Education
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{(!formData.experience || formData.experience.length === 0) && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{
|
||||
textAlign: 'center',
|
||||
py: { xs: 2, sm: 4 },
|
||||
fontSize: { xs: '0.8rem', sm: '0.875rem' }
|
||||
}}>
|
||||
No work experience added yet. Click "Add Experience" to get started.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
@ -715,16 +709,6 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
|
||||
icon={<School sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||
iconPosition={isMobile ? "top" : "start"}
|
||||
/>
|
||||
<Tab
|
||||
label="Docs"
|
||||
icon={<CloudUpload sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||
iconPosition={isMobile ? "top" : "start"}
|
||||
/>
|
||||
<Tab
|
||||
label="RAG"
|
||||
icon={<BubbleChart sx={{ fontSize: { xs: 18, sm: 24 } }} />}
|
||||
iconPosition={isMobile ? "top" : "start"}
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
@ -741,20 +725,7 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={3}>
|
||||
<ComingSoon>
|
||||
<Typography variant="h6">Education (Coming Soon)</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Education management will be available in a future update.
|
||||
</Typography>
|
||||
</ComingSoon>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={4}>
|
||||
{renderResume()}
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={5}>
|
||||
<VectorVisualizer {...backstoryProps} />
|
||||
<ComingSoon>{renderEducation()}</ComingSoon>
|
||||
</TabPanel>
|
||||
</Paper>
|
||||
|
||||
@ -980,4 +951,4 @@ const CandidateProfilePage: React.FC<BackstoryPageProps> = (props: BackstoryPage
|
||||
);
|
||||
};
|
||||
|
||||
export { CandidateProfilePage };
|
||||
export { CandidateProfile };
|
@ -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;
|
||||
|
@ -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(...),
|
||||
|
Loading…
x
Reference in New Issue
Block a user