New UI flows...
This commit is contained in:
parent
23a48738a3
commit
2e6a8d1366
1217
frontend/public/docs/backstory-ui-mockups.md
Executable file
1217
frontend/public/docs/backstory-ui-mockups.md
Executable file
File diff suppressed because it is too large
Load Diff
@ -1,18 +1,23 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
|
||||||
import { Tooltip } from '@mui/material';
|
import { Tooltip } from '@mui/material';
|
||||||
import { SxProps, Theme } from '@mui/material';
|
import { SxProps, Theme } from '@mui/material';
|
||||||
|
|
||||||
interface CopyBubbleProps {
|
interface CopyBubbleProps extends IconButtonProps {
|
||||||
content: string | undefined,
|
content: string | undefined,
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
|
tooltip?: string;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CopyBubble = ({
|
const CopyBubble = ({
|
||||||
content,
|
content,
|
||||||
sx,
|
sx,
|
||||||
|
tooltip = "Copy to clipboard",
|
||||||
|
onClick,
|
||||||
|
...rest
|
||||||
} : CopyBubbleProps) => {
|
} : CopyBubbleProps) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
@ -25,10 +30,14 @@ const CopyBubble = ({
|
|||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds
|
setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (onClick) {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title="Copy to clipboard" placement="top" arrow>
|
<Tooltip title={tooltip} placement="top" arrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
sx={{
|
sx={{
|
||||||
@ -41,6 +50,7 @@ return (
|
|||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
color={copied ? "success" : "default"}
|
color={copied ? "success" : "default"}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{copied ? <CheckIcon sx={{ width: 16, height: 16 }} /> : <ContentCopyIcon sx={{ width: 16, height: 16 }} />}
|
{copied ? <CheckIcon sx={{ width: 16, height: 16 }} /> : <ContentCopyIcon sx={{ width: 16, height: 16 }} />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -1,39 +1,16 @@
|
|||||||
import React, { ReactElement, useEffect, useState, useRef, useCallback} from 'react';
|
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
import { Box, Typography, Container, Paper } from '@mui/material';
|
|
||||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
|
||||||
import PersonIcon from '@mui/icons-material/Person';
|
|
||||||
import ChatIcon from '@mui/icons-material/Chat';
|
|
||||||
import HistoryIcon from '@mui/icons-material/History';
|
|
||||||
import DescriptionIcon from '@mui/icons-material/Description';
|
|
||||||
import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer';
|
|
||||||
import BarChartIcon from '@mui/icons-material/BarChart';
|
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
|
||||||
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
|
||||||
import WorkIcon from '@mui/icons-material/Work';
|
|
||||||
import InfoIcon from '@mui/icons-material/Info';
|
|
||||||
import BusinessIcon from '@mui/icons-material/Business';
|
|
||||||
import { SxProps, Theme } from '@mui/material';
|
|
||||||
import { ThemeProvider } from '@mui/material/styles';
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
|
|
||||||
import { backstoryTheme } from '../BackstoryTheme';
|
import { backstoryTheme } from '../BackstoryTheme';
|
||||||
|
|
||||||
import {Header} from './Components/Header';
|
import { SeverityType } from '../Components/Snack';
|
||||||
import { Scrollable } from '../Components/Scrollable';
|
|
||||||
import { Footer } from './Components/Footer';
|
|
||||||
import { Snack, SeverityType } from '../Components/Snack';
|
|
||||||
import { Query } from '../Components/ChatQuery';
|
import { Query } from '../Components/ChatQuery';
|
||||||
import { ConversationHandle } from './Components/Conversation';
|
import { ConversationHandle } from './Components/Conversation';
|
||||||
|
import { UserProvider } from './Components/UserContext';
|
||||||
import { HomePage } from './Pages/HomePage';
|
|
||||||
import { ChatPage } from './Pages/ChatPage';
|
|
||||||
import { ResumeBuilderPage } from '../Pages/ResumeBuilderPage';
|
|
||||||
// import { BackstoryThemeVisualizer } from './BackstoryThemeVisualizer';
|
|
||||||
import { DocsPage } from './Pages/DocsPage';
|
|
||||||
import { BetaPage } from './Pages/BetaPage';
|
import { BetaPage } from './Pages/BetaPage';
|
||||||
import { CreateProfilePage } from './Pages/CreateProfilePage';
|
import { UserRoute } from './routes/UserRoute';
|
||||||
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
|
import { BackstoryLayout } from './Components/BackstoryLayout';
|
||||||
|
|
||||||
import './BackstoryApp.css';
|
import './BackstoryApp.css';
|
||||||
import '@fontsource/roboto/300.css';
|
import '@fontsource/roboto/300.css';
|
||||||
@ -43,118 +20,6 @@ import '@fontsource/roboto/700.css';
|
|||||||
|
|
||||||
import { connectionBase } from '../Global';
|
import { connectionBase } from '../Global';
|
||||||
|
|
||||||
type NavigationLinkType = {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
icon?: ReactElement<any>;
|
|
||||||
label?: ReactElement<any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DefaultNavItems : NavigationLinkType[] = [
|
|
||||||
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
|
||||||
{ name: 'Resume Builder', path: '/resume-builder', icon: <WorkIcon /> },
|
|
||||||
{ name: 'Knowledge Explorer', path: '/knowledge-explorer', icon: <WorkIcon /> },
|
|
||||||
{ name: 'Docs', path: '/docs', icon: <InfoIcon /> },
|
|
||||||
// { name: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
|
|
||||||
// { name: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
|
|
||||||
// { name: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
|
|
||||||
// { name: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
|
|
||||||
];
|
|
||||||
|
|
||||||
const CandidateNavItems : NavigationLinkType[]= [
|
|
||||||
{ 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: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/qa-setup' },
|
|
||||||
{ name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' },
|
|
||||||
{ name: 'Settings', icon: <SettingsIcon />, path: '/settings' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const EmployerNavItems: NavigationLinkType[] = [
|
|
||||||
{ name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' },
|
|
||||||
{ name: 'Search', icon: <SearchIcon />, path: '/search' },
|
|
||||||
{ 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
|
|
||||||
const getNavigationLinks = (userType: string, isLoggedIn: boolean) : NavigationLinkType[] => {
|
|
||||||
if (!isLoggedIn) {
|
|
||||||
return DefaultNavItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userType === 'candidate') {
|
|
||||||
return CandidateNavItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Employer navigation
|
|
||||||
return EmployerNavItems;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Placeholder for page components
|
|
||||||
const DashboardPage = () => <Box p={3}><Typography variant="h4">Dashboard</Typography></Box>;
|
|
||||||
const ProfilePage = () => <Box p={3}><Typography variant="h4">Profile</Typography></Box>;
|
|
||||||
const BackstoryPage = () => <Box p={3}><Typography variant="h4">Backstory</Typography></Box>;
|
|
||||||
const ResumesPage = () => <Box p={3}><Typography variant="h4">Resumes</Typography></Box>;
|
|
||||||
const QASetupPage = () => <Box p={3}><Typography variant="h4">Q&A Setup</Typography></Box>;
|
|
||||||
const AnalyticsPage = () => <Box p={3}><Typography variant="h4">Analytics</Typography></Box>;
|
|
||||||
const SettingsPage = () => <Box p={3}><Typography variant="h4">Settings</Typography></Box>;
|
|
||||||
const SearchPage = () => <Box p={3}><Typography variant="h4">Search</Typography></Box>;
|
|
||||||
const SavedPage = () => <Box p={3}><Typography variant="h4">Saved</Typography></Box>;
|
|
||||||
const JobsPage = () => <Box p={3}><Typography variant="h4">Jobs</Typography></Box>;
|
|
||||||
const CompanyPage = () => <Box p={3}><Typography variant="h4">Company</Typography></Box>;
|
|
||||||
|
|
||||||
// This is a placeholder for your actual user context
|
|
||||||
type UserContext = {
|
|
||||||
user: {
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
avatar?: any;
|
|
||||||
};
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
logout: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useUserContext = () : UserContext => {
|
|
||||||
return {
|
|
||||||
user: {
|
|
||||||
type: 'candidate', // or 'employer'
|
|
||||||
name: 'John Doe',
|
|
||||||
avatar: null,
|
|
||||||
},
|
|
||||||
isAuthenticated: false,
|
|
||||||
logout: () => console.log('Logging out'),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
interface BackstoryPageContainerProps {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
sx?: SxProps<Theme>;
|
|
||||||
userContext: UserContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
|
|
||||||
const { children, sx } = props;
|
|
||||||
return (
|
|
||||||
<Container maxWidth="xl" sx={{ mt: 2, mb: 2, ...sx }}>
|
|
||||||
<Paper
|
|
||||||
elevation={2}
|
|
||||||
sx={{
|
|
||||||
p: 3,
|
|
||||||
backgroundColor: 'background.paper',
|
|
||||||
borderRadius: 2,
|
|
||||||
minHeight: '80vh',
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</Paper>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cookie handling functions
|
// Cookie handling functions
|
||||||
const getCookie = (name: string) => {
|
const getCookie = (name: string) => {
|
||||||
const value = `; ${document.cookie}`;
|
const value = `; ${document.cookie}`;
|
||||||
@ -171,9 +36,6 @@ const setCookie = (name: string, value: string, days = 7) => {
|
|||||||
const BackstoryApp = () => {
|
const BackstoryApp = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const userContext : UserContext = useUserContext();
|
|
||||||
const { user, isAuthenticated } = userContext;
|
|
||||||
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
|
|
||||||
const snackRef = useRef<any>(null);
|
const snackRef = useRef<any>(null);
|
||||||
const chatRef = useRef<ConversationHandle>(null);
|
const chatRef = useRef<ConversationHandle>(null);
|
||||||
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||||
@ -199,15 +61,17 @@ const BackstoryApp = () => {
|
|||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
let newSessionId;
|
let newSessionId;
|
||||||
|
let action = ""
|
||||||
if (urlSessionId) {
|
if (urlSessionId) {
|
||||||
// Attempt to join session from URL
|
// Attempt to join session from URL
|
||||||
response = await fetch(`${connectionBase}/join-session/${urlSessionId}`, {
|
response = await fetch(`${connectionBase}/api/join-session/${urlSessionId}`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Session not found');
|
throw new Error('Session not found');
|
||||||
}
|
}
|
||||||
newSessionId = (await response.json()).id;
|
newSessionId = (await response.json()).id;
|
||||||
|
action = "Joined";
|
||||||
} else if (cookieSessionId) {
|
} else if (cookieSessionId) {
|
||||||
// Attempt to join session from cookie
|
// Attempt to join session from cookie
|
||||||
response = await fetch(`${connectionBase}/api/join-session/${cookieSessionId}`, {
|
response = await fetch(`${connectionBase}/api/join-session/${cookieSessionId}`, {
|
||||||
@ -222,6 +86,9 @@ const BackstoryApp = () => {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to create session');
|
throw new Error('Failed to create session');
|
||||||
}
|
}
|
||||||
|
action = "Created new";
|
||||||
|
} else {
|
||||||
|
action = "Joined";
|
||||||
}
|
}
|
||||||
newSessionId = (await response.json()).id;
|
newSessionId = (await response.json()).id;
|
||||||
} else {
|
} else {
|
||||||
@ -233,6 +100,7 @@ const BackstoryApp = () => {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to create session');
|
throw new Error('Failed to create session');
|
||||||
}
|
}
|
||||||
|
action = "Created new";
|
||||||
newSessionId = (await response.json()).id;
|
newSessionId = (await response.json()).id;
|
||||||
}
|
}
|
||||||
setSessionId(newSessionId);
|
setSessionId(newSessionId);
|
||||||
@ -241,9 +109,14 @@ const BackstoryApp = () => {
|
|||||||
setCookie('session_id', newSessionId);
|
setCookie('session_id', newSessionId);
|
||||||
}
|
}
|
||||||
// Update URL without reloading
|
// Update URL without reloading
|
||||||
if (!storeInCookie || (urlSessionId && urlSessionId !== newSessionId)) {
|
if (!storeInCookie) {
|
||||||
window.history.replaceState(null, '', `?id=${newSessionId}`);
|
// Update only the 'id' query parameter, preserving the current path
|
||||||
|
navigate(`${location.pathname}?id=${newSessionId}`, { replace: true });
|
||||||
|
} else {
|
||||||
|
// Clear all query parameters, preserve the current path
|
||||||
|
navigate(location.pathname, { replace: true });
|
||||||
}
|
}
|
||||||
|
setSnack(`${action} session ${newSessionId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSnack("" + err);
|
setSnack("" + err);
|
||||||
}
|
}
|
||||||
@ -251,89 +124,40 @@ const BackstoryApp = () => {
|
|||||||
fetchSession();
|
fetchSession();
|
||||||
}, [cookieSessionId, setSnack, storeInCookie, urlSessionId]);
|
}, [cookieSessionId, setSnack, storeInCookie, urlSessionId]);
|
||||||
|
|
||||||
const copyLink = () => {
|
|
||||||
const link = `${window.location.origin}${window.location.pathname}?id=${sessionId}`;
|
|
||||||
navigator.clipboard.writeText(link).then(() => {
|
|
||||||
alert('Link copied to clipboard!');
|
|
||||||
}).catch(() => {
|
|
||||||
alert('Failed to copy link');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
|
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
|
||||||
setPage(currentRoute);
|
setPage(currentRoute);
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNavigationLinks(getNavigationLinks(user.type, isAuthenticated));
|
|
||||||
}, [user.type, isAuthenticated]);
|
|
||||||
|
|
||||||
// Render appropriate routes based on user type
|
// Render appropriate routes based on user type
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={backstoryTheme}>
|
<ThemeProvider theme={backstoryTheme}>
|
||||||
<Header currentPath={page} navigate={navigate} navigationLinks={navigationLinks} showLogin={false}/>
|
<UserProvider>
|
||||||
<Box sx={{ display: "flex", minHeight: "72px", height: "72px" }}/>
|
<Routes>
|
||||||
<Scrollable sx={{
|
<Route path="/u/:user" element={<UserRoute />} />
|
||||||
display: 'flex',
|
{/* Static/shared routes */}
|
||||||
flexDirection: 'column',
|
<Route
|
||||||
backgroundColor: 'background.default',
|
path="/*"
|
||||||
maxHeight: "calc(100vh - 72px)",
|
element={
|
||||||
minHeight: "calc(100vh - 72px)",
|
<BackstoryLayout
|
||||||
}}>
|
sessionId={sessionId}
|
||||||
<BackstoryPageContainer userContext={userContext}>
|
setSnack={setSnack}
|
||||||
{sessionId !== undefined &&
|
page={page}
|
||||||
<Routes>
|
chatRef={chatRef}
|
||||||
<Route path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
snackRef={snackRef}
|
||||||
<Route path="/docs" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
submitQuery={submitQuery}
|
||||||
<Route path="/docs/:subPage" element={<DocsPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
/>
|
||||||
<Route path="/resume-builder" element={<ResumeBuilderPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
}
|
||||||
<Route path="/knowledge-explorer" element={<VectorVisualizerPage setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
/>
|
||||||
<Route path="/create-your-profile" element={<CreateProfilePage />} />
|
<Route path="*" element={<BetaPage />} />
|
||||||
<Route path="/dashboard" element={<DashboardPage />} />
|
</Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
</UserProvider>
|
||||||
{/* Candidate-specific routes */}
|
|
||||||
{user.type === 'candidate' && (
|
|
||||||
<>
|
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
|
||||||
<Route path="/backstory" element={<BackstoryPage />} />
|
|
||||||
<Route path="/resumes" element={<ResumesPage />} />
|
|
||||||
<Route path="/qa-setup" element={<QASetupPage />} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Employer-specific routes */}
|
|
||||||
{user.type === 'employer' && (
|
|
||||||
<>
|
|
||||||
<Route path="/search" element={<SearchPage />} />
|
|
||||||
<Route path="/saved" element={<SavedPage />} />
|
|
||||||
<Route path="/jobs" element={<JobsPage />} />
|
|
||||||
<Route path="/company" element={<CompanyPage />} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Common routes */}
|
|
||||||
<Route path="/analytics" element={<AnalyticsPage />} />
|
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
|
||||||
|
|
||||||
{/* Redirect to BETA by default */}
|
|
||||||
<Route path="*" element={<BetaPage />} />
|
|
||||||
</Routes>
|
|
||||||
}
|
|
||||||
{location.pathname === "/" && <Footer />}
|
|
||||||
</BackstoryPageContainer>
|
|
||||||
</Scrollable>
|
|
||||||
<Snack ref={snackRef}/>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type {
|
|
||||||
UserContext,
|
|
||||||
NavigationLinkType
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
BackstoryApp
|
BackstoryApp
|
||||||
};
|
};
|
71
frontend/src/NewApp/Components/BackstoryDynamicRoutes.tsx
Normal file
71
frontend/src/NewApp/Components/BackstoryDynamicRoutes.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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 };
|
160
frontend/src/NewApp/Components/BackstoryLayout.tsx
Normal file
160
frontend/src/NewApp/Components/BackstoryLayout.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import React, { ReactElement, useEffect, useState, useRef, useCallback, createContext, useContext } from 'react';
|
||||||
|
import { Outlet, useLocation, Routes } from "react-router-dom";
|
||||||
|
import { Box, Container, Paper } from '@mui/material';
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||||
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
|
import ChatIcon from '@mui/icons-material/Chat';
|
||||||
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
|
import DescriptionIcon from '@mui/icons-material/Description';
|
||||||
|
import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer';
|
||||||
|
import BarChartIcon from '@mui/icons-material/BarChart';
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
||||||
|
import WorkIcon from '@mui/icons-material/Work';
|
||||||
|
import InfoIcon from '@mui/icons-material/Info';
|
||||||
|
import BusinessIcon from '@mui/icons-material/Business';
|
||||||
|
import { SxProps, Theme } from '@mui/material';
|
||||||
|
|
||||||
|
|
||||||
|
import {Header} from './Header';
|
||||||
|
import { Scrollable } from '../../Components/Scrollable';
|
||||||
|
import { Footer } from './Footer';
|
||||||
|
import { Snack } from '../../Components/Snack';
|
||||||
|
import { UserProvider, useUser, UserType } from './UserContext';
|
||||||
|
import { getBackstoryDynamicRoutes } from './BackstoryDynamicRoutes';
|
||||||
|
|
||||||
|
type NavigationLinkType = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
icon?: ReactElement<any>;
|
||||||
|
label?: ReactElement<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultNavItems : NavigationLinkType[] = [
|
||||||
|
{ name: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||||
|
{ name: 'Resume Builder', path: '/resume-builder', icon: <WorkIcon /> },
|
||||||
|
{ name: 'Knowledge Explorer', path: '/knowledge-explorer', icon: <WorkIcon /> },
|
||||||
|
{ name: 'Docs', path: '/docs', icon: <InfoIcon /> },
|
||||||
|
// { name: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
|
||||||
|
// { name: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
|
||||||
|
// { name: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
|
||||||
|
// { name: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CandidateNavItems : NavigationLinkType[]= [
|
||||||
|
{ 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: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/qa-setup' },
|
||||||
|
{ name: 'Analytics', icon: <BarChartIcon />, path: '/analytics' },
|
||||||
|
{ name: 'Settings', icon: <SettingsIcon />, path: '/settings' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const EmployerNavItems: NavigationLinkType[] = [
|
||||||
|
{ name: 'Dashboard', icon: <DashboardIcon />, path: '/dashboard' },
|
||||||
|
{ name: 'Search', icon: <SearchIcon />, path: '/search' },
|
||||||
|
{ 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
|
||||||
|
const getNavigationLinks = (user: UserType | null) : NavigationLinkType[] => {
|
||||||
|
if (!user) {
|
||||||
|
return DefaultNavItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.type === 'candidate') {
|
||||||
|
return CandidateNavItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Employer navigation
|
||||||
|
return EmployerNavItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BackstoryPageContainerProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
|
||||||
|
const { children, sx } = props;
|
||||||
|
return (
|
||||||
|
<Container maxWidth="xl" sx={{ mt: 2, mb: 2, ...sx }}>
|
||||||
|
<Paper
|
||||||
|
elevation={2}
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
borderRadius: 2,
|
||||||
|
minHeight: '80vh',
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BackstoryLayout: React.FC<{
|
||||||
|
sessionId?: string;
|
||||||
|
setSnack: (msg: string) => void;
|
||||||
|
page: string;
|
||||||
|
chatRef: React.Ref<any>;
|
||||||
|
snackRef: React.Ref<any>;
|
||||||
|
submitQuery: any;
|
||||||
|
}> = ({ sessionId, setSnack, page, chatRef, snackRef, submitQuery }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const { user } = useUser();
|
||||||
|
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNavigationLinks(getNavigationLinks(user));
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
let dynamicRoutes;
|
||||||
|
if (sessionId) {
|
||||||
|
dynamicRoutes = getBackstoryDynamicRoutes({
|
||||||
|
sessionId,
|
||||||
|
setSnack,
|
||||||
|
submitQuery,
|
||||||
|
chatRef
|
||||||
|
}, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header setSnack={setSnack} sessionId={sessionId} user={user} currentPath={page} navigate={navigate} navigationLinks={navigationLinks} showLogin={false} />
|
||||||
|
<Box sx={{ display: "flex", minHeight: "72px", height: "72px" }} />
|
||||||
|
<Scrollable
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
backgroundColor: "background.default",
|
||||||
|
maxHeight: "calc(100vh - 72px)",
|
||||||
|
minHeight: "calc(100vh - 72px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BackstoryPageContainer>
|
||||||
|
<Outlet />
|
||||||
|
{dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>}
|
||||||
|
{location.pathname === "/" && <Footer />}
|
||||||
|
</BackstoryPageContainer>
|
||||||
|
</Scrollable>
|
||||||
|
<Snack ref={snackRef} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
NavigationLinkType
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
BackstoryLayout
|
||||||
|
};
|
14
frontend/src/NewApp/Components/Conversation.css
Normal file
14
frontend/src/NewApp/Components/Conversation.css
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.Conversation {
|
||||||
|
display: flex;
|
||||||
|
background-color: #F5F5F5;
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: calc(100vh - 72px);
|
||||||
|
}
|
||||||
|
|
573
frontend/src/NewApp/Components/Conversation.tsx
Normal file
573
frontend/src/NewApp/Components/Conversation.tsx
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
import React, { useState, useImperativeHandle, forwardRef, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
import { SxProps, Theme } from '@mui/material';
|
||||||
|
import PropagateLoader from "react-spinners/PropagateLoader";
|
||||||
|
|
||||||
|
import { Message, MessageList, BackstoryMessage } from '../../Components/Message';
|
||||||
|
import { DeleteConfirmation } from '../../Components/DeleteConfirmation';
|
||||||
|
import { Query } from '../../Components/ChatQuery';
|
||||||
|
import { BackstoryTextField, BackstoryTextFieldRef } from '../../Components/BackstoryTextField';
|
||||||
|
import { BackstoryElementProps } from '../../Components/BackstoryTab';
|
||||||
|
import { connectionBase } from '../../Global';
|
||||||
|
|
||||||
|
import './Conversation.css';
|
||||||
|
|
||||||
|
const loadingMessage: BackstoryMessage = { "role": "status", "content": "Establishing connection with server..." };
|
||||||
|
|
||||||
|
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check';
|
||||||
|
|
||||||
|
interface ConversationHandle {
|
||||||
|
submitQuery: (query: Query) => void;
|
||||||
|
fetchHistory: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConversationProps extends BackstoryElementProps {
|
||||||
|
className?: string, // Override default className
|
||||||
|
type: ConversationMode, // Type of Conversation chat
|
||||||
|
placeholder?: string, // Prompt to display in TextField input
|
||||||
|
actionLabel?: string, // Label to put on the primary button
|
||||||
|
resetAction?: () => void, // Callback when Reset is pressed
|
||||||
|
resetLabel?: string, // Label to put on Reset button
|
||||||
|
defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField
|
||||||
|
defaultQuery?: string, // Default text to populate the TextField input
|
||||||
|
preamble?: MessageList, // Messages to display at start of Conversation until Action has been invoked
|
||||||
|
hidePreamble?: boolean, // Whether to hide the preamble after an Action has been invoked
|
||||||
|
hideDefaultPrompts?: boolean, // Whether to hide the defaultPrompts after an Action has been invoked
|
||||||
|
messageFilter?: ((messages: MessageList) => MessageList) | undefined, // Filter callback to determine which Messages to display in Conversation
|
||||||
|
messages?: MessageList, //
|
||||||
|
sx?: SxProps<Theme>,
|
||||||
|
onResponse?: ((message: BackstoryMessage) => void) | undefined, // Event called when a query completes (provides messages)
|
||||||
|
};
|
||||||
|
|
||||||
|
const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: ConversationProps, ref) => {
|
||||||
|
const {
|
||||||
|
sessionId,
|
||||||
|
actionLabel,
|
||||||
|
className,
|
||||||
|
defaultPrompts,
|
||||||
|
defaultQuery,
|
||||||
|
hideDefaultPrompts,
|
||||||
|
hidePreamble,
|
||||||
|
messageFilter,
|
||||||
|
messages,
|
||||||
|
onResponse,
|
||||||
|
placeholder,
|
||||||
|
preamble,
|
||||||
|
resetAction,
|
||||||
|
resetLabel,
|
||||||
|
setSnack,
|
||||||
|
submitQuery,
|
||||||
|
sx,
|
||||||
|
type,
|
||||||
|
} = props;
|
||||||
|
const [contextUsedPercentage, setContextUsedPercentage] = useState<number>(0);
|
||||||
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
|
const [countdown, setCountdown] = useState<number>(0);
|
||||||
|
const [conversation, setConversation] = useState<MessageList>([]);
|
||||||
|
const [filteredConversation, setFilteredConversation] = useState<MessageList>([]);
|
||||||
|
const [processingMessage, setProcessingMessage] = useState<BackstoryMessage | undefined>(undefined);
|
||||||
|
const [streamingMessage, setStreamingMessage] = useState<BackstoryMessage | undefined>(undefined);
|
||||||
|
const timerRef = useRef<any>(null);
|
||||||
|
const [contextWarningShown, setContextWarningShown] = useState<boolean>(false);
|
||||||
|
const [noInteractions, setNoInteractions] = useState<boolean>(true);
|
||||||
|
const conversationRef = useRef<MessageList>([]);
|
||||||
|
const viewableElementRef = useRef<HTMLDivElement>(null);
|
||||||
|
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
||||||
|
const stopRef = useRef(false);
|
||||||
|
|
||||||
|
// Keep the ref updated whenever items changes
|
||||||
|
useEffect(() => {
|
||||||
|
conversationRef.current = conversation;
|
||||||
|
}, [conversation]);
|
||||||
|
|
||||||
|
// Update the context status
|
||||||
|
/* Transform the 'Conversation' by filtering via callback, then adding
|
||||||
|
* preamble and messages based on whether the conversation
|
||||||
|
* has any elements yet */
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = [];
|
||||||
|
if (messageFilter === undefined) {
|
||||||
|
filtered = conversation;
|
||||||
|
// console.log('No message filter provided. Using all messages.', filtered);
|
||||||
|
} else {
|
||||||
|
//console.log('Filtering conversation...')
|
||||||
|
filtered = messageFilter(conversation); /* Do not copy conversation or useEffect will loop forever */
|
||||||
|
//console.log(`${conversation.length - filtered.length} messages filtered out.`);
|
||||||
|
}
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
setFilteredConversation([
|
||||||
|
...(preamble || []),
|
||||||
|
...(messages || []),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setFilteredConversation([
|
||||||
|
...(hidePreamble ? [] : (preamble || [])),
|
||||||
|
...(messages || []),
|
||||||
|
...filtered,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]);
|
||||||
|
|
||||||
|
const fetchHistory = useCallback(async () => {
|
||||||
|
let retries = 5;
|
||||||
|
while (--retries > 0) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(connectionBase + `/api/history/${sessionId}/${type}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messages } = await response.json();
|
||||||
|
|
||||||
|
if (messages === undefined || messages.length === 0) {
|
||||||
|
console.log(`History returned for ${type} from server with 0 entries`)
|
||||||
|
setConversation([])
|
||||||
|
setNoInteractions(true);
|
||||||
|
} else {
|
||||||
|
console.log(`History returned for ${type} from server with ${messages.length} entries:`, messages)
|
||||||
|
|
||||||
|
const backstoryMessages: BackstoryMessage[] = messages;
|
||||||
|
|
||||||
|
setConversation(backstoryMessages.flatMap((backstoryMessage: BackstoryMessage) => {
|
||||||
|
if (backstoryMessage.status === "partial") {
|
||||||
|
return [{
|
||||||
|
...backstoryMessage,
|
||||||
|
role: "assistant",
|
||||||
|
content: backstoryMessage.response || "",
|
||||||
|
expanded: false,
|
||||||
|
expandable: true,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
return [{
|
||||||
|
role: 'user',
|
||||||
|
content: backstoryMessage.prompt || "",
|
||||||
|
}, {
|
||||||
|
...backstoryMessage,
|
||||||
|
role: ['done'].includes(backstoryMessage.status || "") ? "assistant" : backstoryMessage.status,
|
||||||
|
content: backstoryMessage.response || "",
|
||||||
|
}] as MessageList;
|
||||||
|
}));
|
||||||
|
setNoInteractions(false);
|
||||||
|
}
|
||||||
|
setProcessingMessage(undefined);
|
||||||
|
setStreamingMessage(undefined);
|
||||||
|
return;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating session ID:', error);
|
||||||
|
setProcessingMessage({ role: "error", content: `Unable to obtain history from server. Retrying in 3 seconds (${retries} remain.)` });
|
||||||
|
setTimeout(() => {
|
||||||
|
setProcessingMessage(undefined);
|
||||||
|
}, 3000);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
setSnack("Unable to obtain chat history.", "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [setConversation,setSnack, type, sessionId]);
|
||||||
|
|
||||||
|
// Set the initial chat history to "loading" or the welcome message if loaded.
|
||||||
|
useEffect(() => {
|
||||||
|
if (sessionId === undefined) {
|
||||||
|
setProcessingMessage(loadingMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchHistory();
|
||||||
|
}, [fetchHistory, sessionId, setProcessing]);
|
||||||
|
|
||||||
|
const startCountdown = (seconds: number) => {
|
||||||
|
if (timerRef.current) clearInterval(timerRef.current);
|
||||||
|
setCountdown(seconds);
|
||||||
|
timerRef.current = setInterval(() => {
|
||||||
|
setCountdown((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopCountdown = () => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
setCountdown(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnter = (value: string) => {
|
||||||
|
const query: Query = {
|
||||||
|
prompt: value
|
||||||
|
}
|
||||||
|
sendQuery(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
submitQuery: (query: Query) => {
|
||||||
|
sendQuery(query);
|
||||||
|
},
|
||||||
|
fetchHistory: () => { return fetchHistory(); }
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
const reset = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(connectionBase + `/api/reset/${sessionId}/${type}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ reset: ['history'] })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error('Response body is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
setProcessingMessage(undefined);
|
||||||
|
setStreamingMessage(undefined);
|
||||||
|
setConversation([]);
|
||||||
|
setNoInteractions(true);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
setSnack("Error resetting history", "error")
|
||||||
|
console.error('Error resetting history:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelQuery = () => {
|
||||||
|
console.log("Stop query");
|
||||||
|
stopRef.current = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendQuery = async (query: Query) => {
|
||||||
|
query.prompt = query.prompt.trim();
|
||||||
|
|
||||||
|
// If the request was empty, a default request was provided,
|
||||||
|
// and there is no prompt for the user, send the default request.
|
||||||
|
if (!query.prompt && defaultQuery && !prompt) {
|
||||||
|
query.prompt = defaultQuery.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not send an empty request.
|
||||||
|
if (!query.prompt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopRef.current = false;
|
||||||
|
|
||||||
|
setNoInteractions(false);
|
||||||
|
|
||||||
|
setConversation([
|
||||||
|
...conversationRef.current,
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
origin: type,
|
||||||
|
content: query.prompt,
|
||||||
|
disableCopy: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add a small delay to ensure React has time to update the UI
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
try {
|
||||||
|
setProcessing(true);
|
||||||
|
|
||||||
|
// Add initial processing message
|
||||||
|
setProcessingMessage(
|
||||||
|
{ role: 'status', content: 'Submitting request...', disableCopy: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a small delay to ensure React has time to update the UI
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
let data: any = query;
|
||||||
|
if (type === "job_description") {
|
||||||
|
data = {
|
||||||
|
prompt: "",
|
||||||
|
agent_options: {
|
||||||
|
job_description: query.prompt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${connectionBase}/api/${type}/${sessionId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
setSnack(`Query sent.`, "info");
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error('Response body is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
let streaming_response = ""
|
||||||
|
// Set up stream processing with explicit chunking
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
const process_line = async (line: string) => {
|
||||||
|
let update = JSON.parse(line);
|
||||||
|
|
||||||
|
switch (update.status) {
|
||||||
|
case 'done':
|
||||||
|
case 'partial':
|
||||||
|
if (update.status === 'done') stopCountdown();
|
||||||
|
if (update.status === 'done') setStreamingMessage(undefined);
|
||||||
|
if (update.status === 'done') setProcessingMessage(undefined);
|
||||||
|
const backstoryMessage: BackstoryMessage = update;
|
||||||
|
setConversation([
|
||||||
|
...conversationRef.current, {
|
||||||
|
...backstoryMessage,
|
||||||
|
role: 'assistant',
|
||||||
|
origin: type,
|
||||||
|
prompt: ['done', 'partial'].includes(update.status) ? update.prompt : '',
|
||||||
|
content: backstoryMessage.response || "",
|
||||||
|
expanded: update.status === "done" ? true : false,
|
||||||
|
expandable: update.status === "done" ? false : true,
|
||||||
|
}] as MessageList);
|
||||||
|
// Add a small delay to ensure React has time to update the UI
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
const metadata = update.metadata;
|
||||||
|
|
||||||
|
if (onResponse) {
|
||||||
|
onResponse(update);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
// Show error
|
||||||
|
setConversation([
|
||||||
|
...conversationRef.current, {
|
||||||
|
...update,
|
||||||
|
role: 'error',
|
||||||
|
origin: type,
|
||||||
|
content: update.response || "",
|
||||||
|
}] as MessageList);
|
||||||
|
|
||||||
|
setProcessing(false);
|
||||||
|
stopCountdown();
|
||||||
|
|
||||||
|
// Add a small delay to ensure React has time to update the UI
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Force an immediate state update based on the message type
|
||||||
|
// Update processing message with immediate re-render
|
||||||
|
if (update.status === "streaming") {
|
||||||
|
streaming_response += update.chunk
|
||||||
|
setStreamingMessage({ role: update.status, content: streaming_response, disableCopy: true });
|
||||||
|
} else {
|
||||||
|
setProcessingMessage({ role: update.status, content: update.response, disableCopy: true });
|
||||||
|
/* Reset stream on non streaming message */
|
||||||
|
streaming_response = ""
|
||||||
|
}
|
||||||
|
startCountdown(Math.ceil(update.remaining_time));
|
||||||
|
// Add a small delay to ensure React has time to update the UI
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!stopRef.current) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// Process each complete line immediately
|
||||||
|
buffer += chunk;
|
||||||
|
let lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
try {
|
||||||
|
await process_line(line);
|
||||||
|
} catch (e) {
|
||||||
|
setSnack("Error processing query", "error")
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process any remaining buffer content
|
||||||
|
if (buffer.trim()) {
|
||||||
|
try {
|
||||||
|
await process_line(buffer);
|
||||||
|
} catch (e) {
|
||||||
|
setSnack("Error processing query", "error")
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopRef.current) {
|
||||||
|
await reader.cancel();
|
||||||
|
setProcessingMessage(undefined);
|
||||||
|
setStreamingMessage(undefined);
|
||||||
|
setSnack("Processing cancelled", "warning");
|
||||||
|
}
|
||||||
|
stopCountdown();
|
||||||
|
setProcessing(false);
|
||||||
|
stopRef.current = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch error:', error);
|
||||||
|
setSnack("Unable to process query", "error");
|
||||||
|
setProcessingMessage({ role: 'error', content: "Unable to process query", disableCopy: true });
|
||||||
|
setTimeout(() => {
|
||||||
|
setProcessingMessage(undefined);
|
||||||
|
}, 5000);
|
||||||
|
stopRef.current = false;
|
||||||
|
setProcessing(false);
|
||||||
|
stopCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <Scrollable
|
||||||
|
// className={`${className || ""} Conversation`}
|
||||||
|
// autoscroll
|
||||||
|
// textFieldRef={viewableElementRef}
|
||||||
|
// fallbackThreshold={0.5}
|
||||||
|
// sx={{
|
||||||
|
// p: 1,
|
||||||
|
// mt: 0,
|
||||||
|
// ...sx
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
<Box sx={{ p: 1, mt: 0, overflow: "hidden", ...sx }}>
|
||||||
|
{
|
||||||
|
filteredConversation.map((message, index) =>
|
||||||
|
<Message key={index} expanded={message.expanded === undefined ? true : message.expanded} {...{ sendQuery, message, connectionBase, sessionId, setSnack, submitQuery }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
processingMessage !== undefined &&
|
||||||
|
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: processingMessage, submitQuery }} />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
streamingMessage !== undefined &&
|
||||||
|
<Message {...{ sendQuery, connectionBase, sessionId, setSnack, message: streamingMessage, submitQuery }} />
|
||||||
|
}
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
m: 1,
|
||||||
|
}}>
|
||||||
|
<PropagateLoader
|
||||||
|
size="10px"
|
||||||
|
loading={processing}
|
||||||
|
aria-label="Loading Spinner"
|
||||||
|
data-testid="loader"
|
||||||
|
/>
|
||||||
|
{processing === true && countdown > 0 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
pt: 1,
|
||||||
|
fontSize: "0.7rem",
|
||||||
|
color: "darkgrey"
|
||||||
|
}}
|
||||||
|
>Response will be stopped in: {countdown}s</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box className="Query" sx={{ display: "flex", flexDirection: "column", p: 1, flexGrow: 1 }}>
|
||||||
|
{placeholder &&
|
||||||
|
<Box sx={{ display: "flex", flexGrow: 1, p: 0, m: 0, flexDirection: "column" }}
|
||||||
|
ref={viewableElementRef}>
|
||||||
|
<BackstoryTextField
|
||||||
|
ref={backstoryTextRef}
|
||||||
|
disabled={processing}
|
||||||
|
onEnter={handleEnter}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
|
||||||
|
<DeleteConfirmation
|
||||||
|
label={resetLabel || "all data"}
|
||||||
|
disabled={sessionId === undefined || processingMessage !== undefined || noInteractions}
|
||||||
|
onDelete={() => { reset(); resetAction && resetAction(); }} />
|
||||||
|
<Tooltip title={actionLabel || "Send"}>
|
||||||
|
<span style={{ display: "flex", flexGrow: 1 }}>
|
||||||
|
<Button
|
||||||
|
sx={{ m: 1, gap: 1, flexGrow: 1 }}
|
||||||
|
variant="contained"
|
||||||
|
disabled={sessionId === undefined || processingMessage !== undefined}
|
||||||
|
onClick={() => { sendQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
|
||||||
|
{actionLabel}<SendIcon />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Cancel">
|
||||||
|
<span style={{ display: "flex" }}> { /* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
|
||||||
|
<IconButton
|
||||||
|
aria-label="cancel"
|
||||||
|
onClick={() => { cancelQuery(); }}
|
||||||
|
sx={{ display: "flex", margin: 'auto 0px' }}
|
||||||
|
size="large"
|
||||||
|
edge="start"
|
||||||
|
disabled={stopRef.current || sessionId === undefined || processing === false}
|
||||||
|
>
|
||||||
|
<CancelIcon />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length &&
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||||
|
{
|
||||||
|
defaultPrompts.map((element, index) => {
|
||||||
|
return (<Box key={index}>{element}</Box>);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
<Box sx={{ display: "flex", flexGrow: 1 }}></Box>
|
||||||
|
</Box >
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ConversationProps,
|
||||||
|
ConversationHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
Conversation
|
||||||
|
};
|
@ -3,6 +3,7 @@ import { NavigateFunction, useLocation } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
IconButton,
|
IconButton,
|
||||||
@ -28,24 +29,14 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
ExpandMore,
|
ExpandMore,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
|
|
||||||
import { NavigationLinkType } from '../BackstoryApp';
|
import { NavigationLinkType } from './BackstoryLayout';
|
||||||
import { Beta } from './Beta';
|
import { Beta } from './Beta';
|
||||||
import './Header.css';
|
import './Header.css';
|
||||||
|
import { UserType } from './UserContext';
|
||||||
// Interface for component props
|
import { SetSnackType } from '../../Components/Snack';
|
||||||
interface HeaderProps {
|
import { CopyBubble } from '../../Components/CopyBubble';
|
||||||
isLoggedIn?: boolean;
|
|
||||||
userType?: 'candidate' | 'employer' | null;
|
|
||||||
userName?: string;
|
|
||||||
transparent?: boolean;
|
|
||||||
onLogout?: () => void;
|
|
||||||
className?: string;
|
|
||||||
navigate: NavigateFunction;
|
|
||||||
navigationLinks: NavigationLinkType[];
|
|
||||||
showLogin?: boolean;
|
|
||||||
currentPath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Styled components
|
// Styled components
|
||||||
const StyledAppBar = styled(AppBar, {
|
const StyledAppBar = styled(AppBar, {
|
||||||
@ -91,17 +82,32 @@ const MobileDrawer = styled(Drawer)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = ({
|
interface HeaderProps {
|
||||||
isLoggedIn = false,
|
user?: UserType | null;
|
||||||
userName = '',
|
transparent?: boolean;
|
||||||
transparent = false,
|
onLogout?: () => void;
|
||||||
onLogout,
|
className?: string;
|
||||||
className,
|
navigate: NavigateFunction;
|
||||||
navigate,
|
navigationLinks: NavigationLinkType[];
|
||||||
navigationLinks,
|
showLogin?: boolean;
|
||||||
showLogin,
|
currentPath: string;
|
||||||
currentPath,
|
sessionId?: string | null;
|
||||||
}) => {
|
setSnack: SetSnackType,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||||
|
const {
|
||||||
|
user,
|
||||||
|
transparent = false,
|
||||||
|
className,
|
||||||
|
navigate,
|
||||||
|
navigationLinks,
|
||||||
|
showLogin,
|
||||||
|
currentPath,
|
||||||
|
sessionId,
|
||||||
|
onLogout,
|
||||||
|
setSnack,
|
||||||
|
} = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@ -147,16 +153,12 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parts = location.pathname.split('/');
|
const parts = location.pathname.split('/');
|
||||||
console.log(location.pathname);
|
|
||||||
let tab = '/';
|
let tab = '/';
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
tab = `/${parts[1]}`;
|
tab = `/${parts[1]}`;
|
||||||
}
|
}
|
||||||
if (tab !== currentTab) {
|
if (tab !== currentTab) {
|
||||||
console.log(`Setting tab to ${tab}`);
|
|
||||||
setCurrentTab(tab);
|
setCurrentTab(tab);
|
||||||
} else {
|
|
||||||
console.log(`Not setting tab to ${tab}`);
|
|
||||||
}
|
}
|
||||||
}, [location, currentTab]);
|
}, [location, currentTab]);
|
||||||
|
|
||||||
@ -228,7 +230,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Divider />
|
<Divider />
|
||||||
{!isLoggedIn && (showLogin === undefined || showLogin !== false) && (
|
{(!user || !user.isAuthenticated) && (showLogin === undefined || showLogin !== false) && (
|
||||||
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@ -258,7 +260,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!user || !user.isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@ -297,10 +299,10 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
height: 32,
|
height: 32,
|
||||||
bgcolor: theme.palette.secondary.main,
|
bgcolor: theme.palette.secondary.main,
|
||||||
}}>
|
}}>
|
||||||
{userName.charAt(0).toUpperCase()}
|
{user?.name.charAt(0).toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
||||||
{userName}
|
{user?.name}
|
||||||
</Box>
|
</Box>
|
||||||
<ExpandMore fontSize="small" />
|
<ExpandMore fontSize="small" />
|
||||||
</UserButton>
|
</UserButton>
|
||||||
@ -391,6 +393,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
{renderUserSection()}
|
{renderUserSection()}
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
|
<Tooltip title="Open Menu">
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="open drawer"
|
aria-label="open drawer"
|
||||||
@ -400,6 +403,24 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{sessionId && <CopyBubble
|
||||||
|
tooltip="Copy link"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="copy link"
|
||||||
|
edge="end"
|
||||||
|
sx={{
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
opacity: 1,
|
||||||
|
bgcolor: 'inherit',
|
||||||
|
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
|
||||||
|
}}
|
||||||
|
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
|
||||||
|
onClick={() => setSnack("Link copied!")}
|
||||||
|
size="large"
|
||||||
|
/>}
|
||||||
</UserActionsContainer>
|
</UserActionsContainer>
|
||||||
|
|
||||||
{/* Mobile Navigation Drawer */}
|
{/* Mobile Navigation Drawer */}
|
||||||
|
39
frontend/src/NewApp/Components/UserContext.tsx
Normal file
39
frontend/src/NewApp/Components/UserContext.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { createContext, useContext, useState } from "react";
|
||||||
|
|
||||||
|
type UserType = {
|
||||||
|
type: 'candidate' | 'employer' | 'guest';
|
||||||
|
name: string;
|
||||||
|
avatar?: any;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
logout: () => void;
|
||||||
|
};
|
||||||
|
type UserContextType = {
|
||||||
|
user: UserType | null;
|
||||||
|
setUser: (user: UserType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
const useUser = () => {
|
||||||
|
const ctx = useContext(UserContext);
|
||||||
|
if (!ctx) throw new Error("useUser must be used within a UserProvider");
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [user, setUser] = useState<UserType | null>(null);
|
||||||
|
return (
|
||||||
|
<UserContext.Provider value={{ user, setUser }}>
|
||||||
|
{children}
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
UserType
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
UserProvider,
|
||||||
|
useUser
|
||||||
|
};
|
417
frontend/src/NewApp/Pages/DocsPage.tsx
Normal file
417
frontend/src/NewApp/Pages/DocsPage.tsx
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate, useLocation, useParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Drawer,
|
||||||
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
IconButton,
|
||||||
|
Typography,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardActionArea,
|
||||||
|
Divider,
|
||||||
|
useTheme,
|
||||||
|
useMediaQuery
|
||||||
|
} from '@mui/material';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import DescriptionIcon from '@mui/icons-material/Description';
|
||||||
|
import CodeIcon from '@mui/icons-material/Code';
|
||||||
|
import LayersIcon from '@mui/icons-material/Layers';
|
||||||
|
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||||
|
import PaletteIcon from '@mui/icons-material/Palette';
|
||||||
|
import AnalyticsIcon from '@mui/icons-material/Analytics';
|
||||||
|
import ViewQuiltIcon from '@mui/icons-material/ViewQuilt';
|
||||||
|
|
||||||
|
import { Document } from '../Components/Document';
|
||||||
|
import { BackstoryPageProps } from '../../Components/BackstoryTab';
|
||||||
|
import { BackstoryUIOverviewPage } from './BackstoryUIOverviewPage';
|
||||||
|
import { BackstoryAppAnalysisPage } from './BackstoryAppAnalysisPage';
|
||||||
|
import { BackstoryThemeVisualizerPage } from './BackstoryThemeVisualizerPage';
|
||||||
|
import { MockupPage } from './MockupPage';
|
||||||
|
|
||||||
|
// Get appropriate icon for document type
|
||||||
|
const getDocumentIcon = (title: string) => {
|
||||||
|
switch (title) {
|
||||||
|
case 'Docs':
|
||||||
|
return <DescriptionIcon />;
|
||||||
|
case 'BETA':
|
||||||
|
return <CodeIcon />;
|
||||||
|
case 'Resume Generation Architecture':
|
||||||
|
case 'Application Architecture':
|
||||||
|
return <LayersIcon />;
|
||||||
|
case 'UI Overview':
|
||||||
|
case 'UI Mockup':
|
||||||
|
return <DashboardIcon />;
|
||||||
|
case 'Theme Visualizer':
|
||||||
|
return <PaletteIcon />;
|
||||||
|
case 'App Analysis':
|
||||||
|
return <AnalyticsIcon />;
|
||||||
|
default:
|
||||||
|
return <ViewQuiltIcon />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sidebar navigation component using MUI components
|
||||||
|
const Sidebar: React.FC<{
|
||||||
|
currentPage: string;
|
||||||
|
onDocumentSelect: (docName: string, open: boolean) => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
isMobile: boolean;
|
||||||
|
}> = ({ currentPage, onDocumentSelect, onClose, isMobile }) => {
|
||||||
|
// Document definitions
|
||||||
|
|
||||||
|
const handleItemClick = (route: string) => {
|
||||||
|
onDocumentSelect(route, true);
|
||||||
|
if (isMobile && onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box sx={{
|
||||||
|
p: 2,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider'
|
||||||
|
}}>
|
||||||
|
<Typography variant="h6" component="h2" fontWeight="bold">
|
||||||
|
Documentation
|
||||||
|
</Typography>
|
||||||
|
{isMobile && onClose && (
|
||||||
|
<IconButton
|
||||||
|
onClick={onClose}
|
||||||
|
size="small"
|
||||||
|
aria-label="Close navigation"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
p: 1
|
||||||
|
}}>
|
||||||
|
<List>
|
||||||
|
{documents.map((doc, index) => (
|
||||||
|
<ListItem key={index} disablePadding>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={() => handleItemClick(doc.route)}
|
||||||
|
selected={currentPage === doc.route}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 1,
|
||||||
|
mb: 0.5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{
|
||||||
|
color: currentPage === doc.route ? 'primary.main' : 'text.secondary',
|
||||||
|
minWidth: 40
|
||||||
|
}}>
|
||||||
|
{getDocumentIcon(doc.title)}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={doc.title}
|
||||||
|
slotProps={{
|
||||||
|
primary: {
|
||||||
|
fontWeight: currentPage === doc.route ? 'medium' : 'regular',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DocType = {
|
||||||
|
title: string;
|
||||||
|
route: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const documents : DocType[] = [
|
||||||
|
{ title: "About", route: "about", description: "General information about the application and its purpose" },
|
||||||
|
{ title: "BETA", route: "beta", description: "Details about the current beta version and upcoming features" },
|
||||||
|
{ title: "Resume Generation Architecture", route: "resume-generation", description: "Technical overview of how resumes are processed and generated" },
|
||||||
|
{ title: "Application Architecture", route: "about-app", description: "System design and technical stack information" },
|
||||||
|
{ title: "UI Overview", route: "ui-overview", description: "Guide to the user interface components and interactions" },
|
||||||
|
{ title: "Theme Visualizer", route: "theme-visualizer", description: "Explore and customize application themes and visual styles" },
|
||||||
|
{ title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application" },
|
||||||
|
{ title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts" },
|
||||||
|
{ title: 'Text Mockups', route: "backstory-ui-mockups", description: "Early text mockups of many of the interaction points." },
|
||||||
|
];
|
||||||
|
|
||||||
|
const documentFromRoute = (route: string) : DocType | null => {
|
||||||
|
const index = documents.findIndex(v => v.route === route);
|
||||||
|
if (index === -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return documents[index];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get document title from route
|
||||||
|
const documentTitleFromRoute = (route: string): string => {
|
||||||
|
const doc = documentFromRoute(route);
|
||||||
|
if (doc === null) {
|
||||||
|
return 'Documentation'
|
||||||
|
}
|
||||||
|
return doc.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocsPage = (props: BackstoryPageProps) => {
|
||||||
|
const { sessionId, submitQuery, setSnack } = props;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const { paramPage = '' } = useParams();
|
||||||
|
const [page, setPage] = useState<string>(paramPage);
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
|
// Track location changes
|
||||||
|
useEffect(() => {
|
||||||
|
const parts = location.pathname.split('/');
|
||||||
|
if (parts.length > 2) {
|
||||||
|
setPage(parts[2]);
|
||||||
|
} else {
|
||||||
|
setPage('');
|
||||||
|
}
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
// Close drawer when changing to desktop view
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile) {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
}
|
||||||
|
}, [isMobile]);
|
||||||
|
|
||||||
|
// Handle document navigation
|
||||||
|
const onDocumentExpand = (docName: string, open: boolean) => {
|
||||||
|
console.log("Document expanded:", { docName, open, location });
|
||||||
|
if (open) {
|
||||||
|
const parts = location.pathname.split('/');
|
||||||
|
if (parts.length > 2) {
|
||||||
|
const basePath = parts.slice(0, -1).join('/');
|
||||||
|
navigate(`${basePath}/${docName}`);
|
||||||
|
} else {
|
||||||
|
navigate(docName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const basePath = location.pathname.split('/').slice(0, -1).join('/');
|
||||||
|
navigate(`${basePath}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle mobile drawer
|
||||||
|
const toggleDrawer = () => {
|
||||||
|
setDrawerOpen(!drawerOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close the drawer
|
||||||
|
const closeDrawer = () => {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
interface DocViewProps {
|
||||||
|
page: string
|
||||||
|
};
|
||||||
|
const DocView = (props: DocViewProps) => {
|
||||||
|
const { page } = props;
|
||||||
|
const title = documentTitleFromRoute(page);
|
||||||
|
const icon = getDocumentIcon(title);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Box sx={{ color: 'inherit', fontSize: "1.75rem", fontWeight: "bold", display: "flex", flexDirection: "row", gap: 1, alignItems: "center", mr: 1.5 }}>
|
||||||
|
{icon}
|
||||||
|
{title}
|
||||||
|
</Box>
|
||||||
|
<Document
|
||||||
|
filepath={`/docs/${page}.md`}
|
||||||
|
sessionId={sessionId}
|
||||||
|
submitQuery={submitQuery}
|
||||||
|
setSnack={setSnack}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render the appropriate content based on current page
|
||||||
|
function renderContent() {
|
||||||
|
switch (page) {
|
||||||
|
case 'ui-overview':
|
||||||
|
return (<BackstoryUIOverviewPage />);
|
||||||
|
case 'theme-visualizer':
|
||||||
|
return (<Paper sx={{ m: 0, p: 1 }}><BackstoryThemeVisualizerPage /></Paper>);
|
||||||
|
case 'app-analysis':
|
||||||
|
return (<BackstoryAppAnalysisPage />);
|
||||||
|
case 'ui-mockup':
|
||||||
|
return (<MockupPage />);
|
||||||
|
default:
|
||||||
|
if (documentFromRoute(page)) {
|
||||||
|
return <DocView page={page}/>
|
||||||
|
}
|
||||||
|
// Document grid for landing page
|
||||||
|
return (
|
||||||
|
<Paper sx={{ p: 3 }} elevation={1}>
|
||||||
|
<Typography variant="h4" component="h1" gutterBottom>
|
||||||
|
Documentation
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" paragraph>
|
||||||
|
Select a document from the sidebar to view detailed technical information about the application.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{documents.map((doc, index) => (
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
|
||||||
|
<Card>
|
||||||
|
<CardActionArea onClick={() => onDocumentExpand(doc.route, true)}>
|
||||||
|
<CardContent>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||||
|
<Box sx={{ color: 'primary.main', mr: 1.5 }}>
|
||||||
|
{getDocumentIcon(doc.title)}
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h6">{doc.title}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ ml: 5 }}>
|
||||||
|
{doc.description}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate drawer width
|
||||||
|
const drawerWidth = 240;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', height: '100%' }}>
|
||||||
|
{/* Mobile App Bar */}
|
||||||
|
{isMobile && (
|
||||||
|
<AppBar
|
||||||
|
position="fixed"
|
||||||
|
sx={{
|
||||||
|
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||||
|
ml: { sm: `${drawerWidth}px` },
|
||||||
|
display: { md: 'none' }
|
||||||
|
}}
|
||||||
|
elevation={0}
|
||||||
|
color="default"
|
||||||
|
>
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton
|
||||||
|
aria-label="open drawer"
|
||||||
|
edge="start"
|
||||||
|
onClick={toggleDrawer}
|
||||||
|
sx={{ mr: 2 }}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6" noWrap component="div" sx={{ color: "white" }}>
|
||||||
|
{page ? documentTitleFromRoute(page) : "Documentation"}
|
||||||
|
</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation drawer */}
|
||||||
|
<Box
|
||||||
|
component="nav"
|
||||||
|
sx={{
|
||||||
|
width: { md: drawerWidth },
|
||||||
|
flexShrink: { md: 0 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Mobile drawer (temporary) */}
|
||||||
|
{isMobile ? (
|
||||||
|
<Drawer
|
||||||
|
variant="temporary"
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true, // Better open performance on mobile
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'block', md: 'none' },
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: drawerWidth
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
currentPage={page}
|
||||||
|
onDocumentSelect={onDocumentExpand}
|
||||||
|
onClose={closeDrawer}
|
||||||
|
isMobile={true}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
) : (
|
||||||
|
// Desktop drawer (permanent)
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
display: { xs: 'none', md: 'block' },
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: drawerWidth,
|
||||||
|
position: 'relative',
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
open
|
||||||
|
>
|
||||||
|
<Sidebar
|
||||||
|
currentPage={page}
|
||||||
|
onDocumentSelect={onDocumentExpand}
|
||||||
|
isMobile={false}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<Box
|
||||||
|
component="main"
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
p: 3,
|
||||||
|
width: { md: `calc(100% - ${drawerWidth}px)` },
|
||||||
|
pt: isMobile ? { xs: 8, sm: 9 } : 3, // Add padding top on mobile to account for AppBar
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DocsPage };
|
38
frontend/src/NewApp/routes/UserRoute.tsx
Normal file
38
frontend/src/NewApp/routes/UserRoute.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useUser, UserType } from "../Components/UserContext";
|
||||||
|
import { Typography, Box } from "@mui/material";
|
||||||
|
|
||||||
|
const mockFetchUser = async (username: string) => {
|
||||||
|
return new Promise<UserType>((resolve) => {
|
||||||
|
const user : UserType = {
|
||||||
|
type: "candidate",
|
||||||
|
name: username,
|
||||||
|
isAuthenticated: true,
|
||||||
|
logout: () => {},
|
||||||
|
};
|
||||||
|
setTimeout(() => resolve(user), 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserRoute: React.FC = () => {
|
||||||
|
const { username } = useParams<{ username: string }>();
|
||||||
|
const { user, setUser } = useUser();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (username) {
|
||||||
|
mockFetchUser(username).then(setUser);
|
||||||
|
}
|
||||||
|
}, [username, setUser]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box m={2}>
|
||||||
|
<Typography variant="h5">User Page</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{user ? `Hello, ${user.name}` : "Loading..."}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserRoute };
|
Loading…
x
Reference in New Issue
Block a user