From d66e1ee1e44a6249ca0a91b792f8b0eb1d7ecd96 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Tue, 8 Jul 2025 12:46:59 -0700 Subject: [PATCH] Added resume styles --- frontend/package-lock.json | 1 + frontend/package.json | 1 + .../src/components/ui/CandidatePicker.tsx | 2 +- frontend/src/components/ui/JobsView.tsx | 10 +- frontend/src/components/ui/ResumeInfo.css | 15 +- frontend/src/components/ui/ResumeInfo.tsx | 629 ++++++++++++++++-- frontend/src/config/navigationConfig.tsx | 15 +- frontend/src/pages/HowItWorks.tsx | 1 - src/backend/agents/generate_resume.py | 4 +- 9 files changed, 596 insertions(+), 82 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6500c53..23ab128 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -29,6 +29,7 @@ "@uiw/react-markdown-editor": "^6.1.4", "country-state-city": "^3.2.1", "jsonrepair": "^3.12.0", + "libphonenumber-js": "^1.12.9", "lodash": "^4.17.21", "lucide-react": "^0.511.0", "luxon": "^3.6.1", diff --git a/frontend/package.json b/frontend/package.json index a100d81..447a70b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "@uiw/react-markdown-editor": "^6.1.4", "country-state-city": "^3.2.1", "jsonrepair": "^3.12.0", + "libphonenumber-js": "^1.12.9", "lodash": "^4.17.21", "lucide-react": "^0.511.0", "luxon": "^3.6.1", diff --git a/frontend/src/components/ui/CandidatePicker.tsx b/frontend/src/components/ui/CandidatePicker.tsx index de15583..1cbb8bc 100644 --- a/frontend/src/components/ui/CandidatePicker.tsx +++ b/frontend/src/components/ui/CandidatePicker.tsx @@ -14,7 +14,7 @@ interface CandidatePickerProps extends BackstoryElementProps { } const CandidatePicker = (props: CandidatePickerProps): JSX.Element => { - const { onSelect, sx } = props; + const { onSelect } = props; const { apiClient } = useAuth(); const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate(); const { setSnack } = useAppState(); diff --git a/frontend/src/components/ui/JobsView.tsx b/frontend/src/components/ui/JobsView.tsx index 20a73aa..a82760a 100644 --- a/frontend/src/components/ui/JobsView.tsx +++ b/frontend/src/components/ui/JobsView.tsx @@ -73,7 +73,7 @@ interface JobsViewProps { onJobSelect?: (selectedJobs: Types.Job[]) => void; onJobView?: (job: Types.Job) => void; onJobEdit?: (job: Types.Job) => void; - onJobDelete?: (job: Types.Job) => void; + onJobDelete?: (job: Types.Job) => Promise; selectable?: boolean; showActions?: boolean; showDetailsPanel?: boolean; @@ -609,7 +609,13 @@ const JobsView: React.FC = ({ )} {onJobDelete && ( - onJobDelete(job)}> + => { + await onJobDelete(job); + fetchJobs(0, searchQuery); + }} + > diff --git a/frontend/src/components/ui/ResumeInfo.css b/frontend/src/components/ui/ResumeInfo.css index 23fcdfb..7272c3a 100644 --- a/frontend/src/components/ui/ResumeInfo.css +++ b/frontend/src/components/ui/ResumeInfo.css @@ -136,4 +136,17 @@ border-radius: 5px; overflow-x: auto; margin: 1em 0; -} \ No newline at end of file +} + +.BackstoryResumeHeader { + gap: 1rem; + display: flex; + flex-direction: column; + /* border: 3px solid orange; */ +} + +.BackstoryResumeHeader p { + /* border: 3px solid purple; */ + margin: 0; +} + diff --git a/frontend/src/components/ui/ResumeInfo.tsx b/frontend/src/components/ui/ResumeInfo.tsx index 1e7f2c4..94c0424 100644 --- a/frontend/src/components/ui/ResumeInfo.tsx +++ b/frontend/src/components/ui/ResumeInfo.tsx @@ -22,6 +22,12 @@ import { Tabs, Tab, Paper, + FormControl, + Select, + MenuItem, + InputLabel, + Chip, + Theme, } from '@mui/material'; import PrintIcon from '@mui/icons-material/Print'; import { @@ -34,11 +40,17 @@ import { Person as PersonIcon, Schedule as ScheduleIcon, ModelTraining, + Style as StyleIcon, + 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'; @@ -62,6 +74,450 @@ interface ResumeInfoProps { variant?: 'minimal' | 'small' | 'normal' | 'all' | null; } +// Resume Style Definitions +interface ResumeStyle { + name: string; + description: string; + headerStyle: SxProps; + footerStyle: SxProps; + contentStyle: SxProps; + markdownStyle: SxProps; + color: { + primary: string; + secondary: string; + accent: string; + text: string; + background: string; + }; +} + +const resumeStyles: Record = { + classic: { + name: 'Classic', + description: 'Traditional, professional serif design', + headerStyle: { + 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: { + 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: { + 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: { + 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', + }, + }, +}; + +// Styled Header Component +interface BackstoryStyledResumeProps { + candidate: Types.Candidate; + style: ResumeStyle; +} + +const StyledFooter: React.FC = ({ candidate, style }) => { + return ( + <> + + Ask any questions you may have at my Backstory... + + {candidate?.username + ? `https://backstory.ketrenos.com/u/${candidate?.username}` + : 'backstory'} + +   + + ); +}; + +const StyledHeader: React.FC = ({ candidate, style }) => { + const phone = parsePhoneNumberFromString(candidate.phone || '', 'US'); + return ( + + + {candidate.fullName} + + + {/* {candidate.title && ( + + {candidate.title} + + )} */} + + + {candidate.email && ( + + + + + {candidate.email} + + + + )} + + {phone?.isValid() && ( + + + + + {phone.formatInternational()} + + + + )} + + {candidate.location && ( + + + + + {candidate.location.city}, {candidate.location.state} + + + + )} + + {/* {(candidate.website || candidate.linkedin) && ( + + + + + {candidate.website || candidate.linkedin} + + + + )} */} + + + ); +}; + const ResumeInfo: React.FC = (props: ResumeInfoProps) => { const { setSnack } = useAppState(); const { resume } = props; @@ -81,6 +537,7 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { const [status, setStatus] = useState(''); const [statusType, setStatusType] = useState(null); const [error, setError] = useState(null); + const [selectedStyle, setSelectedStyle] = useState('modern'); const printContentRef = useRef(null); const reactToPrintFn = useReactToPrint({ @@ -94,7 +551,9 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { } }, [resume, activeResume]); - // Check if content needs truncation + const currentStyle = resumeStyles[selectedStyle]; + + // Rest of the component remains the same... const deleteResume = async (id: string | undefined): Promise => { if (id) { try { @@ -195,7 +654,7 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { const generateResume = async (): Promise => { setStatusType('thinking'); setStatus('Starting resume generation...'); - setActiveResume({ ...activeResume, resume: '' }); // Reset resume content + setActiveResume({ ...activeResume, resume: '' }); const request = await apiClient.generateResume( activeResume.candidateId || '', activeResume.jobId || '', @@ -210,7 +669,6 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { return; } if (newValue === 'regenerate') { - // Handle resume regeneration logic here setSnack('Regenerating resume...'); generateResume(); return; @@ -487,18 +945,65 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { overflow: 'hidden', }} > - - } label="Markdown" /> - {activeResume.systemPrompt && ( - } label="System Prompt" /> - )} - {activeResume.systemPrompt && ( - } label="Prompt" /> - )} - } label="Preview" /> - } label="Print" /> - } label="Regenerate" /> - + + + } label="Markdown" /> + {activeResume.systemPrompt && ( + } label="System Prompt" /> + )} + {activeResume.systemPrompt && ( + } label="Prompt" /> + )} + } label="Preview" /> + } label="Print" /> + } label="Regenerate" /> + + + {/* Style Selector */} + + + + Resume Style + + + + + {/* Style Preview Chip */} + } + label={currentStyle.name} + sx={{ + backgroundColor: currentStyle.color.primary, + color: '#ffffff', + fontWeight: 'bold', + }} + /> + + {status && ( @@ -514,12 +1019,11 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { sx={{ display: 'flex', flexDirection: 'column', - height: '100%' /* Restrict to main-container's height */, + height: '100%', width: '100%', - minHeight: 0 /* Prevent flex overflow */, - //maxHeight: "min-content", + minHeight: 0, '& > *:not(.Scrollable)': { - flexShrink: 0 /* Prevent shrinking */, + flexShrink: 0, }, position: 'relative', }} @@ -535,10 +1039,9 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { width: '100%', display: 'flex', minHeight: '100%', - flexGrow: 1, - flex: 1 /* Take remaining space in some-container */, - overflowY: 'auto' /* Scroll if content overflows */, + flex: 1, + overflowY: 'auto', }} placeholder="Enter resume content..." /> @@ -550,13 +1053,12 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { style={{ position: 'relative', maxHeight: '100%', - // height: '100%', width: '100%', display: 'flex', minHeight: '100%', flexGrow: 1, - flex: 1 /* Take remaining space in some-container */, - overflowY: 'auto' /* Scroll if content overflows */, + flex: 1, + overflowY: 'auto', }} placeholder="Edit system prompt..." /> @@ -572,52 +1074,53 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { width: '100%', display: 'flex', minHeight: '100%', - flexGrow: 1, - flex: 1 /* Take remaining space in some-container */, - overflowY: 'auto' /* Scroll if content overflows */, + flex: 1, + overflowY: 'auto', }} placeholder="Edit prompt..." /> )} {tabValue === 'preview' && ( - - - - - See my full Backstory at... - + + {/* Custom Header */} + {activeResume.candidate && ( + + )} + + {/* Styled Markdown Content */} + + - {activeResume.candidate?.username - ? `https://backstory.ketrenos.com/u/${activeResume.candidate?.username}` - : 'backstory'} + + {/* QR Code Footer */} + {activeResume.candidate && ( + + )} -   )} @@ -647,7 +1150,7 @@ const ResumeInfo: React.FC = (props: ResumeInfoProps) => { job={activeResume.job} sx={{ mt: 2, - backgroundColor: '#f8f0e0', //theme.palette.background.paper, + backgroundColor: '#f8f0e0', }} /> )} diff --git a/frontend/src/config/navigationConfig.tsx b/frontend/src/config/navigationConfig.tsx index f9abae0..2f0d0ec 100644 --- a/frontend/src/config/navigationConfig.tsx +++ b/frontend/src/config/navigationConfig.tsx @@ -27,11 +27,9 @@ import { VectorVisualizer } from 'components/VectorVisualizer'; import { DocumentManager } from 'components/DocumentManager'; import { useAuth } from 'hooks/AuthContext'; import { useNavigate } from 'react-router-dom'; -import { JobsView } from 'components/ui/JobsView'; +import { JobsViewPage } from 'pages/JobsViewPage'; import { ResumeViewer } from 'components/ui/ResumeViewer'; -import * as Types from 'types/types'; - const LogoutPage = (): JSX.Element => { const { logout } = useAuth(); const navigate = useNavigate(); @@ -127,16 +125,7 @@ export const navigationConfig: NavigationConfig = { label: 'Jobs', path: '/candidate/jobs/:jobId?', icon: , - component: ( - console.log('Selected:', selectedJobs)} - onJobView={(job: Types.Job): void => console.log('View job:', job)} - onJobEdit={(job: Types.Job): void => console.log('Edit job:', job)} - onJobDelete={(job: Types.Job): void => console.log('Delete job:', job)} - selectable={true} - showActions={true} - /> - ), + component: , variant: 'fullWidth', userTypes: ['candidate', 'guest', 'employer'], showInNavigation: false, diff --git a/frontend/src/pages/HowItWorks.tsx b/frontend/src/pages/HowItWorks.tsx index 1de193e..46a414d 100644 --- a/frontend/src/pages/HowItWorks.tsx +++ b/frontend/src/pages/HowItWorks.tsx @@ -29,7 +29,6 @@ import waitPng from 'assets/wait.png'; import finalResumePng from 'assets/final-resume.png'; import { Beta } from 'components/ui/Beta'; -import { Quote } from '@uiw/react-json-view'; // Styled components matching HomePage patterns const HeroSection = styled(Box)(({ theme }) => ({ diff --git a/src/backend/agents/generate_resume.py b/src/backend/agents/generate_resume.py index 92dd99f..1b15145 100644 --- a/src/backend/agents/generate_resume.py +++ b/src/backend/agents/generate_resume.py @@ -77,6 +77,8 @@ class GenerateResume(Agent): Create a polished, concise, and ATS-friendly resume for the candidate based on the assessment data provided. Rephrase skills to avoid direct duplication from the assessment. +Do not provide header information like name, email, or phone number in the resume, as that information will be added later. + ## CANDIDATE INFORMATION: Name: {self.user.full_name} Email: {self.user.email or "N/A"} @@ -164,7 +166,7 @@ ELSE: Provide template format only ## OUTPUT FORMAT: -Provide the resume in clean markdown format, ready for the candidate to use. +Provide the resume in clean markdown format, ready for the candidate to use. Do not provide header contact information, as that will be added later. """ prompt = """\