import React, { useEffect, useRef, useState } from 'react'; import { Box, Typography, SxProps, CardHeader, Button, LinearProgress, IconButton, Tooltip, Card, CardContent, Divider, useTheme, useMediaQuery, Dialog, DialogTitle, DialogContent, DialogActions, Tabs, Tab, Paper, FormControl, Select, MenuItem, InputLabel, Theme, } from '@mui/material'; import PrintIcon from '@mui/icons-material/Print'; import { Delete as DeleteIcon, Restore as RestoreIcon, Save as SaveIcon, Edit as EditIcon, Description as DescriptionIcon, Work as WorkIcon, Person as PersonIcon, Schedule as ScheduleIcon, ModelTraining, Email as EmailIcon, Phone as PhoneIcon, LocationOn as LocationIcon, // Language as WebsiteIcon, } from '@mui/icons-material'; import InputIcon from '@mui/icons-material/Input'; import TuneIcon from '@mui/icons-material/Tune'; import PreviewIcon from '@mui/icons-material/Preview'; import EditDocumentIcon from '@mui/icons-material/EditDocument'; import { parsePhoneNumberFromString } from 'libphonenumber-js'; import { useReactToPrint } from 'react-to-print'; import { useAuth } from 'hooks/AuthContext'; import { useAppState } from 'hooks/GlobalContext'; import { StyledMarkdown } from 'components/StyledMarkdown'; import { Resume } from 'types/types'; import { BackstoryTextField } from 'components/BackstoryTextField'; import { JobInfo } from './JobInfo'; import './ResumeInfo.css'; import { Scrollable } from 'components/Scrollable'; import * as Types from 'types/types'; import { StreamingOptions } from 'services/api-client'; import { StatusBox, StatusIcon } from './StatusIcon'; import { ResumeChat } from 'components/ResumeChat'; interface ResumeInfoProps { resume: Resume; sx?: SxProps; action?: string; elevation?: number; variant?: 'minimal' | 'small' | 'normal' | 'all' | null; } // Resume Style Definitions interface ResumeStyle { name: string; description: string; headerStyle: SxProps; footerStyle: SxProps; contentStyle: SxProps; markdownStyle: SxProps; color: { primary: string; secondary: string; accent: string; text: string; background: string; }; } const generateResumeStyles = () => { const defaultStyle = { display: 'flex', flexDirection: 'row', }; return { classic: { name: 'Classic', description: 'Traditional, professional serif design', headerStyle: { ...defaultStyle, fontFamily: '"Times New Roman", Times, serif', borderBottom: '2px solid #2c3e50', paddingBottom: 2, marginBottom: 3, }, footerStyle: { fontFamily: '"Times New Roman", Times, serif', borderTop: '2px solid #2c3e50', paddingTop: 2, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textTransform: 'uppercase', alignContent: 'center', fontSize: '0.8rem', pb: 2, mb: 2, }, contentStyle: { fontFamily: '"Times New Roman", Times, serif', lineHeight: 1.6, color: '#2c3e50', }, markdownStyle: { fontFamily: '"Times New Roman", Times, serif', '& h1, & h2, & h3': { fontFamily: '"Times New Roman", Times, serif', color: '#2c3e50', borderBottom: '1px solid #bdc3c7', paddingBottom: 1, marginBottom: 2, }, '& p, & li': { lineHeight: 1.6, marginBottom: 1, }, '& ul': { paddingLeft: 3, }, }, color: { primary: '#2c3e50', secondary: '#34495e', accent: '#3498db', text: '#2c3e50', background: '#ffffff', }, }, modern: { name: 'Modern', description: 'Clean, minimalist sans-serif layout', headerStyle: { ...defaultStyle, fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', borderLeft: '4px solid #3498db', paddingLeft: 2, marginBottom: 3, backgroundColor: '#f8f9fa', padding: 2, borderRadius: 1, }, footerStyle: { fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', borderLeft: '4px solid #3498db', backgroundColor: '#f8f9fa', paddingTop: 2, borderRadius: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textTransform: 'uppercase', alignContent: 'center', fontSize: '0.8rem', pb: 2, mb: 2, }, contentStyle: { fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', lineHeight: 1.5, color: '#2c3e50', }, markdownStyle: { fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', '& h1, & h2, & h3': { fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', color: '#3498db', fontWeight: 300, marginBottom: 1.5, }, '& h1': { fontSize: '1.75rem', }, '& h2': { fontSize: '1.5rem', }, '& h3': { fontSize: '1.25rem', }, '& p, & li': { lineHeight: 1.5, marginBottom: 0.75, }, '& ul': { paddingLeft: 2.5, }, }, color: { primary: '#3498db', secondary: '#2c3e50', accent: '#e74c3c', text: '#2c3e50', background: '#ffffff', }, }, creative: { name: 'Creative', description: 'Colorful, unique design with personality', headerStyle: { ...defaultStyle, fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: '#ffffff', padding: 2.5, borderRadius: 1.5, marginBottom: 3, }, footerStyle: { fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: '#ffffff', paddingTop: 2, borderRadius: 1.5, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textTransform: 'uppercase', alignContent: 'center', fontSize: '0.8rem', pb: 2, mb: 2, }, contentStyle: { fontFamily: '"Open Sans", Arial, sans-serif', lineHeight: 1.6, color: '#444444', }, markdownStyle: { fontFamily: '"Open Sans", Arial, sans-serif', '& h1, & h2, & h3': { fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif', color: '#667eea', fontWeight: 600, marginBottom: 2, }, '& h1': { fontSize: '1.5rem', }, '& h2': { fontSize: '1.25rem', }, '& h3': { fontSize: '1.1rem', }, '& p, & li': { lineHeight: 1.6, marginBottom: 1, color: '#444444', }, '& strong': { color: '#764ba2', fontWeight: 600, }, '& ul': { paddingLeft: 3, }, }, color: { primary: '#667eea', secondary: '#764ba2', accent: '#f093fb', text: '#444444', background: '#ffffff', }, }, corporate: { name: 'Corporate', description: 'Formal, structured business format', headerStyle: { ...defaultStyle, fontFamily: '"Arial", sans-serif', border: '2px solid #34495e', padding: 2.5, marginBottom: 3, backgroundColor: '#ecf0f1', }, footerStyle: { fontFamily: '"Arial", sans-serif', border: '2px solid #34495e', backgroundColor: '#ecf0f1', paddingTop: 2, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textTransform: 'uppercase', alignContent: 'center', fontSize: '0.8rem', pb: 2, mb: 2, }, contentStyle: { fontFamily: '"Arial", sans-serif', lineHeight: 1.4, color: '#2c3e50', }, markdownStyle: { fontFamily: '"Arial", sans-serif', '& h1, & h2, & h3': { fontFamily: '"Arial", sans-serif', color: '#34495e', fontWeight: 'bold', textTransform: 'uppercase', fontSize: '0.875rem', letterSpacing: '1px', marginBottom: 1.5, borderBottom: '1px solid #bdc3c7', paddingBottom: 0.5, }, '& h1': { fontSize: '1rem', }, '& h2': { fontSize: '0.875rem', }, '& h3': { fontSize: '0.75rem', }, '& p, & li': { lineHeight: 1.4, marginBottom: 0.75, fontSize: '0.75rem', }, '& ul': { paddingLeft: 2, }, }, color: { primary: '#34495e', secondary: '#2c3e50', accent: '#95a5a6', text: '#2c3e50', background: '#ffffff', }, }, }; }; const resumeStyles: Record = generateResumeStyles(); // Styled Header Component interface BackstoryStyledResumeProps { candidate: Types.Candidate; job?: Types.Job; style: ResumeStyle; } const StyledFooter: React.FC = ({ candidate, job, style }) => { return ( <> Dive deeper into my qualifications at Backstory... {candidate?.username ? `${window.location.protocol}://${window.location.host}/u/${candidate?.username}` : 'backstory'}   ); }; const StyledHeader: React.FC = ({ candidate, style }) => { const phone = parsePhoneNumberFromString(candidate.phone || '', 'US'); return ( {candidate.fullName} {candidate.description && ( {candidate.description} )} {candidate.email && ( {candidate.email} )} {phone?.isValid() && ( {phone.formatInternational()} )} {candidate.location && ( {candidate.location.city ? `${candidate.location.city}, ${candidate.location.state}` : candidate.location.text} )} {/* {(candidate.website || candidate.linkedin) && ( {candidate.website || candidate.linkedin} )} */} ); }; const ResumeInfo: React.FC = (props: ResumeInfoProps) => { const { setSnack } = useAppState(); const { resume } = props; const { user, apiClient } = useAuth(); const { sx, variant = 'normal' } = props; const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')) || variant === 'minimal'; const isAdmin = user?.isAdmin; const [activeResume, setActiveResume] = useState({ ...resume }); const [deleted, setDeleted] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false); const [editContent, setEditContent] = useState(''); const [editSystemPrompt, setEditSystemPrompt] = useState(''); const [editPrompt, setEditPrompt] = useState(''); const [saving, setSaving] = useState(false); const [tabValue, setTabValue] = useState('markdown'); const [jobTabValue, setJobTabValue] = useState('chat'); const [status, setStatus] = useState(''); const [statusType, setStatusType] = useState(null); const [error, setError] = useState(null); const [selectedStyle, setSelectedStyle] = useState('corporate'); const printContentRef = useRef(null); const reactToPrintFn = useReactToPrint({ contentRef: printContentRef, pageStyle: '@page { margin: 8mm !important; }', }); useEffect(() => { if (resume && resume.id !== activeResume?.id) { setActiveResume(resume); } }, [resume, activeResume]); const currentStyle = resumeStyles[selectedStyle]; // Rest of the component remains the same... const deleteResume = async (id: string | undefined): Promise => { if (id) { try { await apiClient.deleteResume(id); setDeleted(true); setSnack('Resume deleted successfully.'); } catch (error) { setSnack('Failed to delete resume.'); } } }; const handleReset = async (): Promise => { setActiveResume({ ...resume }); }; const handleSave = async (): Promise => { setSaving(true); try { const resumeUpdate = { ...activeResume, resume: editContent, systemPrompt: editSystemPrompt, prompt: editPrompt, }; const result = await apiClient.updateResume(resumeUpdate); console.log('Resume updated:', result); const updatedResume = { ...activeResume, ...result, }; setActiveResume(updatedResume); setSnack('Resume updated successfully.'); } catch (error) { setSnack('Failed to update resume.'); } finally { setSaving(false); } }; const handleEditOpen = (): void => { setEditContent(activeResume.resume); setEditSystemPrompt(activeResume.systemPrompt || ''); setEditPrompt(activeResume.prompt || ''); setEditDialogOpen(true); }; if (!resume) { return No resume provided.; } const formatDate = (date: Date | undefined): string => { if (!date) return 'N/A'; try { return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', }).format(date); } catch (error) { console.error('Error formatting date:', error); return 'Invalid date'; } }; const generateResumeHandlers: StreamingOptions = { onMessage: (message: Types.ChatMessageResume) => { const resume: Resume = message.resume; setEditSystemPrompt(resume.systemPrompt || ''); setEditPrompt(resume.prompt || ''); setEditContent(resume.resume); setActiveResume({ ...resume }); setStatus(''); setSnack('Resume generation completed successfully.'); }, onStreaming: (chunk: Types.ChatMessageStreaming) => { if (status === '') { setStatus('Generating resume...'); setStatusType('generating'); } setEditContent(chunk.content); }, onStatus: (status: Types.ChatMessageStatus) => { console.log('status:', status.content); setStatusType(status.activity); setStatus(status.content); }, onError: (error: Types.ChatMessageError) => { console.log('error:', error); setStatusType(null); setStatus(error.content); setError(error); }, }; const generateResume = async (): Promise => { setStatusType('thinking'); setStatus('Starting resume generation...'); setActiveResume({ ...activeResume, resume: '' }); const request = await apiClient.generateResume( activeResume.candidateId || '', activeResume.jobId || '', generateResumeHandlers ); await request.promise; }; const handleTabChange = (event: React.SyntheticEvent, newValue: string): void => { if (newValue === 'print') { reactToPrintFn(); return; } if (newValue === 'regenerate') { setSnack('Regenerating resume...'); generateResume(); return; } setTabValue(newValue); }; const handleJobTabChange = (event: React.SyntheticEvent, newValue: string): void => { setJobTabValue(newValue); }; return ( {/* Header Information */} {activeResume.candidate && ( Candidate )} {activeResume.candidate?.fullName || activeResume.candidateId} {activeResume.job && ( <> Job {activeResume.job.title} at {activeResume.job.company} )} Timeline Created: {formatDate(activeResume.createdAt)} Updated: {formatDate(activeResume.updatedAt)} Job ID: {activeResume.job?.id} Resume ID: {activeResume.id} {/* Resume Content */} {activeResume.resume && ( } sx={{ p: 0, pb: 1 }} action={ } /> {variant === 'all' && activeResume.resume && ( )} )} {/* Admin Controls */} {isAdmin && ( { e.stopPropagation(); handleEditOpen(); }} > { e.stopPropagation(); deleteResume(activeResume.id); }} > { e.stopPropagation(); handleReset(); }} > {saving && ( Saving resume... )} )} {/* Edit Dialog */} { setEditDialogOpen(false); }} maxWidth="lg" fullWidth disableEscapeKeyDown={true} fullScreen={true} > Edit Resume Content Resume for {activeResume.candidate?.fullName || activeResume.candidateId},{' '} {activeResume.job?.title || 'No Job Title Assigned'},{' '} {activeResume.job?.company || 'No Company Assigned'} Resume ID: # {activeResume.id} Last saved:{' '} {activeResume.updatedAt ? new Date(activeResume.updatedAt).toLocaleString() : 'N/A'} {/* Style Selector */} Resume Style } label="Markdown" /> {activeResume.systemPrompt && ( } label="System Prompt" /> )} {activeResume.systemPrompt && ( } label="Prompt" /> )} } label="Preview" /> } label="Print" /> } label="Regenerate" /> {status && ( {statusType && } {status || 'Processing...'} {status && !error && } )} *:not(.Scrollable)': { flexShrink: 0, }, position: 'relative', }} > {tabValue === 'markdown' && ( setEditContent(value)} style={{ position: 'relative', maxHeight: '100%', height: '100%', width: '100%', display: 'flex', minHeight: '100%', flexGrow: 1, flex: 1, overflowY: 'auto', }} placeholder="Enter resume content..." /> )} {tabValue === 'systemPrompt' && ( setEditSystemPrompt(value)} style={{ position: 'relative', maxHeight: '100%', width: '100%', display: 'flex', minHeight: '100%', flexGrow: 1, flex: 1, overflowY: 'auto', }} placeholder="Edit system prompt..." /> )} {tabValue === 'prompt' && ( setEditPrompt(value)} style={{ position: 'relative', maxHeight: '100%', height: '100%', width: '100%', display: 'flex', minHeight: '100%', flexGrow: 1, flex: 1, overflowY: 'auto', }} placeholder="Edit prompt..." /> )} {tabValue === 'preview' && ( {/* Custom Header */} {activeResume.candidate && ( )} {/* Styled Markdown Content */} {/* QR Code Footer */} {activeResume.candidate && activeResume.job && ( )} )} {activeResume.job !== undefined && ( } label="Job" /> )} } label="AI Edit" /> {activeResume.job !== undefined && jobTabValue === 'job' && ( )} {jobTabValue === 'chat' && ( { setEditContent(newResume); setActiveResume({ ...activeResume, resume: newResume }); }} sx={{ m: 1, p: 1, flexGrow: 1, }} /> )} ); }; export { ResumeInfo };