Compare commits

..

No commits in common. "74201d0a71c334d56f9f1615e790c00335728a16" and "7c78f39b02afecf47299995a516ca508ac1500c4" have entirely different histories.

14 changed files with 236 additions and 570 deletions

View File

@ -89,7 +89,7 @@ const JobCreator = (props: JobCreatorProps) => {
const [company, setCompany] = useState<string>('');
const [summary, setSummary] = useState<string>('');
const [job, setJob] = useState<Types.Job | null>(null);
const [jobStatus, setJobStatus] = useState<string>('');
const [jobStatus, setJobStatus] = useState<string>('');
const [jobStatusType, setJobStatusType] = useState<Types.ApiActivityType | null>(null);
const [isProcessing, setIsProcessing] = useState<boolean>(false);

View File

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

View File

@ -50,7 +50,7 @@ const BackstoryPageContainer = (props: BackstoryPageContainerProps) => {
<Box sx={{
display: "flex",
p: { xs: 0, sm: 0.5 },
flexGrow: 1,
flexGrow: 1,
minHeight: "min-content",
}}>
<Paper
@ -79,7 +79,7 @@ interface BackstoryLayoutProps {
}
const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => {
const { page, chatRef, } = props;
const { page, chatRef } = props;
const { setSnack } = useAppState();
const navigate = useNavigate();
const location = useLocation();
@ -91,12 +91,18 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
setNavigationItems(getMainNavigationItems(userType, user?.isAdmin ? true : false));
}, [user]);
useEffect(() => {
console.log({ guest, user });
}, [guest, user]);
// Generate dynamic routes from navigation config
const generateRoutes = () => {
if (!guest && !user) return null;
const userType = user?.userType || null;
const isAdmin = user?.isAdmin ? true : false;
// Get all routes from navigation config
const routes = getAllRoutes(userType, isAdmin);
return routes.map((route, index) => {
@ -123,9 +129,10 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
height: "100%",
maxHeight: "100%",
minHeight: "100%",
flexDirection: "column"
flexDirection: "column"
}}>
<Header
<Header
setSnack={setSnack}
currentPath={page}
navigate={navigate}
navigationItems={navigationItems}

View File

@ -1,5 +1,5 @@
// components/layout/Header.tsx
import React, { useEffect, useState } from 'react';
import React, { JSX, useEffect, useState } from 'react';
import { NavigateFunction, useLocation } from 'react-router-dom';
import {
AppBar,
@ -40,7 +40,7 @@ import {
KeyboardArrowDown,
} from '@mui/icons-material';
import FaceRetouchingNaturalIcon from '@mui/icons-material/FaceRetouchingNatural';
import { getUserMenuItemsByGroup } from 'config/navigationConfig';
import { NavigationItem } from 'types/navigation';
import { Beta } from 'components/ui/Beta';
import { Candidate, Employer } from 'types/types';
@ -121,11 +121,11 @@ interface HeaderProps {
navigationItems: NavigationItem[];
currentPath: string;
sessionId?: string | null;
setSnack: SetSnackType;
}
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const { user, logout } = useAuth();
const { setSnack } = useAppState();
const {
transparent = false,
className,
@ -133,6 +133,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
navigationItems,
sessionId,
} = props;
const { setSnack } = useAppState();
const theme = useTheme();
const location = useLocation();
@ -149,108 +151,52 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
const userMenuOpen = Boolean(userMenuAnchor);
// Get user menu items from navigation config
const userMenuGroups = getUserMenuItemsByGroup(user?.userType || null);
// Create user menu items array with proper actions
const createUserMenuItems = () => {
const items: Array<{
id: string;
label: string;
icon: React.ReactElement | null;
action: () => void;
group?: string;
}> = [];
// Add profile group items
userMenuGroups.profile.forEach(item => {
if (!item.divider) {
items.push({
id: item.id,
label: item.label as string,
icon: item.icon || null,
action: () => item.path && navigate(item.path),
group: 'profile'
});
// User menu items
const userMenuItems = [
{
id: 'profile',
label: 'Profile',
icon: <Person fontSize="small" />,
action: () => navigate(`/${user?.userType}/profile`)
},
{
id: 'dashboard',
label: 'Dashboard',
icon: <Dashboard fontSize="small" />,
action: () => navigate(`/${user?.userType}/dashboard`)
},
{
id: 'settings',
label: 'Settings',
icon: <Settings fontSize="small" />,
action: () => navigate(`/${user?.userType}/settings`)
},
{
id: 'divider',
label: '',
icon: null,
action: () => { }
},
{
id: 'logout',
label: 'Logout',
icon: <Logout fontSize="small" />,
action: () => {
logout();
navigate('/');
}
});
// 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
userMenuGroups.account.forEach(item => {
if (!item.divider) {
items.push({
id: item.id,
label: item.label as string,
icon: item.icon || null,
action: () => item.path && navigate(item.path),
group: 'account'
});
}
});
// 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'
});
if (user?.isAdmin) {
const divider = userMenuItems.findIndex(p => p.id === 'divider');
if (divider !== -1) {
userMenuItems.splice(divider, 0, ...[
{ id: 'divider', label: '', icon: null, action: () => { } },
{ id: 'generate', label: 'Generate Candidate', icon: <FaceRetouchingNaturalIcon fontSize="small" />, action: () => { navigate('/admin/generate-candidate'); } }
]);
}
// Add system group items with special handling for logout
userMenuGroups.system.forEach(item => {
if (item.id === 'logout') {
items.push({
id: 'logout',
label: 'Logout',
icon: <Logout fontSize="small" />,
action: () => {
logout();
navigate('/');
},
group: 'system'
});
} else if (!item.divider) {
items.push({
id: item.id,
label: item.label as string,
icon: item.icon || null,
action: () => item.path && navigate(item.path),
group: 'system'
});
}
});
// Add other group items
userMenuGroups.other.forEach(item => {
if (!item.divider) {
items.push({
id: item.id,
label: item.label as string,
icon: item.icon || null,
action: () => item.path && navigate(item.path),
group: 'other'
});
}
});
return items;
};
const userMenuItems = createUserMenuItems();
}
// Helper function to check if current path matches navigation item
const isCurrentPath = (item: NavigationItem): boolean => {
@ -293,8 +239,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
setUserMenuAnchor(null);
};
const handleUserMenuAction = (item: { id: string; label: string; icon: React.ReactElement | null; action: () => void; group?: string }) => {
if (item.group !== 'divider') {
const handleUserMenuAction = (item: typeof userMenuItems[0]) => {
if (item.id !== 'divider') {
item.action();
handleUserMenuClose();
}
@ -311,14 +257,23 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
// Render desktop navigation with dropdowns
const renderDesktopNavigation = () => {
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{navigationItems.map((item) => {
<Box sx={{ display: 'flex', width: "100%", alignItems: 'center', justifyContent: "space-between", "& > :last-of-type": { marginRight: "auto"} }}>
{navigationItems.map((item, index) => {
const hasChildren = item.children && item.children.length > 0;
const isActive = isCurrentPath(item) || hasActiveChild(item);
// Default is centered for all menu items
let sx : SxProps = { justifycontent: "center"};
// First item ("Backstory") is left aligned
if (index === 0) {
sx = { marginRight: "auto" };
}
// If there is an Admin menu, it is on the far right
if (item.userTypes?.includes('admin')) {
sx = { marginLeft: "auto"};
}
if (hasChildren) {
return (
<Box key={item.id}>
<Box key={item.id} sx={sx}>
<DropdownButton
onClick={(e) => handleDropdownOpen(e, item.id)}
endIcon={<KeyboardArrowDown />}
@ -338,15 +293,16 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
TransitionComponent={Fade}
>
{item.children?.map(child => (
{item.children?.map((child) => (
<MenuItem
key={child.id}
onClick={() => child.path && handleNavigate(child.path)}
selected={isCurrentPath(child)}
disabled={!child.path}
sx={{ alignContent: 'center', margin: "0 !important" }}
>
{child.icon && <ListItemIcon>{child.icon}</ListItemIcon>}
<ListItemText>{child.label}</ListItemText>
<ListItemText sx={{ p: 0, margin: "0 !important", "& > *": { m: 0 } }}>{child.label}</ListItemText>
</MenuItem>
))}
</Menu>
@ -360,6 +316,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
sx={{
backgroundColor: isActive ? 'action.selected' : 'transparent',
color: isActive ? 'secondary.main' : 'primary.contrastText',
...sx
}}
>
{item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
@ -410,11 +367,11 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<ListItemText
primary={item.label}
sx={{
'& .MuiTypography-root': {
'& .MuiTypography-root': {
fontSize: depth > 0 ? '0.875rem' : '1rem',
fontWeight: depth === 0 ? 500 : 400,
}
}}
}}
/>
{hasChildren && (
<IconButton size="small">
@ -426,7 +383,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
{hasChildren && (
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<List disablePadding>
{item.children?.map(child => renderNavigationItem(child, depth + 1))}
{item.children?.map((child) => renderNavigationItem(child, depth + 1))}
</List>
</Collapse>
)}
@ -455,13 +412,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<UserMenuContainer>
<List dense>
{userMenuItems.map((item, index) => (
item.group === 'divider' ? (
item.id === 'divider' ? (
<Divider key={`divider-${index}`} />
) : (
<ListItem key={item.id} disablePadding>
<ListItemButton onClick={() => handleUserMenuAction(item)}>
<ListItemButton onClick={() => handleUserMenuAction(item)} sx={{ alignContent: "center" }}>
{item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
<ListItemText primary={item.label} />
<ListItemText primary={item.label} sx={{ padding: 0 }} />
</ListItemButton>
</ListItem>
)
@ -578,7 +535,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
onClick={() => {
navigate(`${window.location.pathname}?id=${sessionId}`);
setSnack("Link copied!");
setSnack("Link copied!");
}}
size="large"
/>
@ -601,7 +558,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
</Container>
<Beta
sx={{ left: "-90px", "& .mobile": { right: "-72px" } }}
onClick={() => { navigate('/docs/beta'); }}
onClick={() => { navigate('/docs/beta'); }}
/>
</StyledAppBar>
);

View File

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

View File

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

View File

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

View File

@ -61,20 +61,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
<Paper key={`${u.username}`}
onClick={() => { onSelect ? onSelect(u) : setSelectedCandidate(u); }}
sx={{ cursor: "pointer" }}>
<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}
/>
<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} />
</Paper>
)}
</Box>

View File

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

View File

@ -52,20 +52,7 @@ const JobPicker = (props: JobPickerProps) => {
<Paper key={`${j.id}`}
onClick={() => { onSelect ? onSelect(j) : setSelectedJob(j); }}
sx={{ cursor: "pointer" }}>
<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}
/>
<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} />
</Paper>
)}
</Box>

View File

@ -1,4 +1,6 @@
import React from "react";
// config/navigationConfig.tsx
import React from 'react';
import {
Chat as ChatIcon,
Dashboard as DashboardIcon,
@ -6,330 +8,158 @@ import {
BarChart as BarChartIcon,
Settings as SettingsIcon,
Work as WorkIcon,
Info as InfoIcon,
Person as PersonIcon,
Business as BusinessIcon,
Quiz as QuizIcon,
Analytics as AnalyticsIcon,
Search as SearchIcon,
Bookmark as BookmarkIcon,
History as HistoryIcon,
QuestionAnswer as QuestionAnswerIcon,
AttachMoney as AttachMoneyIcon,
Quiz as QuizIcon,
Analytics as AnalyticsIcon,
BubbleChart,
} from "@mui/icons-material";
} from '@mui/icons-material';
import SchoolIcon from '@mui/icons-material/School';
import { BackstoryLogo } from "components/ui/BackstoryLogo";
import { HomePage } from "pages/HomePage";
import { CandidateChatPage } from "pages/CandidateChatPage";
import { DocsPage } from "pages/DocsPage";
import { CreateProfilePage } from "pages/candidate/ProfileWizard";
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 { LoginPage } from "pages/LoginPage";
import { EmailVerificationPage } from "components/EmailVerificationComponents";
import { Box, Typography } from "@mui/material";
import { CandidateDashboard } from "pages/candidate/Dashboard";
import { NavigationConfig, NavigationItem } from "types/navigation";
import { HowItWorks } from "pages/HowItWorks";
import SchoolIcon from "@mui/icons-material/School";
import { CandidateProfile } from "pages/candidate/Profile";
import { Settings } from "pages/candidate/Settings";
import { VectorVisualizer } from "components/VectorVisualizer";
import { DocumentManager } from "components/DocumentManager";
import { useAuth } from "hooks/AuthContext";
import { useNavigate } from "react-router-dom";
import FaceRetouchingNaturalIcon from '@mui/icons-material/FaceRetouchingNatural';
import LibraryBooksIcon from '@mui/icons-material/LibraryBooks';
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import { HomePage } from 'pages/HomePage';
import { CandidateChatPage } from 'pages/CandidateChatPage';
import { DocsPage } from 'pages/DocsPage';
import { CreateProfilePage } from 'pages/candidate/ProfileWizard';
import { VectorVisualizerPage } from 'pages/VectorVisualizerPage';
import { BetaPage } from 'pages/BetaPage';
import { JobAnalysisPage } from 'pages/JobAnalysisPage';
import { GenerateCandidate } from 'pages/GenerateCandidate';
import { Settings } from 'pages/candidate/Settings';
import { LoginPage } from 'pages/LoginPage';
import { CandidateDashboard } from 'pages/candidate/Dashboard';
import { EmailVerificationPage } from 'components/EmailVerificationComponents';
import { Box, Typography } from '@mui/material';
import { NavigationConfig, NavigationItem } from 'types/navigation';
import { CandidateProfile } from 'pages/candidate/Profile';
import { DocumentManager } from 'components/DocumentManager';
import { VectorVisualizer } from 'components/VectorVisualizer';
import { HowItWorks } from 'pages/HowItWorks';
// 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 = () => {
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>
);
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: <HowItWorks />,
userTypes: ["guest", "candidate", "employer"],
exact: true,
{ 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: 'job-analysis', label: 'Job Analysis', path: '/job-analysis', icon: <WorkIcon />, component: <JobAnalysisPage />, userTypes: ['guest', 'candidate', 'employer',], },
{ id: 'chat', label: 'Candidate Chat', path: '/chat', icon: <ChatIcon />, component: <CandidateChatPage />, userTypes: ['guest', 'candidate', 'employer',], }, {
id: 'candidate-menu', label: 'Tools', icon: <PersonIcon />, userTypes: ['candidate'], children: [
{ id: 'candidate-dashboard', label: 'Dashboard', path: '/candidate/dashboard', icon: <DashboardIcon />, component: <CandidateDashboard />, userTypes: ['candidate'] },
{ id: 'candidate-profile', label: 'Profile', icon: <PersonIcon />, path: '/candidate/profile', component: <CandidateProfile />, userTypes: ['candidate'] },
{ 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'], },
{ 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: 'employer-job-analysis', label: 'Job Analysis', path: '/employer/job-analysis', icon: <WorkIcon />, component: <JobAnalysisPage />, 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: 'find-candidate', label: 'Find a Candidate', path: '/find-a-candidate', icon: <PersonSearchIcon />, component: <CandidateListingPage />, userTypes: ['guest', 'candidate', 'employer'], },
{
id: "job-analysis",
label: "Job Analysis",
path: "/job-analysis",
icon: <WorkIcon />,
component: <JobAnalysisPage />,
userTypes: ["guest", "candidate", "employer"],
},
{
id: "chat",
label: "Candidate Chat",
path: "/chat",
icon: <ChatIcon />,
component: <CandidateChatPage />,
userTypes: ["guest", "candidate", "employer"],
},
{
id: "generate-candidate",
label: "Generate Candidate",
path: "/admin/generate-candidate",
icon: <PersonIcon />,
component: <GenerateCandidate />,
userTypes: ["admin"],
showInNavigation: true,
userMenuGroup: "system",
},
// User menu only items (not shown in main navigation)
{
id: "candidate-profile",
label: "Profile",
icon: <PersonIcon />,
path: "/candidate/profile",
component: <CandidateProfile />,
userTypes: ["candidate"],
userMenuGroup: "profile",
showInNavigation: false,
showInUserMenu: true,
},
{
id: "candidate-dashboard",
label: "Dashboard",
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 />,
component: <Settings />,
userTypes: ["candidate"],
userMenuGroup: "account",
showInNavigation: false,
showInUserMenu: true,
},
{
id: "logout",
label: "Logout",
icon: <PersonIcon />, // This will be handled specially in Header
userTypes: ["candidate", "employer"],
showInNavigation: false,
showInUserMenu: true,
userMenuGroup: "system",
id: 'admin-menu',
label: 'Admin',
icon: <PersonIcon />,
userTypes: ['admin'],
children: [
{ id: 'generate-candidate', label: 'Generate Candidate', path: '/admin/generate-candidate', icon: <FaceRetouchingNaturalIcon />, component: <GenerateCandidate />, userTypes: ['admin'] },
{ id: 'docs', label: 'Docs', path: '/docs/*', icon: <LibraryBooksIcon />, component: <DocsPage />, userTypes: ['admin'], },
],
},
// Auth routes (special handling)
{
id: "auth",
label: "Auth",
userTypes: ["guest", "candidate", "employer"],
showInNavigation: false,
id: 'auth',
label: 'Auth',
userTypes: ['guest', 'candidate', 'employer'],
children: [
{
id: "register",
label: "Register",
path: "/login/register",
component: (
<BetaPage>
<CreateProfilePage />
</BetaPage>
),
userTypes: ["guest"],
showInNavigation: false,
id: 'register',
label: 'Register',
path: '/register',
component: <BetaPage><CreateProfilePage /></BetaPage>,
userTypes: ['guest'],
},
{
id: "login",
label: "Login",
path: "/login/*",
id: 'login',
label: 'Login',
path: '/login/*',
component: <LoginPage />,
userTypes: ["guest", "candidate", "employer"],
showInNavigation: false,
userTypes: ['guest', 'candidate', 'employer'],
},
{
id: "verify-email",
label: "Verify Email",
path: "/login/verify-email",
id: 'verify-email',
label: 'Verify Email',
path: '/login/verify-email',
component: <EmailVerificationPage />,
userTypes: ["guest", "candidate", "employer"],
showInNavigation: false,
userTypes: ['guest', 'candidate', 'employer'],
},
{
id: "logout-page",
label: "Logout",
path: "/logout",
id: 'logout',
label: 'Logout',
path: '/logout',
component: <LogoutPage />,
userTypes: ["candidate", "employer"],
showInNavigation: false,
userTypes: ['candidate', 'employer'],
},
],
},
// Catch-all route
{
id: "catch-all",
label: "Not Found",
path: "*",
id: 'catch-all',
label: 'Not Found',
path: '*',
component: <BetaPage />,
userTypes: ["guest", "candidate", "employer"],
showInNavigation: false,
userTypes: ['guest', 'candidate', 'employer'],
},
],
};
// Utility functions for working with navigation config
export const getNavigationItemsForUser = (
userType: "guest" | "candidate" | "employer" | null,
isAdmin: boolean = false
): NavigationItem[] => {
const currentUserType = userType || "guest";
export const getNavigationItemsForUser = (userType: 'guest' | 'candidate' | 'employer' | null, isAdmin: boolean = false): NavigationItem[] => {
const currentUserType = userType || 'guest';
const filterItems = (items: NavigationItem[]): NavigationItem[] => {
return items
.filter(
(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) => ({
.filter(item => !item.userTypes || item.userTypes.includes(currentUserType) || (item.userTypes.includes('admin') && isAdmin))
.map(item => ({
...item,
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);
};
export const getAllRoutes = (
userType: "guest" | "candidate" | "employer" | null,
isAdmin: boolean = false
): NavigationItem[] => {
const currentUserType = userType || "guest";
export const getAllRoutes = (userType: 'guest' | 'candidate' | 'employer' | null, isAdmin: boolean = false): NavigationItem[] => {
const currentUserType = userType || 'guest';
const extractRoutes = (items: NavigationItem[]): NavigationItem[] => {
const routes: NavigationItem[] = [];
items.forEach((item) => {
if (!item.userTypes || item.userTypes.includes(currentUserType) || (item.userTypes.includes("admin") && isAdmin)) {
items.forEach(item => {
if (!item.userTypes || item.userTypes.includes(currentUserType) || (item.userTypes.includes('admin') && isAdmin)) {
if (item.path && item.component) {
routes.push(item);
}
@ -338,67 +168,18 @@ export const getAllRoutes = (
}
}
});
return routes;
};
return extractRoutes(navigationConfig.items);
};
export const getMainNavigationItems = (
userType: "guest" | "candidate" | "employer" | null,
isAdmin: boolean = false
): NavigationItem[] => {
return getNavigationItemsForUser(userType, isAdmin).filter(
(item) =>
item.id !== "auth" &&
item.id !== "catch-all" &&
item.showInNavigation !== false &&
export const getMainNavigationItems = (userType: 'guest' | 'candidate' | 'employer' | null, isAdmin: boolean = false): NavigationItem[] => {
return getNavigationItemsForUser(userType, isAdmin)
.filter(item =>
item.id !== 'auth' &&
item.id !== 'catch-all' &&
(item.path || (item.children && item.children.length > 0))
);
};
export const getUserMenuItems = (userType: "candidate" | "employer" | "guest" | null): NavigationItem[] => {
if (!userType) return [];
const extractUserMenuItems = (items: NavigationItem[]): NavigationItem[] => {
const menuItems: NavigationItem[] = [];
items.forEach((item) => {
if (!item.userTypes || item.userTypes.includes(userType)) {
if (item.showInUserMenu) {
menuItems.push(item);
}
if (item.children) {
menuItems.push(...extractUserMenuItems(item.children));
}
}
});
return menuItems;
};
return extractUserMenuItems(navigationConfig.items);
};
export const getUserMenuItemsByGroup = (
userType: "candidate" | "employer" | "guest" | null
): { [key: string]: NavigationItem[] } => {
const menuItems = getUserMenuItems(userType);
const grouped: { [key: string]: NavigationItem[] } = {
profile: [],
account: [],
system: [],
other: [],
};
menuItems.forEach((item) => {
const group = item.userMenuGroup || "other";
if (!grouped[group]) {
grouped[group] = [];
}
grouped[group].push(item);
});
return grouped;
};
);
};

View File

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

View File

@ -1,3 +1,4 @@
// types/navigation.ts
import { ReactElement } from 'react';
export interface NavigationItem {
@ -10,9 +11,6 @@ export interface NavigationItem {
userTypes?: ('candidate' | 'employer' | 'guest' | 'admin')[];
exact?: boolean;
divider?: boolean;
showInNavigation?: boolean; // Controls if item appears in main navigation
showInUserMenu?: boolean; // Controls if item appears in user menu
userMenuGroup?: 'profile' | 'account' | 'system'; // Groups items in user menu
}
export interface NavigationConfig {

View File

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