= (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
-
-
- }
- fullWidth
- >
- Create New
-
-
-
-
- {/* 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
-
-
-
- }
- fullWidth
- >
- Edit Backstory
-
-
-
-
- {/* Improvement Suggestions Card */}
-
-
-
- Improvement Suggestions
-
-
-
-
- • Add certifications
-
-
- • Enhance your project details
-
-
-
- }
- fullWidth
- >
- View All Tips
-
-
-
-
-
-
-
- );
-};
-
-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 (
-
- {value === index && (
-
- {children}
-
- )}
-
- );
-}
-
-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
- }
- onClick={() => setSkillDialog(true)}
- fullWidth={isMobile}
- size={isMobile ? "small" : "medium"}
- >
- Add Skill
-
-
-
-
- {(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
- }
- onClick={() => setExperienceDialog(true)}
- fullWidth={isMobile}
- size={isMobile ? "small" : "medium"}
- >
- Add 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 */}
-
-
- {/* Add Experience Dialog */}
-
-
- {/* 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(...),