diff --git a/frontend/src/components/JobCreator.tsx b/frontend/src/components/JobCreator.tsx index dfe936e..d248f47 100644 --- a/frontend/src/components/JobCreator.tsx +++ b/frontend/src/components/JobCreator.tsx @@ -337,10 +337,22 @@ const JobCreator = (props: JobCreator) => { onSave ? onSave(job) : setSelectedJob(job); }; - const handleExtractRequirements = () => { - // Implement requirements extraction logic here - setIsProcessing(true); - // This would call your API to extract requirements from the job description + const handleExtractRequirements = async () => { + try { + setIsProcessing(true); + const controller = apiClient.createJobFromDescription(jobDescription, jobStatusHandlers); + const job = await controller.promise; + if (!job) { + setIsProcessing(false); + return; + } + console.log(`Job id: ${job.id}`); + } catch (error) { + console.error(error); + setSnack('Failed to upload document', 'error'); + setIsProcessing(false); + } + setIsProcessing(false); }; const renderJobCreation = () => { diff --git a/frontend/src/components/JobManagement.tsx b/frontend/src/components/JobManagement.tsx deleted file mode 100644 index 59834b5..0000000 --- a/frontend/src/components/JobManagement.tsx +++ /dev/null @@ -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 ; - case 'heartbeat': - return ; - case 'system': - return ; - case 'info': - return ; - case 'searching': - return ; - case 'generating': - return ; - case 'generating_image': - return ; - case 'thinking': - return ; - case 'tooling': - return ; - default: - return ; - } -}; - -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(''); - const [jobRequirements, setJobRequirements] = useState(null); - const [jobTitle, setJobTitle] = useState(''); - const [company, setCompany] = useState(''); - const [summary, setSummary] = useState(''); - const [jobStatus, setJobStatus] = useState(''); - const [jobStatusIcon, setJobStatusIcon] = useState(<>); - const [isProcessing, setIsProcessing] = useState(false); - - const fileInputRef = useRef(null); - - if (!user?.id) { - return ( - - ); - } - - 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) => { - 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 ( - - - {icon} - - {title} - - {required && } - - - {items.map((item, index) => ( - - ))} - - - ); - }; - - const renderJobRequirements = () => { - if (!jobRequirements) return null; - - return ( - - } - sx={{ pb: 1 }} - /> - - {renderRequirementSection( - "Technical Skills (Required)", - jobRequirements.technicalSkills.required, - , - true - )} - {renderRequirementSection( - "Technical Skills (Preferred)", - jobRequirements.technicalSkills.preferred, - - )} - {renderRequirementSection( - "Experience Requirements (Required)", - jobRequirements.experienceRequirements.required, - , - true - )} - {renderRequirementSection( - "Experience Requirements (Preferred)", - jobRequirements.experienceRequirements.preferred, - - )} - {renderRequirementSection( - "Soft Skills", - jobRequirements.softSkills, - - )} - {renderRequirementSection( - "Experience", - jobRequirements.experience, - - )} - {renderRequirementSection( - "Education", - jobRequirements.education, - - )} - {renderRequirementSection( - "Certifications", - jobRequirements.certifications, - - )} - {renderRequirementSection( - "Preferred Attributes", - jobRequirements.preferredAttributes, - - )} - - - ); - }; - - 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 You must be logged in; - } - - return ( - - {/* Upload Section */} - - } - /> - - - - - - Upload Job Description - - - - - Drop your job description here - - - Supported formats: PDF, DOCX, TXT, MD - - - - - - - - - - Or Enter Manually - - setJobDescription(e.target.value)} - disabled={isProcessing} - sx={{ mb: 2 }} - /> - {jobRequirements === null && jobDescription && ( - - )} - - - - {(jobStatus || isProcessing) && ( - - - {jobStatusIcon} - - {jobStatus || 'Processing...'} - - - {isProcessing && } - - )} - - - - {/* Job Details Section */} - - } - /> - - - - setJobTitle(e.target.value)} - required - disabled={isProcessing} - InputProps={{ - startAdornment: - }} - /> - - - - setCompany(e.target.value)} - required - disabled={isProcessing} - InputProps={{ - startAdornment: - }} - /> - - - {/* - setJobLocation(e.target.value)} - disabled={isProcessing} - InputProps={{ - startAdornment: - }} - /> - */} - - - - - - - - - - - {/* Job Summary */} - {summary !== '' && - - } - sx={{ pb: 1 }} - /> - - {summary} - - - } - - {/* Requirements Display */} - {renderJobRequirements()} - - - ); - }; - - return ( - - {selectedJob === null && renderJobCreation()} - - ); -}; - -export { JobManagement }; \ No newline at end of file diff --git a/frontend/src/components/layout/BackstoryLayout.tsx b/frontend/src/components/layout/BackstoryLayout.tsx index a00eb26..0744d2e 100644 --- a/frontend/src/components/layout/BackstoryLayout.tsx +++ b/frontend/src/components/layout/BackstoryLayout.tsx @@ -47,7 +47,7 @@ const CandidateNavItems : NavigationLinkType[]= [ { label: 'Resume Builder', path: '/candidate/resume-builder', icon: }, // { label: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: }, // { label: 'Dashboard', icon: , path: '/candidate/dashboard' }, - // { label: 'Profile', icon: , path: '/candidate/profile' }, + // { label: 'Profile', icon: , path: '/candidate/dashboard/profile' }, // { label: 'Backstory', icon: , path: '/candidate/backstory' }, // { label: 'Resumes', icon: , path: '/candidate/resumes' }, // { label: 'Q&A Setup', icon: , path: '/candidate/qa-setup' }, diff --git a/frontend/src/components/layout/BackstoryRoutes.tsx b/frontend/src/components/layout/BackstoryRoutes.tsx index 660ba15..2ecf550 100644 --- a/frontend/src/components/layout/BackstoryRoutes.tsx +++ b/frontend/src/components/layout/BackstoryRoutes.tsx @@ -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 = () => (Backstory); @@ -69,8 +68,8 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNod if (user.userType === 'candidate') { routes.splice(-1, 0, ...[ - } />, - } />, + } />, + } />, } />, } />, } />, diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index 528e592..9452c5a 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -232,7 +232,7 @@ const Header: React.FC = (props: HeaderProps) => { id: 'profile', label: 'Profile', icon: , - action: () => navigate(`/${user?.userType}/profile`) + action: () => navigate(`/${user?.userType}/dashboard/profile`) }, { id: 'dashboard', diff --git a/frontend/src/components/ui/JobInfo.tsx b/frontend/src/components/ui/JobInfo.tsx index c850c01..fe6931e 100644 --- a/frontend/src/components/ui/JobInfo.tsx +++ b/frontend/src/components/ui/JobInfo.tsx @@ -67,13 +67,23 @@ const JobInfo: React.FC = (props: JobInfoProps) => { Location: {job.location.city}, {job.location.state || job.location.country} } + {job.title && + + Title: {job.title} + + } + {/* {job.datePosted && + + Posted: {job.datePosted.toISOString()} + + } */} {job.company && Company: {job.company} } {job.summary && - Summary: {job.summary} + Summary: {job.summary} } } diff --git a/frontend/src/pages/BetaPage.tsx b/frontend/src/pages/BetaPage.tsx index 7555fd9..db82c84 100644 --- a/frontend/src/pages/BetaPage.tsx +++ b/frontend/src/pages/BetaPage.tsx @@ -87,6 +87,7 @@ const BetaPage: React.FC = ({ = (props: DashboardProps) => { - const navigate = useNavigate(); - const { setSnack } = props; - const { user, isLoading, isInitializing, isAuthenticated } = useAuth(); - const profileCompletion = 75; - const sidebarItems = [ - { icon: , text: 'Dashboard', active: true }, - { icon: , text: 'Profile', active: false }, - { icon: , text: 'Backstory', active: false }, - { icon: , text: 'Resumes', active: false }, - { icon: , text: 'Q&A Setup', active: false }, - { icon: , text: 'Analytics', active: false }, - { icon: , text: 'Settings', active: false }, - ]; - - if (isLoading || isInitializing) { - return (); - } - if (!user || !isAuthenticated) { - return (); - } - if (user.userType !== 'candidate') { - setSnack(`The page you were on is only available for candidates (you are a ${user.userType}`, 'warning'); - navigate('/'); - return (<>); - } - - return ( - - {/* Sidebar */} - - - JobPortal - - - - {sidebarItems.map((item, index) => ( - - - - {item.icon} - - - - - ))} - - - - {/* Main Content */} - - {/* Welcome Section */} - - - Welcome back, {user.firstName}! - - - - - Your profile is {profileCompletion}% complete - - - - - - - - {/* Cards Grid */} - - {/* Top Row */} - - {/* Resume Builder Card */} - - - - Resume Builder - - - - 3 custom resumes - - - - Last created: May 15, 2025 - - - - - - - {/* Recent Activity Card */} - - - - Recent Activity - - - - - - 5 profile views - - - - - 2 resume downloads - - - - - 1 direct contact - - - - - - - - - {/* Bottom Row */} - - {/* Complete Your Backstory Card */} - - - - Complete Your Backstory - - - - - • Add projects - - - • Detail skills - - - • Work history - - - - - - - - {/* Improvement Suggestions Card */} - - - - Improvement Suggestions - - - - - • Add certifications - - - • Enhance your project details - - - - - - - - - - - ); -}; - -export { CandidateDashboardPage }; \ No newline at end of file diff --git a/frontend/src/pages/JobAnalysisPage.tsx b/frontend/src/pages/JobAnalysisPage.tsx index a7b7123..f3deda8 100644 --- a/frontend/src/pages/JobAnalysisPage.tsx +++ b/frontend/src/pages/JobAnalysisPage.tsx @@ -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'; diff --git a/frontend/src/pages/candidate/Profile.tsx b/frontend/src/pages/candidate/Profile.tsx deleted file mode 100644 index 660a3aa..0000000 --- a/frontend/src/pages/candidate/Profile.tsx +++ /dev/null @@ -1,983 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - Box, - Button, - Container, - Grid, - Paper, - TextField, - Typography, - Avatar, - IconButton, - Tabs, - Tab, - useMediaQuery, - CircularProgress, - Snackbar, - Alert, - Card, - CardContent, - CardActions, - Chip, - Divider, - List, - ListItem, - ListItemText, - ListItemSecondaryAction, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - MenuItem, - Select, - FormControl, - InputLabel, - Switch, - FormControlLabel -} from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { - CloudUpload, - PhotoCamera, - Edit, - Save, - Cancel, - Add, - Delete, - Work, - School, - Language, - EmojiEvents, - LocationOn, - Phone, - Email, - AccountCircle, - BubbleChart -} from '@mui/icons-material'; -import { useTheme } from '@mui/material/styles'; -import { useAuth } from "hooks/AuthContext"; -import * as Types from 'types/types'; -import { ComingSoon } from 'components/ui/ComingSoon'; -import { VectorVisualizer } from 'components/VectorVisualizer'; -import { BackstoryPageProps } from 'components/BackstoryTab'; -import { DocumentManager } from 'components/DocumentManager'; - -// Styled components -const VisuallyHiddenInput = styled('input')({ - clip: 'rect(0 0 0 0)', - clipPath: 'inset(50%)', - height: 1, - overflow: 'hidden', - position: 'absolute', - bottom: 0, - left: 0, - whiteSpace: 'nowrap', - width: 1, -}); - -interface TabPanelProps { - children?: React.ReactNode; - index: number; - value: number; -} - -function TabPanel(props: TabPanelProps) { - const { children, value, index, ...other } = props; - - return ( - - ); -} - -const CandidateProfilePage: React.FC = (props: BackstoryPageProps) => { - const { setSnack, submitQuery } = props; - const backstoryProps = { setSnack, submitQuery }; - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - const { user, updateUserData, apiClient } = useAuth(); - - // Check if user is a candidate - const candidate = user?.userType === 'candidate' ? user as Types.Candidate : null; - - // State management - const [tabValue, setTabValue] = useState(0); - const [editMode, setEditMode] = useState<{ [key: string]: boolean }>({}); - const [loading, setLoading] = useState(false); - const [snackbar, setSnackbar] = useState<{ - open: boolean; - message: string; - severity: "success" | "error" | "info" | "warning"; - }>({ - open: false, - message: '', - severity: 'success' - }); - - // Form data state - const [formData, setFormData] = useState>({}); - const [profileImage, setProfileImage] = useState(null); - - // Dialog states - const [skillDialog, setSkillDialog] = useState(false); - const [experienceDialog, setExperienceDialog] = useState(false); - const [educationDialog, setEducationDialog] = useState(false); - const [languageDialog, setLanguageDialog] = useState(false); - const [certificationDialog, setCertificationDialog] = useState(false); - - // New item states - const [newSkill, setNewSkill] = useState>({ - name: '', - category: '', - level: 'beginner', - yearsOfExperience: 0 - }); - const [newExperience, setNewExperience] = useState>({ - companyName: '', - position: '', - startDate: new Date(), - isCurrent: false, - description: '', - skills: [], - location: { city: '', country: '' } - }); - const [newEducation, setNewEducation] = useState>({ - institution: '', - degree: '', - fieldOfStudy: '', - startDate: new Date(), - isCurrent: false - }); - const [newLanguage, setNewLanguage] = useState>({ - language: '', - proficiency: 'basic' - }); - const [newCertification, setNewCertification] = useState>({ - name: '', - issuingOrganization: '', - issueDate: new Date() - }); - - useEffect(() => { - if (candidate) { - setFormData(candidate); - setProfileImage(candidate.profileImage || null); - } - }, [candidate]); - - if (!candidate) { - return ( - - - Access denied. This page is only available for candidates. - - - ); - } - - // Handle tab change - const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { - setTabValue(newValue); - }; - - // Handle form input changes - const handleInputChange = (field: string, value: any) => { - setFormData({ - ...formData, - [field]: value, - }); - }; - - // Handle profile image upload - const handleImageUpload = async (e: React.ChangeEvent) => { - if (e.target.files && e.target.files[0]) { - if (await apiClient.uploadCandidateProfile(e.target.files[0])) { - candidate.profileImage = 'profile.' + e.target.files[0].name.replace(/^.*\./, ''); - console.log(`Set profile image to: ${candidate.profileImage}`); - updateUserData(candidate); - } - } - }; - - // Toggle edit mode for a section - const toggleEditMode = (section: string) => { - setEditMode({ - ...editMode, - [section]: !editMode[section] - }); - }; - - // Save changes - const handleSave = async (section: string) => { - setLoading(true); - try { - if (candidate.id) { - const updatedCandidate = await apiClient.updateCandidate(candidate.id, formData); - updateUserData(updatedCandidate); - setSnackbar({ - open: true, - message: 'Profile updated successfully!', - severity: 'success' - }); - toggleEditMode(section); - } - } catch (error) { - setSnackbar({ - open: true, - message: 'Failed to update profile. Please try again.', - severity: 'error' - }); - } finally { - setLoading(false); - } - }; - - // Cancel edit - const handleCancel = (section: string) => { - setFormData(candidate); - toggleEditMode(section); - }; - - // Add new skill - const handleAddSkill = () => { - if (newSkill.name && newSkill.category) { - const updatedSkills = [...(formData.skills || []), newSkill as Types.Skill]; - setFormData({ ...formData, skills: updatedSkills }); - setNewSkill({ name: '', category: '', level: 'beginner', yearsOfExperience: 0 }); - setSkillDialog(false); - } - }; - - // Remove skill - const handleRemoveSkill = (index: number) => { - const updatedSkills = (formData.skills || []).filter((_, i) => i !== index); - setFormData({ ...formData, skills: updatedSkills }); - }; - - // Add new work experience - const handleAddExperience = () => { - if (newExperience.companyName && newExperience.position) { - const updatedExperience = [...(formData.experience || []), newExperience as Types.WorkExperience]; - setFormData({ ...formData, experience: updatedExperience }); - setNewExperience({ - companyName: '', - position: '', - startDate: new Date(), - isCurrent: false, - description: '', - skills: [], - location: { city: '', country: '' } - }); - setExperienceDialog(false); - } - }; - - // Remove work experience - const handleRemoveExperience = (index: number) => { - const updatedExperience = (formData.experience || []).filter((_, i) => i !== index); - setFormData({ ...formData, experience: updatedExperience }); - }; - - // Basic Information Tab - const renderBasicInfo = () => ( - - - - - {!profileImage && } - - {editMode.basic && ( - <> - - - - - - Update profile photo - - - )} - - - - - {editMode.basic ? ( - handleInputChange('firstName', e.target.value)} - variant="outlined" - /> - ) : (<> - First Name - {candidate.firstName} - )} - - - - {editMode.basic ? ( - handleInputChange('lastName', e.target.value)} - variant="outlined" - /> - ) : (<> - Last Name - {candidate.lastName} - )} - - - - {(false && editMode.basic) ? ( - handleInputChange('email', e.target.value)} - variant="outlined" - /> - ) : (<> - - Email - {candidate.email} - - )} - - - - {editMode.basic ? ( - handleInputChange('phone', e.target.value)} - variant="outlined" - /> - ) : (<> - - Phone - {candidate.phone || 'Not provided'} - - )} - - - - {editMode.basic ? ( - handleInputChange('description', e.target.value)} - variant="outlined" - /> - ) : (<> - Professional Summary - {candidate.description || 'No summary provided'} - )} - - - - {false && editMode.basic ? ( - handleInputChange('location', { - ...formData.location, - city: e.target.value - })} - variant="outlined" - placeholder="City, State, Country" - /> - ) : (<> - - Location - {candidate.location?.city || 'Not specified'} {candidate.location?.country || ''} - - )} - - - - - {editMode.basic ? ( - <> - - - - ) : ( - - )} - - - - ); - - // Skills Tab - const renderSkills = () => ( - - - Skills & Expertise - - - - - {(formData.skills || []).map((skill, index) => ( - - - - - - - {skill.name} - - - {skill.category} - - - {skill.yearsOfExperience && ( - - {skill.yearsOfExperience} years experience - - )} - - handleRemoveSkill(index)} - color="error" - sx={{ ml: 1 }} - > - - - - - - - ))} - - - {(!formData.skills || formData.skills.length === 0) && ( - - No skills added yet. Click "Add Skill" to get started. - - )} - - ); - - // Experience Tab - const renderExperience = () => ( - - - Work Experience - - - - {(formData.experience || []).map((exp, index) => ( - - - - - - {exp.position} - - - {exp.companyName} - - - {exp.startDate?.toLocaleDateString()} - {exp.isCurrent ? 'Present' : exp.endDate?.toLocaleDateString()} - - - {exp.description} - - {exp.skills && exp.skills.length > 0 && ( - - {exp.skills.map((skill, skillIndex) => ( - - ))} - - )} - - handleRemoveExperience(index)} - color="error" - size="small" - sx={{ - alignSelf: { xs: 'flex-end', sm: 'flex-start' }, - ml: { sm: 1 } - }} - > - - - - - - ))} - - {(!formData.experience || formData.experience.length === 0) && ( - - No work experience added yet. Click "Add Experience" to get started. - - )} - - ); - - // Resume Tab - const renderResume = () => ( - - ); - - return ( - - - - - } - iconPosition={isMobile ? "top" : "start"} - /> - } - iconPosition={isMobile ? "top" : "start"} - /> - } - iconPosition={isMobile ? "top" : "start"} - /> - } - iconPosition={isMobile ? "top" : "start"} - /> - } - iconPosition={isMobile ? "top" : "start"} - /> - } - iconPosition={isMobile ? "top" : "start"} - /> - - - - - {renderBasicInfo()} - - - - {renderSkills()} - - - - {renderExperience()} - - - - - Education (Coming Soon) - - Education management will be available in a future update. - - - - - - {renderResume()} - - - - - - - - {/* Add Skill Dialog */} - setSkillDialog(false)} - maxWidth="sm" - fullWidth - fullScreen={isMobile} - PaperProps={{ - sx: { - ...(isMobile && { - margin: 0, - width: '100%', - height: '100%', - maxHeight: '100%' - }) - } - }} - > - Add New Skill - - - - setNewSkill({ ...newSkill, name: e.target.value })} - size={isMobile ? "small" : "medium"} - /> - - - setNewSkill({ ...newSkill, category: e.target.value })} - placeholder="e.g., Programming, Design, Marketing" - size={isMobile ? "small" : "medium"} - /> - - - - Proficiency Level - - - - - setNewSkill({ ...newSkill, yearsOfExperience: parseInt(e.target.value) || 0 })} - size={isMobile ? "small" : "medium"} - /> - - - - - - - - - - {/* Add Experience Dialog */} - setExperienceDialog(false)} - maxWidth="md" - fullWidth - fullScreen={isMobile} - PaperProps={{ - sx: { - ...(isMobile && { - margin: 0, - width: '100%', - height: '100%', - maxHeight: '100%' - }) - } - }} - > - Add Work Experience - - - - setNewExperience({ ...newExperience, companyName: e.target.value })} - size={isMobile ? "small" : "medium"} - /> - - - setNewExperience({ ...newExperience, position: e.target.value })} - size={isMobile ? "small" : "medium"} - /> - - - setNewExperience({ ...newExperience, startDate: new Date(e.target.value) })} - InputLabelProps={{ shrink: true }} - size={isMobile ? "small" : "medium"} - /> - - - setNewExperience({ ...newExperience, isCurrent: e.target.checked })} - size={isMobile ? "small" : "medium"} - /> - } - label="Currently working here" - sx={{ - '& .MuiFormControlLabel-label': { - fontSize: { xs: '0.875rem', sm: '1rem' } - } - }} - /> - - - setNewExperience({ ...newExperience, description: e.target.value })} - placeholder="Describe your responsibilities and achievements..." - size={isMobile ? "small" : "medium"} - /> - - - - - - - - - - {/* Snackbar for notifications */} - setSnackbar({ ...snackbar, open: false })} - > - setSnackbar({ ...snackbar, open: false })} - severity={snackbar.severity} - sx={{ width: '100%' }} - > - {snackbar.message} - - - - ); -}; - -export { CandidateProfilePage }; \ No newline at end of file diff --git a/frontend/src/services/api-client.ts b/frontend/src/services/api-client.ts index 36b7b06..ba26d6c 100644 --- a/frontend/src/services/api-client.ts +++ b/frontend/src/services/api-client.ts @@ -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(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): StreamingResponse { + const body = JSON.stringify(job_description); + return this.streamify('/jobs/from-content', body, streamingOptions); + } + async createJob(job: Omit): Promise { 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; diff --git a/src/backend/main.py b/src/backend/main.py index 4af0067..2ae6041 100644 --- a/src/backend/main.py +++ b/src/backend/main.py @@ -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(...),