import React, { ReactElement, JSXElementConstructor, 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 { useTheme } from '@mui/material/styles'; import { SxProps } from '@mui/material'; import { ResumeBuilder } from './ResumeBuilder'; import { Message, ChatQuery, MessageList } from './Message'; import { Snack, SeverityType } from './Snack'; import { VectorVisualizer } from './VectorVisualizer'; import { Controls } from './Controls'; import { Conversation, ConversationHandle } from './Conversation'; import { Scrollable } from './AutoScroll'; import { BackstoryTab } from './BackstoryTab'; 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'; import MuiMarkdown from 'mui-markdown'; const getConnectionBase = (loc: any): string => { if (!loc.host.match(/.*battle-linux.*/)) { return loc.protocol + "//" + loc.host; } else { return loc.protocol + "//battle-linux.ketrenos.com:8912"; } } interface TabProps { label?: string, path: string, tabProps?: { label?: string, sx?: SxProps, icon?: string | ReactElement> | undefined, iconPosition?: "bottom" | "top" | "start" | "end" | undefined } }; const App = () => { const [sessionId, setSessionId] = useState(undefined); const [connectionBase,] = useState(getConnectionBase(window.location)) const [menuOpen, setMenuOpen] = useState(false); const [isMenuClosing, setIsMenuClosing] = useState(false); const [activeTab, setActiveTab] = useState(0); const [about, setAbout] = useState(""); const isDesktop = useMediaQuery('(min-width:650px)'); const prevIsDesktopRef = useRef(isDesktop); const chatRef = useRef(null); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const snackRef = useRef(null); 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]); // Get the About markdown useEffect(() => { if (about !== "") { return; } const fetchAbout = async () => { try { const response = await fetch("/docs/about.md", { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw Error("/docs/about.md not found"); } const data = await response.text(); setAbout(data); } catch (error: any) { console.error('Error obtaining About content information:', error); setAbout("No information provided."); }; }; fetchAbout(); }, [about, setAbout]) const handleSubmitChatQuery = (query: string) => { console.log(`handleSubmitChatQuery: ${query} -- `, chatRef.current ? ' sending' : 'no handler'); chatRef.current?.submitQuery(query); setActiveTab(0); }; const tabs: TabProps[] = useMemo(() => { const backstoryPreamble: MessageList = [ { role: 'content', title: 'Welcome to Backstory', content: ` 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 questions potential employers may have about his work history. What would you like to know about James? ` } ]; const backstoryQuestions = [ , As with all LLM interactions, the results may not be 100% accurate. If you have questions about my career, I'd love to hear from you. You can send me an email at **james_backstory@ketrenos.com**. ]; const tabSx = { flexGrow: 1, fontSize: '1rem' }; return [{ label: "", path: "", tabProps: { label: "Backstory", sx: tabSx, icon: , iconPosition: "start" }, children: ( ) }, { label: "Resume Builder", path: "resume-builder", children: ( ) }, { label: "Context Visualizer", path: "context-visualizer", children: }, { label: "About", path: "about", children: ( ) }, { path: "settings", tabProps: { sx: { flexShrink: 1, flexGrow: 0, fontSize: '1rem' }, icon: }, children: ( {sessionId !== undefined && } ) }]; }, [about, connectionBase, sessionId, setSnack, isMobile]); useEffect(() => { const url = new URL(window.location.href); const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId] const fetchSession = async () => { 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 data = await response.json(); setSessionId(data.id); const newPath = `/${data.id}`; window.history.replaceState({}, '', newPath); } catch (error: any) { console.error(error); setSnack("Server is temporarily down", "error"); } }; if (pathParts.length < 1) { console.log("No session id or path -- creating new session"); fetchSession(); } else { 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); } }, [setSessionId, connectionBase, setSnack, tabs]); 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; if (tabPath) { window.history.pushState({}, '', `/${tabPath}/${sessionId}`); } else { window.history.pushState({}, '', `/${sessionId}`); } 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;