From b6cde81cfb2f42fdbeb1bb13c88352fc52296e5d Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Fri, 25 Apr 2025 21:37:32 -0700 Subject: [PATCH] Refactored elements --- Dockerfile | 2 + frontend/src/App.tsx | 48 +--- frontend/src/ChatBubble.tsx | 36 ++- frontend/src/Conversation.tsx | 2 +- frontend/src/Document.tsx | 45 --- frontend/src/DocumentViewer.tsx | 462 ------------------------------ frontend/src/Message.tsx | 23 +- frontend/src/ResumeBuilder.tsx | 480 ++++++++++++++++++++++++++++++-- frontend/src/Snack.tsx | 73 +++++ 9 files changed, 600 insertions(+), 571 deletions(-) delete mode 100644 frontend/src/Document.tsx delete mode 100644 frontend/src/DocumentViewer.tsx diff --git a/Dockerfile b/Dockerfile index ca85044..5abee11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -448,6 +448,8 @@ COPY /src/requirements.txt /opt/backstory/src/requirements.txt RUN pip install -r /opt/backstory/src/requirements.txt +RUN pip install timm xformers + SHELL [ "/bin/bash", "-c" ] RUN { \ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 83510a1..2d60618 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,8 +6,6 @@ import Avatar from '@mui/material/Avatar'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; import Tooltip from '@mui/material/Tooltip'; -import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar'; -import Alert from '@mui/material/Alert'; import AppBar from '@mui/material/AppBar'; import Drawer from '@mui/material/Drawer'; import Toolbar from '@mui/material/Toolbar'; @@ -21,7 +19,7 @@ import { useTheme } from '@mui/material/styles'; import { ResumeBuilder } from './ResumeBuilder'; import { Message, ChatQuery, MessageList, MessageData } from './Message'; -import { SetSnackType, SeverityType } from './Snack'; +import { Snack, SeverityType } from './Snack'; import { VectorVisualizer } from './VectorVisualizer'; import { Controls } from './Controls'; import { Conversation, ConversationHandle } from './Conversation'; @@ -74,9 +72,6 @@ const App = () => { const [connectionBase,] = useState(getConnectionBase(window.location)) const [menuOpen, setMenuOpen] = useState(false); const [isMenuClosing, setIsMenuClosing] = useState(false); - const [snackOpen, setSnackOpen] = useState(false); - const [snackMessage, setSnackMessage] = useState(""); - const [snackSeverity, setSnackSeverity] = useState("success"); const [tab, setTab] = useState(0); const [about, setAbout] = useState(""); const [resume, setResume] = useState(undefined); @@ -86,15 +81,7 @@ const App = () => { const chatRef = useRef(null); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); - - // Set the snack pop-up and open it - const setSnack: SetSnackType = useCallback((message: string, severity: SeverityType = "success") => { - setTimeout(() => { - setSnackMessage(message); - setSnackSeverity(severity); - setSnackOpen(true); - }); - }, [setSnackMessage, setSnackSeverity, setSnackOpen]); + const snackRef = useRef(null); useEffect(() => { if (prevIsDesktopRef.current === isDesktop) @@ -107,6 +94,10 @@ const App = () => { prevIsDesktopRef.current = isDesktop; }, [isDesktop, setMenuOpen, menuOpen]) + const setSnack = useCallback((message: string, severity?: SeverityType) => { + snackRef.current?.setSnack(message, severity); + }, [snackRef]); + // Get the About markdown useEffect(() => { if (about !== "") { @@ -143,9 +134,8 @@ const App = () => { const chatPreamble: MessageList = [ { role: 'content', + title: 'Welcome to Backstory', content: ` -# Welcome to Backstory - Backstory is a RAG enabled expert system with access to real-time data running self-hosted (no cloud) versions of industry leading Large and Small Language Models (LLM/SLMs). It was written by James Ketrenos in order to provide answers to @@ -278,17 +268,6 @@ What would you like to know about James? ); - const handleSnackClose = ( - event: React.SyntheticEvent | Event, - reason?: SnackbarCloseReason, - ) => { - if (reason === 'clickaway') { - return; - } - - setSnackOpen(false); - }; - /* toolbar height is 56px + 8px margin-top */ const Offset = styled('div')(({ theme }) => ({ ...theme.mixins.toolbar, minHeight: '64px', height: '64px' })); @@ -454,16 +433,9 @@ What would you like to know about James? - - - {snackMessage} - - + ); }; diff --git a/frontend/src/ChatBubble.tsx b/frontend/src/ChatBubble.tsx index ec857b1..95068ff 100644 --- a/frontend/src/ChatBubble.tsx +++ b/frontend/src/ChatBubble.tsx @@ -1,7 +1,12 @@ -import { Box } from '@mui/material'; +import React from 'react'; +import { Box, Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { SxProps, Theme } from '@mui/material'; -import React from 'react'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; + import { MessageRoles } from './Message'; interface ChatBubbleProps { @@ -11,9 +16,10 @@ interface ChatBubbleProps { children: React.ReactNode; sx?: SxProps; className?: string; + title?: string; } -function ChatBubble({ role, isFullWidth, children, sx, className }: ChatBubbleProps) { +function ChatBubble({ role, isFullWidth, children, sx, className, title }: ChatBubbleProps) { const theme = useTheme(); const defaultRadius = '16px'; @@ -96,7 +102,7 @@ function ChatBubble({ role, isFullWidth, children, sx, className }: ChatBubblePr alignSelf: 'center', // Centered in the chat color: theme.palette.text.primary, // Charcoal Black for maximum readability padding: '8px 8px', // More generous padding for better text framing - marginBottom: '20px', // Space between content and conversation + marginBottom: '0px', // Space between content and conversation boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)', // Subtle elevation fontSize: '0.9rem', // Slightly smaller than default lineHeight: '1.3', // More compact line height @@ -104,6 +110,28 @@ function ChatBubble({ role, isFullWidth, children, sx, className }: ChatBubblePr }, }; + if (role === 'content' && title) { + return ( + + } + slotProps={{ content: { sx: { fontWeight: 'bold', fontSize: '1.1rem', m: 0, p: 0, display: 'flex', justifyItems: 'center' } } }} + > + {title} + + + {children} + + + + ); + + } + return ( {children} diff --git a/frontend/src/Conversation.tsx b/frontend/src/Conversation.tsx index 068d30e..5c10f65 100644 --- a/frontend/src/Conversation.tsx +++ b/frontend/src/Conversation.tsx @@ -500,7 +500,7 @@ const Conversation = forwardRef(({ return ( { diff --git a/frontend/src/Document.tsx b/frontend/src/Document.tsx deleted file mode 100644 index f960f0c..0000000 --- a/frontend/src/Document.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; -import { SxProps, Theme } from '@mui/material'; - -/** - * Props for the Document component - * @interface DocumentComponentProps - * @property {string} title - The title of the document - * @property {React.ReactNode} [children] - The content of the document - */ -interface DocumentComponentProps { - title: string; - children?: React.ReactNode; - sx?: SxProps; -} - -/** - * Document component renders a container with optional title and scrollable content - * - * This component provides a consistent document viewing experience across the application - * with a title header and scrollable content area - */ -const Document: React.FC = ({ title, children, sx }) => ( - - { - title !== "" && - {title} - } - - {children} - - -); - -export { - Document -}; \ No newline at end of file diff --git a/frontend/src/DocumentViewer.tsx b/frontend/src/DocumentViewer.tsx deleted file mode 100644 index c1f041a..0000000 --- a/frontend/src/DocumentViewer.tsx +++ /dev/null @@ -1,462 +0,0 @@ -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'; - -/** - * Props for the DocumentViewer component - * @interface DocumentViewerProps - * @property {SxProps} [sx] - Optional styling properties - * @property {string} [connectionBase] - Base URL for fetch calls - * @property {string} [sessionId] - Session ID - * @property {SetSnackType} - setSnack UI callback - */ -export interface DocumentViewerProps { - sx?: SxProps; - connectionBase: string; - sessionId: string; - setSnack: SetSnackType; -} -/** - * 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 = ({ - 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 'user' - reduced[0].role = 'assistant'; - 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].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].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(() => { - /* Left panel - Job Description */ - const showResume = hasResume - const showFactCheck = hasFacts - const ratio = 75 + 25 * splitRatio / 100; - const otherRatio = showResume ? 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 (showResume) { - children.push( - - - {renderResumeView(false)} - - ); - } - - /* Fact Check panel - conditionally rendered if facts defined, or processing is in progress */ - if (showFactCheck) { - children.push( - - - {renderFactCheckView(false)} - - ); - } - - /* Split control panel - conditionally rendered if either facts or resume is set */ - let slider = ; - if (showResume || showFactCheck) { - slider = ( - - - setSplitRatio(s => Math.max(0, s - 10))}> - - - - - - setSplitRatio(s => Math.min(100, s + 10))}> - - - - - - - - - ); - } - - return ( - - - {children} - - {slider} - - ) - }, [renderFactCheckView, renderJobDescriptionView, renderResumeView, splitRatio, sx, 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 { - DocumentViewer -}; \ No newline at end of file diff --git a/frontend/src/Message.tsx b/frontend/src/Message.tsx index 23653fc..2de7679 100644 --- a/frontend/src/Message.tsx +++ b/frontend/src/Message.tsx @@ -35,6 +35,7 @@ type MessageData = { role: MessageRoles, content: string, user?: string, + title?: string, origin?: string, display?: string, /* Messages generated on the server for filler should not be shown */ id?: string, @@ -250,8 +251,21 @@ const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, conne const formattedContent = message.content.trim(); return ( - - + + {message.metadata && <> - + } [sx] - Optional styling properties + * @property {string} [connectionBase] - Base URL for fetch calls + * @property {string} [sessionId] - Session ID + * @property {SetSnackType} - setSnack UI callback + */ +export interface DocumentViewerProps { + sx?: SxProps; + connectionBase: string; + sessionId: string; + setSnack: SetSnackType; +} +/** + * 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 = ({ + 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(() => { + /* Left panel - Job Description */ + const showResume = hasResume + const showFactCheck = hasFacts + const ratio = 75 + 25 * splitRatio / 100; + const otherRatio = showResume ? 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 (showResume) { + children.push( + + + {renderResumeView(false)} + + ); + } + + /* Fact Check panel - conditionally rendered if facts defined, or processing is in progress */ + if (showFactCheck) { + children.push( + + + {renderFactCheckView(false)} + + ); + } + + /* Split control panel - conditionally rendered if either facts or resume is set */ + let slider = ; + if (showResume || showFactCheck) { + slider = ( + + + setSplitRatio(s => Math.max(0, s - 10))}> + + + + + + setSplitRatio(s => Math.min(100, s + 10))}> + + + + + + + + + ); + } + + return ( + + + {children} + + {slider} + + ) + }, [renderFactCheckView, renderJobDescriptionView, renderResumeView, splitRatio, sx, 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()} + + ); +}; interface ResumeBuilderProps { - setProcessing: (processing: boolean) => void, - processing: boolean, connectionBase: string, sessionId: string | undefined, setSnack: (message: string, severity?: SeverityType) => void, - resume: MessageData | undefined, - setResume: (resume: MessageData | undefined) => void, - facts: MessageData | undefined, - setFacts: (facts: MessageData | undefined) => void, }; -// type Resume = { -// resume: MessageData | undefined, -// fact_check: MessageData | undefined, -// job_description: string, -// metadata: MessageMetaProps -// }; - -const ResumeBuilder = ({ facts, setFacts, resume, setResume, setProcessing, processing, connectionBase, sessionId, setSnack }: ResumeBuilderProps) => { +const ResumeBuilder = ({ connectionBase, sessionId, setSnack }: ResumeBuilderProps) => { if (sessionId === undefined) { return (<>); } diff --git a/frontend/src/Snack.tsx b/frontend/src/Snack.tsx index 8714201..a31a501 100644 --- a/frontend/src/Snack.tsx +++ b/frontend/src/Snack.tsx @@ -1,7 +1,80 @@ +import React, { useState, useCallback, useImperativeHandle, forwardRef } from 'react'; +import { SxProps, Theme } from '@mui/material'; +import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; + +import './Snack.css'; + type SeverityType = 'error' | 'info' | 'success' | 'warning' | undefined; type SetSnackType = (message: string, severity?: SeverityType) => void; +interface SnackHandle { + setSnack: SetSnackType; +}; + +interface SnackProps { + sx?: SxProps; + className?: string; +}; + +const Snack = forwardRef(({ + className, + sx +}: SnackProps, ref) => { + const [open, setOpen] = useState(false); + const [message, setMessage] = useState(""); + const [severity, setSeverity] = useState("success"); + + // Set the snack pop-up and open it + const setSnack: SetSnackType = useCallback((message: string, severity: SeverityType = "success") => { + setTimeout(() => { + setMessage(message); + setSeverity(severity); + setOpen(true); + }); + }, [setMessage, setSeverity, setOpen]); + + useImperativeHandle(ref, () => ({ + setSnack: (message: string, severity?: SeverityType) => { + setSnack(message, severity); + } + })); + + const handleSnackClose = ( + event: React.SyntheticEvent | Event, + reason?: SnackbarCloseReason, + ) => { + if (reason === 'clickaway') { + return; + } + + setOpen(false); + }; + + return ( + + + {message} + + + ) +}); + export type { SeverityType, SetSnackType +}; + +export { + Snack }; \ No newline at end of file