Looking snazzy

This commit is contained in:
James Ketr 2025-05-19 14:19:05 -07:00
parent 540d286d7a
commit 142c2baac4
17 changed files with 1803 additions and 336 deletions

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

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

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

@ -28,19 +28,21 @@ import { ConversationHandle } from '../Components/Conversation';
import { HomePage } from './Pages/HomePage';
import { HomePage as ChatPage } 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 { calculatePoint } from 'mermaid/dist/utils';
import { connectionBase } from '../Global';
type NavigationLinkType = {
name: string;
@ -153,6 +155,20 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
</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();
@ -161,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]);
@ -170,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]}` : "/";
@ -193,42 +283,44 @@ const BackstoryApp = () => {
minHeight: "calc(100vh - 72px)",
}}>
<BackstoryPageContainer userContext={userContext}>
<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} />} />
{sessionId !== undefined &&
<Routes>
<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 */}
{user.type === 'candidate' && (
<>
<Route path="/profile" element={<ProfilePage />} />
<Route path="/backstory" element={<BackstoryPage />} />
<Route path="/resumes" element={<ResumesPage />} />
<Route path="/qa-setup" element={<QASetupPage />} />
</>
)}
{/* Employer-specific routes */}
{user.type === 'employer' && (
<>
<Route path="/search" element={<SearchPage />} />
<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 />} />
{/* Redirect to BETA by default */}
<Route path="*" element={<BetaPage />} />
</Routes>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/" element={<HomePage />} />
{/* Candidate-specific routes */}
{user.type === 'candidate' && (
<>
<Route path="/profile" element={<ProfilePage />} />
<Route path="/backstory" element={<BackstoryPage />} />
<Route path="/resumes" element={<ResumesPage />} />
<Route path="/qa-setup" element={<QASetupPage />} />
</>
)}
{/* Employer-specific routes */}
{user.type === 'employer' && (
<>
<Route path="/search" element={<SearchPage />} />
<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 />} />
{/* Redirect to BETA by default */}
<Route path="*" element={<BetaPage />} />
</Routes>
}
{location.pathname === "/" && <Footer />}
</BackstoryPageContainer>
</Scrollable>

View File

@ -0,0 +1,84 @@
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';
// 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 }) => {
// 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
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,7 +1,37 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation, useParams } from 'react-router-dom';
import { FileText, Code, Layers, Layout, Activity, Palette, Menu, X } from 'lucide-react';
import { Document } from '../../Components/Document';
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 { Document } from '../Components/Document';
import { BackstoryPageProps } from '../../Components/BackstoryTab';
import { BackstoryUIOverviewPage } from './BackstoryUIOverviewPage';
import { BackstoryAppAnalysisPage } from './BackstoryAppAnalysisPage';
@ -12,29 +42,31 @@ import { MockupPage } from './MockupPage';
const getDocumentIcon = (title: string) => {
switch (title) {
case 'About':
return <FileText className="w-5 h-5" />;
return <DescriptionIcon />;
case 'BETA':
return <Code className="w-5 h-5" />;
return <CodeIcon />;
case 'Resume Generation Architecture':
case 'Application Architecture':
return <Layers className="w-5 h-5" />;
return <LayersIcon />;
case 'UI Overview':
case 'UI Mockup':
return <Layout className="w-5 h-5" />;
return <DashboardIcon />;
case 'Theme Visualizer':
return <Palette className="w-5 h-5" />;
return <PaletteIcon />;
case 'App Analysis':
return <Activity className="w-5 h-5" />;
return <AnalyticsIcon />;
default:
return <FileText className="w-5 h-5" />;
return <ViewQuiltIcon />;
}
};
// Sidebar navigation component with improved styling
// Sidebar navigation component using MUI components
const Sidebar: React.FC<{
currentPage: string;
onDocumentSelect: (docName: string, open: boolean) => void;
}> = ({ currentPage, onDocumentSelect }) => {
onClose?: () => void;
isMobile: boolean;
}> = ({ currentPage, onDocumentSelect, onClose, isMobile }) => {
// Document definitions
const documents = [
{ title: "About", route: "about" },
@ -47,30 +79,73 @@ const Sidebar: React.FC<{
{ title: "UI Mockup", route: "ui-mockup" }
];
const handleItemClick = (route: string) => {
onDocumentSelect(route, true);
if (isMobile && onClose) {
onClose();
}
};
return (
<div className="bg-white rounded-lg shadow-md p-3 h-full">
<h2 className="text-xl font-bold px-2 py-3 border-b mb-4">Documentation</h2>
<nav>
<ul className="space-y-1">
<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) => (
<li key={index}>
<button
onClick={() => onDocumentSelect(doc.route, true)}
className={`w-full text-left px-4 py-3 rounded-md flex items-center transition-colors ${currentPage === doc.route
? 'bg-blue-50 text-blue-700 font-medium shadow-sm'
: 'text-gray-700 hover:bg-gray-50'
}`}
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => handleItemClick(doc.route)}
selected={currentPage === doc.route}
sx={{
borderRadius: 1,
mb: 0.5
}}
>
<span className={`mr-3 ${currentPage === doc.route ? 'text-blue-600' : 'text-gray-500'}`}>
<ListItemIcon sx={{
color: currentPage === doc.route ? 'primary.main' : 'text.secondary',
minWidth: 40
}}>
{getDocumentIcon(doc.title)}
</span>
<span className="text-base">{doc.title}</span>
</button>
</li>
</ListItemIcon>
<ListItemText
primary={doc.title}
slotProps={{
primary: {
fontWeight: currentPage === doc.route ? 'medium' : 'regular',
}
}}
/>
</ListItemButton>
</ListItem>
))}
</ul>
</nav>
</div>
</List>
</Box>
</Box>
);
};
@ -81,7 +156,10 @@ const AboutPage = (props: BackstoryPageProps) => {
const location = useLocation();
const { paramPage = '' } = useParams();
const [page, setPage] = useState<string>(paramPage);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [drawerOpen, setDrawerOpen] = useState(false);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Track location changes
useEffect(() => {
@ -93,6 +171,13 @@ 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 });
@ -110,170 +195,227 @@ const AboutPage = (props: BackstoryPageProps) => {
}
};
// Toggle sidebar on mobile
const toggleSidebar = () => {
setSidebarOpen(!sidebarOpen);
// Toggle mobile drawer
const toggleDrawer = () => {
setDrawerOpen(!drawerOpen);
};
return (
<div className="flex flex-col h-full">
{/* Mobile Toggle Button */}
<button
className="md:hidden flex items-center justify-center gap-2 mx-auto mb-4 px-4 py-2 bg-white rounded-md shadow-sm border text-gray-700 hover:bg-gray-50"
onClick={toggleSidebar}
>
{sidebarOpen ? (
<>
<X size={18} />
<span>Hide Navigation</span>
</>
) : (
<>
<Menu size={18} />
<span>Show Navigation</span>
</>
)}
</button>
// Close the drawer
const closeDrawer = () => {
setDrawerOpen(false);
};
<div className="flex flex-col md:flex-row gap-5 h-full">
{/* Sidebar - hidden on mobile when closed */}
{sidebarOpen && (
<div className="w-full md:w-64 flex-shrink-0 mb-4 md:mb-0">
// 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';
}
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>
);
};
// 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}
/>
</div>
</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 Area */}
<div className="flex-grow">
{page === 'about' && (
<Document
title="About"
filepath="/docs/about.md"
onExpand={(open: boolean) => { onDocumentExpand('about', open); }}
expanded={page === 'about'}
sessionId={sessionId}
submitQuery={submitQuery}
setSnack={setSnack}
/>
)}
{page === 'beta' && (
<Document
title="BETA"
filepath="/docs/beta.md"
onExpand={(open: boolean) => { onDocumentExpand('beta', open); }}
expanded={page === 'beta'}
sessionId={sessionId}
submitQuery={submitQuery}
setSnack={setSnack}
/>
)}
{page === 'resume-generation' && (
<Document
title="Resume Generation Architecture"
filepath="/docs/resume-generation.md"
onExpand={(open: boolean) => { onDocumentExpand('resume-generation', open); }}
expanded={page === 'resume-generation'}
sessionId={sessionId}
submitQuery={submitQuery}
setSnack={setSnack}
/>
)}
{page === 'about-app' && (
<Document
title="Application Architecture"
filepath="/docs/about-app.md"
onExpand={(open: boolean) => { onDocumentExpand('about-app', open); }}
expanded={page === 'about-app'}
sessionId={sessionId}
submitQuery={submitQuery}
setSnack={setSnack}
/>
)}
{page === 'ui-overview' && (
<Document
title="UI Overview"
children={<BackstoryUIOverviewPage />}
onExpand={(open: boolean) => { onDocumentExpand('ui-overview', open); }}
expanded={page === 'ui-overview'}
sessionId={sessionId}
submitQuery={submitQuery}
setSnack={setSnack}
/>
)}
{page === 'theme-visualizer' && (
<Document
title="Theme Visualizer"
onExpand={(open: boolean) => { onDocumentExpand('theme-visualizer', open); }}
expanded={page === 'theme-visualizer'}
children={<BackstoryThemeVisualizerPage />}
sessionId={sessionId}
submitQuery={submitQuery}
setSnack={setSnack}
/>
)}
{page === 'app-analysis' && (
<Document
title="App Analysis"
onExpand={(open: boolean) => { onDocumentExpand('app-analysis', open); }}
expanded={page === 'app-analysis'}
children={<BackstoryAppAnalysisPage />}
sessionId={sessionId}
submitQuery={submitQuery}
setSnack={setSnack}
/>
)}
{page === 'ui-mockup' && (
<Document
title="UI Mockup"
onExpand={(open: boolean) => { onDocumentExpand('ui-mockup', open); }}
expanded={page === 'ui-mockup'}
children={<MockupPage />}
sessionId={sessionId}
submitQuery={submitQuery}
setSnack={setSnack}
/>
)}
{/* Show welcome grid if no specific page is selected */}
{!page && (
<div className="bg-white rounded-lg shadow-md p-6">
<h1 className="text-2xl font-bold mb-3">Documentation</h1>
<p className="text-gray-600 mb-6">
Select a document from the sidebar to view detailed technical information about the application.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[
{ 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) => (
<button
key={index}
onClick={() => onDocumentExpand(doc.route, true)}
className="text-left p-4 bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex items-center mb-2">
<span className="text-blue-600 mr-3">{getDocumentIcon(doc.title)}</span>
<span className="font-medium">{doc.title}</span>
</div>
<p className="text-sm text-gray-500 ml-8">{doc.description}</p>
</button>
))}
</div>
</div>
)}
</div>
</div>
</div>
{/* 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>
);
};

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,263 @@
import React, { useState, useEffect } from 'react';
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';
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 Dashboard",
onReturn,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [showSparkle, setShowSparkle] = useState<boolean>(false);
// 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) {
window.location.href = 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,103 @@
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 <LoadingComponent
loadingText="Fetching user information..."
loaderType="linear"
withFade={true}
fadeDuration={1200} />;
}
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

@ -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):
@ -576,6 +576,8 @@ class WebServer:
"last_name": user.last_name,
"full_name": user.full_name,
"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 +732,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 +900,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,71 +912,74 @@ 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:
# Read and deserialize the data
with open(file_path, "r") as f:
content = f.read()
logger.info(
f"Loading context from {file_path}, content length: {len(content)}"
)
json_data = {}
try:
# Try parsing as JSON first to ensure valid JSON
json_data = json.loads(content)
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}")
return None
# Read and deserialize the data
with open(file_path, "r") as f:
content = f.read()
logger.info(
f"Loading context from {file_path}, content length: {len(content)}"
)
json_data = {}
try:
# Try parsing as JSON first to ensure valid JSON
json_data = json.loads(content)
logger.info("JSON parsed successfully, attempting model validation")
matching_user = next((user for user in self.users if user.username == username), None)
if matching_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
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)
if matching_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
agent_types = [agent.agent_type for agent in context.agents]
if len(agent_types) != len(set(agent_types)):
raise ValueError(
"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()
# Now set context on agents manually
agent_types = [agent.agent_type for agent in context.agents]
if len(agent_types) != len(set(agent_types)):
raise ValueError(
"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)
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:

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("first_name", 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()