1192 lines
36 KiB
TypeScript

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<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,
// pt: 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 }}>
<Typography
variant="h4"
sx={{
fontWeight: 'bold',
mb: 1,
color: style.name === 'creative' ? '#ffffff' : style.color.primary,
fontFamily: 'inherit',
}}
>
{candidate.fullName}
</Typography>
{candidate.description && (
<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>
{/* {(candidate.website || candidate.linkedin) && (
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<WebsiteIcon
fontSize="small"
sx={{ color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
/>
<Typography
variant="body2"
sx={{
color: style.name === 'creative' ? '#ffffff' : style.color.text,
fontFamily: 'inherit',
}}
>
{candidate.website || candidate.linkedin}
</Typography>
</Box>
</Grid>
)} */}
</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');
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]);
const currentStyle = resumeStyles[selectedStyle];
// Rest of the component remains the same...
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.');
} 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 <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>
)}
<Scrollable
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
minHeight: 0,
'& > *:not(.Scrollable)': {
flexShrink: 0,
},
position: 'relative',
}}
>
{tabValue === 'markdown' && (
<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..."
/>
)}
{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',
// backgroundColor: '#f8f0e0',
}}
>
<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 };