Compare commits

...

3 Commits

Author SHA1 Message Date
8dc77dd8f7 Really snazzy 2025-05-19 14:29:24 -07:00
142c2baac4 Looking snazzy 2025-05-19 14:19:05 -07:00
540d286d7a Made changes to allow DNS from backstory-beta 2025-05-19 12:24:09 -07:00
30 changed files with 1979 additions and 309 deletions

View File

@ -12,6 +12,7 @@ RUN apt-get update \
wget \ wget \
nano \ nano \
rsync \ rsync \
iputils-ping \
jq \ jq \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log} && rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}

View File

@ -78,6 +78,8 @@ services:
- 3000:3000 # REACT expo while developing frontend - 3000:3000 # REACT expo while developing frontend
volumes: volumes:
- ./frontend:/opt/backstory/frontend:rw # Live mount frontend src - ./frontend:/opt/backstory/frontend:rw # Live mount frontend src
networks:
- internal
ollama: ollama:
build: build:

View File

@ -7,6 +7,13 @@ module.exports = {
// cert: '/path/to/cert.pem', // cert: '/path/to/cert.pem',
// key: '/path/to/key.pem', // key: '/path/to/key.pem',
// } // }
},
proxy: {
'/api': {
target: 'https://backstory:8911', // Replace with your FastAPI server URL
changeOrigin: true,
secure: false, // Set to true if your FastAPI server uses HTTPS
},
} }
}, },
webpack: { webpack: {

View File

@ -41,7 +41,7 @@
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "HTTPS=true craco start", "start": "HTTPS=true WDS_SOCKET_HOST=backstory-beta.ketrenos.com WDS_SOCKET_PORT=443 craco start",
"build": "craco build", "build": "craco build",
"test": "craco test" "test": "craco test"
}, },

View File

@ -4,12 +4,12 @@ First, what works:
1. There are two personas populated: 1. There are two personas populated:
1. One is me [jketreno](/u/jketreno) 1. One is me [jketreno](/u/jketreno)
2. The other is a ficticious AI generated persona named [Eliz](/u/eliza). 2. The other is a ficticious AI generated persona named [Eliza](/u/eliza).
2. **Candidate Skill Chat** You can go to the Chat tab to ask questions about the active candaite. 2. **Chat** You can go to the Chat tab to ask questions about the active candaite.
3. **Resume Builder** You can build a resume for a person given a Job Description 3. **Resume Builder** You can build a resume for a person given a Job Description
What doesn't work: What doesn't work:
1. User login, registration, etc. 1. User login, registration, etc.
2. Lots of the links on the site. 2. Lots of the links on the site.
3. Anything that isn't "Chat", "Resume Builder", or "About". 3. Basically.. anything that isn't "Chat", "Resume Builder", or "About".

BIN
frontend/public/profile.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@ -1,43 +1,6 @@
import React, { useRef, useCallback } from 'react'; import React from 'react';
import { BrowserRouter as Router, Routes, Route, useLocation } from "react-router-dom"; import { BrowserRouter as Router } from "react-router-dom";
import { SessionWrapper } from "./App/SessionWrapper";
import { Main } from "./App/Main";
import { BackstoryApp } from './NewApp/BackstoryApp'; import { BackstoryApp } from './NewApp/BackstoryApp';
import { Snack, SeverityType } from './Components/Snack';
const PathRouter = ({ setSnack }: { setSnack: any }) => {
const location = useLocation();
const segments = location.pathname.split("/").filter(Boolean);
const sessionId = segments[segments.length - 1];
return (
<SessionWrapper setSnack={setSnack}>
<Main setSnack={setSnack} sessionId={sessionId} />
</SessionWrapper>
);
}
function App2() {
const snackRef = useRef<any>(null);
const setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity);
}, [snackRef]);
return (
<>
<Router>
<Routes>
<Route path="*" element={<PathRouter setSnack={setSnack} />} />
</Routes>
</Router>
<Snack
ref={snackRef}
/>
</>
);
}
const App = () => { const App = () => {
return ( return (

View File

@ -24,7 +24,9 @@ const Scrollable = (props: ScrollableProps) => {
className={`Scrollable ${className || ""}`} className={`Scrollable ${className || ""}`}
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column',
margin: '0 auto', margin: '0 auto',
p: 0,
flexGrow: 1, flexGrow: 1,
overflow: 'auto', overflow: 'auto',
// backgroundColor: '#F5F5F5', // backgroundColor: '#F5F5F5',

View File

@ -69,7 +69,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
return <pre><code className="JsonRaw">{content}</code></pre> return <pre><code className="JsonRaw">{content}</code></pre>
}; };
} }
return <pre><code className={className}>{element.children}</code></pre>; return <pre><code className={className || ''}>{element.children}</code></pre>;
}, },
}, },
a: { a: {
@ -81,7 +81,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
if (href) { if (href) {
if (href.match(/^\//)) { if (href.match(/^\//)) {
event.preventDefault(); event.preventDefault();
window.history.replaceState({}, '', `${href}/${sessionId}`); window.history.replaceState({}, '', `${href}`);
} }
} }
}, },

View File

@ -3,11 +3,7 @@
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
flex-direction: column; flex-direction: column;
background-color: #F5F5F5;
border: 1px solid #E0E0E0;
font-size: 0.9rem; font-size: 0.9rem;
margin: 0 auto;
padding: 10px;
position: relative; position: relative;
width: 100%; max-width: 100%;
} }

View File

@ -21,6 +21,7 @@ import { connectionBase } from '../Global';
import './VectorVisualizer.css'; import './VectorVisualizer.css';
import { BackstoryPageProps } from './BackstoryTab'; import { BackstoryPageProps } from './BackstoryTab';
import { relative } from 'path';
interface VectorVisualizerProps extends BackstoryPageProps { interface VectorVisualizerProps extends BackstoryPageProps {
inline?: boolean; inline?: boolean;
@ -478,19 +479,13 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
...sx ...sx
}}> }}>
<Box sx={{ p: 0, m: 0, gap: 0 }}> <Box sx={{ p: 0, m: 0, gap: 0 }}>
{
!inline &&
<Paper sx={{ display: 'flex', flexDirection: 'column', flexGrow: 0, minHeight: '2.5rem', maxHeight: '2.5rem', height: '2.5rem', justifyContent: 'center', alignItems: 'center', m: 0, p: 0, mb: 1 }}>
RAG Vector Visualization
</Paper>
}
<Paper sx={{ <Paper sx={{
p: 0.5, m: 0, p: 0.5, m: 0,
display: "flex", display: "flex",
flexGrow: 0, flexGrow: 0,
height: isMobile ? "auto" : "320px", height: isMobile ? "auto" : "auto", //"320px",
minHeight: isMobile ? "auto" : "320px", minHeight: isMobile ? "auto" : "auto", //"320px",
maxHeight: isMobile ? "auto" : "320px", maxHeight: isMobile ? "auto" : "auto", //"320px",
position: "relative", position: "relative",
flexDirection: "column" flexDirection: "column"
}}> }}>
@ -528,20 +523,20 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
<Paper sx={{ display: "flex", flexDirection: isMobile ? "column" : "row", mt: 0.5, p: 0.5, flexGrow: 1, minHeight: "fit-content" }}> <Paper sx={{ display: "flex", flexDirection: isMobile ? "column" : "row", mt: 0.5, p: 0.5, flexGrow: 1, minHeight: "fit-content" }}>
{node !== null && {node !== null &&
<Box sx={{ display: "flex", fontSize: "0.75rem", flexDirection: "column", minWidth: "25rem", flexBasis: 0, maxHeight: "min-content" }}> <Box sx={{ display: "flex", fontSize: "0.75rem", flexDirection: "column", flexGrow: 1, maxWidth: "100%", flexBasis: 1, maxHeight: "min-content" }}>
<TableContainer component={Paper} sx={{ mb: isMobile ? 1 : 0, mr: isMobile ? 0 : 1 }}> <TableContainer component={Paper} sx={{ mb: isMobile ? 1 : 0, mr: isMobile ? 0 : 1 }}>
<Table size="small" sx={{ tableLayout: 'fixed' }}> <Table size="small" sx={{ tableLayout: 'fixed' }}>
<TableBody sx={{ '& td': { verticalAlign: "top", fontSize: "0.75rem", }, '& td:first-of-type': { whiteSpace: "nowrap", width: "5rem" } }}> <TableBody sx={{ '& td': { verticalAlign: "top", fontSize: "0.75rem", }, '& td:first-of-type': { whiteSpace: "nowrap", width: "1rem" } }}>
<TableRow> <TableRow>
<TableCell>Type</TableCell> <TableCell>Type</TableCell>
<TableCell>{node.emoji} {node.doc_type}</TableCell> <TableCell>{node.emoji} {node.doc_type}</TableCell>
</TableRow> </TableRow>
{node.source_file !== undefined && <TableRow> {node.source_file !== undefined && <TableRow>
<TableCell>File</TableCell> <TableCell>File</TableCell>
<TableCell>{node.source_file.replace(/^.*\//, '')}, lines: {node.line_begin}-{node.line_end}</TableCell> <TableCell>{node.source_file.replace(/^.*\//, '')}</TableCell>
</TableRow>} </TableRow>}
{node.path !== undefined && <TableRow> {node.path !== undefined && <TableRow>
<TableCell>Location</TableCell> <TableCell>Section</TableCell>
<TableCell>{node.path}</TableCell> <TableCell>{node.path}</TableCell>
</TableRow>} </TableRow>}
{node.distance !== undefined && <TableRow> {node.distance !== undefined && <TableRow>
@ -560,7 +555,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
</Box> </Box>
} }
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 1 }}> <Box sx={{ display: "flex", flexDirection: "column", flexGrow: 2, flexBasis: 0, flexShrink: 1 }}>
{node === null && {node === null &&
<Paper sx={{ m: 0.5, p: 2, flexGrow: 1 }}> <Paper sx={{ m: 0.5, p: 2, flexGrow: 1 }}>
Click a point in the scatter-graph to see information about that node. Click a point in the scatter-graph to see information about that node.
@ -576,6 +571,8 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
p: 0.5, p: 0.5,
pl: 1, pl: 1,
flexShrink: 1, flexShrink: 1,
position: "relative",
maxWidth: "100%",
}} }}
> >
{ {
@ -584,7 +581,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
const bgColor = (index > node.line_begin && index <= node.line_end) ? '#f0f0f0' : 'auto'; const bgColor = (index > node.line_begin && index <= node.line_end) ? '#f0f0f0' : 'auto';
return <Box key={index} sx={{ display: "flex", flexDirection: "row", borderBottom: '1px solid #d0d0d0', ':first-of-type': { borderTop: '1px solid #d0d0d0' }, backgroundColor: bgColor }}> return <Box key={index} sx={{ display: "flex", flexDirection: "row", borderBottom: '1px solid #d0d0d0', ':first-of-type': { borderTop: '1px solid #d0d0d0' }, backgroundColor: bgColor }}>
<Box sx={{ fontFamily: 'courier', fontSize: "0.8rem", minWidth: "2rem", pt: "0.1rem", align: "left", verticalAlign: "top" }}>{index}</Box> <Box sx={{ fontFamily: 'courier', fontSize: "0.8rem", minWidth: "2rem", pt: "0.1rem", align: "left", verticalAlign: "top" }}>{index}</Box>
<pre style={{ margin: 0, padding: 0, border: "none", minHeight: "1rem" }} >{line || " "}</pre> <pre style={{ margin: 0, padding: 0, border: "none", minHeight: "1rem", overflow: "hidden" }} >{line || " "}</pre>
</Box>; </Box>;
}) })
} }

