import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import useMediaQuery from '@mui/material/useMediaQuery'; import Card from '@mui/material/Card'; import { styled } from '@mui/material/styles'; 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 AppBar from '@mui/material/AppBar'; import Drawer from '@mui/material/Drawer'; import Toolbar from '@mui/material/Toolbar'; import SettingsIcon from '@mui/icons-material/Settings'; import IconButton from '@mui/material/IconButton'; import Box from '@mui/material/Box'; import CssBaseline from '@mui/material/CssBaseline'; import MenuIcon from '@mui/icons-material/Menu'; import { Snack, SeverityType } from './Snack'; import { ConversationHandle } from './Conversation'; import { QueryOptions } from './ChatQuery'; import { Scrollable } from './Scrollable'; import { BackstoryPage, BackstoryTabProps } from './BackstoryTab'; import { connectionBase } from './Global'; import { HomePage } from './HomePage'; import { ResumeBuilderPage } from './ResumeBuilderPage'; import { VectorVisualizerPage } from './VectorVisualizer'; import { AboutPage } from './AboutPage'; import { ControlsPage } from './ControlsPage'; import './App.css'; import './Conversation.css'; import '@fontsource/roboto/300.css'; import '@fontsource/roboto/400.css'; import '@fontsource/roboto/500.css'; import '@fontsource/roboto/700.css'; const isValidUUIDv4 = (str: string): boolean => { const pattern = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89ab][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/i; return pattern.test(str); } const App = () => { const [sessionId, setSessionId] = useState(undefined); const [menuOpen, setMenuOpen] = useState(false); const [isMenuClosing, setIsMenuClosing] = useState(false); const [activeTab, setActiveTab] = useState(0); const isDesktop = useMediaQuery('(min-width:650px)'); const prevIsDesktopRef = useRef(isDesktop); const chatRef = useRef(null); const snackRef = useRef(null); const [subRoute, setSubRoute] = useState(""); useEffect(() => { if (prevIsDesktopRef.current === isDesktop) return; if (menuOpen) { setMenuOpen(false); } prevIsDesktopRef.current = isDesktop; }, [isDesktop, setMenuOpen, menuOpen]) const setSnack = useCallback((message: string, severity?: SeverityType) => { snackRef.current?.setSnack(message, severity); }, [snackRef]); const handleSubmitChatQuery = (prompt: string, tunables?: QueryOptions) => { console.log(`handleSubmitChatQuery: ${prompt} ${tunables || {}} -- `, chatRef.current ? ' sending' : 'no handler'); chatRef.current?.submitQuery(prompt, tunables); setActiveTab(0); }; const tabs: BackstoryTabProps[] = useMemo(() => { const homeTab: BackstoryTabProps = { label: "", path: "", tabProps: { label: "Backstory", sx: { flexGrow: 1, fontSize: '1rem' }, icon: , iconPosition: "start" }, children: }; const resumeBuilderTab: BackstoryTabProps = { label: "Resume Builder", path: "resume-builder", children: }; const contextVisualizerTab: BackstoryTabProps = { label: "Context Visualizer", path: "context-visualizer", children: }; const aboutTab = { label: "About", path: "about", children: }; const controlsTab: BackstoryTabProps = { path: "controls", tabProps: { sx: { flexShrink: 1, flexGrow: 0, fontSize: '1rem' }, icon: }, children: ( {sessionId !== undefined && } ) }; return [ homeTab, resumeBuilderTab, contextVisualizerTab, aboutTab, controlsTab, ]; }, [sessionId, setSnack, subRoute]); useEffect(() => { if (sessionId === undefined || activeTab > tabs.length - 1) { return; } console.log(`route - '${tabs[activeTab].path}', subRoute - '${subRoute}'`); let path = tabs[activeTab].path ? `/${tabs[activeTab].path}` : ''; if (subRoute) { path += `/${subRoute}`; } path += `/${sessionId}`; console.log('pushState: ', path); // window.history.pushState({}, '', path); }, [activeTab, sessionId, subRoute, tabs]); const fetchSession = useCallback((async (pathParts?: string[]) => { try { const response = await fetch(connectionBase + `/api/context`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw Error("Server is temporarily down."); } const new_session = (await response.json()).id; console.log(`Session created: ${new_session}`); if (pathParts === undefined) { setSessionId(new_session); const newPath = `/${new_session}`; window.history.replaceState({}, '', newPath); } else { const currentPath = pathParts.length < 2 ? '' : pathParts[0]; let tabIndex = tabs.findIndex((tab) => tab.path === currentPath); if (-1 === tabIndex) { console.log(`Invalid path "${currentPath}" -- redirecting to default`); window.history.replaceState({}, '', `/${new_session}`); setActiveTab(0); } else { window.history.replaceState({}, '', `/${pathParts.join('/')}/${new_session}`); // tabs[tabIndex].route = pathParts[2] || ""; setActiveTab(tabIndex); } setSessionId(new_session); } } catch (error: any) { console.error(error); setSnack("Server is temporarily down", "error"); } }), [setSnack, tabs]); useEffect(() => { const url = new URL(window.location.href); const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId] if (pathParts.length < 1) { console.log("No session id or path -- creating new session"); fetchSession(); } else { const currentPath = pathParts.length < 2 ? '' : pathParts[0]; const path_session = pathParts.length < 2 ? pathParts[0] : pathParts[1]; if (!isValidUUIDv4(path_session)) { console.log(`Invalid session id ${path_session}-- creating new session`); fetchSession(); } else { let tabIndex = tabs.findIndex((tab) => tab.path === currentPath); if (-1 === tabIndex) { console.log(`Invalid path "${currentPath}" -- redirecting to default`); tabIndex = 0; } // tabs[tabIndex].route = pathParts[2] || "" setSessionId(path_session); setActiveTab(tabIndex); } } }, [setSessionId, setSnack, tabs, fetchSession]); const handleMenuClose = () => { setIsMenuClosing(true); setMenuOpen(false); }; const handleMenuTransitionEnd = () => { setIsMenuClosing(false); }; const handleMenuToggle = () => { if (!isMenuClosing) { setMenuOpen(!menuOpen); } }; const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { if (newValue > tabs.length) { return; } setActiveTab(newValue); const tabPath = tabs[newValue].path; let path = `/${sessionId}`; if (tabPath) { // if (openDocument) { // path = `/${tabPath}/${openDocument}/${sessionId}`; // } else { path = `/${tabPath}/${sessionId}`; // } } window.history.pushState({}, '', path); handleMenuClose(); }; useEffect(() => { const handlePopState = () => { const url = new URL(window.location.href); const pathParts = url.pathname.split('/').filter(Boolean); const currentPath = pathParts.length < 2 ? '' : pathParts[0]; const session = pathParts.length < 2 ? pathParts[0] : pathParts[1]; let tabIndex = tabs.findIndex((tab) => tab.path === currentPath); if (-1 === tabIndex) { console.log(`Invalid path "${currentPath}" -- redirecting to default`); tabIndex = 0; } setSessionId(session); setActiveTab(tabIndex); }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, [setSessionId, tabs]); /* toolbar height is 64px + 8px margin-top */ const Offset = styled('div')(() => ({ minHeight: '72px', height: '72px' })); return ( theme.zIndex.drawer + 1, maxWidth: "100vw" }} > {!isDesktop && { setActiveTab(0); setMenuOpen(false); }} > BACKSTORY } {menuOpen === false && isDesktop && {tabs.map((tab, index) => )} } {tabs.map((tab, index) => )} { tabs.map((tab: any, i: number) => {tab.children} ) } ); }; export default App;