New navigation system

This commit is contained in:
James Ketr 2025-06-08 13:38:49 -07:00
parent 18863a23d9
commit d41f9a9e75
29 changed files with 914 additions and 571 deletions

View File

@ -5,8 +5,8 @@ import { ChatSubmitQueryInterface } from './BackstoryQuery';
import { SetSnackType } from './Snack'; import { SetSnackType } from './Snack';
interface BackstoryElementProps { interface BackstoryElementProps {
setSnack: SetSnackType, // setSnack: SetSnackType,
submitQuery: ChatSubmitQueryInterface, // submitQuery: ChatSubmitQueryInterface,
sx?: SxProps<Theme>, sx?: SxProps<Theme>,
} }

View File

@ -19,7 +19,7 @@ import { ChatMessage, ChatContext, ChatSession, ChatQuery, ChatMessageUser, Chat
import { PaginatedResponse } from 'types/conversion'; import { PaginatedResponse } from 'types/conversion';
import './Conversation.css'; import './Conversation.css';
import { useSelectedCandidate } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "assistant" status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "assistant"
@ -65,8 +65,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
preamble, preamble,
resetAction, resetAction,
resetLabel, resetLabel,
setSnack,
submitQuery,
sx, sx,
type, type,
} = props; } = props;
@ -84,6 +82,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
const stopRef = useRef(false); const stopRef = useRef(false);
const controllerRef = useRef<StreamingResponse>(null); const controllerRef = useRef<StreamingResponse>(null);
const [chatSession, setChatSession] = useState<ChatSession | null>(null); const [chatSession, setChatSession] = useState<ChatSession | null>(null);
const { setSnack } = useAppState();
// Keep the ref updated whenever items changes // Keep the ref updated whenever items changes
useEffect(() => { useEffect(() => {
@ -326,16 +325,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
<Box sx={{ p: 1, mt: 0, ...sx }}> <Box sx={{ p: 1, mt: 0, ...sx }}>
{ {
filteredConversation.map((message, index) => filteredConversation.map((message, index) =>
<Message key={index} {...{ chatSession, sendQuery: processQuery, message, connectionBase, setSnack, submitQuery }} /> <Message key={index} {...{ chatSession, sendQuery: processQuery, message, connectionBase, }} />
) )
} }
{ {
processingMessage !== undefined && processingMessage !== undefined &&
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: processingMessage, submitQuery }} /> <Message {...{ chatSession, sendQuery: processQuery, connectionBase, message: processingMessage, }} />
} }
{ {
streamingMessage !== undefined && streamingMessage !== undefined &&
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: streamingMessage, submitQuery }} /> <Message {...{ chatSession, sendQuery: processQuery, connectionBase, message: streamingMessage }} />
} }
<Box sx={{ <Box sx={{
display: "flex", display: "flex",

View File

@ -7,11 +7,7 @@ interface DocumentProps extends BackstoryElementProps {
} }
const Document = (props: DocumentProps) => { const Document = (props: DocumentProps) => {
const { setSnack, submitQuery, filepath } = props; const { filepath } = props;
const backstoryProps = {
submitQuery,
setSnack,
};
const [document, setDocument] = useState<string>(""); const [document, setDocument] = useState<string>("");
@ -43,7 +39,7 @@ const Document = (props: DocumentProps) => {
}, [document, setDocument, filepath]) }, [document, setDocument, filepath])
return (<> return (<>
<StyledMarkdown {...backstoryProps} content={document}/> <StyledMarkdown content={document} />
</>); </>);
}; };

View File

