Lots of mobile and desktop edit view tweaks

This commit is contained in:
James Ketr 2025-07-19 14:23:28 -07:00
parent 97272d9175
commit 064868e96e
6 changed files with 505 additions and 350 deletions

View File

@ -6,8 +6,9 @@ import React, {
useState, useState,
useImperativeHandle, useImperativeHandle,
} from 'react'; } from 'react';
import { useTheme } from '@mui/material/styles'; import { SxProps, useTheme } from '@mui/material/styles';
import './BackstoryTextField.css'; import './BackstoryTextField.css';
import { Box } from '@mui/material';
// Define ref interface for exposed methods // Define ref interface for exposed methods
interface BackstoryTextFieldRef { interface BackstoryTextFieldRef {
@ -23,11 +24,12 @@ interface BackstoryTextFieldProps {
onEnter?: (value: string) => void; onEnter?: (value: string) => void;
onChange?: (value: string) => void; onChange?: (value: string) => void;
style?: CSSProperties; style?: CSSProperties;
sx?: SxProps;
} }
const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryTextFieldProps>( const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryTextFieldProps>(
(props, ref) => { (props, ref) => {
const { value = '', disabled = false, placeholder, onEnter, onChange, style } = props; const { value = '', disabled = false, placeholder, onEnter, onChange, style, sx } = props;
const theme = useTheme(); const theme = useTheme();
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const shadowRef = useRef<HTMLTextAreaElement>(null); const shadowRef = useRef<HTMLTextAreaElement>(null);
@ -115,7 +117,7 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
}; };
return ( return (
<> <Box sx={{ display: 'flex', flexGrow: 1, boxSizing: 'border-box', p: 1, ...sx }}>
<textarea <textarea
className="BackstoryTextField" className="BackstoryTextField"
ref={textareaRef} ref={textareaRef}
@ -148,7 +150,7 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
readOnly readOnly
tabIndex={-1} tabIndex={-1}
/> />
</> </Box>
); );
} }
); );

View File

