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