Menus restructured

This commit is contained in:
James Ketr 2025-06-11 15:11:35 -07:00
parent 7a166fe920
commit 74201d0a71
12 changed files with 389 additions and 216 deletions

View File

@ -6,6 +6,7 @@ import {
Button, Button,
Paper, Paper,
Typography, Typography,
LinearProgress,
} from '@mui/material'; } from '@mui/material';
import { Job, Candidate, SkillAssessment } from "types/types"; import { Job, Candidate, SkillAssessment } from "types/types";
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
@ -16,6 +17,9 @@ import { Message } from './Message';
import InputIcon from '@mui/icons-material/Input'; import InputIcon from '@mui/icons-material/Input';
import TuneIcon from '@mui/icons-material/Tune'; import TuneIcon from '@mui/icons-material/Tune';
import ArticleIcon from '@mui/icons-material/Article'; import ArticleIcon from '@mui/icons-material/Article';
import { StatusBox, StatusIcon } from './ui/StatusIcon';
import { CopyBubble } from './CopyBubble';
import { useAppState } from 'hooks/GlobalContext';
interface ResumeGeneratorProps { interface ResumeGeneratorProps {
job: Job; job: Job;
@ -30,12 +34,15 @@ const defaultMessage: Types.ChatMessageStatus = {
const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => { const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => {
const { job, candidate, skills, onComplete } = props; const { job, candidate, skills, onComplete } = props;
const { setSnack } = useAppState();
const { apiClient, user } = useAuth(); const { apiClient, user } = useAuth();
const [resume, setResume] = useState<string>(''); const [resume, setResume] = useState<string>('');
const [prompt, setPrompt] = useState<string>(''); const [prompt, setPrompt] = useState<string>('');
const [systemPrompt, setSystemPrompt] = useState<string>(''); const [systemPrompt, setSystemPrompt] = useState<string>('');
const [generated, setGenerated] = useState<boolean>(false); const [generated, setGenerated] = useState<boolean>(false);
const [tabValue, setTabValue] = useState<string>('resume'); const [tabValue, setTabValue] = useState<string>('resume');
const [status, setStatus] = useState<string>('');
const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null);
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
setTabValue(newValue); setTabValue(newValue);
@ -48,18 +55,36 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
setGenerated(true); setGenerated(true);
setStatusType("thinking");
setStatus("Starting resume generation...");
const generateResumeHandlers = { const generateResumeHandlers = {
onMessage: (message: Types.ChatMessageResume) => {
setSystemPrompt(message.systemPrompt || '');
setPrompt(message.prompt || '');
setResume(message.resume || '');
setStatus('');
},
onStreaming: (chunk: Types.ChatMessageStreaming) => { onStreaming: (chunk: Types.ChatMessageStreaming) => {
if (status === '') {
setStatus('Generating resume...');
setStatusType("generating");
}
setResume(chunk.content); setResume(chunk.content);
},
onStatus: (status: Types.ChatMessageStatus) => {
console.log('status:', status.content);
setStatusType(status.activity);
setStatus(status.content);
},
onComplete: () => {
onComplete && onComplete(resume);
} }
}; };
const generateResume = async () => { const generateResume = async () => {
const request: any = await apiClient.generateResume(candidate.id || '', skills, generateResumeHandlers); const request: any = await apiClient.generateResume(candidate.id || '', skills, generateResumeHandlers);
const result = await request.promise; const result = await request.promise;
setSystemPrompt(result.systemPrompt)
setPrompt(result.prompt)
setResume(result.resume)
}; };
generateResume(); generateResume();
@ -72,18 +97,28 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
}}> }}>
{user?.isAdmin && <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}> {user?.isAdmin && <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 1 }}>
<Tabs value={tabValue} onChange={handleTabChange} centered> <Tabs value={tabValue} onChange={handleTabChange} centered>
<Tab sx={{ display: systemPrompt ? "flex" : "none" }} value="system" icon={<TuneIcon />} label="System" /> <Tab disabled={systemPrompt === ''} value="system" icon={<TuneIcon />} label="System" />
<Tab sx={{ display: prompt ? "flex" : "none" }} value="prompt" icon={<InputIcon />} label="Prompt" /> <Tab disabled={prompt === ''} value="prompt" icon={<InputIcon />} label="Prompt" />
<Tab sx={{ display: resume ? "flex" : "none" }} value="resume" icon={<ArticleIcon />} label="Resume" /> <Tab disabled={resume === ''} value="resume" icon={<ArticleIcon />} label="Resume" />
</Tabs> </Tabs>
</Box>} </Box>}
<Paper elevation={3} sx={{ p: 3, m: 1, mt: 0 }}><Scrollable autoscroll sx={{ display: "flex", flexGrow: 1 }}> {status && <Box sx={{ mt: 0, mb: 1 }}>
<StatusBox>
{statusType && <StatusIcon type={statusType} />}
<Typography variant="body2" sx={{ ml: 1 }}>
{status || 'Processing...'}
</Typography>
</StatusBox>
{status && <LinearProgress sx={{ mt: 1 }} />}
</Box>}
<Paper elevation={3} sx={{ p: 3, m: 1, mt: 0 }}><Scrollable autoscroll sx={{ display: "flex", flexGrow: 1, position: "relative" }}>
{tabValue === 'system' && <pre>{systemPrompt}</pre>} {tabValue === 'system' && <pre>{systemPrompt}</pre>}
{tabValue === 'prompt' && <pre>{prompt}</pre>} {tabValue === 'prompt' && <pre>{prompt}</pre>}
{tabValue === 'resume' && <StyledMarkdown content={resume} />} {tabValue === 'resume' && <><CopyBubble onClick={() => { setSnack('Resume copied to clipboard!'); }} sx={{ position: "absolute", top: 0, right: 0 }} content={resume} /><StyledMarkdown content={resume} /></>}
</Scrollable></Paper> </Scrollable></Paper>
</Box> </Box>
) )