View File

@ -0,0 +1,257 @@
div {
box-sizing: border-box;
overflow-wrap: break-word;
word-break: break-word;
}
.gl-container #scene {
top: 0px !important;
left: 0px !important;
}
pre {
max-width: 100%;
max-height: 100%;
overflow: auto;
white-space: pre-wrap;
box-sizing: border-box;
border: 3px solid #E0E0E0;
}
button {
overflow-wrap: initial;
word-break: initial;
}
.TabPanel {
display: flex;
height: 100%;
}
.MuiToolbar-root .MuiBox-root {
border-bottom: none;
}
.MuiTabs-root .MuiTabs-indicator {
background-color: orange;
}
.SystemInfo {
display: flex;
flex-direction: column;
gap: 5px;
padding: 5px;
flex-grow: 1;
}
.SystemInfoItem {
display: flex; /* Grid for individual items */
flex-direction: row;
flex-grow: 1;
}
.SystemInfoItem > div:first-child {
display: flex;
justify-self: end; /* Align the first column content to the right */
width: 10rem;
}
.SystemInfoItem > div:last-child {
display: flex;
flex-grow: 1;
justify-self: end; /* Align the first column content to the right */
}
.DocBox {
display: flex;
flex-direction: column;
flex-grow: 1;
max-width: 2048px;
margin: 0 auto;
}
.Controls {
display: flex;
background-color: #F5F5F5;
border: 1px solid #E0E0E0;
overflow-y: auto;
padding: 10px;
flex-direction: column;
margin-left: 10px;
box-sizing: border-box;
overflow-x: visible;
min-width: 10rem;
flex-grow: 1;
}
.MessageContent div > p:first-child {
margin-top: 0;
}
.MenuCard.MuiCard-root {
display: flex;
flex-direction: column;
min-width: 10rem;
flex-grow: 1;
background-color: #1A2536; /* Midnight Blue */
color: #D3CDBF; /* Warm Gray */
border-radius: 0;
}
.MenuCard.MuiCard-root button {
min-height: 64px;
}
/* Prevent toolbar from shrinking vertically when media < 600px */
.MuiToolbar-root {
min-height: 72px !important;
padding-left: 16px !important;
padding-right: 16px !important;
}
.ChatBox {
display: flex;
flex-direction: column;
flex-grow: 1;
max-width: 1024px;
width: 100%;
margin: 0 auto;
background-color: #D3CDBF;
}
.user-message.MuiCard-root {
background-color: #DCF8C6;
border: 1px solid #B2E0A7;
color: #333333;
margin-bottom: 0.75rem;
margin-left: 1rem;
border-radius: 0.25rem;
min-width: 80%;
max-width: 80%;
justify-self: right;
display: flex;
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
flex-direction: column;
align-items: self-end;
align-self: end;
flex-grow: 0;
}
.About.MuiCard-root,
.assistant-message.MuiCard-root {
border: 1px solid #E0E0E0;
background-color: #FFFFFF;
color: #333333;
margin-bottom: 0.75rem;
margin-right: 1rem;
min-width: 70%;
border-radius: 0.25rem;
justify-self: left;
display: flex;
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
flex-direction: column;
flex-grow: 0;
padding: 16px 0;
font-size: 0.9rem;
}
.About.MuiCard-root {
display: flex;
flex-grow: 1;
width: 100%;
margin-left: 0;
margin-right: 0;
}
.About .MuiCardContent-root,
.assistant-message .MuiCardContent-root {
padding: 0 16px !important;
font-size: 0.9rem;
}
.About span,
.assistant-message span {
font-size: 0.9rem;
}
.user-message .MuiCardContent-root:last-child,
.assistant-message .MuiCardContent-root:last-child,
.About .MuiCardContent-root:last-child {
padding: 16px;
}
.users > div {
padding: 0.25rem;
}
.user-active {
font-weight: bold;
}
.metadata {
border: 1px solid #E0E0E0;
font-size: 0.75rem;
padding: 0.125rem;
}
/* Reduce general whitespace in markdown content */
* p.MuiTypography-root {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
/* Reduce space between headings and content */
* h1.MuiTypography-root,
* h2.MuiTypography-root,
* h3.MuiTypography-root,
* h4.MuiTypography-root,
* h5.MuiTypography-root,
* h6.MuiTypography-root {
margin-top: 1rem;
margin-bottom: 0.5rem;
font-size: 1rem;
}
/* Reduce space in lists */
* ul.MuiTypography-root,
* ol.MuiTypography-root {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
* li.MuiTypography-root {
margin-bottom: 0.25rem;
font-size: 0.9rem;
}
* .MuiTypography-root li {
margin-top: 0;
margin-bottom: 0;
padding: 0;
font-size: 0.9rem;
}
/* Reduce space around code blocks */
* .MuiTypography-root pre {
border: 1px solid #F5F5F5;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
margin-top: 0;
margin-bottom: 0;
font-size: 0.9rem;
}
.PromptStats .MuiTableCell-root {
font-size: 0.8rem;
}
#SystemPromptInput {
font-size: 0.9rem;
line-height: 1.25rem;
}

View File

@ -17,24 +17,32 @@ import BusinessIcon from '@mui/icons-material/Business';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import { ThemeProvider } from '@mui/material/styles'; import { ThemeProvider } from '@mui/material/styles';
import { backstoryTheme } from '../BackstoryTheme';
import {Header} from './Components/Header'; import {Header} from './Components/Header';
import { Scrollable } from '../Components/Scrollable'; import { Scrollable } from '../Components/Scrollable';
import { Footer } from './Components/Footer'; import { Footer } from './Components/Footer';
import { HomePage } from './Pages/HomePage';
// import { BackstoryThemeVisualizer } from './BackstoryThemeVisualizer';
import { HomePage as ChatPage } from '../Pages/HomePage';
import { ResumeBuilderPage } from '../Pages/ResumeBuilderPage';
import { Snack, SeverityType } from '../Components/Snack'; import { Snack, SeverityType } from '../Components/Snack';
import { Query } from '../Components/ChatQuery'; import { Query } from '../Components/ChatQuery';
import { ConversationHandle } from '../Components/Conversation'; import { ConversationHandle } from '../Components/Conversation';
import { AboutPage } from './Pages/AboutPage';
import { backstoryTheme } from '../BackstoryTheme';
import { HomePage } from './Pages/HomePage';
import { ChatPage } from './Pages/ChatPage';
import { ResumeBuilderPage } from '../Pages/ResumeBuilderPage';
// import { BackstoryThemeVisualizer } from './BackstoryThemeVisualizer';
import { AboutPage } from './Pages/AboutPage';
import { BetaPage } from './Pages/BetaPage';
import { CreateProfilePage } from './Pages/CreateProfilePage';
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
import './BackstoryApp.css';
import '@fontsource/roboto/300.css'; import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css'; import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css'; import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
import { connectionBase } from '../Global';
type NavigationLinkType = { type NavigationLinkType = {
name: string; name: string;
@ -133,23 +141,34 @@ interface BackstoryPageContainerProps {
const BackstoryPageContainer = (props : BackstoryPageContainerProps) => { const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
const { children, sx } = props; const { children, sx } = props;
return ( return (
<Container maxWidth="xl" sx={{ mt: 2, mb: 2, height: "calc(1024px - 72px)", ...sx }}> <Container maxWidth="xl" sx={{ mt: 2, mb: 2, ...sx }}>
<Paper <Paper
elevation={2} elevation={2}
sx={{ sx={{
p: 3, p: 3,
backgroundColor: 'background.paper', backgroundColor: 'background.paper',
borderRadius: 2, borderRadius: 2,
minHeight: '80vh', minHeight: '80vh',
}} }}>
> {children}
<Scrollable> </Paper>
{children} </Container>
</Scrollable>
</Paper>
</Container>
); );
} }
// Cookie handling functions
const getCookie = (name: string) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift();
return null;
};
const setCookie = (name: string, value: string, days = 7) => {
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Strict`;
};
const BackstoryApp = () => { const BackstoryApp = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -158,6 +177,7 @@ const BackstoryApp = () => {
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]); const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
const snackRef = useRef<any>(null); const snackRef = useRef<any>(null);
const chatRef = useRef<ConversationHandle>(null); const chatRef = useRef<ConversationHandle>(null);
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
const setSnack = useCallback((message: string, severity?: SeverityType) => { const setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity); snackRef.current?.setSnack(message, severity);
}, [snackRef]); }, [snackRef]);
@ -167,6 +187,79 @@ const BackstoryApp = () => {
navigate('/chat'); navigate('/chat');
}; };
const [page, setPage] = useState<string>(""); const [page, setPage] = useState<string>("");
const [storeInCookie, setStoreInCookie] = useState(true);
// Extract session ID from URL query parameter or cookie
const urlParams = new URLSearchParams(window.location.search);
const urlSessionId = urlParams.get('id');
const cookieSessionId = getCookie('session_id');
// Fetch or join session on mount
useEffect(() => {
const fetchSession = async () => {
try {
let response;
let newSessionId;
if (urlSessionId) {
// Attempt to join session from URL
response = await fetch(`${connectionBase}/join-session/${urlSessionId}`, {
credentials: 'include',
});
if (!response.ok) {
throw new Error('Session not found');
}
newSessionId = (await response.json()).id;
} else if (cookieSessionId) {
// Attempt to join session from cookie
response = await fetch(`${connectionBase}/api/join-session/${cookieSessionId}`, {
credentials: 'include',
});
if (!response.ok) {
// Cookie session invalid, create new session
response = await fetch(`${connectionBase}/api/create-session`, {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to create session');
}
}
newSessionId = (await response.json()).id;
} else {
// Create a new session
response = await fetch(`${connectionBase}/api/create-session`, {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to create session');
}
newSessionId = (await response.json()).id;
}
setSessionId(newSessionId);
// Store in cookie if user opts in
if (storeInCookie) {
setCookie('session_id', newSessionId);
}
// Update URL without reloading
if (!storeInCookie || (urlSessionId && urlSessionId !== newSessionId)) {
window.history.replaceState(null, '', `?id=${newSessionId}`);
}
} catch (err) {
setSnack("" + err);
}
};
fetchSession();
}, []);
const copyLink = () => {
const link = `${window.location.origin}${window.location.pathname}?id=${sessionId}`;
navigator.clipboard.writeText(link).then(() => {
alert('Link copied to clipboard!');
}).catch(() => {
alert('Failed to copy link');
});
};
useEffect(() => { useEffect(() => {
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/"; const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
@ -187,45 +280,49 @@ const BackstoryApp = () => {
flexDirection: 'column', flexDirection: 'column',
backgroundColor: 'background.default', backgroundColor: 'background.default',
maxHeight: "calc(100vh - 72px)", maxHeight: "calc(100vh - 72px)",
minHeight: "calc(100vh - 72px)",
}}> }}>
<BackstoryPageContainer userContext={userContext}> <BackstoryPageContainer userContext={userContext}>
<Routes> {sessionId !== undefined &&
<Route path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} /> <Routes>
<Route path="/about" element={<AboutPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} /> <Route path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/about/:subPage" element={<AboutPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} /> <Route path="/about" element={<AboutPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} /> <Route path="/about/:subPage" element={<AboutPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/rag-visualizer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery} />} /> <Route path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/dashboard" element={<DashboardPage />} /> <Route path="/rag-visualizer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/" element={<HomePage/>}/> <Route path="/create-your-profile" element={<CreateProfilePage />} />
{/* Candidate-specific routes */} <Route path="/dashboard" element={<DashboardPage />} />
{user.type === 'candidate' && ( <Route path="/" element={<HomePage />} />
<> {/* Candidate-specific routes */}
<Route path="/profile" element={<ProfilePage />} /> {user.type === 'candidate' && (
<Route path="/backstory" element={<BackstoryPage />} /> <>
<Route path="/resumes" element={<ResumesPage />} /> <Route path="/profile" element={<ProfilePage />} />
<Route path="/qa-setup" element={<QASetupPage />} /> <Route path="/backstory" element={<BackstoryPage />} />
</> <Route path="/resumes" element={<ResumesPage />} />
)} <Route path="/qa-setup" element={<QASetupPage />} />
</>
{/* Employer-specific routes */} )}
{user.type === 'employer' && (
<> {/* Employer-specific routes */}
<Route path="/search" element={<SearchPage />} /> {user.type === 'employer' && (
<Route path="/saved" element={<SavedPage />} /> <>
<Route path="/jobs" element={<JobsPage />} /> <Route path="/search" element={<SearchPage />} />
<Route path="/company" element={<CompanyPage />} /> <Route path="/saved" element={<SavedPage />} />
</> <Route path="/jobs" element={<JobsPage />} />
)} <Route path="/company" element={<CompanyPage />} />
</>
{/* Common routes */} )}
<Route path="/analytics" element={<AnalyticsPage />} />
<Route path="/settings" element={<SettingsPage />} /> {/* Common routes */}
<Route path="/analytics" element={<AnalyticsPage />} />
{/* Redirect to dashboard by default */} <Route path="/settings" element={<SettingsPage />} />
<Route path="*" element={<HomePage />} />
</Routes> {/* Redirect to BETA by default */}
<Route path="*" element={<BetaPage />} />
</Routes>
}
{location.pathname === "/" && <Footer />}
</BackstoryPageContainer> </BackstoryPageContainer>
{ location.pathname === "/" && <Footer /> }
</Scrollable> </Scrollable>
<Snack ref={snackRef}/> <Snack ref={snackRef}/>
</ThemeProvider> </ThemeProvider>

View File

@ -0,0 +1,87 @@
import React from 'react';
import { Box, Typography, Avatar, Paper, Grid, Chip } from '@mui/material';
import { styled } from '@mui/material/styles';
import { Tunables } from '../../Components/ChatQuery';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
// Define the UserInfo interface for type safety
interface UserInfo {
profile_url: string;
description: string;
rag_content_size: number;
user_name: string;
first_name: string;
last_name: string;
full_name: string;
contact_info: Record<string, string>;
questions: [{
question: string;
tunables?: Tunables
}]
};
// Define props interface for the component
interface CandidateInfoProps {
userInfo: UserInfo;
}
// Styled components
const StyledPaper = styled(Paper)(({ theme }) => ({
padding: theme.spacing(2),
marginBottom: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
}));
const CandidateInfo: React.FC<CandidateInfoProps> = ({ userInfo }) => {
const navigate = useNavigate();
// Format RAG content size (e.g., if it's in bytes, convert to KB/MB)
const formatRagSize = (size: number): string => {
if (size < 1000) return `${size} RAG elements`;
if (size < 1000000) return `${(size / 1000).toFixed(1)}K RAG elements`;
return `${(size / 1000000).toFixed(1)}M RAG elements`;
};
return (
<StyledPaper>
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 2 }} sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Avatar
src={userInfo.profile_url}
alt={`${userInfo.full_name}'s profile`}
sx={{
width: 80,
height: 80,
border: '2px solid #e0e0e0',
}}
/>
</Grid>
<Grid size={{ xs: 12, sm: 10 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
<Typography variant="h5" component="h1" sx={{ fontWeight: 'bold' }}>
{userInfo.full_name}
</Typography>
<Chip
onClick={() => navigate('/rag-visualizer')}
label={formatRagSize(userInfo.rag_content_size)}
color="primary"
size="small"
sx={{ ml: 2 }}
/>
</Box>
<Typography variant="body1" color="text.secondary">
{userInfo.description}
</Typography>
</Grid>
</Grid>
</StyledPaper>
);
};
export type {
UserInfo
};
export { CandidateInfo };

View File

@ -0,0 +1,57 @@
import React, { useState, useEffect } from 'react';
import { Box, Typography } from '@mui/material';
import { Message } from '../../Components/Message';
import { ChatBubble } from '../../Components/ChatBubble';
import { BackstoryElementProps } from '../../Components/BackstoryTab';
import { StyledMarkdown } from '../../Components/StyledMarkdown';
interface DocumentProps extends BackstoryElementProps {
filepath?: string;
}
const Document = (props: DocumentProps) => {
const { sessionId, setSnack, submitQuery, filepath } = props;
const backstoryProps = {
submitQuery,
setSnack,
sessionId
};
const [document, setDocument] = useState<string>("");
// Get the markdown
useEffect(() => {
if (!filepath) {
return;
}
const fetchDocument = async () => {
try {
const response = await fetch(filepath, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw Error(`${filepath} not found.`);
}
const data = await response.text();
setDocument(data);
} catch (error: any) {
console.error('Error obtaining About content information:', error);
setDocument(`${filepath} not found.`);
};
};
fetchDocument();
}, [document, setDocument, filepath])
return (<>
<StyledMarkdown {...backstoryProps} content={document}/>
</>);
};
export {
Document
};

View File

@ -0,0 +1,68 @@
import React from 'react';
import { Box, CircularProgress, Typography, Grid, LinearProgress, Fade } from '@mui/material';
import { styled } from '@mui/material/styles';
// Types for props
interface LoadingComponentProps {
/** Text to display while loading */
loadingText?: string;
/** Type of loader to show */
loaderType?: 'circular' | 'linear';
/** Whether to show with fade-in animation */
withFade?: boolean;
/** Duration of fade-in animation in ms */
fadeDuration?: number;
}
// Styled components
const LoadingContainer = styled(Box)(({ theme }) => ({
width: '100%',
padding: theme.spacing(3),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}));
/**
* A loading component to display at the top of pages while content is loading
*/
const LoadingComponent: React.FC<LoadingComponentProps> = ({
loadingText = 'Loading content...',
loaderType = 'circular',
withFade = true,
fadeDuration = 800,
}) => {
const content = (
<LoadingContainer>
<Grid container spacing={2}>
<Grid size={{ xs: 12 }} sx={{ textAlign: 'center', mb: 2 }}>
{loaderType === 'circular' ? (
<CircularProgress color="primary" />
) : (
<Box sx={{ width: '100%', maxWidth: 400 }}>
<LinearProgress color="primary" />
</Box>
)}
</Grid>
<Grid size={{ xs: 12 }} sx={{ textAlign: 'center' }}>
<Typography variant="body1" color="textSecondary">
{loadingText}
</Typography>
</Grid>
</Grid>
</LoadingContainer>
);
// Return with or without fade animation
return withFade ? (
<Fade in={true} timeout={fadeDuration}>
{content}
</Fade>
) : (
content
);
};
export { LoadingComponent};

View File

@ -1,24 +1,167 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import {useNavigate, useLocation, useParams } from 'react-router-dom'; import { useNavigate, useLocation, useParams } from 'react-router-dom';
import {
import { Box } from '@mui/material'; Box,
Drawer,
AppBar,
Toolbar,
IconButton,
Typography,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Paper,
Grid,
Card,
CardContent,
CardActionArea,
Divider,
useTheme,
useMediaQuery
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import CloseIcon from '@mui/icons-material/Close';
import DescriptionIcon from '@mui/icons-material/Description';
import CodeIcon from '@mui/icons-material/Code';
import LayersIcon from '@mui/icons-material/Layers';
import DashboardIcon from '@mui/icons-material/Dashboard';
import PaletteIcon from '@mui/icons-material/Palette';
import AnalyticsIcon from '@mui/icons-material/Analytics';
import ViewQuiltIcon from '@mui/icons-material/ViewQuilt';
import { Document } from '../Components/Document';
import { BackstoryPageProps } from '../../Components/BackstoryTab'; import { BackstoryPageProps } from '../../Components/BackstoryTab';
import { Document } from '../../Components/Document'; import { BackstoryUIOverviewPage } from './BackstoryUIOverviewPage';
import { BackstoryUIOverviewPage} from './BackstoryUIOverviewPage';
import { BackstoryAppAnalysisPage } from './BackstoryAppAnalysisPage'; import { BackstoryAppAnalysisPage } from './BackstoryAppAnalysisPage';
import { BackstoryThemeVisualizerPage } from './BackstoryThemeVisualizerPage'; import { BackstoryThemeVisualizerPage } from './BackstoryThemeVisualizerPage';
import { MockupPage } from './MockupPage'; import { MockupPage } from './MockupPage';
// Get appropriate icon for document type
const getDocumentIcon = (title: string) => {
switch (title) {
case 'About':
return <DescriptionIcon />;
case 'BETA':
return <CodeIcon />;
case 'Resume Generation Architecture':
case 'Application Architecture':
return <LayersIcon />;
case 'UI Overview':
case 'UI Mockup':
return <DashboardIcon />;
case 'Theme Visualizer':
return <PaletteIcon />;
case 'App Analysis':
return <AnalyticsIcon />;
default:
return <ViewQuiltIcon />;
}
};
// Sidebar navigation component using MUI components
const Sidebar: React.FC<{
currentPage: string;
onDocumentSelect: (docName: string, open: boolean) => void;
onClose?: () => void;
isMobile: boolean;
}> = ({ currentPage, onDocumentSelect, onClose, isMobile }) => {
// Document definitions
const documents = [
{ title: "About", route: "about" },
{ title: "BETA", route: "beta" },
{ title: "Resume Generation Architecture", route: "resume-generation" },
{ title: "Application Architecture", route: "about-app" },
{ title: "UI Overview", route: "ui-overview" },
{ title: "Theme Visualizer", route: "theme-visualizer" },
{ title: "App Analysis", route: "app-analysis" },
{ title: "UI Mockup", route: "ui-mockup" }
];
const handleItemClick = (route: string) => {
onDocumentSelect(route, true);
if (isMobile && onClose) {
onClose();
}
};
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{
p: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: 1,
borderColor: 'divider'
}}>
<Typography variant="h6" component="h2" fontWeight="bold">
Documentation
</Typography>
{isMobile && onClose && (
<IconButton
onClick={onClose}
size="small"
aria-label="Close navigation"
>
<CloseIcon />
</IconButton>
)}
</Box>
<Box sx={{
flexGrow: 1,
overflow: 'auto',
p: 1
}}>
<List>
{documents.map((doc, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => handleItemClick(doc.route)}
selected={currentPage === doc.route}
sx={{
borderRadius: 1,
mb: 0.5
}}
>
<ListItemIcon sx={{
color: currentPage === doc.route ? 'primary.main' : 'text.secondary',
minWidth: 40
}}>
{getDocumentIcon(doc.title)}
</ListItemIcon>
<ListItemText
primary={doc.title}
slotProps={{
primary: {
fontWeight: currentPage === doc.route ? 'medium' : 'regular',
}
}}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Box>
</Box>
);
};
// AboutPage main component
const AboutPage = (props: BackstoryPageProps) => { const AboutPage = (props: BackstoryPageProps) => {
const { sessionId, submitQuery, setSnack } = props; const { sessionId, submitQuery, setSnack } = props;
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { paramPage = '' } = useParams(); const { paramPage = '' } = useParams();
const [page, setPage] = useState<string>(paramPage); const [page, setPage] = useState<string>(paramPage);
const [drawerOpen, setDrawerOpen] = useState(false);
/* If the location changes, set the page based on the const theme = useTheme();
* second part of the path, or clear if no path */ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Track location changes
useEffect(() => { useEffect(() => {
const parts = location.pathname.split('/'); const parts = location.pathname.split('/');
if (parts.length > 2) { if (parts.length > 2) {
@ -28,6 +171,14 @@ const AboutPage = (props: BackstoryPageProps) => {
} }
}, [location]); }, [location]);
// Close drawer when changing to desktop view
useEffect(() => {
if (!isMobile) {
setDrawerOpen(false);
}
}, [isMobile]);
// Handle document navigation
const onDocumentExpand = (docName: string, open: boolean) => { const onDocumentExpand = (docName: string, open: boolean) => {
console.log("Document expanded:", { docName, open, location }); console.log("Document expanded:", { docName, open, location });
if (open) { if (open) {
@ -42,84 +193,230 @@ const AboutPage = (props: BackstoryPageProps) => {
const basePath = location.pathname.split('/').slice(0, -1).join('/'); const basePath = location.pathname.split('/').slice(0, -1).join('/');
navigate(`${basePath}`); navigate(`${basePath}`);
} }
};
// Toggle mobile drawer
const toggleDrawer = () => {
setDrawerOpen(!drawerOpen);
};
// Close the drawer
const closeDrawer = () => {
setDrawerOpen(false);
};
// Helper function to get document title from route
function documentTitleFromRoute(route: string): string {
const titles: Record<string, string> = {
'about': 'About',
'beta': 'BETA',
'resume-generation': 'Resume Generation Architecture',
'about-app': 'Application Architecture',
'ui-overview': 'UI Overview',
'theme-visualizer': 'Theme Visualizer',
'app-analysis': 'App Analysis',
'ui-mockup': 'UI Mockup'
};
return titles[route] || 'Documentation';
} }
return (<Box sx={{gap: 1}}> interface DocViewProps {
<Document {...{ page: string
title: "About", };
filepath: "/docs/about.md", const DocView = (props: DocViewProps) => {
onExpand: (open: boolean) => { onDocumentExpand('about', open); }, const { page } = props;
expanded: page === 'about', const title = documentTitleFromRoute(page);
sessionId, const icon = getDocumentIcon(title);
submitQuery: submitQuery,
setSnack, return (
}} /> <Card>
<Document {...{ <CardContent>
title: "BETA", <Box sx={{ color: 'inherit', fontSize: "1.75rem", fontWeight: "bold", display: "flex", flexDirection: "row", gap: 1, alignItems: "center", mr: 1.5 }}>
filepath: "/docs/beta.md", {icon}
onExpand: (open: boolean) => { onDocumentExpand('beta', open); }, {title}
expanded: page === 'beta', </Box>
sessionId, <Document
submitQuery: submitQuery, filepath={`/docs/${page}.md`}
setSnack, sessionId={sessionId}
}} /> submitQuery={submitQuery}
<Document {...{ setSnack={setSnack}
title: "Resume Generation Architecture", />
filepath: "/docs/resume-generation.md", </CardContent>
onExpand: (open: boolean) => { onDocumentExpand('resume-generation', open); }, </Card>
expanded: page === 'resume-generation', );
sessionId, };
submitQuery: submitQuery,
setSnack, // Render the appropriate content based on current page
}} /> function renderContent() {
<Document {...{ switch (page) {
title: "Application Architecture", case 'about':
filepath: "/docs/about-app.md", return <DocView page={page} />
onExpand: (open: boolean) => { onDocumentExpand('about-app', open); }, case 'beta':
expanded: page === 'about-app', return <DocView page={page} />
sessionId, case 'resume-generation':
submitQuery: submitQuery, return <DocView page={page} />
setSnack, case 'about-app':
}} /> return <DocView page={page} />
<Document {...{ case 'ui-overview':
title: "UI Overview", return (<BackstoryUIOverviewPage />);
children: <BackstoryUIOverviewPage/>, case 'theme-visualizer':
onExpand: (open: boolean) => { onDocumentExpand('ui-overview', open); }, return (<Paper sx={{ m: 0, p: 1 }}><BackstoryThemeVisualizerPage /></Paper>);
expanded: page === 'ui-overview', case 'app-analysis':
sessionId, return (<BackstoryAppAnalysisPage />);
submitQuery: submitQuery, case 'ui-mockup':
setSnack, return (<MockupPage />);
}} /> default:
<Document {...{ // Document grid for landing page
title: "Theme Visualizer", return (
onExpand: (open: boolean) => { onDocumentExpand('theme-visualizer', open); }, <Paper sx={{ p: 3 }} elevation={1}>
expanded: page === 'theme-visualizer', <Typography variant="h4" component="h1" gutterBottom>
children: <BackstoryThemeVisualizerPage/>, Documentation
sessionId, </Typography>
submitQuery: submitQuery, <Typography variant="body1" color="text.secondary" paragraph>
setSnack, Select a document from the sidebar to view detailed technical information about the application.
}} /> </Typography>
<Document {...{
title: "App Analysis", <Grid container spacing={2}>
onExpand: (open: boolean) => { onDocumentExpand('app-analysis', open); }, {[
expanded: page === 'app-analysis', { title: "About", route: "about", description: "General information about the application and its purpose" },
children: <BackstoryAppAnalysisPage/>, { title: "BETA", route: "beta", description: "Details about the current beta version and upcoming features" },
sessionId, { title: "Resume Generation Architecture", route: "resume-generation", description: "Technical overview of how resumes are processed and generated" },
submitQuery: submitQuery, { title: "Application Architecture", route: "about-app", description: "System design and technical stack information" },
setSnack, { title: "UI Overview", route: "ui-overview", description: "Guide to the user interface components and interactions" },
}} /> { title: "Theme Visualizer", route: "theme-visualizer", description: "Explore and customize application themes and visual styles" },
<Document {...{ { title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application" },
title: "UI Mockup", { title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts" }
onExpand: (open: boolean) => { onDocumentExpand('ui-mockup', open); }, ].map((doc, index) => (
expanded: page === 'ui-mockup', <Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
children: <MockupPage />, <Card>
sessionId, <CardActionArea onClick={() => onDocumentExpand(doc.route, true)}>
submitQuery: submitQuery, <CardContent>
setSnack, <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
}} /> <Box sx={{ color: 'primary.main', mr: 1.5 }}>
</Box>) {getDocumentIcon(doc.title)}
</Box>
<Typography variant="h6">{doc.title}</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ ml: 5 }}>
{doc.description}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
</Paper>
);
}
}
// Calculate drawer width
const drawerWidth = 240;
return (
<Box sx={{ display: 'flex', height: '100%' }}>
{/* Mobile App Bar */}
{isMobile && (
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
display: { md: 'none' }
}}
elevation={0}
color="default"
>
<Toolbar>
<IconButton
aria-label="open drawer"
edge="start"
onClick={toggleDrawer}
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div" sx={{ color: "white" }}>
{page ? documentTitleFromRoute(page) : "Documentation"}
</Typography>
</Toolbar>
</AppBar>
)}
{/* Navigation drawer */}
<Box
component="nav"
sx={{
width: { md: drawerWidth },
flexShrink: { md: 0 }
}}
>
{/* Mobile drawer (temporary) */}
{isMobile ? (
<Drawer
variant="temporary"
open={drawerOpen}
onClose={closeDrawer}
ModalProps={{
keepMounted: true, // Better open performance on mobile
}}
sx={{
display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth
},
}}
>
<Sidebar
currentPage={page}
onDocumentSelect={onDocumentExpand}
onClose={closeDrawer}
isMobile={true}
/>
</Drawer>
) : (
// Desktop drawer (permanent)
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', md: 'block' },
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth,
position: 'relative',
height: '100%'
},
}}
open
>
<Sidebar
currentPage={page}
onDocumentSelect={onDocumentExpand}
isMobile={false}
/>
</Drawer>
)}
</Box>
{/* Main content */}
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { md: `calc(100% - ${drawerWidth}px)` },
pt: isMobile ? { xs: 8, sm: 9 } : 3, // Add padding top on mobile to account for AppBar
height: '100%',
overflow: 'auto'
}}
>
{renderContent()}
</Box>
</Box>
);
}; };
export { export { AboutPage };
AboutPage
};

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { backstoryTheme } from '../BackstoryTheme'; import { backstoryTheme } from '../BackstoryTheme';
import { Box, Typography, Paper, Container } from '@mui/material';
// This component provides a visual demonstration of the theme colors // This component provides a visual demonstration of the theme colors
const BackstoryThemeVisualizerPage = () => { const BackstoryThemeVisualizerPage = () => {
@ -15,6 +16,10 @@ const BackstoryThemeVisualizerPage = () => {
); );
return ( return (
<Box sx={{ backgroundColor: 'background.default', minHeight: '100vh', py: 4 }}>
<Container maxWidth="lg">
<Paper sx={{ p: 4, boxShadow: 2 }}>
<div className="p-8"> <div className="p-8">
<h1 className="text-2xl font-bold mb-6" style={{ color: backstoryTheme.palette.text.primary }}> <h1 className="text-2xl font-bold mb-6" style={{ color: backstoryTheme.palette.text.primary }}>
Backstory Theme Visualization Backstory Theme Visualization
@ -188,6 +193,7 @@ const BackstoryThemeVisualizerPage = () => {
</table> </table>
</div> </div>
</div> </div>
</Paper></Container></Box>
); );
}; };

View File

@ -0,0 +1,266 @@
import React, { useState, useEffect } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import {
Box,
Container,
Typography,
Paper,
Grid,
Button,
useMediaQuery,
alpha,
GlobalStyles
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import ConstructionIcon from '@mui/icons-material/Construction';
import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';
import { Navigate } from 'react-router-dom';
interface BetaPageProps {
children?: React.ReactNode;
title?: string;
subtitle?: string;
returnPath?: string;
returnLabel?: string;
onReturn?: () => void;
}
export const BetaPage: React.FC<BetaPageProps> = ({
children,
title = "Coming Soon",
subtitle = "This page is currently in development",
returnPath = "/",
returnLabel = "Return to Backstory",
onReturn,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [showSparkle, setShowSparkle] = useState<boolean>(false);
const navigate = useNavigate();
// Enhanced sparkle effect for background elements
const [sparkles, setSparkles] = useState<Array<{
id: number;
x: number;
y: number;
size: number;
opacity: number;
duration: number;
delay: number;
}>>([]);
useEffect(() => {
// Generate sparkle elements with random properties
const newSparkles = Array.from({ length: 30 }).map((_, index) => ({
id: index,
x: Math.random() * 100,
y: Math.random() * 100,
size: 2 + Math.random() * 5,
opacity: 0.3 + Math.random() * 0.7,
duration: 2 + Math.random() * 4,
delay: Math.random() * 3,
}));
setSparkles(newSparkles);
// Show main sparkle effect after a short delay
const timer = setTimeout(() => {
setShowSparkle(true);
}, 500);
return () => clearTimeout(timer);
}, []);
const handleReturn = () => {
if (onReturn) {
onReturn();
} else if (returnPath) {
navigate(returnPath);
}
};
return (
<Box
sx={{
minHeight: 'calc(100vh-72px)',
position: 'relative',
overflow: 'hidden',
bgcolor: theme.palette.background.default,
pt: 8,
pb: 6,
}}
>
{/* Animated background elements */}
<Box sx={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 0, overflow: 'hidden' }}>
{sparkles.map((sparkle) => (
<Box
key={sparkle.id}
sx={{
position: 'absolute',
left: `${sparkle.x}%`,
top: `${sparkle.y}%`,
width: sparkle.size,
height: sparkle.size,
borderRadius: '50%',
bgcolor: alpha(theme.palette.primary.main, sparkle.opacity),
boxShadow: `0 0 ${sparkle.size * 2}px ${alpha(theme.palette.primary.main, sparkle.opacity)}`,
animation: `float ${sparkle.duration}s ease-in-out ${sparkle.delay}s infinite alternate`,
}}
/>
))}
</Box>
<Container maxWidth="lg" sx={{ position: 'relative', zIndex: 2 }}>
<Grid container spacing={4} direction="column" alignItems="center">
<Grid size={{xs: 12}} sx={{ textAlign: 'center', mb: 2 }}>
<Typography
variant="h2"
component="h1"
gutterBottom
sx={{
fontWeight: 'bold',
color: theme.palette.primary.main,
textShadow: `0 0 10px ${alpha(theme.palette.primary.main, 0.3)}`,
animation: showSparkle ? 'titleGlow 3s ease-in-out infinite alternate' : 'none',
}}
>
{title}
</Typography>
<Typography
variant="h5"
component="h2"
color="textSecondary"
sx={{ mb: 6 }}
>
{subtitle}
</Typography>
</Grid>
<Grid size={{xs: 12, md: 10, lg: 8}} sx={{ mb: 4 }}>
<Paper
elevation={8}
sx={{
p: { xs: 3, md: 5 },
borderRadius: 2,
bgcolor: alpha(theme.palette.background.paper, 0.8),
backdropFilter: 'blur(8px)',
boxShadow: `0 8px 32px ${alpha(theme.palette.primary.main, 0.15)}`,
border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}`,
position: 'relative',
overflow: 'hidden',
}}
>
{/* Construction icon */}
<Box
sx={{
position: 'absolute',
top: -15,
right: -15,
bgcolor: theme.palette.warning.main,
color: theme.palette.warning.contrastText,
borderRadius: '50%',
p: 2,
boxShadow: 3,
transform: 'rotate(15deg)',
}}
>
<ConstructionIcon fontSize="large" />
</Box>
{/* Content */}
<Box sx={{ mt: 3, mb: 3 }}>
{children || (
<Box sx={{ textAlign: 'center', py: 4 }}>
<RocketLaunchIcon
fontSize="large"
color="primary"
sx={{
fontSize: 80,
mb: 2,
animation: 'rocketWobble 3s ease-in-out infinite'
}}
/>
<Typography>
We're working hard to bring you this exciting new feature!
</Typography>
<Typography color="textSecondary" sx={{ mt: 1 }}>
Check back soon for updates.
</Typography>
</Box>
)}
</Box>
{/* Return button */}
<Box sx={{ mt: 4, textAlign: 'center' }}>
<Button
variant="contained"
color="primary"
size="large"
onClick={handleReturn}
sx={{
px: 4,
py: 1,
borderRadius: 4,
boxShadow: `0 4px 14px ${alpha(theme.palette.primary.main, 0.4)}`,
'&:hover': {
boxShadow: `0 6px 20px ${alpha(theme.palette.primary.main, 0.6)}`,
}
}}
>
{returnLabel}
</Button>
</Box>
</Paper>
</Grid>
</Grid>
</Container>
{/* Global styles added with MUI's GlobalStyles component */}
<GlobalStyles
styles={{
'@keyframes float': {
'0%': {
transform: 'translateY(0) scale(1)',
},
'100%': {
transform: 'translateY(-20px) scale(1.1)',
},
},
'@keyframes sparkleFloat': {
'0%': {
transform: 'translateY(0) scale(1)',
opacity: 0.7,
},
'50%': {
opacity: 1,
},
'100%': {
transform: 'translateY(-15px) scale(1.2)',
opacity: 0.7,
},
},
'@keyframes titleGlow': {
'0%': {
textShadow: `0 0 10px ${alpha(theme.palette.primary.main, 0.3)}`,
},
'100%': {
textShadow: `0 0 25px ${alpha(theme.palette.primary.main, 0.7)}, 0 0 40px ${alpha(theme.palette.primary.main, 0.4)}`,
},
},
'@keyframes rocketWobble': {
'0%': {
transform: 'translateY(0) rotate(0deg)',
},
'50%': {
transform: 'translateY(-10px) rotate(3deg)',
},
'100%': {
transform: 'translateY(0) rotate(-2deg)',
},
},
}}
/>
</Box>
);
};

View File

@ -0,0 +1,25 @@
import React from 'react';
import {
Typography,
} from '@mui/material';
import { BetaPage } from './BetaPage';
const MyIncompletePage = () => {
return (
<BetaPage
title="Analytics Dashboard"
subtitle="Our powerful analytics tools are coming soon"
returnLabel="Back to Home"
returnPath="/home"
>
<Typography variant="body1">
We're building a comprehensive analytics dashboard that will provide real-time insights
into your business performance. The expected completion date is June 15, 2025.
</Typography>
<Typography variant="body1" sx={{ mt: 2 }}>
Features will include custom reports, data visualization, and export capabilities.
</Typography>
</BetaPage>
);
};

View File

@ -0,0 +1,105 @@
import React, { forwardRef, useEffect, useState } from 'react';
import useMediaQuery from '@mui/material/useMediaQuery';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
import MuiMarkdown from 'mui-markdown';
import { BackstoryPageProps } from '../../Components//BackstoryTab';
import { Conversation, ConversationHandle } from '../../Components/Conversation';
import { ChatQuery, Tunables } from '../../Components/ChatQuery';
import { MessageList } from '../../Components/Message';
import { CandidateInfo, UserInfo } from 'NewApp/Components/CandidateInfo';
import { connectionBase } from '../../Global';
import { LoadingComponent } from 'NewApp/Components/LoadingComponent';
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
const { sessionId, setSnack, submitQuery } = props;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [preamble, setPreamble] = useState<MessageList>([]);
const [questions, setQuestions] = useState<React.ReactElement[]>([]);
const [user, setUser] = useState<UserInfo | undefined>(undefined)
useEffect(() => {
if (user === undefined) {
return;
}
setPreamble([{
role: 'system',
disableCopy: true,
content: `
What would you like to know about ${user.first_name}?
`,
}]);
setQuestions([
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
{user.questions.map(({ question, tunables }, i: number) =>
<ChatQuery key={i} query={{ prompt: question, tunables: tunables }} submitQuery={submitQuery} />
)}
</Box>,
<Box sx={{ p: 1 }}>
<MuiMarkdown>
{`As with all LLM interactions, the results may not be 100% accurate. Please contact **${user.full_name}** if you have any questions.`}
</MuiMarkdown>
</Box>]);
}, [user, isMobile, submitQuery]);
useEffect(() => {
const fetchUserInfo = async () => {
try {
const response = await fetch(connectionBase + `/api/user/${sessionId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
}
const data = await response.json();
setUser(data);
}
catch (error) {
console.error('Error getting user info:', error);
setSnack("Unable to obtain user information.", "error");
}
};
fetchUserInfo();
}, [setSnack, sessionId]);
if (sessionId === undefined || user === undefined) {
return (<Box>
<LoadingComponent
loadingText="Fetching user information..."
loaderType="linear"
withFade={true}
fadeDuration={1200} />
</Box>);
}
return (
<Box>
<CandidateInfo userInfo={user} />
<Conversation
ref={ref}
{...{
multiline: true,
type: "chat",
placeholder: `What would you like to know about ${user.first_name}?`,
resetLabel: "chat",
sessionId,
setSnack,
// preamble: preamble,
defaultPrompts: questions,
submitQuery,
}} />
</Box>);
});
export {
ChatPage
};

