New UI flows...

This commit is contained in:
James Ketr 2025-05-19 20:31:30 -07:00
parent 23a48738a3
commit 2e6a8d1366
11 changed files with 2636 additions and 252 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,23 @@
import { useState } from 'react';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import CheckIcon from '@mui/icons-material/Check';
import IconButton from '@mui/material/IconButton';
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
import { Tooltip } from '@mui/material';
import { SxProps, Theme } from '@mui/material';
interface CopyBubbleProps {
interface CopyBubbleProps extends IconButtonProps {
content: string | undefined,
sx?: SxProps<Theme>;
tooltip?: string;
onClick?: () => void;
}
const CopyBubble = ({
content,
sx,
tooltip = "Copy to clipboard",
onClick,
...rest
} : CopyBubbleProps) => {
const [copied, setCopied] = useState(false);
@ -25,10 +30,14 @@ const CopyBubble = ({
setCopied(true);
setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds
});
if (onClick) {
onClick();
}
};
return (
<Tooltip title="Copy to clipboard" placement="top" arrow>
<Tooltip title={tooltip} placement="top" arrow>
<IconButton
onClick={handleCopy}
sx={{
@ -41,6 +50,7 @@ return (
}}
size="small"
color={copied ? "success" : "default"}
{...rest}
>
{copied ? <CheckIcon sx={{ width: 16, height: 16 }} /> : <ContentCopyIcon sx={{ width: 16, height: 16 }} />}
</IconButton>

View File

@ -1,39 +1,16 @@
import React, { ReactElement, useEffect, useState, useRef, useCallback} from 'react';
import { Box, Typography, Container, Paper } from '@mui/material';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import DashboardIcon from '@mui/icons-material/Dashboard';
import PersonIcon from '@mui/icons-material/Person';
import ChatIcon from '@mui/icons-material/Chat';
import HistoryIcon from '@mui/icons-material/History';
import DescriptionIcon from '@mui/icons-material/Description';
import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer';
import BarChartIcon from '@mui/icons-material/BarChart';
import SettingsIcon from '@mui/icons-material/Settings';
import SearchIcon from '@mui/icons-material/Search';
import BookmarkIcon from '@mui/icons-material/Bookmark';
import WorkIcon from '@mui/icons-material/Work';
import InfoIcon from '@mui/icons-material/Info';
import BusinessIcon from '@mui/icons-material/Business';
import { SxProps, Theme } from '@mui/material';
import { ThemeProvider } from '@mui/material/styles';
import { backstoryTheme } from '../BackstoryTheme';
import {Header} from './Components/Header';
import { Scrollable } from '../Components/Scrollable';
import { Footer } from './Components/Footer';
import { Snack, SeverityType } from '../Components/Snack';
import { SeverityType } from '../Components/Snack';
import { Query } from '../Components/ChatQuery';
import { ConversationHandle } from './Components/Conversation';
import { HomePage } from './Pages/HomePage';
import { ChatPage } from './Pages/ChatPage';
import { ResumeBuilderPage } from '../Pages/ResumeBuilderPage';
// import { BackstoryThemeVisualizer } from './BackstoryThemeVisualizer';
import { DocsPage } from './Pages/DocsPage';
import { UserProvider } from './Components/UserContext';
import { BetaPage } from './Pages/BetaPage';
import { CreateProfilePage } from './Pages/CreateProfilePage';
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
import { UserRoute } from './routes/UserRoute';
import { BackstoryLayout } from './Components/BackstoryLayout';
import './BackstoryApp.css';
import '@fontsource/roboto/300.css';
@ -43,118 +20,6 @@ import '@fontsource/roboto/700.css';
import { connectionBase } from '../Global';
type NavigationLinkType = {
name: string;
path: string;
icon?: ReactElement<any>;
label?: ReactElement<any>;
};
const DefaultNavItems : NavigationLinkType[] = [
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
{ name: 'Resume Builder', path: '/resume-builder', icon: <WorkIcon /> },
{ name: 'Knowledge Explorer', path: '/knowledge-explorer', icon: <WorkIcon /> },
{ name: 'Docs', path: '/docs', icon: <InfoIcon /> },
// { name: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
// { name: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
// { name: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
// { name: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
];
const CandidateNavItems : NavigationLinkType[]= [
{ name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' },
{ name: 'Profile', icon: <PersonIcon />, path: '/profile' },
{ name: 'Backstory', icon: <HistoryIcon />, path: '/backstory' },
{ name: 'Resumes', icon: <DescriptionIcon />, path: '/resumes' },
{ name: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/qa-setup' },
{ name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' },
{ name: 'Settings', icon: <SettingsIcon />, path: '/settings' },
];
const EmployerNavItems: NavigationLinkType[] = [
{ name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' },
{ name: 'Search', icon: <SearchIcon />, path: '/search' },
{ name: 'Saved', icon: <BookmarkIcon />, path: '/saved' },
{ name: 'Jobs', icon: <WorkIcon />, path: '/jobs' },
{ name: 'Company', icon: <BusinessIcon />, path: '/company' },
{ name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' },
{ name: 'Settings', icon: <SettingsIcon />, path: '/settings' },
];
// Navigation links based on user type
const getNavigationLinks = (userType: string, isLoggedIn: boolean) : NavigationLinkType[] => {
if (!isLoggedIn) {
return DefaultNavItems;
}
if (userType === 'candidate') {
return CandidateNavItems;
}
// Employer navigation
return EmployerNavItems;
};
// Placeholder for page components
const DashboardPage = () => <Box p={3}><Typography variant="h4">Dashboard</Typography></Box>;
const ProfilePage = () => <Box p={3}><Typography variant="h4">Profile</Typography></Box>;
const BackstoryPage = () => <Box p={3}><Typography variant="h4">Backstory</Typography></Box>;
const ResumesPage = () => <Box p={3}><Typography variant="h4">Resumes</Typography></Box>;
const QASetupPage = () => <Box p={3}><Typography variant="h4">Q&A Setup</Typography></Box>;
const AnalyticsPage = () => <Box p={3}><Typography variant="h4">Analytics</Typography></Box>;
const SettingsPage = () => <Box p={3}><Typography variant="h4">Settings</Typography></Box>;
const SearchPage = () => <Box p={3}><Typography variant="h4">Search</Typography></Box>;
const SavedPage = () => <Box p={3}><Typography variant="h4">Saved</Typography></Box>;
const JobsPage = () => <Box p={3}><Typography variant="h4">Jobs</Typography></Box>;
const CompanyPage = () => <Box p={3}><Typography variant="h4">Company</Typography></Box>;
// This is a placeholder for your actual user context
type UserContext = {
user: {
type: string;
name: string;
avatar?: any;
};
isAuthenticated: boolean;
logout: () => void;
};
const useUserContext = () : UserContext => {
return {
user: {
type: 'candidate', // or 'employer'
name: 'John Doe',
avatar: null,
},
isAuthenticated: false,
logout: () => console.log('Logging out'),
};
};
interface BackstoryPageContainerProps {
children?: React.ReactNode;
sx?: SxProps<Theme>;
userContext: UserContext;
};
const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
const { children, sx } = props;
return (
<Container maxWidth="xl" sx={{ mt: 2, mb: 2, ...sx }}>
<Paper
elevation={2}
sx={{
p: 3,
backgroundColor: 'background.paper',
borderRadius: 2,
minHeight: '80vh',
}}>
{children}
</Paper>
</Container>
);
}
// Cookie handling functions
const getCookie = (name: string) => {
const value = `; ${document.cookie}`;
@ -171,9 +36,6 @@ const setCookie = (name: string, value: string, days = 7) => {
const BackstoryApp = () => {
const navigate = useNavigate();
const location = useLocation();
const userContext : UserContext = useUserContext();
const { user, isAuthenticated } = userContext;
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
const snackRef = useRef<any>(null);
const chatRef = useRef<ConversationHandle>(null);
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
@ -199,15 +61,17 @@ const BackstoryApp = () => {
try {
let response;
let newSessionId;
let action = ""
if (urlSessionId) {
// Attempt to join session from URL
response = await fetch(`${connectionBase}/join-session/${urlSessionId}`, {
response = await fetch(`${connectionBase}/api/join-session/${urlSessionId}`, {
credentials: 'include',
});
if (!response.ok) {
throw new Error('Session not found');
}
newSessionId = (await response.json()).id;
action = "Joined";
} else if (cookieSessionId) {
// Attempt to join session from cookie
response = await fetch(`${connectionBase}/api/join-session/${cookieSessionId}`, {
@ -222,6 +86,9 @@ const BackstoryApp = () => {
if (!response.ok) {
throw new Error('Failed to create session');
}
action = "Created new";
} else {
action = "Joined";
}
newSessionId = (await response.json()).id;
} else {
@ -233,6 +100,7 @@ const BackstoryApp = () => {
if (!response.ok) {
throw new Error('Failed to create session');
}
action = "Created new";
newSessionId = (await response.json()).id;
}
setSessionId(newSessionId);
@ -241,9 +109,14 @@ const BackstoryApp = () => {
setCookie('session_id', newSessionId);
}
// Update URL without reloading
if (!storeInCookie || (urlSessionId && urlSessionId !== newSessionId)) {
window.history.replaceState(null, '', `?id=${newSessionId}`);
if (!storeInCookie) {
// Update only the 'id' query parameter, preserving the current path
navigate(`${location.pathname}?id=${newSessionId}`, { replace: true });
} else {
// Clear all query parameters, preserve the current path
navigate(location.pathname, { replace: true });
}
setSnack(`${action} session ${newSessionId}`);
} catch (err) {
setSnack("" + err);
}
@ -251,89 +124,40 @@ const BackstoryApp = () => {
fetchSession();
}, [cookieSessionId, setSnack, storeInCookie, urlSessionId]);
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]}` : "/";
setPage(currentRoute);
}, [location.pathname]);
useEffect(() => {
setNavigationLinks(getNavigationLinks(user.type, isAuthenticated));
}, [user.type, isAuthenticated]);
// Render appropriate routes based on user type
return (
<ThemeProvider theme={backstoryTheme}>
<Header currentPath={page} navigate={navigate} navigationLinks={navigationLinks} showLogin={false}/>
<Box sx={{ display: "flex", minHeight: "72px", height: "72px" }}/>
<Scrollable sx={{
display: 'flex',
flexDirection: 'column',
backgroundColor: 'background.default',
maxHeight: "calc(100vh - 72px)",
minHeight: "calc(100vh - 72px)",
}}>
<BackstoryPageContainer userContext={userContext}>
{sessionId !== undefined &&
<Routes>
<Route path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/docs" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/docs/:subPage" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
<Route path="/knowledge-explorer" 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 />} />
</>
)}
<UserProvider>
<Routes>
<Route path="/u/:user" element={<UserRoute />} />
{/* Static/shared routes */}
<Route
path="/*"
element={
<BackstoryLayout
sessionId={sessionId}
setSnack={setSnack}
page={page}
chatRef={chatRef}
snackRef={snackRef}
submitQuery={submitQuery}
/>
}
/>
<Route path="*" element={<BetaPage />} />
</Routes>
</UserProvider>
{/* 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>
<Snack ref={snackRef}/>
</ThemeProvider>
);
};
export type {
UserContext,
NavigationLinkType
};
export {
BackstoryApp
};

View File

@ -0,0 +1,71 @@
import React, { Ref, Fragment, ReactNode } from "react";
import { Route } from "react-router-dom";
import { useUser } from "./UserContext";
import { Box, Typography, Container, Paper } from '@mui/material';
import { BackstoryPageProps } from '../../Components/BackstoryTab';
import { ConversationHandle } from '../Components/Conversation';
import { UserType } from '../Components/UserContext';
import { ChatPage } from '../Pages/ChatPage';
import { ResumeBuilderPage } from '../../Pages/ResumeBuilderPage';
import { DocsPage } from '../Pages/DocsPage';
import { CreateProfilePage } from '../Pages/CreateProfilePage';
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
import { HomePage } from '../Pages/HomePage';
import { BetaPage } from '../Pages/BetaPage'
const DashboardPage = () => <BetaPage><Typography variant="h4">Dashboard</Typography></BetaPage>;
const ProfilePage = () => <BetaPage><Typography variant="h4">Profile</Typography></BetaPage>;
const BackstoryPage = () => <BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>;
const ResumesPage = () => <BetaPage><Typography variant="h4">Resumes</Typography></BetaPage>;
const QASetupPage = () => <BetaPage><Typography variant="h4">Q&A Setup</Typography></BetaPage>;
const AnalyticsPage = () => <BetaPage><Typography variant="h4">Analytics</Typography></BetaPage>;
const SettingsPage = () => <BetaPage><Typography variant="h4">Settings</Typography></BetaPage>;
const SearchPage = () => <BetaPage><Typography variant="h4">Search</Typography></BetaPage>;
const SavedPage = () => <BetaPage><Typography variant="h4">Saved</Typography></BetaPage>;
const JobsPage = () => <BetaPage><Typography variant="h4">Jobs</Typography></BetaPage>;
const CompanyPage = () => <BetaPage><Typography variant="h4">Company</Typography></BetaPage>;
interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
chatRef: Ref<ConversationHandle>
}
const getBackstoryDynamicRoutes = (props : BackstoryDynamicRoutesProps, user?: UserType | null) : ReactNode => {
const { sessionId, setSnack, submitQuery, chatRef } = props;
const routes = [
<Route key="backstory" path="/" element={<HomePage/>} />,
<Route key="chat" path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key="docs" path="/docs" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key="docs-sub" path="/docs/:subPage" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key="resume-builder" path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key="vector" path="/knowledge-explorer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key="create-profile" path="/create-your-profile" element={<CreateProfilePage />} />,
];
if (user === undefined || user === null) {
return routes;
}
if (user.type === "candidate") {
routes.splice(-1, 0, ...[
<Route path="/profile" element={<ProfilePage />} />,
<Route path="/backstory" element={<BackstoryPage />} />,
<Route path="/resumes" element={<ResumesPage />} />,
<Route path="/qa-setup" element={<QASetupPage />} />,
]);
}
if (user.type === "employer") {
routes.splice(-1, 0, ...[
<Route path="/search" element={<SearchPage />} />,
<Route path="/saved" element={<SavedPage />} />,
<Route path="/jobs" element={<JobsPage />} />,
<Route path="/company" element={<CompanyPage />} />,
]);
}
return routes;
};
export { getBackstoryDynamicRoutes };

View File

@ -0,0 +1,160 @@
import React, { ReactElement, useEffect, useState, useRef, useCallback, createContext, useContext } from 'react';
import { Outlet, useLocation, Routes } from "react-router-dom";
import { Box, Container, Paper } from '@mui/material';
import { useNavigate } from "react-router-dom";
import DashboardIcon from '@mui/icons-material/Dashboard';
import PersonIcon from '@mui/icons-material/Person';
import ChatIcon from '@mui/icons-material/Chat';
import HistoryIcon from '@mui/icons-material/History';
import DescriptionIcon from '@mui/icons-material/Description';
import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer';
import BarChartIcon from '@mui/icons-material/BarChart';
import SettingsIcon from '@mui/icons-material/Settings';
import SearchIcon from '@mui/icons-material/Search';
import BookmarkIcon from '@mui/icons-material/Bookmark';
import WorkIcon from '@mui/icons-material/Work';
import InfoIcon from '@mui/icons-material/Info';
import BusinessIcon from '@mui/icons-material/Business';
import { SxProps, Theme } from '@mui/material';
import {Header} from './Header';
import { Scrollable } from '../../Components/Scrollable';
import { Footer } from './Footer';
import { Snack } from '../../Components/Snack';
import { UserProvider, useUser, UserType } from './UserContext';
import { getBackstoryDynamicRoutes } from './BackstoryDynamicRoutes';
type NavigationLinkType = {
name: string;
path: string;
icon?: ReactElement<any>;
label?: ReactElement<any>;
};
const DefaultNavItems : NavigationLinkType[] = [
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
{ name: 'Resume Builder', path: '/resume-builder', icon: <WorkIcon /> },
{ name: 'Knowledge Explorer', path: '/knowledge-explorer', icon: <WorkIcon /> },
{ name: 'Docs', path: '/docs', icon: <InfoIcon /> },
// { name: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
// { name: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
// { name: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
// { name: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
];
const CandidateNavItems : NavigationLinkType[]= [
{ name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' },
{ name: 'Profile', icon: <PersonIcon />, path: '/profile' },
{ name: 'Backstory', icon: <HistoryIcon />, path: '/backstory' },
{ name: 'Resumes', icon: <DescriptionIcon />, path: '/resumes' },
{ name: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/qa-setup' },
{ name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' },
{ name: 'Settings', icon: <SettingsIcon />, path: '/settings' },
];
const EmployerNavItems: NavigationLinkType[] = [
{ name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' },
{ name: 'Search', icon: <SearchIcon />, path: '/search' },
{ name: 'Saved', icon: <BookmarkIcon />, path: '/saved' },
{ name: 'Jobs', icon: <WorkIcon />, path: '/jobs' },
{ name: 'Company', icon: <BusinessIcon />, path: '/company' },
{ name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' },
{ name: 'Settings', icon: <SettingsIcon />, path: '/settings' },
];
// Navigation links based on user type
const getNavigationLinks = (user: UserType | null) : NavigationLinkType[] => {
if (!user) {
return DefaultNavItems;
}
if (user.type === 'candidate') {
return CandidateNavItems;
}
// Employer navigation
return EmployerNavItems;
};
interface BackstoryPageContainerProps {
children?: React.ReactNode;
sx?: SxProps<Theme>;
};
const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
const { children, sx } = props;
return (
<Container maxWidth="xl" sx={{ mt: 2, mb: 2, ...sx }}>
<Paper
elevation={2}
sx={{
p: 3,
backgroundColor: 'background.paper',
borderRadius: 2,
minHeight: '80vh',
}}>
{children}
</Paper>
</Container>
);
}
const BackstoryLayout: React.FC<{
sessionId?: string;
setSnack: (msg: string) => void;
page: string;
chatRef: React.Ref<any>;
snackRef: React.Ref<any>;
submitQuery: any;
}> = ({ sessionId, setSnack, page, chatRef, snackRef, submitQuery }) => {
const navigate = useNavigate();
const location = useLocation();
const { user } = useUser();
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
useEffect(() => {
setNavigationLinks(getNavigationLinks(user));
}, [user]);
let dynamicRoutes;
if (sessionId) {
dynamicRoutes = getBackstoryDynamicRoutes({
sessionId,
setSnack,
submitQuery,
chatRef
}, user);
}
return (
<>
<Header setSnack={setSnack} sessionId={sessionId} user={user} currentPath={page} navigate={navigate} navigationLinks={navigationLinks} showLogin={false} />
<Box sx={{ display: "flex", minHeight: "72px", height: "72px" }} />
<Scrollable
sx={{
display: "flex",
flexDirection: "column",
backgroundColor: "background.default",
maxHeight: "calc(100vh - 72px)",
minHeight: "calc(100vh - 72px)",
}}
>
<BackstoryPageContainer>
<Outlet />
{dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>}
{location.pathname === "/" && <Footer />}
</BackstoryPageContainer>
</Scrollable>
<Snack ref={snackRef} />
</>
);
};
export type {
NavigationLinkType
};
export {
BackstoryLayout
};

View File

@ -0,0 +1,14 @@
.Conversation {
display: flex;
background-color: #F5F5F5;
border: 1px solid #E0E0E0;
flex-grow: 1;
padding: 10px;
flex-direction: column;
font-size: 0.9rem;
width: 100%;
margin: 0 auto;
overflow-y: auto;
height: calc(100vh - 72px);
}

View File

@ -0,0 +1,573 @@
import React, { useState, useImperativeHandle, forwardRef, useEffect, useRef, useCallback } from 'react';
import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import SendIcon from '@mui/icons-material/Send';
import CancelIcon from '@mui/icons-material/Cancel';
import { SxProps, Theme } from '@mui/material';
import PropagateLoader from "react-spinners/PropagateLoader";
import { Message, MessageList, BackstoryMessage } from '../../Components/Message';
import { DeleteConfirmation } from '../../Components/DeleteConfirmation';
import { Query } from '../../Components/ChatQuery';
import { BackstoryTextField, BackstoryTextFieldRef } from '../../Components/BackstoryTextField';
import { BackstoryElementProps } from '../../Components/BackstoryTab';
import { connectionBase } from '../../Global';
import './Conversation.css';
const loadingMessage: BackstoryMessage = { "role": "status", "content": "Establishing connection with server..." };
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check';
interface ConversationHandle {
submitQuery: (query: Query) => void;
fetchHistory: () => void;
}
interface ConversationProps extends BackstoryElementProps {
className?: string, // Override default className
type: ConversationMode, // Type of Conversation chat
placeholder?: string, // Prompt to display in TextField input
actionLabel?: string, // Label to put on the primary button
resetAction?: () => void, // Callback when Reset is pressed
resetLabel?: string, // Label to put on Reset button
defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField
defaultQuery?: string, // Default text to populate the TextField input
preamble?: MessageList, // Messages to display at start of Conversation until Action has been invoked
hidePreamble?: boolean, // Whether to hide the preamble after an Action has been invoked
hideDefaultPrompts?: boolean, // Whether to hide the defaultPrompts after an Action has been invoked
messageFilter?: ((messages: MessageList) => MessageList) | undefined, // Filter callback to determine which Messages to display in Conversation
messages?: MessageList, //
sx?: SxProps<Theme>,
onResponse?: ((message: BackstoryMessage) => void) | undefined, // Event called when a query completes (provides messages)
};
const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: ConversationProps, ref) => {
const {
sessionId,
actionLabel,
className,
defaultPrompts,
defaultQuery,
hideDefaultPrompts,
hidePreamble,
messageFilter,
messages,
onResponse,
placeholder,
preamble,
resetAction,
resetLabel,
setSnack,
submitQuery,
sx,
type,
} = props;
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
const [processing, setProcessing] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0);
const [conversation, setConversation] = useState<MessageList>([]);
const [filteredConversation, setFilteredConversation] = useState<MessageList>([]);
const [processingMessage, setProcessingMessage] = useState<BackstoryMessage | undefined>(undefined);
const [streamingMessage, setStreamingMessage] = useState<BackstoryMessage | undefined>(undefined);
const timerRef = useRef<any>(null);
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
const [noInteractions, setNoInteractions] = useState<boolean>(true);
const conversationRef = useRef<MessageList>([]);
const viewableElementRef = useRef<HTMLDivElement>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const stopRef = useRef(false);
// Keep the ref updated whenever items changes
useEffect(() => {
conversationRef.current = conversation;
}, [conversation]);
// Update the context status
/* Transform the 'Conversation' by filtering via callback, then adding
* preamble and messages based on whether the conversation
* has any elements yet */
useEffect(() => {
let filtered = [];
if (messageFilter === undefined) {
filtered = conversation;
// console.log('No message filter provided. Using all messages.', filtered);
} else {
//console.log('Filtering conversation...')
filtered = messageFilter(conversation); /* Do not copy conversation or useEffect will loop forever */
//console.log(`${conversation.length - filtered.length} messages filtered out.`);
}
if (filtered.length === 0) {
setFilteredConversation([
...(preamble || []),
...(messages || []),
]);
} else {
setFilteredConversation([
...(hidePreamble ? [] : (preamble || [])),
...(messages || []),
...filtered,
]);
};
}, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]);
const fetchHistory = useCallback(async () => {
let retries = 5;
while (--retries > 0) {
try {
const response = await fetch(connectionBase + `/api/history/${sessionId}/${type}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
}
const { messages } = await response.json();
if (messages === undefined || messages.length === 0) {
console.log(`History returned for ${type} from server with 0 entries`)
setConversation([])
setNoInteractions(true);
} else {
console.log(`History returned for ${type} from server with ${messages.length} entries:`, messages)
const backstoryMessages: BackstoryMessage[] = messages;
setConversation(backstoryMessages.flatMap((backstoryMessage: BackstoryMessage) => {
if (backstoryMessage.status === "partial") {
return [{
...backstoryMessage,
role: "assistant",
content: backstoryMessage.response || "",
expanded: false,
expandable: true,
}]
}
return [{
role: 'user',
content: backstoryMessage.prompt || "",
}, {
...backstoryMessage,
role: ['done'].includes(backstoryMessage.status || "") ? "assistant" : backstoryMessage.status,
content: backstoryMessage.response || "",
}] as MessageList;
}));
setNoInteractions(false);
}
setProcessingMessage(undefined);
setStreamingMessage(undefined);
return;
} catch (error) {
console.error('Error generating session ID:', error);
setProcessingMessage({ role: "error", content: `Unable to obtain history from server. Retrying in 3 seconds (${retries} remain.)` });
setTimeout(() => {
setProcessingMessage(undefined);
}, 3000);
await new Promise(resolve => setTimeout(resolve, 3000));
setSnack("Unable to obtain chat history.", "error");
}
};
}, [setConversation,setSnack, type, sessionId]);
// Set the initial chat history to "loading" or the welcome message if loaded.
useEffect(() => {
if (sessionId === undefined) {
setProcessingMessage(loadingMessage);
return;
}
fetchHistory();
}, [fetchHistory, sessionId, setProcessing]);
const startCountdown = (seconds: number) => {
if (timerRef.current) clearInterval(timerRef.current);
setCountdown(seconds);
timerRef.current = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timerRef.current);
timerRef.current = null;
return 0;
}
return prev - 1;
});
}, 1000);
};
const stopCountdown = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
setCountdown(0);
}
};
const handleEnter = (value: string) => {
const query: Query = {
prompt: value
}
sendQuery(query);
};
useImperativeHandle(ref, () => ({
submitQuery: (query: Query) => {
sendQuery(query);
},
fetchHistory: () => { return fetchHistory(); }
}));
const reset = async () => {
try {
const response = await fetch(connectionBase + `/api/reset/${sessionId}/${type}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({ reset: ['history'] })
});
if (!response.ok) {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
}
if (!response.body) {
throw new Error('Response body is null');
}
setProcessingMessage(undefined);
setStreamingMessage(undefined);
setConversation([]);
setNoInteractions(true);
} catch (e) {
setSnack("Error resetting history", "error")
console.error('Error resetting history:', e);
}
};
const cancelQuery = () => {
console.log("Stop query");
stopRef.current = true;
};
const sendQuery = async (query: Query) => {
query.prompt = query.prompt.trim();
// If the request was empty, a default request was provided,
// and there is no prompt for the user, send the default request.
if (!query.prompt && defaultQuery && !prompt) {
query.prompt = defaultQuery.trim();
}
// Do not send an empty request.
if (!query.prompt) {
return;
}
stopRef.current = false;
setNoInteractions(false);
setConversation([
...conversationRef.current,
{
role: 'user',
origin: type,
content: query.prompt,
disableCopy: true
}
]);
// Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0));
try {
setProcessing(true);
// Add initial processing message
setProcessingMessage(
{ role: 'status', content: 'Submitting request...', disableCopy: true }
);
// Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0));
let data: any = query;
if (type === "job_description") {
data = {
prompt: "",
agent_options: {
job_description: query.prompt,
}
}
}
const response = await fetch(`${connectionBase}/api/${type}/${sessionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(data)
});
setSnack(`Query sent.`, "info");
if (!response.ok) {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
}
if (!response.body) {
throw new Error('Response body is null');
}
let streaming_response = ""
// Set up stream processing with explicit chunking
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const process_line = async (line: string) => {
let update = JSON.parse(line);
switch (update.status) {
case 'done':
case 'partial':
if (update.status === 'done') stopCountdown();
if (update.status === 'done') setStreamingMessage(undefined);
if (update.status === 'done') setProcessingMessage(undefined);
const backstoryMessage: BackstoryMessage = update;
setConversation([
...conversationRef.current, {
...backstoryMessage,
role: 'assistant',
origin: type,
prompt: ['done', 'partial'].includes(update.status) ? update.prompt : '',
content: backstoryMessage.response || "",
expanded: update.status === "done" ? true : false,
expandable: update.status === "done" ? false : true,
}] as MessageList);
// Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0));
const metadata = update.metadata;
if (onResponse) {
onResponse(update);
}
break;
case 'error':
// Show error
setConversation([
...conversationRef.current, {
...update,
role: 'error',
origin: type,
content: update.response || "",
}] as MessageList);
setProcessing(false);
stopCountdown();
// Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0));
break;
default:
// Force an immediate state update based on the message type
// Update processing message with immediate re-render
if (update.status === "streaming") {
streaming_response += update.chunk
setStreamingMessage({ role: update.status, content: streaming_response, disableCopy: true });
} else {
setProcessingMessage({ role: update.status, content: update.response, disableCopy: true });
/* Reset stream on non streaming message */
streaming_response = ""
}
startCountdown(Math.ceil(update.remaining_time));
// Add a small delay to ensure React has time to update the UI
await new Promise(resolve => setTimeout(resolve, 0));
break;
}
}
while (!stopRef.current) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = decoder.decode(value, { stream: true });
// Process each complete line immediately
buffer += chunk;
let lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
for (const line of lines) {
if (!line.trim()) continue;
try {
await process_line(line);
} catch (e) {
setSnack("Error processing query", "error")
console.error(e);
}
}
}
// Process any remaining buffer content
if (buffer.trim()) {
try {
await process_line(buffer);
} catch (e) {
setSnack("Error processing query", "error")
console.error(e);
}
}
if (stopRef.current) {
await reader.cancel();
setProcessingMessage(undefined);
setStreamingMessage(undefined);
setSnack("Processing cancelled", "warning");
}
stopCountdown();
setProcessing(false);
stopRef.current = false;
} catch (error) {
console.error('Fetch error:', error);
setSnack("Unable to process query", "error");
setProcessingMessage({ role: 'error', content: "Unable to process query", disableCopy: true });
setTimeout(() => {
setProcessingMessage(undefined);
}, 5000);
stopRef.current = false;
setProcessing(false);
stopCountdown();
return;
}
};
return (
// <Scrollable
// className={`${className || ""} Conversation`}
// autoscroll
// textFieldRef={viewableElementRef}
// fallbackThreshold={0.5}
// sx={{
// p: 1,
// mt: 0,
// ...sx
// }}
// >
<Box sx={{ p: 1, mt: 0, overflow: "hidden", ...sx }}>
{
filteredConversation.map((message, index) =>
<Message key={index} expanded={message.expanded === undefined ? true : message.expanded} {...{ sendQuery, message, connectionBase, sessionId, setSnack, submitQuery }} />
)
}
{
processingMessage !== undefined &&
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: processingMessage, submitQuery }} />
}
{
streamingMessage !== undefined &&
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: streamingMessage, submitQuery }} />
}
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
m: 1,
}}>
<PropagateLoader
size="10px"
loading={processing}
aria-label="Loading Spinner"
data-testid="loader"
/>
{processing === true && countdown > 0 && (
<Box
sx={{
pt: 1,
fontSize: "0.7rem",
color: "darkgrey"
}}
>Response will be stopped in: {countdown}s</Box>
)}
</Box>
<Box className="Query" sx={{ display: "flex", flexDirection: "column", p: 1, flexGrow: 1 }}>
{placeholder &&
<Box sx={{ display: "flex", flexGrow: 1, p: 0, m: 0, flexDirection: "column" }}
ref={viewableElementRef}>
<BackstoryTextField
ref={backstoryTextRef}
disabled={processing}
onEnter={handleEnter}
placeholder={placeholder}
/>
</Box>
}
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
<DeleteConfirmation
label={resetLabel || "all data"}
disabled={sessionId === undefined || processingMessage !== undefined || noInteractions}
onDelete={() => { reset(); resetAction && resetAction(); }} />
<Tooltip title={actionLabel || "Send"}>
<span style={{ display: "flex", flexGrow: 1 }}>
<Button
sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained"
disabled={sessionId === undefined || processingMessage !== undefined}
onClick={() => { sendQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
{actionLabel}<SendIcon />
</Button>
</span>
</Tooltip>
<Tooltip title="Cancel">
<span style={{ display: "flex" }}> { /* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton
aria-label="cancel"
onClick={() => { cancelQuery(); }}
sx={{ display: "flex", margin: 'auto 0px' }}
size="large"
edge="start"
disabled={stopRef.current || sessionId === undefined || processing === false}
>
<CancelIcon />
</IconButton>
</span>
</Tooltip>
</Box>
</Box>
{(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length &&
<Box sx={{ display: "flex", flexDirection: "column" }}>
{
defaultPrompts.map((element, index) => {
return (<Box key={index}>{element}</Box>);
})
}
</Box>
}
<Box sx={{ display: "flex", flexGrow: 1 }}></Box>
</Box >
);
});
export type {
ConversationProps,
ConversationHandle,
};
export {
Conversation
};

View File

@ -3,6 +3,7 @@ import { NavigateFunction, useLocation } from 'react-router-dom';
import {
AppBar,
Toolbar,
Tooltip,
Typography,
Button,
IconButton,
@ -28,24 +29,14 @@ import {
Settings,
ExpandMore,
} from '@mui/icons-material';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { NavigationLinkType } from '../BackstoryApp';
import { NavigationLinkType } from './BackstoryLayout';
import { Beta } from './Beta';
import './Header.css';
// Interface for component props
interface HeaderProps {
isLoggedIn?: boolean;
userType?: 'candidate' | 'employer' | null;
userName?: string;
transparent?: boolean;
onLogout?: () => void;
className?: string;
navigate: NavigateFunction;
navigationLinks: NavigationLinkType[];
showLogin?: boolean;
currentPath: string;
}
import { UserType } from './UserContext';
import { SetSnackType } from '../../Components/Snack';
import { CopyBubble } from '../../Components/CopyBubble';
// Styled components
const StyledAppBar = styled(AppBar, {
@ -91,17 +82,32 @@ const MobileDrawer = styled(Drawer)(({ theme }) => ({
},
}));
const Header: React.FC<HeaderProps> = ({
isLoggedIn = false,
userName = '',
transparent = false,
onLogout,
className,
navigate,
navigationLinks,
showLogin,
currentPath,
}) => {
interface HeaderProps {
user?: UserType | null;
transparent?: boolean;
onLogout?: () => void;
className?: string;
navigate: NavigateFunction;
navigationLinks: NavigationLinkType[];
showLogin?: boolean;
currentPath: string;
sessionId?: string | null;
setSnack: SetSnackType,
}
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const {
user,
transparent = false,
className,
navigate,
navigationLinks,
showLogin,
currentPath,
sessionId,
onLogout,
setSnack,
} = props;
const theme = useTheme();
const location = useLocation();
@ -147,16 +153,12 @@ const Header: React.FC<HeaderProps> = ({
useEffect(() => {
const parts = location.pathname.split('/');
console.log(location.pathname);
let tab = '/';
if (parts.length > 1) {
tab = `/${parts[1]}`;
}
if (tab !== currentTab) {
console.log(`Setting tab to ${tab}`);
setCurrentTab(tab);
} else {
console.log(`Not setting tab to ${tab}`);
}
}, [location, currentTab]);
@ -228,7 +230,7 @@ const Header: React.FC<HeaderProps> = ({
))}
</Tabs>
<Divider />
{!isLoggedIn && (showLogin === undefined || showLogin !== false) && (
{(!user || !user.isAuthenticated) && (showLogin === undefined || showLogin !== false) && (
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Button
variant="contained"
@ -258,7 +260,7 @@ const Header: React.FC<HeaderProps> = ({
return <></>;
}
if (!isLoggedIn) {
if (!user || !user.isAuthenticated) {
return (
<>
<Button
@ -297,10 +299,10 @@ const Header: React.FC<HeaderProps> = ({
height: 32,
bgcolor: theme.palette.secondary.main,
}}>
{userName.charAt(0).toUpperCase()}
{user?.name.charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
{userName}
{user?.name}
</Box>
<ExpandMore fontSize="small" />
</UserButton>
@ -391,6 +393,7 @@ const Header: React.FC<HeaderProps> = ({
{renderUserSection()}
{/* Mobile Menu Button */}
<Tooltip title="Open Menu">
<IconButton
color="inherit"
aria-label="open drawer"
@ -400,6 +403,24 @@ const Header: React.FC<HeaderProps> = ({
>
<MenuIcon />
</IconButton>
</Tooltip>
{sessionId && <CopyBubble
tooltip="Copy link"
color="inherit"
aria-label="copy link"
edge="end"
sx={{
width: 36,
height: 36,
opacity: 1,
bgcolor: 'inherit',
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
}}
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
onClick={() => setSnack("Link copied!")}
size="large"
/>}
</UserActionsContainer>
{/* Mobile Navigation Drawer */}

View File

@ -0,0 +1,39 @@
import React, { createContext, useContext, useState } from "react";
type UserType = {
type: 'candidate' | 'employer' | 'guest';
name: string;
avatar?: any;
isAuthenticated: boolean;
logout: () => void;
};
type UserContextType = {
user: UserType | null;
setUser: (user: UserType) => void;
};
const UserContext = createContext<UserContextType | undefined>(undefined);
const useUser = () => {
const ctx = useContext(UserContext);
if (!ctx) throw new Error("useUser must be used within a UserProvider");
return ctx;
};
const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<UserType | null>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
export type {
UserType
};
export {
UserProvider,
useUser
};

View File

@ -0,0 +1,417 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation, useParams } from 'react-router-dom';
import {
Box,
Drawer,
AppBar,
Toolbar,
IconButton,
Typography,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Paper,
Grid,
Card,
CardContent,
CardActionArea,
Divider,
useTheme,
useMediaQuery
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import CloseIcon from '@mui/icons-material/Close';
import DescriptionIcon from '@mui/icons-material/Description';
import CodeIcon from '@mui/icons-material/Code';
import LayersIcon from '@mui/icons-material/Layers';
import DashboardIcon from '@mui/icons-material/Dashboard';
import PaletteIcon from '@mui/icons-material/Palette';
import AnalyticsIcon from '@mui/icons-material/Analytics';
import ViewQuiltIcon from '@mui/icons-material/ViewQuilt';
import { Document } from '../Components/Document';
import { BackstoryPageProps } from '../../Components/BackstoryTab';
import { BackstoryUIOverviewPage } from './BackstoryUIOverviewPage';
import { BackstoryAppAnalysisPage } from './BackstoryAppAnalysisPage';
import { BackstoryThemeVisualizerPage } from './BackstoryThemeVisualizerPage';
import { MockupPage } from './MockupPage';
// Get appropriate icon for document type
const getDocumentIcon = (title: string) => {
switch (title) {
case 'Docs':
return <DescriptionIcon />;
case 'BETA':
return <CodeIcon />;
case 'Resume Generation Architecture':
case 'Application Architecture':
return <LayersIcon />;
case 'UI Overview':
case 'UI Mockup':
return <DashboardIcon />;
case 'Theme Visualizer':
return <PaletteIcon />;
case 'App Analysis':
return <AnalyticsIcon />;
default:
return <ViewQuiltIcon />;
}
};
// Sidebar navigation component using MUI components
const Sidebar: React.FC<{
currentPage: string;
onDocumentSelect: (docName: string, open: boolean) => void;
onClose?: () => void;
isMobile: boolean;
}> = ({ currentPage, onDocumentSelect, onClose, isMobile }) => {
// Document definitions
const handleItemClick = (route: string) => {
onDocumentSelect(route, true);
if (isMobile && onClose) {
onClose();
}
};
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{
p: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: 1,
borderColor: 'divider'
}}>
<Typography variant="h6" component="h2" fontWeight="bold">
Documentation
</Typography>
{isMobile && onClose && (
<IconButton
onClick={onClose}
size="small"
aria-label="Close navigation"
>
<CloseIcon />
</IconButton>
)}
</Box>
<Box sx={{
flexGrow: 1,
overflow: 'auto',
p: 1
}}>
<List>
{documents.map((doc, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => handleItemClick(doc.route)}
selected={currentPage === doc.route}
sx={{
borderRadius: 1,
mb: 0.5
}}
>
<ListItemIcon sx={{
color: currentPage === doc.route ? 'primary.main' : 'text.secondary',
minWidth: 40
}}>
{getDocumentIcon(doc.title)}
</ListItemIcon>
<ListItemText
primary={doc.title}
slotProps={{
primary: {
fontWeight: currentPage === doc.route ? 'medium' : 'regular',
}
}}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Box>
</Box>
);
};
type DocType = {
title: string;
route: string;
description: string;
};
const documents : DocType[] = [
{ 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" },
{ title: 'Text Mockups', route: "backstory-ui-mockups", description: "Early text mockups of many of the interaction points." },
];
const documentFromRoute = (route: string) : DocType | null => {
const index = documents.findIndex(v => v.route === route);
if (index === -1) {
return null
}
return documents[index];
};
// Helper function to get document title from route
const documentTitleFromRoute = (route: string): string => {
const doc = documentFromRoute(route);
if (doc === null) {
return 'Documentation'
}
return doc.title;
}
const DocsPage = (props: BackstoryPageProps) => {
const { sessionId, submitQuery, setSnack } = props;
const navigate = useNavigate();
const location = useLocation();
const { paramPage = '' } = useParams();
const [page, setPage] = useState<string>(paramPage);
const [drawerOpen, setDrawerOpen] = useState(false);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Track location changes
useEffect(() => {
const parts = location.pathname.split('/');
if (parts.length > 2) {
setPage(parts[2]);
} else {
setPage('');
}
}, [location]);
// Close drawer when changing to desktop view
useEffect(() => {
if (!isMobile) {
setDrawerOpen(false);
}
}, [isMobile]);
// Handle document navigation
const onDocumentExpand = (docName: string, open: boolean) => {
console.log("Document expanded:", { docName, open, location });
if (open) {
const parts = location.pathname.split('/');
if (parts.length > 2) {
const basePath = parts.slice(0, -1).join('/');
navigate(`${basePath}/${docName}`);
} else {
navigate(docName);
}
} else {
const basePath = location.pathname.split('/').slice(0, -1).join('/');
navigate(`${basePath}`);
}
};
// Toggle mobile drawer
const toggleDrawer = () => {
setDrawerOpen(!drawerOpen);
};
// Close the drawer
const closeDrawer = () => {
setDrawerOpen(false);
};
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 '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:
if (documentFromRoute(page)) {
return <DocView page={page}/>
}
// 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}>
{documents.map((doc, index) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<Card>
<CardActionArea onClick={() => onDocumentExpand(doc.route, true)}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Box sx={{ color: 'primary.main', mr: 1.5 }}>
{getDocumentIcon(doc.title)}
</Box>
<Typography variant="h6">{doc.title}</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ ml: 5 }}>
{doc.description}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
</Paper>
);
}
}
// Calculate drawer width
const drawerWidth = 240;
return (
<Box sx={{ display: 'flex', height: '100%' }}>
{/* Mobile App Bar */}
{isMobile && (
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
display: { md: 'none' }
}}
elevation={0}
color="default"
>
<Toolbar>
<IconButton
aria-label="open drawer"
edge="start"
onClick={toggleDrawer}
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div" sx={{ color: "white" }}>
{page ? documentTitleFromRoute(page) : "Documentation"}
</Typography>
</Toolbar>
</AppBar>
)}
{/* Navigation drawer */}
<Box
component="nav"
sx={{
width: { md: drawerWidth },
flexShrink: { md: 0 }
}}
>
{/* Mobile drawer (temporary) */}
{isMobile ? (
<Drawer
variant="temporary"
open={drawerOpen}
onClose={closeDrawer}
ModalProps={{
keepMounted: true, // Better open performance on mobile
}}
sx={{
display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth
},
}}
>
<Sidebar
currentPage={page}
onDocumentSelect={onDocumentExpand}
onClose={closeDrawer}
isMobile={true}
/>
</Drawer>
) : (
// Desktop drawer (permanent)
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', md: 'block' },
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth,
position: 'relative',
height: '100%'
},
}}
open
>
<Sidebar
currentPage={page}
onDocumentSelect={onDocumentExpand}
isMobile={false}
/>
</Drawer>
)}
</Box>
{/* Main content */}
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { md: `calc(100% - ${drawerWidth}px)` },
pt: isMobile ? { xs: 8, sm: 9 } : 3, // Add padding top on mobile to account for AppBar
height: '100%',
overflow: 'auto'
}}
>
{renderContent()}
</Box>
</Box>
);
};
export { DocsPage };

View File

@ -0,0 +1,38 @@
import React, { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useUser, UserType } from "../Components/UserContext";
import { Typography, Box } from "@mui/material";
const mockFetchUser = async (username: string) => {
return new Promise<UserType>((resolve) => {
const user : UserType = {
type: "candidate",
name: username,
isAuthenticated: true,
logout: () => {},
};
setTimeout(() => resolve(user), 500);
});
};
const UserRoute: React.FC = () => {
const { username } = useParams<{ username: string }>();
const { user, setUser } = useUser();
useEffect(() => {
if (username) {
mockFetchUser(username).then(setUser);
}
}, [username, setUser]);
return (
<Box m={2}>
<Typography variant="h5">User Page</Typography>
<Typography variant="body1">
{user ? `Hello, ${user.name}` : "Loading..."}
</Typography>
</Box>
);
};
export { UserRoute };