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 &&