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 ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
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 { SxProps, Theme } from '@mui/material';
|
||||
|
||||
interface CopyBubbleProps {
|
||||
interface CopyBubbleProps extends IconButtonProps {
|
||||
content: string | undefined,
|
||||
sx?: SxProps<Theme>;
|
||||
tooltip?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const CopyBubble = ({
|
||||
content,
|
||||
sx,
|
||||
tooltip = "Copy to clipboard",
|
||||
onClick,
|
||||
...rest
|
||||
} : CopyBubbleProps) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@ -25,10 +30,14 @@ const CopyBubble = ({
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds
|
||||
});
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title="Copy to clipboard" placement="top" arrow>
|
||||
<Tooltip title={tooltip} placement="top" arrow>
|
||||
<IconButton
|
||||
onClick={handleCopy}
|
||||
sx={{
|
||||
@ -41,6 +50,7 @@ return (
|
||||
}}
|
||||
size="small"
|
||||
color={copied ? "success" : "default"}
|
||||
{...rest}
|
||||
>
|
||||
{copied ? <CheckIcon sx={{ width: 16, height: 16 }} /> : <ContentCopyIcon sx={{ width: 16, height: 16 }} />}
|
||||
</IconButton>
|
||||
|
@ -1,39 +1,16 @@
|
||||
import React, { ReactElement, useEffect, useState, useRef, useCallback} from 'react';
|
||||
import { Box, Typography, Container, Paper } from '@mui/material';
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
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 { backstoryTheme } from '../BackstoryTheme';
|
||||
|
||||
import {Header} from './Components/Header';
|
||||
import { Scrollable } from '../Components/Scrollable';
|
||||
import { Footer } from './Components/Footer';
|
||||
import { Snack, SeverityType } from '../Components/Snack';
|
||||
import { SeverityType } from '../Components/Snack';
|
||||
import { Query } from '../Components/ChatQuery';
|
||||
import { ConversationHandle } from './Components/Conversation';
|
||||
|
||||
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 { UserProvider } from './Components/UserContext';
|
||||
import { BetaPage } from './Pages/BetaPage';
|
||||
import { CreateProfilePage } from './Pages/CreateProfilePage';
|
||||
import { VectorVisualizerPage } from 'Pages/VectorVisualizerPage';
|
||||
import { UserRoute } from './routes/UserRoute';
|
||||
import { BackstoryLayout } from './Components/BackstoryLayout';
|
||||
|
||||
import './BackstoryApp.css';
|
||||
import '@fontsource/roboto/300.css';
|
||||
@ -43,118 +20,6 @@ import '@fontsource/roboto/700.css';
|
||||
|
||||
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
|
||||
const getCookie = (name: string) => {
|
||||
const value = `; ${document.cookie}`;
|
||||
@ -171,9 +36,6 @@ const setCookie = (name: string, value: string, days = 7) => {
|
||||
const BackstoryApp = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const userContext : UserContext = useUserContext();
|
||||
const { user, isAuthenticated } = userContext;
|
||||
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
|
||||
const snackRef = useRef<any>(null);
|
||||
const chatRef = useRef<ConversationHandle>(null);
|
||||
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||
@ -199,15 +61,17 @@ const BackstoryApp = () => {
|
||||
try {
|
||||
let response;
|
||||
let newSessionId;
|
||||
let action = ""
|
||||
if (urlSessionId) {
|
||||
// Attempt to join session from URL
|
||||
response = await fetch(`${connectionBase}/join-session/${urlSessionId}`, {
|
||||
response = await fetch(`${connectionBase}/api/join-session/${urlSessionId}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
newSessionId = (await response.json()).id;
|
||||
action = "Joined";
|
||||
} else if (cookieSessionId) {
|
||||
// Attempt to join session from cookie
|
||||
response = await fetch(`${connectionBase}/api/join-session/${cookieSessionId}`, {
|
||||
@ -222,6 +86,9 @@ const BackstoryApp = () => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create session');
|
||||
}
|
||||
action = "Created new";
|
||||
} else {
|
||||
action = "Joined";
|
||||
}
|
||||
newSessionId = (await response.json()).id;
|
||||
} else {
|
||||
@ -233,6 +100,7 @@ const BackstoryApp = () => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create session');
|
||||
}
|
||||
action = "Created new";
|
||||
newSessionId = (await response.json()).id;
|
||||
}
|
||||
setSessionId(newSessionId);
|
||||
@ -241,9 +109,14 @@ const BackstoryApp = () => {
|
||||
setCookie('session_id', newSessionId);
|
||||
}
|
||||
// Update URL without reloading
|
||||
if (!storeInCookie || (urlSessionId && urlSessionId !== newSessionId)) {
|
||||
window.history.replaceState(null, '', `?id=${newSessionId}`);
|
||||
if (!storeInCookie) {
|
||||
// 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) {
|
||||
setSnack("" + err);
|
||||
}
|
||||
@ -251,89 +124,40 @@ const BackstoryApp = () => {
|
||||
fetchSession();
|
||||
}, [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(() => {
|
||||
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
|
||||
setPage(currentRoute);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
setNavigationLinks(getNavigationLinks(user.type, isAuthenticated));
|
||||
}, [user.type, isAuthenticated]);
|
||||
|
||||
// Render appropriate routes based on user type
|
||||
return (
|
||||
<ThemeProvider theme={backstoryTheme}>
|
||||
<Header 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 userContext={userContext}>
|
||||
{sessionId !== undefined &&
|
||||
<Routes>
|
||||
<Route path="/chat" element={<ChatPage ref={chatRef} setSnack={setSnack} sessionId={sessionId} submitQuery={submitQuery} />} />
|
||||
<Route path="/docs" element={<DocsPage setSnack={setSnack} sessionId={sessionId} 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="/dashboard" element={<DashboardPage />} />
|
||||
<Route path="/" element={<HomePage />} />
|
||||
{/* 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 />} />
|
||||
</>
|
||||
)}
|
||||
<UserProvider>
|
||||
<Routes>
|
||||
<Route path="/u/:user" element={<UserRoute />} />
|
||||
{/* Static/shared routes */}
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<BackstoryLayout
|
||||
sessionId={sessionId}
|
||||
setSnack={setSnack}
|
||||
page={page}
|
||||
chatRef={chatRef}
|
||||
snackRef={snackRef}
|
||||
submitQuery={submitQuery}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<BetaPage />} />
|
||||
</Routes>
|
||||
</UserProvider>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export type {
|
||||
UserContext,
|
||||
NavigationLinkType
|
||||
};
|
||||
|
||||
export {
|
||||
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 {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Button,
|
||||
IconButton,
|
||||
@ -28,24 +29,14 @@ import {
|
||||
Settings,
|
||||
ExpandMore,
|
||||
} from '@mui/icons-material';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
|
||||
import { NavigationLinkType } from '../BackstoryApp';
|
||||
import { NavigationLinkType } from './BackstoryLayout';
|
||||
import { Beta } from './Beta';
|
||||
import './Header.css';
|
||||
|
||||
// Interface for component props
|
||||
interface HeaderProps {
|
||||
isLoggedIn?: boolean;
|
||||
userType?: 'candidate' | 'employer' | null;
|
||||
userName?: string;
|
||||
transparent?: boolean;
|
||||
onLogout?: () => void;
|
||||
className?: string;
|
||||
navigate: NavigateFunction;
|
||||
navigationLinks: NavigationLinkType[];
|
||||
showLogin?: boolean;
|
||||
currentPath: string;
|
||||
}
|
||||
import { UserType } from './UserContext';
|
||||
import { SetSnackType } from '../../Components/Snack';
|
||||
import { CopyBubble } from '../../Components/CopyBubble';
|
||||
|
||||
// Styled components
|
||||
const StyledAppBar = styled(AppBar, {
|
||||
@ -91,17 +82,32 @@ const MobileDrawer = styled(Drawer)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({
|
||||
isLoggedIn = false,
|
||||
userName = '',
|
||||
transparent = false,
|
||||
onLogout,
|
||||
className,
|
||||
navigate,
|
||||
navigationLinks,
|
||||
showLogin,
|
||||
currentPath,
|
||||
}) => {
|
||||
interface HeaderProps {
|
||||
user?: UserType | null;
|
||||
transparent?: boolean;
|
||||
onLogout?: () => void;
|
||||
className?: string;
|
||||
navigate: NavigateFunction;
|
||||
navigationLinks: NavigationLinkType[];
|
||||
showLogin?: boolean;
|
||||
currentPath: string;
|
||||
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 location = useLocation();
|
||||
|
||||
@ -147,16 +153,12 @@ const Header: React.FC<HeaderProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
const parts = location.pathname.split('/');
|
||||
console.log(location.pathname);
|
||||
let tab = '/';
|
||||
if (parts.length > 1) {
|
||||
tab = `/${parts[1]}`;
|
||||
}
|
||||
if (tab !== currentTab) {
|
||||
console.log(`Setting tab to ${tab}`);
|
||||
setCurrentTab(tab);
|
||||
} else {
|
||||
console.log(`Not setting tab to ${tab}`);
|
||||
}
|
||||
}, [location, currentTab]);
|
||||
|
||||
@ -228,7 +230,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
))}
|
||||
</Tabs>
|
||||
<Divider />
|
||||
{!isLoggedIn && (showLogin === undefined || showLogin !== false) && (
|
||||
{(!user || !user.isAuthenticated) && (showLogin === undefined || showLogin !== false) && (
|
||||
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
@ -258,7 +260,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
if (!user || !user.isAuthenticated) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
@ -297,10 +299,10 @@ const Header: React.FC<HeaderProps> = ({
|
||||
height: 32,
|
||||
bgcolor: theme.palette.secondary.main,
|
||||
}}>
|
||||
{userName.charAt(0).toUpperCase()}
|
||||
{user?.name.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
||||
{userName}
|
||||
{user?.name}
|
||||
</Box>
|
||||
<ExpandMore fontSize="small" />
|
||||
</UserButton>
|
||||
@ -391,6 +393,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
{renderUserSection()}
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<Tooltip title="Open Menu">
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
@ -400,6 +403,24 @@ const Header: React.FC<HeaderProps> = ({
|
||||
>
|
||||
<MenuIcon />
|
||||
</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>
|
||||
|
||||
{/* 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