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

BIN
frontend/public/profile.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

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

View File

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

View File

@ -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 } from './Pages/HomePage';
import { HomePage as ChatPage } from '../Pages/HomePage'; import { ChatPage } from './Pages/ChatPage';
import { ResumeBuilderPage } from '../Pages/ResumeBuilderPage'; import { ResumeBuilderPage } from '../Pages/ResumeBuilderPage';
// import { BackstoryThemeVisualizer } from './BackstoryThemeVisualizer'; // import { BackstoryThemeVisualizer } from './BackstoryThemeVisualizer';
import { AboutPage } from './Pages/AboutPage'; import { AboutPage } from './Pages/AboutPage';
import { BetaPage } from './Pages/BetaPage'; import { BetaPage } from './Pages/BetaPage';
import { CreateProfilePage } from './Pages/CreateProfilePage'; import { CreateProfilePage } from './Pages/CreateProfilePage';
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
import './BackstoryApp.css';
import '@fontsource/roboto/300.css'; import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css'; import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css'; import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
import { calculatePoint } from 'mermaid/dist/utils'; import { connectionBase } from '../Global';
type NavigationLinkType = { type NavigationLinkType = {
name: string; name: string;
@ -153,6 +155,20 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
</Container> </Container>
); );
} }
// Cookie handling functions
const getCookie = (name: string) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift();
return null;
};
const setCookie = (name: string, value: string, days = 7) => {
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Strict`;
};
const BackstoryApp = () => { const BackstoryApp = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -161,6 +177,7 @@ const BackstoryApp = () => {
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]); const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
const snackRef = useRef<any>(null); const snackRef = useRef<any>(null);
const chatRef = useRef<ConversationHandle>(null); const chatRef = useRef<ConversationHandle>(null);
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
const setSnack = useCallback((message: string, severity?: SeverityType) => { const setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity); snackRef.current?.setSnack(message, severity);
}, [snackRef]); }, [snackRef]);
@ -170,6 +187,79 @@ const BackstoryApp = () => {
navigate('/chat'); navigate('/chat');
}; };
const [page, setPage] = useState<string>(""); const [page, setPage] = useState<string>("");
const [storeInCookie, setStoreInCookie] = useState(true);
// Extract session ID from URL query parameter or cookie
const urlParams = new URLSearchParams(window.location.search);
const urlSessionId = urlParams.get('id');
const cookieSessionId = getCookie('session_id');
// Fetch or join session on mount
useEffect(() => {
const fetchSession = async () => {
try {
let response;
let newSessionId;
if (urlSessionId) {
// Attempt to join session from URL
response = await fetch(`${connectionBase}/join-session/${urlSessionId}`, {
credentials: 'include',
});
if (!response.ok) {
throw new Error('Session not found');
}
newSessionId = (await response.json()).id;
} else if (cookieSessionId) {
// Attempt to join session from cookie
response = await fetch(`${connectionBase}/api/join-session/${cookieSessionId}`, {
credentials: 'include',
});
if (!response.ok) {
// Cookie session invalid, create new session
response = await fetch(`${connectionBase}/api/create-session`, {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to create session');
}
}
newSessionId = (await response.json()).id;
} else {
// Create a new session
response = await fetch(`${connectionBase}/api/create-session`, {
method: 'POST',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to create session');
}
newSessionId = (await response.json()).id;
}
setSessionId(newSessionId);
// Store in cookie if user opts in
if (storeInCookie) {
setCookie('session_id', newSessionId);
}
// Update URL without reloading
if (!storeInCookie || (urlSessionId && urlSessionId !== newSessionId)) {
window.history.replaceState(null, '', `?id=${newSessionId}`);
}
} catch (err) {
setSnack("" + err);
}
};
fetchSession();
}, []);
const copyLink = () => {
const link = `${window.location.origin}${window.location.pathname}?id=${sessionId}`;
navigator.clipboard.writeText(link).then(() => {
alert('Link copied to clipboard!');
}).catch(() => {
alert('Failed to copy link');
});
};
useEffect(() => { useEffect(() => {
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/"; const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
@ -193,15 +283,16 @@ const BackstoryApp = () => {
minHeight: "calc(100vh - 72px)", minHeight: "calc(100vh - 72px)",
}}> }}>
<BackstoryPageContainer userContext={userContext}> <BackstoryPageContainer userContext={userContext}>
{sessionId !== undefined &&
<Routes> <Routes>
<Route path="/chat" element={<ChatPage ref={chatRef} 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={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} /> <Route path="/about" element={<AboutPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/about/:subPage" element={<AboutPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} /> <Route path="/about/:subPage" element={<AboutPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery}/>} /> <Route path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/rag-visualizer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={"684a0c7e-e638-4db7-b00d-0558bfefb710"} submitQuery={submitQuery} />} /> <Route path="/rag-visualizer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/create-your-profile" element={<CreateProfilePage />} /> <Route path="/create-your-profile" element={<CreateProfilePage />} />
<Route path="/dashboard" element={<DashboardPage />} /> <Route path="/dashboard" element={<DashboardPage />} />
<Route path="/" element={<HomePage/>}/> <Route path="/" element={<HomePage />} />
{/* Candidate-specific routes */} {/* Candidate-specific routes */}
{user.type === 'candidate' && ( {user.type === 'candidate' && (
<> <>
@ -229,6 +320,7 @@ const BackstoryApp = () => {
{/* Redirect to BETA by default */} {/* Redirect to BETA by default */}
<Route path="*" element={<BetaPage />} /> <Route path="*" element={<BetaPage />} />
</Routes> </Routes>
}
{location.pathname === "/" && <Footer />} {location.pathname === "/" && <Footer />}
</BackstoryPageContainer> </BackstoryPageContainer>
</Scrollable> </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 React, { useState, useEffect } from 'react';
import { useNavigate, useLocation, useParams } from 'react-router-dom'; import { useNavigate, useLocation, useParams } from 'react-router-dom';
import { FileText, Code, Layers, Layout, Activity, Palette, Menu, X } from 'lucide-react'; import {
import { Document } from '../../Components/Document'; Box,
Drawer,
AppBar,
Toolbar,
IconButton,
Typography,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Paper,
Grid,
Card,
CardContent,
CardActionArea,
Divider,
useTheme,
useMediaQuery
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import CloseIcon from '@mui/icons-material/Close';
import DescriptionIcon from '@mui/icons-material/Description';
import CodeIcon from '@mui/icons-material/Code';
import LayersIcon from '@mui/icons-material/Layers';
import DashboardIcon from '@mui/icons-material/Dashboard';
import PaletteIcon from '@mui/icons-material/Palette';
import AnalyticsIcon from '@mui/icons-material/Analytics';
import ViewQuiltIcon from '@mui/icons-material/ViewQuilt';
import { Document } from '../Components/Document';
import { BackstoryPageProps } from '../../Components/BackstoryTab'; import { BackstoryPageProps } from '../../Components/BackstoryTab';
import { BackstoryUIOverviewPage } from './BackstoryUIOverviewPage'; import { BackstoryUIOverviewPage } from './BackstoryUIOverviewPage';
import { BackstoryAppAnalysisPage } from './BackstoryAppAnalysisPage'; import { BackstoryAppAnalysisPage } from './BackstoryAppAnalysisPage';
@ -12,29 +42,31 @@ import { MockupPage } from './MockupPage';
const getDocumentIcon = (title: string) => { const getDocumentIcon = (title: string) => {
switch (title) { switch (title) {
case 'About': case 'About':
return <FileText className="w-5 h-5" />; return <DescriptionIcon />;
case 'BETA': case 'BETA':
return <Code className="w-5 h-5" />; return <CodeIcon />;
case 'Resume Generation Architecture': case 'Resume Generation Architecture':
case 'Application Architecture': case 'Application Architecture':
return <Layers className="w-5 h-5" />; return <LayersIcon />;
case 'UI Overview': case 'UI Overview':
case 'UI Mockup': case 'UI Mockup':
return <Layout className="w-5 h-5" />; return <DashboardIcon />;
case 'Theme Visualizer': case 'Theme Visualizer':
return <Palette className="w-5 h-5" />; return <PaletteIcon />;
case 'App Analysis': case 'App Analysis':
return <Activity className="w-5 h-5" />; return <AnalyticsIcon />;
default: 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<{ const Sidebar: React.FC<{
currentPage: string; currentPage: string;
onDocumentSelect: (docName: string, open: boolean) => void; onDocumentSelect: (docName: string, open: boolean) => void;
}> = ({ currentPage, onDocumentSelect }) => { onClose?: () => void;
isMobile: boolean;
}> = ({ currentPage, onDocumentSelect, onClose, isMobile }) => {
// Document definitions // Document definitions
const documents = [ const documents = [
{ title: "About", route: "about" }, { title: "About", route: "about" },
@ -47,30 +79,73 @@ const Sidebar: React.FC<{
{ title: "UI Mockup", route: "ui-mockup" } { title: "UI Mockup", route: "ui-mockup" }
]; ];
const handleItemClick = (route: string) => {
onDocumentSelect(route, true);
if (isMobile && onClose) {
onClose();
}
};
return ( return (
<div className="bg-white rounded-lg shadow-md p-3 h-full"> <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<h2 className="text-xl font-bold px-2 py-3 border-b mb-4">Documentation</h2> <Box sx={{
<nav> p: 2,
<ul className="space-y-1"> display: 'flex',
{documents.map((doc, index) => ( alignItems: 'center',
<li key={index}> justifyContent: 'space-between',
<button borderBottom: 1,
onClick={() => onDocumentSelect(doc.route, true)} borderColor: 'divider'
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' <Typography variant="h6" component="h2" fontWeight="bold">
: 'text-gray-700 hover:bg-gray-50' Documentation
}`} </Typography>
{isMobile && onClose && (
<IconButton
onClick={onClose}
size="small"
aria-label="Close navigation"
> >
<span className={`mr-3 ${currentPage === doc.route ? 'text-blue-600' : 'text-gray-500'}`}> <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)} {getDocumentIcon(doc.title)}
</span> </ListItemIcon>
<span className="text-base">{doc.title}</span> <ListItemText
</button> primary={doc.title}
</li> slotProps={{
primary: {
fontWeight: currentPage === doc.route ? 'medium' : 'regular',
}
}}
/>
</ListItemButton>
</ListItem>
))} ))}
</ul> </List>
</nav> </Box>
</div> </Box>
); );
}; };
@ -81,7 +156,10 @@ const AboutPage = (props: BackstoryPageProps) => {
const location = useLocation(); const location = useLocation();
const { paramPage = '' } = useParams(); const { paramPage = '' } = useParams();
const [page, setPage] = useState<string>(paramPage); const [page, setPage] = useState<string>(paramPage);
const [sidebarOpen, setSidebarOpen] = useState(true); const [drawerOpen, setDrawerOpen] = useState(false);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Track location changes // Track location changes
useEffect(() => { useEffect(() => {
@ -93,6 +171,13 @@ const AboutPage = (props: BackstoryPageProps) => {
} }
}, [location]); }, [location]);
// Close drawer when changing to desktop view
useEffect(() => {
if (!isMobile) {
setDrawerOpen(false);
}
}, [isMobile]);
// Handle document navigation // Handle document navigation
const onDocumentExpand = (docName: string, open: boolean) => { const onDocumentExpand = (docName: string, open: boolean) => {
console.log("Document expanded:", { docName, open, location }); console.log("Document expanded:", { docName, open, location });
@ -110,142 +195,89 @@ const AboutPage = (props: BackstoryPageProps) => {
} }
}; };
// Toggle sidebar on mobile // Toggle mobile drawer
const toggleSidebar = () => { const toggleDrawer = () => {
setSidebarOpen(!sidebarOpen); 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';
}
interface DocViewProps {
page: string
};
const DocView = (props: DocViewProps) => {
const { page } = props;
const title = documentTitleFromRoute(page);
const icon = getDocumentIcon(title);
return ( return (
<div className="flex flex-col h-full"> <Card>
{/* Mobile Toggle Button */} <CardContent>
<button <Box sx={{ color: 'inherit', fontSize: "1.75rem", fontWeight: "bold", display: "flex", flexDirection: "row", gap: 1, alignItems: "center", mr: 1.5 }}>
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" {icon}
onClick={toggleSidebar} {title}
> </Box>
{sidebarOpen ? ( <Document
<> filepath={`/docs/${page}.md`}
<X size={18} /> sessionId={sessionId}
<span>Hide Navigation</span> submitQuery={submitQuery}
</> setSnack={setSnack}
) : ( />
<> </CardContent>
<Menu size={18} /> </Card>
<span>Show Navigation</span> );
</> };
)}
</button>
<div className="flex flex-col md:flex-row gap-5 h-full"> // Render the appropriate content based on current page
{/* Sidebar - hidden on mobile when closed */} function renderContent() {
{sidebarOpen && ( switch (page) {
<div className="w-full md:w-64 flex-shrink-0 mb-4 md:mb-0"> case 'about':
<Sidebar return <DocView page={page} />
currentPage={page} case 'beta':
onDocumentSelect={onDocumentExpand} return <DocView page={page} />
/> case 'resume-generation':
</div> return <DocView page={page} />
)} case 'about-app':
return <DocView page={page} />
{/* Main Content Area */} case 'ui-overview':
<div className="flex-grow"> return (<BackstoryUIOverviewPage />);
{page === 'about' && ( case 'theme-visualizer':
<Document return (<Paper sx={{ m: 0, p: 1 }}><BackstoryThemeVisualizerPage /></Paper>);
title="About" case 'app-analysis':
filepath="/docs/about.md" return (<BackstoryAppAnalysisPage />);
onExpand={(open: boolean) => { onDocumentExpand('about', open); }} case 'ui-mockup':
expanded={page === 'about'} return (<MockupPage />);
sessionId={sessionId} default:
submitQuery={submitQuery} // Document grid for landing page
setSnack={setSnack} return (
/> <Paper sx={{ p: 3 }} elevation={1}>
)} <Typography variant="h4" component="h1" gutterBottom>
{page === 'beta' && ( Documentation
<Document </Typography>
title="BETA" <Typography variant="body1" color="text.secondary" paragraph>
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. Select a document from the sidebar to view detailed technical information about the application.
</p> </Typography>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <Grid container spacing={2}>
{[ {[
{ title: "About", route: "about", description: "General information about the application and its purpose" }, { 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: "BETA", route: "beta", description: "Details about the current beta version and upcoming features" },
@ -256,24 +288,134 @@ const AboutPage = (props: BackstoryPageProps) => {
{ title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application" }, { 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" } { title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts" }
].map((doc, index) => ( ].map((doc, index) => (
<button <Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
key={index} <Card>
onClick={() => onDocumentExpand(doc.route, true)} <CardActionArea onClick={() => onDocumentExpand(doc.route, true)}>
className="text-left p-4 bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow" <CardContent>
> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<div className="flex items-center mb-2"> <Box sx={{ color: 'primary.main', mr: 1.5 }}>
<span className="text-blue-600 mr-3">{getDocumentIcon(doc.title)}</span> {getDocumentIcon(doc.title)}
<span className="font-medium">{doc.title}</span> </Box>
</div> <Typography variant="h6">{doc.title}</Typography>
<p className="text-sm text-gray-500 ml-8">{doc.description}</p> </Box>
</button> <Typography variant="body2" color="text.secondary" sx={{ ml: 5 }}>
{doc.description}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Grid>
))} ))}
</div> </Grid>
</div> </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>
)} )}
</div>
</div> {/* Navigation drawer */}
</div> <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>
); );
}; };

View File

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

View File

@ -0,0 +1,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() self.setup_routes()
def setup_routes(self): def setup_routes(self):
@self.app.get("/") # @self.app.get("/")
async def root(): # async def root():
context = self.create_context(username=defines.default_username) # context = self.create_context(username=defines.default_username)
logger.info(f"Redirecting non-context to {context.id}") # logger.info(f"Redirecting non-context to {context.id}")
return RedirectResponse(url=f"/{context.id}", status_code=307) # return RedirectResponse(url=f"/{context.id}", status_code=307)
# return JSONResponse({"redirect": f"/{context.id}"}) # # return JSONResponse({"redirect": f"/{context.id}"})
@self.app.get("/api/umap/entry/{doc_id}/{context_id}") @self.app.get("/api/umap/entry/{doc_id}/{context_id}")
async def get_umap(doc_id: str, context_id: str, request: Request): async def get_umap(doc_id: str, context_id: str, request: Request):
@ -576,6 +576,8 @@ class WebServer:
"last_name": user.last_name, "last_name": user.last_name,
"full_name": user.full_name, "full_name": user.full_name,
"contact_info": user.contact_info, "contact_info": user.contact_info,
"rag_content_size": user.rag_content_size,
"profile_url": user.profile_url,
"questions": [ q.model_dump(mode='json') for q in user.user_questions], "questions": [ q.model_dump(mode='json') for q in user.user_questions],
} }
return JSONResponse(user_data) return JSONResponse(user_data)
@ -730,6 +732,20 @@ class WebServer:
logger.error(f"Error in post_chat_endpoint: {e}") logger.error(f"Error in post_chat_endpoint: {e}")
return JSONResponse({"error": str(e)}, status_code=500) return JSONResponse({"error": str(e)}, status_code=500)
@self.app.post("/api/create-session")
async def create_session(request: Request):
logger.info(f"{request.method} {request.url.path}")
context = self.create_context(username=defines.default_username)
return JSONResponse({"id": context.id})
@self.app.get("/api/join-session/{context_id}")
async def join_session(context_id: str, request: Request):
logger.info(f"{request.method} {request.url.path}")
context = self.load_context(context_id=context_id)
if not context:
return JSONResponse({"error": f"{context_id} does not exist."}, 404)
return JSONResponse({"id": context.id})
@self.app.post("/api/context/u/{username}") @self.app.post("/api/context/u/{username}")
async def create_user_context(username: str, request: Request): async def create_user_context(username: str, request: Request):
logger.info(f"{request.method} {request.url.path}") logger.info(f"{request.method} {request.url.path}")
@ -884,7 +900,7 @@ class WebServer:
return context_id return context_id
def load_or_create_context(self, context_id: str) -> Context: def load_context(self, context_id: str) -> Context | None:
""" """
Load a context from a file in the context directory or create a new one if it doesn't exist. Load a context from a file in the context directory or create a new one if it doesn't exist.
Args: Args:
@ -896,9 +912,8 @@ class WebServer:
# Check if the file exists # Check if the file exists
if not os.path.exists(file_path): if not os.path.exists(file_path):
logger.info(f"Context file {file_path} not found. Creating new context.") return None
self.contexts[context_id] = self.create_context(username=defines.default_username, context_id=context_id)
else:
# Read and deserialize the data # Read and deserialize the data
with open(file_path, "r") as f: with open(file_path, "r") as f:
content = f.read() content = f.read()
@ -948,21 +963,25 @@ class WebServer:
for key in json_data: 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(f"{key} = {type(json_data[key])} {str(json_data[key])[:60] if json_data[key] else "None"}")
logger.info("*" * 50) logger.info("*" * 50)
if len(self.users) == 0: return None
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 self.contexts[context_id] 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: def create_context(self, username: str, context_id=None) -> Context:
""" """
Create a new context with a unique ID and default settings. Create a new context with a unique ID and default settings.

View File

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