@ -36,6 +36,7 @@ import { useTheme } from '@mui/material/styles';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from "hooks/AuthContext";
import * as Types from 'types/types'; import * as Types from 'types/types';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { useAppState } from 'hooks/GlobalContext';
const VisuallyHiddenInput = styled('input')({ const VisuallyHiddenInput = styled('input')({
clip: 'rect(0 0 0 0)', clip: 'rect(0 0 0 0)',
@ -50,8 +51,8 @@ const VisuallyHiddenInput = styled('input')({
}); });
const DocumentManager = (props: BackstoryElementProps) => { const DocumentManager = (props: BackstoryElementProps) => {
const { setSnack, submitQuery } = props;
const theme = useTheme(); const theme = useTheme();
const { setSnack } = useAppState();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();

View File

@ -5,6 +5,7 @@ import { Quote } from 'components/Quote';
import { BackstoryElementProps } from 'components/BackstoryTab'; import { BackstoryElementProps } from 'components/BackstoryTab';
import { Candidate, ChatSession } from 'types/types'; import { Candidate, ChatSession } from 'types/types';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useAppState } from 'hooks/GlobalContext';
interface GenerateImageProps extends BackstoryElementProps { interface GenerateImageProps extends BackstoryElementProps {
prompt: string; prompt: string;
@ -13,7 +14,8 @@ interface GenerateImageProps extends BackstoryElementProps {
const GenerateImage = (props: GenerateImageProps) => { const GenerateImage = (props: GenerateImageProps) => {
const { user } = useAuth(); const { user } = useAuth();
const { setSnack, chatSession, prompt } = props; const { chatSession, prompt } = props;
const { setSnack } = useAppState();
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [status, setStatus] = useState<string>(''); const [status, setStatus] = useState<string>('');
const [image, setImage] = useState<string>(''); const [image, setImage] = useState<string>('');

View File

@ -46,7 +46,7 @@ import DescriptionIcon from '@mui/icons-material/Description';
import FileUploadIcon from '@mui/icons-material/FileUpload'; import FileUploadIcon from '@mui/icons-material/FileUpload';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext'; import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { LoginRequired } from 'components/ui/LoginRequired'; import { LoginRequired } from 'components/ui/LoginRequired';
@ -122,8 +122,7 @@ const JobCreator = (props: JobCreator) => {
const { onSave } = props; const { onSave } = props;
const { selectedCandidate } = useSelectedCandidate(); const { selectedCandidate } = useSelectedCandidate();
const { selectedJob, setSelectedJob } = useSelectedJob(); const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack, submitQuery } = props; const { setSnack } = useAppState();
const backstoryProps = { setSnack, submitQuery };
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isTablet = useMediaQuery(theme.breakpoints.down('md')); const isTablet = useMediaQuery(theme.breakpoints.down('md'));

View File

@ -31,6 +31,7 @@ import { StyledMarkdown } from './StyledMarkdown';
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
import { start } from 'repl'; import { start } from 'repl';
import { TypesElement } from '@uiw/react-json-view'; import { TypesElement } from '@uiw/react-json-view';
import { useAppState } from 'hooks/GlobalContext';
interface JobAnalysisProps extends BackstoryPageProps { interface JobAnalysisProps extends BackstoryPageProps {
job: Job; job: Job;
@ -45,11 +46,9 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
const { const {
job, job,
candidate, candidate,
setSnack,
submitQuery
} = props } = props
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const backstoryProps = { setSnack, submitQuery }; const { setSnack } = useAppState();
const theme = useTheme(); const theme = useTheme();
const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]); const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]);
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]); const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
@ -250,7 +249,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
</Typography> </Typography>
<Paper sx={{ p: 2, maxHeight: "22rem" }}> <Paper sx={{ p: 2, maxHeight: "22rem" }}>
<Scrollable sx={{ display: "flex", maxHeight: "100%" }}> <Scrollable sx={{ display: "flex", maxHeight: "100%" }}>
<StyledMarkdown content={job.description} {...backstoryProps} /> <StyledMarkdown content={job.description} />
</Scrollable> </Scrollable>
</Paper> </Paper>
</Grid> </Grid>

View File

@ -358,12 +358,8 @@ const MessageContainer = (props: MessageContainerProps) => {
}; };
const Message = (props: MessageProps) => { const Message = (props: MessageProps) => {
const { message, title, submitQuery, sx, className, chatSession, onExpand, setSnack, expanded, expandable } = props; const { message, title, sx, className, chatSession, onExpand, expanded, expandable } = props;
const [metaExpanded, setMetaExpanded] = useState<boolean>(false); const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
const backstoryProps = {
submitQuery,
setSnack
};
const theme = useTheme(); const theme = useTheme();
const type: ApiActivityType | ChatSenderType | "error" = ('activity' in message) ? message.activity : ('error' in message) ? 'error' : (message as ChatMessage).role; const type: ApiActivityType | ChatSenderType | "error" = ('activity' in message) ? message.activity : ('error' in message) ? 'error' : (message as ChatMessage).role;
const style: any = getStyle(theme, type); const style: any = getStyle(theme, type);
@ -385,7 +381,7 @@ const Message = (props: MessageProps) => {
}; };
const messageView = ( const messageView = (
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={content} {...backstoryProps} /> <StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={content} />
); );
let metadataView = (<></>); let metadataView = (<></>);

View File

@ -2,7 +2,7 @@ import React from 'react';
import { MuiMarkdown } from 'mui-markdown'; import { MuiMarkdown } from 'mui-markdown';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { Link } from '@mui/material'; import { Link } from '@mui/material';
import { BackstoryQuery } from 'components/BackstoryQuery'; import { BackstoryQuery, BackstoryQueryInterface } from 'components/BackstoryQuery';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import JsonView from '@uiw/react-json-view'; import JsonView from '@uiw/react-json-view';
import { vscodeTheme } from '@uiw/react-json-view/vscode'; import { vscodeTheme } from '@uiw/react-json-view/vscode';
@ -13,17 +13,19 @@ import { GenerateImage } from 'components/GenerateImage';
import './StyledMarkdown.css'; import './StyledMarkdown.css';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { CandidateQuestion, ChatSession } from 'types/types'; import { CandidateQuestion, ChatQuery, ChatSession } from 'types/types';
import { ChatSubmitQueryInterface } from 'components/BackstoryQuery';
interface StyledMarkdownProps extends BackstoryElementProps { interface StyledMarkdownProps extends BackstoryElementProps {
className?: string, className?: string,
content: string, content: string,
streaming?: boolean, streaming?: boolean,
chatSession?: ChatSession, chatSession?: ChatSession,
submitQuery?: ChatSubmitQueryInterface
}; };
const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => { const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => {
const { className, content, chatSession, submitQuery, sx, streaming, setSnack } = props; const { className, content, chatSession, submitQuery, sx, streaming } = props;
const theme = useTheme(); const theme = useTheme();
const overrides: any = { const overrides: any = {
@ -110,7 +112,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
question: queryString question: queryString
} }
return <BackstoryQuery submitQuery={submitQuery} question={query} /> return submitQuery ? <BackstoryQuery submitQuery={submitQuery} question={query} /> : query.question;
} catch (e) { } catch (e) {
console.log("StyledMarkdown error:", queryString, e); console.log("StyledMarkdown error:", queryString, e);
return props.query; return props.query;
@ -124,7 +126,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
component: (props: { prompt: string }) => { component: (props: { prompt: string }) => {
const prompt = props.prompt.replace(/(\w+):/g, '"$1":'); const prompt = props.prompt.replace(/(\w+):/g, '"$1":');
try { try {
return <GenerateImage {...{ chatSession, prompt, submitQuery, setSnack }} /> return <GenerateImage {...{ chatSession, prompt }} />
} catch (e) { } catch (e) {
console.log("StyledMarkdown error:", prompt, e); console.log("StyledMarkdown error:", prompt, e);
return props.prompt; return props.prompt;

View File

@ -23,7 +23,7 @@ import './VectorVisualizer.css';
import { BackstoryPageProps } from './BackstoryTab'; import { BackstoryPageProps } from './BackstoryTab';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import * as Types from 'types/types'; import * as Types from 'types/types';
import { useSelectedCandidate } from 'hooks/GlobalContext'; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
interface VectorVisualizerProps extends BackstoryPageProps { interface VectorVisualizerProps extends BackstoryPageProps {
@ -180,8 +180,8 @@ type Node = {
const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => { const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const { setSnack, submitQuery, rag, inline, sx } = props; const { rag, inline, sx } = props;
const backstoryProps = { setSnack, submitQuery }; const { setSnack } = useAppState();
const [plotData, setPlotData] = useState<PlotData | null>(null); const [plotData, setPlotData] = useState<PlotData | null>(null);
const [newQuery, setNewQuery] = useState<string>(''); const [newQuery, setNewQuery] = useState<string>('');
const [querySet, setQuerySet] = useState<Types.ChromaDBGetResponse>(rag || emptyQuerySet); const [querySet, setQuerySet] = useState<Types.ChromaDBGetResponse>(rag || emptyQuerySet);

View File

@ -1,168 +1,136 @@
// components/layout/BackstoryLayout.tsx
import React, { ReactElement, useEffect, useState } from 'react'; import React, { ReactElement, useEffect, useState } from 'react';
import { Outlet, useLocation, Routes } from "react-router-dom"; import { Outlet, useLocation, Routes, Route } from "react-router-dom";
import { Box, Container, Paper } from '@mui/material'; import { Box, Container, Paper } from '@mui/material';
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import ChatIcon from '@mui/icons-material/Chat';
import DashboardIcon from '@mui/icons-material/Dashboard';
import DescriptionIcon from '@mui/icons-material/Description';
import BarChartIcon from '@mui/icons-material/BarChart';
import SettingsIcon from '@mui/icons-material/Settings';
import WorkIcon from '@mui/icons-material/Work';
import InfoIcon from '@mui/icons-material/Info';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import { Header } from 'components/layout/Header'; import { Header } from 'components/layout/Header';
import { Scrollable } from 'components/Scrollable'; import { Scrollable } from 'components/Scrollable';
import { Footer } from 'components/layout/Footer'; import { Footer } from 'components/layout/Footer';
import { Snack, SetSnackType } from 'components/Snack'; import { Snack, SetSnackType } from 'components/Snack';
import { User } from 'types/types'; import { User } from 'types/types';
import { getBackstoryDynamicRoutes } from 'components/layout/BackstoryRoutes';
import { LoadingComponent } from "components/LoadingComponent"; import { LoadingComponent } from "components/LoadingComponent";
import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext'; import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext';
import { useSelectedCandidate } from 'hooks/GlobalContext'; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import {
getMainNavigationItems,
getAllRoutes,
} from 'config/navigationConfig';
import { NavigationItem } from 'types/navigation';
type NavigationLinkType = { // Legacy type for backward compatibility
export type NavigationLinkType = {
label: ReactElement<any> | string; label: ReactElement<any> | string;
path: string; path: string;
icon?: ReactElement<any>; icon?: ReactElement<any>;
}; };
const DefaultNavItems: NavigationLinkType[] = [
{ label: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
{ label: 'Docs', path: '/docs', icon: <InfoIcon /> },
// { label: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
// { label: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
// { label: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
// { label: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
];
const ViewerNavItems: NavigationLinkType[] = [
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
];
const CandidateNavItems : NavigationLinkType[]= [
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
{ label: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon /> },
{ label: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> },
// { label: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> },
// { label: 'Dashboard', icon: <DashboardIcon />, path: '/candidate/dashboard' },
// { label: 'Profile', icon: <PersonIcon />, path: '/candidate/dashboard/profile' },
// { label: 'Backstory', icon: <HistoryIcon />, path: '/candidate/backstory' },
// { label: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes' },
// { label: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/candidate/qa-setup' },
// { label: 'Analytics', icon: <BarChartIcon />, path: '/candidate/analytics' },
// { label: 'Settings', icon: <SettingsIcon />, path: '/candidate/settings' },
];
const EmployerNavItems: NavigationLinkType[] = [
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
{ label: 'Job Analysis', path: '/employer/job-analysis', icon: <WorkIcon /> },
{ label: 'Resume Builder', path: '/employer/resume-builder', icon: <WorkIcon /> },
{ label: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: <WorkIcon /> },
{ label: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
// { label: 'Dashboard', icon: <DashboardIcon />, path: '/employer/dashboard' },
// { label: 'Search', icon: <SearchIcon />, path: '/employer/search' },
// { label: 'Saved', icon: <BookmarkIcon />, path: '/employer/saved' },
// { label: 'Jobs', icon: <WorkIcon />, path: '/employer/jobs' },
// { label: 'Company', icon: <BusinessIcon />, path: '/employer/company' },
// { label: 'Analytics', icon: <BarChartIcon />, path: '/employer/analytics' },
// { label: 'Settings', icon: <SettingsIcon />, path: '/employer/settings' },
];
// Navigation links based on user type
const getNavigationLinks = (user: User | null): NavigationLinkType[] => {
if (!user) {
return DefaultNavItems;
}
switch (user.userType) {
case 'candidate':
return DefaultNavItems.concat(CandidateNavItems);
case 'employer':
return DefaultNavItems.concat(EmployerNavItems);
default:
return DefaultNavItems;
}
};
interface BackstoryPageContainerProps { interface BackstoryPageContainerProps {
children?: React.ReactNode; children?: React.ReactNode;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
};
const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
const { children, sx } = props;
return (
<Container
className="BackstoryPageContainer"
sx={{
display: "flex",
flexDirection: "row", // Must be row; if column, the box will expand for all children
flexGrow: 1,
p: "0 !important", // Let the first box use padding to offset main content
m: "0 auto !important",
maxWidth: '1024px', //{ xs: '100%', md: '700px', lg: '1024px' },
height: "100%", // Restrict to main-container's height
minHeight: 0,//"min-content", // Prevent flex overflow
...sx
}}>
<Box sx={{
display: "flex", p: { xs: 0, sm: 0.5 }, flexGrow: 1, minHeight: "min-content", // Prevent flex overflow
}}>
<Paper
elevation={2}
sx={{
display: "flex",
flexGrow: 1,
m: 0,
p: 0.5,
minHeight: "min-content", // Prevent flex overflow
backgroundColor: 'background.paper',
borderRadius: 0.5,
maxWidth: '100%',
flexDirection: "column",
}}>
{children}
</Paper>
</Box>
</Container>
);
} }
interface BackstoryLayoutProps { const BackstoryPageContainer = (props: BackstoryPageContainerProps) => {
setSnack: SetSnackType; const { children, sx } = props;
page: string; return (
chatRef: React.Ref<any>; <Container
snackRef: React.Ref<any>; className="BackstoryPageContainer"
submitQuery: any; sx={{
display: "flex",
flexDirection: "row",
flexGrow: 1,
p: "0 !important",
m: "0 auto !important",
maxWidth: '1024px',
height: "100%",
minHeight: 0,
...sx
}}>
<Box sx={{
display: "flex",
p: { xs: 0, sm: 0.5 },
flexGrow: 1,
minHeight: "min-content",
}}>
<Paper
elevation={2}
sx={{
display: "flex",
flexGrow: 1,
m: 0,
p: 0.5,
minHeight: "min-content",
backgroundColor: 'background.paper',
borderRadius: 0.5,
maxWidth: '100%',
flexDirection: "column",
}}>
{children}
</Paper>
</Box>
</Container>
);
}; };
interface BackstoryLayoutProps {
page: string;
chatRef: React.Ref<any>;
}
const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => { const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => {
const { setSnack, page, chatRef, snackRef, submitQuery } = props; const { page, chatRef } = props;
const { setSnack } = useAppState();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { guest, user } = useAuth(); const { guest, user } = useAuth();
const { selectedCandidate } = useSelectedCandidate(); const { selectedCandidate } = useSelectedCandidate();
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]); const [navigationItems, setNavigationItems] = useState<NavigationItem[]>([]);
useEffect(() => { useEffect(() => {
setNavigationLinks(getNavigationLinks(user)); const userType = user?.userType || null;
setNavigationItems(getMainNavigationItems(userType));
}, [user]); }, [user]);
let dynamicRoutes; // Generate dynamic routes from navigation config
if (guest) { const generateRoutes = () => {
dynamicRoutes = getBackstoryDynamicRoutes({ if (!guest) return null;
user,
setSnack, const userType = user?.userType || null;
submitQuery, const routes = getAllRoutes(userType);
chatRef
}); return routes.map((route, index) => {
} if (!route.path || !route.component) return null;
// Clone the component and pass necessary props if it's a page component
const componentWithProps = React.cloneElement(route.component as ReactElement, {
...(route.id === 'chat' && { ref: chatRef }),
...(route.component.props || {}),
});
return (
<Route
key={`${route.id}-${index}`}
path={route.path}
element={componentWithProps}
/>
);
}).filter(Boolean);
};
return ( return (
<Box sx={{ height: "100%", maxHeight: "100%", minHeight: "100%", flexDirection: "column" }}> <Box sx={{
<Header {...{ setSnack, currentPath: page, navigate, navigationLinks }} /> height: "100%",
maxHeight: "100%",
minHeight: "100%",
flexDirection: "column"
}}>
<Header
setSnack={setSnack}
currentPath={page}
navigate={navigate}
navigationItems={navigationItems}
/>
<Box sx={{ <Box sx={{
display: "flex", display: "flex",
width: "100%", width: "100%",
@ -174,49 +142,48 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
flexDirection: "column", flexDirection: "column",
backgroundColor: "#D3CDBF", /* Warm Gray */ backgroundColor: "#D3CDBF", /* Warm Gray */
}}> }}>
<Scrollable <Scrollable
className="BackstoryPageScrollable" className="BackstoryPageScrollable"
sx={{ sx={{
m: 0, m: 0,
p: 0, p: 0,
mt: "72px", /* Needs to be kept in sync with the height of Header if the Header theme changes */ mt: "72px", /* Needs to be kept in sync with the height of Header */
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
backgroundColor: "background.default", backgroundColor: "background.default",
height: "100%", height: "100%",
maxHeight: "100%", maxHeight: "100%",
minHeight: "100%", minHeight: "100%",
minWidth: "min-content", minWidth: "min-content",
}} }}
> >
<BackstoryPageContainer> <BackstoryPageContainer>
{!guest && {!guest && (
<Box> <Box>
<LoadingComponent <LoadingComponent
loadingText="Creating session..." loadingText="Creating session..."
loaderType="linear" loaderType="linear"
withFade={true} withFade={true}
fadeDuration={1200} /> fadeDuration={1200}
</Box> />
} </Box>
{guest && <> )}
<Outlet /> {guest && (
{dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>} <>
</> <Outlet />
} <Routes>
{location.pathname === "/" && <Footer />} {generateRoutes()}
</BackstoryPageContainer> </Routes>
</Scrollable> </>
<Snack ref={snackRef} /> )}
{location.pathname === "/" && <Footer />}
</BackstoryPageContainer>
</Scrollable>
</Box> </Box>
</Box> </Box>
); );
}; };
export type {
NavigationLinkType
};
export { export {
BackstoryLayout BackstoryLayout
}; };

View File

@ -1,95 +1,45 @@
// components/layout/BackstoryRoutes.tsx
import React, { Ref, ReactNode } from "react"; import React, { Ref, ReactNode } from "react";
import { Route } from "react-router-dom"; import { Route } from "react-router-dom";
import { Typography } from '@mui/material';
import { BackstoryPageProps } from '../BackstoryTab'; import { BackstoryPageProps } from '../BackstoryTab';
import { ConversationHandle } from '../Conversation'; import { ConversationHandle } from '../Conversation';
import { User } from 'types/types'; import { User } from 'types/types';
import { getAllRoutes } from 'config/navigationConfig';
import { NavigationItem } from 'types/navigation';
import { useAppState } from "hooks/GlobalContext";
import { CandidateChatPage } from 'pages/CandidateChatPage';
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';
import { CandidateListingPage } from 'pages/FindCandidatePage';
import { JobAnalysisPage } from 'pages/JobAnalysisPage';
import { GenerateCandidate } from "pages/GenerateCandidate";
import { ControlsPage } from 'pages/ControlsPage';
import { LoginPage } from "pages/LoginPage";
import { CandidateDashboardPage } from "pages/candidate/Dashboard"
import { EmailVerificationPage } from "components/EmailVerificationComponents";
import { JobMatchAnalysis } from "components/JobMatchAnalysis";
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 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>);
const LogoutPage = () => (<BetaPage><Typography variant="h4">Logout page...</Typography></BetaPage>);
// const DashboardPage = () => (<BetaPage><Typography variant="h4">Dashboard</Typography></BetaPage>);
// const AnalyticsPage = () => (<BetaPage><Typography variant="h4">Analytics</Typography></BetaPage>);
// const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typography></BetaPage>);
interface BackstoryDynamicRoutesProps extends BackstoryPageProps { interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
chatRef: Ref<ConversationHandle>; chatRef: Ref<ConversationHandle>;
user?: User | null; user?: User | null;
} }
const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNode => { const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNode => {
const { user, setSnack, submitQuery, chatRef } = props; const { user, chatRef } = props;
const backstoryProps = { const userType = user?.userType || null;
setSnack, submitQuery
};
let index=0
const routes = [
<Route key={`${index++}`} path="/" element={<HomePage/>} />,
<Route key={`${index++}`} path="/chat" element={<CandidateChatPage ref={chatRef} {...backstoryProps} />} />,
<Route key={`${index++}`} path="/docs" element={<DocsPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/docs/:subPage" element={<DocsPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/resume-builder" element={<ResumeBuilderPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/knowledge-explorer" element={<VectorVisualizerPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/find-a-candidate" element={<CandidateListingPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/job-analysis" element={<JobAnalysisPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/generate-candidate" element={<GenerateCandidate {...backstoryProps} />} />,
<Route key={`${index++}`} path="/settings" element={<ControlsPage {...backstoryProps} />} />,
];
if (!user) { // Get all routes from navigation config
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />); const routes = getAllRoutes(userType);
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage {...backstoryProps} />} />);
routes.push(<Route key={`${index++}`} path="/login/verify-email" element={<EmailVerificationPage {...backstoryProps} />} />);
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
} else {
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage {...backstoryProps} />} />);
routes.push(<Route key={`${index++}`} path="/login/verify-email" element={<EmailVerificationPage {...backstoryProps} />} />);
routes.push(<Route key={`${index++}`} path="/logout" element={<LogoutPage />} />);
if (user.userType === 'candidate') { return routes.map((route: NavigationItem, index: number) => {
routes.splice(-1, 0, ...[ if (!route.path || !route.component) return null;
<Route key={`${index++}`} path="/candidate/dashboard" element={<CandidateDashboardPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/candidate/dashboard/:subPage" element={<CandidateDashboardPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/candidate/job-analysis" element={<JobAnalysisPage {...backstoryProps} />} />,
<Route key={`${index++}`} path="/candidate/backstory" element={<BackstoryPage />} />,
<Route key={`${index++}`} path="/candidate/resumes" element={<ResumesPage />} />,
<Route key={`${index++}`} path="/candidate/qa-setup" element={<QASetupPage />} />,
]);
}
if (user.userType === 'employer') { // Clone the component and pass necessary props
routes.splice(-1, 0, ...[ const componentWithProps = React.cloneElement(route.component as React.ReactElement, {
<Route key={`${index++}`} path="/search" element={<SearchPage />} />, // Special handling for chat component ref
<Route key={`${index++}`} path="/saved" element={<SavedPage />} />, ...(route.id === 'chat' && { ref: chatRef }),
<Route key={`${index++}`} path="/jobs" element={<JobsPage />} />, // Preserve any existing props
<Route key={`${index++}`} path="/company" element={<CompanyPage />} />, ...(route.component.props || {}),
]); });
}
}
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />); return (
<Route
return routes; key={`${route.id}-${index}`}
path={route.path}
element={componentWithProps}
/>
);
}).filter(Boolean);
}; };
export { getBackstoryDynamicRoutes }; export { getBackstoryDynamicRoutes };

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; // components/layout/Header.tsx
import React, { JSX, useEffect, useState } from 'react';
import { NavigateFunction, useLocation } from 'react-router-dom'; import { NavigateFunction, useLocation } from 'react-router-dom';
import { import {
AppBar, AppBar,
@ -17,6 +18,14 @@ import {
Fade, Fade,
Popover, Popover,
Paper, Paper,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
Collapse,
List,
ListItem,
ListItemButton,
} from '@mui/material'; } from '@mui/material';
import { styled, useTheme } from '@mui/material/styles'; import { styled, useTheme } from '@mui/material/styles';
import { import {
@ -26,14 +35,15 @@ import {
Logout, Logout,
Settings, Settings,
ExpandMore, ExpandMore,
ExpandLess,
KeyboardArrowDown,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { NavigationLinkType } from 'components/layout/BackstoryLayout'; import { NavigationItem } from 'types/navigation';
import { Beta } from 'components/ui/Beta'; import { Beta } from 'components/ui/Beta';
import { Candidate, Employer } from 'types/types'; import { Candidate, Employer } from 'types/types';
import { SetSnackType } from 'components/Snack'; import { SetSnackType } from 'components/Snack';
import { CopyBubble } from 'components/CopyBubble'; import { CopyBubble } from 'components/CopyBubble';
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import 'components/layout/Header.css'; import 'components/layout/Header.css';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
@ -79,39 +89,17 @@ const UserButton = styled(Button)(({ theme }) => ({
const MobileDrawer = styled(Drawer)(({ theme }) => ({ const MobileDrawer = styled(Drawer)(({ theme }) => ({
'& .MuiDrawer-paper': { '& .MuiDrawer-paper': {
width: 280, width: 320,
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
}, },
})); }));
const MobileMenuTabs = styled(Tabs)(({ theme }) => ({ const DropdownButton = styled(Button)(({ theme }) => ({
'& .MuiTabs-flexContainer': { color: theme.palette.primary.contrastText,
flexDirection: 'column', textTransform: 'none',
}, minHeight: 48,
'& .MuiTab-root': { '&:hover': {
minHeight: 48, backgroundColor: theme.palette.action.hover,
textTransform: 'uppercase',
justifyContent: 'flex-start',
alignItems: 'center',
padding: theme.spacing(1, 2),
gap: theme.spacing(1),
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
'&.Mui-selected': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
'& .MuiTypography-root': {
color: theme.palette.primary.contrastText,
},
'& .MuiSvgIcon-root': {
color: theme.palette.primary.contrastText,
},
},
},
'& .MuiTabs-indicator': {
display: 'none',
}, },
})); }));
@ -123,51 +111,11 @@ const UserMenuContainer = styled(Paper)(({ theme }) => ({
minWidth: 200, minWidth: 200,
})); }));
const UserMenuTabs = styled(Tabs)(({ theme }) => ({
'& .MuiTabs-flexContainer': {
flexDirection: 'column',
},
'& .MuiTab-root': {
minHeight: 48,
textTransform: 'uppercase',
justifyContent: 'flex-start',
alignItems: 'center',
padding: theme.spacing(1, 2),
gap: theme.spacing(1),
color: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
'&.Mui-selected': {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
},
},
'& .MuiTabs-indicator': {
display: 'none',
},
}));
const MenuItemBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
width: '100%',
'& .MuiSvgIcon-root': {
color: theme.palette.primary.main,
},
}));
const MenuDivider = styled(Divider)(({ theme }) => ({
margin: theme.spacing(0.5, 0),
backgroundColor: theme.palette.divider,
}));
interface HeaderProps { interface HeaderProps {
transparent?: boolean; transparent?: boolean;
className?: string; className?: string;
navigate: NavigateFunction; navigate: NavigateFunction;
navigationLinks: NavigationLinkType[]; navigationItems: NavigationItem[];
currentPath: string; currentPath: string;
sessionId?: string | null; sessionId?: string | null;
setSnack: SetSnackType; setSnack: SetSnackType;
@ -181,7 +129,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
transparent = false, transparent = false,
className, className,
navigate, navigate,
navigationLinks, navigationItems,
sessionId, sessionId,
setSnack, setSnack,
} = props; } = props;
@ -190,42 +138,17 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const name = (user?.firstName || user?.email || ''); const name = (user?.firstName || user?.email || '');
const mainNavSections: NavigationLinkType[] = [ // State for desktop dropdown menus
{ path: '/', label: <BackstoryLogo /> }, const [dropdownAnchors, setDropdownAnchors] = useState<{ [key: string]: HTMLElement | null }>({});
...navigationLinks
];
// State for page navigation - only for main navigation
const [currentTab, setCurrentTab] = useState<string | false>("/");
const [userMenuTab, setUserMenuTab] = useState<string>("");
// State for mobile drawer // State for mobile drawer
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const [mobileExpanded, setMobileExpanded] = useState<{ [key: string]: boolean }>({});
// State for user menu // State for user menu
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null); const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
const userMenuOpen = Boolean(userMenuAnchor); const userMenuOpen = Boolean(userMenuAnchor);
// Helper function to determine which action page we're in
const getTailSection = (pathname: string): string | false => {
// Handle home page
if (pathname === '/') return '/';
// Split path and check against main sections
const segments = pathname.split('/').filter(Boolean);
if (segments.length === 0) return '/';
const lastSegment = `/${segments[segments.length - 1]}`;
// Check if this matches any of our main navigation sections
const matchingSection = mainNavSections.find(section =>
section.path === lastSegment ||
(section.path !== '/' && pathname.endsWith(section.path))
);
return matchingSection ? matchingSection.path : false; // Return false for routes that shouldn't show in main nav
};
// User menu items // User menu items
const userMenuItems = [ const userMenuItems = [
{ {
@ -263,14 +186,38 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
} }
]; ];
useEffect(() => { // Helper function to check if current path matches navigation item
const mainSection = getTailSection(location.pathname); const isCurrentPath = (item: NavigationItem): boolean => {
if (!item.path) return false;
// Only update if the section is different from current tab if (item.exact) {
if (mainSection !== currentTab) { return location.pathname === item.path;
setCurrentTab(mainSection); // mainSection is either a string or false
} }
}, [location.pathname, currentTab]); return location.pathname.startsWith(item.path);
};
// Helper function to check if any child is current path
const hasActiveChild = (item: NavigationItem): boolean => {
if (!item.children) return false;
return item.children.some(child => isCurrentPath(child) || hasActiveChild(child));
};
// Desktop dropdown handlers
const handleDropdownOpen = (event: React.MouseEvent<HTMLElement>, itemId: string) => {
setDropdownAnchors(prev => ({ ...prev, [itemId]: event.currentTarget }));
};
const handleDropdownClose = (itemId: string) => {
setDropdownAnchors(prev => ({ ...prev, [itemId]: null }));
};
// Mobile accordion handlers
const handleMobileToggle = (itemId: string) => {
setMobileExpanded(prev => ({ ...prev, [itemId]: !prev[itemId] }));
};
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => { const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setUserMenuAnchor(event.currentTarget); setUserMenuAnchor(event.currentTarget);
@ -278,7 +225,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const handleUserMenuClose = () => { const handleUserMenuClose = () => {
setUserMenuAnchor(null); setUserMenuAnchor(null);
setUserMenuTab("");
}; };
const handleUserMenuAction = (item: typeof userMenuItems[0]) => { const handleUserMenuAction = (item: typeof userMenuItems[0]) => {
@ -288,83 +234,152 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
} }
}; };
const handleDrawerToggle = () => { // Navigation handlers
setMobileOpen(!mobileOpen); const handleNavigate = (path: string) => {
navigate(path);
setMobileOpen(false);
// Close all dropdowns
setDropdownAnchors({});
}; };
const handleTabChange = (event: React.SyntheticEvent, newValue: string | false) => { // Render desktop navigation with dropdowns
if (newValue !== false) { const renderDesktopNavigation = () => {
setCurrentTab(newValue);
navigate(newValue);
}
};
// Render desktop navigation links
const renderNavLinks = () => {
return ( return (
<Tabs <Box sx={{ display: 'flex', alignItems: 'center' }}>
value={currentTab} {navigationItems.map((item) => {
onChange={handleTabChange} const hasChildren = item.children && item.children.length > 0;
indicatorColor="secondary" const isActive = isCurrentPath(item) || hasActiveChild(item);
textColor="inherit"
variant="fullWidth" if (hasChildren) {
allowScrollButtonsMobile return (
aria-label="Backstory navigation" <Box key={item.id}>
> <DropdownButton
{mainNavSections.map((section) => ( onClick={(e) => handleDropdownOpen(e, item.id)}
<Tab endIcon={<KeyboardArrowDown />}
sx={{ sx={{
minWidth: section.path === '/' ? "max-content" : "auto", backgroundColor: isActive ? 'action.selected' : 'transparent',
}} color: isActive ? 'secondary.main' : 'primary.contrastText',
key={section.path} }}
value={section.path} >
label={section.label} {item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
/> {item.label}
))} </DropdownButton>
</Tabs> <Menu
anchorEl={dropdownAnchors[item.id]}
open={Boolean(dropdownAnchors[item.id])}
onClose={() => handleDropdownClose(item.id)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
TransitionComponent={Fade}
>
{item.children?.map((child) => (
<MenuItem
key={child.id}
onClick={() => child.path && handleNavigate(child.path)}
selected={isCurrentPath(child)}
disabled={!child.path}
>
{child.icon && <ListItemIcon>{child.icon}</ListItemIcon>}
<ListItemText>{child.label}</ListItemText>
</MenuItem>
))}
</Menu>
</Box>
);
} else {
return (
<DropdownButton
key={item.id}
onClick={() => item.path && handleNavigate(item.path)}
sx={{
backgroundColor: isActive ? 'action.selected' : 'transparent',
color: isActive ? 'secondary.main' : 'primary.contrastText',
}}
>
{item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
{item.label}
</DropdownButton>
);
}
})}
</Box>
); );
}; };
// Render mobile drawer content // Render mobile accordion navigation
const renderDrawerContent = () => { const renderMobileNavigation = () => {
return ( const renderNavigationItem = (item: NavigationItem, depth: number = 0) => {
<> const hasChildren = item.children && item.children.length > 0;
<MobileMenuTabs const isActive = isCurrentPath(item) || hasActiveChild(item);
orientation="vertical" const isExpanded = mobileExpanded[item.id];
value={currentTab}
onChange={handleTabChange} return (
> <Box key={item.id}>
{mainNavSections.map((section) => ( <ListItem disablePadding sx={{ pl: depth * 2 }}>
<Tab <ListItemButton
key={section.path || '/'} onClick={() => {
value={section.path} if (hasChildren) {
label={ handleMobileToggle(item.id);
<MenuItemBox> } else if (item.path) {
{section.label} handleNavigate(item.path);
</MenuItemBox> }
} }}
onClick={(e) => { selected={isActive}
handleDrawerToggle(); sx={{
setCurrentTab(section.path); backgroundColor: isActive ? 'action.selected' : 'transparent',
navigate(section.path); '&.Mui-selected': {
backgroundColor: 'primary.main',
color: 'primary.contrastText',
'& .MuiListItemIcon-root': {
color: 'primary.contrastText',
},
},
}} }}
/>
))}
</MobileMenuTabs>
<MenuDivider />
{!user && (
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Button
variant="contained"
color="secondary"
fullWidth
onClick={() => { handleDrawerToggle(); navigate("/login"); }}
> >
Login {item.icon && (
</Button> <ListItemIcon sx={{ minWidth: 36 }}>
</Box> {item.icon}
</ListItemIcon>
)}
<ListItemText
primary={item.label}
sx={{
'& .MuiTypography-root': {
fontSize: depth > 0 ? '0.875rem' : '1rem',
fontWeight: depth === 0 ? 500 : 400,
}
}}
/>
{hasChildren && (
<IconButton size="small">
{isExpanded ? <ExpandLess /> : <ExpandMore />}
</IconButton>
)}
</ListItemButton>
</ListItem>
{hasChildren && (
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<List disablePadding>
{item.children?.map((child) => renderNavigationItem(child, depth + 1))}
</List>
</Collapse>
)}
</Box>
);
};
return (
<List sx={{ pt: 0 }}>
{navigationItems.map((item) => renderNavigationItem(item))}
<Divider sx={{ my: 1 }} />
{!user && (
<ListItem disablePadding>
<ListItemButton onClick={() => handleNavigate('/login')}>
<ListItemText primary="Login" />
</ListItemButton>
</ListItem>
)} )}
</> </List>
); );
}; };
@ -372,29 +387,20 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const renderUserMenu = () => { const renderUserMenu = () => {
return ( return (
<UserMenuContainer> <UserMenuContainer>
<UserMenuTabs <List dense>
orientation="vertical"
value={userMenuTab}
onChange={(e, newValue) => setUserMenuTab(newValue)}
>
{userMenuItems.map((item, index) => ( {userMenuItems.map((item, index) => (
item.id === 'divider' ? ( item.id === 'divider' ? (
<MenuDivider key={`divider-${index}`} /> <Divider key={`divider-${index}`} />
) : ( ) : (
<Tab <ListItem key={item.id} disablePadding>
key={item.id} <ListItemButton onClick={() => handleUserMenuAction(item)}>
value={item.id} {item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
label={ <ListItemText primary={item.label} />
<MenuItemBox> </ListItemButton>
{item.icon} </ListItem>
<Typography variant="body2">{item.label}</Typography>
</MenuItemBox>
}
onClick={() => handleUserMenuAction(item)}
/>
) )
))} ))}
</UserMenuTabs> </List>
</UserMenuContainer> </UserMenuContainer>
); );
}; };
@ -403,19 +409,17 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const renderUserSection = () => { const renderUserSection = () => {
if (!user) { if (!user) {
return ( return (
<> <Button
<Button color="info"
color="info" variant="contained"
variant="contained" onClick={() => navigate("/login")}
onClick={() => navigate("/login") } sx={{
sx={{ display: { xs: 'none', sm: 'block' },
display: { xs: 'none', sm: 'block' }, color: theme.palette.primary.contrastText,
color: theme.palette.primary.contrastText, }}
}} >
> Login
Login </Button>
</Button>
</>
); );
} }
@ -428,10 +432,10 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
aria-expanded={userMenuOpen ? 'true' : undefined} aria-expanded={userMenuOpen ? 'true' : undefined}
> >
<Avatar sx={{ <Avatar sx={{
width: 32, width: 32,
height: 32, height: 32,
bgcolor: theme.palette.secondary.main, bgcolor: theme.palette.secondary.main,
}}> }}>
{name.charAt(0).toUpperCase()} {name.charAt(0).toUpperCase()}
</Avatar> </Avatar>
<Box sx={{ display: { xs: 'none', sm: 'block' } }}> <Box sx={{ display: { xs: 'none', sm: 'block' } }}>
@ -472,7 +476,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<Toolbar disableGutters> <Toolbar disableGutters>
{/* Navigation Links - Desktop */} {/* Navigation Links - Desktop */}
<NavLinksContainer> <NavLinksContainer>
{renderNavLinks()} {renderDesktopNavigation()}
</NavLinksContainer> </NavLinksContainer>
{/* User Actions Section */} {/* User Actions Section */}
@ -492,22 +496,27 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{sessionId && <CopyBubble {sessionId && (
tooltip="Copy link" <CopyBubble
color="inherit" tooltip="Copy link"
aria-label="copy link" color="inherit"
edge="end" aria-label="copy link"
sx={{ edge="end"
width: 36, sx={{
height: 36, width: 36,
opacity: 1, height: 36,
bgcolor: 'inherit', opacity: 1,
'&:hover': { bgcolor: 'action.hover', opacity: 1 }, bgcolor: 'inherit',
}} '&:hover': { bgcolor: 'action.hover', opacity: 1 },
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`} }}
onClick={() => { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }} content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
size="large" onClick={() => {
/>} navigate(`${window.location.pathname}?id=${sessionId}`);
setSnack("Link copied!");
}}
size="large"
/>
)}
</UserActionsContainer> </UserActionsContainer>
{/* Mobile Navigation Drawer */} {/* Mobile Navigation Drawer */}
@ -520,11 +529,14 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
keepMounted: true, keepMounted: true,
}} }}
> >
{renderDrawerContent()} {renderMobileNavigation()}
</MobileDrawer> </MobileDrawer>
</Toolbar> </Toolbar>
</Container> </Container>
<Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} /> <Beta
sx={{ left: "-90px", "& .mobile": { right: "-72px" } }}
onClick={() => { navigate('/docs/beta'); }}
/>
</StyledAppBar> </StyledAppBar>
); );
}; };

View File

@ -7,7 +7,7 @@ import { BackstoryElementProps } from 'components/BackstoryTab';
import { CandidateInfo } from 'components/ui/CandidateInfo'; import { CandidateInfo } from 'components/ui/CandidateInfo';
import { Candidate } from "types/types"; import { Candidate } from "types/types";
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate } from 'hooks/GlobalContext'; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
interface CandidatePickerProps extends BackstoryElementProps { interface CandidatePickerProps extends BackstoryElementProps {
onSelect?: (candidate: Candidate) => void onSelect?: (candidate: Candidate) => void
@ -18,7 +18,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
const { apiClient, user } = useAuth(); const { apiClient, user } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate(); const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const navigate = useNavigate(); const navigate = useNavigate();
const { setSnack } = props; const { setSnack } = useAppState();
const [candidates, setCandidates] = useState<Candidate[] | null>(null); const [candidates, setCandidates] = useState<Candidate[] | null>(null);
useEffect(() => { useEffect(() => {

View File

@ -7,7 +7,7 @@ import { BackstoryElementProps } from 'components/BackstoryTab';
import { JobInfo } from 'components/ui/JobInfo'; import { JobInfo } from 'components/ui/JobInfo';
import { Job, JobFull } from "types/types"; import { Job, JobFull } from "types/types";
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useSelectedJob } from 'hooks/GlobalContext'; import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
interface JobPickerProps extends BackstoryElementProps { interface JobPickerProps extends BackstoryElementProps {
onSelect?: (job: JobFull) => void onSelect?: (job: JobFull) => void
@ -17,7 +17,7 @@ const JobPicker = (props: JobPickerProps) => {
const { onSelect } = props; const { onSelect } = props;
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { selectedJob, setSelectedJob } = useSelectedJob(); const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack } = props; const { setSnack } = useAppState();
const [jobs, setJobs] = useState<JobFull[] | null>(null); const [jobs, setJobs] = useState<JobFull[] | null>(null);
useEffect(() => { useEffect(() => {

View File

@ -0,0 +1,397 @@
// config/navigationConfig.tsx
import React from 'react';
import {
Chat as ChatIcon,
Dashboard as DashboardIcon,
Description as DescriptionIcon,
BarChart as BarChartIcon,
Settings as SettingsIcon,
Work as WorkIcon,
Info as InfoIcon,
Person as PersonIcon,
Business as BusinessIcon,
Search as SearchIcon,
Bookmark as BookmarkIcon,
History as HistoryIcon,
QuestionAnswer as QuestionAnswerIcon,
AttachMoney as AttachMoneyIcon,
} from '@mui/icons-material';
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import { HomePage } from 'pages/HomePage';
import { CandidateChatPage } from 'pages/CandidateChatPage';
import { ResumeBuilderPage } from 'pages/ResumeBuilderPage';
import { DocsPage } from 'pages/DocsPage';
import { CreateProfilePage } from 'pages/CreateProfilePage';
import { VectorVisualizerPage } from 'pages/VectorVisualizerPage';
import { BetaPage } from 'pages/BetaPage';
import { CandidateListingPage } from 'pages/FindCandidatePage';
import { JobAnalysisPage } from 'pages/JobAnalysisPage';
import { GenerateCandidate } from 'pages/GenerateCandidate';
import { ControlsPage } from 'pages/ControlsPage';
import { LoginPage } from 'pages/LoginPage';
import { CandidateDashboardPage } from 'pages/candidate/Dashboard';
import { EmailVerificationPage } from 'components/EmailVerificationComponents';
import { Typography } from '@mui/material';
import { NavigationConfig, NavigationItem } from 'types/navigation';
// Beta page components for placeholder routes
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 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>);
const LogoutPage = () => (<BetaPage><Typography variant="h4">Logout page...</Typography></BetaPage>);
const AnalyticsPage = () => (<BetaPage><Typography variant="h4">Analytics</Typography></BetaPage>);
const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typography></BetaPage>);
export const navigationConfig: NavigationConfig = {
items: [
{
id: 'home',
label: <BackstoryLogo />,
path: '/',
component: <HomePage />,
userTypes: ['guest', 'candidate', 'employer'],
exact: true,
},
{
id: 'find-candidate',
label: 'Find a Candidate',
path: '/find-a-candidate',
icon: <InfoIcon />,
component: <CandidateListingPage />,
userTypes: ['guest', 'candidate', 'employer'],
},
{
id: 'docs',
label: 'Docs',
path: '/docs',
icon: <InfoIcon />,
component: <DocsPage />,
userTypes: ['guest', 'candidate', 'employer'],
children: [
{
id: 'docs-subpage',
label: 'Docs Subpage',
path: '/docs/:subPage',
component: <DocsPage />,
userTypes: ['guest', 'candidate', 'employer'],
},
],
},
{
id: 'chat',
label: 'Chat',
path: '/chat',
icon: <ChatIcon />,
component: <CandidateChatPage />,
userTypes: ['candidate', 'employer'],
},
{
id: 'candidate-menu',
label: 'Candidate Tools',
icon: <PersonIcon />,
userTypes: ['candidate'],
children: [
{
id: 'candidate-dashboard',
label: 'Dashboard',
path: '/candidate/dashboard',
icon: <DashboardIcon />,
component: <CandidateDashboardPage />,
userTypes: ['candidate'],
children: [
{
id: 'candidate-dashboard-subpage',
label: 'Dashboard Subpage',
path: '/candidate/dashboard/:subPage',
component: <CandidateDashboardPage />,
userTypes: ['candidate'],
},
],
},
{
id: 'candidate-job-analysis',
label: 'Job Analysis',
path: '/candidate/job-analysis',
icon: <WorkIcon />,
component: <JobAnalysisPage />,
userTypes: ['candidate'],
},
{
id: 'candidate-resume-builder',
label: 'Resume Builder',
path: '/candidate/resume-builder',
icon: <DescriptionIcon />,
component: <ResumeBuilderPage />,
userTypes: ['candidate'],
},
{
id: 'candidate-backstory',
label: 'Backstory',
path: '/candidate/backstory',
icon: <HistoryIcon />,
component: <BackstoryPage />,
userTypes: ['candidate'],
},
{
id: 'candidate-resumes',
label: 'Resumes',
path: '/candidate/resumes',
icon: <DescriptionIcon />,
component: <ResumesPage />,
userTypes: ['candidate'],
},
{
id: 'candidate-qa-setup',
label: 'Q&A Setup',
path: '/candidate/qa-setup',
icon: <QuestionAnswerIcon />,
component: <QASetupPage />,
userTypes: ['candidate'],
},
{
id: 'candidate-analytics',
label: 'Analytics',
path: '/candidate/analytics',
icon: <BarChartIcon />,
component: <AnalyticsPage />,
userTypes: ['candidate'],
},
{
id: 'candidate-settings',
label: 'Settings',
path: '/candidate/settings',
icon: <SettingsIcon />,
component: <SettingsPage />,
userTypes: ['candidate'],
},
],
},
{
id: 'employer-menu',
label: 'Employer Tools',
icon: <BusinessIcon />,
userTypes: ['employer'],
children: [
{
id: 'employer-job-analysis',
label: 'Job Analysis',
path: '/employer/job-analysis',
icon: <WorkIcon />,
component: <JobAnalysisPage />,
userTypes: ['employer'],
},
{
id: 'employer-resume-builder',
label: 'Resume Builder',
path: '/employer/resume-builder',
icon: <DescriptionIcon />,
component: <ResumeBuilderPage />,
userTypes: ['employer'],
},
{
id: 'employer-knowledge-explorer',
label: 'Knowledge Explorer',
path: '/employer/knowledge-explorer',
icon: <WorkIcon />,
component: <VectorVisualizerPage />,
userTypes: ['employer'],
},
{
id: 'employer-search',
label: 'Search',
path: '/employer/search',
icon: <SearchIcon />,
component: <SearchPage />,
userTypes: ['employer'],
},
{
id: 'employer-saved',
label: 'Saved',
path: '/employer/saved',
icon: <BookmarkIcon />,
component: <SavedPage />,
userTypes: ['employer'],
},
{
id: 'employer-jobs',
label: 'Jobs',
path: '/employer/jobs',
icon: <WorkIcon />,
component: <JobsPage />,
userTypes: ['employer'],
},
{
id: 'employer-company',
label: 'Company',
path: '/employer/company',
icon: <BusinessIcon />,
component: <CompanyPage />,
userTypes: ['employer'],
},
{
id: 'employer-analytics',
label: 'Analytics',
path: '/employer/analytics',
icon: <BarChartIcon />,
component: <AnalyticsPage />,
userTypes: ['employer'],
},
{
id: 'employer-settings',
label: 'Settings',
path: '/employer/settings',
icon: <SettingsIcon />,
component: <SettingsPage />,
userTypes: ['employer'],
},
],
},
{
id: 'global-tools',
label: 'Tools',
icon: <SettingsIcon />,
userTypes: ['candidate', 'employer'],
children: [
{
id: 'resume-builder',
label: 'Resume Builder',
path: '/resume-builder',
icon: <DescriptionIcon />,
component: <ResumeBuilderPage />,
userTypes: ['candidate', 'employer'],
},
{
id: 'knowledge-explorer',
label: 'Knowledge Explorer',
path: '/knowledge-explorer',
icon: <WorkIcon />,
component: <VectorVisualizerPage />,
userTypes: ['candidate', 'employer'],
},
{
id: 'job-analysis',
label: 'Job Analysis',
path: '/job-analysis',
icon: <WorkIcon />,
component: <JobAnalysisPage />,
userTypes: ['candidate', 'employer'],
},
{
id: 'generate-candidate',
label: 'Generate Candidate',
path: '/generate-candidate',
icon: <PersonIcon />,
component: <GenerateCandidate />,
userTypes: ['candidate', 'employer'],
},
{
id: 'settings',
label: 'Settings',
path: '/settings',
icon: <SettingsIcon />,
component: <ControlsPage />,
userTypes: ['candidate', 'employer'],
},
],
},
// Auth routes (special handling)
{
id: 'auth',
label: 'Auth',
userTypes: ['guest', 'candidate', 'employer'],
children: [
{
id: 'register',
label: 'Register',
path: '/register',
component: <BetaPage><CreateProfilePage /></BetaPage>,
userTypes: ['guest'],
},
{
id: 'login',
label: 'Login',
path: '/login',
component: <LoginPage />,
userTypes: ['guest', 'candidate', 'employer'],
},
{
id: 'verify-email',
label: 'Verify Email',
path: '/login/verify-email',
component: <EmailVerificationPage />,
userTypes: ['guest', 'candidate', 'employer'],
},
{
id: 'logout',
label: 'Logout',
path: '/logout',
component: <LogoutPage />,
userTypes: ['candidate', 'employer'],
},
],
},
// Catch-all route
{
id: 'catch-all',
label: 'Not Found',
path: '*',
component: <BetaPage />,
userTypes: ['guest', 'candidate', 'employer'],
},
],
};
// Utility functions for working with navigation config
export const getNavigationItemsForUser = (userType: 'guest' | 'candidate' | 'employer' | null): NavigationItem[] => {
const currentUserType = userType || 'guest';
const filterItems = (items: NavigationItem[]): NavigationItem[] => {
return items
.filter(item => !item.userTypes || item.userTypes.includes(currentUserType))
.map(item => ({
...item,
children: item.children ? filterItems(item.children) : undefined,
}))
.filter(item => item.path || (item.children && item.children.length > 0));
};
return filterItems(navigationConfig.items);
};
export const getAllRoutes = (userType: 'guest' | 'candidate' | 'employer' | null): NavigationItem[] => {
const currentUserType = userType || 'guest';
const extractRoutes = (items: NavigationItem[]): NavigationItem[] => {
const routes: NavigationItem[] = [];
items.forEach(item => {
if (!item.userTypes || item.userTypes.includes(currentUserType)) {
if (item.path && item.component) {
routes.push(item);
}
if (item.children) {
routes.push(...extractRoutes(item.children));
}
}
});
return routes;
};
return extractRoutes(navigationConfig.items);
};
export const getMainNavigationItems = (userType: 'guest' | 'candidate' | 'employer' | null): NavigationItem[] => {
return getNavigationItemsForUser(userType)
.filter(item =>
item.id !== 'auth' &&
item.id !== 'catch-all' &&
(item.path || (item.children && item.children.length > 0))
);
};

View File

@ -1,8 +1,9 @@
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'; import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
import * as Types from 'types/types'; import * as Types from 'types/types';
// Assuming you're using React Router // Assuming you're using React Router
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { SetSnackType, SeverityType, Snack } from 'components/Snack';
// ============================ // ============================
// Storage Keys // Storage Keys
@ -43,6 +44,10 @@ export interface AppState {
} }
export interface AppStateActions { export interface AppStateActions {
// Snackbar
setSnack: SetSnackType;
//
setSelectedCandidate: (candidate: Types.Candidate | null) => void; setSelectedCandidate: (candidate: Types.Candidate | null) => void;
setSelectedJob: (job: Types.Job | null) => void; setSelectedJob: (job: Types.Job | null) => void;
setSelectedEmployer: (employer: Types.Employer | null) => void; setSelectedEmployer: (employer: Types.Employer | null) => void;
@ -358,7 +363,12 @@ export function useAppStateLogic(): AppStateContextType {
console.log('Cleared all route state'); console.log('Cleared all route state');
}, []); }, []);
const emptySetSnack: SetSnackType = (message: string, severity?: SeverityType) => {
return;
};
return { return {
setSnack: emptySetSnack,
selectedCandidate, selectedCandidate,
selectedJob, selectedJob,
selectedEmployer, selectedEmployer,
@ -385,10 +395,17 @@ const AppStateContext = createContext<AppStateContextType | null>(null);
export function AppStateProvider({ children }: { children: React.ReactNode }) { export function AppStateProvider({ children }: { children: React.ReactNode }) {
const appState = useAppStateLogic(); const appState = useAppStateLogic();
const snackRef = useRef<any>(null);
// Global UI components
appState.setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity);
}, [snackRef]);
return ( return (
<AppStateContext.Provider value={appState}> <AppStateContext.Provider value={appState}>
{children} {children}
<Snack ref={snackRef} />
</AppStateContext.Provider> </AppStateContext.Provider>
); );
} }

View File

@ -19,7 +19,7 @@ import { Message } from 'components/Message';
import { DeleteConfirmation } from 'components/DeleteConfirmation'; import { DeleteConfirmation } from 'components/DeleteConfirmation';
import { CandidateInfo } from 'components/ui/CandidateInfo'; import { CandidateInfo } from 'components/ui/CandidateInfo';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useSelectedCandidate } from 'hooks/GlobalContext'; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import PropagateLoader from 'react-spinners/PropagateLoader'; import PropagateLoader from 'react-spinners/PropagateLoader';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField'; import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
import { BackstoryQuery } from 'components/BackstoryQuery'; import { BackstoryQuery } from 'components/BackstoryQuery';
@ -37,10 +37,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null); const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null); const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const { const { setSnack } = useAppState();
setSnack,
submitQuery,
} = props;
const [chatSession, setChatSession] = useState<ChatSession | null>(null); const [chatSession, setChatSession] = useState<ChatSession | null>(null);
const [messages, setMessages] = useState<ChatMessage[]>([]); const [messages, setMessages] = useState<ChatMessage[]>([]);
@ -236,15 +233,15 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
}, },
}, },
}}> }}>
{messages.length === 0 && <Message {...{ chatSession, message: welcomeMessage, setSnack, submitQuery }} />} {messages.length === 0 && <Message {...{ chatSession, message: welcomeMessage, }} />}
{messages.map((message: ChatMessage) => ( {messages.map((message: ChatMessage) => (
<Message key={message.id} {...{ chatSession, message, setSnack, submitQuery }} /> <Message key={message.id} {...{ chatSession, message }} />
))} ))}
{processingMessage !== null && ( {processingMessage !== null && (
<Message {...{ chatSession, message: processingMessage, setSnack, submitQuery }} /> <Message {...{ chatSession, message: processingMessage }} />
)} )}
{streamingMessage !== null && ( {streamingMessage !== null && (
<Message {...{ chatSession, message: streamingMessage, setSnack, submitQuery }} /> <Message {...{ chatSession, message: streamingMessage }} />
)} )}
{streaming && ( {streaming && (
<Box sx={{ <Box sx={{

View File

@ -16,6 +16,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { connectionBase } from '../utils/Global'; import { connectionBase } from '../utils/Global';
import { BackstoryPageProps } from '../components/BackstoryTab'; import { BackstoryPageProps } from '../components/BackstoryTab';
import { useAppState } from 'hooks/GlobalContext';
interface ServerTunables { interface ServerTunables {
system_prompt: string, system_prompt: string,
@ -85,7 +86,7 @@ const SystemInfoComponent: React.FC<{ systemInfo: SystemInfo | undefined }> = ({
}; };
const ControlsPage = (props: BackstoryPageProps) => { const ControlsPage = (props: BackstoryPageProps) => {
const { setSnack } = props; const { setSnack } = useAppState();
const [editSystemPrompt, setEditSystemPrompt] = useState<string>(""); const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined); const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
const [tools, setTools] = useState<Tool[]>([]); const [tools, setTools] = useState<Tool[]>([]);

View File

@ -39,6 +39,7 @@ import { BackstoryAppAnalysisPage } from 'documents/BackstoryAppAnalysisPage';
import { BackstoryThemeVisualizerPage } from 'documents/BackstoryThemeVisualizerPage'; import { BackstoryThemeVisualizerPage } from 'documents/BackstoryThemeVisualizerPage';
import { UserManagement } from 'documents/UserManagement'; import { UserManagement } from 'documents/UserManagement';
import { MockupPage } from 'documents/MockupPage'; import { MockupPage } from 'documents/MockupPage';
import { useAppState } from 'hooks/GlobalContext';
// Sidebar navigation component using MUI components // Sidebar navigation component using MUI components
const Sidebar: React.FC<{ const Sidebar: React.FC<{
@ -170,7 +171,7 @@ const documentTitleFromRoute = (route: string): string => {
} }
const DocsPage = (props: BackstoryPageProps) => { const DocsPage = (props: BackstoryPageProps) => {
const { submitQuery, setSnack } = props; const { setSnack } = useAppState();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { paramPage = '' } = useParams(); const { paramPage = '' } = useParams();
@ -245,8 +246,6 @@ const DocsPage = (props: BackstoryPageProps) => {
</Box> </Box>
{page && <Document {page && <Document
filepath={`/docs/${page}.md`} filepath={`/docs/${page}.md`}
submitQuery={submitQuery}
setSnack={setSnack}
/>} />}
</CardContent> </CardContent>
</Card> </Card>

View File

@ -20,6 +20,7 @@ import { StreamingResponse } from 'services/api-client';
import { ChatMessage, ChatMessageUser, ChatSession, CandidateAI, ChatMessageStatus, ChatMessageError } from 'types/types'; import { ChatMessage, ChatMessageUser, ChatSession, CandidateAI, ChatMessageStatus, ChatMessageError } from 'types/types';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { Message } from 'components/Message'; import { Message } from 'components/Message';
import { useAppState } from 'hooks/GlobalContext';
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "user" status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "user"
@ -27,7 +28,7 @@ const defaultMessage: ChatMessage = {
const GenerateCandidate = (props: BackstoryElementProps) => { const GenerateCandidate = (props: BackstoryElementProps) => {
const { apiClient, user } = useAuth(); const { apiClient, user } = useAuth();
const { setSnack, submitQuery } = props; const { setSnack } = useAppState();
const [processingMessage, setProcessingMessage] = useState<ChatMessage | null>(null); const [processingMessage, setProcessingMessage] = useState<ChatMessage | null>(null);
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [generatedUser, setGeneratedUser] = useState<CandidateAI | null>(null); const [generatedUser, setGeneratedUser] = useState<CandidateAI | null>(null);
@ -220,7 +221,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
justifyContent: "center", justifyContent: "center",
m: 2, m: 2,
}}> }}>
{processingMessage && chatSession && <Message message={processingMessage} {...{ chatSession, submitQuery, setSnack }} />} {processingMessage && chatSession && <Message message={processingMessage} {...{ chatSession }} />}
<PropagateLoader <PropagateLoader
size="10px" size="10px"
loading={processing} loading={processing}
@ -266,7 +267,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
{resume && {resume &&
<Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}> <Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}>
<Scrollable sx={{flexGrow: 1}}> <Scrollable sx={{flexGrow: 1}}>
<StyledMarkdown {...{ content: resume, setSnack, submitQuery }} /> <StyledMarkdown content={resume} />
</Scrollable> </Scrollable>
</Paper> </Paper>
} }

View File

@ -26,7 +26,7 @@ import { Candidate, Job } from "types/types";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { BackstoryPageProps } from 'components/BackstoryTab'; import { BackstoryPageProps } from 'components/BackstoryTab';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext'; import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
import { CandidateInfo } from 'components/ui/CandidateInfo'; import { CandidateInfo } from 'components/ui/CandidateInfo';
import { ComingSoon } from 'components/ui/ComingSoon'; import { ComingSoon } from 'components/ui/ComingSoon';
import { LoginRequired } from 'components/ui/LoginRequired'; import { LoginRequired } from 'components/ui/LoginRequired';
@ -66,9 +66,8 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate() const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
const { selectedJob, setSelectedJob } = useSelectedJob() const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack, submitQuery } = props; const { setSnack } = useAppState();
const backstoryProps = { setSnack, submitQuery };
// State management // State management
const [activeStep, setActiveStep] = useState(0); const [activeStep, setActiveStep] = useState(0);
const [analysisStarted, setAnalysisStarted] = useState(false); const [analysisStarted, setAnalysisStarted] = useState(false);
@ -161,7 +160,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
Select a Candidate Select a Candidate
</Typography> </Typography>
<CandidatePicker onSelect={onCandidateSelect} {...backstoryProps} /> <CandidatePicker onSelect={onCandidateSelect} />
</Paper> </Paper>
); );
@ -184,11 +183,10 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
</Box> </Box>
{jobTab === 'load' && {jobTab === 'load' &&
<JobPicker {...backstoryProps} onSelect={onJobSelect} /> <JobPicker onSelect={onJobSelect} />
} }
{jobTab === 'create' && {jobTab === 'create' &&
<JobCreator <JobCreator
{...backstoryProps}
onSave={onJobSelect} onSave={onJobSelect}
/>} />}
</Box> </Box>
@ -202,7 +200,6 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<JobMatchAnalysis <JobMatchAnalysis
job={selectedJob} job={selectedJob}
candidate={selectedCandidate} candidate={selectedCandidate}
{...backstoryProps}
/> />
)} )}
</Box> </Box>

View File

@ -4,19 +4,15 @@ import {
Container, Container,
Paper, Paper,
Typography, Typography,
Grid,
Alert, Alert,
Tabs, Tabs,
Tab, Tab,
Card, Card,
CardContent, CardContent,
Divider,
Avatar,
} from '@mui/material'; } from '@mui/material';
import { import {
Person, Person,
PersonAdd, PersonAdd,
AccountCircle,
} from '@mui/icons-material'; } from '@mui/icons-material';
import 'react-phone-number-input/style.css'; import 'react-phone-number-input/style.css';
import './LoginPage.css'; import './LoginPage.css';
@ -29,10 +25,11 @@ import { BackstoryPageProps } from 'components/BackstoryTab';
import { LoginForm } from "components/EmailVerificationComponents"; import { LoginForm } from "components/EmailVerificationComponents";
import { CandidateRegistrationForm } from "components/RegistrationForms"; import { CandidateRegistrationForm } from "components/RegistrationForms";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAppState } from 'hooks/GlobalContext';
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => { const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { setSnack } = props; const { setSnack } = useAppState();
const [tabValue, setTabValue] = useState(0); const [tabValue, setTabValue] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);

View File

@ -10,9 +10,10 @@ import { BackstoryQuery } from '../components/BackstoryQuery';
import { CandidateInfo } from 'components/ui/CandidateInfo'; import { CandidateInfo } from 'components/ui/CandidateInfo';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { Candidate } from 'types/types'; import { Candidate } from 'types/types';
import { useAppState } from 'hooks/GlobalContext';
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => { const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
const { setSnack, submitQuery } = props; const { setSnack } = useAppState();
const { user } = useAuth(); const { user } = useAuth();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@ -28,7 +29,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
setQuestions([ setQuestions([
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}> <Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
{candidate.questions?.map((q, i: number) => {candidate.questions?.map((q, i: number) =>
<BackstoryQuery key={i} question={q} submitQuery={submitQuery} /> <BackstoryQuery key={i} question={q} />
)} )}
</Box>, </Box>,
<Box sx={{ p: 1 }}> <Box sx={{ p: 1 }}>
@ -36,7 +37,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
{`As with all LLM interactions, the results may not be 100% accurate. Please contact **${candidate.fullName}** if you have any questions.`} {`As with all LLM interactions, the results may not be 100% accurate. Please contact **${candidate.fullName}** if you have any questions.`}
</MuiMarkdown> </MuiMarkdown>
</Box>]); </Box>]);
}, [candidate, isMobile, submitQuery]); }, [candidate, isMobile]);
if (!candidate) { if (!candidate) {
return (<></>); return (<></>);
@ -51,9 +52,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
type: "chat", type: "chat",
placeholder: `What would you like to know about ${candidate?.firstName}?`, placeholder: `What would you like to know about ${candidate?.firstName}?`,
resetLabel: "chat", resetLabel: "chat",
setSnack, defaultPrompts: questions,
defaultPrompts: questions,
submitQuery,
}} /> }} />
</Box>); </Box>);
}); });

View File

@ -11,6 +11,7 @@ import { Conversation } from 'components/Conversation';
import { BackstoryPageProps } from 'components/BackstoryTab'; import { BackstoryPageProps } from 'components/BackstoryTab';
import { ChatQuery, ChatMessage } from "types/types"; import { ChatQuery, ChatMessage } from "types/types";
import './ResumeBuilderPage.css'; import './ResumeBuilderPage.css';
import { useAppState } from 'hooks/GlobalContext';
/** /**
* ResumeBuilder component * ResumeBuilder component
@ -19,11 +20,6 @@ import './ResumeBuilderPage.css';
* with different layouts for mobile and desktop views. * with different layouts for mobile and desktop views.
*/ */
const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => { const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const {
sx,
setSnack,
submitQuery,
} = props
// State for editing job description // State for editing job description
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false); const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
const [hasResume, setHasResume] = useState<boolean>(false); const [hasResume, setHasResume] = useState<boolean>(false);

View File

@ -46,6 +46,7 @@ import { CandidateProfile } from 'pages/candidate/dashboard/Profile';
import { VectorVisualizer } from 'components/VectorVisualizer'; import { VectorVisualizer } from 'components/VectorVisualizer';
import { DocumentManager } from 'components/DocumentManager'; import { DocumentManager } from 'components/DocumentManager';
import { JobPicker } from 'components/ui/JobPicker'; import { JobPicker } from 'components/ui/JobPicker';
import { useAppState } from 'hooks/GlobalContext';
interface DashboardProps extends BackstoryPageProps { interface DashboardProps extends BackstoryPageProps {
userName?: string; userName?: string;
@ -58,15 +59,14 @@ const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps)
const [activeTab, setActiveTab] = useState<string>(subPage); const [activeTab, setActiveTab] = useState<string>(subPage);
const { user, isLoading, isInitializing, isAuthenticated } = useAuth(); const { user, isLoading, isInitializing, isAuthenticated } = useAuth();
const profileCompletion = 75; const profileCompletion = 75;
const { setSnack, submitQuery } = props; const { setSnack } = useAppState();
const backstoryProps = { setSnack, submitQuery };
const sidebarItems = [ const sidebarItems = [
{ text: 'Dashboard', icon: <DashboardIcon />,path: '/', element: <CandidateDashboard {...backstoryProps}/> }, { text: 'Dashboard', icon: <DashboardIcon />, path: '/', element: <CandidateDashboard /> },
{ text: 'Profile', icon: <PersonIcon />,path: '/profile', element: <CandidateProfile {...backstoryProps}/> }, { text: 'Profile', icon: <PersonIcon />, path: '/profile', element: <CandidateProfile /> },
{ text: 'Jobs', icon: <WorkIcon />,path: '/jobs', element: <JobPicker {...backstoryProps}/> }, { text: 'Jobs', icon: <WorkIcon />, path: '/jobs', element: <JobPicker /> },
{ text: 'Resumes', icon: <DescriptionIcon />,path: '/resumes', element: <BetaPage><Box>Candidate resumes page</Box></BetaPage> }, { text: 'Resumes', icon: <DescriptionIcon />,path: '/resumes', element: <BetaPage><Box>Candidate resumes page</Box></BetaPage> },
{ text: 'Content', icon: <BubbleChart />, path: '/rag', element: <Box sx={{display: "flex", width: "100%", flexDirection: "column"}}><VectorVisualizer {...backstoryProps} /><DocumentManager {...backstoryProps} /></Box>}, { text: 'Content', icon: <BubbleChart />, path: '/rag', element: <Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}><VectorVisualizer /><DocumentManager /></Box> },
{ text: 'Q&A Setup', icon: <QuizIcon />,path: '/q-a-setup', element: <BetaPage><Box>Candidate q&a setup page</Box></BetaPage> }, { text: 'Q&A Setup', icon: <QuizIcon />,path: '/q-a-setup', element: <BetaPage><Box>Candidate q&a setup page</Box></BetaPage> },
{ text: 'Analytics', icon: <AnalyticsIcon />,path: '/analytics', element: <BetaPage><Box>Candidate analytics page</Box></BetaPage> }, { text: 'Analytics', icon: <AnalyticsIcon />,path: '/analytics', element: <BetaPage><Box>Candidate analytics page</Box></BetaPage> },
{ text: 'Settings', icon: <SettingsIcon />,path: '/settings', element: <BetaPage><Box>Candidate settings page</Box></BetaPage> }, { text: 'Settings', icon: <SettingsIcon />,path: '/settings', element: <BetaPage><Box>Candidate settings page</Box></BetaPage> },

View File

@ -21,19 +21,19 @@ import { LoginRequired } from 'pages/LoginRequired';
import { BackstoryElementProps } from 'components/BackstoryTab'; import { BackstoryElementProps } from 'components/BackstoryTab';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ComingSoon } from 'components/ui/ComingSoon'; import { ComingSoon } from 'components/ui/ComingSoon';
import { useAppState } from 'hooks/GlobalContext';
interface CandidateDashboardProps extends BackstoryElementProps { interface CandidateDashboardProps extends BackstoryElementProps {
}; };
const CandidateDashboard = (props: CandidateDashboardProps) => { const CandidateDashboard = (props: CandidateDashboardProps) => {
const { setSnack, submitQuery } = props; const { setSnack } = useAppState();
const backstoryProps = { setSnack, submitQuery };
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth(); const { user } = useAuth();
const profileCompletion = 75; const profileCompletion = 75;
if (!user) { if (!user) {
return <LoginRequired {...backstoryProps}/>; return <LoginRequired />;
} }
if (user?.userType !== 'candidate') { if (user?.userType !== 'candidate') {

View File

@ -50,6 +50,7 @@ import { useAuth } from "hooks/AuthContext";
import * as Types from 'types/types'; import * as Types from 'types/types';
import { ComingSoon } from 'components/ui/ComingSoon'; import { ComingSoon } from 'components/ui/ComingSoon';
import { BackstoryPageProps } from 'components/BackstoryTab'; import { BackstoryPageProps } from 'components/BackstoryTab';
import { useAppState } from 'hooks/GlobalContext';
// Styled components // Styled components
const VisuallyHiddenInput = styled('input')({ const VisuallyHiddenInput = styled('input')({
@ -95,7 +96,7 @@ function TabPanel(props: TabPanelProps) {
} }
const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => { const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const { setSnack } = props; const { setSnack } = useAppState();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const { user, updateUserData, apiClient } = useAuth(); const { user, updateUserData, apiClient } = useAuth();

View File

@ -0,0 +1,18 @@
// types/navigation.ts
import { ReactElement } from 'react';
export interface NavigationItem {
id: string;
label: string | ReactElement;
path?: string;
icon?: ReactElement;
children?: NavigationItem[];
component?: ReactElement;
userTypes?: ('candidate' | 'employer' | 'guest')[];
exact?: boolean;
divider?: boolean;
}
export interface NavigationConfig {
items: NavigationItem[];
}