import React, { useState, useCallback, useRef } from 'react'; import { Tabs, Tab, Paper, IconButton, Box, useMediaQuery, Divider, Slider, Stack, } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { ChevronLeft, ChevronRight, SwapHoriz, } from '@mui/icons-material'; import { SxProps, Theme } from '@mui/material'; import { ChatQuery } from './Message'; import { MessageList, MessageData } from './Message'; import { SetSnackType } from './Snack'; import { Conversation } from './Conversation'; interface ResumeBuilderProps { connectionBase: string, sessionId: string | undefined, setSnack: SetSnackType, sx?: SxProps; }; /** * ResumeBuilder component * * A responsive component that displays job descriptions, generated resumes and fact checks * with different layouts for mobile and desktop views. */ const ResumeBuilder: React.FC = ({ sx, connectionBase, sessionId, setSnack }) => { // State for editing job description const [hasJobDescription, setHasJobDescription] = useState(false); const [hasResume, setHasResume] = useState(false); const [hasFacts, setHasFacts] = useState(false); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const jobConversationRef = useRef(null); const resumeConversationRef = useRef(null); const factsConversationRef = useRef(null); const [activeTab, setActiveTab] = useState(0); const [splitRatio, setSplitRatio] = useState(100); /** * 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); }; const handleJobQuery = (query: string) => { console.log(`handleJobQuery: ${query} -- `, jobConversationRef.current ? ' sending' : 'no handler'); jobConversationRef.current?.submitQuery(query); }; const handleResumeQuery = (query: string) => { console.log(`handleResumeQuery: ${query} -- `, resumeConversationRef.current ? ' sending' : 'no handler'); resumeConversationRef.current?.submitQuery(query); }; const handleFactsQuery = (query: string) => { console.log(`handleFactsQuery: ${query} -- `, factsConversationRef.current ? ' sending' : 'no handler'); factsConversationRef.current?.submitQuery(query); }; const filterJobDescriptionMessages = useCallback((messages: MessageList): MessageList => { if (messages === undefined || messages.length === 0) { return []; } let reduced = messages.filter((m, i) => { const keep = (m.metadata?.origin || m.origin || "no origin") === 'job_description'; if ((m.metadata?.origin || m.origin || "no origin") === 'resume') { setHasResume(true); } // if (!keep) { // console.log(`filterJobDescriptionMessages: ${i + 1} filtered:`, m); // } else { // console.log(`filterJobDescriptionMessages: ${i + 1}:`, m); // } return keep; }); if (reduced.length > 0) { // First message is always 'content' reduced[0].title = 'Job Description'; reduced[0].role = 'content'; setHasJobDescription(true); } /* If there is more than one message, it is user: "...JOB_DESCRIPTION...", assistant: "...stored..." * which means a resume has been generated. */ if (reduced.length > 1) { setHasResume(true); } /* Filter out any messages which the server injected for state management */ reduced = reduced.filter(m => m.display !== "hide"); return reduced; }, [setHasJobDescription, setHasResume]); const filterResumeMessages = useCallback((messages: MessageList): MessageList => { if (messages === undefined || messages.length === 0) { return []; } let reduced = messages.filter((m, i) => { const keep = (m.metadata?.origin || m.origin || "no origin") === 'resume'; if ((m.metadata?.origin || m.origin || "no origin") === 'fact_check') { setHasFacts(true); } // if (!keep) { // console.log(`filterResumeMessages: ${i + 1} filtered:`, m); // } else { // console.log(`filterResumeMessages: ${i + 1}:`, m); // } return keep; }); /* If there is more than one message, it is user: "...JOB_DESCRIPTION...", assistant: "...RESUME..." * which means a resume has been generated. */ if (reduced.length > 1) { /* Remove the assistant message from the UI */ if (reduced[0].role === "user") { reduced.splice(0, 1); } } /* If Fact Check hasn't occurred yet and there is still more than one message, * facts have have been generated. */ if (!hasFacts && reduced.length > 1) { setHasFacts(true); } /* Filter out any messages which the server injected for state management */ reduced = reduced.filter(m => m.display !== "hide"); /* If there are any messages, there is a resume */ if (reduced.length > 0) { // First message is always 'content' reduced[0].title = 'Resume'; reduced[0].role = 'content'; setHasResume(true); } return reduced; }, [setHasResume, hasFacts, setHasFacts]); const filterFactsMessages = useCallback((messages: MessageList): MessageList => { if (messages === undefined || messages.length === 0) { return []; } // messages.forEach((m, i) => console.log(`filterFactsMessages: ${i + 1}:`, m)) const reduced = messages.filter(m => { return (m.metadata?.origin || m.origin || "no origin") === 'fact_check'; }); /* If there is more than one message, it is user: "Fact check this resume...", assistant: "...FACT CHECK..." * which means facts have been generated. */ if (reduced.length > 1) { /* Remove the user message from the UI */ if (reduced[0].role === "user") { reduced.splice(0, 1); } // First message is always 'content' reduced[0].title = 'Fact Check'; reduced[0].role = 'content'; setHasFacts(true); } return reduced; }, [setHasFacts]); const jobResponse = useCallback((message: MessageData): MessageData => { console.log('onJobResponse', message); setHasResume(true); return message; }, []); const resumeResponse = useCallback((message: MessageData): MessageData => { console.log('onResumeResponse', message); setHasFacts(true); return message; }, [setHasFacts]); const factsResponse = useCallback((message: MessageData): MessageData => { console.log('onFactsResponse', message); return message; }, []); const renderJobDescriptionView = useCallback((small: boolean) => { const jobDescriptionQuestions = [ , ]; if (!hasJobDescription) { return } else { return } }, [connectionBase, filterJobDescriptionMessages, hasJobDescription, sessionId, setSnack, jobResponse]); /** * Renders the resume view with loading indicator */ const renderResumeView = useCallback((small: boolean) => { const resumeQuestions = [ , ]; if (!hasFacts) { return } else { return } }, [connectionBase, filterResumeMessages, hasFacts, sessionId, setSnack, resumeResponse]); /** * Renders the fact check view */ const renderFactCheckView = useCallback((small: boolean) => { const factsQuestions = [ , ]; return }, [connectionBase, sessionId, setSnack, factsResponse, filterFactsMessages]); /** * Gets the appropriate content based on active state for Desktop */ const getActiveDesktopContent = useCallback(() => { const hasSlider = hasResume || hasFacts; const ratio = 75 + 25 * splitRatio / 100; const otherRatio = hasResume ? ratio / (hasFacts ? 3 : 2) : 100; const resumeRatio = 100 - otherRatio * (hasFacts ? 2 : 1); const children = []; children.push( {renderJobDescriptionView(false)} ); /* Resume panel - conditionally rendered if resume defined, or processing is in progress */ if (hasResume) { children.push( {renderResumeView(false)} ); } /* Fact Check panel - conditionally rendered if facts defined, or processing is in progress */ if (hasFacts) { children.push( {renderFactCheckView(false)} ); } /* Split control panel - conditionally rendered if either facts or resume is set */ let slider = ; if (hasSlider) { slider = ( setSplitRatio(s => Math.max(0, s - 10))}> setSplitRatio(s => Math.min(100, s + 10))}> ); } return ( {children} {slider} ) }, [renderFactCheckView, renderJobDescriptionView, renderResumeView, splitRatio, hasFacts, hasResume]); // Render mobile view if (isMobile) { /** * Gets the appropriate content based on active tab */ const getActiveMobileContent = () => { switch (activeTab) { case 0: return renderJobDescriptionView(true); case 1: return renderResumeView(true); case 2: return renderFactCheckView(true); default: return renderJobDescriptionView(true); } }; return ( {/* Tabs */} {hasResume && } {hasFacts && } {/* Document display area */} {getActiveMobileContent()} ); } return getActiveDesktopContent(); }; export type { ResumeBuilderProps }; export { ResumeBuilder };