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 \
nano \
rsync \
iputils-ping \
jq \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}

View File

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

View File

@ -7,6 +7,13 @@ module.exports = {
// cert: '/path/to/cert.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: {

View File

@ -41,7 +41,7 @@
"web-vitals": "^2.1.4"
},
"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",
"test": "craco test"
},

View File

@ -4,12 +4,12 @@ First, what works:
1. There are two personas populated:
1. One is me [jketreno](/u/jketreno)
2. The other is a ficticious AI generated persona named [Eliz](/u/eliza).
2. **Candidate Skill Chat** You can go to the Chat tab to ask questions about the active candaite.
2. The other is a ficticious AI generated persona named [Eliza](/u/eliza).
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
What doesn't work:
1. User login, registration, etc.
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 { BrowserRouter as Router, Routes, Route, useLocation } from "react-router-dom";
import { SessionWrapper } from "./App/SessionWrapper";
import { Main } from "./App/Main";
import React from 'react';
import { BrowserRouter as Router } from "react-router-dom";
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 = () => {
return (

View File

@ -24,7 +24,9 @@ const Scrollable = (props: ScrollableProps) => {
className={`Scrollable ${className || ""}`}
sx={{
display: 'flex',
flexDirection: 'column',
margin: '0 auto',
p: 0,
flexGrow: 1,
overflow: 'auto',
// 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={className}>{element.children}</code></pre>;
return <pre><code className={className || ''}>{element.children}</code></pre>;
},
},
a: {
@ -81,7 +81,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
if (href) {
if (href.match(/^\//)) {
event.preventDefault();
window.history.replaceState({}, '', `${href}/${sessionId}`);
window.history.replaceState({}, '', `${href}`);
}
}
},

View File

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

View File

@ -21,6 +21,7 @@ import { connectionBase } from '../Global';
import './VectorVisualizer.css';
import { BackstoryPageProps } from './BackstoryTab';
import { relative } from 'path';
interface VectorVisualizerProps extends BackstoryPageProps {
inline?: boolean;
@ -478,19 +479,13 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
...sx
}}>
<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={{
p: 0.5, m: 0,
display: "flex",
flexGrow: 0,
height: isMobile ? "auto" : "320px",
minHeight: isMobile ? "auto" : "320px",
maxHeight: isMobile ? "auto" : "320px",
height: isMobile ? "auto" : "auto", //"320px",
minHeight: isMobile ? "auto" : "auto", //"320px",
maxHeight: isMobile ? "auto" : "auto", //"320px",
position: "relative",
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" }}>
{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 }}>
<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>
<TableCell>Type</TableCell>
<TableCell>{node.emoji} {node.doc_type}</TableCell>
</TableRow>
{node.source_file !== undefined && <TableRow>
<TableCell>File</TableCell>
<TableCell>{node.source_file.replace(/^.*\//, '')}, lines: {node.line_begin}-{node.line_end}</TableCell>
<TableCell>{node.source_file.replace(/^.*\//, '')}</TableCell>
</TableRow>}
{node.path !== undefined && <TableRow>
<TableCell>Location</TableCell>
<TableCell>Section</TableCell>
<TableCell>{node.path}</TableCell>
</TableRow>}
{node.distance !== undefined && <TableRow>
@ -560,7 +555,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
</Box>
}
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 1 }}>
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 2, flexBasis: 0, flexShrink: 1 }}>
{node === null &&
<Paper sx={{ m: 0.5, p: 2, flexGrow: 1 }}>
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,
pl: 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';
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>
<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>;
})
}

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 { ThemeProvider } from '@mui/material/styles';
import { backstoryTheme } from '../BackstoryTheme';
import {Header} from './Components/Header';
import { Scrollable } from '../Components/Scrollable';
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 { Query } from '../Components/ChatQuery';
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/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
import { connectionBase } from '../Global';
type NavigationLinkType = {
name: string;
@ -133,7 +141,7 @@ interface BackstoryPageContainerProps {
const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
const { children, sx } = props;
return (
<Container maxWidth="xl" sx={{ mt: 2, mb: 2, height: "calc(1024px - 72px)", ...sx }}>
<Container maxWidth="xl" sx={{ mt: 2, mb: 2, ...sx }}>
<Paper
elevation={2}
sx={{
@ -141,15 +149,26 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
backgroundColor: 'background.paper',
borderRadius: 2,
minHeight: '80vh',
}}
>
<Scrollable>
}}>
{children}
</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 navigate = useNavigate();
const location = useLocation();
@ -158,6 +177,7 @@ const BackstoryApp = () => {
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
const snackRef = useRef<any>(null);
const chatRef = useRef<ConversationHandle>(null);
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
const setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity);
}, [snackRef]);
@ -167,6 +187,79 @@ const BackstoryApp = () => {
navigate('/chat');
};
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(() => {
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
@ -187,14 +280,17 @@ const BackstoryApp = () => {
flexDirection: 'column',
backgroundColor: 'background.default',
maxHeight: "calc(100vh - 72px)",
minHeight: "calc(100vh - 72px)",
}}>
<BackstoryPageContainer userContext={userContext}>
{sessionId !== undefined &&
<Routes>
<Route path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} />
<Route path="/about" element={<AboutPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} />
<Route path="/about/:subPage" element={<AboutPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} />
<Route path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} />
<Route path="/rag-visualizer" element={<VectorVisualizerPage 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" element={<AboutPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/about/:subPage" element={<AboutPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/rag-visualizer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/create-your-profile" element={<CreateProfilePage />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/" element={<HomePage />} />
{/* Candidate-specific routes */}
@ -221,11 +317,12 @@ const BackstoryApp = () => {
<Route path="/analytics" element={<AnalyticsPage />} />
<Route path="/settings" element={<SettingsPage />} />
{/* Redirect to dashboard by default */}
<Route path="*" element={<HomePage />} />
{/* Redirect to BETA by default */}
<Route path="*" element={<BetaPage />} />
</Routes>
</BackstoryPageContainer>
}
{location.pathname === "/" && <Footer />}
</BackstoryPageContainer>
</Scrollable>
<Snack ref={snackRef}/>
</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 { useNavigate, useLocation, useParams } from 'react-router-dom';
import {
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 { Box } from '@mui/material';
import { Document } from '../Components/Document';
import { BackstoryPageProps } from '../../Components/BackstoryTab';
import { Document } from '../../Components/Document';
import { BackstoryUIOverviewPage } from './BackstoryUIOverviewPage';
import { BackstoryAppAnalysisPage } from './BackstoryAppAnalysisPage';
import { BackstoryThemeVisualizerPage } from './BackstoryThemeVisualizerPage';
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 { sessionId, submitQuery, setSnack } = props;
const navigate = useNavigate();
const location = useLocation();
const { paramPage = '' } = useParams();
const [page, setPage] = useState<string>(paramPage);
const [drawerOpen, setDrawerOpen] = useState(false);
/* If the location changes, set the page based on the
* second part of the path, or clear if no path */
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Track location changes
useEffect(() => {
const parts = location.pathname.split('/');
if (parts.length > 2) {
@ -28,6 +171,14 @@ const AboutPage = (props: BackstoryPageProps) => {
}
}, [location]);
// Close drawer when changing to desktop view
useEffect(() => {
if (!isMobile) {
setDrawerOpen(false);
}
}, [isMobile]);
// Handle document navigation
const onDocumentExpand = (docName: string, open: boolean) => {
console.log("Document expanded:", { docName, open, location });
if (open) {
@ -42,84 +193,230 @@ const AboutPage = (props: BackstoryPageProps) => {
const basePath = location.pathname.split('/').slice(0, -1).join('/');
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}}>
<Document {...{
title: "About",
filepath: "/docs/about.md",
onExpand: (open: boolean) => { onDocumentExpand('about', open); },
expanded: page === 'about',
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
<Document {...{
title: "BETA",
filepath: "/docs/beta.md",
onExpand: (open: boolean) => { onDocumentExpand('beta', open); },
expanded: page === 'beta',
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
<Document {...{
title: "Resume Generation Architecture",
filepath: "/docs/resume-generation.md",
onExpand: (open: boolean) => { onDocumentExpand('resume-generation', open); },
expanded: page === 'resume-generation',
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
<Document {...{
title: "Application Architecture",
filepath: "/docs/about-app.md",
onExpand: (open: boolean) => { onDocumentExpand('about-app', open); },
expanded: page === 'about-app',
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
<Document {...{
title: "UI Overview",
children: <BackstoryUIOverviewPage/>,
onExpand: (open: boolean) => { onDocumentExpand('ui-overview', open); },
expanded: page === 'ui-overview',
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
<Document {...{
title: "Theme Visualizer",
onExpand: (open: boolean) => { onDocumentExpand('theme-visualizer', open); },
expanded: page === 'theme-visualizer',
children: <BackstoryThemeVisualizerPage/>,
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
<Document {...{
title: "App Analysis",
onExpand: (open: boolean) => { onDocumentExpand('app-analysis', open); },
expanded: page === 'app-analysis',
children: <BackstoryAppAnalysisPage/>,
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
<Document {...{
title: "UI Mockup",
onExpand: (open: boolean) => { onDocumentExpand('ui-mockup', open); },
expanded: page === 'ui-mockup',
children: <MockupPage />,
sessionId,
submitQuery: submitQuery,
setSnack,
}} />
</Box>)
interface DocViewProps {
page: string
};
const DocView = (props: DocViewProps) => {
const { page } = props;
const title = documentTitleFromRoute(page);
const icon = getDocumentIcon(title);
return (
<Card>
<CardContent>
<Box sx={{ color: 'inherit', fontSize: "1.75rem", fontWeight: "bold", display: "flex", flexDirection: "row", gap: 1, alignItems: "center", mr: 1.5 }}>
{icon}
{title}
</Box>
<Document
filepath={`/docs/${page}.md`}
sessionId={sessionId}
submitQuery={submitQuery}
setSnack={setSnack}
/>
</CardContent>
</Card>
);
};
export {
AboutPage
// Render the appropriate content based on current page
function renderContent() {
switch (page) {
case 'about':
return <DocView page={page} />
case 'beta':
return <DocView page={page} />
case 'resume-generation':
return <DocView page={page} />
case 'about-app':
return <DocView page={page} />
case 'ui-overview':
return (<BackstoryUIOverviewPage />);
case 'theme-visualizer':
return (<Paper sx={{ m: 0, p: 1 }}><BackstoryThemeVisualizerPage /></Paper>);
case 'app-analysis':
return (<BackstoryAppAnalysisPage />);
case 'ui-mockup':
return (<MockupPage />);
default:
// Document grid for landing page
return (
<Paper sx={{ p: 3 }} elevation={1}>
<Typography variant="h4" component="h1" gutterBottom>
Documentation
</Typography>
<Typography variant="body1" color="text.secondary" paragraph>
Select a document from the sidebar to view detailed technical information about the application.
</Typography>
<Grid container spacing={2}>
{[
{ title: "About", route: "about", description: "General information about the application and its purpose" },
{ title: "BETA", route: "beta", description: "Details about the current beta version and upcoming features" },
{ title: "Resume Generation Architecture", route: "resume-generation", description: "Technical overview of how resumes are processed and generated" },
{ title: "Application Architecture", route: "about-app", description: "System design and technical stack information" },
{ 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" },
{ title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application" },
{ title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts" }
].map((doc, index) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<Card>
<CardActionArea onClick={() => onDocumentExpand(doc.route, true)}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Box sx={{ color: 'primary.main', mr: 1.5 }}>
{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 { AboutPage };

View File

@ -1,5 +1,6 @@
import React from 'react';
import { backstoryTheme } from '../BackstoryTheme';
import { Box, Typography, Paper, Container } from '@mui/material';
// This component provides a visual demonstration of the theme colors
const BackstoryThemeVisualizerPage = () => {
@ -15,6 +16,10 @@ const BackstoryThemeVisualizerPage = () => {
);
return (
<Box sx={{ backgroundColor: 'background.default', minHeight: '100vh', py: 4 }}>
<Container maxWidth="lg">
<Paper sx={{ p: 4, boxShadow: 2 }}>
<div className="p-8">
<h1 className="text-2xl font-bold mb-6" style={{ color: backstoryTheme.palette.text.primary }}>
Backstory Theme Visualization
@ -188,6 +193,7 @@ const BackstoryThemeVisualizerPage = () => {
</table>
</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 { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import {
Box,
Button,
ButtonProps,
Container,
Paper,
Typography,
@ -37,7 +39,20 @@ const HeroSection = styled(Box)(({ theme }) => ({
},
}));
const HeroButton = styled(Button)(({ theme }) => ({
interface HeroButtonProps extends ButtonProps {
children?: string;
}
const HeroButton = (props: HeroButtonProps) => {
const { children, onClick, ...rest } = props;
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,
@ -48,6 +63,29 @@ const HeroButton = styled(Button)(({ theme }) => ({
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 }) => ({
backgroundColor: theme.palette.action.active,
@ -95,6 +133,8 @@ const FeatureCard = ({
};
const HomePage = () => {
const navigate = useNavigate();
return (<Box sx={{display: "flex", flexDirection: "column"}}>
{/* Hero Section */}
<HeroSection>
@ -251,14 +291,14 @@ const HomePage = () => {
</Box>
</Stack>
<Button
<ActionButton
variant="contained"
color="secondary"
sx={{ mt: 4 }}
endIcon={<ArrowForwardIcon />}
>
Create Your Profile
</Button>
</ActionButton>
</Box>
<Box sx={{ flex: 1 }}>
@ -339,14 +379,14 @@ const HomePage = () => {
</Box>
</Stack>
<Button
<ActionButton
variant="contained"
color="secondary"
sx={{ mt: 4 }}
endIcon={<ArrowForwardIcon />}
>
Start Recruiting
</Button>
</ActionButton>
</Box>
</Box>
</Container>

View File

@ -189,7 +189,7 @@ const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPagePro
setHasFacts(false);
}, [setHasFacts]);
const renderJobDescriptionView = useCallback((sx: SxProps) => {
const renderJobDescriptionView = useCallback((sx?: SxProps) => {
console.log('renderJobDescriptionView');
const jobDescriptionQuestions = [
<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
*/
const renderResumeView = useCallback((sx: SxProps) => {
const renderResumeView = useCallback((sx?: SxProps) => {
const resumeQuestions = [
<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} />
@ -311,7 +311,7 @@ See [About > Resume Generation Architecture](/about/resume-generation) for more
/**
* Renders the fact check view
*/
const renderFactCheckView = useCallback((sx: SxProps) => {
const renderFactCheckView = useCallback((sx?: SxProps) => {
const factsQuestions = [
<Box sx={{ display: "flex", flexDirection: "column" }}>
<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,
overflow: "hidden"
}}>
<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 === 2 ? "flex" : "none" }}>{renderFactCheckView({ 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 === 2 ? "flex" : "none" }}>{renderFactCheckView(/*{ height: "calc(100vh - 72px - 48px)" }*/)}</Box>
</Box>
</Box>
);

View File

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

View File

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

View File

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

View File

@ -278,12 +278,12 @@ class WebServer:
self.setup_routes()
def setup_routes(self):
@self.app.get("/")
async def root():
context = self.create_context(username=defines.default_username)
logger.info(f"Redirecting non-context to {context.id}")
return RedirectResponse(url=f"/{context.id}", status_code=307)
# return JSONResponse({"redirect": f"/{context.id}"})
# @self.app.get("/")
# async def root():
# context = self.create_context(username=defines.default_username)
# logger.info(f"Redirecting non-context to {context.id}")
# return RedirectResponse(url=f"/{context.id}", status_code=307)
# # return JSONResponse({"redirect": f"/{context.id}"})
@self.app.get("/api/umap/entry/{doc_id}/{context_id}")
async def get_umap(doc_id: str, context_id: str, request: Request):
@ -575,7 +575,10 @@ class WebServer:
"first_name": user.first_name,
"last_name": user.last_name,
"full_name": user.full_name,
"description": user.description,
"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],
}
return JSONResponse(user_data)
@ -730,6 +733,20 @@ class WebServer:
logger.error(f"Error in post_chat_endpoint: {e}")
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}")
async def create_user_context(username: str, request: Request):
logger.info(f"{request.method} {request.url.path}")
@ -884,7 +901,7 @@ class WebServer:
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.
Args:
@ -896,9 +913,8 @@ class WebServer:
# Check if the file exists
if not os.path.exists(file_path):
logger.info(f"Context file {file_path} not found. Creating new context.")
self.contexts[context_id] = self.create_context(username=defines.default_username, context_id=context_id)
else:
return None
# Read and deserialize the data
with open(file_path, "r") as f:
content = f.read()
@ -948,21 +964,25 @@ class WebServer:
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()
)
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]
def create_context(self, username: str, context_id=None) -> Context:
"""
Create a new context with a unique ID and default settings.

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)
# Maximum number of entries for ChromaDB to find
default_rag_top_k = 80
default_rag_top_k = 50
# Cosine Distance Equivalent Similarity Retrieval Characteristics
# 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 = ""
last_name: str = ""
full_name: str = ""
description: str = ""
profile_url: str = ""
rag_content_size : int = 0
contact_info : Dict[str, str] = {}
user_questions : List[Question] = []
@ -197,6 +200,8 @@ class User(BaseModel):
self.first_name = info.get("first_name", self.username)
self.last_name = info.get("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", {})
questions = info.get("questions", [ f"Tell me about {self.first_name}.", f"What are {self.first_name}'s professional strengths?"])
self.user_questions = []
@ -226,5 +231,7 @@ class User(BaseModel):
name=self.username,
description=f"Expert data about {self.full_name}.",
))
self.rag_content_size = self.file_watcher.collection.count()
User.model_rebuild()