@ -5,13 +5,11 @@ import {
Typography, Typography,
SxProps, SxProps,
Chip, Chip,
Stack,
CardHeader,
LinearProgress, LinearProgress,
IconButton, IconButton,
Tooltip, Tooltip,
} from '@mui/material'; } from '@mui/material';
import { Card, CardContent, Divider, useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { useMediaQuery } from '@mui/material'; import { useMediaQuery } from '@mui/material';
@ -128,7 +126,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
if (!items || items.length === 0) return <></>; if (!items || items.length === 0) return <></>;
return ( return (
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2, display: 'flex', position: 'relative', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}>
{icon} {icon}
<Typography <Typography
@ -146,17 +144,22 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
/> />
)} )}
</Box> </Box>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, position: 'relative' }}>
{items.map((item, index) => ( {items.map((item, index) => (
<Chip <Box
key={index} key={index}
label={item} sx={{
variant="outlined" border: '1px solid grey',
size="small" p: 0.5,
sx={{ mb: 1, fontSize: '0.75rem !important' }} borderRadius: 1,
/> fontSize: '0.75rem !important',
display: 'flex',
}}
>
{item}
</Box>
))} ))}
</Stack> </Box>
</Box> </Box>
); );
}; };
@ -165,13 +168,24 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
if (!activeJob.requirements) return <></>; if (!activeJob.requirements) return <></>;
return ( return (
<Card elevation={0} sx={{ m: 0, p: 0, mt: 2, background: 'transparent !important' }}> <Box
<CardHeader sx={{
title="Job Requirements Analysis" maxWidth: '100%',
avatar={<CheckCircle color="success" />} position: 'relative',
sx={{ p: 0, pb: 1 }} m: 0,
/> p: 0,
<CardContent sx={{ p: 0 }}> background: 'transparent !important',
display: 'flex',
flexDirection: 'column',
}}
>
<Box
sx={{ p: 0, pb: 1, alignItems: 'center', display: 'flex', flexDirection: 'row', gap: 1 }}
>
<CheckCircle color="success" />
<Box>Job Requirements Analysis</Box>
</Box>
<Box sx={{ p: 0 }}>
{renderRequirementSection( {renderRequirementSection(
'Technical Skills (Required)', 'Technical Skills (Required)',
activeJob.requirements.technicalSkills.required, activeJob.requirements.technicalSkills.required,
@ -219,8 +233,8 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
activeJob.requirements.preferredAttributes, activeJob.requirements.preferredAttributes,
<Star color="secondary" /> <Star color="secondary" />
)} )}
</CardContent> </Box>
</Card> </Box>
); );
}; };
@ -232,9 +246,9 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
borderColor: 'transparent', borderColor: 'transparent',
borderWidth: 2, borderWidth: 2,
borderStyle: 'solid', borderStyle: 'solid',
transition: 'all 0.3s ease',
flexDirection: 'column', flexDirection: 'column',
minWidth: 0, minWidth: 0,
maxWidth: '100%',
opacity: deleted ? 0.5 : 1.0, opacity: deleted ? 0.5 : 1.0,
backgroundColor: deleted backgroundColor: deleted
? theme.palette.action.disabledBackground ? theme.palette.action.disabledBackground
@ -273,6 +287,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}, },
'& > div > div > :last-of-type': { mb: 0.75, mr: 1 }, '& > div > div > :last-of-type': { mb: 0.75, mr: 1 },
position: 'relative',
}} }}
> >
<Box <Box
@ -401,12 +416,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<StyledMarkdown sx={{ display: 'flex' }} content={activeJob.description} /> <StyledMarkdown sx={{ display: 'flex' }} content={activeJob.description} />
)} )}
{variant !== 'small' && variant !== 'minimal' && ( {variant !== 'small' && variant !== 'minimal' && renderJobRequirements()}
<Box>
<Divider />
{renderJobRequirements()}
</Box>
)}
{isAdmin && ( {isAdmin && (
<Box sx={{ display: 'flex', flexDirection: 'column', p: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', p: 1 }}>

View File

@ -20,7 +20,7 @@ import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import PropagateLoader from 'react-spinners/PropagateLoader'; import PropagateLoader from 'react-spinners/PropagateLoader';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField'; import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
import { Scrollable } from 'components/Scrollable'; import { Scrollable } from 'components/Scrollable';
import { StyledMarkdown } from './StyledMarkdown'; import { StyledMarkdown } from '../StyledMarkdown';
const emptyMetadata: ChatMessageMetaData = { const emptyMetadata: ChatMessageMetaData = {
model: 'qwen2.5', model: 'qwen2.5',
@ -259,6 +259,7 @@ const ResumeChat = forwardRef<ConversationHandle, ResumeChatProps>(
return ( return (
<Box <Box
className="ResumeChat"
ref={ref} ref={ref}
sx={{ sx={{
display: 'flex', display: 'flex',
@ -358,7 +359,18 @@ const ResumeChat = forwardRef<ConversationHandle, ResumeChatProps>(
</Scrollable> </Scrollable>
)} )}
{/* Fixed Message Input */} {/* Fixed Message Input */}
<Box sx={{ display: 'flex', flexShrink: 1, gap: 1 }}> <Box
sx={{
display: 'flex',
flexGrow: 1,
gap: 1,
position: 'relative',
width: '100%',
minWidth: '100%',
flexDirection: 'row',
overflow: 'hidden',
}}
>
<DeleteConfirmation <DeleteConfirmation
onDelete={(): void => { onDelete={(): void => {
chatSession && onDelete(chatSession); chatSession && onDelete(chatSession);

View File

@ -7,19 +7,19 @@ import {
IconButton, IconButton,
Tooltip, Tooltip,
Tabs, Tabs,
Tab,
Paper,
FormControl, FormControl,
Select, Select,
MenuItem, MenuItem,
InputLabel, InputLabel,
Chip, Chip,
Alert, Alert,
Stack,
SelectChangeEvent, SelectChangeEvent,
useTheme,
useMediaQuery,
} from '@mui/material'; } from '@mui/material';
import PrintIcon from '@mui/icons-material/Print'; import PrintIcon from '@mui/icons-material/Print';
import DifferenceIcon from '@mui/icons-material/Difference'; import DifferenceIcon from '@mui/icons-material/Difference';
import ForumIcon from '@mui/icons-material/Forum';
import { import {
Save as SaveIcon, Save as SaveIcon,
ModelTraining, ModelTraining,
@ -42,10 +42,12 @@ import { Scrollable } from 'components/Scrollable';
import * as Types from 'types/types'; import * as Types from 'types/types';
import { StreamingOptions } from 'services/api-client'; import { StreamingOptions } from 'services/api-client';
import { StatusBox, StatusIcon } from './StatusIcon'; import { StatusBox, StatusIcon } from './StatusIcon';
import { ResumeChat } from 'components/ResumeChat'; import { ResumeChat } from 'components/ui/ResumeChat';
import { DiffViewer } from 'components/DiffViewer'; import { DiffViewer } from 'components/DiffViewer';
import { ResumePreview, resumeStyles } from './ResumePreview'; import { ResumePreview, resumeStyles } from './ResumePreview';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
import { useNavigate } from 'react-router-dom';
import { TabWithTooltip } from './TabWithTooltip';
interface ResumeRevision { interface ResumeRevision {
revisionId: string; revisionId: string;
@ -65,13 +67,17 @@ interface ResumeEditProps {
const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => { const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const { onClose, resumeId, onSave } = props; const { onClose, resumeId, onSave } = props;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isLarge = useMediaQuery(theme.breakpoints.up('md'));
const navigate = useNavigate();
const { selectedCandidate, selectedJob } = useAppState(); const { selectedCandidate, selectedJob } = useAppState();
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const [editContent, setEditContent] = useState<string>(''); const [editContent, setEditContent] = useState<string>('');
const [saving, setSaving] = useState<boolean>(false); const [saving, setSaving] = useState<boolean>(false);
const [tabValue, setTabValue] = useState('markdown'); const [leftColumn, setLeftColumn] = useState('markdown');
const [jobTabValue, setJobTabValue] = useState('chat'); const [rightColumn, setRightColumn] = useState('chat');
const [status, setStatus] = useState<string>(''); const [status, setStatus] = useState<string>('');
const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null); const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null);
const [error, setError] = useState<Types.ChatMessageError | null>(null); const [error, setError] = useState<Types.ChatMessageError | null>(null);
@ -79,6 +85,7 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
const [resume, setResume] = useState<Resume | null>(null); const [resume, setResume] = useState<Resume | null>(null);
const [isAIGenerated, setIsAIGenerated] = useState<boolean>(false); const [isAIGenerated, setIsAIGenerated] = useState<boolean>(false);
const [editPrompt, setEditPrompt] = useState<string>(''); const [editPrompt, setEditPrompt] = useState<string>('');
const [columnView, setColumnView] = useState<'left' | 'right'>('left');
// Revision-related state // Revision-related state
const [revisions, setRevisions] = useState<ResumeRevision[]>([]); const [revisions, setRevisions] = useState<ResumeRevision[]>([]);
@ -325,7 +332,8 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
return generatedResume; return generatedResume;
}; };
const handleTabChange = (event: React.SyntheticEvent, newValue: string): void => { const handleLeftColumnChange = (event: React.SyntheticEvent, newValue: string): void => {
setColumnView('left');
if (newValue === 'print') { if (newValue === 'print') {
console.log('Printing resume...'); console.log('Printing resume...');
reactToPrintFn(); reactToPrintFn();
@ -333,7 +341,7 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
} }
if (newValue === 'regenerate') { if (newValue === 'regenerate') {
setSnack('Regenerating resume...'); setSnack('Regenerating resume...');
setTabValue('markdown'); setLeftColumn('markdown');
generateResume(); generateResume();
return; return;
} }
@ -341,14 +349,15 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
setEditContent(current?.resume || ''); setEditContent(current?.resume || '');
setEditPrompt(current?.prompt || ''); setEditPrompt(current?.prompt || '');
setIsAIGenerated(current?.aiGenerated || false); setIsAIGenerated(current?.aiGenerated || false);
setTabValue('markdown'); setLeftColumn('markdown');
return; return;
} }
setTabValue(newValue); setLeftColumn(newValue);
}; };
const handleJobTabChange = (event: React.SyntheticEvent, newValue: string): void => { const handleRightColumnChange = (event: React.SyntheticEvent, newValue: string): void => {
setJobTabValue(newValue); setColumnView('right');
setRightColumn(newValue);
}; };
const handleClose = (): void => { const handleClose = (): void => {
@ -359,12 +368,10 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
(selectedRevision !== 'current' && resume?.aiGenerated ? resume?.prompt : '') || (selectedRevision !== 'current' && resume?.aiGenerated ? resume?.prompt : '') ||
(isAIGenerated ? editPrompt : '') || (isAIGenerated ? editPrompt : '') ||
'Manual edits'; 'Manual edits';
console.log(
`Change log: ${changeLog}, isAIGenerated: ${isAIGenerated}, editPrompt: ${editPrompt}, selectedRevision: ${selectedRevision}, resume?.aiGenerated: ${resume?.aiGenerated}`
);
return ( return (
<Box <Box
className="ResumeEdit"
sx={{ sx={{
display: 'flex', display: 'flex',
flexGrow: 1, flexGrow: 1,
@ -375,6 +382,7 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
}} }}
> >
<Box <Box
className="ResumeEditHeader"
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
@ -384,7 +392,14 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
}} }}
> >
<Box sx={{ display: 'flex', flexDirection: 'column' }}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}> <Box
sx={{
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
gap: 2,
flexWrap: 'wrap',
}}
>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@ -392,7 +407,7 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
gap: 0.5, gap: 0.5,
m: 0, m: 0,
p: 0, p: 0,
minWidth: '25rem', minWidth: '20rem',
}} }}
> >
{resume && ( {resume && (
@ -402,8 +417,21 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
{resume.job?.title || 'No Job Title Assigned'},{' '} {resume.job?.title || 'No Job Title Assigned'},{' '}
{resume.job?.company || 'No Company Assigned'} {resume.job?.company || 'No Company Assigned'}
</Typography> </Typography>
<Typography variant="caption" display="block" color="text.secondary"> <Typography
Resume ID: {resume.id} variant="caption"
display="block"
color="text.secondary"
sx={{ display: 'flex', flexDirection: 'row' }}
>
Resume ID:{' '}
<Box
sx={{ cursor: 'pointer', textDecoration: 'underline' }}
onClick={() => {
navigate(`/chat/${resume.id}`);
}}
>
{resume.id}
</Box>
</Typography> </Typography>
</> </>
)} )}
@ -413,7 +441,7 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
</Typography> </Typography>
)} )}
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, flexWrap: 'wrap' }}>
{/* Style Selector */} {/* Style Selector */}
<FormControl size="small" sx={{ minWidth: 'min-content' }}> <FormControl size="small" sx={{ minWidth: 'min-content' }}>
<InputLabel id="resume-style-label">Resume Style</InputLabel> <InputLabel id="resume-style-label">Resume Style</InputLabel>
@ -444,87 +472,86 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
</Select> </Select>
</FormControl> </FormControl>
<Box sx={{ display: 'flex', flexDirection: 'row' }}> <Box sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
<Stack direction="row" spacing={2} alignItems="center"> <FormControl size="small" sx={{ minWidth: 200 }}>
<FormControl size="small" sx={{ minWidth: 200 }}> <InputLabel id="revision-select-label">
<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 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<HistoryIcon fontSize="small" /> <Chip label="CURRENT" size="small" color="primary" variant="outlined" />
Version History <Typography variant="body2">Current Version</Typography>
</Box> </Box>
</InputLabel> </MenuItem>
<Select {revisions.map(revision => (
labelId="revision-select-label" <MenuItem key={revision.revisionId} value={revision.revisionId}>
value={selectedRevision} <Box
onChange={handleRevisionChange} sx={{
label="Version History" display: 'flex',
disabled={loadingRevisions} flexDirection: 'column',
> alignItems: 'flex-start',
<MenuItem value="current"> }}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> >
<Chip label="CURRENT" size="small" color="primary" variant="outlined" /> <Typography variant="body2">
<Typography variant="body2">Current Version</Typography> {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> </Box>
</MenuItem> </MenuItem>
{revisions.map(revision => ( ))}
<MenuItem key={revision.revisionId} value={revision.revisionId}> </Select>
<Box </FormControl>
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"> <Tooltip title="Refresh Revisions">
<IconButton <IconButton
size="small" size="small"
onClick={loadResumeRevisions} onClick={loadResumeRevisions}
disabled={loadingRevisions} disabled={loadingRevisions}
> >
<RefreshIcon /> <RefreshIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{selectedRevision !== 'current' && ( {selectedRevision !== 'current' && (
<> <>
<Tooltip title="Restore this revision to editor"> <Tooltip title="Restore this revision to editor">
<Button <Button
size="small" size="small"
startIcon={<RestoreFromTrashIcon />} startIcon={<RestoreFromTrashIcon />}
onClick={restoreRevision} onClick={restoreRevision}
disabled={loadingRevision} disabled={loadingRevision}
> >
Restore Restore
</Button> </Button>
</Tooltip> </Tooltip>
</> </>
)} )}
</Stack>
</Box> </Box>
</Box> </Box>
</Box> </Box>
</Box> </Box>
</Box> </Box>
<Box <Box
className="ResumeEditContent"
sx={{ sx={{
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
@ -533,6 +560,99 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
height: '100%', height: '100%',
}} }}
> >
<Box className="ResumeEditTabs" sx={{ display: 'flex', m: 0, p: 0 }}>
<Tabs
value={isLarge ? leftColumn : columnView === 'left' ? leftColumn : undefined}
onChange={handleLeftColumnChange}
sx={{
'& .MuiTab-root': {
minWidth: 'fit-content',
width: 'fit-content',
fontSize: isLarge ? '0.6rem' : '0.5rem',
},
}}
>
<TabWithTooltip
tooltip="Markdown Editor"
value="markdown"
icon={<EditDocumentIcon />}
label={isLarge ? 'Markdown' : ''}
/>
<TabWithTooltip
tooltip="View Changes"
value="diff"
disabled={editContent === current?.resume}
icon={<DifferenceIcon />}
label={isLarge ? 'Changes' : undefined}
/>
<TabWithTooltip
tooltip="Preview Resume"
value="preview"
icon={<PreviewIcon />}
label={isLarge ? 'Preview' : undefined}
/>
<TabWithTooltip
tooltip="Print Resume"
disabled={leftColumn !== 'preview'}
value="print"
icon={<PrintIcon />}
label={isLarge ? 'Print' : undefined}
/>
<TabWithTooltip
tooltip="Regenerate Resume"
value="regenerate"
icon={<ModelTraining />}
label={isLarge ? 'Regenerate' : undefined}
/>
<TabWithTooltip
tooltip="Undo Changes"
value="undo"
disabled={editContent === resume?.resume}
icon={<UndoIcon />}
label={isLarge ? 'Revert' : undefined}
/>
</Tabs>
<Tabs
value={isLarge ? rightColumn : columnView === 'right' ? rightColumn : undefined}
onChange={handleRightColumnChange}
sx={{
'& .MuiTab-root': {
minWidth: 'fit-content',
width: 'fit-content',
fontSize: isLarge ? '0.6rem' : '0.5rem',
},
borderLeft: isLarge ? '1px solid #ccc' : 'none',
}}
>
{resume && resume.job !== undefined && (
<TabWithTooltip
tooltip="Job"
value="job"
icon={<WorkIcon />}
label={isLarge ? 'Job' : ''}
/>
)}
<TabWithTooltip
tooltip="AI Edit"
value="chat"
icon={<ForumIcon />}
label={isLarge ? 'AI Edit' : ''}
/>
</Tabs>
</Box>
{status && (
<Box sx={{ mt: 1, mb: 1, width: '100%' }}>
<StatusBox>
{statusType && <StatusIcon type={statusType} />}
<Typography variant="body2" sx={{ ml: 1 }}>
{status || 'Processing...'}
</Typography>
</StatusBox>
{status && !error && <LinearProgress sx={{ mt: 1 }} />}
</Box>
)}
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@ -545,246 +665,213 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
<Paper {(isLarge || columnView === 'left') && (
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
position: 'relative',
maxWidth: '100%',
height: '100%',
overflow: 'hidden',
alignItems: 'center',
}}
>
<Box sx={{ display: 'flex', m: 0, p: 0 }}>
<Tabs value={tabValue} onChange={handleTabChange}>
<Tab value="markdown" icon={<EditDocumentIcon />} label="Markdown" />
<Tab
value="diff"
disabled={editContent === current?.resume}
icon={<DifferenceIcon />}
label="Changes"
/>
<Tab value="preview" icon={<PreviewIcon />} label="Preview" />
<Tab
disabled={tabValue !== 'preview'}
value="print"
icon={<PrintIcon />}
label="Print"
/>
<Tab value="regenerate" icon={<ModelTraining />} label="Regenerate" />
<Tab
value="undo"
disabled={editContent === resume?.resume}
icon={<UndoIcon />}
label="Revert"
/>
</Tabs>
</Box>
{status && (
<Box sx={{ mt: 1, mb: 1, width: '100%' }}>
<StatusBox>
{statusType && <StatusIcon type={statusType} />}
<Typography variant="body2" sx={{ ml: 1 }}>
{status || 'Processing...'}
</Typography>
</StatusBox>
{status && !error && <LinearProgress sx={{ mt: 1 }} />}
</Box>
)}
<Box <Box
sx={{ sx={{
display: 'flex',
flexGrow: 1,
width: '100%',
position: 'relative',
flexDirection: 'column',
m: 0,
p: 0,
gap: 0,
}}
>
{selectedRevision !== 'current' && (
<Alert severity="info" sx={{ width: '100%' }}>
You are viewing a previous version. Click &quot;Restore&quot; to load this content
into the editor.
</Alert>
)}
{isAIGenerated && (
<Alert severity="warning" sx={{ width: '100%' }}>
This resume was generated by AI and has not been manually edited. Review and then
selecte &apos;Save&apos;.
</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',
fontFamily: 'monospace',
backgroundColor: '#fafafa',
fontSize: '12px',
}}
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 }}>{resume?.resume}</pre>
)}
</Box>
)}
</>
)}
{tabValue === 'diff' && current && (
<DiffViewer
changeLog={changeLog}
original={{ content: current.resume || '', name: 'original' }}
modified={{
content: selectedRevision !== 'current' && resume ? resume.resume : editContent,
name: 'modified',
}}
/>
)}
{tabValue === 'preview' && resume && resume.candidate && (
<Box
className="document-container"
ref={printContentRef}
sx={currentStyle.contentStyle}
>
<ResumePreview resume={resume} selectedStyle={selectedStyle} />
</Box>
)}
</Scrollable>
</Paper>
<Scrollable
sx={{
flex: 1,
display: 'flex',
height: '100%',
overflowY: 'auto',
position: 'relative',
}}
>
<Paper
sx={{
p: 1,
flex: 1, flex: 1,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
position: 'relative', position: 'relative',
maxWidth: '100%',
height: '100%',
overflow: 'hidden',
alignItems: 'center',
}} }}
> >
<Tabs value={jobTabValue} onChange={handleJobTabChange}> <Box
{resume && resume.job !== undefined && ( sx={{
<Tab value="job" icon={<WorkIcon />} label="Job" /> display: 'flex',
flexGrow: 1,
width: '100%',
position: 'relative',
flexDirection: 'column',
m: 0,
p: 0,
gap: 0,
}}
>
{selectedRevision !== 'current' && (
<Alert severity="info" sx={{ width: '100%' }}>
You are viewing a previous version. Click &quot;Restore&quot; to load this
content into the editor.
</Alert>
)} )}
<Tab value="chat" icon={<ModelTraining />} label="AI Edit" />
</Tabs>
{resume && resume.job !== undefined && jobTabValue === 'job' && ( {isAIGenerated && (
<JobInfo <Alert severity="warning" sx={{ width: '100%' }}>
variant={'all'} This resume was generated by AI and has not been manually edited. Review and
job={resume.job} then selecte &apos;Save&apos;.
</Alert>
)}
</Box>
<Scrollable
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
minHeight: 0,
'& > *:not(.Scrollable)': {
flexShrink: 0,
},
position: 'relative',
}}
>
{leftColumn === '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',
fontFamily: 'monospace',
backgroundColor: '#fafafa',
fontSize: '12px',
}}
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 }}>{resume?.resume}</pre>
)}
</Box>
)}
</>
)}
{leftColumn === 'diff' && current && (
<DiffViewer
changeLog={changeLog}
original={{ content: current.resume || '', name: 'original' }}
modified={{
content:
selectedRevision !== 'current' && resume ? resume.resume : editContent,
name: 'modified',
}}
/>
)}
{leftColumn === 'preview' && resume && resume.candidate && (
<Box
className="document-container"
ref={printContentRef}
sx={currentStyle.contentStyle}
>
<ResumePreview resume={resume} selectedStyle={selectedStyle} />
</Box>
)}
</Scrollable>
{resume && (
<Box
sx={{ sx={{
m: 0, display: 'flex',
flexDirection: 'row',
gap: 1,
alignItems: 'center',
p: 1, p: 1,
backgroundColor: '#f8f0e0',
}} }}
/> >
{onClose && <Button onClick={handleClose}>Cancel</Button>}
<Button
onClick={() => {
handleSave();
}}
variant="contained"
disabled={saveDisabled}
startIcon={<SaveIcon />}
>
{saving ? 'Saving...' : 'Save'}
</Button>
<Typography variant="caption" display="block" color="text.secondary">
Last saved:{' '}
{resume.updatedAt ? new Date(resume.updatedAt).toLocaleString() : 'N/A'}
</Typography>
</Box>
)} )}
{jobTabValue === 'chat' && resume && ( </Box>
<ResumeChat )}
session={resume.id || ''} {(isLarge || columnView === 'right') && (
resume={editContent} <Box
onResumeChange={(prompt: string, newResume: string): void => { sx={{
console.log('onResumeChange:', prompt); display: 'flex',
if (newResume !== editContent) { flexDirection: 'column',
setBackupContent(editContent); flex: 1,
setEditPrompt(prompt); position: 'relative',
setEditContent(newResume); width: '100%',
setIsAIGenerated(true); }}
} >
}} <Scrollable
sx={{ sx={{
m: 1, flex: 1,
p: 1, display: 'flex',
flexGrow: 1, height: '100%',
position: 'relative', width: '100%',
maxWidth: 'fit-content', overflowY: 'auto',
minWidth: '100%', position: 'relative',
}} p: 1,
/> flexDirection: 'column',
)} overflow: 'hidden',
</Paper> }}
</Scrollable> >
{resume && rightColumn === 'job' && (
<>
{resume.job !== undefined ? (
<JobInfo variant={'all'} job={resume.job} />
) : (
<Box>No matching job found.</Box>
)}
</>
)}
{rightColumn === 'chat' && resume && (
<ResumeChat
session={resume.id || ''}
resume={editContent}
onResumeChange={(prompt: string, newResume: string): void => {
console.log('onResumeChange:', prompt);
if (newResume !== editContent) {
setBackupContent(editContent);
setEditPrompt(prompt);
setEditContent(newResume);
setIsAIGenerated(true);
}
}}
sx={{
m: 1,
p: 1,
flexGrow: 1,
position: 'relative',
}}
/>
)}
</Scrollable>
</Box>
)}
</Box> </Box>
</Box> </Box>
{resume && (
<Box
sx={{ display: 'flex', flexDirection: 'row', gap: 1, alignItems: 'center', pl: 1, pt: 1 }}
>
{onClose && <Button onClick={handleClose}>Cancel</Button>}
<Button
onClick={() => {
handleSave();
}}
variant="contained"
disabled={saveDisabled}
startIcon={<SaveIcon />}
>
{saving ? 'Saving...' : 'Save'}
</Button>
<Typography variant="caption" display="block" color="text.secondary">
Last saved: {resume.updatedAt ? new Date(resume.updatedAt).toLocaleString() : 'N/A'}
</Typography>
</Box>
)}
</Box> </Box>
); );
}; };

