import React, { useEffect, useState, useCallback } from 'react'; import { Typography, Card, Button, Tabs, Tab, Paper, IconButton, Box, useMediaQuery, Divider, Slider, Stack, TextField, Tooltip } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import SendIcon from '@mui/icons-material/Send'; import { ChevronLeft, ChevronRight, SwapHoriz, RestartAlt as ResetIcon, } from '@mui/icons-material'; import PropagateLoader from "react-spinners/PropagateLoader"; import { Message } from './Message'; import { Document } from './Document'; import { DocumentViewerProps } from './DocumentTypes'; import MuiMarkdown from 'mui-markdown'; /** * DocumentViewer component * * A responsive component that displays job descriptions, generated resumes and fact checks * with different layouts for mobile and desktop views. */ const DocumentViewer: React.FC = ({ generateResume, jobDescription, factCheck, resume, setResume, facts, setFacts, sx }) => { // State for editing job description const [editJobDescription, setEditJobDescription] = useState(jobDescription); // Processing state to show loading indicators const [processing, setProcessing] = useState(undefined); // Theme and responsive design setup const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); // State for controlling which document is active on mobile const [activeTab, setActiveTab] = useState(0); // State for controlling split ratio on desktop const [splitRatio, setSplitRatio] = useState(0); /** * Reset processing state when resume is generated */ useEffect(() => { if (resume !== undefined && processing === "resume") { setProcessing(undefined); } }, [processing, resume]); /** * Reset processing state when facts is generated */ useEffect(() => { if (facts !== undefined && processing === "facts") { setProcessing(undefined); } }, [processing, facts]); /** * Trigger resume generation and update UI state */ const triggerGeneration = useCallback((jobDescription: string | undefined) => { if (jobDescription === undefined) { setProcessing(undefined); setResume(undefined); setActiveTab(0); return; } setProcessing("resume"); setTimeout(() => { setActiveTab(1); }, 250); // Switch to resume view on mobile generateResume(jobDescription); }, [generateResume, setProcessing, setActiveTab, setResume]); /** * Trigger fact check and update UI state */ const triggerFactCheck = useCallback((resume: string | undefined) => { if (resume === undefined) { setProcessing(undefined); setResume(undefined); setFacts(undefined); setActiveTab(1); return; } setProcessing("facts"); factCheck(resume); setTimeout(() => { setActiveTab(2); }, 250); // Switch to resume view on mobile }, [factCheck, setResume, setProcessing, setActiveTab, setFacts]); /** * Switch to resume tab when resume become available */ useEffect(() => { if (resume !== undefined) { setTimeout(() => { setActiveTab(1); }, 250); // Switch to resume view on mobile } }, [resume]); /** * Switch to fact check tab when facts become available */ useEffect(() => { if (facts !== undefined) { setTimeout(() => { setActiveTab(2); }, 250); // Switch to resume view on mobile } }, [facts]); /** * Handle tab change for mobile view */ const handleTabChange = (_event: React.SyntheticEvent, newValue: number): void => { setActiveTab(newValue); }; /** * Adjust split ratio for desktop view */ const handleSliderChange = (_event: Event, newValue: number | number[]): void => { setSplitRatio(newValue as number); }; /** * Reset split ratio to default */ const resetSplit = (): void => { setSplitRatio(50); }; /** * Handle keyboard shortcuts */ const handleKeyPress = (event: React.KeyboardEvent): void => { if (event.key === 'Enter' && event.ctrlKey) { triggerGeneration(editJobDescription || ""); } }; const renderJobDescriptionView = () => { const jobDescription = []; if (resume === undefined && processing === undefined) { jobDescription.push( setEditJobDescription(e.target.value)} onKeyDown={handleKeyPress} placeholder="Paste a job description, then click Generate..." /> ); } else { jobDescription.push({editJobDescription}) } jobDescription.push( { setEditJobDescription(""); triggerGeneration(undefined); }} > ); return jobDescription; } /** * Renders the resume view with loading indicator */ const renderResumeView = () => ( {resume !== undefined && } {processing === "resume" && ( Generating resume... )} ); /** * Renders the fact check view */ const renderFactCheckView = () => ( {facts !== undefined && } {processing === "facts" && ( Fact Checking resume... )} ); // Render mobile view if (isMobile) { /** * Gets the appropriate content based on active tab */ const getActiveMobileContent = () => { switch (activeTab) { case 0: return renderJobDescriptionView(); case 1: return renderResumeView(); case 2: return renderFactCheckView(); default: return renderJobDescriptionView(); } }; return ( {/* Tabs */} {(resume !== undefined || processing === "resume") && } {(facts !== undefined || processing === "facts") && } {/* Document display area */} {getActiveMobileContent()} ); } /** * Gets the appropriate content based on active state for Desktop */ const getActiveDesktopContent = () => { /* Left panel - Job Description */ const showResume = resume !== undefined || processing === "resume" const showFactCheck = facts !== undefined || processing === "facts" const otherRatio = showResume ? (100 - splitRatio / 2) : 100; const children = []; children.push( {renderJobDescriptionView()} ); /* Resume panel - conditionally rendered if resume defined, or processing is in progress */ if (showResume) { children.push( {renderResumeView()} ); } /* Fact Check panel - conditionally rendered if facts defined, or processing is in progress */ if (showFactCheck) { children.push( {renderFactCheckView()} ); } /* Split control panel - conditionally rendered if either facts or resume is set */ let slider = ; if (showResume || showFactCheck) { slider = ( setSplitRatio(Math.max(0, splitRatio - 10))}> setSplitRatio(Math.min(100, splitRatio + 10))}> ); } return ( {children} {slider} ) } return ( {getActiveDesktopContent()} ); }; /** * Props for the ResumeActionCard component */ interface ResumeActionCardProps { resume: any; processing: string | undefined; triggerFactCheck: (resume: string | undefined) => void; } /** * Action card displayed underneath the resume with notes and fact check button */ const ResumeActionCard: React.FC = ({ resume, processing, triggerFactCheck }) => ( {resume !== undefined || processing === "resume" ? ( NOTE: As with all LLMs, hallucination is always a possibility. If the generated resume seems too good to be true, Fact Check or, expand the LLM information for this query section (at the end of the resume) and click the links in the Top RAG matches to view the relavent RAG source document to read the details. Or go back to 'Backstory' and ask a question. ) : ( Once you click Generate under the Job Description, a resume will be generated based on the user's RAG content and the job description. )} { triggerFactCheck(undefined); }} > ); export { DocumentViewer };