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';
interface BackstoryElementProps {
setSnack: SetSnackType,
submitQuery: ChatSubmitQueryInterface,
// setSnack: SetSnackType,
// submitQuery: ChatSubmitQueryInterface,
sx?: SxProps<Theme>,
}

View File

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

View File

@ -7,11 +7,7 @@ interface DocumentProps extends BackstoryElementProps {
}
const Document = (props: DocumentProps) => {
const { setSnack, submitQuery, filepath } = props;
const backstoryProps = {
submitQuery,
setSnack,
};
const { filepath } = props;
const [document, setDocument] = useState<string>("");
@ -43,7 +39,7 @@ const Document = (props: DocumentProps) => {
}, [document, setDocument, filepath])
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 * as Types from 'types/types';
import { BackstoryElementProps } from './BackstoryTab';
import { useAppState } from 'hooks/GlobalContext';
const VisuallyHiddenInput = styled('input')({
clip: 'rect(0 0 0 0)',
@ -50,8 +51,8 @@ const VisuallyHiddenInput = styled('input')({
});
const DocumentManager = (props: BackstoryElementProps) => {
const { setSnack, submitQuery } = props;
const theme = useTheme();
const { setSnack } = useAppState();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const { user, apiClient } = useAuth();

View File

@ -5,6 +5,7 @@ import { Quote } from 'components/Quote';
import { BackstoryElementProps } from 'components/BackstoryTab';
import { Candidate, ChatSession } from 'types/types';
import { useAuth } from 'hooks/AuthContext';
import { useAppState } from 'hooks/GlobalContext';
interface GenerateImageProps extends BackstoryElementProps {
prompt: string;
@ -13,7 +14,8 @@ interface GenerateImageProps extends BackstoryElementProps {
const GenerateImage = (props: GenerateImageProps) => {
const { user } = useAuth();
const { setSnack, chatSession, prompt } = props;
const { chatSession, prompt } = props;
const { setSnack } = useAppState();
const [processing, setProcessing] = useState<boolean>(false);
const [status, setStatus] = 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 { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
import { BackstoryElementProps } from './BackstoryTab';
import { LoginRequired } from 'components/ui/LoginRequired';
@ -122,8 +122,7 @@ const JobCreator = (props: JobCreator) => {
const { onSave } = props;
const { selectedCandidate } = useSelectedCandidate();
const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack, submitQuery } = props;
const backstoryProps = { setSnack, submitQuery };
const { setSnack } = useAppState();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isTablet = useMediaQuery(theme.breakpoints.down('md'));

View File

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

View File

@ -358,12 +358,8 @@ const MessageContainer = (props: MessageContainerProps) => {
};
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 backstoryProps = {
submitQuery,
setSnack
};
const theme = useTheme();
const type: ApiActivityType | ChatSenderType | "error" = ('activity' in message) ? message.activity : ('error' in message) ? 'error' : (message as ChatMessage).role;
const style: any = getStyle(theme, type);
@ -385,7 +381,7 @@ const Message = (props: MessageProps) => {
};
const messageView = (
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={content} {...backstoryProps} />
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={content} />
);
let metadataView = (<></>);

View File

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

View File

@ -23,7 +23,7 @@ import './VectorVisualizer.css';
import { BackstoryPageProps } from './BackstoryTab';
import { useAuth } from 'hooks/AuthContext';
import * as Types from 'types/types';
import { useSelectedCandidate } from 'hooks/GlobalContext';
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import { useNavigate } from 'react-router-dom';
interface VectorVisualizerProps extends BackstoryPageProps {
@ -180,8 +180,8 @@ type Node = {
const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
const { user, apiClient } = useAuth();
const { setSnack, submitQuery, rag, inline, sx } = props;
const backstoryProps = { setSnack, submitQuery };
const { rag, inline, sx } = props;
const { setSnack } = useAppState();
const [plotData, setPlotData] = useState<PlotData | null>(null);
const [newQuery, setNewQuery] = useState<string>('');
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 { Outlet, useLocation, Routes } from "react-router-dom";
import { Outlet, useLocation, Routes, Route } from "react-router-dom";
import { Box, Container, Paper } from '@mui/material';
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 { Header } from 'components/layout/Header';
import { Scrollable } from 'components/Scrollable';
import { Footer } from 'components/layout/Footer';
import { Snack, SetSnackType } from 'components/Snack';
import { User } from 'types/types';
import { getBackstoryDynamicRoutes } from 'components/layout/BackstoryRoutes';
import { LoadingComponent } from "components/LoadingComponent";
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;
path: string;
path: string;
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 {
children?: React.ReactNode;
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 {
setSnack: SetSnackType;
page: string;
chatRef: React.Ref<any>;
snackRef: React.Ref<any>;
submitQuery: any;
const BackstoryPageContainer = (props: BackstoryPageContainerProps) => {
const { children, sx } = props;
return (
<Container
className="BackstoryPageContainer"
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 { setSnack, page, chatRef, snackRef, submitQuery } = props;
const { page, chatRef } = props;
const { setSnack } = useAppState();
const navigate = useNavigate();
const location = useLocation();
const { guest, user } = useAuth();
const { selectedCandidate } = useSelectedCandidate();
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
const [navigationItems, setNavigationItems] = useState<NavigationItem[]>([]);
useEffect(() => {
setNavigationLinks(getNavigationLinks(user));
const userType = user?.userType || null;
setNavigationItems(getMainNavigationItems(userType));
}, [user]);
let dynamicRoutes;
if (guest) {
dynamicRoutes = getBackstoryDynamicRoutes({
user,
setSnack,
submitQuery,
chatRef
});
}
// Generate dynamic routes from navigation config
const generateRoutes = () => {
if (!guest) return null;
const userType = user?.userType || null;
const routes = getAllRoutes(userType);
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 (
<Box sx={{ height: "100%", maxHeight: "100%", minHeight: "100%", flexDirection: "column" }}>
<Header {...{ setSnack, currentPath: page, navigate, navigationLinks }} />
<Box sx={{
height: "100%",
maxHeight: "100%",
minHeight: "100%",
flexDirection: "column"
}}>
<Header
setSnack={setSnack}
currentPath={page}
navigate={navigate}
navigationItems={navigationItems}
/>
<Box sx={{
display: "flex",
width: "100%",
@ -174,49 +142,48 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
flexDirection: "column",
backgroundColor: "#D3CDBF", /* Warm Gray */
}}>
<Scrollable
className="BackstoryPageScrollable"
sx={{
m: 0,
p: 0,
mt: "72px", /* Needs to be kept in sync with the height of Header if the Header theme changes */
display: "flex",
flexDirection: "column",
backgroundColor: "background.default",
height: "100%",
maxHeight: "100%",
minHeight: "100%",
minWidth: "min-content",
}}
>
<BackstoryPageContainer>
{!guest &&
<Box>
<LoadingComponent
loadingText="Creating session..."
loaderType="linear"
withFade={true}
fadeDuration={1200} />
</Box>
}
{guest && <>
<Outlet />
{dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>}
</>
}
{location.pathname === "/" && <Footer />}
</BackstoryPageContainer>
</Scrollable>
<Snack ref={snackRef} />
<Scrollable
className="BackstoryPageScrollable"
sx={{
m: 0,
p: 0,
mt: "72px", /* Needs to be kept in sync with the height of Header */
display: "flex",
flexDirection: "column",
backgroundColor: "background.default",
height: "100%",
maxHeight: "100%",
minHeight: "100%",
minWidth: "min-content",
}}
>
<BackstoryPageContainer>
{!guest && (
<Box>
<LoadingComponent
loadingText="Creating session..."
loaderType="linear"
withFade={true}
fadeDuration={1200}
/>
</Box>
)}
{guest && (
<>
<Outlet />
<Routes>
{generateRoutes()}
</Routes>
</>
)}
{location.pathname === "/" && <Footer />}
</BackstoryPageContainer>
</Scrollable>
</Box>
</Box>
);
};
export type {
NavigationLinkType
};
export {
BackstoryLayout
};
};

View File

@ -1,95 +1,45 @@
// components/layout/BackstoryRoutes.tsx
import React, { Ref, ReactNode } from "react";
import { Route } from "react-router-dom";
import { Typography } from '@mui/material';
import { BackstoryPageProps } from '../BackstoryTab';
import { ConversationHandle } from '../Conversation';
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 {
chatRef: Ref<ConversationHandle>;
user?: User | null;
}
const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNode => {
const { user, setSnack, submitQuery, chatRef } = props;
const backstoryProps = {
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} />} />,
];
const { user, chatRef } = props;
const userType = user?.userType || null;
if (!user) {
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />);
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 />} />);
// Get all routes from navigation config
const routes = getAllRoutes(userType);
if (user.userType === 'candidate') {
routes.splice(-1, 0, ...[
<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 />} />,
]);
}
return routes.map((route: NavigationItem, index: number) => {
if (!route.path || !route.component) return null;
if (user.userType === 'employer') {
routes.splice(-1, 0, ...[
<Route key={`${index++}`} path="/search" element={<SearchPage />} />,
<Route key={`${index++}`} path="/saved" element={<SavedPage />} />,
<Route key={`${index++}`} path="/jobs" element={<JobsPage />} />,
<Route key={`${index++}`} path="/company" element={<CompanyPage />} />,
]);
}
}
// Clone the component and pass necessary props
const componentWithProps = React.cloneElement(route.component as React.ReactElement, {
// Special handling for chat component ref
...(route.id === 'chat' && { ref: chatRef }),
// Preserve any existing props
...(route.component.props || {}),
});
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
return routes;
return (
<Route
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 {
AppBar,
@ -17,6 +18,14 @@ import {
Fade,
Popover,
Paper,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
Collapse,
List,
ListItem,
ListItemButton,
} from '@mui/material';
import { styled, useTheme } from '@mui/material/styles';
import {
@ -26,14 +35,15 @@ import {
Logout,
Settings,
ExpandMore,
ExpandLess,
KeyboardArrowDown,
} from '@mui/icons-material';
import { NavigationLinkType } from 'components/layout/BackstoryLayout';
import { NavigationItem } from 'types/navigation';
import { Beta } from 'components/ui/Beta';
import { Candidate, Employer } from 'types/types';
import { SetSnackType } from 'components/Snack';
import { CopyBubble } from 'components/CopyBubble';
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import 'components/layout/Header.css';
import { useAuth } from 'hooks/AuthContext';
@ -79,39 +89,17 @@ const UserButton = styled(Button)(({ theme }) => ({
const MobileDrawer = styled(Drawer)(({ theme }) => ({
'& .MuiDrawer-paper': {
width: 280,
width: 320,
backgroundColor: theme.palette.background.paper,
},
}));
const MobileMenuTabs = 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,
'& .MuiTypography-root': {
color: theme.palette.primary.contrastText,
},
'& .MuiSvgIcon-root': {
color: theme.palette.primary.contrastText,
},
},
},
'& .MuiTabs-indicator': {
display: 'none',
const DropdownButton = styled(Button)(({ theme }) => ({
color: theme.palette.primary.contrastText,
textTransform: 'none',
minHeight: 48,
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}));
@ -123,51 +111,11 @@ const UserMenuContainer = styled(Paper)(({ theme }) => ({
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 {
transparent?: boolean;
className?: string;
navigate: NavigateFunction;
navigationLinks: NavigationLinkType[];
navigationItems: NavigationItem[];
currentPath: string;
sessionId?: string | null;
setSnack: SetSnackType;
@ -181,7 +129,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
transparent = false,
className,
navigate,
navigationLinks,
navigationItems,
sessionId,
setSnack,
} = props;
@ -190,42 +138,17 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const name = (user?.firstName || user?.email || '');
const mainNavSections: NavigationLinkType[] = [
{ path: '/', label: <BackstoryLogo /> },
...navigationLinks
];
// State for page navigation - only for main navigation
const [currentTab, setCurrentTab] = useState<string | false>("/");
const [userMenuTab, setUserMenuTab] = useState<string>("");
// State for desktop dropdown menus
const [dropdownAnchors, setDropdownAnchors] = useState<{ [key: string]: HTMLElement | null }>({});
// State for mobile drawer
const [mobileOpen, setMobileOpen] = useState(false);
const [mobileExpanded, setMobileExpanded] = useState<{ [key: string]: boolean }>({});
// State for user menu
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
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
const userMenuItems = [
{
@ -263,22 +186,45 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
}
];
useEffect(() => {
const mainSection = getTailSection(location.pathname);
// Only update if the section is different from current tab
if (mainSection !== currentTab) {
setCurrentTab(mainSection); // mainSection is either a string or false
// Helper function to check if current path matches navigation item
const isCurrentPath = (item: NavigationItem): boolean => {
if (!item.path) return false;
if (item.exact) {
return location.pathname === item.path;
}
}, [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>) => {
setUserMenuAnchor(event.currentTarget);
};
const handleUserMenuClose = () => {
setUserMenuAnchor(null);
setUserMenuTab("");
};
const handleUserMenuAction = (item: typeof userMenuItems[0]) => {
@ -288,83 +234,152 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
}
};
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
// Navigation handlers
const handleNavigate = (path: string) => {
navigate(path);
setMobileOpen(false);
// Close all dropdowns
setDropdownAnchors({});
};
const handleTabChange = (event: React.SyntheticEvent, newValue: string | false) => {
if (newValue !== false) {
setCurrentTab(newValue);
navigate(newValue);
}
};
// Render desktop navigation links
const renderNavLinks = () => {
// Render desktop navigation with dropdowns
const renderDesktopNavigation = () => {
return (
<Tabs
value={currentTab}
onChange={handleTabChange}
indicatorColor="secondary"
textColor="inherit"
variant="fullWidth"
allowScrollButtonsMobile
aria-label="Backstory navigation"
>
{mainNavSections.map((section) => (
<Tab
sx={{
minWidth: section.path === '/' ? "max-content" : "auto",
}}
key={section.path}
value={section.path}
label={section.label}
/>
))}
</Tabs>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{navigationItems.map((item) => {
const hasChildren = item.children && item.children.length > 0;
const isActive = isCurrentPath(item) || hasActiveChild(item);
if (hasChildren) {
return (
<Box key={item.id}>
<DropdownButton
onClick={(e) => handleDropdownOpen(e, item.id)}
endIcon={<KeyboardArrowDown />}
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>
<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
const renderDrawerContent = () => {
return (
<>
<MobileMenuTabs
orientation="vertical"
value={currentTab}
onChange={handleTabChange}
>
{mainNavSections.map((section) => (
<Tab
key={section.path || '/'}
value={section.path}
label={
<MenuItemBox>
{section.label}
</MenuItemBox>
}
onClick={(e) => {
handleDrawerToggle();
setCurrentTab(section.path);
navigate(section.path);
// Render mobile accordion navigation
const renderMobileNavigation = () => {
const renderNavigationItem = (item: NavigationItem, depth: number = 0) => {
const hasChildren = item.children && item.children.length > 0;
const isActive = isCurrentPath(item) || hasActiveChild(item);
const isExpanded = mobileExpanded[item.id];
return (
<Box key={item.id}>
<ListItem disablePadding sx={{ pl: depth * 2 }}>
<ListItemButton
onClick={() => {
if (hasChildren) {
handleMobileToggle(item.id);
} else if (item.path) {
handleNavigate(item.path);
}
}}
selected={isActive}
sx={{
backgroundColor: isActive ? 'action.selected' : 'transparent',
'&.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
</Button>
</Box>
{item.icon && (
<ListItemIcon sx={{ minWidth: 36 }}>
{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 = () => {
return (
<UserMenuContainer>
<UserMenuTabs
orientation="vertical"
value={userMenuTab}
onChange={(e, newValue) => setUserMenuTab(newValue)}
>
<List dense>
{userMenuItems.map((item, index) => (
item.id === 'divider' ? (
<MenuDivider key={`divider-${index}`} />
<Divider key={`divider-${index}`} />
) : (
<Tab
key={item.id}
value={item.id}
label={
<MenuItemBox>
{item.icon}
<Typography variant="body2">{item.label}</Typography>
</MenuItemBox>
}
onClick={() => handleUserMenuAction(item)}
/>
<ListItem key={item.id} disablePadding>
<ListItemButton onClick={() => handleUserMenuAction(item)}>
{item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
)
))}
</UserMenuTabs>
</List>
</UserMenuContainer>
);
};
@ -403,19 +409,17 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const renderUserSection = () => {
if (!user) {
return (
<>
<Button
color="info"
variant="contained"
onClick={() => navigate("/login") }
sx={{
display: { xs: 'none', sm: 'block' },
color: theme.palette.primary.contrastText,
}}
>
Login
</Button>
</>
<Button
color="info"
variant="contained"
onClick={() => navigate("/login")}
sx={{
display: { xs: 'none', sm: 'block' },
color: theme.palette.primary.contrastText,
}}
>
Login
</Button>
);
}
@ -428,10 +432,10 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
aria-expanded={userMenuOpen ? 'true' : undefined}
>
<Avatar sx={{
width: 32,
height: 32,
bgcolor: theme.palette.secondary.main,
}}>
width: 32,
height: 32,
bgcolor: theme.palette.secondary.main,
}}>
{name.charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
@ -472,7 +476,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<Toolbar disableGutters>
{/* Navigation Links - Desktop */}
<NavLinksContainer>
{renderNavLinks()}
{renderDesktopNavigation()}
</NavLinksContainer>
{/* User Actions Section */}
@ -492,22 +496,27 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
</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={() => { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }}
size="large"
/>}
{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={() => {
navigate(`${window.location.pathname}?id=${sessionId}`);
setSnack("Link copied!");
}}
size="large"
/>
)}
</UserActionsContainer>
{/* Mobile Navigation Drawer */}
@ -520,11 +529,14 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
keepMounted: true,
}}
>
{renderDrawerContent()}
{renderMobileNavigation()}
</MobileDrawer>
</Toolbar>
</Container>
<Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} />
<Beta
sx={{ left: "-90px", "& .mobile": { right: "-72px" } }}
onClick={() => { navigate('/docs/beta'); }}
/>
</StyledAppBar>
);
};

View File

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

View File

@ -7,7 +7,7 @@ import { BackstoryElementProps } from 'components/BackstoryTab';
import { JobInfo } from 'components/ui/JobInfo';
import { Job, JobFull } from "types/types";
import { useAuth } from 'hooks/AuthContext';
import { useSelectedJob } from 'hooks/GlobalContext';
import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
interface JobPickerProps extends BackstoryElementProps {
onSelect?: (job: JobFull) => void
@ -17,7 +17,7 @@ const JobPicker = (props: JobPickerProps) => {
const { onSelect } = props;
const { apiClient } = useAuth();
const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack } = props;
const { setSnack } = useAppState();
const [jobs, setJobs] = useState<JobFull[] | null>(null);
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';
// Assuming you're using React Router
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from 'hooks/AuthContext';
import { SetSnackType, SeverityType, Snack } from 'components/Snack';
// ============================
// Storage Keys
@ -43,6 +44,10 @@ export interface AppState {
}
export interface AppStateActions {
// Snackbar
setSnack: SetSnackType;
//
setSelectedCandidate: (candidate: Types.Candidate | null) => void;
setSelectedJob: (job: Types.Job | null) => void;
setSelectedEmployer: (employer: Types.Employer | null) => void;
@ -358,7 +363,12 @@ export function useAppStateLogic(): AppStateContextType {
console.log('Cleared all route state');
}, []);
const emptySetSnack: SetSnackType = (message: string, severity?: SeverityType) => {
return;
};
return {
setSnack: emptySetSnack,
selectedCandidate,
selectedJob,
selectedEmployer,
@ -385,10 +395,17 @@ const AppStateContext = createContext<AppStateContextType | null>(null);
export function AppStateProvider({ children }: { children: React.ReactNode }) {
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 (
<AppStateContext.Provider value={appState}>
{children}
<Snack ref={snackRef} />
</AppStateContext.Provider>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,19 +4,15 @@ import {
Container,
Paper,
Typography,
Grid,
Alert,
Tabs,
Tab,
Card,
CardContent,
Divider,
Avatar,
} from '@mui/material';
import {
Person,
PersonAdd,
AccountCircle,
} from '@mui/icons-material';
import 'react-phone-number-input/style.css';
import './LoginPage.css';
@ -29,10 +25,11 @@ import { BackstoryPageProps } from 'components/BackstoryTab';
import { LoginForm } from "components/EmailVerificationComponents";
import { CandidateRegistrationForm } from "components/RegistrationForms";
import { useNavigate } from 'react-router-dom';
import { useAppState } from 'hooks/GlobalContext';
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const navigate = useNavigate();
const { setSnack } = props;
const { setSnack } = useAppState();
const [tabValue, setTabValue] = useState(0);
const [loading, setLoading] = useState(false);
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 { useAuth } from 'hooks/AuthContext';
import { Candidate } from 'types/types';
import { useAppState } from 'hooks/GlobalContext';
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
const { setSnack, submitQuery } = props;
const { setSnack } = useAppState();
const { user } = useAuth();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@ -28,7 +29,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
setQuestions([
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
{candidate.questions?.map((q, i: number) =>
<BackstoryQuery key={i} question={q} submitQuery={submitQuery} />
<BackstoryQuery key={i} question={q} />
)}
</Box>,
<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.`}
</MuiMarkdown>
</Box>]);
}, [candidate, isMobile, submitQuery]);
}, [candidate, isMobile]);
if (!candidate) {
return (<></>);
@ -51,9 +52,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
type: "chat",
placeholder: `What would you like to know about ${candidate?.firstName}?`,
resetLabel: "chat",
setSnack,
defaultPrompts: questions,
submitQuery,
defaultPrompts: questions,
}} />
</Box>);
});

View File

@ -11,6 +11,7 @@ import { Conversation } from 'components/Conversation';
import { BackstoryPageProps } from 'components/BackstoryTab';
import { ChatQuery, ChatMessage } from "types/types";
import './ResumeBuilderPage.css';
import { useAppState } from 'hooks/GlobalContext';
/**
* ResumeBuilder component
@ -19,11 +20,6 @@ import './ResumeBuilderPage.css';
* with different layouts for mobile and desktop views.
*/
const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const {
sx,
setSnack,
submitQuery,
} = props
// State for editing job description
const [hasJobDescription, setHasJobDescription] = 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 { DocumentManager } from 'components/DocumentManager';
import { JobPicker } from 'components/ui/JobPicker';
import { useAppState } from 'hooks/GlobalContext';
interface DashboardProps extends BackstoryPageProps {
userName?: string;
@ -58,15 +59,14 @@ const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps)
const [activeTab, setActiveTab] = useState<string>(subPage);
const { user, isLoading, isInitializing, isAuthenticated } = useAuth();
const profileCompletion = 75;
const { setSnack, submitQuery } = props;
const backstoryProps = { setSnack, submitQuery };
const { setSnack } = useAppState();
const sidebarItems = [
{ text: 'Dashboard', icon: <DashboardIcon />,path: '/', element: <CandidateDashboard {...backstoryProps}/> },
{ text: 'Profile', icon: <PersonIcon />,path: '/profile', element: <CandidateProfile {...backstoryProps}/> },
{ text: 'Jobs', icon: <WorkIcon />,path: '/jobs', element: <JobPicker {...backstoryProps}/> },
{ text: 'Dashboard', icon: <DashboardIcon />, path: '/', element: <CandidateDashboard /> },
{ text: 'Profile', icon: <PersonIcon />, path: '/profile', element: <CandidateProfile /> },
{ text: 'Jobs', icon: <WorkIcon />, path: '/jobs', element: <JobPicker /> },
{ 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: '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> },

View File

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

View File

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