View File

@ -175,6 +175,17 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
} }
}); });
// Add divider if we have items before system group
if (items.length > 0 && userMenuGroups.system.length > 0) {
items.push({
id: 'divider',
label: '',
icon: null,
action: () => { },
group: 'divider'
});
}
// Add account group items // Add account group items
userMenuGroups.account.forEach(item => { userMenuGroups.account.forEach(item => {
if (!item.divider) { if (!item.divider) {

View File

@ -16,17 +16,17 @@
} }
.aibanner-label { .aibanner-label {
width: 300px;
position: absolute;
display: flex;
right: -70px; right: -70px;
top: 40px; top: 40px;
height: 32px; height: 32px;
font-size: 20px;
width: 300px;
position: absolute;
display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transform: rotate(45deg); transform: rotate(45deg);
transform-origin: center center; transform-origin: center center;
font-size: 20px;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
color: #484848; color: #484848;
@ -36,3 +36,12 @@
pointer-events: auto; pointer-events: auto;
opacity: 0.5; opacity: 0.5;
} }
.aibanner-label-minimal,
.aibanner-label-small {
right: -100px;
top: 30px;
height: 20px;
font-size: 15px;
width: 300px;
}

View File

@ -3,18 +3,25 @@ import Box from '@mui/material/Box';
import { SxProps } from '@mui/material/styles'; import { SxProps } from '@mui/material/styles';
import './AIBanner.css'; import './AIBanner.css';
import { useMediaQuery, useTheme } from '@mui/material';
type AIBannerProps = { type AIBannerProps = {
sx?: SxProps; sx?: SxProps;
variant?: "minimal" | "small" | "normal" | undefined;
} }
const AIBanner: React.FC<AIBannerProps> = (props : AIBannerProps) => { const AIBanner: React.FC<AIBannerProps> = (props : AIBannerProps) => {
const { sx = {} } = props; const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const {
sx = {},
variant = isMobile ? "small" : "normal",
} = props;
const aibannerRef = useRef<HTMLElement | null>(null); const aibannerRef = useRef<HTMLElement | null>(null);
return ( return (
<Box sx={sx} className='aibanner-clipper'> <Box sx={sx} className='aibanner-clipper'>
<Box ref={aibannerRef} className='aibanner-label'> <Box ref={aibannerRef} className={` aibanner-label-${variant} aibanner-label`}>
<Box>AI Generated</Box> <Box>AI Generated</Box>
</Box> </Box>
</Box> </Box>

View File

@ -20,7 +20,7 @@ interface CandidateInfoProps {
sx?: SxProps; sx?: SxProps;
action?: string; action?: string;
elevation?: number; elevation?: number;
variant?: "small" | "normal" | null variant?: "minimal" | "small" | "normal" | undefined;
}; };
const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => { const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
@ -33,7 +33,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
variant = "normal" variant = "normal"
} = props; } = props;
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md')) || variant === "minimal";
const ai: CandidateAI | null = ('isAI' in candidate) ? candidate as CandidateAI : null; const ai: CandidateAI | null = ('isAI' in candidate) ? candidate as CandidateAI : null;
const isAdmin = user?.isAdmin; const isAdmin = user?.isAdmin;
@ -77,7 +77,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
}} }}
{...rest} {...rest}
> >
{ai && <AIBanner />} {ai && <AIBanner variant={variant} />}
<Box sx={{ display: "flex", flexDirection: "row" }}> <Box sx={{ display: "flex", flexDirection: "row" }}>
<Avatar <Avatar
@ -132,7 +132,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
</Box> </Box>
<Box> <Box>
{(!isMobile || variant !== "small") && ( {(!isMobile && variant === "normal") && (
<Box sx={{ minHeight: "5rem" }}> <Box sx={{ minHeight: "5rem" }}>
<Typography <Typography
ref={descriptionRef} ref={descriptionRef}
@ -178,7 +178,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
</Box> </Box>
)} )}
{variant !== "small" && <> {(variant !== "small" && variant !== "minimal") && <>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
{candidate.location && {candidate.location &&

View File

@ -61,7 +61,20 @@ const CandidatePicker = (props: CandidatePickerProps) => {
<Paper key={`${u.username}`} <Paper key={`${u.username}`}
onClick={() => { onSelect ? onSelect(u) : setSelectedCandidate(u); }} onClick={() => { onSelect ? onSelect(u) : setSelectedCandidate(u); }}
sx={{ cursor: "pointer" }}> sx={{ cursor: "pointer" }}>
<CandidateInfo variant="small" sx={{ maxWidth: "320px", "cursor": "pointer", backgroundColor: (selectedCandidate?.id === u.id) ? "#f0f0f0" : "inherit", border: "2px solid transparent", "&:hover": { border: "2px solid orange" } }} candidate={u} /> <CandidateInfo variant="small"
sx={{
maxWidth: "100%",
minWidth: "320px",
width: "320px",
"cursor": "pointer",
backgroundColor: (selectedCandidate?.id === u.id) ? "#f0f0f0" : "inherit",
border: "2px solid transparent",
"&:hover": {
border: "2px solid orange"
}
}}
candidate={u}
/>
</Paper> </Paper>
)} )}
</Box> </Box>

View File

@ -27,7 +27,7 @@ interface JobInfoProps {
sx?: SxProps; sx?: SxProps;
action?: string; action?: string;
elevation?: number; elevation?: number;
variant?: "small" | "normal" | null variant?: "minimal" | "small" | "normal" | null
}; };
@ -42,7 +42,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
variant = "normal" variant = "normal"
} = props; } = props;
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md')) || variant === "minimal";
const isAdmin = user?.isAdmin; const isAdmin = user?.isAdmin;
const [adminStatus, setAdminStatus] = useState<string | null>(null); const [adminStatus, setAdminStatus] = useState<string | null>(null);
const [adminStatusType, setAdminStatusType] = useState<Types.ApiActivityType | null>(null); const [adminStatusType, setAdminStatusType] = useState<Types.ApiActivityType | null>(null);
@ -212,21 +212,22 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
borderStyle: 'solid', borderStyle: 'solid',
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
flexDirection: "column", flexDirection: "column",
...sx ...sx,
minWidth: 0,
}} }}
{...rest} {...rest}
> >
<Box sx={{ display: "flex", flexGrow: 1, p: 1, pb: 0, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}> <Box sx={{ display: "flex", flexGrow: 1, p: 1, pb: 0, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}>
<Box sx={{ <Box sx={{
display: "flex", flexDirection: (isMobile || variant === "small") ? "column" : "row", display: "flex", flexDirection: (isMobile || variant === "small") ? "column" : "row",
"& > div > div > :first-of-type": { fontWeight: "bold" }, "& > div > div > :first-of-type": { fontWeight: "bold", whiteSpace: "nowrap" },
"& > div > div > :last-of-type": { mb: 0.75, mr: 1 } "& > div > div > :last-of-type": { mb: 0.75, mr: 1 }
}}> }}>
<Box sx={{ display: "flex", flexDirection: isMobile ? "row" : "column", flexGrow: 1, gap: 1 }}> <Box sx={{ display: "flex", flexDirection: isMobile ? "row" : "column", flexGrow: 1, gap: 1 }}>
{activeJob.company && {activeJob.company &&
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: "0.8rem" }}>
<Box>Company</Box> <Box>Company</Box>
<Box>{activeJob.company}</Box> <Box sx={{ whiteSpace: "nowrap" }}>{activeJob.company}</Box>
</Box> </Box>
} }
{activeJob.title && {activeJob.title &&
@ -236,10 +237,10 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
</Box> </Box>
} }
</Box> </Box>
<Box sx={{ display: "flex", flexDirection: "column", width: (variant !== "small") ? "75%" : "100%" }}> <Box sx={{ display: "flex", flexDirection: "column", width: (variant !== "small" && variant !== "minimal") ? "75%" : "100%" }}>
{!isMobile && activeJob.summary && <Box sx={{ fontSize: "0.8rem" }}> {!isMobile && activeJob.summary && <Box sx={{ fontSize: "0.8rem" }}>
<Box>Summary</Box> <Box>Summary</Box>
<Box sx={{ minHeight: "5rem" }}> <Box sx={{ minHeight: variant === "small" ? "5rem" : "inherit" }}>
<Typography <Typography
ref={descriptionRef} ref={descriptionRef}
variant="body1" variant="body1"
@ -286,14 +287,14 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
</Box> </Box>
</Box> </Box>
{variant !== "small" && <> {(variant !== "small" && variant !== "minimal") && <>
{activeJob.details && {activeJob.details &&
<Typography variant="body2" sx={{ mb: 1 }}> <Typography variant="body2" sx={{ mb: 1 }}>
<strong>Location:</strong> {activeJob.details.location.city}, {activeJob.details.location.state || activeJob.details.location.country} <strong>Location:</strong> {activeJob.details.location.city}, {activeJob.details.location.state || activeJob.details.location.country}
</Typography> </Typography>
} }
{activeJob.owner && <Typography variant="body2"> {activeJob.owner && <Typography variant="body2">
<strong>Created by:</strong> {activeJob.owner.fullName} <strong>Submitted by:</strong> {activeJob.owner.fullName}
</Typography>} </Typography>}
{activeJob.createdAt && {activeJob.createdAt &&
<Typography variant="caption">Created: {activeJob.createdAt.toISOString()}</Typography> <Typography variant="caption">Created: {activeJob.createdAt.toISOString()}</Typography>
@ -304,7 +305,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Typography variant="caption">Job ID: {job.id}</Typography> <Typography variant="caption">Job ID: {job.id}</Typography>
</>} </>}
{variant !== 'small' && <><Divider />{renderJobRequirements()}</>} {(variant !== 'small' && variant !== 'minimal') && <><Divider />{renderJobRequirements()}</>}
</Box > </Box >
{isAdmin && {isAdmin &&

View File

@ -52,7 +52,20 @@ const JobPicker = (props: JobPickerProps) => {
<Paper key={`${j.id}`} <Paper key={`${j.id}`}
onClick={() => { onSelect ? onSelect(j) : setSelectedJob(j); }} onClick={() => { onSelect ? onSelect(j) : setSelectedJob(j); }}
sx={{ cursor: "pointer" }}> sx={{ cursor: "pointer" }}>
<JobInfo variant="small" sx={{ maxWidth: "320px", "cursor": "pointer", backgroundColor: (selectedJob?.id === j.id) ? "#f0f0f0" : "inherit", border: "2px solid transparent", "&:hover": { border: "2px solid orange" } }} job={j} /> <JobInfo variant="small"
sx={{
maxWidth: "100%",
minWidth: "320px",
width: "320px",
"cursor": "pointer",
backgroundColor: (selectedJob?.id === j.id) ? "#f0f0f0" : "inherit",
border: "2px solid transparent",
"&:hover": {
border: "2px solid orange"
}
}}
job={j}
/>
</Paper> </Paper>
)} )}
</Box> </Box>

View File

@ -1,4 +1,4 @@
import React from 'react'; import React from "react";
import { import {
Chat as ChatIcon, Chat as ChatIcon,
Dashboard as DashboardIcon, Dashboard as DashboardIcon,
@ -17,239 +17,319 @@ import {
Quiz as QuizIcon, Quiz as QuizIcon,
Analytics as AnalyticsIcon, Analytics as AnalyticsIcon,
BubbleChart, BubbleChart,
} from '@mui/icons-material'; } from "@mui/icons-material";
import { BackstoryLogo } from 'components/ui/BackstoryLogo'; import { BackstoryLogo } from "components/ui/BackstoryLogo";
import { HomePage } from 'pages/HomePage'; import { HomePage } from "pages/HomePage";
import { CandidateChatPage } from 'pages/CandidateChatPage'; import { CandidateChatPage } from "pages/CandidateChatPage";
import { DocsPage } from 'pages/DocsPage'; import { DocsPage } from "pages/DocsPage";
import { CreateProfilePage } from 'pages/candidate/ProfileWizard'; import { CreateProfilePage } from "pages/candidate/ProfileWizard";
import { VectorVisualizerPage } from 'pages/VectorVisualizerPage'; import { VectorVisualizerPage } from "pages/VectorVisualizerPage";
import { BetaPage } from 'pages/BetaPage'; import { BetaPage } from "pages/BetaPage";
import { CandidateListingPage } from 'pages/FindCandidatePage'; import { CandidateListingPage } from "pages/FindCandidatePage";
import { JobAnalysisPage } from 'pages/JobAnalysisPage'; import { JobAnalysisPage } from "pages/JobAnalysisPage";
import { GenerateCandidate } from 'pages/GenerateCandidate'; import { GenerateCandidate } from "pages/GenerateCandidate";
import { LoginPage } from 'pages/LoginPage'; import { LoginPage } from "pages/LoginPage";
import { EmailVerificationPage } from 'components/EmailVerificationComponents'; import { EmailVerificationPage } from "components/EmailVerificationComponents";
import { Box, Typography } from '@mui/material'; import { Box, Typography } from "@mui/material";
import { CandidateDashboard } from 'pages/candidate/Dashboard'; import { CandidateDashboard } from "pages/candidate/Dashboard";
import { NavigationConfig, NavigationItem } from 'types/navigation'; import { NavigationConfig, NavigationItem } from "types/navigation";
import { HowItWorks } from 'pages/HowItWorks'; import { HowItWorks } from "pages/HowItWorks";
import SchoolIcon from '@mui/icons-material/School'; import SchoolIcon from "@mui/icons-material/School";
import { CandidateProfile } from 'pages/candidate/Profile'; import { CandidateProfile } from "pages/candidate/Profile";
import { Settings } from 'pages/candidate/Settings'; import { Settings } from "pages/candidate/Settings";
import { VectorVisualizer } from 'components/VectorVisualizer'; import { VectorVisualizer } from "components/VectorVisualizer";
import { DocumentManager } from 'components/DocumentManager'; import { DocumentManager } from "components/DocumentManager";
import { useAuth } from "hooks/AuthContext";
import { useNavigate } from "react-router-dom";
// Beta page components for placeholder routes // Beta page components for placeholder routes
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>); const BackstoryPage = () => (
const ResumesPage = () => (<BetaPage><Typography variant="h4">Resumes</Typography></BetaPage>); <BetaPage>
const QASetupPage = () => (<BetaPage><Typography variant="h4">Q&A Setup</Typography></BetaPage>); <Typography variant="h4">Backstory</Typography>
const SearchPage = () => (<BetaPage><Typography variant="h4">Search</Typography></BetaPage>); </BetaPage>
const SavedPage = () => (<BetaPage><Typography variant="h4">Saved</Typography></BetaPage>); );
const JobsPage = () => (<BetaPage><Typography variant="h4">Jobs</Typography></BetaPage>); const ResumesPage = () => (
const CompanyPage = () => (<BetaPage><Typography variant="h4">Company</Typography></BetaPage>); <BetaPage>
const LogoutPage = () => (<BetaPage><Typography variant="h4">Logout page...</Typography></BetaPage>); <Typography variant="h4">Resumes</Typography>
const AnalyticsPage = () => (<BetaPage><Typography variant="h4">Analytics</Typography></BetaPage>); </BetaPage>
const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</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 = () => {
const { logout } = useAuth();
const navigate = useNavigate();
logout().then(() => {
navigate("/");
});
return (
<Typography variant="h4">Logging out...</Typography>
);
}
const AnalyticsPage = () => (
<BetaPage>
<Typography variant="h4">Analytics</Typography>
</BetaPage>
);
const SettingsPage = () => (
<BetaPage>
<Typography variant="h4">Settings</Typography>
</BetaPage>
);
export const navigationConfig: NavigationConfig = { export const navigationConfig: NavigationConfig = {
items: [ items: [
{ id: 'home', label: <BackstoryLogo />, path: '/', component: <HowItWorks />, userTypes: ['guest', 'candidate', 'employer'], exact: true, }, {
// { id: 'how-it-works', label: 'How It Works', path: '/how-it-works', icon: <SchoolIcon />, component: <HowItWorks />, userTypes: ['guest', 'candidate', 'employer',], }, id: "home",
{ id: 'job-analysis', label: 'Job Analysis', path: '/job-analysis', icon: <WorkIcon />, component: <JobAnalysisPage />, userTypes: ['guest', 'candidate', 'employer',], }, label: <BackstoryLogo />,
{ id: 'chat', label: 'Candidate Chat', path: '/chat', icon: <ChatIcon />, component: <CandidateChatPage />, userTypes: ['guest', 'candidate', 'employer',], }, { path: "/",
id: 'candidate-menu', label: 'Tools', icon: <PersonIcon />, userTypes: ['candidate'], children: [ component: <HowItWorks />,
{ id: 'candidate-dashboard', label: 'Dashboard', path: '/candidate/dashboard', icon: <DashboardIcon />, component: <CandidateDashboard />, userTypes: ['candidate'] }, userTypes: ["guest", "candidate", "employer"],
{ id: 'candidate-profile', label: 'Profile', icon: <PersonIcon />, path: '/candidate/profile', component: <CandidateProfile />, userTypes: ['candidate'] }, exact: true,
{ id: 'candidate-docs', label: 'Documents', icon: <BubbleChart />, path: '/candidate/documents', component: <Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}><VectorVisualizer /><DocumentManager /></Box>, userTypes: ['candidate'] },
{ id: 'candidate-qa-setup', label: 'Q&A Setup', icon: <QuizIcon />, path: '/candidate/qa-setup', component: <BetaPage><Box>Candidate q&a setup page</Box></BetaPage>, userTypes: ['candidate'] },
{ id: 'candidate-analytics', label: 'Analytics', icon: <AnalyticsIcon />, path: '/candidate/analytics', component: <BetaPage><Box>Candidate analytics page</Box></BetaPage>, userTypes: ['candidate'] },
{ id: 'candidate-job-analysis', label: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon />, component: <JobAnalysisPage />, userTypes: ['candidate'], showInNavigation: false,
showInUserMenu: true,
userMenuGroup: 'profile',},
{ id: 'candidate-resumes', label: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes', component: <BetaPage><Box>Candidate resumes page</Box></BetaPage>, userTypes: ['candidate'] },
{ id: 'candidate-settings', label: 'Settings', path: '/candidate/settings', icon: <SettingsIcon />, component: <Settings />, userTypes: ['candidate'], },
],
}, },
{ {
id: 'employer-menu', label: 'Employer Tools', icon: <BusinessIcon />, userTypes: ['employer'], children: [ id: "job-analysis",
{ id: 'employer-job-analysis', label: 'Job Analysis', path: '/employer/job-analysis', icon: <WorkIcon />, component: <JobAnalysisPage />, userTypes: ['employer'], }, label: "Job Analysis",
{ id: 'employer-knowledge-explorer', label: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: <WorkIcon />, component: <VectorVisualizerPage />, userTypes: ['employer'], }, path: "/job-analysis",
{ 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'],
showInNavigation: true,
children: [
{
id: 'knowledge-explorer',
label: 'Knowledge Explorer',
path: '/knowledge-explorer',
icon: <WorkIcon />,
component: <VectorVisualizerPage />,
userTypes: ['candidate', 'employer'],
showInNavigation: true,
},
{
id: 'job-analysis',
label: 'Job Analysis',
path: '/job-analysis',
icon: <WorkIcon />, icon: <WorkIcon />,
component: <JobAnalysisPage />, component: <JobAnalysisPage />,
userTypes: ['candidate', 'employer'], userTypes: ["guest", "candidate", "employer"],
showInNavigation: true,
}, },
{ {
id: 'generate-candidate', id: "chat",
label: 'Generate Candidate', label: "Candidate Chat",
path: '/generate-candidate', path: "/chat",
icon: <ChatIcon />,
component: <CandidateChatPage />,
userTypes: ["guest", "candidate", "employer"],
},
{
id: "generate-candidate",
label: "Generate Candidate",
path: "/admin/generate-candidate",
icon: <PersonIcon />, icon: <PersonIcon />,
component: <GenerateCandidate />, component: <GenerateCandidate />,
userTypes: ['candidate', 'employer'], userTypes: ["admin"],
showInNavigation: true, showInNavigation: true,
}, userMenuGroup: "system",
],
}, },
// User menu only items (not shown in main navigation) // User menu only items (not shown in main navigation)
{ {
id: 'user-profile', id: "candidate-profile",
label: 'Profile', label: "Profile",
path: '/profile',
icon: <PersonIcon />, icon: <PersonIcon />,
component: <AnalyticsPage />, // Replace with actual profile page path: "/candidate/profile",
userTypes: ['candidate', 'employer'], component: <CandidateProfile />,
userTypes: ["candidate"],
userMenuGroup: "profile",
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
userMenuGroup: 'profile',
}, },
{ {
id: 'account-settings', id: "candidate-dashboard",
label: 'Account Settings', label: "Dashboard",
path: '/account/settings', path: "/candidate/dashboard",
icon: <DashboardIcon />,
component: <CandidateDashboard />,
userTypes: ["candidate"],
userMenuGroup: "profile",
showInNavigation: false,
showInUserMenu: true,
},
{
id: "candidate-docs",
label: "Content",
icon: <BubbleChart />,
path: "/candidate/documents",
component: (
<Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}>
<VectorVisualizer />
<DocumentManager />
</Box>
),
userTypes: ["candidate"],
userMenuGroup: "profile",
showInNavigation: false,
showInUserMenu: true,
},
// {
// id: "candidate-qa-setup",
// label: "Q&A Setup",
// icon: <QuizIcon />,
// path: "/candidate/qa-setup",
// component: (
// <BetaPage>
// <Box>Candidate q&a setup page</Box>
// </BetaPage>
// ),
// userTypes: ["candidate"],
// showInNavigation: false,
// showInUserMenu: true,
// },
// {
// id: "candidate-analytics",
// label: "Analytics",
// icon: <AnalyticsIcon />,
// path: "/candidate/analytics",
// component: (
// <BetaPage>
// <Box>Candidate analytics page</Box>
// </BetaPage>
// ),
// userTypes: ["candidate"],
// showInNavigation: false,
// showInUserMenu: true,
// },
// {
// id: "candidate-resumes",
// label: "Resumes",
// icon: <DescriptionIcon />,
// path: "/candidate/resumes",
// component: (
// <BetaPage>
// <Box>Candidate resumes page</Box>
// </BetaPage>
// ),
// userTypes: ["candidate"],
// showInNavigation: false,
// showInUserMenu: true,
// },
{
id: "candidate-settings",
label: "Settings",
path: "/candidate/settings",
icon: <SettingsIcon />, icon: <SettingsIcon />,
component: <SettingsPage />, component: <Settings />,
userTypes: ['candidate', 'employer'], userTypes: ["candidate"],
userMenuGroup: "account",
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
userMenuGroup: 'account',
}, },
{ {
id: 'billing', id: "logout",
label: 'Billing', label: "Logout",
path: '/billing',
icon: <AttachMoneyIcon />,
component: <AnalyticsPage />, // Replace with actual billing page
userTypes: ['candidate', 'employer'],
showInNavigation: false,
showInUserMenu: true,
userMenuGroup: 'account',
},
{
id: 'user-menu-divider',
label: '',
userTypes: ['candidate', 'employer'],
showInNavigation: false,
showInUserMenu: true,
divider: true,
},
{
id: 'logout',
label: 'Logout',
icon: <PersonIcon />, // This will be handled specially in Header icon: <PersonIcon />, // This will be handled specially in Header
userTypes: ['candidate', 'employer'], userTypes: ["candidate", "employer"],
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
userMenuGroup: 'system', userMenuGroup: "system",
}, },
// Auth routes (special handling) // Auth routes (special handling)
{ {
id: 'auth', id: "auth",
label: 'Auth', label: "Auth",
userTypes: ['guest', 'candidate', 'employer'], userTypes: ["guest", "candidate", "employer"],
showInNavigation: false, showInNavigation: false,
children: [ children: [
{ {
id: 'register', id: "register",
label: 'Register', label: "Register",
path: '/register', path: "/login/register",
component: <BetaPage><CreateProfilePage /></BetaPage>, component: (
userTypes: ['guest'], <BetaPage>
<CreateProfilePage />
</BetaPage>
),
userTypes: ["guest"],
showInNavigation: false, showInNavigation: false,
}, },
{ {
id: 'login', id: "login",
label: 'Login', label: "Login",
path: '/login/*', path: "/login/*",
component: <LoginPage />, component: <LoginPage />,
userTypes: ['guest', 'candidate', 'employer'], userTypes: ["guest", "candidate", "employer"],
showInNavigation: false, showInNavigation: false,
}, },
{ {
id: 'verify-email', id: "verify-email",
label: 'Verify Email', label: "Verify Email",
path: '/login/verify-email', path: "/login/verify-email",
component: <EmailVerificationPage />, component: <EmailVerificationPage />,
userTypes: ['guest', 'candidate', 'employer'], userTypes: ["guest", "candidate", "employer"],
showInNavigation: false, showInNavigation: false,
}, },
{ {
id: 'logout-page', id: "logout-page",
label: 'Logout', label: "Logout",
path: '/logout', path: "/logout",
component: <LogoutPage />, component: <LogoutPage />,
userTypes: ['candidate', 'employer'], userTypes: ["candidate", "employer"],
showInNavigation: false, showInNavigation: false,
}, },
], ],
}, },
// Catch-all route // Catch-all route
{ {
id: 'catch-all', id: "catch-all",
label: 'Not Found', label: "Not Found",
path: '*', path: "*",
component: <BetaPage />, component: <BetaPage />,
userTypes: ['guest', 'candidate', 'employer'], userTypes: ["guest", "candidate", "employer"],
showInNavigation: false, showInNavigation: false,
}, },
], ],
}; };
// Utility functions for working with navigation config // Utility functions for working with navigation config
export const getNavigationItemsForUser = (userType: 'guest' | 'candidate' | 'employer' | null, isAdmin: boolean = false): NavigationItem[] => { export const getNavigationItemsForUser = (
const currentUserType = userType || 'guest'; userType: "guest" | "candidate" | "employer" | null,
isAdmin: boolean = false
): NavigationItem[] => {
const currentUserType = userType || "guest";
const filterItems = (items: NavigationItem[]): NavigationItem[] => { const filterItems = (items: NavigationItem[]): NavigationItem[] => {
return items return items
.filter(item => !item.userTypes || item.userTypes.includes(currentUserType) || (item.userTypes.includes('admin') && isAdmin)) .filter(
.filter(item => item.showInNavigation !== false) // Default to true if not specified (item) =>
.map(item => ({ !item.userTypes || item.userTypes.includes(currentUserType) || (item.userTypes.includes("admin") && isAdmin)
)
.filter((item) => item.showInNavigation !== false) // Default to true if not specified
.map((item) => ({
...item, ...item,
children: item.children ? filterItems(item.children) : undefined, children: item.children ? filterItems(item.children) : undefined,
})) }))
.filter(item => item.path || (item.children && item.children.length > 0)); .filter((item) => item.path || (item.children && item.children.length > 0));
}; };
return filterItems(navigationConfig.items); return filterItems(navigationConfig.items);
}; };
export const getAllRoutes = (userType: 'guest' | 'candidate' | 'employer' | null, isAdmin: boolean = false): NavigationItem[] => { export const getAllRoutes = (
const currentUserType = userType || 'guest'; userType: "guest" | "candidate" | "employer" | null,
isAdmin: boolean = false
): NavigationItem[] => {
const currentUserType = userType || "guest";
const extractRoutes = (items: NavigationItem[]): NavigationItem[] => { const extractRoutes = (items: NavigationItem[]): NavigationItem[] => {
const routes: NavigationItem[] = []; const routes: NavigationItem[] = [];
items.forEach(item => { items.forEach((item) => {
if (!item.userTypes || item.userTypes.includes(currentUserType) || (item.userTypes.includes('admin') && isAdmin)) { if (!item.userTypes || item.userTypes.includes(currentUserType) || (item.userTypes.includes("admin") && isAdmin)) {
if (item.path && item.component) { if (item.path && item.component) {
routes.push(item); routes.push(item);
} }
@ -265,23 +345,26 @@ export const getAllRoutes = (userType: 'guest' | 'candidate' | 'employer' | null
return extractRoutes(navigationConfig.items); return extractRoutes(navigationConfig.items);
}; };
export const getMainNavigationItems = (userType: 'guest' | 'candidate' | 'employer' | null, isAdmin: boolean = false): NavigationItem[] => { export const getMainNavigationItems = (
return getNavigationItemsForUser(userType, isAdmin) userType: "guest" | "candidate" | "employer" | null,
.filter(item => isAdmin: boolean = false
item.id !== 'auth' && ): NavigationItem[] => {
item.id !== 'catch-all' && return getNavigationItemsForUser(userType, isAdmin).filter(
(item) =>
item.id !== "auth" &&
item.id !== "catch-all" &&
item.showInNavigation !== false && item.showInNavigation !== false &&
(item.path || (item.children && item.children.length > 0)) (item.path || (item.children && item.children.length > 0))
); );
}; };
export const getUserMenuItems = (userType: 'candidate' | 'employer' | 'guest' | null): NavigationItem[] => { export const getUserMenuItems = (userType: "candidate" | "employer" | "guest" | null): NavigationItem[] => {
if (!userType) return []; if (!userType) return [];
const extractUserMenuItems = (items: NavigationItem[]): NavigationItem[] => { const extractUserMenuItems = (items: NavigationItem[]): NavigationItem[] => {
const menuItems: NavigationItem[] = []; const menuItems: NavigationItem[] = [];
items.forEach(item => { items.forEach((item) => {
if (!item.userTypes || item.userTypes.includes(userType)) { if (!item.userTypes || item.userTypes.includes(userType)) {
if (item.showInUserMenu) { if (item.showInUserMenu) {
menuItems.push(item); menuItems.push(item);
@ -298,17 +381,19 @@ export const getUserMenuItems = (userType: 'candidate' | 'employer' | 'guest' |
return extractUserMenuItems(navigationConfig.items); return extractUserMenuItems(navigationConfig.items);
}; };
export const getUserMenuItemsByGroup = (userType: 'candidate' | 'employer' | 'guest' | null): { [key: string]: NavigationItem[] } => { export const getUserMenuItemsByGroup = (
userType: "candidate" | "employer" | "guest" | null
): { [key: string]: NavigationItem[] } => {
const menuItems = getUserMenuItems(userType); const menuItems = getUserMenuItems(userType);
const grouped: { [key: string]: NavigationItem[] } = { const grouped: { [key: string]: NavigationItem[] } = {
profile: [], profile: [],
account: [], account: [],
system: [], system: [],
other: [] other: [],
}; };
menuItems.forEach(item => { menuItems.forEach((item) => {
const group = item.userMenuGroup || 'other'; const group = item.userMenuGroup || "other";
if (!grouped[group]) { if (!grouped[group]) {
grouped[group] = []; grouped[group] = [];
} }

View File

@ -336,14 +336,14 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<WorkIcon /> <WorkIcon />
</Avatar> </Avatar>
} }
<JobInfo variant="small" job={analysisState.job} /> <JobInfo variant="minimal" job={analysisState.job} />
</Box> </Box>
} }
{isMobile && <Box sx={{ display: "flex", borderBottom: "1px solid grey" }} />} {isMobile && <Box sx={{ display: "flex", borderBottom: "1px solid lightgrey" }} />}
{!isMobile && <Box sx={{ display: "flex", borderLeft: "1px solid lightgrey" }} />} {!isMobile && <Box sx={{ display: "flex", borderLeft: "1px solid lightgrey" }} />}
{analysisState && analysisState.candidate && {analysisState && analysisState.candidate &&
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}> <Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}>
<CandidateInfo variant="small" candidate={analysisState.candidate} sx={{}} /> <CandidateInfo variant="minimal" candidate={analysisState.candidate} sx={{}} />
</Box> </Box>
} }
</Box> </Box>

View File

@ -4936,7 +4936,6 @@ async def generate_resume(
yield error_message yield error_message
return return
# Generate new skill match
final_message = None final_message = None
async for generated_message in agent.generate_resume( async for generated_message in agent.generate_resume(
llm=llm_manager.get_llm(), llm=llm_manager.get_llm(),