Lots of changes

This commit is contained in:
James Ketr 2025-05-20 00:14:01 -07:00
parent 2e6a8d1366
commit d1e178aa61
19 changed files with 595 additions and 256 deletions

BIN
frontend/public/eliza.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -9,7 +9,6 @@ interface CopyBubbleProps extends IconButtonProps {
content: string | undefined, content: string | undefined,
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
tooltip?: string; tooltip?: string;
onClick?: () => void;
} }
const CopyBubble = ({ const CopyBubble = ({
@ -21,7 +20,7 @@ const CopyBubble = ({
} : CopyBubbleProps) => { } : CopyBubbleProps) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const handleCopy = () => { const handleCopy = (e: any) => {
if (content === undefined) { if (content === undefined) {
return; return;
} }
@ -32,14 +31,14 @@ const CopyBubble = ({
}); });
if (onClick) { if (onClick) {
onClick(); onClick(e);
} }
}; };
return ( return (
<Tooltip title={tooltip} placement="top" arrow> <Tooltip title={tooltip} placement="top" arrow>
<IconButton <IconButton
onClick={handleCopy} onClick={(e) => { handleCopy(e) }}
sx={{ sx={{
width: 24, width: 24,
height: 24, height: 24,

View File

@ -104,6 +104,8 @@ const BackstoryApp = () => {
newSessionId = (await response.json()).id; newSessionId = (await response.json()).id;
} }
setSessionId(newSessionId); setSessionId(newSessionId);
setSnack(`${action} session ${newSessionId}`);
// Store in cookie if user opts in // Store in cookie if user opts in
if (storeInCookie) { if (storeInCookie) {
setCookie('session_id', newSessionId); setCookie('session_id', newSessionId);
@ -116,7 +118,6 @@ const BackstoryApp = () => {
// Clear all query parameters, preserve the current path // Clear all query parameters, preserve the current path
navigate(location.pathname, { replace: true }); navigate(location.pathname, { replace: true });
} }
setSnack(`${action} session ${newSessionId}`);
} catch (err) { } catch (err) {
setSnack("" + err); setSnack("" + err);
} }
@ -132,9 +133,9 @@ const BackstoryApp = () => {
// Render appropriate routes based on user type // Render appropriate routes based on user type
return ( return (
<ThemeProvider theme={backstoryTheme}> <ThemeProvider theme={backstoryTheme}>
<UserProvider> <UserProvider sessionId={sessionId} setSnack={setSnack}>
<Routes> <Routes>
<Route path="/u/:user" element={<UserRoute />} /> <Route path="/u/:username" element={<UserRoute sessionId={sessionId} setSnack={setSnack} />} />
{/* Static/shared routes */} {/* Static/shared routes */}
<Route <Route
path="/*" path="/*"
@ -149,7 +150,6 @@ const BackstoryApp = () => {
/> />
} }
/> />
<Route path="*" element={<BetaPage />} />
</Routes> </Routes>
</UserProvider> </UserProvider>

View File

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

View File

@ -21,9 +21,10 @@ import { SxProps, Theme } from '@mui/material';
import {Header} from './Header'; import {Header} from './Header';
import { Scrollable } from '../../Components/Scrollable'; import { Scrollable } from '../../Components/Scrollable';
import { Footer } from './Footer'; import { Footer } from './Footer';
import { Snack } from '../../Components/Snack'; import { Snack, SetSnackType } from '../../Components/Snack';
import { UserProvider, useUser, UserType } from './UserContext'; import { UserProvider, useUser, UserInfo } from './UserContext';
import { getBackstoryDynamicRoutes } from './BackstoryDynamicRoutes'; import { getBackstoryDynamicRoutes } from './BackstoryRoutes';
import { LoadingComponent } from "../Components/LoadingComponent";
type NavigationLinkType = { type NavigationLinkType = {
name: string; name: string;
@ -33,9 +34,7 @@ type NavigationLinkType = {
}; };
const DefaultNavItems: NavigationLinkType[] = [ const DefaultNavItems: NavigationLinkType[] = [
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> }, { name: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
{ name: 'Resume Builder', path: '/resume-builder', icon: <WorkIcon /> },
{ name: 'Knowledge Explorer', path: '/knowledge-explorer', icon: <WorkIcon /> },
{ name: 'Docs', path: '/docs', icon: <InfoIcon /> }, { name: 'Docs', path: '/docs', icon: <InfoIcon /> },
// { name: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> }, // { name: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
// { name: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> }, // { name: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
@ -44,32 +43,40 @@ const DefaultNavItems : NavigationLinkType[] = [
]; ];
const CandidateNavItems : NavigationLinkType[]= [ const CandidateNavItems : NavigationLinkType[]= [
{ name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' }, { name: 'Chat', path: '/chat', icon: <ChatIcon /> },
{ name: 'Profile', icon: <PersonIcon />, path: '/profile' }, { name: 'Resume Builder', path: '/resume-builder', icon: <WorkIcon /> },
{ name: 'Backstory', icon: <HistoryIcon />, path: '/backstory' }, { name: 'Knowledge Explorer', path: '/knowledge-explorer', icon: <WorkIcon /> },
{ name: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
// { name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' },
// { name: 'Profile', icon: <PersonIcon />, path: '/profile' },
// { name: 'Backstory', icon: <HistoryIcon />, path: '/backstory' },
{ name: 'Resumes', icon: <DescriptionIcon />, path: '/resumes' }, { name: 'Resumes', icon: <DescriptionIcon />, path: '/resumes' },
{ name: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/qa-setup' }, // { name: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/qa-setup' },
{ name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' }, { name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' },
{ name: 'Settings', icon: <SettingsIcon />, path: '/settings' }, { name: 'Settings', icon: <SettingsIcon />, path: '/settings' },
]; ];
const EmployerNavItems: NavigationLinkType[] = [ const EmployerNavItems: NavigationLinkType[] = [
{ name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' }, { name: 'Chat', path: '/chat', icon: <ChatIcon /> },
{ name: 'Search', icon: <SearchIcon />, path: '/search' }, { name: 'Resume Builder', path: '/resume-builder', icon: <WorkIcon /> },
{ name: 'Saved', icon: <BookmarkIcon />, path: '/saved' }, { name: 'Knowledge Explorer', path: '/knowledge-explorer', icon: <WorkIcon /> },
{ name: 'Jobs', icon: <WorkIcon />, path: '/jobs' }, { name: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
{ name: 'Company', icon: <BusinessIcon />, path: '/company' }, // { name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' },
{ name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' }, // { name: 'Search', icon: <SearchIcon />, path: '/search' },
{ name: 'Settings', icon: <SettingsIcon />, path: '/settings' }, // { name: 'Saved', icon: <BookmarkIcon />, path: '/saved' },
// { name: 'Jobs', icon: <WorkIcon />, path: '/jobs' },
// { name: 'Company', icon: <BusinessIcon />, path: '/company' },
// { name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' },
// { name: 'Settings', icon: <SettingsIcon />, path: '/settings' },
]; ];
// Navigation links based on user type // Navigation links based on user type
const getNavigationLinks = (user: UserType | null) : NavigationLinkType[] => { const getNavigationLinks = (user: UserInfo | null): NavigationLinkType[] => {
if (!user) { if (!user) {
return DefaultNavItems; return DefaultNavItems;
} }
if (user.type === 'candidate') { if (user.type === 'candidate' && user.isAuthenticated) {
return CandidateNavItems; return CandidateNavItems;
} }
@ -101,8 +108,8 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
} }
const BackstoryLayout: React.FC<{ const BackstoryLayout: React.FC<{
sessionId?: string; sessionId: string | undefined;
setSnack: (msg: string) => void; setSnack: SetSnackType;
page: string; page: string;
chatRef: React.Ref<any>; chatRef: React.Ref<any>;
snackRef: React.Ref<any>; snackRef: React.Ref<any>;
@ -129,7 +136,7 @@ const BackstoryLayout: React.FC<{
return ( return (
<> <>
<Header setSnack={setSnack} sessionId={sessionId} user={user} currentPath={page} navigate={navigate} navigationLinks={navigationLinks} showLogin={false} /> <Header {...{ setSnack, sessionId, user, currentPath: page, navigate, navigationLinks }} />
<Box sx={{ display: "flex", minHeight: "72px", height: "72px" }} /> <Box sx={{ display: "flex", minHeight: "72px", height: "72px" }} />
<Scrollable <Scrollable
sx={{ sx={{
@ -141,8 +148,20 @@ const BackstoryLayout: React.FC<{
}} }}
> >
<BackstoryPageContainer> <BackstoryPageContainer>
{!sessionId &&
<Box>
<LoadingComponent
loadingText="Creating session..."
loaderType="linear"
withFade={true}
fadeDuration={1200} />
</Box>
}
{sessionId && <>
<Outlet /> <Outlet />
{dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>} {dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>}
</>
}
{location.pathname === "/" && <Footer />} {location.pathname === "/" && <Footer />}
</BackstoryPageContainer> </BackstoryPageContainer>
</Scrollable> </Scrollable>

View File

@ -0,0 +1,86 @@
import React, { Ref, Fragment, ReactNode } from "react";
import { Route } from "react-router-dom";
import { useUser } from "./UserContext";
import { Box, Typography, Container, Paper } from '@mui/material';
import { BackstoryPageProps } from '../../Components/BackstoryTab';
import { ConversationHandle } from './Conversation';
import { UserInfo } from './UserContext';
import { ChatPage } from '../Pages/ChatPage';
import { ResumeBuilderPage } from '../../Pages/ResumeBuilderPage';
import { DocsPage } from '../Pages/DocsPage';
import { CreateProfilePage } from '../Pages/CreateProfilePage';
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
import { HomePage } from '../Pages/HomePage';
import { BetaPage } from '../Pages/BetaPage';
import { CandidateListingPage } from '../Pages/CandidateListingPage';
const DashboardPage = () => (<BetaPage><Typography variant="h4">Dashboard</Typography></BetaPage>);
const ProfilePage = () => (<BetaPage><Typography variant="h4">Profile</Typography></BetaPage>);
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
const ResumesPage = () => (<BetaPage><Typography variant="h4">Resumes</Typography></BetaPage>);
const QASetupPage = () => (<BetaPage><Typography variant="h4">Q&A Setup</Typography></BetaPage>);
const AnalyticsPage = () => (<BetaPage><Typography variant="h4">Analytics</Typography></BetaPage>);
const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typography></BetaPage>);
const SearchPage = () => (<BetaPage><Typography variant="h4">Search</Typography></BetaPage>);
const SavedPage = () => (<BetaPage><Typography variant="h4">Saved</Typography></BetaPage>);
const JobsPage = () => (<BetaPage><Typography variant="h4">Jobs</Typography></BetaPage>);
const CompanyPage = () => (<BetaPage><Typography variant="h4">Company</Typography></BetaPage>);
const LogoutPage = () => (<BetaPage><Typography variant="h4">Logout page...</Typography></BetaPage>);
const LoginPage = () => (<BetaPage><Typography variant="h4">Login page...</Typography></BetaPage>);
interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
chatRef: Ref<ConversationHandle>
}
const getBackstoryDynamicRoutes = (props : BackstoryDynamicRoutesProps, user?: UserInfo | null) : ReactNode => {
const { sessionId, setSnack, submitQuery, chatRef } = props;
let index=0
const routes = [
<Route key={`${index++}`} path="/" element={<HomePage/>} />,
<Route key={`${index++}`} path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/docs" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/docs/:subPage" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/knowledge-explorer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />,
<Route key={`${index++}`} path="/find-a-candidate" element={<CandidateListingPage {...{sessionId, setSnack, submitQuery}} />} />,
];
if (user === undefined || user === null) {
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />);
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage />} />);
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
} else {
if (!user.isAuthenticated) {
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />);
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage />} />);
} else {
routes.push(<Route key={`${index++}`} path="/logout" element={<LogoutPage />} />);
}
if (user.type === "candidate" && user.isAuthenticated) {
routes.splice(-1, 0, ...[
<Route key={`${index++}`} path="/profile" element={<ProfilePage />} />,
<Route key={`${index++}`} path="/backstory" element={<BackstoryPage />} />,
<Route key={`${index++}`} path="/resumes" element={<ResumesPage />} />,
<Route key={`${index++}`} path="/qa-setup" element={<QASetupPage />} />,
]);
}
if (user.type === "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 />} />,
]);
}
}
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
return routes;
};
export { getBackstoryDynamicRoutes };

View File

@ -1,30 +1,10 @@
import React from 'react'; import React from 'react';
import { Box, Typography, Avatar, Paper, Grid, Chip } from '@mui/material'; import { Box, Link, Typography, Avatar, Paper, Grid, Chip, SxProps } from '@mui/material';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { Tunables } from '../../Components/ChatQuery';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { UserInfo, useUser } from "./UserContext";
// Define the UserInfo interface for type safety import LinkIcon from '@mui/icons-material/Link';
interface UserInfo { import { CopyBubble } from "../../Components/CopyBubble";
profile_url: string;
description: string;
rag_content_size: number;
user_name: string;
first_name: string;
last_name: string;
full_name: string;
contact_info: Record<string, string>;
questions: [{
question: string;
tunables?: Tunables
}]
};
// Define props interface for the component
interface CandidateInfoProps {
userInfo: UserInfo;
}
// Styled components // Styled components
const StyledPaper = styled(Paper)(({ theme }) => ({ const StyledPaper = styled(Paper)(({ theme }) => ({
padding: theme.spacing(2), padding: theme.spacing(2),
@ -33,7 +13,19 @@ const StyledPaper = styled(Paper)(({ theme }) => ({
boxShadow: theme.shadows[2], boxShadow: theme.shadows[2],
})); }));
const CandidateInfo: React.FC<CandidateInfoProps> = ({ userInfo }) => { interface CandidateInfoProps {
user?: UserInfo;
sx?: SxProps;
action?: string;
};
const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
const { user } = useUser();
const {
sx,
action = ''
} = props;
const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
// Format RAG content size (e.g., if it's in bytes, convert to KB/MB) // Format RAG content size (e.g., if it's in bytes, convert to KB/MB)
const formatRagSize = (size: number): string => { const formatRagSize = (size: number): string => {
@ -41,14 +33,19 @@ const CandidateInfo: React.FC<CandidateInfoProps> = ({ userInfo }) => {
if (size < 1000000) return `${(size / 1000).toFixed(1)}K RAG elements`; if (size < 1000000) return `${(size / 1000).toFixed(1)}K RAG elements`;
return `${(size / 1000000).toFixed(1)}M RAG elements`; return `${(size / 1000000).toFixed(1)}M RAG elements`;
}; };
const view = props.user || user;
if (!view) {
return <Box>No user loaded.</Box>;
}
return ( return (
<StyledPaper> <StyledPaper sx={sx}>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 2 }} sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}> <Grid size={{ xs: 12, sm: 2 }} sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Avatar <Avatar
src={userInfo.profile_url} src={view.profile_url}
alt={`${userInfo.full_name}'s profile`} alt={`${view.full_name}'s profile`}
sx={{ sx={{
width: 80, width: 80,
height: 80, height: 80,
@ -59,20 +56,32 @@ const CandidateInfo: React.FC<CandidateInfoProps> = ({ userInfo }) => {
<Grid size={{ xs: 12, sm: 10 }}> <Grid size={{ xs: 12, sm: 10 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
<Box>
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "center", gap: 1, "& > .MuiTypography-root": { m: 0 } }}>
{action !== '' && <Typography variant="body1">{action}</Typography>}
<Typography variant="h5" component="h1" sx={{ fontWeight: 'bold' }}> <Typography variant="h5" component="h1" sx={{ fontWeight: 'bold' }}>
{userInfo.full_name} {view.full_name}
</Typography> </Typography>
<Chip </Box>
onClick={() => navigate('/knowledge-explorer')} <Box sx={{ fontSize: "0.75rem", alignItems: "center" }} >
label={formatRagSize(userInfo.rag_content_size)} <Link href={`/u/${view.username}`}>/u/{view.username}</Link>
<CopyBubble
onClick={(event: any) => { event.stopPropagation() }}
tooltip="Copy link" content={`${window.location.origin}/u/{view.username}`} />
</Box>
</Box>
{view.rag_content_size !== undefined && view.rag_content_size > 0 && <Chip
onClick={(event: React.MouseEvent<HTMLDivElement>) => { navigate('/knowledge-explorer'); event.stopPropagation() }}
label={formatRagSize(view.rag_content_size)}
color="primary" color="primary"
size="small" size="small"
sx={{ ml: 2 }} sx={{ ml: 2 }}
/> />}
</Box> </Box>
<Typography variant="body1" color="text.secondary"> <Typography variant="body1" color="text.secondary">
{userInfo.description} {view.description}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
@ -80,8 +89,4 @@ const CandidateInfo: React.FC<CandidateInfoProps> = ({ userInfo }) => {
); );
}; };
export type {
UserInfo
};
export { CandidateInfo }; export { CandidateInfo };

View File

@ -15,6 +15,7 @@ import { Query } from '../../Components/ChatQuery';
import { BackstoryTextField, BackstoryTextFieldRef } from '../../Components/BackstoryTextField'; import { BackstoryTextField, BackstoryTextFieldRef } from '../../Components/BackstoryTextField';
import { BackstoryElementProps } from '../../Components/BackstoryTab'; import { BackstoryElementProps } from '../../Components/BackstoryTab';
import { connectionBase } from '../../Global'; import { connectionBase } from '../../Global';
import { useUser } from "../Components/UserContext";
import './Conversation.css'; import './Conversation.css';
@ -66,6 +67,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
sx, sx,
type, type,
} = props; } = props;
const { user } = useUser()
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0); const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0); const [countdown, setCountdown] = useState<number>(0);
@ -184,8 +186,15 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
return; return;
} }
setProcessingMessage(undefined);
setStreamingMessage(undefined);
setConversation([]);
setNoInteractions(true);
if (user) {
fetchHistory(); fetchHistory();
}, [fetchHistory, sessionId, setProcessing]); }
}, [fetchHistory, sessionId, setProcessing, user]);
const startCountdown = (seconds: number) => { const startCountdown = (seconds: number) => {
if (timerRef.current) clearInterval(timerRef.current); if (timerRef.current) clearInterval(timerRef.current);
@ -549,7 +558,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
{(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length && {(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length !== 0 &&
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: "flex", flexDirection: "column" }}>
{ {
defaultPrompts.map((element, index) => { defaultPrompts.map((element, index) => {

View File

@ -34,7 +34,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { NavigationLinkType } from './BackstoryLayout'; import { NavigationLinkType } from './BackstoryLayout';
import { Beta } from './Beta'; import { Beta } from './Beta';
import './Header.css'; import './Header.css';
import { UserType } from './UserContext'; import { useUser, UserInfo } from './UserContext';
import { SetSnackType } from '../../Components/Snack'; import { SetSnackType } from '../../Components/Snack';
import { CopyBubble } from '../../Components/CopyBubble'; import { CopyBubble } from '../../Components/CopyBubble';
@ -83,7 +83,6 @@ const MobileDrawer = styled(Drawer)(({ theme }) => ({
})); }));
interface HeaderProps { interface HeaderProps {
user?: UserType | null;
transparent?: boolean; transparent?: boolean;
onLogout?: () => void; onLogout?: () => void;
className?: string; className?: string;
@ -96,8 +95,9 @@ interface HeaderProps {
} }
const Header: React.FC<HeaderProps> = (props: HeaderProps) => { const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const { user } = useUser();
const { const {
user,
transparent = false, transparent = false,
className, className,
navigate, navigate,
@ -236,7 +236,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
variant="contained" variant="contained"
color="secondary" color="secondary"
fullWidth fullWidth
onClick={() => navigate("/login") } onClick={() => { navigate("/login"); }}
> >
Login Login
</Button> </Button>
@ -244,7 +244,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
variant="outlined" variant="outlined"
color="secondary" color="secondary"
fullWidth fullWidth
onClick={() => navigate("/register") } onClick={() => { navigate("/register"); }}
> >
Register Register
</Button> </Button>
@ -277,7 +277,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<Button <Button
color="secondary" color="secondary"
variant="contained" variant="contained"
onClick={() => navigate("/navigate") } onClick={() => { navigate("/register"); }}
sx={{ display: { xs: 'none', sm: 'block' } }} sx={{ display: { xs: 'none', sm: 'block' } }}
> >
Register Register
@ -299,10 +299,10 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
height: 32, height: 32,
bgcolor: theme.palette.secondary.main, bgcolor: theme.palette.secondary.main,
}}> }}>
{user?.name.charAt(0).toUpperCase()} {user?.full_name.charAt(0).toUpperCase()}
</Avatar> </Avatar>
<Box sx={{ display: { xs: 'none', sm: 'block' } }}> <Box sx={{ display: { xs: 'none', sm: 'block' } }}>
{user?.name} {user?.full_name}
</Box> </Box>
<ExpandMore fontSize="small" /> <ExpandMore fontSize="small" />
</UserButton> </UserButton>
@ -418,7 +418,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
'&:hover': { bgcolor: 'action.hover', opacity: 1 }, '&:hover': { bgcolor: 'action.hover', opacity: 1 },
}} }}
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`} content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
onClick={() => setSnack("Link copied!")} onClick={() => { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }}
size="large" size="large"
/>} />}
</UserActionsContainer> </UserActionsContainer>
@ -437,7 +437,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
</MobileDrawer> </MobileDrawer>
</Toolbar> </Toolbar>
</Container> </Container>
<Beta sx={{ left: "-72px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} /> <Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} />
</StyledAppBar> </StyledAppBar>
</Box> </Box>
); );

View File

@ -1,15 +1,29 @@
import React, { createContext, useContext, useState } from "react"; import React, { createContext, useContext, useEffect, useState } from "react";
import { Tunables } from '../../Components/ChatQuery';
import { SetSnackType } from '../../Components/Snack';
import { connectionBase } from '../../Global';
type UserType = { // Define the UserInfo interface for type safety
interface UserInfo {
type: 'candidate' | 'employer' | 'guest'; type: 'candidate' | 'employer' | 'guest';
name: string; profile_url: string;
avatar?: any; description: string;
isAuthenticated: boolean; rag_content_size: number;
logout: () => void; username: string;
first_name: string;
last_name: string;
full_name: string;
contact_info: Record<string, string>;
questions: [{
question: string;
tunables?: Tunables
}],
isAuthenticated: boolean
}; };
type UserContextType = { type UserContextType = {
user: UserType | null; user: UserInfo | null;
setUser: (user: UserType) => void; setUser: (user: UserInfo | null) => void;
}; };
const UserContext = createContext<UserContextType | undefined>(undefined); const UserContext = createContext<UserContextType | undefined>(undefined);
@ -20,8 +34,51 @@ const useUser = () => {
return ctx; return ctx;
}; };
const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { interface UserProviderProps {
const [user, setUser] = useState<UserType | null>(null); children: React.ReactNode;
sessionId: string | undefined;
setSnack: SetSnackType;
};
const UserProvider: React.FC<UserProviderProps> = (props: UserProviderProps) => {
const { sessionId, children, setSnack } = props;
const [user, setUser] = useState<UserInfo | null>(null);
useEffect(() => {
if (!sessionId || user) {
return;
}
const fetchUserFromSession = async (): Promise<UserInfo | null> => {
try {
let response;
response = await fetch(`${connectionBase}/api/user/${sessionId}`, {
method: 'GET',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Session not found');
}
const user: UserInfo = {
...(await response.json()),
type: "guest",
isAuthenticated: false,
logout: () => { },
}
console.log("Loaded user:", user);
setUser(user);
} catch (err) {
setSnack("" + err);
setUser(null);
}
return null;
};
fetchUserFromSession();
}, [sessionId, user, setUser]);
if (sessionId === undefined) {
return <></>;
}
return ( return (
<UserContext.Provider value={{ user, setUser }}> <UserContext.Provider value={{ user, setUser }}>
{children} {children}
@ -30,7 +87,7 @@ const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =>
}; };
export type { export type {
UserType UserInfo
}; };
export { export {

View File

@ -25,7 +25,7 @@ interface BetaPageProps {
onReturn?: () => void; onReturn?: () => void;
} }
export const BetaPage: React.FC<BetaPageProps> = ({ const BetaPage: React.FC<BetaPageProps> = ({
children, children,
title = "Coming Soon", title = "Coming Soon",
subtitle = "This page is currently in development", subtitle = "This page is currently in development",
@ -38,6 +38,8 @@ export const BetaPage: React.FC<BetaPageProps> = ({
const [showSparkle, setShowSparkle] = useState<boolean>(false); const [showSparkle, setShowSparkle] = useState<boolean>(false);
const navigate = useNavigate(); const navigate = useNavigate();
console.log("BetaPage", children);
// Enhanced sparkle effect for background elements // Enhanced sparkle effect for background elements
const [sparkles, setSparkles] = useState<Array<{ const [sparkles, setSparkles] = useState<Array<{
id: number; id: number;
@ -187,9 +189,9 @@ export const BetaPage: React.FC<BetaPageProps> = ({
<Typography color="textSecondary" sx={{ mt: 1 }}> <Typography color="textSecondary" sx={{ mt: 1 }}>
Check back soon for updates. Check back soon for updates.
</Typography> </Typography>
<Beta adaptive={false} sx={{ left: "-72px", "& > div": { paddingRight: "30px", background: "gold", color: "#808080" } }} onClick={() => { navigate('/docs/beta'); }} />
</Box> </Box>
)} )}
<Beta adaptive={false} sx={{ opacity: 0.5, left: "-72px", "& > div": { paddingRight: "30px", background: "gold", color: "#808080" } }} onClick={() => { navigate('/docs/beta'); }} />
</Box> </Box>
{/* Return button */} {/* Return button */}
@ -265,3 +267,7 @@ export const BetaPage: React.FC<BetaPageProps> = ({
</Box> </Box>
); );
}; };
export {
BetaPage
}

View File

@ -0,0 +1,82 @@
import React, { forwardRef, useEffect, useState, MouseEventHandler } from 'react';
import { useNavigate } from "react-router-dom";
import useMediaQuery from '@mui/material/useMediaQuery';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
import MuiMarkdown from 'mui-markdown';
import { BackstoryPageProps } from '../../Components/BackstoryTab';
import { Conversation, ConversationHandle } from '../Components/Conversation';
import { ChatQuery, Tunables } from '../../Components/ChatQuery';
import { MessageList } from '../../Components/Message';
import { CandidateInfo } from 'NewApp/Components/CandidateInfo';
import { connectionBase } from '../../Global';
import { LoadingComponent } from 'NewApp/Components/LoadingComponent';
import { useUser, UserInfo } from "../Components/UserContext";
import { Navigate } from 'react-router-dom';
const CandidateListingPage = (props: BackstoryPageProps) => {
const navigate = useNavigate();
const { sessionId, setSnack, submitQuery } = props;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const { user } = useUser();
const [users, setUsers] = useState<UserInfo[] | undefined>(undefined);
useEffect(() => {
if (users !== undefined) {
return;
}
const fetchUsers = async () => {
try {
let response;
response = await fetch(`${connectionBase}/api/u/${sessionId}`, {
credentials: 'include',
});
if (!response.ok) {
throw new Error('Session not found');
}
const users: UserInfo[] = await response.json();
users.forEach(u => {
u.type = 'guest';
u.isAuthenticated = false;
});
users.sort((a, b) => {
let result = a.last_name.localeCompare(b.last_name);
if (result === 0) {
result = a.first_name.localeCompare(b.first_name);
}
if (result === 0) {
result = a.username.localeCompare(b.username);
}
return result;
});
console.log(users);
setUsers(users);
} catch (err) {
setSnack("" + err);
}
};
fetchUsers();
}, [users]);
return (
<Box>
{users?.map((u, i) =>
<Box key={`${u.username}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) : void => {
navigate(`/u/${u.username}`)
}}
sx={{ cursor: "pointer" }}
>
<CandidateInfo sx={{ "cursor": "pointer", "&:hover": { border: "2px solid orange" }, border: "2px solid transparent"}} user={u}/>
</Box>
)}
</Box>
);
};
export {
CandidateListingPage
};

View File

@ -1,27 +1,30 @@
import React, { forwardRef, useEffect, useState } from 'react'; import React, { forwardRef, useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom";
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import MuiMarkdown from 'mui-markdown'; import MuiMarkdown from 'mui-markdown';
import { BackstoryPageProps } from '../../Components//BackstoryTab'; import { BackstoryPageProps } from '../../Components/BackstoryTab';
import { Conversation, ConversationHandle } from '../Components/Conversation'; import { Conversation, ConversationHandle } from '../Components/Conversation';
import { ChatQuery, Tunables } from '../../Components/ChatQuery'; import { ChatQuery, Tunables } from '../../Components/ChatQuery';
import { MessageList } from '../../Components/Message'; import { MessageList } from '../../Components/Message';
import { CandidateInfo, UserInfo } from 'NewApp/Components/CandidateInfo'; import { CandidateInfo } from 'NewApp/Components/CandidateInfo';
import { connectionBase } from '../../Global'; import { connectionBase } from '../../Global';
import { LoadingComponent } from 'NewApp/Components/LoadingComponent'; import { LoadingComponent } from 'NewApp/Components/LoadingComponent';
import { Typography } from '@mui/material'; import { useUser } from "../Components/UserContext";
import { Navigate } from 'react-router-dom';
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => { const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
const navigate = useNavigate();
const { sessionId, setSnack, submitQuery } = props; const { sessionId, setSnack, submitQuery } = props;
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [questions, setQuestions] = useState<React.ReactElement[]>([]); const [questions, setQuestions] = useState<React.ReactElement[]>([]);
const [user, setUser] = useState<UserInfo | undefined>(undefined) const { user } = useUser();
useEffect(() => { useEffect(() => {
if (user === undefined) { if (!user) {
return; return;
} }
@ -38,50 +41,18 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
</Box>]); </Box>]);
}, [user, isMobile, submitQuery]); }, [user, isMobile, submitQuery]);
useEffect(() => { if (!user) {
const fetchUserInfo = async () => { return (<></>);
try {
const response = await fetch(connectionBase + `/api/user/${sessionId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
} }
const data = await response.json();
setUser(data);
}
catch (error) {
console.error('Error getting user info:', error);
setSnack("Unable to obtain user information.", "error");
}
};
fetchUserInfo();
}, [setSnack, sessionId]);
if (sessionId === undefined || user === undefined) {
return (<Box>
<LoadingComponent
loadingText="Fetching user information..."
loaderType="linear"
withFade={true}
fadeDuration={1200} />
</Box>);
}
return ( return (
<Box> <Box>
<CandidateInfo userInfo={user} /> <CandidateInfo action="Chat with AI about " />
<Conversation <Conversation
ref={ref} ref={ref}
{...{ {...{
multiline: true, multiline: true,
type: "chat", type: "chat",
placeholder: `What would you like to know about ${user.first_name}?`, placeholder: `What would you like to know about ${user?.first_name}?`,
resetLabel: "chat", resetLabel: "chat",
sessionId, sessionId,
setSnack, setSnack,

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { import {
Box, Box,
Button, Button,
@ -20,6 +21,7 @@ import {
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { CloudUpload, PhotoCamera } from '@mui/icons-material'; import { CloudUpload, PhotoCamera } from '@mui/icons-material';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { Beta } from '../Components/Beta';
// Interfaces // Interfaces
interface ProfileFormData { interface ProfileFormData {
@ -47,6 +49,7 @@ const VisuallyHiddenInput = styled('input')({
const CreateProfilePage: React.FC = () => { const CreateProfilePage: React.FC = () => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
// State management // State management
@ -280,7 +283,7 @@ const CreateProfilePage: React.FC = () => {
<Grid size={{xs: 12}}> <Grid size={{xs: 12}}>
<Typography variant="body1" component="p"> <Typography variant="body1" component="p">
Upload your resume to complete your profile. We'll analyze it to better understand your skills and experience. Upload your resume to complete your profile. We'll analyze it to better understand your skills and experience.
(Supported formats: PDF, DOCX) (Supported formats: .pdf, .docx, .md, and .txt)
</Typography> </Typography>
<Box sx={{ textAlign: 'center', mt: 2 }}> <Box sx={{ textAlign: 'center', mt: 2 }}>
<Button <Button
@ -292,7 +295,7 @@ const CreateProfilePage: React.FC = () => {
Upload Resume Upload Resume
<VisuallyHiddenInput <VisuallyHiddenInput
type="file" type="file"
accept=".pdf,.docx" accept=".pdf,.docx,.txt,.md"
onChange={handleResumeUpload} onChange={handleResumeUpload}
/> />
</Button> </Button>
@ -318,7 +321,7 @@ const CreateProfilePage: React.FC = () => {
sx={{ sx={{
p: { xs: 2, sm: 4 }, p: { xs: 2, sm: 4 },
mt: { xs: 2, sm: 4 }, mt: { xs: 2, sm: 4 },
mb: { xs: 2, sm: 4 } mb: { xs: 2, sm: 4 },
}} }}
> >
<Typography component="h1" variant="h4" align="center" gutterBottom> <Typography component="h1" variant="h4" align="center" gutterBottom>

View File

@ -1,38 +1,77 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useParams } from "react-router-dom"; import { Navigate, useParams, useNavigate, useLocation } from "react-router-dom";
import { useUser, UserType } from "../Components/UserContext"; import { useUser, UserInfo } from "../Components/UserContext";
import { Typography, Box } from "@mui/material"; import { Box } from "@mui/material";
import { connectionBase } from "../../Global";
import { SetSnackType } from '../../Components/Snack';
import { LoadingComponent } from "../Components/LoadingComponent";
const mockFetchUser = async (username: string) => { interface UserRouteProps {
return new Promise<UserType>((resolve) => { sessionId?: string | null;
const user : UserType = { setSnack: SetSnackType,
type: "candidate",
name: username,
isAuthenticated: true,
logout: () => {},
};
setTimeout(() => resolve(user), 500);
});
}; };
const UserRoute: React.FC = () => { const UserRoute: React.FC<UserRouteProps> = (props: UserRouteProps) => {
const { sessionId, setSnack } = props;
const { username } = useParams<{ username: string }>(); const { username } = useParams<{ username: string }>();
const { user, setUser } = useUser(); const { user, setUser } = useUser();
const navigate = useNavigate();
const location = useLocation();
useEffect(() => { useEffect(() => {
if (username) { if (!sessionId) {
mockFetchUser(username).then(setUser); return;
} }
}, [username, setUser]);
return ( const fetchUser = async (username: string): Promise<UserInfo | null> => {
<Box m={2}> try {
<Typography variant="h5">User Page</Typography> let response;
<Typography variant="body1"> response = await fetch(`${connectionBase}/api/u/${username}/${sessionId}`, {
{user ? `Hello, ${user.name}` : "Loading..."} method: 'POST',
</Typography> credentials: 'include',
</Box> });
); if (!response.ok) {
throw new Error('Session not found');
}
const user: UserInfo = {
...(await response.json()),
type: "guest",
isAuthenticated: false,
logout: () => { },
}
console.log("Loaded user:", user);
setUser(user);
navigate('/chat');
} catch (err) {
setSnack("" + err);
setUser(null);
navigate('/');
}
return null;
};
if (user?.username !== username && username) {
fetchUser(username);
} else {
if (user?.username) {
navigate('/chat');
} else {
navigate('/');
}
}
}, [user, username, setUser, sessionId, setSnack, navigate]);
if (sessionId === undefined || !user) {
return (<Box>
<LoadingComponent
loadingText="Fetching user information..."
loaderType="linear"
withFade={true}
fadeDuration={1200} />
</Box>);
} else {
return (<></>);
}
}; };
export { UserRoute }; export { UserRoute };

View File

@ -752,6 +752,69 @@ class WebServer:
return JSONResponse({"error": f"{context_id} does not exist."}, 404) return JSONResponse({"error": f"{context_id} does not exist."}, 404)
return JSONResponse({"id": context.id}) return JSONResponse({"id": context.id})
@self.app.get("/api/u/{context_id}")
async def get_users(context_id: str, request: Request):
logger.info(f"{request.method} {request.url.path}")
try:
context = self.load_context(context_id)
if not context:
return JSONResponse({"error": f"Context {context_id} not found."}, status_code=404)
users = [User.sanitize(u) for u in User.get_users()]
return JSONResponse(users)
except Exception as e:
logger.error(traceback.format_exc())
logger.error(f"get_users error: {str(e)}")
return JSONResponse({ "error": "Unable to parse users"}, 500)
@self.app.post("/api/u/{username}/{context_id}")
async def post_user(username: str, context_id: str, request: Request):
logger.info(f"{request.method} {request.url.path}")
try:
if not User.exists(username):
return JSONResponse({"error": f"User {username} not found."}, status_code=404)
context = self.load_context(context_id)
if not context:
return JSONResponse({"error": f"Context {context_id} not found."}, status_code=404)
matching_user = next((user for user in self.users if user.username == username), None)
if matching_user:
user = matching_user
else:
user = User(username=username, llm=self.llm)
user.initialize(prometheus_collector=self.prometheus_collector)
self.users.append(user)
reset_map = (
"chat",
"job_description",
"resume",
"fact_check",
)
for mode in reset_map:
tmp = context.get_agent(mode)
if not tmp:
continue
logger.info(f"User change: Resetting history for {mode}")
if mode != "chat":
context.remove_agent(tmp)
tmp.conversation.reset()
context.user = user
user_data = {
"username": user.username,
"first_name": user.first_name,
"last_name": user.last_name,
"full_name": user.full_name,
"description": user.description,
"contact_info": user.contact_info,
"rag_content_size": user.rag_content_size,
"profile_url": user.profile_url,
"questions": [ q.model_dump(mode='json') for q in user.user_questions],
}
self.save_context(context_id)
return JSONResponse(user_data)
except Exception as e:
return JSONResponse({ "error": "Unable to load user {username}"}, 500)
@self.app.post("/api/context/u/{username}") @self.app.post("/api/context/u/{username}")
async def create_user_context(username: str, request: Request): async def create_user_context(username: str, request: Request):
logger.info(f"{request.method} {request.url.path}") logger.info(f"{request.method} {request.url.path}")

View File

@ -70,12 +70,14 @@ class Context(BaseModel):
raise ValueError("Attempt to dereference default_factory constructed User") raise ValueError("Attempt to dereference default_factory constructed User")
return self.Context__user return self.Context__user
# Only allow setting of 'user' once
@user.setter @user.setter
def user(self, new_user: User) -> User: def user(self, new_user: User) -> User:
if self.Context__user.username != "__invalid__":
raise ValueError("user can only be set once")
logger.info(f"Binding context {self.id} to user {new_user.username}") logger.info(f"Binding context {self.id} to user {new_user.username}")
if self.Context__user.username != "__invalid__" and new_user.username != self.Context__user.username:
logger.info(f"Resetting context changing from {self.Context__user.username} to user {new_user.username}")
self.agents = [a for a in self.agents if a.agent_type == "chat"]
for a in self.agents:
a.conversation.reset()
self.username = new_user.username self.username = new_user.username
self.Context__user = new_user self.Context__user = new_user
return new_user return new_user

View File

@ -171,6 +171,73 @@ class User(BaseModel):
yield message yield message
return return
@classmethod
def sanitize(cls, user: Dict[str, Any]):
sanitized : Dict[str, Any] = {}
sanitized["username"] = user.get("username")
sanitized["first_name"] = user.get("first_name", sanitized["username"])
sanitized["last_name"] = user.get("last_name", "")
sanitized["full_name"] = user.get("full_name", f"{sanitized["first_name"]} {sanitized["last_name"]}")
sanitized["description"] = user.get("description", "")
sanitized["profile_url"] = user.get("profile_url", "")
contact_info = user.get("contact_info", {})
sanitized["contact_info"] = {}
for key in contact_info:
if not isinstance(contact_info[key], (str, int, float, complex)):
continue
sanitized["contact_info"][key] = contact_info[key]
questions = user.get("questions", [ f"Tell me about {sanitized['first_name']}.", f"What are {sanitized['first_name']}'s professional strengths?"])
sanitized["user_questions"] = []
for question in questions:
if type(question) == str:
sanitized["user_questions"].append({"question": question})
else:
try:
tmp = Question.model_validate(question)
sanitized["user_questions"].append({"question": tmp.question})
except Exception as e:
continue
return sanitized
@classmethod
def get_users(cls):
# Initialize an empty list to store parsed JSON data
user_data = []
# Define the users directory path
users_dir = os.path.join(defines.user_dir)
# Check if the users directory exists
if not os.path.exists(users_dir):
return user_data
# Iterate through all items in the users directory
for item in os.listdir(users_dir):
# Construct the full path to the item
item_path = os.path.join(users_dir, item)
# Check if the item is a directory
if os.path.isdir(item_path):
# Construct the path to info.json
info_path = os.path.join(item_path, "info.json")
# Check if info.json exists
if os.path.exists(info_path):
try:
# Read and parse the JSON file
with open(info_path, 'r') as file:
data = json.load(file)
data["username"] = item
user_data.append(data)
except json.JSONDecodeError:
# Skip files that aren't valid JSON
continue
except Exception as e:
# Skip files that can't be read
continue
return user_data
def initialize(self, prometheus_collector): def initialize(self, prometheus_collector):
if self.User__initialized: if self.User__initialized:
# Initialization can only be attempted once; if there are multiple attempts, it means # Initialization can only be attempted once; if there are multiple attempts, it means
@ -200,8 +267,8 @@ class User(BaseModel):
self.first_name = info.get("first_name", self.username) self.first_name = info.get("first_name", self.username)
self.last_name = info.get("last_name", "") self.last_name = info.get("last_name", "")
self.full_name = info.get("full_name", f"{self.first_name} {self.last_name}") self.full_name = info.get("full_name", f"{self.first_name} {self.last_name}")
self.description = info.get("description", self.description) self.description = info.get("description", "")
self.profile_url = info.get("profile_url", self.description) self.profile_url = info.get("profile_url", "")
self.contact_info = info.get("contact_info", {}) self.contact_info = info.get("contact_info", {})
questions = info.get("questions", [ f"Tell me about {self.first_name}.", f"What are {self.first_name}'s professional strengths?"]) questions = info.get("questions", [ f"Tell me about {self.first_name}.", f"What are {self.first_name}'s professional strengths?"])
self.user_questions = [] self.user_questions = []

View File

@ -1,6 +1,8 @@
{ {
"first_name": "Eliza", "first_name": "Eliza",
"last_name": "Morgan", "last_name": "Morgan",
"description": "Eliza Morgan is an AI generated persona. In addition, she is a conservation botanist with over a decade of experience in leading ecological restoration projects, managing native plant programs, and advancing rare plant propagation methods across the Pacific Northwest. Her proven record of scientific innovation, effective stakeholder engagement, and successful grant writing are key to her professional strengths.",
"profile_url": "https://backstory.ketrenos.com/eliza.png",
"questions": [ "questions": [
"Is Eliza real?", "Is Eliza real?",
"What are Eliza's skills?" "What are Eliza's skills?"