View File

@ -528,9 +528,9 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
if (mode === 'edit' && selectedResume) { if (mode === 'edit' && selectedResume) {
return ( return (
<ResumeEdit <ResumeEdit
onClose={() => { // onClose={() => {
navigate(window.location.pathname.replace('/edit', '')); // navigate(window.location.pathname.replace('/edit', ''));
}} // }}
resumeId={selectedResume.id} resumeId={selectedResume.id}
onSave={handleSave} onSave={handleSave}
/> />

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Tab, Tooltip, TabProps } from '@mui/material';
interface TabWithTooltipProps extends TabProps {
tooltip: string;
showTooltip?: boolean; // Optional prop to control when tooltip shows
}
const TabWithTooltip: React.FC<TabWithTooltipProps> = ({
tooltip,
showTooltip = true,
disabled = false,
children,
...tabProps
}) => {
const tabElement = (
<Tab disabled={disabled} {...tabProps}>
{children}
</Tab>
);
// Don't show tooltip if showTooltip is false
if (!showTooltip) {
return tabElement;
}
// For disabled tabs, wrap in span since Tooltip doesn't work on disabled elements
if (disabled) {
return (
<Tooltip title={tooltip} placement="bottom">
<span style={{ display: 'inline-block' }}>{tabElement}</span>
</Tooltip>
);
}
// Normal case with tooltip
return (
<Tooltip title={tooltip} placement="bottom">
{tabElement}
</Tooltip>
);
};
export { TabWithTooltip };