View File

@ -0,0 +1,381 @@
import React, { useState } from 'react';
import {
Box,
Button,
Container,
Grid,
Paper,
TextField,
Typography,
Avatar,
IconButton,
Stepper,
Step,
StepLabel,
useMediaQuery,
CircularProgress,
Snackbar,
Alert
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { CloudUpload, PhotoCamera } from '@mui/icons-material';
import { useTheme } from '@mui/material/styles';
// Interfaces
interface ProfileFormData {
firstName: string;
lastName: string;
email: string;
phoneNumber: string;
jobTitle: string;
location: string;
bio: string;
}
// Styled components
const VisuallyHiddenInput = styled('input')({
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: 1,
overflow: 'hidden',
position: 'absolute',
bottom: 0,
left: 0,
whiteSpace: 'nowrap',
width: 1,
});
const CreateProfilePage: React.FC = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
// State management
const [activeStep, setActiveStep] = useState<number>(0);
const [profileImage, setProfileImage] = useState<string | null>(null);
const [resumeFile, setResumeFile] = useState<File | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [snackbar, setSnackbar] = useState<{open: boolean, message: string, severity: "success" | "error"}>({
open: false,
message: '',
severity: 'success'
});
const [formData, setFormData] = useState<ProfileFormData>({
firstName: '',
lastName: '',
email: '',
phoneNumber: '',
jobTitle: '',
location: '',
bio: '',
});
// Steps for the profile creation process
const steps = ['Personal Information', 'Professional Details', 'Resume Upload'];
// Handle form input changes
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
};
// Handle profile image upload
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onload = (event) => {
if (event.target?.result) {
setProfileImage(event.target.result.toString());
}
};
reader.readAsDataURL(e.target.files[0]);
}
};
// Handle resume file upload
const handleResumeUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setResumeFile(e.target.files[0]);
setSnackbar({
open: true,
message: `Resume uploaded: ${e.target.files[0].name}`,
severity: 'success'
});
}
};
// Navigation functions
const handleNext = () => {
if (activeStep === steps.length - 1) {
handleSubmit();
} else {
setActiveStep((prevStep) => prevStep + 1);
}
};
const handleBack = () => {
setActiveStep((prevStep) => prevStep - 1);
};
// Form submission
const handleSubmit = async () => {
setLoading(true);
// Simulate API call with timeout
setTimeout(() => {
setLoading(false);
setSnackbar({
open: true,
message: 'Profile created successfully! Redirecting to dashboard...',
severity: 'success'
});
// Redirect would happen here in a real application
// history.push('/dashboard');
}, 2000);
};
// Form validation
const isStepValid = () => {
switch (activeStep) {
case 0:
return formData.firstName.trim() !== '' &&
formData.lastName.trim() !== '' &&
formData.email.trim() !== '';
case 1:
return formData.jobTitle.trim() !== '';
case 2:
return resumeFile !== null;
default:
return true;
}
};
// Stepper content based on active step
const getStepContent = (step: number) => {
switch (step) {
case 0:
return (
<Grid container spacing={3}>
<Grid size={{xs: 12}} sx={{ textAlign: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Avatar
src={profileImage || ''}
sx={{
width: 120,
height: 120,
mb: 2,
border: `2px solid ${theme.palette.primary.main}`
}}
/>
<IconButton
color="primary"
aria-label="upload picture"
component="label"
>
<PhotoCamera />
<VisuallyHiddenInput
type="file"
accept="image/*"
onChange={handleImageUpload}
/>
</IconButton>
<Typography variant="caption" color="textSecondary">
Add profile photo
</Typography>
</Box>
</Grid>
<Grid size={{xs: 12, sm: 6}}>
<TextField
required
fullWidth
label="First Name"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
variant="outlined"
/>
</Grid>
<Grid size={{xs: 12, sm: 6}}>
<TextField
required
fullWidth
label="Last Name"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
variant="outlined"
/>
</Grid>
<Grid size={{xs: 12}}>
<TextField
required
fullWidth
label="Email Address"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
variant="outlined"
/>
</Grid>
<Grid size={{xs:12}}>
<TextField
fullWidth
label="Phone Number"
name="phoneNumber"
value={formData.phoneNumber}
onChange={handleInputChange}
variant="outlined"
/>
</Grid>
</Grid>
);
case 1:
return (
<Grid container spacing={3}>
<Grid size={{xs:12}}>
<TextField
required
fullWidth
label="Job Title"
name="jobTitle"
value={formData.jobTitle}
onChange={handleInputChange}
variant="outlined"
/>
</Grid>
<Grid size={{xs: 12}}>
<TextField
fullWidth
label="Location"
name="location"
placeholder="City, State, Country"
value={formData.location}
onChange={handleInputChange}
variant="outlined"
/>
</Grid>
<Grid size={{xs:12}}>
<TextField
fullWidth
multiline
rows={4}
label="Professional Bio"
name="bio"
placeholder="Tell us about yourself and your professional experience"
value={formData.bio}
onChange={handleInputChange}
variant="outlined"
/>
</Grid>
</Grid>
);
case 2:
return (
<Grid container spacing={3}>
<Grid size={{xs: 12}}>
<Typography variant="body1" component="p">
Upload your resume to complete your profile. We'll analyze it to better understand your skills and experience.
(Supported formats: PDF, DOCX)
</Typography>
<Box sx={{ textAlign: 'center', mt: 2 }}>
<Button
component="label"
variant="contained"
startIcon={<CloudUpload />}
sx={{ mb: 2 }}
>
Upload Resume
<VisuallyHiddenInput
type="file"
accept=".pdf,.docx"
onChange={handleResumeUpload}
/>
</Button>
{resumeFile && (
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
File uploaded: {resumeFile.name}
</Typography>
)}
</Box>
</Grid>
</Grid>
);
default:
return 'Unknown step';
}
};
return (
<Container component="main">
<Paper
elevation={3}
sx={{
p: { xs: 2, sm: 4 },
mt: { xs: 2, sm: 4 },
mb: { xs: 2, sm: 4 }
}}
>
<Typography component="h1" variant="h4" align="center" gutterBottom>
Create Your Profile
</Typography>
<Stepper
activeStep={activeStep}
alternativeLabel={!isMobile}
orientation={isMobile ? 'vertical' : 'horizontal'}
sx={{ mt: 3, mb: 5 }}
>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<Box sx={{ mt: 2, mb: 4 }}>
{getStepContent(activeStep)}
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 4 }}>
<Button
disabled={activeStep === 0}
onClick={handleBack}
variant="outlined"
>
Back
</Button>
<Button
variant="contained"
onClick={handleNext}
disabled={!isStepValid() || loading}
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : null}
>
{activeStep === steps.length - 1 ? 'Create Profile' : 'Next'}
</Button>
</Box>
</Paper>
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={() => setSnackbar({ ...snackbar, open: false })}
>
<Alert
onClose={() => setSnackbar({ ...snackbar, open: false })}
severity={snackbar.severity}
sx={{ width: '100%' }}
>
{snackbar.message}
</Alert>
</Snackbar>
</Container>
);
};
export { CreateProfilePage };

