Almost working through automatic flow
This commit is contained in:
parent
504985a06b
commit
1a13d41f28
@ -112,9 +112,9 @@ const DocumentManager = (props: BackstoryElementProps) => {
|
||||
try {
|
||||
// Upload file (replace with actual API call)
|
||||
const controller = apiClient.uploadCandidateDocument(file, { includeInRAG: true, isJobDocument: false });
|
||||
const newDocument = await controller.promise;
|
||||
const result = await controller.promise;
|
||||
|
||||
setDocuments(prev => [...prev, newDocument]);
|
||||
setDocuments(prev => [...prev, result.document]);
|
||||
setSnack(`Document uploaded: ${file.name}`, 'success');
|
||||
|
||||
// Reset file input
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, JSX } from 'react';
|
||||
import React, { useState, useEffect, useRef, JSX } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@ -6,7 +6,6 @@ import {
|
||||
Paper,
|
||||
TextField,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
@ -14,7 +13,15 @@ import {
|
||||
DialogActions,
|
||||
IconButton,
|
||||
useTheme,
|
||||
useMediaQuery
|
||||
useMediaQuery,
|
||||
Chip,
|
||||
Divider,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
LinearProgress,
|
||||
Stack,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import {
|
||||
SyncAlt,
|
||||
@ -25,7 +32,14 @@ import {
|
||||
AutoFixHigh,
|
||||
Image,
|
||||
Psychology,
|
||||
Build
|
||||
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';
|
||||
@ -37,7 +51,6 @@ import { BackstoryElementProps } from './BackstoryTab';
|
||||
import { LoginRequired } from 'components/ui/LoginRequired';
|
||||
|
||||
import * as Types from 'types/types';
|
||||
import { StreamingResponse } from 'services/api-client';
|
||||
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
clip: 'rect(0 0 0 0)',
|
||||
@ -51,52 +64,85 @@ const VisuallyHiddenInput = styled('input')({
|
||||
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 />;
|
||||
return <SyncAlt color="primary" />;
|
||||
case 'heartbeat':
|
||||
return <Favorite />;
|
||||
return <Favorite color="error" />;
|
||||
case 'system':
|
||||
return <Settings />;
|
||||
return <Settings color="action" />;
|
||||
case 'info':
|
||||
return <Info />;
|
||||
return <Info color="info" />;
|
||||
case 'searching':
|
||||
return <Search />;
|
||||
return <Search color="primary" />;
|
||||
case 'generating':
|
||||
return <AutoFixHigh />;
|
||||
return <AutoFixHigh color="secondary" />;
|
||||
case 'generating_image':
|
||||
return <Image />;
|
||||
return <Image color="primary" />;
|
||||
case 'thinking':
|
||||
return <Psychology />;
|
||||
return <Psychology color="secondary" />;
|
||||
case 'tooling':
|
||||
return <Build />;
|
||||
return <Build color="action" />;
|
||||
default:
|
||||
return <Info />; // fallback icon
|
||||
}
|
||||
return <Info color="action" />;
|
||||
}
|
||||
};
|
||||
|
||||
const JobManagement = (props: BackstoryElementProps) => {
|
||||
const { user, apiClient } = useAuth();
|
||||
const { selectedCandidate } = useSelectedCandidate()
|
||||
const { selectedJob, setSelectedJob } = useSelectedJob()
|
||||
const { selectedCandidate } = useSelectedCandidate();
|
||||
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||
const { setSnack, submitQuery } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const [openUploadDialog, setOpenUploadDialog] = useState<boolean>(false);
|
||||
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 [jobLocation, setJobLocation] = useState<string>('');
|
||||
const [jobId, setJobId] = useState<string>('');
|
||||
const [jobStatus, setJobStatus] = useState<string>('');
|
||||
const [jobStatusIcon, setJobStatusIcon] = useState<JSX.Element>(<></>);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
}, [jobTitle, jobDescription, company]);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
||||
if (!user?.id) {
|
||||
return (
|
||||
<LoginRequired asset="candidate analysis" />
|
||||
@ -105,33 +151,46 @@ const JobManagement = (props: BackstoryElementProps) => {
|
||||
|
||||
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.Document) => {
|
||||
onMessage: (document: Types.DocumentMessage) => {
|
||||
if ('document' in document) {
|
||||
console.log('onMessage - document', document);
|
||||
const job: Types.Job = document as any;
|
||||
setJobDescription(job.description);
|
||||
setJobTitle(job.title || '');
|
||||
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]) {
|
||||
@ -159,85 +218,240 @@ const JobManagement = (props: BackstoryElementProps) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Upload file (replace with actual API call)
|
||||
const controller : StreamingResponse<Types.Document> = apiClient.uploadCandidateDocument(file, { isJobDocument: true}, documentStatusHandlers);
|
||||
const document : Types.Document | null = await controller.promise;
|
||||
setIsProcessing(true);
|
||||
setJobDescription('');
|
||||
setJobTitle('');
|
||||
setJobRequirements(null);
|
||||
setSummary('');
|
||||
const controller = apiClient.uploadCandidateDocument(file, { isJobDocument: true, overwrite: true }, documentStatusHandlers);
|
||||
const document = await controller.promise;
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
console.log(`Document id: ${document.id}`)
|
||||
console.log(`Document id: ${document.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 job : Types.Job = {
|
||||
const newJob: Types.Job = {
|
||||
ownerId: user?.id || '',
|
||||
ownerType: 'candidate',
|
||||
description: jobDescription,
|
||||
company: company,
|
||||
summary: summary,
|
||||
title: jobTitle,
|
||||
}
|
||||
apiClient.createJob(job, jobStatusHandlers);
|
||||
}
|
||||
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 </Box>
|
||||
return <Box>You must be logged in</Box>;
|
||||
}
|
||||
return (<>
|
||||
<Paper elevation={3} sx={{ p: 3, pt: 1, mt: 0, mb: 4, borderRadius: 2 }}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', mt: 0, mb: 1, flexDirection: "column" }}>
|
||||
<Typography variant="subtitle1" sx={{ mr: 2 }}>
|
||||
Job Selection
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 1200, 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>
|
||||
<Box sx={{display: "flex", flexDirection: "column"}}>
|
||||
<Button
|
||||
component="label"
|
||||
variant="contained"
|
||||
startIcon={<FileUploadIcon />}
|
||||
size={isMobile ? "small" : "medium"}>
|
||||
Upload
|
||||
disabled={isProcessing}
|
||||
// onClick={handleUploadClick}
|
||||
>
|
||||
Choose File
|
||||
</Button>
|
||||
</UploadBox>
|
||||
<VisuallyHiddenInput
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".txt,.md,.docx,.pdf"
|
||||
onChange={handleJobUpload}
|
||||
/>
|
||||
</Button>
|
||||
<Typography variant="caption">Accepted document formats: .pdf, .docx, .txt, or .md</Typography>
|
||||
</Box>
|
||||
<Box>{jobStatusIcon} {jobStatus}</Box>
|
||||
</Box>
|
||||
</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={12}
|
||||
placeholder="Enter the job description here..."
|
||||
rows={isMobile ? 8 : 12}
|
||||
placeholder="Paste or type the job description here..."
|
||||
variant="outlined"
|
||||
value={jobDescription}
|
||||
onChange={(e) => setJobDescription(e.target.value)}
|
||||
required
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start" sx={{ alignSelf: 'flex-start', mt: 1.5 }}>
|
||||
<DescriptionIcon color="action" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
disabled={isProcessing}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||
The job description will be used to extract requirements for candidate matching.
|
||||
</Typography>
|
||||
{jobRequirements === null && jobDescription && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleExtractRequirements}
|
||||
startIcon={<AutoFixHigh />}
|
||||
disabled={isProcessing}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
Extract Requirements
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Enter Job Details
|
||||
</Typography>
|
||||
|
||||
{(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
|
||||
@ -247,7 +461,10 @@ const JobManagement = (props: BackstoryElementProps) => {
|
||||
value={jobTitle}
|
||||
onChange={(e) => setJobTitle(e.target.value)}
|
||||
required
|
||||
margin="normal"
|
||||
disabled={isProcessing}
|
||||
InputProps={{
|
||||
startAdornment: <Work sx={{ mr: 1, color: 'text.secondary' }} />
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@ -259,34 +476,75 @@ const JobManagement = (props: BackstoryElementProps) => {
|
||||
value={company}
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
required
|
||||
margin="normal"
|
||||
disabled={isProcessing}
|
||||
InputProps={{
|
||||
startAdornment: <Business sx={{ mr: 1, color: 'text.secondary' }} />
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
{/* <Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Job Location"
|
||||
variant="outlined"
|
||||
value={jobLocation}
|
||||
onChange={(e) => setJobLocation(e.target.value)}
|
||||
margin="normal"
|
||||
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>
|
||||
}
|
||||
|
||||
</Paper>
|
||||
{/* Requirements Display */}
|
||||
{renderJobRequirements()}
|
||||
|
||||
</>);
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{display: "flex", flexDirection: isMobile ? "column" : "row", gap: 1, m: 0, p: 0}}>
|
||||
<Box sx={{
|
||||
minHeight: '100vh',
|
||||
backgroundColor: 'background.default',
|
||||
pt: { xs: 2, sm: 3 }
|
||||
}}>
|
||||
{selectedJob === null && renderJobCreation()}
|
||||
{/* { selectedJob !== null && renderJob() } */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { JobManagement };
|
@ -57,6 +57,52 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
setExpanded(isExpanded ? panel : false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!job || !job.requirements) {
|
||||
return;
|
||||
}
|
||||
const requirements: { requirement: string, domain: string }[] = [];
|
||||
if (job.requirements?.technicalSkills) {
|
||||
job.requirements.technicalSkills.required?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (required)' }));
|
||||
job.requirements.technicalSkills.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (preferred)' }));
|
||||
}
|
||||
if (job.requirements?.experienceRequirements) {
|
||||
job.requirements.experienceRequirements.required?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (required)' }));
|
||||
job.requirements.experienceRequirements.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (preferred)' }));
|
||||
}
|
||||
if (job.requirements?.softSkills) {
|
||||
job.requirements.softSkills.forEach(req => requirements.push({ requirement: req, domain: 'Soft Skills' }));
|
||||
}
|
||||
if (job.requirements?.experience) {
|
||||
job.requirements.experience.forEach(req => requirements.push({ requirement: req, domain: 'Experience' }));
|
||||
}
|
||||
if (job.requirements?.education) {
|
||||
job.requirements.education.forEach(req => requirements.push({ requirement: req, domain: 'Education' }));
|
||||
}
|
||||
if (job.requirements?.certifications) {
|
||||
job.requirements.certifications.forEach(req => requirements.push({ requirement: req, domain: 'Certifications' }));
|
||||
}
|
||||
if (job.requirements?.preferredAttributes) {
|
||||
job.requirements.preferredAttributes.forEach(req => requirements.push({ requirement: req, domain: 'Preferred Attributes' }));
|
||||
}
|
||||
|
||||
const initialSkillMatches = requirements.map(req => ({
|
||||
requirement: req.requirement,
|
||||
domain: req.domain,
|
||||
status: 'waiting' as const,
|
||||
matchScore: 0,
|
||||
assessment: '',
|
||||
description: '',
|
||||
citations: []
|
||||
}));
|
||||
|
||||
setRequirements(requirements);
|
||||
setSkillMatches(initialSkillMatches);
|
||||
setStatusMessage(null);
|
||||
setLoadingRequirements(false);
|
||||
|
||||
}, [job, setRequirements]);
|
||||
|
||||
useEffect(() => {
|
||||
if (requirementsSession || creatingSession) {
|
||||
return;
|
||||
|
@ -89,6 +89,12 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
}
|
||||
}, [selectedCandidate, activeStep]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedJob && activeStep === 1) {
|
||||
setActiveStep(2);
|
||||
}
|
||||
}, [selectedJob, activeStep]);
|
||||
|
||||
// Steps in our process
|
||||
const steps = [
|
||||
{ index: 1, label: 'Job Selection', icon: <WorkIcon /> },
|
||||
|
@ -622,9 +622,15 @@ class ApiClient {
|
||||
// Job Methods with Date Conversion
|
||||
// ============================
|
||||
|
||||
createJob(job: Omit<Types.Job, 'id' | 'datePosted' | 'views' | 'applicationCount'>, streamingOptions?: StreamingOptions<Types.Job>): StreamingResponse<Types.Job> {
|
||||
async createJob(job: Omit<Types.Job, 'id' | 'datePosted' | 'views' | 'applicationCount'>): Promise<Types.Job> {
|
||||
const body = JSON.stringify(formatApiRequest(job));
|
||||
return this.streamify<Types.Job>(`/jobs`, body, streamingOptions);
|
||||
const response = await fetch(`${this.baseUrl}/jobs`, {
|
||||
method: 'POST',
|
||||
headers: this.defaultHeaders,
|
||||
body: body
|
||||
});
|
||||
|
||||
return this.handleApiResponseWithConversion<Types.Job>(response, 'Job');
|
||||
}
|
||||
|
||||
async getJob(id: string): Promise<Types.Job> {
|
||||
@ -824,7 +830,7 @@ class ApiClient {
|
||||
const document : Types.Document = await controller.promise;
|
||||
console.log(`Document id: ${document.id}`)
|
||||
*/
|
||||
uploadCandidateDocument(file: File, options: Types.DocumentOptions, streamingOptions?: StreamingOptions<Types.Document>): StreamingResponse<Types.Document> {
|
||||
uploadCandidateDocument(file: File, options: Types.DocumentOptions, streamingOptions?: StreamingOptions<Types.DocumentMessage>): StreamingResponse<Types.DocumentMessage> {
|
||||
const convertedOptions = toSnakeCase(options);
|
||||
const formData = new FormData()
|
||||
formData.append('file', file);
|
||||
@ -837,7 +843,7 @@ class ApiClient {
|
||||
'Authorization': this.defaultHeaders['Authorization']
|
||||
}
|
||||
};
|
||||
return this.streamify<Types.Document>('/candidates/documents/upload', formData, streamingOptions);
|
||||
return this.streamify<Types.DocumentMessage>('/candidates/documents/upload', formData, streamingOptions);
|
||||
// {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
@ -1003,12 +1009,6 @@ class ApiClient {
|
||||
|
||||
let messageId = '';
|
||||
let finalMessage : T | null = null;
|
||||
console.log('streamify: ', {
|
||||
api,
|
||||
method,
|
||||
headers,
|
||||
body: data
|
||||
});
|
||||
const promise = new Promise<T>(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}${api}`, {
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Generated TypeScript types from Pydantic models
|
||||
// Source: src/backend/models.py
|
||||
// Generated on: 2025-06-05T20:17:00.575243
|
||||
// Generated on: 2025-06-05T22:02:22.004513
|
||||
// DO NOT EDIT MANUALLY - This file is auto-generated
|
||||
|
||||
// ============================
|
||||
@ -502,11 +502,14 @@ export interface DocumentMessage {
|
||||
type: "binary" | "text" | "json";
|
||||
timestamp?: Date;
|
||||
document: Document;
|
||||
content?: string;
|
||||
converted: boolean;
|
||||
}
|
||||
|
||||
export interface DocumentOptions {
|
||||
includeInRAG?: boolean;
|
||||
isJobDocument?: boolean;
|
||||
overwrite?: boolean;
|
||||
}
|
||||
|
||||
export interface DocumentUpdateRequest {
|
||||
|
@ -132,21 +132,20 @@ class JobRequirementsAgent(Agent):
|
||||
yield error_message
|
||||
return
|
||||
|
||||
job_requirements : JobRequirements | None = None
|
||||
requirements = None
|
||||
job_requirements_data = ""
|
||||
company_name = ""
|
||||
job_summary = ""
|
||||
job_title = ""
|
||||
company = ""
|
||||
summary = ""
|
||||
title = ""
|
||||
try:
|
||||
json_str = self.extract_json_from_text(generated_message.content)
|
||||
job_requirements_data = json.loads(json_str)
|
||||
job_requirements_data = job_requirements_data.get("job_requirements", None)
|
||||
job_title = job_requirements_data.get("job_title", "")
|
||||
company_name = job_requirements_data.get("company_name", "")
|
||||
job_summary = job_requirements_data.get("job_summary", "")
|
||||
job_requirements = JobRequirements.model_validate(job_requirements_data)
|
||||
if not job_requirements:
|
||||
raise ValueError("Job requirements data is empty or invalid.")
|
||||
requirements_json = json.loads(json_str)
|
||||
|
||||
company = requirements_json.get("company_name", "")
|
||||
title = requirements_json.get("job_title", "")
|
||||
summary = requirements_json.get("job_summary", "")
|
||||
job_requirements_data = requirements_json.get("job_requirements", None)
|
||||
requirements = JobRequirements.model_validate(job_requirements_data)
|
||||
except json.JSONDecodeError as e:
|
||||
status_message.status = ApiStatusType.ERROR
|
||||
status_message.content = f"Failed to parse job requirements JSON: {str(e)}\n\n{job_requirements_data}"
|
||||
@ -157,6 +156,7 @@ class JobRequirementsAgent(Agent):
|
||||
status_message.status = ApiStatusType.ERROR
|
||||
status_message.content = f"Job requirements validation error: {str(e)}\n\n{job_requirements_data}"
|
||||
logger.error(f"⚠️ {status_message.content}")
|
||||
logger.error(f"Content: {prompt}")
|
||||
yield status_message
|
||||
return
|
||||
except Exception as e:
|
||||
@ -169,14 +169,13 @@ class JobRequirementsAgent(Agent):
|
||||
job_requirements_message = JobRequirementsMessage(
|
||||
session_id=session_id,
|
||||
status=ApiStatusType.DONE,
|
||||
requirements=job_requirements,
|
||||
company=company_name,
|
||||
title=job_title,
|
||||
summary=job_summary,
|
||||
requirements=requirements,
|
||||
company=company,
|
||||
title=title,
|
||||
summary=summary,
|
||||
description=prompt,
|
||||
)
|
||||
yield job_requirements_message
|
||||
|
||||
logger.info(f"✅ Job requirements analysis completed successfully.")
|
||||
return
|
||||
|
||||
|
@ -1730,7 +1730,7 @@ async def upload_candidate_document(
|
||||
)
|
||||
|
||||
"""Upload a document for the current candidate"""
|
||||
async def upload_stream_generator():
|
||||
async def upload_stream_generator(file_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}")
|
||||
@ -1763,6 +1763,7 @@ async def upload_candidate_document(
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
file_path = os.path.join(dir_path, file.filename)
|
||||
if os.path.exists(file_path):
|
||||
if not options.overwrite:
|
||||
logger.warning(f"⚠️ File already exists: {file_path}")
|
||||
error_message = ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
@ -1770,6 +1771,14 @@ async def upload_candidate_document(
|
||||
)
|
||||
yield error_message
|
||||
return
|
||||
else:
|
||||
logger.info(f"🔄 Overwriting existing file: {file_path}")
|
||||
status_message = ChatMessageStatus(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content=f"Overwriting existing file: {file.filename}",
|
||||
activity=ApiActivityType.INFO
|
||||
)
|
||||
yield status_message
|
||||
|
||||
# Validate file type
|
||||
allowed_types = ['.txt', '.md', '.docx', '.pdf', '.png', '.jpg', '.jpeg', '.gif']
|
||||
@ -1818,6 +1827,7 @@ async def upload_candidate_document(
|
||||
yield error_message
|
||||
return
|
||||
|
||||
converted = False;
|
||||
if document_type != DocumentType.MARKDOWN and document_type != DocumentType.TXT:
|
||||
p = pathlib.Path(file_path)
|
||||
p_as_md = p.with_suffix(".md")
|
||||
@ -1828,7 +1838,7 @@ async def upload_candidate_document(
|
||||
):
|
||||
status_message = ChatMessageStatus(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content=f"Converting {file.filename} to Markdown format for better processing...",
|
||||
content=f"Converting content from {document_type}...",
|
||||
activity=ApiActivityType.CONVERTING
|
||||
)
|
||||
yield status_message
|
||||
@ -1837,6 +1847,9 @@ async def upload_candidate_document(
|
||||
md = MarkItDown(enable_plugins=False) # Set to True to enable plugins
|
||||
result = md.convert(file_path, output_format="markdown")
|
||||
p_as_md.write_text(result.text_content)
|
||||
file_content = result.text_content
|
||||
converted = True
|
||||
logger.info(f"✅ Converted {file.filename} to Markdown format: {p_as_md}")
|
||||
file_path = p_as_md
|
||||
except Exception as e:
|
||||
error_message = ChatMessageError(
|
||||
@ -1856,21 +1869,23 @@ async def upload_candidate_document(
|
||||
type=ApiMessageType.JSON,
|
||||
status=ApiStatusType.DONE,
|
||||
document=document_data,
|
||||
converted=converted,
|
||||
content=file_content,
|
||||
)
|
||||
yield chat_message
|
||||
|
||||
# If this is a job description, process it with the job requirements agent
|
||||
if options.is_job_document:
|
||||
content = None
|
||||
with open(file_path, "r") as f:
|
||||
content = f.read()
|
||||
if not content or len(content) == 0:
|
||||
error_message = ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content="Job description file is empty"
|
||||
)
|
||||
yield error_message
|
||||
if not options.is_job_document:
|
||||
return
|
||||
|
||||
status_message = ChatMessageStatus(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content=f"Initiating connection with {candidate.first_name}'s AI agent...",
|
||||
activity=ApiActivityType.INFO
|
||||
)
|
||||
yield status_message
|
||||
await asyncio.sleep(0)
|
||||
|
||||
async with entities.get_candidate_entity(candidate=candidate) as candidate_entity:
|
||||
chat_agent = candidate_entity.get_or_create_agent(agent_type=ChatContextType.JOB_REQUIREMENTS)
|
||||
if not chat_agent:
|
||||
@ -1881,14 +1896,21 @@ async def upload_candidate_document(
|
||||
yield error_message
|
||||
return
|
||||
message = None
|
||||
status_message = ChatMessageStatus(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
content=f"Analyzing document for company and requirement details...",
|
||||
activity=ApiActivityType.SEARCHING
|
||||
)
|
||||
yield status_message
|
||||
await asyncio.sleep(0)
|
||||
|
||||
async for message in chat_agent.generate(
|
||||
llm=llm_manager.get_llm(),
|
||||
model=defines.model,
|
||||
session_id=MOCK_UUID,
|
||||
prompt=content
|
||||
prompt=file_content
|
||||
):
|
||||
if message.status != ApiStatusType.DONE:
|
||||
yield message
|
||||
pass
|
||||
if not message or not isinstance(message, JobRequirementsMessage):
|
||||
error_message = ChatMessageError(
|
||||
session_id=MOCK_UUID, # No session ID for document uploads
|
||||
@ -1912,7 +1934,7 @@ async def upload_candidate_document(
|
||||
|
||||
# return DebugStreamingResponse(
|
||||
return StreamingResponse(
|
||||
to_json(upload_stream_generator()),
|
||||
to_json(upload_stream_generator(file_content)),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
|
@ -521,6 +521,7 @@ class DocumentType(str, Enum):
|
||||
class DocumentOptions(BaseModel):
|
||||
include_in_RAG: Optional[bool] = Field(True, alias="includeInRAG")
|
||||
is_job_document: Optional[bool] = Field(False, alias="isJobDocument")
|
||||
overwrite: Optional[bool] = Field(False, alias="overwrite")
|
||||
model_config = {
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
@ -829,6 +830,8 @@ class JobRequirementsMessage(ApiMessage):
|
||||
class DocumentMessage(ApiMessage):
|
||||
type: ApiMessageType = ApiMessageType.JSON
|
||||
document: Document = Field(..., alias="document")
|
||||
content: Optional[str] = ""
|
||||
converted: bool = Field(False, alias="converted")
|
||||
model_config = {
|
||||
"populate_by_name": True # Allow both field names and aliases
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user