2025-07-15 12:03:07 -07:00

1398 lines
44 KiB
TypeScript

import React, { useEffect, useMemo, 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,
Chip,
Alert,
Stack,
SelectChangeEvent,
} 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,
History as HistoryIcon,
RestoreFromTrash as RestoreFromTrashIcon,
Refresh as RefreshIcon,
// 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 revision interface
interface ResumeRevision {
revisionId: string;
revisionTimestamp: string;
updatedAt: string;
createdAt: string;
candidateId: string;
jobId: string;
}
// Resume Style Definitions
interface ResumeStyle {
name: string;
description: string;
headerStyle: SxProps<Theme>;
footerStyle: SxProps<Theme>;
contentStyle: SxProps<Theme>;
markdownStyle: SxProps<Theme>;
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<string, ResumeStyle> = generateResumeStyles();
// Styled Header Component
interface BackstoryStyledResumeProps {
candidate: Types.Candidate;
job?: Types.Job;
style: ResumeStyle;
}
const StyledFooter: React.FC<BackstoryStyledResumeProps> = ({ candidate, job, style }) => {
return (
<>
<Box
className="BackstoryResumeFooter"
sx={{
...style.footerStyle,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textTransform: 'uppercase',
alignContent: 'center',
fontSize: '0.8rem',
pb: 2,
mb: 2,
color: style.color.secondary,
}}
>
Dive deeper into my qualifications at Backstory...
<Box
component="img"
src={`/api/1.0/candidates/qr-code/${candidate.id || ''}/${(job && job.id) || ''}`}
alt="QR Code"
className="qr-code"
sx={{ display: 'flex', mt: 1, mb: 1 }}
/>
{candidate?.username
? `${window.location.protocol}://${window.location.host}/u/${candidate?.username}`
: 'backstory'}
</Box>
<Box sx={{ pb: 2 }}>&nbsp;</Box>
</>
);
};
const StyledHeader: React.FC<BackstoryStyledResumeProps> = ({ candidate, style }) => {
const phone = parsePhoneNumberFromString(candidate.phone || '', 'US');
return (
<Box className="BackstoryResumeHeader" sx={style.headerStyle}>
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
<Box sx={{ display: 'flex' }}>
<Typography
variant="h4"
sx={{
fontWeight: 'bold',
mb: 1,
color: style.name === 'creative' ? '#ffffff' : style.color.primary,
fontFamily: 'inherit',
}}
>
{candidate.fullName}
</Typography>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-start',
gap: 1,
}}
>
{candidate.description && (
<Box sx={{ display: 'flex' }}>
<Typography
variant="h6"
sx={{
mb: 2,
fontWeight: 300,
color: style.name === 'creative' ? '#ffffff' : style.color.secondary,
fontFamily: 'inherit',
fontSize: '0.8rem !important',
}}
>
{candidate.description}
</Typography>
</Box>
)}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
alignContent: 'center',
flexGrow: 1,
minWidth: 'fit-content',
gap: 1,
}}
>
{candidate.email && (
<Box sx={{ display: 'flex', alignItems: 'center', m: 0, p: 0 }}>
<EmailIcon
fontSize="small"
sx={{ mr: 1, color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
/>
<Typography
variant="body2"
sx={{
color: style.name === 'creative' ? '#ffffff' : style.color.text,
fontFamily: 'inherit',
}}
>
{candidate.email}
</Typography>
</Box>
)}
{phone?.isValid() && (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<PhoneIcon
fontSize="small"
sx={{ mr: 1, color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
/>
<Typography
variant="body2"
sx={{
color: style.name === 'creative' ? '#ffffff' : style.color.text,
fontFamily: 'inherit',
}}
>
{phone.formatInternational()}
</Typography>
</Box>
)}
{candidate.location && (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<LocationIcon
fontSize="small"
sx={{ mr: 1, color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
/>
<Typography
variant="body2"
sx={{
color: style.name === 'creative' ? '#ffffff' : style.color.text,
fontFamily: 'inherit',
}}
>
{candidate.location.city
? `${candidate.location.city}, ${candidate.location.state}`
: candidate.location.text}
</Typography>
</Box>
)}
</Box>
</Box>
</Box>
</Box>
);
};
const ResumeInfo: React.FC<ResumeInfoProps> = (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>({ ...resume });
const [deleted, setDeleted] = useState<boolean>(false);
const [editDialogOpen, setEditDialogOpen] = useState<boolean>(false);
const [editContent, setEditContent] = useState<string>('');
const [editSystemPrompt, setEditSystemPrompt] = useState<string>('');
const [editPrompt, setEditPrompt] = useState<string>('');
const [saving, setSaving] = useState<boolean>(false);
const [tabValue, setTabValue] = useState('markdown');
const [jobTabValue, setJobTabValue] = useState('chat');
const [status, setStatus] = useState<string>('');
const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null);
const [error, setError] = useState<Types.ChatMessageError | null>(null);
const [selectedStyle, setSelectedStyle] = useState<string>('corporate');
// New revision-related state
const [revisions, setRevisions] = useState<ResumeRevision[]>([]);
const [selectedRevision, setSelectedRevision] = useState<string>('current');
const [loadingRevisions, setLoadingRevisions] = useState<boolean>(false);
const [loadingRevision, setLoadingRevision] = useState<boolean>(false);
const [revisionContent, setRevisionContent] = useState<string>('');
const printContentRef = useRef<HTMLDivElement>(null);
const reactToPrintFn = useReactToPrint({
contentRef: printContentRef,
pageStyle: '@page { margin: 8mm !important; }',
});
useEffect(() => {
if (resume && resume.id !== activeResume?.id) {
setActiveResume(resume);
}
}, [resume, activeResume]);
// Load revisions when dialog opens
useEffect(() => {
if (editDialogOpen && activeResume.id) {
loadResumeRevisions();
}
}, [editDialogOpen, activeResume.id]);
const currentStyle = useMemo(() => {
return resumeStyles[selectedStyle];
}, [selectedStyle]);
// Load resume revisions
const loadResumeRevisions = async (): Promise<void> => {
if (!activeResume.id) return;
setLoadingRevisions(true);
try {
const response = await apiClient.getResumeRevisions(activeResume.id);
console.log('Loaded revisions:', response.revisions);
setRevisions(response.revisions || []);
} catch (error) {
if (error instanceof Error) {
console.error('Failed to load revisions:', error);
setSnack(`Failed to load resume revisions: ${error.message || 'Unknown error'}`);
}
} finally {
setLoadingRevisions(false);
}
};
// Load specific revision content
const loadRevisionContent = async (revisionId: string): Promise<void> => {
if (!activeResume.id || revisionId === 'current') {
setRevisionContent(editContent);
return;
}
setLoadingRevision(true);
try {
const response = await apiClient.getResumeRevision(activeResume.id, revisionId);
setRevisionContent(response.revision.resume || '');
} catch (error) {
console.error('Failed to load revision:', error);
setSnack('Failed to load revision content.');
} finally {
setLoadingRevision(false);
}
};
// Handle revision selection
const handleRevisionChange = async (event: SelectChangeEvent<string>): Promise<void> => {
const newRevisionId = event.target.value;
setSelectedRevision(newRevisionId);
await loadRevisionContent(newRevisionId);
};
// Restore revision to editor
const restoreRevision = (): void => {
setEditContent(revisionContent);
setSelectedRevision('current');
setSnack('Revision restored to editor.');
};
// Format timestamp for display
const formatRevisionTimestamp = (timestamp: string): string => {
try {
const date = new Date(timestamp);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return timestamp;
}
};
// Rest of the existing methods...
const deleteResume = async (id: string | undefined): Promise<void> => {
if (id) {
try {
await apiClient.deleteResume(id);
setDeleted(true);
setSnack('Resume deleted successfully.');
} catch (error) {
setSnack('Failed to delete resume.');
}
}
};
const handleReset = async (): Promise<void> => {
setActiveResume({ ...resume });
};
const handleSave = async (): Promise<void> => {
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.');
// Reload revisions to include the new version
await loadResumeRevisions();
} catch (error) {
setSnack('Failed to update resume.');
} finally {
setSaving(false);
}
};
const handleEditOpen = (): void => {
setEditContent(activeResume.resume);
setEditSystemPrompt(activeResume.systemPrompt || '');
setEditPrompt(activeResume.prompt || '');
setSelectedRevision('current');
setRevisionContent(activeResume.resume);
setEditDialogOpen(true);
};
if (!resume) {
return <Box>No resume provided.</Box>;
}
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<Types.ChatMessageResume> = {
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<void> => {
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 (
<Box
sx={{
display: 'flex',
borderColor: 'transparent',
borderWidth: 2,
borderStyle: 'solid',
transition: 'all 0.3s ease',
flexDirection: 'column',
minWidth: 0,
opacity: deleted ? 0.5 : 1.0,
backgroundColor: deleted
? theme.palette.action.disabledBackground
: theme.palette.background.paper,
pointerEvents: deleted ? 'none' : 'auto',
...sx,
}}
>
<Box
sx={{
display: 'flex',
flexGrow: 1,
p: 1,
pb: 0,
height: '100%',
flexDirection: 'column',
alignItems: 'stretch',
position: 'relative',
}}
>
{/* Header Information */}
<Box
sx={{
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
gap: 2,
mb: 2,
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
{activeResume.candidate && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<PersonIcon color="primary" fontSize="small" />
<Typography variant="subtitle2" fontWeight="bold">
Candidate
</Typography>
</Box>
)}
<Typography variant="body2" color="text.secondary">
{activeResume.candidate?.fullName || activeResume.candidateId}
</Typography>
{activeResume.job && (
<>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mt: 1,
}}
>
<WorkIcon color="primary" fontSize="small" />
<Typography variant="subtitle2" fontWeight="bold">
Job
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
{activeResume.job.title} at {activeResume.job.company}
</Typography>
</>
)}
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ScheduleIcon color="primary" fontSize="small" />
<Typography variant="subtitle2" fontWeight="bold">
Timeline
</Typography>
</Box>
<Typography variant="caption" color="text.secondary">
Created: {formatDate(activeResume.createdAt)}
</Typography>
<Typography variant="caption" color="text.secondary">
Updated: {formatDate(activeResume.updatedAt)}
</Typography>
<Typography variant="caption" color="text.secondary">
Job ID: {activeResume.job?.id}
</Typography>
<Typography variant="caption" color="text.secondary">
Resume ID: {activeResume.id}
</Typography>
</Box>
</Box>
<Divider sx={{ mb: 2 }} />
{/* Resume Content */}
{activeResume.resume && (
<Card elevation={0} sx={{ m: 0, p: 0, background: 'transparent !important' }}>
<CardHeader
title="Resume Content"
avatar={<DescriptionIcon color="success" />}
sx={{ p: 0, pb: 1 }}
action={
<Tooltip title="Edit Resume Content">
<IconButton size="small" onClick={handleEditOpen}>
<EditIcon />
</IconButton>
</Tooltip>
}
/>
{variant === 'all' && activeResume.resume && (
<CardContent sx={{ p: 0 }}>
<StyledMarkdown content={activeResume.resume} />
</CardContent>
)}
</Card>
)}
</Box>
{/* Admin Controls */}
{isAdmin && (
<Box sx={{ display: 'flex', flexDirection: 'column', p: 1 }}>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
pl: 1,
pr: 1,
gap: 1,
alignContent: 'center',
height: '32px',
}}
>
<Tooltip title="Edit Resume">
<IconButton
size="small"
onClick={(e): void => {
e.stopPropagation();
handleEditOpen();
}}
>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete Resume">
<IconButton
size="small"
onClick={(e): void => {
e.stopPropagation();
deleteResume(activeResume.id);
}}
>
<DeleteIcon />
</IconButton>
</Tooltip>
<Tooltip title="Reset Resume">
<IconButton
size="small"
onClick={(e): void => {
e.stopPropagation();
handleReset();
}}
>
<RestoreIcon />
</IconButton>
</Tooltip>
</Box>
{saving && (
<Box sx={{ mt: 1 }}>
<LinearProgress />
<Typography variant="caption" sx={{ mt: 0.5 }}>
Saving resume...
</Typography>
</Box>
)}
</Box>
)}
{/* Edit Dialog */}
<Dialog
open={editDialogOpen}
onClose={(): void => {
setEditDialogOpen(false);
}}
maxWidth="lg"
fullWidth
disableEscapeKeyDown={true}
fullScreen={true}
>
<DialogTitle>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, m: 0, p: 0 }}>
Edit Resume Content
<Typography variant="caption" display="block" color="text.secondary">
Resume for {activeResume.candidate?.fullName || activeResume.candidateId},{' '}
{activeResume.job?.title || 'No Job Title Assigned'},{' '}
{activeResume.job?.company || 'No Company Assigned'}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Resume ID: # {activeResume.id}
</Typography>
<Typography variant="caption" display="block" color="text.secondary">
Last saved:{' '}
{activeResume.updatedAt ? new Date(activeResume.updatedAt).toLocaleString() : 'N/A'}
</Typography>
</Box>
<Box
sx={{
display: 'flex',
flexGrow: 1,
flexDirection: 'row',
gap: 1,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{/* Style Selector */}
<FormControl size="small" sx={{ minWidth: 'min-content' }}>
<InputLabel id="resume-style-label">Resume Style</InputLabel>
<Select
labelId="resume-style-label"
value={selectedStyle}
onChange={e => setSelectedStyle(e.target.value)}
label="Resume Style"
>
{Object.entries(resumeStyles).map(([key, style]) => (
<MenuItem key={key} value={key}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<Typography variant="body2" fontWeight="bold">
{style.name}
</Typography>
<Typography variant="caption" color="text.secondary">
{style.description}
</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
</Box>
<Tabs value={tabValue} onChange={handleTabChange}>
<Tab value="markdown" icon={<EditDocumentIcon />} label="Markdown" />
{activeResume.systemPrompt && (
<Tab value="systemPrompt" icon={<TuneIcon />} label="System Prompt" />
)}
{activeResume.systemPrompt && (
<Tab value="prompt" icon={<InputIcon />} label="Prompt" />
)}
<Tab value="preview" icon={<PreviewIcon />} label="Preview" />
<Tab value="print" icon={<PrintIcon />} label="Print" />
<Tab value="regenerate" icon={<ModelTraining />} label="Regenerate" />
</Tabs>
</Box>
</Box>
</DialogTitle>
<DialogContent
sx={{
position: 'relative',
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
height: '100%',
gap: 1,
pt: 1,
width: '100%',
position: 'relative',
overflow: 'hidden',
}}
>
<Paper
sx={{
p: 1,
flex: 1,
display: 'flex',
flexDirection: 'column',
position: 'relative',
maxWidth: '100%',
height: '100%',
overflow: 'hidden',
}}
>
{status && (
<Box sx={{ mt: 0, mb: 1 }}>
<StatusBox>
{statusType && <StatusIcon type={statusType} />}
<Typography variant="body2" sx={{ ml: 1 }}>
{status || 'Processing...'}
</Typography>
</StatusBox>
{status && !error && <LinearProgress sx={{ mt: 1 }} />}
</Box>
)}
{/* Revision History Dropdown for Markdown Tab */}
{tabValue === 'markdown' && (
<Box sx={{ mb: 2 }}>
<Stack direction="row" spacing={2} alignItems="center">
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel id="revision-select-label">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<HistoryIcon fontSize="small" />
Version History
</Box>
</InputLabel>
<Select
labelId="revision-select-label"
value={selectedRevision}
onChange={handleRevisionChange}
label="Version History"
disabled={loadingRevisions}
>
<MenuItem value="current">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip label="CURRENT" size="small" color="primary" variant="outlined" />
<Typography variant="body2">Current Version</Typography>
</Box>
</MenuItem>
{revisions.map(revision => (
<MenuItem key={revision.revisionId} value={revision.revisionId}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<Typography variant="body2">
{formatRevisionTimestamp(revision.revisionTimestamp)}
</Typography>
<Typography variant="caption" color="text.secondary">
Updated: {formatRevisionTimestamp(revision.updatedAt)}
</Typography>
<Typography
variant="caption"
color="text.secondary"
sx={{ fontSize: '0.65rem' }}
>
ID: {revision.revisionId.substring(0, 8)}...
</Typography>
</Box>
</MenuItem>
))}
</Select>
</FormControl>
<Tooltip title="Refresh Revisions">
<IconButton
size="small"
onClick={loadResumeRevisions}
disabled={loadingRevisions}
>
<RefreshIcon />
</IconButton>
</Tooltip>
{selectedRevision !== 'current' && (
<Tooltip title="Restore this revision to editor">
<Button
size="small"
startIcon={<RestoreFromTrashIcon />}
onClick={restoreRevision}
disabled={loadingRevision}
>
Restore
</Button>
</Tooltip>
)}
</Stack>
{selectedRevision !== 'current' && (
<Alert severity="info" sx={{ mt: 1 }}>
You are viewing a previous version. Click &quot;Restore&quot; to load this
content into the editor.
</Alert>
)}
</Box>
)}
<Scrollable
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
minHeight: 0,
'& > *:not(.Scrollable)': {
flexShrink: 0,
},
position: 'relative',
}}
>
{tabValue === 'markdown' && (
<>
{selectedRevision === 'current' ? (
<BackstoryTextField
value={editContent}
onChange={(value): void => 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..."
/>
) : (
<Box
sx={{
p: 2,
border: '1px solid',
borderColor: 'divider',
borderRadius: 1,
backgroundColor: 'grey.50',
height: '100%',
overflow: 'auto',
}}
>
{loadingRevision ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<LinearProgress sx={{ flexGrow: 1 }} />
<Typography variant="body2">Loading revision...</Typography>
</Box>
) : (
<pre style={{ border: 0 }}>{revisionContent}</pre>
)}
</Box>
)}
</>
)}
{tabValue === 'systemPrompt' && (
<BackstoryTextField
value={editSystemPrompt}
onChange={(value): void => 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' && (
<BackstoryTextField
value={editPrompt}
onChange={(value): void => 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' && (
<Box
className="document-container"
ref={printContentRef}
sx={currentStyle.contentStyle}
>
<Box
className="a4-document"
sx={{
backgroundColor: currentStyle.color.background,
padding: 5,
minHeight: '100vh',
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
}}
>
{/* Custom Header */}
{activeResume.candidate && (
<StyledHeader candidate={activeResume.candidate} style={currentStyle} />
)}
{/* Styled Markdown Content */}
<Box sx={currentStyle.markdownStyle}>
<StyledMarkdown
sx={{
position: 'relative',
maxHeight: '100%',
display: 'flex',
flexGrow: 1,
flex: 1,
...currentStyle.markdownStyle,
}}
content={editContent || activeResume.resume || ''}
/>
</Box>
{/* QR Code Footer */}
{activeResume.candidate && activeResume.job && (
<StyledFooter
candidate={activeResume.candidate}
job={activeResume.job}
style={currentStyle}
/>
)}
</Box>
</Box>
)}
</Scrollable>
</Paper>
<Scrollable
sx={{
flex: 1,
display: 'flex',
height: '100%',
overflowY: 'auto',
position: 'relative',
}}
>
<Paper
sx={{
p: 1,
flex: 1,
display: 'flex',
flexDirection: 'column',
position: 'relative',
}}
>
<Tabs value={jobTabValue} onChange={handleJobTabChange}>
{activeResume.job !== undefined && (
<Tab value="job" icon={<WorkIcon />} label="Job" />
)}
<Tab value="chat" icon={<ModelTraining />} label="AI Edit" />
</Tabs>
{activeResume.job !== undefined && jobTabValue === 'job' && (
<JobInfo
variant={'all'}
job={activeResume.job}
sx={{
m: 0,
p: 1,
backgroundColor: '#f8f0e0',
}}
/>
)}
{jobTabValue === 'chat' && (
<ResumeChat
session={activeResume.id || ''}
resume={editContent}
onResumeChange={(newResume: string): void => {
setEditContent(newResume);
setActiveResume({ ...activeResume, resume: newResume });
}}
sx={{
m: 1,
p: 1,
flexGrow: 1,
}}
/>
)}
</Paper>
</Scrollable>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={(): void => setEditDialogOpen(false)}>Cancel</Button>
<Button
onClick={handleSave}
variant="contained"
disabled={saving}
startIcon={<SaveIcon />}
>
{saving ? 'Saving...' : 'Save'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export { ResumeInfo };