View File

@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { import {
Box, Box,
Button, Button,
ButtonProps,
Container, Container,
Paper, Paper,
Typography, Typography,
@ -37,17 +39,53 @@ const HeroSection = styled(Box)(({ theme }) => ({
}, },
})); }));
const HeroButton = styled(Button)(({ theme }) => ({ interface HeroButtonProps extends ButtonProps {
marginTop: theme.spacing(2), children?: string;
padding: theme.spacing(1, 3), }
fontWeight: 500, const HeroButton = (props: HeroButtonProps) => {
backgroundColor: theme.palette.action.active, const { children, onClick, ...rest } = props;
color: theme.palette.background.paper,
'&:hover': { const navigate = useNavigate();
const handleClick = () => {
const path = children?.replace(/ /g, '-').toLocaleLowerCase() || '/';
navigate(path);
};
const HeroStyledButton = styled(Button)(({ theme }) => ({
marginTop: theme.spacing(2),
padding: theme.spacing(1, 3),
fontWeight: 500,
backgroundColor: theme.palette.action.active, backgroundColor: theme.palette.action.active,
opacity: 0.9, color: theme.palette.background.paper,
}, '&:hover': {
})); backgroundColor: theme.palette.action.active,
opacity: 0.9,
},
}));
return <HeroStyledButton onClick={onClick ? onClick : handleClick} {...rest}>
{children}
</HeroStyledButton>
}
interface ActionButtonProps extends ButtonProps {
children?: string;
}
const ActionButton = (props: ActionButtonProps) => {
const { children, onClick, ...rest } = props;
const navigate = useNavigate();
const handleClick = () => {
const path = children?.replace(/ /g, '-').toLocaleLowerCase() || '/';
navigate(path);
};
return <Button onClick={onClick ? onClick : handleClick} {...rest}>
{children}
</Button>
}
const FeatureIcon = styled(Box)(({ theme }) => ({ const FeatureIcon = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.action.active, backgroundColor: theme.palette.action.active,
@ -95,6 +133,8 @@ const FeatureCard = ({
}; };
const HomePage = () => { const HomePage = () => {
const navigate = useNavigate();
return (<Box sx={{display: "flex", flexDirection: "column"}}> return (<Box sx={{display: "flex", flexDirection: "column"}}>
{/* Hero Section */} {/* Hero Section */}
<HeroSection> <HeroSection>
@ -251,14 +291,14 @@ const HomePage = () => {
</Box> </Box>
</Stack> </Stack>
<Button <ActionButton
variant="contained" variant="contained"
color="secondary" color="secondary"
sx={{ mt: 4 }} sx={{ mt: 4 }}
endIcon={<ArrowForwardIcon />} endIcon={<ArrowForwardIcon />}
> >
Create Your Profile Create Your Profile
</Button> </ActionButton>
</Box> </Box>
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
@ -339,14 +379,14 @@ const HomePage = () => {
</Box> </Box>
</Stack> </Stack>
<Button <ActionButton
variant="contained" variant="contained"
color="secondary" color="secondary"
sx={{ mt: 4 }} sx={{ mt: 4 }}
endIcon={<ArrowForwardIcon />} endIcon={<ArrowForwardIcon />}
> >
Start Recruiting Start Recruiting
</Button> </ActionButton>
</Box> </Box>
</Box> </Box>
</Container> </Container>

View File

@ -189,7 +189,7 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
setHasFacts(false); setHasFacts(false);
}, [setHasFacts]); }, [setHasFacts]);
const renderJobDescriptionView = useCallback((sx: SxProps) => { const renderJobDescriptionView = useCallback((sx?: SxProps) => {
console.log('renderJobDescriptionView'); console.log('renderJobDescriptionView');
const jobDescriptionQuestions = [ const jobDescriptionQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: "flex", flexDirection: "column" }}>
@ -262,7 +262,7 @@ See [About > Resume Generation Architecture](/about/resume-generation) for more
/** /**
* Renders the resume view with loading indicator * Renders the resume view with loading indicator
*/ */
const renderResumeView = useCallback((sx: SxProps) => { const renderResumeView = useCallback((sx?: SxProps) => {
const resumeQuestions = [ const resumeQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: "flex", flexDirection: "column" }}>
<ChatQuery query={{ prompt: "Is this resume a good fit for the provided job description?", tunables: { enable_tools: false } }} submitQuery={handleResumeQuery} /> <ChatQuery query={{ prompt: "Is this resume a good fit for the provided job description?", tunables: { enable_tools: false } }} submitQuery={handleResumeQuery} />
@ -311,7 +311,7 @@ See [About > Resume Generation Architecture](/about/resume-generation) for more
/** /**
* Renders the fact check view * Renders the fact check view
*/ */
const renderFactCheckView = useCallback((sx: SxProps) => { const renderFactCheckView = useCallback((sx?: SxProps) => {
const factsQuestions = [ const factsQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: "flex", flexDirection: "column" }}>
<ChatQuery query={{ prompt: "Rewrite the resume to address any discrepancies.", tunables: { enable_tools: false } }} submitQuery={handleFactsQuery} /> <ChatQuery query={{ prompt: "Rewrite the resume to address any discrepancies.", tunables: { enable_tools: false } }} submitQuery={handleFactsQuery} />
@ -368,9 +368,9 @@ See [About > Resume Generation Architecture](/about/resume-generation) for more
display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx, display: 'flex', flexDirection: 'column', flexGrow: 1, p: 0, width: "100%", ...sx,
overflow: "hidden" overflow: "hidden"
}}> }}>
<Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView({ height: "calc(100vh - 72px - 48px)" })}</Box> <Box sx={{ display: activeTab === 0 ? "flex" : "none" }}>{renderJobDescriptionView(/*{ height: "calc(100vh - 72px - 48px)" }*/)}</Box>
<Box sx={{ display: activeTab === 1 ? "flex" : "none" }}>{renderResumeView({ height: "calc(100vh - 72px - 48px)" })}</Box> <Box sx={{ display: activeTab === 1 ? "flex" : "none" }}>{renderResumeView(/*{ height: "calc(100vh - 72px - 48px)" }*/)}</Box>
<Box sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView({ height: "calc(100vh - 72px - 48px)" })}</Box> <Box sx={{ display: activeTab === 2 ? "flex" : "none" }}>{renderFactCheckView(/*{ height: "calc(100vh - 72px - 48px)" }*/)}</Box>
</Box> </Box>
</Box> </Box>
); );

View File

@ -9,7 +9,4 @@
margin: 0 auto; margin: 0 auto;
padding: 10px; padding: 10px;
position: relative; position: relative;
width: 100%;
max-width: 1024px;
height: calc(100vh - 72px);
} }

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Scrollable } from '../Components/Scrollable';
import { VectorVisualizer } from '../Components/VectorVisualizer'; import { VectorVisualizer } from '../Components/VectorVisualizer';
import { BackstoryPageProps } from '../Components/BackstoryTab'; import { BackstoryPageProps } from '../Components/BackstoryTab';
@ -12,15 +11,7 @@ interface VectorVisualizerProps extends BackstoryPageProps {
}; };
const VectorVisualizerPage: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => { const VectorVisualizerPage: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
return <Scrollable return <VectorVisualizer inline={false} {...props} />;
className="VectorVisualizerPage"
autoscroll={false}
sx={{
overflowY: "scroll",
}}
>
<VectorVisualizer {...props} />
</Scrollable>;
}; };
export type { VectorVisualizerProps }; export type { VectorVisualizerProps };

View File

@ -8,6 +8,7 @@ body {
padding: 0; padding: 0;
height: 100dvh; height: 100dvh;
overflow: hidden; overflow: hidden;
min-width: 300px;
} }
code { code {

View File

@ -278,12 +278,12 @@ class WebServer:
self.setup_routes() self.setup_routes()
def setup_routes(self): def setup_routes(self):
@self.app.get("/") # @self.app.get("/")
async def root(): # async def root():
context = self.create_context(username=defines.default_username) # context = self.create_context(username=defines.default_username)
logger.info(f"Redirecting non-context to {context.id}") # logger.info(f"Redirecting non-context to {context.id}")
return RedirectResponse(url=f"/{context.id}", status_code=307) # return RedirectResponse(url=f"/{context.id}", status_code=307)
# return JSONResponse({"redirect": f"/{context.id}"}) # # return JSONResponse({"redirect": f"/{context.id}"})
@self.app.get("/api/umap/entry/{doc_id}/{context_id}") @self.app.get("/api/umap/entry/{doc_id}/{context_id}")
async def get_umap(doc_id: str, context_id: str, request: Request): async def get_umap(doc_id: str, context_id: str, request: Request):
@ -575,7 +575,10 @@ class WebServer:
"first_name": user.first_name, "first_name": user.first_name,
"last_name": user.last_name, "last_name": user.last_name,
"full_name": user.full_name, "full_name": user.full_name,
"description": user.description,
"contact_info": user.contact_info, "contact_info": user.contact_info,
"rag_content_size": user.rag_content_size,
"profile_url": user.profile_url,
"questions": [ q.model_dump(mode='json') for q in user.user_questions], "questions": [ q.model_dump(mode='json') for q in user.user_questions],
} }
return JSONResponse(user_data) return JSONResponse(user_data)
@ -730,6 +733,20 @@ class WebServer:
logger.error(f"Error in post_chat_endpoint: {e}") logger.error(f"Error in post_chat_endpoint: {e}")
return JSONResponse({"error": str(e)}, status_code=500) return JSONResponse({"error": str(e)}, status_code=500)
@self.app.post("/api/create-session")
async def create_session(request: Request):
logger.info(f"{request.method} {request.url.path}")
context = self.create_context(username=defines.default_username)
return JSONResponse({"id": context.id})
@self.app.get("/api/join-session/{context_id}")
async def join_session(context_id: str, request: Request):
logger.info(f"{request.method} {request.url.path}")
context = self.load_context(context_id=context_id)
if not context:
return JSONResponse({"error": f"{context_id} does not exist."}, 404)
return JSONResponse({"id": context.id})
@self.app.post("/api/context/u/{username}") @self.app.post("/api/context/u/{username}")
async def create_user_context(username: str, request: Request): async def create_user_context(username: str, request: Request):
logger.info(f"{request.method} {request.url.path}") logger.info(f"{request.method} {request.url.path}")
@ -884,7 +901,7 @@ class WebServer:
return context_id return context_id
def load_or_create_context(self, context_id: str) -> Context: def load_context(self, context_id: str) -> Context | None:
""" """
Load a context from a file in the context directory or create a new one if it doesn't exist. Load a context from a file in the context directory or create a new one if it doesn't exist.
Args: Args:
@ -896,71 +913,74 @@ class WebServer:
# Check if the file exists # Check if the file exists
if not os.path.exists(file_path): if not os.path.exists(file_path):
logger.info(f"Context file {file_path} not found. Creating new context.") return None
self.contexts[context_id] = self.create_context(username=defines.default_username, context_id=context_id)
else: # Read and deserialize the data
# Read and deserialize the data with open(file_path, "r") as f:
with open(file_path, "r") as f: content = f.read()
content = f.read() logger.info(
logger.info( f"Loading context from {file_path}, content length: {len(content)}"
f"Loading context from {file_path}, content length: {len(content)}" )
) json_data = {}
json_data = {} try:
try: # Try parsing as JSON first to ensure valid JSON
# Try parsing as JSON first to ensure valid JSON json_data = json.loads(content)
json_data = json.loads(content) logger.info("JSON parsed successfully, attempting model validation")
logger.info("JSON parsed successfully, attempting model validation")
context = Context.model_validate(json_data)
username = context.username
if not User.exists(username):
raise ValueError(f"Attempt to load context {context.id} with invalid user {username}")
matching_user = next((user for user in self.users if user.username == username), None) context = Context.model_validate(json_data)
if matching_user: username = context.username
user = matching_user if not User.exists(username):
else: raise ValueError(f"Attempt to load context {context.id} with invalid user {username}")
user = User(username=username, llm=self.llm)
user.initialize(prometheus_collector=self.prometheus_collector) matching_user = next((user for user in self.users if user.username == username), None)
self.users.append(user) if matching_user:
context.user = user user = matching_user
else:
user = User(username=username, llm=self.llm)
user.initialize(prometheus_collector=self.prometheus_collector)
self.users.append(user)
context.user = user
# Now set context on agents manually # Now set context on agents manually
agent_types = [agent.agent_type for agent in context.agents] agent_types = [agent.agent_type for agent in context.agents]
if len(agent_types) != len(set(agent_types)): if len(agent_types) != len(set(agent_types)):
raise ValueError( raise ValueError(
"Context cannot contain multiple agents of the same agent_type" "Context cannot contain multiple agents of the same agent_type"
)
for agent in context.agents:
agent.set_context(context)
self.contexts[context_id] = context
logger.info(f"Successfully loaded context {context_id}")
except ValidationError as e:
logger.error(e)
logger.error(traceback.format_exc())
for error in e.errors():
print(f"Field: {error['loc'][0]}, Error: {error['msg']}")
except Exception as e:
logger.error(f"Error validating context: {str(e)}")
logger.error(traceback.format_exc())
for key in json_data:
logger.info(f"{key} = {type(json_data[key])} {str(json_data[key])[:60] if json_data[key] else "None"}")
logger.info("*" * 50)
if len(self.users) == 0:
user = User(username=defines.default_username, llm=self.llm)
user.initialize(prometheus_collector=self.prometheus_collector)
self.users.append(user)
# Fallback to creating a new context
user = self.users[0]
self.contexts[context_id] = Context(
id=context_id,
user=user,
rags=[ rag.model_copy() for rag in user.rags ],
tools=Tools.all_tools()
) )
for agent in context.agents:
agent.set_context(context)
self.contexts[context_id] = context
logger.info(f"Successfully loaded context {context_id}")
except ValidationError as e:
logger.error(e)
logger.error(traceback.format_exc())
for error in e.errors():
print(f"Field: {error['loc'][0]}, Error: {error['msg']}")
except Exception as e:
logger.error(f"Error validating context: {str(e)}")
logger.error(traceback.format_exc())
for key in json_data:
logger.info(f"{key} = {type(json_data[key])} {str(json_data[key])[:60] if json_data[key] else "None"}")
logger.info("*" * 50)
return None
return self.contexts[context_id]
def load_or_create_context(self, context_id: str) -> Context:
"""
Load a context from a file in the context directory or create a new one if it doesn't exist.
Args:
context_id: UUID string for the context.
Returns:
A Context object with the specified ID and default settings.
"""
context = self.load_context(context_id)
if context:
return context
logger.info(f"Context not found. Creating new instance of context {context_id}.")
self.contexts[context_id] = self.create_context(username=defines.default_username, context_id=context_id)
return self.contexts[context_id] return self.contexts[context_id]
def create_context(self, username: str, context_id=None) -> Context: def create_context(self, username: str, context_id=None) -> Context:

