Lots of mobile and desktop edit view tweaks
This commit is contained in:
parent
97272d9175
commit
064868e96e
@ -6,8 +6,9 @@ import React, {
|
||||
useState,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { SxProps, useTheme } from '@mui/material/styles';
|
||||
import './BackstoryTextField.css';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
// Define ref interface for exposed methods
|
||||
interface BackstoryTextFieldRef {
|
||||
@ -23,11 +24,12 @@ interface BackstoryTextFieldProps {
|
||||
onEnter?: (value: string) => void;
|
||||
onChange?: (value: string) => void;
|
||||
style?: CSSProperties;
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryTextFieldProps>(
|
||||
(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 textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const shadowRef = useRef<HTMLTextAreaElement>(null);
|
||||
@ -115,7 +117,7 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, boxSizing: 'border-box', p: 1, ...sx }}>
|
||||
<textarea
|
||||
className="BackstoryTextField"
|
||||
ref={textareaRef}
|
||||
@ -148,7 +150,7 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -5,13 +5,11 @@ import {
|
||||
Typography,
|
||||
SxProps,
|
||||
Chip,
|
||||
Stack,
|
||||
CardHeader,
|
||||
LinearProgress,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import { Card, CardContent, Divider, useTheme } from '@mui/material';
|
||||
import { useTheme } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
@ -128,7 +126,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
if (!items || items.length === 0) 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 }}>
|
||||
{icon}
|
||||
<Typography
|
||||
@ -146,17 +144,22 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, position: 'relative' }}>
|
||||
{items.map((item, index) => (
|
||||
<Chip
|
||||
<Box
|
||||
key={index}
|
||||
label={item}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ mb: 1, fontSize: '0.75rem !important' }}
|
||||
/>
|
||||
sx={{
|
||||
border: '1px solid grey',
|
||||
p: 0.5,
|
||||
borderRadius: 1,
|
||||
fontSize: '0.75rem !important',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@ -165,13 +168,24 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
if (!activeJob.requirements) return <></>;
|
||||
|
||||
return (
|
||||
<Card elevation={0} sx={{ m: 0, p: 0, mt: 2, background: 'transparent !important' }}>
|
||||
<CardHeader
|
||||
title="Job Requirements Analysis"
|
||||
avatar={<CheckCircle color="success" />}
|
||||
sx={{ p: 0, pb: 1 }}
|
||||
/>
|
||||
<CardContent sx={{ p: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: '100%',
|
||||
position: 'relative',
|
||||
m: 0,
|
||||
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(
|
||||
'Technical Skills (Required)',
|
||||
activeJob.requirements.technicalSkills.required,
|
||||
@ -219,8 +233,8 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
activeJob.requirements.preferredAttributes,
|
||||
<Star color="secondary" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -232,9 +246,9 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
transition: 'all 0.3s ease',
|
||||
flexDirection: 'column',
|
||||
minWidth: 0,
|
||||
maxWidth: '100%',
|
||||
opacity: deleted ? 0.5 : 1.0,
|
||||
backgroundColor: deleted
|
||||
? theme.palette.action.disabledBackground
|
||||
@ -273,6 +287,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
'& > div > div > :last-of-type': { mb: 0.75, mr: 1 },
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@ -401,12 +416,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
<StyledMarkdown sx={{ display: 'flex' }} content={activeJob.description} />
|
||||
)}
|
||||
|
||||
{variant !== 'small' && variant !== 'minimal' && (
|
||||
<Box>
|
||||
<Divider />
|
||||
{renderJobRequirements()}
|
||||
</Box>
|
||||
)}
|
||||
{variant !== 'small' && variant !== 'minimal' && renderJobRequirements()}
|
||||
|
||||
{isAdmin && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', p: 1 }}>
|
||||
|
@ -20,7 +20,7 @@ import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||
import { Scrollable } from 'components/Scrollable';
|
||||
import { StyledMarkdown } from './StyledMarkdown';
|
||||
import { StyledMarkdown } from '../StyledMarkdown';
|
||||
|
||||
const emptyMetadata: ChatMessageMetaData = {
|
||||
model: 'qwen2.5',
|
||||
@ -259,6 +259,7 @@ const ResumeChat = forwardRef<ConversationHandle, ResumeChatProps>(
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="ResumeChat"
|
||||
ref={ref}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@ -358,7 +359,18 @@ const ResumeChat = forwardRef<ConversationHandle, ResumeChatProps>(
|
||||
</Scrollable>
|
||||
)}
|
||||
{/* 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
|
||||
onDelete={(): void => {
|
||||
chatSession && onDelete(chatSession);
|
@ -7,19 +7,19 @@ import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Tabs,
|
||||
Tab,
|
||||
Paper,
|
||||
FormControl,
|
||||
Select,
|
||||
MenuItem,
|
||||
InputLabel,
|
||||
Chip,
|
||||
Alert,
|
||||
Stack,
|
||||
SelectChangeEvent,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import PrintIcon from '@mui/icons-material/Print';
|
||||
import DifferenceIcon from '@mui/icons-material/Difference';
|
||||
import ForumIcon from '@mui/icons-material/Forum';
|
||||
import {
|
||||
Save as SaveIcon,
|
||||
ModelTraining,
|
||||
@ -42,10 +42,12 @@ 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';
|
||||
import { ResumeChat } from 'components/ui/ResumeChat';
|
||||
import { DiffViewer } from 'components/DiffViewer';
|
||||
import { ResumePreview, resumeStyles } from './ResumePreview';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TabWithTooltip } from './TabWithTooltip';
|
||||
|
||||
interface ResumeRevision {
|
||||
revisionId: string;
|
||||
@ -65,13 +67,17 @@ interface ResumeEditProps {
|
||||
const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
const { setSnack } = useAppState();
|
||||
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 { apiClient } = useAuth();
|
||||
const [editContent, setEditContent] = useState<string>('');
|
||||
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
const [tabValue, setTabValue] = useState('markdown');
|
||||
const [jobTabValue, setJobTabValue] = useState('chat');
|
||||
const [leftColumn, setLeftColumn] = useState('markdown');
|
||||
const [rightColumn, setRightColumn] = useState('chat');
|
||||
const [status, setStatus] = useState<string>('');
|
||||
const [statusType, setStatusType] = useState<Types.ApiActivityType | 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 [isAIGenerated, setIsAIGenerated] = useState<boolean>(false);
|
||||
const [editPrompt, setEditPrompt] = useState<string>('');
|
||||
const [columnView, setColumnView] = useState<'left' | 'right'>('left');
|
||||
|
||||
// Revision-related state
|
||||
const [revisions, setRevisions] = useState<ResumeRevision[]>([]);
|
||||
@ -325,7 +332,8 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
return generatedResume;
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: string): void => {
|
||||
const handleLeftColumnChange = (event: React.SyntheticEvent, newValue: string): void => {
|
||||
setColumnView('left');
|
||||
if (newValue === 'print') {
|
||||
console.log('Printing resume...');
|
||||
reactToPrintFn();
|
||||
@ -333,7 +341,7 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
}
|
||||
if (newValue === 'regenerate') {
|
||||
setSnack('Regenerating resume...');
|
||||
setTabValue('markdown');
|
||||
setLeftColumn('markdown');
|
||||
generateResume();
|
||||
return;
|
||||
}
|
||||
@ -341,14 +349,15 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
setEditContent(current?.resume || '');
|
||||
setEditPrompt(current?.prompt || '');
|
||||
setIsAIGenerated(current?.aiGenerated || false);
|
||||
setTabValue('markdown');
|
||||
setLeftColumn('markdown');
|
||||
return;
|
||||
}
|
||||
setTabValue(newValue);
|
||||
setLeftColumn(newValue);
|
||||
};
|
||||
|
||||
const handleJobTabChange = (event: React.SyntheticEvent, newValue: string): void => {
|
||||
setJobTabValue(newValue);
|
||||
const handleRightColumnChange = (event: React.SyntheticEvent, newValue: string): void => {
|
||||
setColumnView('right');
|
||||
setRightColumn(newValue);
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
@ -359,12 +368,10 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
(selectedRevision !== 'current' && resume?.aiGenerated ? resume?.prompt : '') ||
|
||||
(isAIGenerated ? editPrompt : '') ||
|
||||
'Manual edits';
|
||||
console.log(
|
||||
`Change log: ${changeLog}, isAIGenerated: ${isAIGenerated}, editPrompt: ${editPrompt}, selectedRevision: ${selectedRevision}, resume?.aiGenerated: ${resume?.aiGenerated}`
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="ResumeEdit"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
@ -375,6 +382,7 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="ResumeEditHeader"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
@ -384,7 +392,14 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
}}
|
||||
>
|
||||
<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
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@ -392,7 +407,7 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
gap: 0.5,
|
||||
m: 0,
|
||||
p: 0,
|
||||
minWidth: '25rem',
|
||||
minWidth: '20rem',
|
||||
}}
|
||||
>
|
||||
{resume && (
|
||||
@ -402,8 +417,21 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
{resume.job?.title || 'No Job Title Assigned'},{' '}
|
||||
{resume.job?.company || 'No Company Assigned'}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" color="text.secondary">
|
||||
Resume ID: {resume.id}
|
||||
<Typography
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
@ -413,7 +441,7 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, flexWrap: 'wrap' }}>
|
||||
{/* Style Selector */}
|
||||
<FormControl size="small" sx={{ minWidth: 'min-content' }}>
|
||||
<InputLabel id="resume-style-label">Resume Style</InputLabel>
|
||||
@ -444,87 +472,86 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<FormControl size="small" sx={{ minWidth: 200 }}>
|
||||
<InputLabel id="revision-select-label">
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
|
||||
<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 }}>
|
||||
<HistoryIcon fontSize="small" />
|
||||
Version History
|
||||
<Chip label="CURRENT" size="small" color="primary" variant="outlined" />
|
||||
<Typography variant="body2">Current Version</Typography>
|
||||
</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>
|
||||
</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>
|
||||
{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>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Tooltip title="Refresh Revisions">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={loadResumeRevisions}
|
||||
disabled={loadingRevisions}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<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' && (
|
||||
<>
|
||||
<Tooltip title="Restore this revision to editor">
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<RestoreFromTrashIcon />}
|
||||
onClick={restoreRevision}
|
||||
disabled={loadingRevision}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
className="ResumeEditContent"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
@ -533,6 +560,99 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
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
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@ -545,246 +665,213 @@ const ResumeEdit: React.FC<ResumeEditProps> = (props: ResumeEditProps) => {
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
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>
|
||||
)}
|
||||
|
||||
{(isLarge || columnView === 'left') && (
|
||||
<Box
|
||||
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 "Restore" 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 'Save'.
|
||||
</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,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
maxWidth: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Tabs value={jobTabValue} onChange={handleJobTabChange}>
|
||||
{resume && resume.job !== undefined && (
|
||||
<Tab value="job" icon={<WorkIcon />} label="Job" />
|
||||
<Box
|
||||
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 "Restore" to load this
|
||||
content into the editor.
|
||||
</Alert>
|
||||
)}
|
||||
<Tab value="chat" icon={<ModelTraining />} label="AI Edit" />
|
||||
</Tabs>
|
||||
|
||||
{resume && resume.job !== undefined && jobTabValue === 'job' && (
|
||||
<JobInfo
|
||||
variant={'all'}
|
||||
job={resume.job}
|
||||
{isAIGenerated && (
|
||||
<Alert severity="warning" sx={{ width: '100%' }}>
|
||||
This resume was generated by AI and has not been manually edited. Review and
|
||||
then selecte 'Save'.
|
||||
</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={{
|
||||
m: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 1,
|
||||
alignItems: 'center',
|
||||
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 && (
|
||||
<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',
|
||||
maxWidth: 'fit-content',
|
||||
minWidth: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
</Scrollable>
|
||||
</Box>
|
||||
)}
|
||||
{(isLarge || columnView === 'right') && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Scrollable
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
overflowY: 'auto',
|
||||
position: 'relative',
|
||||
p: 1,
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
@ -528,9 +528,9 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
|
||||
if (mode === 'edit' && selectedResume) {
|
||||
return (
|
||||
<ResumeEdit
|
||||
onClose={() => {
|
||||
navigate(window.location.pathname.replace('/edit', ''));
|
||||
}}
|
||||
// onClose={() => {
|
||||
// navigate(window.location.pathname.replace('/edit', ''));
|
||||
// }}
|
||||
resumeId={selectedResume.id}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
|
44
frontend/src/components/ui/TabWithTooltip.tsx
Normal file
44
frontend/src/components/ui/TabWithTooltip.tsx
Normal 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 };
|
Loading…
x
Reference in New Issue
Block a user