From 5f184b4a1d7eb512ecab9f87800ea0cc954c245f Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Sat, 26 Apr 2025 16:06:51 -0700 Subject: [PATCH] Lots of UI fixes --- frontend/public/docs/about.md | 2 - frontend/src/App.tsx | 412 +++++++++++++++--------------- frontend/src/AutoScroll.tsx | 20 +- frontend/src/ChatBubble.tsx | 7 +- frontend/src/Conversation.tsx | 3 +- frontend/src/Message.tsx | 11 +- frontend/src/VectorVisualizer.tsx | 8 +- 7 files changed, 230 insertions(+), 233 deletions(-) diff --git a/frontend/public/docs/about.md b/frontend/public/docs/about.md index e5f5fb7..59dba18 100644 --- a/frontend/public/docs/about.md +++ b/frontend/public/docs/about.md @@ -1,5 +1,3 @@ -# About Backstory - The backstory about Backstory... ## Backstory is two things diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 993ee51..954349d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; +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'; @@ -15,6 +15,7 @@ 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'; @@ -24,6 +25,7 @@ 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'; @@ -43,34 +45,20 @@ const getConnectionBase = (loc: any): string => { } } - -interface TabPanelProps { - children?: React.ReactNode; - index: number; - tab: number; -} - -function CustomTabPanel(props: TabPanelProps) { - const { children, tab, index, ...other } = props; - - return ( -
- {children} -
- ); -} +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 [selectedPath, setSelectedPath] = useState(""); const [menuOpen, setMenuOpen] = useState(false); const [isMenuClosing, setIsMenuClosing] = useState(false); const [activeTab, setActiveTab] = useState(0); @@ -132,15 +120,154 @@ const App = () => { }; - // Extract the sessionId from the URL if present, otherwise - // request a sessionId from the server. - const validPaths = useMemo(() => ['chat', 'notes', 'tasks'], []); // allowed paths + 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 (pathOverride?: string) => { + const fetchSession = async () => { try { const response = await fetch(connectionBase + `/api/context`, { method: 'POST', @@ -155,30 +282,29 @@ const App = () => { const data = await response.json(); setSessionId(data.id); - const newPath = pathOverride || 'chat'; // default fallback - window.history.replaceState({}, '', `/${newPath}/${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 < 2) { + if (pathParts.length < 1) { console.log("No session id or path -- creating new session"); fetchSession(); } else { - const currentPath = pathParts[0]; - const session = pathParts[1]; - - if (!validPaths.includes(currentPath)) { + 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`); - fetchSession(); // or you could window.location.replace if you want - } else { - console.log(`Path: ${currentPath}, Session id: ${session}`); - setSessionId(session); - setSelectedPath(currentPath); + tabIndex = 0; } + setSessionId(session); + setActiveTab(tabIndex); } - }, [setSessionId, setSelectedPath, connectionBase, setSnack, validPaths]); + }, [setSessionId, connectionBase, setSnack, tabs]); const handleMenuClose = () => { setIsMenuClosing(true); @@ -196,159 +322,38 @@ const App = () => { }; 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(); }; - const handleTabSelect = (newPath: string) => { - if (!sessionId) return; // safety - setSelectedPath(newPath); - window.history.pushState({}, '', `/${newPath}/${sessionId}`); - }; - 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]; - if (pathParts.length >= 2) { - const path = pathParts[0]; - const session = pathParts[1]; - - if (validPaths.includes(path)) { - setSelectedPath(path); - setSessionId(session); - } + 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); - }, [setSelectedPath, setSessionId, validPaths]); - - const menuDrawer = ( - - - - } - iconPosition="start" /> - - - - } /> - - - ); - - const tabs = useMemo(() => { - const chatPreamble: 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 chatQuestions = [ - - - - - - , - - - 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**. - - - ]; - - return [ - - - , - , - - - , - - - - - , - - {sessionId !== undefined && - - } - - ]; - }, [about, connectionBase, sessionId, setSnack, isMobile]); - + }, [setSessionId, tabs]); /* toolbar height is 64px + 8px margin-top */ const Offset = styled('div')(() => ({ minHeight: '72px', height: '72px' })); @@ -406,34 +411,7 @@ const App = () => { allowScrollButtonsMobile onChange={handleTabChange} aria-label="Backstory navigation"> - - } - iconPosition="start" /> - - - - } /> + {tabs.map((tab, index) => )} } @@ -467,12 +445,24 @@ const App = () => { }} > - {menuDrawer} + + + {tabs.map((tab, index) => )} + + { tabs.map((tab: any, i: number) => - {tab} + {tab.children} ) } diff --git a/frontend/src/AutoScroll.tsx b/frontend/src/AutoScroll.tsx index d7e4c56..cdb097e 100644 --- a/frontend/src/AutoScroll.tsx +++ b/frontend/src/AutoScroll.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, RefObject } from 'react'; +import { useEffect, useRef, RefObject } from 'react'; import Box from '@mui/material/Box'; import { SxProps, Theme } from '@mui/material'; @@ -53,15 +53,15 @@ const useAutoScrollToBottom = ( behavior: smooth ? 'smooth' : 'auto' }); } else { - // console.log('Not scrolling', { - // isNearBottom, - // isUserScrollingUp, - // scrollHeight: container.scrollHeight, - // scrollTop: container.scrollTop, - // clientHeight: container.clientHeight, - // threshold, - // delta: container.scrollHeight - container.scrollTop - container.clientHeight - // }); + console.log('Not scrolling', { + isNearBottom, + isUserScrollingUp, + scrollHeight: container.scrollHeight, + scrollTop: container.scrollTop, + clientHeight: container.clientHeight, + threshold, + delta: container.scrollHeight - container.scrollTop - container.clientHeight + }); } }; diff --git a/frontend/src/ChatBubble.tsx b/frontend/src/ChatBubble.tsx index 6d0e98b..7f96e19 100644 --- a/frontend/src/ChatBubble.tsx +++ b/frontend/src/ChatBubble.tsx @@ -19,7 +19,8 @@ interface ChatBubbleProps { title?: string; } -function ChatBubble({ role, isFullWidth, children, sx, className, title }: ChatBubbleProps) { +function ChatBubble(props: ChatBubbleProps) { + const { role, isFullWidth, children, sx, className, title } = props; const theme = useTheme(); const defaultRadius = '16px'; @@ -122,7 +123,7 @@ function ChatBubble({ role, isFullWidth, children, sx, className, title }: ChatB } @@ -134,9 +135,7 @@ function ChatBubble({ role, isFullWidth, children, sx, className, title }: ChatB {children} - ); - } return ( diff --git a/frontend/src/Conversation.tsx b/frontend/src/Conversation.tsx index 36f3772..c6a318d 100644 --- a/frontend/src/Conversation.tsx +++ b/frontend/src/Conversation.tsx @@ -444,7 +444,8 @@ const Conversation = forwardRef(({ { diff --git a/frontend/src/Message.tsx b/frontend/src/Message.tsx index 8c92c95..f0a0524 100644 --- a/frontend/src/Message.tsx +++ b/frontend/src/Message.tsx @@ -18,6 +18,7 @@ import Collapse from '@mui/material/Collapse'; import Typography from '@mui/material/Typography'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { ExpandMore } from './ExpandMore'; +import { SxProps, Theme } from '@mui/material'; import { ChatBubble } from './ChatBubble'; import { StyledMarkdown } from './StyledMarkdown'; @@ -61,12 +62,14 @@ interface MessageMetaProps { type MessageList = MessageData[]; interface MessageProps { + sx?: SxProps, message?: MessageData, isFullWidth?: boolean, submitQuery?: (text: string) => void, sessionId?: string, connectionBase: string, setSnack: SetSnackType, + className?: string, }; interface ChatQueryInterface { @@ -216,7 +219,8 @@ const ChatQuery = ({ text, submitQuery }: ChatQueryInterface) => { ); } -const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, connectionBase }: MessageProps) => { +const Message = (props: MessageProps) => { + const { message, submitQuery, isFullWidth, sessionId, setSnack, connectionBase, sx, className } = props; const [expanded, setExpanded] = useState(false); const textFieldRef = useRef(null); @@ -237,7 +241,7 @@ const Message = ({ message, submitQuery, isFullWidth, sessionId, setSnack, conne return ( diff --git a/frontend/src/VectorVisualizer.tsx b/frontend/src/VectorVisualizer.tsx index 3fa185a..63b4972 100644 --- a/frontend/src/VectorVisualizer.tsx +++ b/frontend/src/VectorVisualizer.tsx @@ -9,6 +9,7 @@ import Button from '@mui/material/Button'; import SendIcon from '@mui/icons-material/Send'; import FormControlLabel from '@mui/material/FormControlLabel'; import Switch from '@mui/material/Switch'; +import { SxProps, Theme } from '@mui/material'; import { SetSnackType } from './Snack'; @@ -47,6 +48,7 @@ interface VectorVisualizerProps { setSnack: SetSnackType; inline?: boolean; rag?: any; + sx?: SxProps; } interface ChromaResult { @@ -98,7 +100,8 @@ const symbolMap: Record = { 'query': 'circle', }; -const VectorVisualizer: React.FC = ({ setSnack, rag, inline, connectionBase, sessionId }) => { +const VectorVisualizer: React.FC = (props: VectorVisualizerProps) => { + const { setSnack, rag, inline, connectionBase, sessionId, sx } = props; const [plotData, setPlotData] = useState(null); const [newQuery, setNewQuery] = useState(''); const [newQueryEmbedding, setNewQueryEmbedding] = useState(undefined); @@ -305,7 +308,8 @@ const VectorVisualizer: React.FC = ({ setSnack, rag, inli sx={{ display: 'flex', flexDirection: 'column', - flexGrow: 1 + flexGrow: 1, + ...sx }}> { !inline &&