View File

@ -42,7 +42,7 @@ logging_level = os.getenv("LOGGING_LEVEL", "INFO").upper()
chunk_buffer = 5 # Number of lines before and after chunk beyond the portion used in embedding (to return to callers) chunk_buffer = 5 # Number of lines before and after chunk beyond the portion used in embedding (to return to callers)
# Maximum number of entries for ChromaDB to find # Maximum number of entries for ChromaDB to find
default_rag_top_k = 80 default_rag_top_k = 50
# Cosine Distance Equivalent Similarity Retrieval Characteristics # Cosine Distance Equivalent Similarity Retrieval Characteristics
# 0.2 - 0.3 0.85 - 0.90 Very strict, highly precise results only # 0.2 - 0.3 0.85 - 0.90 Very strict, highly precise results only

View File

@ -39,6 +39,9 @@ class User(BaseModel):
first_name: str = "" first_name: str = ""
last_name: str = "" last_name: str = ""
full_name: str = "" full_name: str = ""
description: str = ""
profile_url: str = ""
rag_content_size : int = 0
contact_info : Dict[str, str] = {} contact_info : Dict[str, str] = {}
user_questions : List[Question] = [] user_questions : List[Question] = []
@ -197,6 +200,8 @@ class User(BaseModel):
self.first_name = info.get("first_name", self.username) self.first_name = info.get("first_name", self.username)
self.last_name = info.get("last_name", "") self.last_name = info.get("last_name", "")
self.full_name = info.get("full_name", f"{self.first_name} {self.last_name}") self.full_name = info.get("full_name", f"{self.first_name} {self.last_name}")
self.description = info.get("description", self.description)
self.profile_url = info.get("profile_url", self.description)
self.contact_info = info.get("contact_info", {}) self.contact_info = info.get("contact_info", {})
questions = info.get("questions", [ f"Tell me about {self.first_name}.", f"What are {self.first_name}'s professional strengths?"]) questions = info.get("questions", [ f"Tell me about {self.first_name}.", f"What are {self.first_name}'s professional strengths?"])
self.user_questions = [] self.user_questions = []
@ -226,5 +231,7 @@ class User(BaseModel):
name=self.username, name=self.username,
description=f"Expert data about {self.full_name}.", description=f"Expert data about {self.full_name}.",
)) ))
self.rag_content_size = self.file_watcher.collection.count()
User.model_rebuild() User.model_rebuild()