Prettier / eslint reformatting

This commit is contained in:
James Ketr 2025-06-18 14:34:52 -07:00
parent f9307070a3
commit 66b68270cd
81 changed files with 5003 additions and 6693 deletions

View File

@ -1,7 +1,6 @@
{ {
"env": { "env": {
"browser": true, "browser": true,
"es2021": true,
"jest": true "jest": true
}, },
"extends": [ "extends": [

View File

@ -51,8 +51,8 @@
"start": "WDS_SOCKET_HOST=backstory-beta.ketrenos.com WDS_SOCKET_PORT=443 craco start", "start": "WDS_SOCKET_HOST=backstory-beta.ketrenos.com WDS_SOCKET_PORT=443 craco start",
"build": "craco build", "build": "craco build",
"test": "craco test", "test": "craco test",
"lint": "eslint src/**/*.{ts,tsx}", "lint": "eslint src/**/*.{ts,tsx} --no-color",
"lint:fix": "eslint src/**/*.{ts,tsx} --fix", "lint:fix": "eslint src/**/*.{ts,tsx} --fix --no-color",
"format": "prettier --write src/**/*.{ts,tsx}" "format": "prettier --write src/**/*.{ts,tsx}"
}, },
"eslintConfig": { "eslintConfig": {

View File

@ -1,67 +1,53 @@
import React, { useEffect, useState, useRef, JSX } from "react"; import React, { useEffect, useState, useRef, JSX } from 'react';
import { Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from '@mui/material/styles';
import { backstoryTheme } from "./BackstoryTheme"; import { backstoryTheme } from './BackstoryTheme';
import { ConversationHandle } from "components/Conversation"; import { ConversationHandle } from 'components/Conversation';
import { CandidateRoute } from "routes/CandidateRoute"; import { CandidateRoute } from 'routes/CandidateRoute';
import { BackstoryLayout } from "components/layout/BackstoryLayout"; import { BackstoryLayout } from 'components/layout/BackstoryLayout';
import { ChatQuery } from "types/types"; import { ChatQuery } from 'types/types';
import { AuthProvider } from "hooks/AuthContext"; import { AuthProvider } from 'hooks/AuthContext';
import { AppStateProvider } from "hooks/GlobalContext"; import { AppStateProvider } from 'hooks/GlobalContext';
import "./BackstoryApp.css"; import './BackstoryApp.css';
import "@fontsource/roboto/300.css"; import '@fontsource/roboto/300.css';
import "@fontsource/roboto/400.css"; import '@fontsource/roboto/400.css';
import "@fontsource/roboto/500.css"; import '@fontsource/roboto/500.css';
import "@fontsource/roboto/700.css"; import '@fontsource/roboto/700.css';
const BackstoryApp = (): JSX.Element => { const BackstoryApp = (): JSX.Element => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const chatRef = useRef<ConversationHandle>(null); const chatRef = useRef<ConversationHandle>(null);
const submitQuery = (query: ChatQuery): void => { const submitQuery = (query: ChatQuery): void => {
console.log( console.log(`handleSubmitChatQuery:`, query, chatRef.current ? ' sending' : 'no handler');
`handleSubmitChatQuery:`, chatRef.current?.submitQuery(query);
query, navigate('/chat');
chatRef.current ? " sending" : "no handler" };
); const [page, setPage] = useState<string>('');
chatRef.current?.submitQuery(query);
navigate("/chat");
};
const [page, setPage] = useState<string>("");
useEffect(() => { useEffect(() => {
const currentRoute = location.pathname.split("/")[1] const currentRoute = location.pathname.split('/')[1]
? `/${location.pathname.split("/")[1]}` ? `/${location.pathname.split('/')[1]}`
: "/"; : '/';
setPage(currentRoute); setPage(currentRoute);
}, [location.pathname]); }, [location.pathname]);
// Render appropriate routes based on user type // Render appropriate routes based on user type
return ( return (
<ThemeProvider theme={backstoryTheme}> <ThemeProvider theme={backstoryTheme}>
<AuthProvider> <AuthProvider>
<AppStateProvider> <AppStateProvider>
<Routes> <Routes>
<Route <Route path="/u/:username" element={<CandidateRoute />} />
path="/u/:username" {/* Static/shared routes */}
element={<CandidateRoute />} <Route path="/*" element={<BackstoryLayout {...{ page, chatRef, submitQuery }} />} />
/> </Routes>
{/* Static/shared routes */} </AppStateProvider>
<Route </AuthProvider>
path="/*" </ThemeProvider>
element={ );
<BackstoryLayout
{...{ page, chatRef, submitQuery }}
/>
}
/>
</Routes>
</AppStateProvider>
</AuthProvider>
</ThemeProvider>
);
}; };
export { BackstoryApp }; export { BackstoryApp };

View File

@ -1,76 +1,76 @@
import { createTheme } from "@mui/material/styles"; import { createTheme } from '@mui/material/styles';
const backstoryTheme = createTheme({ const backstoryTheme = createTheme({
palette: { palette: {
primary: { primary: {
main: "#1A2536", // Midnight Blue main: '#1A2536', // Midnight Blue
contrastText: "#D3CDBF", // Warm Gray contrastText: '#D3CDBF', // Warm Gray
}, },
secondary: { secondary: {
main: "#4A7A7D", // Dusty Teal main: '#4A7A7D', // Dusty Teal
contrastText: "#FFFFFF", // White contrastText: '#FFFFFF', // White
}, },
text: { text: {
primary: "#2E2E2E", // Charcoal Black primary: '#2E2E2E', // Charcoal Black
secondary: "#1A2536", // Midnight Blue secondary: '#1A2536', // Midnight Blue
}, },
background: { background: {
default: "#D3CDBF", // Warm Gray default: '#D3CDBF', // Warm Gray
paper: "#FFFFFF", // White paper: '#FFFFFF', // White
}, },
action: { action: {
active: "#D4A017", // Golden Ochre active: '#D4A017', // Golden Ochre
hover: "rgba(212, 160, 23, 0.1)", // Golden Ochre with opacity hover: 'rgba(212, 160, 23, 0.1)', // Golden Ochre with opacity
}, },
custom: { custom: {
highlight: "#D4A017", // Golden Ochre highlight: '#D4A017', // Golden Ochre
contrast: "#2E2E2E", // Charcoal Black contrast: '#2E2E2E', // Charcoal Black
}, },
}, },
typography: { typography: {
fontFamily: "'Roboto', sans-serif", fontFamily: "'Roboto', sans-serif",
h1: { h1: {
fontSize: "2rem", fontSize: '2rem',
fontWeight: 500, fontWeight: 500,
color: "#2E2E2E", // Charcoal Black color: '#2E2E2E', // Charcoal Black
}, },
h2: { h2: {
fontSize: "1.75rem", fontSize: '1.75rem',
fontWeight: 500, fontWeight: 500,
color: "#2E2E2E", // Charcoal Black color: '#2E2E2E', // Charcoal Black
marginBottom: "1rem", marginBottom: '1rem',
}, },
h3: { h3: {
fontSize: "1.5rem", fontSize: '1.5rem',
fontWeight: 500, fontWeight: 500,
color: "#2E2E2E", // Charcoal Black color: '#2E2E2E', // Charcoal Black
marginBottom: "0.75rem", marginBottom: '0.75rem',
}, },
h4: { h4: {
fontSize: "1.25rem", fontSize: '1.25rem',
fontWeight: 500, fontWeight: 500,
color: "#2E2E2E", // Charcoal Black color: '#2E2E2E', // Charcoal Black
marginBottom: "0.5rem", marginBottom: '0.5rem',
}, },
body1: { body1: {
fontSize: "1rem", fontSize: '1rem',
color: "#2E2E2E", // Charcoal Black color: '#2E2E2E', // Charcoal Black
marginBottom: "0.5rem", marginBottom: '0.5rem',
}, },
body2: { body2: {
fontSize: "0.875rem", fontSize: '0.875rem',
color: "#2E2E2E", // Charcoal Black color: '#2E2E2E', // Charcoal Black
}, },
}, },
components: { components: {
MuiLink: { MuiLink: {
styleOverrides: { styleOverrides: {
root: { root: {
color: "#4A7A7D", // Dusty Teal (your secondary color) color: '#4A7A7D', // Dusty Teal (your secondary color)
textDecoration: "none", textDecoration: 'none',
"&:hover": { '&:hover': {
color: "#D4A017", // Golden Ochre on hover color: '#D4A017', // Golden Ochre on hover
textDecoration: "underline", textDecoration: 'underline',
}, },
}, },
}, },
@ -78,9 +78,9 @@ const backstoryTheme = createTheme({
MuiButton: { MuiButton: {
styleOverrides: { styleOverrides: {
root: { root: {
textTransform: "none", textTransform: 'none',
"&:hover": { '&:hover': {
backgroundColor: "rgba(212, 160, 23, 0.2)", // Golden Ochre hover backgroundColor: 'rgba(212, 160, 23, 0.2)', // Golden Ochre hover
}, },
}, },
}, },
@ -88,7 +88,7 @@ const backstoryTheme = createTheme({
MuiAppBar: { MuiAppBar: {
styleOverrides: { styleOverrides: {
root: { root: {
backgroundColor: "#1A2536", // Midnight Blue backgroundColor: '#1A2536', // Midnight Blue
}, },
}, },
}, },
@ -96,23 +96,23 @@ const backstoryTheme = createTheme({
styleOverrides: { styleOverrides: {
root: { root: {
// padding: '0.5rem', // padding: '0.5rem',
borderRadius: "4px", borderRadius: '4px',
}, },
}, },
}, },
MuiList: { MuiList: {
styleOverrides: { styleOverrides: {
root: { root: {
padding: "0.5rem", padding: '0.5rem',
}, },
}, },
}, },
MuiListItem: { MuiListItem: {
styleOverrides: { styleOverrides: {
root: { root: {
borderRadius: "4px", borderRadius: '4px',
"&:hover": { '&:hover': {
backgroundColor: "rgba(212, 160, 23, 0.1)", // Golden Ochre with opacity backgroundColor: 'rgba(212, 160, 23, 0.1)', // Golden Ochre with opacity
}, },
}, },
}, },

View File

@ -1,7 +1,7 @@
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import { CandidateQuestion } from "types/types"; import { CandidateQuestion } from 'types/types';
type ChatSubmitQueryInterface = (query: CandidateQuestion) => void; type ChatSubmitQueryInterface = (query: CandidateQuestion) => void;
@ -20,8 +20,8 @@ const BackstoryQuery = (props: BackstoryQueryInterface) => {
<Button <Button
variant="outlined" variant="outlined"
sx={{ sx={{
color: (theme) => theme.palette.custom.highlight, // Golden Ochre (#D4A017) color: theme => theme.palette.custom.highlight, // Golden Ochre (#D4A017)
borderColor: (theme) => theme.palette.custom.highlight, borderColor: theme => theme.palette.custom.highlight,
m: 1, m: 1,
}} }}
size="small" size="small"

View File

@ -1,8 +1,8 @@
import React, { ReactElement, JSXElementConstructor } from "react"; import React, { ReactElement, JSXElementConstructor } from 'react';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import { SxProps, Theme } from "@mui/material"; import { SxProps, Theme } from '@mui/material';
import { ChatSubmitQueryInterface } from "./BackstoryQuery"; import { ChatSubmitQueryInterface } from './BackstoryQuery';
import { SetSnackType } from "./Snack"; import { SetSnackType } from './Snack';
interface BackstoryElementProps { interface BackstoryElementProps {
// setSnack: SetSnackType, // setSnack: SetSnackType,
@ -24,11 +24,8 @@ interface BackstoryTabProps {
tabProps?: { tabProps?: {
label?: string; label?: string;
sx?: SxProps; sx?: SxProps;
icon?: icon?: string | ReactElement<unknown, string | JSXElementConstructor<any>> | undefined;
| string iconPosition?: 'bottom' | 'top' | 'start' | 'end' | undefined;
| ReactElement<unknown, string | JSXElementConstructor<any>>
| undefined;
iconPosition?: "bottom" | "top" | "start" | "end" | undefined;
}; };
} }
@ -37,8 +34,8 @@ function BackstoryPage(props: BackstoryTabProps) {
return ( return (
<Box <Box
className={className || "BackstoryTab"} className={className || 'BackstoryTab'}
sx={{ display: active ? "flex" : "none", p: 0, m: 0, borders: "none" }} sx={{ display: active ? 'flex' : 'none', p: 0, m: 0, borders: 'none' }}
> >
{children} {children}
</Box> </Box>

View File

@ -5,9 +5,9 @@ import React, {
KeyboardEvent, KeyboardEvent,
useState, useState,
useImperativeHandle, useImperativeHandle,
} from "react"; } from 'react';
import { useTheme } from "@mui/material/styles"; import { useTheme } from '@mui/material/styles';
import "./BackstoryTextField.css"; import './BackstoryTextField.css';
// Define ref interface for exposed methods // Define ref interface for exposed methods
interface BackstoryTextFieldRef { interface BackstoryTextFieldRef {
@ -25,141 +25,133 @@ interface BackstoryTextFieldProps {
style?: CSSProperties; style?: CSSProperties;
} }
const BackstoryTextField = React.forwardRef< const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryTextFieldProps>(
BackstoryTextFieldRef, (props, ref) => {
BackstoryTextFieldProps const { value = '', disabled = false, placeholder, onEnter, onChange, style } = props;
>((props, ref) => { const theme = useTheme();
const { const textareaRef = useRef<HTMLTextAreaElement>(null);
value = "", const shadowRef = useRef<HTMLTextAreaElement>(null);
disabled = false, const [editValue, setEditValue] = useState<string>(value);
placeholder,
onEnter,
onChange,
style,
} = props;
const theme = useTheme();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const shadowRef = useRef<HTMLTextAreaElement>(null);
const [editValue, setEditValue] = useState<string>(value);
// Sync editValue with prop value if it changes externally // Sync editValue with prop value if it changes externally
useEffect(() => { useEffect(() => {
setEditValue(value || ""); setEditValue(value || '');
}, [value]); }, [value]);
// Adjust textarea height based on content // Adjust textarea height based on content
useEffect(() => { useEffect(() => {
if (!textareaRef.current || !shadowRef.current) { if (!textareaRef.current || !shadowRef.current) {
return; return;
} }
const textarea = textareaRef.current; const textarea = textareaRef.current;
const shadow = shadowRef.current; const shadow = shadowRef.current;
// Set shadow value to editValue or placeholder if editValue is empty // Set shadow value to editValue or placeholder if editValue is empty
shadow.value = editValue || placeholder; shadow.value = editValue || placeholder;
// Ensure shadow textarea has same content-relevant styles // Ensure shadow textarea has same content-relevant styles
const computed = getComputedStyle(textarea); const computed = getComputedStyle(textarea);
shadow.style.width = computed.width; // Match width for accurate wrapping shadow.style.width = computed.width; // Match width for accurate wrapping
shadow.style.fontSize = computed.fontSize; shadow.style.fontSize = computed.fontSize;
shadow.style.lineHeight = computed.lineHeight; shadow.style.lineHeight = computed.lineHeight;
shadow.style.fontFamily = computed.fontFamily; shadow.style.fontFamily = computed.fontFamily;
shadow.style.letterSpacing = computed.letterSpacing; shadow.style.letterSpacing = computed.letterSpacing;
shadow.style.wordSpacing = computed.wordSpacing; shadow.style.wordSpacing = computed.wordSpacing;
// Use requestAnimationFrame to ensure DOM is settled // Use requestAnimationFrame to ensure DOM is settled
const raf = requestAnimationFrame(() => { const raf = requestAnimationFrame(() => {
const paddingTop = parseFloat(computed.paddingTop || "0"); const paddingTop = parseFloat(computed.paddingTop || '0');
const paddingBottom = parseFloat(computed.paddingBottom || "0"); const paddingBottom = parseFloat(computed.paddingBottom || '0');
const totalPadding = paddingTop + paddingBottom; const totalPadding = paddingTop + paddingBottom;
// Reset height to auto to allow shrinking // Reset height to auto to allow shrinking
textarea.style.height = "auto"; textarea.style.height = 'auto';
const newHeight = shadow.scrollHeight + totalPadding; const newHeight = shadow.scrollHeight + totalPadding;
textarea.style.height = `${newHeight}px`; textarea.style.height = `${newHeight}px`;
}); });
// Cleanup RAF to prevent memory leaks // Cleanup RAF to prevent memory leaks
return () => cancelAnimationFrame(raf); return () => cancelAnimationFrame(raf);
}, [editValue, placeholder]); }, [editValue, placeholder]);
// Expose getValue method via ref // Expose getValue method via ref
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getValue: () => editValue, getValue: () => editValue,
setValue: (value: string) => setEditValue(value), setValue: (value: string) => setEditValue(value),
getAndResetValue: () => { getAndResetValue: () => {
const _ev = editValue; const _ev = editValue;
setEditValue(""); setEditValue('');
return _ev; return _ev;
}, },
})); }));
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
if (!onEnter) { if (!onEnter) {
return; return;
} }
if (event.key === "Enter" && !event.shiftKey) { if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault(); // Prevent newline event.preventDefault(); // Prevent newline
onEnter(editValue); onEnter(editValue);
setEditValue(""); // Clear textarea setEditValue(''); // Clear textarea
} }
}; };
const fullStyle: CSSProperties = { const fullStyle: CSSProperties = {
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
width: "100%", width: '100%',
padding: "16.5px 14px", padding: '16.5px 14px',
resize: "none", resize: 'none',
overflow: "hidden", overflow: 'hidden',
boxSizing: "border-box", boxSizing: 'border-box',
// minHeight: 'calc(1.5rem + 28px)', // lineHeight + padding // minHeight: 'calc(1.5rem + 28px)', // lineHeight + padding
lineHeight: "1.5", lineHeight: '1.5',
borderRadius: "4px", borderRadius: '4px',
fontSize: "16px", fontSize: '16px',
backgroundColor: "rgba(0, 0, 0, 0)", backgroundColor: 'rgba(0, 0, 0, 0)',
fontFamily: theme.typography.fontFamily, fontFamily: theme.typography.fontFamily,
...style, ...style,
}; };
return ( return (
<> <>
<textarea <textarea
className="BackstoryTextField" className="BackstoryTextField"
ref={textareaRef} ref={textareaRef}
value={editValue} value={editValue}
disabled={disabled} disabled={disabled}
placeholder={placeholder} placeholder={placeholder}
onChange={(e) => { onChange={e => {
setEditValue(e.target.value); setEditValue(e.target.value);
onChange && onChange(e.target.value); onChange && onChange(e.target.value);
}} }}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
style={fullStyle} style={fullStyle}
/> />
<textarea <textarea
className="BackgroundTextField" className="BackgroundTextField"
ref={shadowRef} ref={shadowRef}
aria-hidden="true" aria-hidden="true"
style={{ style={{
...fullStyle, ...fullStyle,
position: "absolute", position: 'absolute',
top: "-9999px", top: '-9999px',
left: "-9999px", left: '-9999px',
visibility: "hidden", visibility: 'hidden',
padding: "0px", // No padding to match content height padding: '0px', // No padding to match content height
margin: "0px", margin: '0px',
border: "0px", // Remove border to avoid extra height border: '0px', // Remove border to avoid extra height
height: "auto", // Allow natural height height: 'auto', // Allow natural height
minHeight: "0px", minHeight: '0px',
}} }}
readOnly readOnly
tabIndex={-1} tabIndex={-1}
/> />
</> </>
); );
}); }
);
export type { BackstoryTextFieldRef }; export type { BackstoryTextFieldRef };

View File

@ -5,25 +5,22 @@ import React, {
useEffect, useEffect,
useRef, useRef,
useCallback, useCallback,
} from "react"; } from 'react';
import Tooltip from "@mui/material/Tooltip"; import Tooltip from '@mui/material/Tooltip';
import IconButton from "@mui/material/IconButton"; import IconButton from '@mui/material/IconButton';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import SendIcon from "@mui/icons-material/Send"; import SendIcon from '@mui/icons-material/Send';
import CancelIcon from "@mui/icons-material/Cancel"; import CancelIcon from '@mui/icons-material/Cancel';
import { SxProps, Theme } from "@mui/material"; import { SxProps, Theme } from '@mui/material';
import PropagateLoader from "react-spinners/PropagateLoader"; import PropagateLoader from 'react-spinners/PropagateLoader';
import { Message } from "./Message"; import { Message } from './Message';
import { DeleteConfirmation } from "components/DeleteConfirmation"; import { DeleteConfirmation } from 'components/DeleteConfirmation';
import { import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
BackstoryTextField, import { BackstoryElementProps } from './BackstoryTab';
BackstoryTextFieldRef, import { useAuth } from 'hooks/AuthContext';
} from "components/BackstoryTextField"; import { StreamingResponse } from 'services/api-client';
import { BackstoryElementProps } from "./BackstoryTab";
import { useAuth } from "hooks/AuthContext";
import { StreamingResponse } from "services/api-client";
import { import {
ChatMessage, ChatMessage,
ChatContext, ChatContext,
@ -33,33 +30,28 @@ import {
ChatMessageError, ChatMessageError,
ChatMessageStreaming, ChatMessageStreaming,
ChatMessageStatus, ChatMessageStatus,
} from "types/types"; } from 'types/types';
import { PaginatedResponse } from "types/conversion"; import { PaginatedResponse } from 'types/conversion';
import "./Conversation.css"; import './Conversation.css';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
status: "done", status: 'done',
type: "text", type: 'text',
sessionId: "", sessionId: '',
timestamp: new Date(), timestamp: new Date(),
content: "", content: '',
role: "assistant", role: 'assistant',
metadata: null as any, metadata: null as any,
}; };
const loadingMessage: ChatMessage = { const loadingMessage: ChatMessage = {
...defaultMessage, ...defaultMessage,
content: "Establishing connection with server...", content: 'Establishing connection with server...',
}; };
type ConversationMode = type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check' | 'persona';
| "chat"
| "job_description"
| "resume"
| "fact_check"
| "persona";
interface ConversationHandle { interface ConversationHandle {
submitQuery: (query: ChatQuery) => void; submitQuery: (query: ChatQuery) => void;
@ -106,15 +98,9 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
const [countdown, setCountdown] = useState<number>(0); const [countdown, setCountdown] = useState<number>(0);
const [conversation, setConversation] = useState<ChatMessage[]>([]); const [conversation, setConversation] = useState<ChatMessage[]>([]);
const conversationRef = useRef<ChatMessage[]>([]); const conversationRef = useRef<ChatMessage[]>([]);
const [filteredConversation, setFilteredConversation] = useState< const [filteredConversation, setFilteredConversation] = useState<ChatMessage[]>([]);
ChatMessage[] const [processingMessage, setProcessingMessage] = useState<ChatMessage | undefined>(undefined);
>([]); const [streamingMessage, setStreamingMessage] = useState<ChatMessage | undefined>(undefined);
const [processingMessage, setProcessingMessage] = useState<
ChatMessage | undefined
>(undefined);
const [streamingMessage, setStreamingMessage] = useState<
ChatMessage | undefined
>(undefined);
const [noInteractions, setNoInteractions] = useState<boolean>(true); const [noInteractions, setNoInteractions] = useState<boolean>(true);
const viewableElementRef = useRef<HTMLDivElement>(null); const viewableElementRef = useRef<HTMLDivElement>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null); const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
@ -140,9 +126,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
} else { } else {
//console.log('Filtering conversation...') //console.log('Filtering conversation...')
filtered = filtered =
messageFilter( messageFilter(conversation); /* Do not copy conversation or useEffect will loop forever */
conversation
); /* Do not copy conversation or useEffect will loop forever */
//console.log(`${conversation.length - filtered.length} messages filtered out.`); //console.log(`${conversation.length - filtered.length} messages filtered out.`);
} }
if (filtered.length === 0) { if (filtered.length === 0) {
@ -154,14 +138,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
...filtered, ...filtered,
]); ]);
} }
}, [ }, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]);
conversation,
setFilteredConversation,
messageFilter,
preamble,
messages,
hidePreamble,
]);
useEffect(() => { useEffect(() => {
if (chatSession) { if (chatSession) {
@ -169,14 +146,12 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
} }
const createChatSession = async () => { const createChatSession = async () => {
try { try {
const chatContext: ChatContext = { type: "general" }; const chatContext: ChatContext = { type: 'general' };
const response: ChatSession = await apiClient.createChatSession( const response: ChatSession = await apiClient.createChatSession(chatContext);
chatContext
);
setChatSession(response); setChatSession(response);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setSnack("Unable to create chat session.", "error"); setSnack('Unable to create chat session.', 'error');
} }
}; };
@ -188,8 +163,9 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
return; return;
} }
try { try {
const response: PaginatedResponse<ChatMessage> = const response: PaginatedResponse<ChatMessage> = await apiClient.getChatMessages(
await apiClient.getChatMessages(chatSession.id); chatSession.id
);
const messages: ChatMessage[] = response.data; const messages: ChatMessage[] = response.data;
setProcessingMessage(undefined); setProcessingMessage(undefined);
@ -200,25 +176,22 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
setConversation([]); setConversation([]);
setNoInteractions(true); setNoInteractions(true);
} else { } else {
console.log( console.log(`History returned with ${messages.length} entries:`, messages);
`History returned with ${messages.length} entries:`,
messages
);
setConversation(messages); setConversation(messages);
setNoInteractions(false); setNoInteractions(false);
} }
} catch (error) { } catch (error) {
console.error("Unable to obtain chat history", error); console.error('Unable to obtain chat history', error);
setProcessingMessage({ setProcessingMessage({
...defaultMessage, ...defaultMessage,
status: "error", status: 'error',
content: `Unable to obtain history from server.`, content: `Unable to obtain history from server.`,
}); });
setTimeout(() => { setTimeout(() => {
setProcessingMessage(undefined); setProcessingMessage(undefined);
setNoInteractions(true); setNoInteractions(true);
}, 3000); }, 3000);
setSnack("Unable to obtain chat history.", "error"); setSnack('Unable to obtain chat history.', 'error');
} }
}, [chatSession]); }, [chatSession]);
@ -284,7 +257,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
// }; // };
const cancelQuery = () => { const cancelQuery = () => {
console.log("Stop query"); console.log('Stop query');
if (controllerRef.current) { if (controllerRef.current) {
controllerRef.current.cancel(); controllerRef.current.cancel();
} }
@ -302,29 +275,29 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
...conversationRef.current, ...conversationRef.current,
{ {
...defaultMessage, ...defaultMessage,
type: "text", type: 'text',
content: query.prompt, content: query.prompt,
}, },
]); ]);
setProcessing(true); setProcessing(true);
setProcessingMessage({ setProcessingMessage({
...defaultMessage, ...defaultMessage,
content: "Submitting request...", content: 'Submitting request...',
}); });
const chatMessage: ChatMessageUser = { const chatMessage: ChatMessageUser = {
role: "user", role: 'user',
sessionId: chatSession.id, sessionId: chatSession.id,
content: query.prompt, content: query.prompt,
tunables: query.tunables, tunables: query.tunables,
status: "done", status: 'done',
type: "text", type: 'text',
timestamp: new Date(), timestamp: new Date(),
}; };
controllerRef.current = apiClient.sendMessageStream(chatMessage, { controllerRef.current = apiClient.sendMessageStream(chatMessage, {
onMessage: (msg: ChatMessage) => { onMessage: (msg: ChatMessage) => {
console.log("onMessage:", msg); console.log('onMessage:', msg);
setConversation([...conversationRef.current, msg]); setConversation([...conversationRef.current, msg]);
setStreamingMessage(undefined); setStreamingMessage(undefined);
setProcessingMessage(undefined); setProcessingMessage(undefined);
@ -334,13 +307,9 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
} }
}, },
onError: (error: string | ChatMessageError) => { onError: (error: string | ChatMessageError) => {
console.log("onError:", error); console.log('onError:', error);
// Type-guard to determine if this is a ChatMessageBase or a string // Type-guard to determine if this is a ChatMessageBase or a string
if ( if (typeof error === 'object' && error !== null && 'content' in error) {
typeof error === "object" &&
error !== null &&
"content" in error
) {
setProcessingMessage(error as ChatMessage); setProcessingMessage(error as ChatMessage);
setProcessing(false); setProcessing(false);
controllerRef.current = null; controllerRef.current = null;
@ -352,14 +321,14 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
} }
}, },
onStreaming: (chunk: ChatMessageStreaming) => { onStreaming: (chunk: ChatMessageStreaming) => {
console.log("onStreaming:", chunk); console.log('onStreaming:', chunk);
setStreamingMessage({ ...defaultMessage, ...chunk }); setStreamingMessage({ ...defaultMessage, ...chunk });
}, },
onStatus: (status: ChatMessageStatus) => { onStatus: (status: ChatMessageStatus) => {
console.log("onStatus:", status); console.log('onStatus:', status);
}, },
onComplete: () => { onComplete: () => {
console.log("onComplete"); console.log('onComplete');
controllerRef.current = null; controllerRef.current = null;
}, },
}); });
@ -384,18 +353,15 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
className="Conversation" className="Conversation"
sx={{ sx={{
flexGrow: 1, flexGrow: 1,
minHeight: "max-content", minHeight: 'max-content',
height: "max-content", height: 'max-content',
maxHeight: "max-content", maxHeight: 'max-content',
overflow: "hidden", overflow: 'hidden',
}} }}
> >
<Box sx={{ p: 1, mt: 0, ...sx }}> <Box sx={{ p: 1, mt: 0, ...sx }}>
{filteredConversation.map((message, index) => ( {filteredConversation.map((message, index) => (
<Message <Message key={index} {...{ chatSession, sendQuery: processQuery, message }} />
key={index}
{...{ chatSession, sendQuery: processQuery, message }}
/>
))} ))}
{processingMessage !== undefined && ( {processingMessage !== undefined && (
<Message <Message
@ -417,10 +383,10 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
)} )}
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
m: 1, m: 1,
}} }}
> >
@ -434,8 +400,8 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
<Box <Box
sx={{ sx={{
pt: 1, pt: 1,
fontSize: "0.7rem", fontSize: '0.7rem',
color: "darkgrey", color: 'darkgrey',
}} }}
> >
Response will be stopped in: {countdown}s Response will be stopped in: {countdown}s
@ -444,16 +410,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
</Box> </Box>
<Box <Box
className="Query" className="Query"
sx={{ display: "flex", flexDirection: "column", p: 1, flexGrow: 1 }} sx={{ display: 'flex', flexDirection: 'column', p: 1, flexGrow: 1 }}
> >
{placeholder && ( {placeholder && (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
p: 0, p: 0,
m: 0, m: 0,
flexDirection: "column", flexDirection: 'column',
}} }}
ref={viewableElementRef} ref={viewableElementRef}
> >
@ -469,24 +435,20 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
<Box <Box
key="jobActions" key="jobActions"
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
flexDirection: "row", flexDirection: 'row',
}} }}
> >
<DeleteConfirmation <DeleteConfirmation
label={resetLabel || "all data"} label={resetLabel || 'all data'}
disabled={ disabled={!chatSession || processingMessage !== undefined || noInteractions}
!chatSession ||
processingMessage !== undefined ||
noInteractions
}
onDelete={() => { onDelete={() => {
/*reset(); resetAction && resetAction(); */ /*reset(); resetAction && resetAction(); */
}} }}
/> />
<Tooltip title={actionLabel || "Send"}> <Tooltip title={actionLabel || 'Send'}>
<span style={{ display: "flex", flexGrow: 1 }}> <span style={{ display: 'flex', flexGrow: 1 }}>
<Button <Button
sx={{ m: 1, gap: 1, flexGrow: 1 }} sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained" variant="contained"
@ -496,7 +458,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
prompt: prompt:
(backstoryTextRef.current && (backstoryTextRef.current &&
backstoryTextRef.current.getAndResetValue()) || backstoryTextRef.current.getAndResetValue()) ||
"", '',
}); });
}} }}
> >
@ -506,20 +468,18 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
</span> </span>
</Tooltip> </Tooltip>
<Tooltip title="Cancel"> <Tooltip title="Cancel">
<span style={{ display: "flex" }}> <span style={{ display: 'flex' }}>
{" "} {' '}
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} {/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton <IconButton
aria-label="cancel" aria-label="cancel"
onClick={() => { onClick={() => {
cancelQuery(); cancelQuery();
}} }}
sx={{ display: "flex", margin: "auto 0px" }} sx={{ display: 'flex', margin: 'auto 0px' }}
size="large" size="large"
edge="start" edge="start"
disabled={ disabled={stopRef.current || !chatSession || processing === false}
stopRef.current || !chatSession || processing === false
}
> >
<CancelIcon /> <CancelIcon />
</IconButton> </IconButton>
@ -530,13 +490,13 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
{(noInteractions || !hideDefaultPrompts) && {(noInteractions || !hideDefaultPrompts) &&
defaultPrompts !== undefined && defaultPrompts !== undefined &&
defaultPrompts.length !== 0 && ( defaultPrompts.length !== 0 && (
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
{defaultPrompts.map((element, index) => { {defaultPrompts.map((element, index) => {
return <Box key={index}>{element}</Box>; return <Box key={index}>{element}</Box>;
})} })}
</Box> </Box>
)} )}
<Box sx={{ display: "flex", flexGrow: 1 }}></Box> <Box sx={{ display: 'flex', flexGrow: 1 }}></Box>
</Box> </Box>
</Box> </Box>
); );

View File

@ -1,9 +1,9 @@
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, { IconButtonProps } 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 extends IconButtonProps { interface CopyBubbleProps extends IconButtonProps {
content: string | undefined; content: string | undefined;
@ -14,7 +14,7 @@ interface CopyBubbleProps extends IconButtonProps {
const CopyBubble = ({ const CopyBubble = ({
content, content,
sx, sx,
tooltip = "Copy to clipboard", tooltip = 'Copy to clipboard',
onClick, onClick,
...rest ...rest
}: CopyBubbleProps) => { }: CopyBubbleProps) => {
@ -38,19 +38,19 @@ const CopyBubble = ({
return ( return (
<Tooltip title={tooltip} placement="top" arrow> <Tooltip title={tooltip} placement="top" arrow>
<IconButton <IconButton
onClick={(e) => { onClick={e => {
handleCopy(e); handleCopy(e);
}} }}
sx={{ sx={{
width: 24, width: 24,
height: 24, height: 24,
opacity: 0.75, opacity: 0.75,
bgcolor: "background.paper", bgcolor: 'background.paper',
"&:hover": { bgcolor: "action.hover", opacity: 1 }, '&:hover': { bgcolor: 'action.hover', opacity: 1 },
...sx, ...sx,
}} }}
size="small" size="small"
color={copied ? "success" : "default"} color={copied ? 'success' : 'default'}
{...rest} {...rest}
> >
{copied ? ( {copied ? (

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { import {
IconButton, IconButton,
Dialog, Dialog,
@ -10,25 +10,25 @@ import {
useMediaQuery, useMediaQuery,
Tooltip, Tooltip,
SxProps, SxProps,
} from "@mui/material"; } from '@mui/material';
import { useTheme } from "@mui/material/styles"; import { useTheme } from '@mui/material/styles';
import ResetIcon from "@mui/icons-material/History"; import ResetIcon from '@mui/icons-material/History';
interface DeleteConfirmationProps { interface DeleteConfirmationProps {
// Legacy props for backward compatibility (uncontrolled mode) // Legacy props for backward compatibility (uncontrolled mode)
onDelete?: () => void; onDelete?: () => void;
disabled?: boolean; disabled?: boolean;
label?: string; label?: string;
action?: "delete" | "reset"; action?: 'delete' | 'reset';
color?: color?:
| "inherit" | 'inherit'
| "default" | 'default'
| "primary" | 'primary'
| "secondary" | 'secondary'
| "error" | 'error'
| "info" | 'info'
| "success" | 'success'
| "warning" | 'warning'
| undefined; | undefined;
sx?: SxProps; sx?: SxProps;
// New props for controlled mode // New props for controlled mode
@ -56,7 +56,7 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
disabled, disabled,
label, label,
color, color,
action = "delete", action = 'delete',
// New props // New props
open: controlledOpen, open: controlledOpen,
onClose: controlledOnClose, onClose: controlledOnClose,
@ -65,7 +65,7 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
message, message,
hideButton = false, hideButton = false,
confirmButtonText, confirmButtonText,
cancelButtonText = "Cancel", cancelButtonText = 'Cancel',
sx, sx,
icon = <ResetIcon />, icon = <ResetIcon />,
} = props; } = props;
@ -73,7 +73,7 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
// Internal state for uncontrolled mode // Internal state for uncontrolled mode
const [internalOpen, setInternalOpen] = useState(false); const [internalOpen, setInternalOpen] = useState(false);
const theme = useTheme(); const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down("md")); const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
// Determine if we're in controlled or uncontrolled mode // Determine if we're in controlled or uncontrolled mode
const isControlled = controlledOpen !== undefined; const isControlled = controlledOpen !== undefined;
@ -103,35 +103,32 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
}; };
// Determine dialog content based on mode // Determine dialog content based on mode
const dialogTitle = title || "Confirm Reset"; const dialogTitle = title || 'Confirm Reset';
const dialogMessage = const dialogMessage =
message || message ||
`This action will permanently ${capitalizeFirstLetter(action)} ${ `This action will permanently ${capitalizeFirstLetter(action)} ${
label ? label.toLowerCase() : "all data" label ? label.toLowerCase() : 'all data'
} without the ability to recover it. Are you sure you want to continue?`; } without the ability to recover it. Are you sure you want to continue?`;
const confirmText = const confirmText =
confirmButtonText || confirmButtonText || `${capitalizeFirstLetter(action)} ${label || 'Everything'}`;
`${capitalizeFirstLetter(action)} ${label || "Everything"}`;
return ( return (
<> <>
{/* Only show button if not hidden (for controlled mode) */} {/* Only show button if not hidden (for controlled mode) */}
{!hideButton && ( {!hideButton && (
<Tooltip <Tooltip title={label ? `${capitalizeFirstLetter(action)} ${label}` : 'Reset'}>
title={label ? `${capitalizeFirstLetter(action)} ${label}` : "Reset"} <span style={{ display: 'flex' }}>
> {' '}
<span style={{ display: "flex" }}>
{" "}
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} {/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton <IconButton
aria-label={action} aria-label={action}
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
handleClickOpen(); handleClickOpen();
}} }}
color={color || "inherit"} color={color || 'inherit'}
sx={{ display: "flex", margin: "auto 0px", ...sx }} sx={{ display: 'flex', margin: 'auto 0px', ...sx }}
size="large" size="large"
edge="start" edge="start"
disabled={disabled} disabled={disabled}

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { BackstoryElementProps } from "./BackstoryTab"; import { BackstoryElementProps } from './BackstoryTab';
import { StyledMarkdown } from "./StyledMarkdown"; import { StyledMarkdown } from './StyledMarkdown';
interface DocumentProps extends BackstoryElementProps { interface DocumentProps extends BackstoryElementProps {
filepath?: string; filepath?: string;
@ -9,7 +9,7 @@ interface DocumentProps extends BackstoryElementProps {
const Document = (props: DocumentProps) => { const Document = (props: DocumentProps) => {
const { filepath } = props; const { filepath } = props;
const [document, setDocument] = useState<string>(""); const [document, setDocument] = useState<string>('');
// Get the markdown // Get the markdown
useEffect(() => { useEffect(() => {
@ -19,9 +19,9 @@ const Document = (props: DocumentProps) => {
const fetchDocument = async () => { const fetchDocument = async () => {
try { try {
const response = await fetch(filepath, { const response = await fetch(filepath, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
if (!response.ok) { if (!response.ok) {
@ -30,7 +30,7 @@ const Document = (props: DocumentProps) => {
const data = await response.text(); const data = await response.text();
setDocument(data); setDocument(data);
} catch (error: any) { } catch (error: any) {
console.error("Error obtaining Docs content information:", error); console.error('Error obtaining Docs content information:', error);
setDocument(`${filepath} not found.`); setDocument(`${filepath} not found.`);
} }
}; };

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { import {
Box, Box,
Button, Button,
@ -22,54 +22,44 @@ import {
Chip, Chip,
Divider, Divider,
Paper, Paper,
} from "@mui/material"; } from '@mui/material';
import { styled } from "@mui/material/styles"; import { styled } from '@mui/material/styles';
import { import { CloudUpload, Edit, Delete, Visibility, Close } from '@mui/icons-material';
CloudUpload, import { useTheme } from '@mui/material/styles';
Edit,
Delete,
Visibility,
Close,
} from "@mui/icons-material";
import { useTheme } from "@mui/material/styles";
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import * as Types from "types/types"; import * as Types from 'types/types';
import { BackstoryElementProps } from "./BackstoryTab"; import { BackstoryElementProps } from './BackstoryTab';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
const VisuallyHiddenInput = styled("input")({ const VisuallyHiddenInput = styled('input')({
clip: "rect(0 0 0 0)", clip: 'rect(0 0 0 0)',
clipPath: "inset(50%)", clipPath: 'inset(50%)',
height: 1, height: 1,
overflow: "hidden", overflow: 'hidden',
position: "absolute", position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
whiteSpace: "nowrap", whiteSpace: 'nowrap',
width: 1, width: 1,
}); });
const DocumentManager = (props: BackstoryElementProps) => { const DocumentManager = (props: BackstoryElementProps) => {
const theme = useTheme(); const theme = useTheme();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const [documents, setDocuments] = useState<Types.Document[]>([]); const [documents, setDocuments] = useState<Types.Document[]>([]);
const [selectedDocument, setSelectedDocument] = const [selectedDocument, setSelectedDocument] = useState<Types.Document | null>(null);
useState<Types.Document | null>(null); const [documentContent, setDocumentContent] = useState<string>('');
const [documentContent, setDocumentContent] = useState<string>("");
const [isViewingContent, setIsViewingContent] = useState(false); const [isViewingContent, setIsViewingContent] = useState(false);
const [editingDocument, setEditingDocument] = useState<Types.Document | null>( const [editingDocument, setEditingDocument] = useState<Types.Document | null>(null);
null const [editingName, setEditingName] = useState('');
);
const [editingName, setEditingName] = useState("");
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
// Check if user is a candidate // Check if user is a candidate
const candidate = const candidate = user?.userType === 'candidate' ? (user as Types.Candidate) : null;
user?.userType === "candidate" ? (user as Types.Candidate) : null;
// Load documents on component mount // Load documents on component mount
useEffect(() => { useEffect(() => {
@ -84,38 +74,33 @@ const DocumentManager = (props: BackstoryElementProps) => {
setDocuments(results.documents); setDocuments(results.documents);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setSnack("Failed to load documents", "error"); setSnack('Failed to load documents', 'error');
} }
}; };
// Handle document upload // Handle document upload
const handleDocumentUpload = async ( const handleDocumentUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.files && e.target.files[0]) { if (e.target.files && e.target.files[0]) {
const file = e.target.files[0]; const file = e.target.files[0];
const fileExtension = "." + file.name.split(".").pop()?.toLowerCase(); const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
let docType: Types.DocumentType | null = null; let docType: Types.DocumentType | null = null;
switch (fileExtension.substring(1)) { switch (fileExtension.substring(1)) {
case "pdf": case 'pdf':
docType = "pdf"; docType = 'pdf';
break; break;
case "docx": case 'docx':
docType = "docx"; docType = 'docx';
break; break;
case "md": case 'md':
docType = "markdown"; docType = 'markdown';
break; break;
case "txt": case 'txt':
docType = "txt"; docType = 'txt';
break; break;
} }
if (!docType) { if (!docType) {
setSnack( setSnack('Invalid file type. Please upload .txt, .md, .docx, or .pdf files only.', 'error');
"Invalid file type. Please upload .txt, .md, .docx, or .pdf files only.",
"error"
);
return; return;
} }
@ -125,23 +110,23 @@ const DocumentManager = (props: BackstoryElementProps) => {
file, file,
{ includeInRag: true, isJobDocument: false }, { includeInRag: true, isJobDocument: false },
{ {
onError: (error) => { onError: error => {
console.error(error); console.error(error);
setSnack(error.content, "error"); setSnack(error.content, 'error');
}, },
} }
); );
const result = await controller.promise; const result = await controller.promise;
if (result && result.document) { if (result && result.document) {
setDocuments((prev) => [...prev, result.document]); setDocuments(prev => [...prev, result.document]);
setSnack(`Document uploaded: ${file.name}`, "success"); setSnack(`Document uploaded: ${file.name}`, 'success');
} }
// Reset file input // Reset file input
e.target.value = ""; e.target.value = '';
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setSnack("Failed to upload document", "error"); setSnack('Failed to upload document', 'error');
} }
} }
}; };
@ -152,51 +137,40 @@ const DocumentManager = (props: BackstoryElementProps) => {
// Call API to delete document // Call API to delete document
await apiClient.deleteCandidateDocument(document); await apiClient.deleteCandidateDocument(document);
setDocuments((prev) => prev.filter((doc) => doc.id !== document.id)); setDocuments(prev => prev.filter(doc => doc.id !== document.id));
setSnack("Document deleted successfully", "success"); setSnack('Document deleted successfully', 'success');
// Close content view if this document was being viewed // Close content view if this document was being viewed
if (selectedDocument?.id === document.id) { if (selectedDocument?.id === document.id) {
setIsViewingContent(false); setIsViewingContent(false);
setSelectedDocument(null); setSelectedDocument(null);
setDocumentContent(""); setDocumentContent('');
} }
} catch (error) { } catch (error) {
setSnack("Failed to delete document", "error"); setSnack('Failed to delete document', 'error');
} }
}; };
// Handle RAG flag toggle // Handle RAG flag toggle
const handleRAGToggle = async ( const handleRAGToggle = async (document: Types.Document, includeInRag: boolean) => {
document: Types.Document,
includeInRag: boolean
) => {
try { try {
document.options = { includeInRag }; document.options = { includeInRag };
// Call API to update RAG flag // Call API to update RAG flag
await apiClient.updateCandidateDocument(document); await apiClient.updateCandidateDocument(document);
setDocuments((prev) => setDocuments(prev =>
prev.map((doc) => prev.map(doc => (doc.id === document.id ? { ...doc, includeInRag } : doc))
doc.id === document.id ? { ...doc, includeInRag } : doc
)
);
setSnack(
`Document ${includeInRag ? "included in" : "excluded from"} RAG`,
"success"
); );
setSnack(`Document ${includeInRag ? 'included in' : 'excluded from'} RAG`, 'success');
} catch (error) { } catch (error) {
setSnack("Failed to update RAG setting", "error"); setSnack('Failed to update RAG setting', 'error');
} }
}; };
// Handle document rename // Handle document rename
const handleRenameDocument = async ( const handleRenameDocument = async (document: Types.Document, newName: string) => {
document: Types.Document,
newName: string
) => {
if (!newName.trim()) { if (!newName.trim()) {
setSnack("Document name cannot be empty", "error"); setSnack('Document name cannot be empty', 'error');
return; return;
} }
@ -205,17 +179,15 @@ const DocumentManager = (props: BackstoryElementProps) => {
document.filename = newName; document.filename = newName;
await apiClient.updateCandidateDocument(document); await apiClient.updateCandidateDocument(document);
setDocuments((prev) => setDocuments(prev =>
prev.map((doc) => prev.map(doc => (doc.id === document.id ? { ...doc, filename: newName.trim() } : doc))
doc.id === document.id ? { ...doc, filename: newName.trim() } : doc
)
); );
setSnack("Document renamed successfully", "success"); setSnack('Document renamed successfully', 'success');
setIsRenameDialogOpen(false); setIsRenameDialogOpen(false);
setEditingDocument(null); setEditingDocument(null);
setEditingName(""); setEditingName('');
} catch (error) { } catch (error) {
setSnack("Failed to rename document", "error"); setSnack('Failed to rename document', 'error');
} }
}; };
@ -229,7 +201,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
const result = await apiClient.getCandidateDocumentText(document); const result = await apiClient.getCandidateDocumentText(document);
setDocumentContent(result.content); setDocumentContent(result.content);
} catch (error) { } catch (error) {
setSnack("Failed to load document content", "error"); setSnack('Failed to load document content', 'error');
setIsViewingContent(false); setIsViewingContent(false);
} }
}; };
@ -243,58 +215,52 @@ const DocumentManager = (props: BackstoryElementProps) => {
// Format file size // Format file size
const formatFileSize = (bytes: number): string => { const formatFileSize = (bytes: number): string => {
if (bytes === 0) return "0 Bytes"; if (bytes === 0) return '0 Bytes';
const k = 1024; const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"]; const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}; };
// Get file type color // Get file type color
const getFileTypeColor = ( const getFileTypeColor = (type: string): 'primary' | 'secondary' | 'success' | 'warning' => {
type: string
): "primary" | "secondary" | "success" | "warning" => {
switch (type) { switch (type) {
case "pdf": case 'pdf':
return "primary"; return 'primary';
case "docx": case 'docx':
return "secondary"; return 'secondary';
case "txt": case 'txt':
return "success"; return 'success';
case "md": case 'md':
return "warning"; return 'warning';
default: default:
return "primary"; return 'primary';
} }
}; };
if (!candidate) { if (!candidate) {
return ( return <Box>You must be logged in as a candidate to view this content.</Box>;
<Box>You must be logged in as a candidate to view this content.</Box>
);
} }
return ( return (
<> <>
<Grid container spacing={{ xs: 1, sm: 3 }} sx={{ maxWidth: "100%" }}> <Grid container spacing={{ xs: 1, sm: 3 }} sx={{ maxWidth: '100%' }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
mb: 2, mb: 2,
width: "100%", width: '100%',
verticalAlign: "center", verticalAlign: 'center',
}} }}
> >
<Typography variant={isMobile ? "subtitle2" : "h6"}> <Typography variant={isMobile ? 'subtitle2' : 'h6'}>Documents</Typography>
Documents
</Typography>
<Button <Button
component="label" component="label"
variant="contained" variant="contained"
startIcon={<CloudUpload />} startIcon={<CloudUpload />}
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
> >
Upload Document Upload Document
<VisuallyHiddenInput <VisuallyHiddenInput
@ -313,15 +279,15 @@ const DocumentManager = (props: BackstoryElementProps) => {
variant="body2" variant="body2"
color="text.secondary" color="text.secondary"
sx={{ sx={{
fontSize: { xs: "0.8rem", sm: "0.875rem" }, fontSize: { xs: '0.8rem', sm: '0.875rem' },
textAlign: "center", textAlign: 'center',
py: 3, py: 3,
}} }}
> >
No additional documents uploaded No additional documents uploaded
</Typography> </Typography>
) : ( ) : (
<List sx={{ width: "100%" }}> <List sx={{ width: '100%' }}>
{documents.map((doc, index) => ( {documents.map((doc, index) => (
<React.Fragment key={doc.id}> <React.Fragment key={doc.id}>
{index > 0 && <Divider />} {index > 0 && <Divider />}
@ -330,17 +296,17 @@ const DocumentManager = (props: BackstoryElementProps) => {
primary={ primary={
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: 1, gap: 1,
flexWrap: "wrap", flexWrap: 'wrap',
}} }}
> >
<Typography <Typography
variant="body1" variant="body1"
sx={{ sx={{
wordBreak: "break-word", wordBreak: 'break-word',
fontSize: { xs: "0.9rem", sm: "1rem" }, fontSize: { xs: '0.9rem', sm: '1rem' },
}} }}
> >
{doc.filename} {doc.filename}
@ -351,47 +317,32 @@ const DocumentManager = (props: BackstoryElementProps) => {
color={getFileTypeColor(doc.type)} color={getFileTypeColor(doc.type)}
/> />
{doc.options?.includeInRag && ( {doc.options?.includeInRag && (
<Chip <Chip label="RAG" size="small" color="success" variant="outlined" />
label="RAG"
size="small"
color="success"
variant="outlined"
/>
)} )}
</Box> </Box>
} }
secondary={ secondary={
<Box sx={{ mt: 0.5 }}> <Box sx={{ mt: 0.5 }}>
<Typography <Typography variant="caption" color="text.secondary">
variant="caption" {formatFileSize(doc.size)} {doc?.uploadDate?.toLocaleDateString()}
color="text.secondary"
>
{formatFileSize(doc.size)} {" "}
{doc?.uploadDate?.toLocaleDateString()}
</Typography> </Typography>
<Box sx={{ mt: 1 }}> <Box sx={{ mt: 1 }}>
<FormControlLabel <FormControlLabel
control={ control={
<Switch <Switch
checked={doc.options?.includeInRag} checked={doc.options?.includeInRag}
onChange={(e) => onChange={e => handleRAGToggle(doc, e.target.checked)}
handleRAGToggle(doc, e.target.checked)
}
size="small" size="small"
/> />
} }
label={ label={<Typography variant="caption">Include in RAG</Typography>}
<Typography variant="caption">
Include in RAG
</Typography>
}
/> />
</Box> </Box>
</Box> </Box>
} }
/> />
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Box sx={{ display: "flex", gap: 0.5 }}> <Box sx={{ display: 'flex', gap: 0.5 }}>
<IconButton <IconButton
edge="end" edge="end"
size="small" size="small"
@ -435,21 +386,19 @@ const DocumentManager = (props: BackstoryElementProps) => {
<CardContent sx={{ p: { xs: 1.5, sm: 3 } }}> <CardContent sx={{ p: { xs: 1.5, sm: 3 } }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
mb: 2, mb: 2,
}} }}
> >
<Typography variant={isMobile ? "subtitle2" : "h6"}> <Typography variant={isMobile ? 'subtitle2' : 'h6'}>Document Content</Typography>
Document Content
</Typography>
<IconButton <IconButton
size="small" size="small"
onClick={() => { onClick={() => {
setIsViewingContent(false); setIsViewingContent(false);
setSelectedDocument(null); setSelectedDocument(null);
setDocumentContent(""); setDocumentContent('');
}} }}
> >
<Close /> <Close />
@ -460,20 +409,20 @@ const DocumentManager = (props: BackstoryElementProps) => {
sx={{ sx={{
p: 2, p: 2,
maxHeight: 400, maxHeight: 400,
overflow: "auto", overflow: 'auto',
backgroundColor: "grey.50", backgroundColor: 'grey.50',
}} }}
> >
<pre <pre
style={{ style={{
margin: 0, margin: 0,
fontFamily: "monospace", fontFamily: 'monospace',
fontSize: isMobile ? "0.75rem" : "0.875rem", fontSize: isMobile ? '0.75rem' : '0.875rem',
whiteSpace: "pre-wrap", whiteSpace: 'pre-wrap',
wordBreak: "break-word", wordBreak: 'break-word',
}} }}
> >
{documentContent || "Loading content..."} {documentContent || 'Loading content...'}
</pre> </pre>
</Paper> </Paper>
</CardContent> </CardContent>
@ -497,9 +446,9 @@ const DocumentManager = (props: BackstoryElementProps) => {
fullWidth fullWidth
variant="outlined" variant="outlined"
value={editingName} value={editingName}
onChange={(e) => setEditingName(e.target.value)} onChange={e => setEditingName(e.target.value)}
onKeyPress={(e) => { onKeyPress={e => {
if (e.key === "Enter" && editingDocument) { if (e.key === 'Enter' && editingDocument) {
handleRenameDocument(editingDocument, editingName); handleRenameDocument(editingDocument, editingName);
} }
}} }}
@ -508,10 +457,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
<DialogActions> <DialogActions>
<Button onClick={() => setIsRenameDialogOpen(false)}>Cancel</Button> <Button onClick={() => setIsRenameDialogOpen(false)}>Cancel</Button>
<Button <Button
onClick={() => onClick={() => editingDocument && handleRenameDocument(editingDocument, editingName)}
editingDocument &&
handleRenameDocument(editingDocument, editingName)
}
variant="contained" variant="contained"
disabled={!editingName.trim()} disabled={!editingName.trim()}
> >

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { import {
Box, Box,
Card, Card,
@ -19,7 +19,7 @@ import {
FormControlLabel, FormControlLabel,
Grid, Grid,
IconButton, IconButton,
} from "@mui/material"; } from '@mui/material';
import { import {
Email as EmailIcon, Email as EmailIcon,
Security as SecurityIcon, Security as SecurityIcon,
@ -29,32 +29,25 @@ import {
DevicesOther as DevicesIcon, DevicesOther as DevicesIcon,
VisibilityOff, VisibilityOff,
Visibility, Visibility,
} from "@mui/icons-material"; } from '@mui/icons-material';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { BackstoryPageProps } from "./BackstoryTab"; import { BackstoryPageProps } from './BackstoryTab';
import { Navigate, useNavigate } from "react-router-dom"; import { Navigate, useNavigate } from 'react-router-dom';
// Email Verification Component // Email Verification Component
const EmailVerificationPage = (props: BackstoryPageProps) => { const EmailVerificationPage = (props: BackstoryPageProps) => {
const { const { verifyEmail, resendEmailVerification, getPendingVerificationEmail, isLoading, error } =
verifyEmail, useAuth();
resendEmailVerification,
getPendingVerificationEmail,
isLoading,
error,
} = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [verificationToken, setVerificationToken] = useState(""); const [verificationToken, setVerificationToken] = useState('');
const [status, setStatus] = useState<"pending" | "success" | "error">( const [status, setStatus] = useState<'pending' | 'success' | 'error'>('pending');
"pending" const [message, setMessage] = useState('');
); const [userType, setUserType] = useState<string>('');
const [message, setMessage] = useState("");
const [userType, setUserType] = useState<string>("");
useEffect(() => { useEffect(() => {
// Get token from URL parameters // Get token from URL parameters
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get("token"); const token = urlParams.get('token');
if (token) { if (token) {
setVerificationToken(token); setVerificationToken(token);
@ -64,8 +57,8 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
const handleVerifyEmail = async (token: string) => { const handleVerifyEmail = async (token: string) => {
if (!token) { if (!token) {
setStatus("error"); setStatus('error');
setMessage("Invalid verification link"); setMessage('Invalid verification link');
return; return;
} }
@ -73,60 +66,58 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
const result = await verifyEmail({ token }); const result = await verifyEmail({ token });
if (result) { if (result) {
setStatus("success"); setStatus('success');
setMessage(result.message); setMessage(result.message);
setUserType(result.userType); setUserType(result.userType);
// Redirect to login after 3 seconds // Redirect to login after 3 seconds
setTimeout(() => { setTimeout(() => {
navigate("/login"); navigate('/login');
}, 3000); }, 3000);
} else { } else {
setStatus("error"); setStatus('error');
setMessage("Email verification failed"); setMessage('Email verification failed');
} }
} catch (error) { } catch (error) {
setStatus("error"); setStatus('error');
setMessage("Email verification failed"); setMessage('Email verification failed');
} }
}; };
const handleResendVerification = async () => { const handleResendVerification = async () => {
const email = getPendingVerificationEmail(); const email = getPendingVerificationEmail();
if (!email) { if (!email) {
setMessage("No pending verification email found."); setMessage('No pending verification email found.');
return; return;
} }
try { try {
const success = await resendEmailVerification(email); const success = await resendEmailVerification(email);
if (success) { if (success) {
setMessage("Verification email sent successfully!"); setMessage('Verification email sent successfully!');
} }
} catch (error) { } catch (error) {
setMessage("Failed to resend verification email."); setMessage('Failed to resend verification email.');
} }
}; };
return ( return (
<Box <Box
sx={{ sx={{
minHeight: "100vh", minHeight: '100vh',
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
bgcolor: "grey.50", bgcolor: 'grey.50',
p: 2, p: 2,
}} }}
> >
<Card sx={{ maxWidth: 500, width: "100%" }}> <Card sx={{ maxWidth: 500, width: '100%' }}>
<CardContent sx={{ p: 4 }}> <CardContent sx={{ p: 4 }}>
<Box textAlign="center" mb={3}> <Box textAlign="center" mb={3}>
{status === "pending" && ( {status === 'pending' && (
<> <>
<EmailIcon <EmailIcon sx={{ fontSize: 64, color: 'primary.main', mb: 2 }} />
sx={{ fontSize: 64, color: "primary.main", mb: 2 }}
/>
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>
Verifying Email Verifying Email
</Typography> </Typography>
@ -136,11 +127,9 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
</> </>
)} )}
{status === "success" && ( {status === 'success' && (
<> <>
<CheckCircleIcon <CheckCircleIcon sx={{ fontSize: 64, color: 'success.main', mb: 2 }} />
sx={{ fontSize: 64, color: "success.main", mb: 2 }}
/>
<Typography variant="h4" gutterBottom color="success.main"> <Typography variant="h4" gutterBottom color="success.main">
Email Verified! Email Verified!
</Typography> </Typography>
@ -150,9 +139,9 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
</> </>
)} )}
{status === "error" && ( {status === 'error' && (
<> <>
<ErrorIcon sx={{ fontSize: 64, color: "error.main", mb: 2 }} /> <ErrorIcon sx={{ fontSize: 64, color: 'error.main', mb: 2 }} />
<Typography variant="h4" gutterBottom color="error.main"> <Typography variant="h4" gutterBottom color="error.main">
Verification Failed Verification Failed
</Typography> </Typography>
@ -171,35 +160,25 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
{(message || error) && ( {(message || error) && (
<Alert <Alert
severity={ severity={status === 'success' ? 'success' : status === 'error' ? 'error' : 'info'}
status === "success"
? "success"
: status === "error"
? "error"
: "info"
}
sx={{ mt: 2 }} sx={{ mt: 2 }}
> >
{message || error} {message || error}
</Alert> </Alert>
)} )}
{status === "success" && ( {status === 'success' && (
<Box mt={3} textAlign="center"> <Box mt={3} textAlign="center">
<Typography variant="body2" color="text.secondary" mb={2}> <Typography variant="body2" color="text.secondary" mb={2}>
You will be redirected to the login page in a few seconds... You will be redirected to the login page in a few seconds...
</Typography> </Typography>
<Button <Button variant="contained" onClick={() => navigate('/login')} fullWidth>
variant="contained"
onClick={() => navigate("/login")}
fullWidth
>
Go to Login Go to Login
</Button> </Button>
</Box> </Box>
)} )}
{status === "error" && ( {status === 'error' && (
<Box mt={3}> <Box mt={3}>
<Button <Button
variant="outlined" variant="outlined"
@ -211,11 +190,7 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
> >
Resend Verification Email Resend Verification Email
</Button> </Button>
<Button <Button variant="contained" onClick={() => navigate('/login')} fullWidth>
variant="contained"
onClick={() => navigate("/login")}
fullWidth
>
Back to Login Back to Login
</Button> </Button>
</Box> </Box>
@ -234,11 +209,10 @@ interface MFAVerificationDialogProps {
} }
const MFAVerificationDialog = (props: MFAVerificationDialogProps) => { const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
const { open, onClose, onVerificationSuccess } = props; const { open, onClose, onVerificationSuccess } = props;
const { verifyMFA, resendMFACode, clearMFA, mfaResponse, isLoading, error } = const { verifyMFA, resendMFACode, clearMFA, mfaResponse, isLoading, error } = useAuth();
useAuth(); const [code, setCode] = useState('');
const [code, setCode] = useState("");
const [rememberDevice, setRememberDevice] = useState(false); const [rememberDevice, setRememberDevice] = useState(false);
const [localError, setLocalError] = useState(""); const [localError, setLocalError] = useState('');
const [timeLeft, setTimeLeft] = useState(600); // 10 minutes in seconds const [timeLeft, setTimeLeft] = useState(600); // 10 minutes in seconds
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
@ -247,7 +221,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
return; return;
} }
/* Remove 'HTTP .*: ' from error string */ /* Remove 'HTTP .*: ' from error string */
const jsonStr = error.replace(/^[^{]*/, ""); const jsonStr = error.replace(/^[^{]*/, '');
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
setErrorMessage(data.error.message); setErrorMessage(data.error.message);
}, [error]); }, [error]);
@ -256,10 +230,10 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
if (!open) return; if (!open) return;
const timer = setInterval(() => { const timer = setInterval(() => {
setTimeLeft((prev) => { setTimeLeft(prev => {
if (prev <= 1) { if (prev <= 1) {
clearInterval(timer); clearInterval(timer);
setLocalError("MFA code has expired. Please try logging in again."); setLocalError('MFA code has expired. Please try logging in again.');
return 0; return 0;
} }
return prev - 1; return prev - 1;
@ -272,21 +246,21 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
const formatTime = (seconds: number) => { const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);
const secs = seconds % 60; const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, "0")}`; return `${mins}:${secs.toString().padStart(2, '0')}`;
}; };
const handleVerifyMFA = async () => { const handleVerifyMFA = async () => {
if (!code || code.length !== 6) { if (!code || code.length !== 6) {
setLocalError("Please enter a valid 6-digit code"); setLocalError('Please enter a valid 6-digit code');
return; return;
} }
if (!mfaResponse || !mfaResponse.mfaData) { if (!mfaResponse || !mfaResponse.mfaData) {
setLocalError("MFA data not available"); setLocalError('MFA data not available');
return; return;
} }
setLocalError(""); setLocalError('');
try { try {
const success = await verifyMFA({ const success = await verifyMFA({
@ -301,13 +275,13 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
onClose(); onClose();
} }
} catch (error) { } catch (error) {
setLocalError("Verification failed. Please try again."); setLocalError('Verification failed. Please try again.');
} }
}; };
const handleResendCode = async () => { const handleResendCode = async () => {
if (!mfaResponse || !mfaResponse.mfaData) { if (!mfaResponse || !mfaResponse.mfaData) {
setLocalError("MFA data not available"); setLocalError('MFA data not available');
return; return;
} }
@ -319,11 +293,11 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
); );
if (success) { if (success) {
setTimeLeft(600); // Reset timer setTimeLeft(600); // Reset timer
setLocalError(""); setLocalError('');
alert("New verification code sent to your email"); alert('New verification code sent to your email');
} }
} catch (error) { } catch (error) {
setLocalError("Failed to resend code"); setLocalError('Failed to resend code');
} }
}; };
@ -345,7 +319,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
<DialogContent> <DialogContent>
<Alert severity="info" sx={{ mb: 3 }}> <Alert severity="info" sx={{ mb: 3 }}>
We've detected a login from a new device:{" "} We've detected a login from a new device:{' '}
<strong>{mfaResponse.mfaData.deviceName}</strong> <strong>{mfaResponse.mfaData.deviceName}</strong>
</Alert> </Alert>
@ -360,17 +334,17 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
fullWidth fullWidth
label="Enter 6-digit code" label="Enter 6-digit code"
value={code} value={code}
onChange={(e) => { onChange={e => {
const value = e.target.value.replace(/\D/g, "").slice(0, 6); const value = e.target.value.replace(/\D/g, '').slice(0, 6);
setCode(value); setCode(value);
setLocalError(""); setLocalError('');
}} }}
placeholder="000000" placeholder="000000"
inputProps={{ inputProps={{
maxLength: 6, maxLength: 6,
style: { style: {
fontSize: 24, fontSize: 24,
textAlign: "center", textAlign: 'center',
letterSpacing: 8, letterSpacing: 8,
}, },
}} }}
@ -379,12 +353,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
helperText={localError || errorMessage} helperText={localError || errorMessage}
/> />
<Box <Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
display="flex"
justifyContent="space-between"
alignItems="center"
mb={2}
>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Code expires in: {formatTime(timeLeft)} Code expires in: {formatTime(timeLeft)}
</Typography> </Typography>
@ -401,7 +370,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
control={ control={
<Checkbox <Checkbox
checked={rememberDevice} checked={rememberDevice}
onChange={(e) => setRememberDevice(e.target.checked)} onChange={e => setRememberDevice(e.target.checked)}
/> />
} }
label="Remember this device for 90 days" label="Remember this device for 90 days"
@ -409,8 +378,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
<Alert severity="warning" sx={{ mt: 2 }}> <Alert severity="warning" sx={{ mt: 2 }}>
<Typography variant="body2"> <Typography variant="body2">
If you didn't attempt to log in, please change your password If you didn't attempt to log in, please change your password immediately.
immediately.
</Typography> </Typography>
</Alert> </Alert>
</DialogContent> </DialogContent>
@ -424,7 +392,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
onClick={handleVerifyMFA} onClick={handleVerifyMFA}
disabled={isLoading || !code || code.length !== 6 || timeLeft === 0} disabled={isLoading || !code || code.length !== 6 || timeLeft === 0}
> >
{isLoading ? <CircularProgress size={20} /> : "Verify"} {isLoading ? <CircularProgress size={20} /> : 'Verify'}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@ -444,23 +412,23 @@ const RegistrationSuccessDialog = ({
userType: string; userType: string;
}) => { }) => {
const { resendEmailVerification, isLoading } = useAuth(); const { resendEmailVerification, isLoading } = useAuth();
const [resendMessage, setResendMessage] = useState(""); const [resendMessage, setResendMessage] = useState('');
const handleResendVerification = async () => { const handleResendVerification = async () => {
try { try {
const success = await resendEmailVerification(email); const success = await resendEmailVerification(email);
if (success) { if (success) {
setResendMessage("Verification email sent!"); setResendMessage('Verification email sent!');
} }
} catch (error: any) { } catch (error: any) {
setResendMessage(error?.message || "Network error. Please try again."); setResendMessage(error?.message || 'Network error. Please try again.');
} }
}; };
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogContent sx={{ textAlign: "center", p: 4 }}> <DialogContent sx={{ textAlign: 'center', p: 4 }}>
<EmailIcon sx={{ fontSize: 64, color: "primary.main", mb: 2 }} /> <EmailIcon sx={{ fontSize: 64, color: 'primary.main', mb: 2 }} />
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
Check Your Email Check Your Email
@ -474,7 +442,7 @@ const RegistrationSuccessDialog = ({
{email} {email}
</Typography> </Typography>
<Alert severity="info" sx={{ mt: 2, mb: 3, textAlign: "left" }}> <Alert severity="info" sx={{ mt: 2, mb: 3, textAlign: 'left' }}>
<Typography variant="body2"> <Typography variant="body2">
<strong>Next steps:</strong> <strong>Next steps:</strong>
<br /> <br />
@ -487,22 +455,17 @@ const RegistrationSuccessDialog = ({
</Alert> </Alert>
{resendMessage && ( {resendMessage && (
<Alert <Alert severity={resendMessage.includes('sent') ? 'success' : 'error'} sx={{ mb: 2 }}>
severity={resendMessage.includes("sent") ? "success" : "error"}
sx={{ mb: 2 }}
>
{resendMessage} {resendMessage}
</Alert> </Alert>
)} )}
</DialogContent> </DialogContent>
<DialogActions sx={{ p: 3, justifyContent: "space-between" }}> <DialogActions sx={{ p: 3, justifyContent: 'space-between' }}>
<Button <Button
onClick={handleResendVerification} onClick={handleResendVerification}
disabled={isLoading} disabled={isLoading}
startIcon={ startIcon={isLoading ? <CircularProgress size={16} /> : <RefreshIcon />}
isLoading ? <CircularProgress size={16} /> : <RefreshIcon />
}
> >
Resend Email Resend Email
</Button> </Button>
@ -517,8 +480,8 @@ const RegistrationSuccessDialog = ({
// Enhanced Login Component with MFA Support // Enhanced Login Component with MFA Support
const LoginForm = () => { const LoginForm = () => {
const { login, mfaResponse, isLoading, error, user } = useAuth(); const { login, mfaResponse, isLoading, error, user } = useAuth();
const [email, setEmail] = useState(""); const [email, setEmail] = useState('');
const [password, setPassword] = useState(""); const [password, setPassword] = useState('');
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
@ -528,7 +491,7 @@ const LoginForm = () => {
return; return;
} }
/* Remove 'HTTP .*: ' from error string */ /* Remove 'HTTP .*: ' from error string */
const jsonStr = error.replace(/^[^{]*/, ""); const jsonStr = error.replace(/^[^{]*/, '');
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
setErrorMessage(data.error.message); setErrorMessage(data.error.message);
}, [error]); }, [error]);
@ -555,11 +518,11 @@ const LoginForm = () => {
const handleLoginSuccess = () => { const handleLoginSuccess = () => {
if (!user) { if (!user) {
navigate("/"); navigate('/');
} else { } else {
navigate(`/${user.userType}/dashboard`); navigate(`/${user.userType}/dashboard`);
} }
console.log("Login successful - redirect to dashboard"); console.log('Login successful - redirect to dashboard');
}; };
return ( return (
@ -570,16 +533,16 @@ const LoginForm = () => {
fullWidth fullWidth
label="Email or Username" label="Email or Username"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={e => setEmail(e.target.value)}
autoComplete="email" autoComplete="email"
autoFocus autoFocus
/> />
<TextField <TextField
fullWidth fullWidth
label="Password" label="Password"
type={showPassword ? "text" : "password"} type={showPassword ? 'text' : 'password'}
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
autoComplete="current-password" autoComplete="current-password"
placeholder="Create a strong password" placeholder="Create a strong password"
required required
@ -589,7 +552,7 @@ const LoginForm = () => {
<IconButton <IconButton
aria-label="toggle password visibility" aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
onMouseDown={(e) => e.preventDefault()} onMouseDown={e => e.preventDefault()}
edge="end" edge="end"
> >
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
@ -612,7 +575,7 @@ const LoginForm = () => {
disabled={isLoading} disabled={isLoading}
sx={{ mt: 3, mb: 2 }} sx={{ mt: 3, mb: 2 }}
> >
{isLoading ? <CircularProgress size={20} /> : "Sign In"} {isLoading ? <CircularProgress size={20} /> : 'Sign In'}
</Button> </Button>
{/* MFA Dialog */} {/* MFA Dialog */}
@ -640,19 +603,19 @@ const TrustedDevicesManager = () => {
<Card> <Card>
<CardContent> <CardContent>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
<DevicesIcon sx={{ mr: 1, verticalAlign: "middle" }} /> <DevicesIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Trusted Devices Trusted Devices
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" paragraph> <Typography variant="body2" color="text.secondary" paragraph>
Manage devices that you've marked as trusted. You won't need to verify Manage devices that you've marked as trusted. You won't need to verify your identity when
your identity when signing in from these devices. signing in from these devices.
</Typography> </Typography>
{devices.length === 0 ? ( {devices.length === 0 ? (
<Alert severity="info"> <Alert severity="info">
No trusted devices yet. When you log in from a new device and choose No trusted devices yet. When you log in from a new device and choose to remember it, it
to remember it, it will appear here. will appear here.
</Alert> </Alert>
) : ( ) : (
<Grid container spacing={2}> <Grid container spacing={2}>
@ -660,15 +623,12 @@ const TrustedDevicesManager = () => {
<Grid key={index} size={{ xs: 12, md: 6 }}> <Grid key={index} size={{ xs: 12, md: 6 }}>
<Card variant="outlined"> <Card variant="outlined">
<CardContent> <CardContent>
<Typography variant="subtitle1"> <Typography variant="subtitle1">{device.deviceName}</Typography>
{device.deviceName}
</Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Added: {new Date(device.addedAt).toLocaleDateString()} Added: {new Date(device.addedAt).toLocaleDateString()}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Last used:{" "} Last used: {new Date(device.lastUsed).toLocaleDateString()}
{new Date(device.lastUsed).toLocaleDateString()}
</Typography> </Typography>
<Button <Button
size="small" size="small"

View File

@ -1,5 +1,5 @@
import { styled } from "@mui/material/styles"; import { styled } from '@mui/material/styles';
import IconButton, { IconButtonProps } from "@mui/material/IconButton"; import IconButton, { IconButtonProps } from '@mui/material/IconButton';
interface ExpandMoreProps extends IconButtonProps { interface ExpandMoreProps extends IconButtonProps {
expand: boolean; expand: boolean;
@ -9,21 +9,21 @@ const ExpandMore = styled((props: ExpandMoreProps) => {
const { expand, ...other } = props; const { expand, ...other } = props;
return <IconButton {...other} />; return <IconButton {...other} />;
})(({ theme }) => ({ })(({ theme }) => ({
marginLeft: "auto", marginLeft: 'auto',
transition: theme.transitions.create("transform", { transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest, duration: theme.transitions.duration.shortest,
}), }),
variants: [ variants: [
{ {
props: ({ expand }) => !expand, props: ({ expand }) => !expand,
style: { style: {
transform: "rotate(0deg)", transform: 'rotate(0deg)',
}, },
}, },
{ {
props: ({ expand }) => !!expand, props: ({ expand }) => !!expand,
style: { style: {
transform: "rotate(180deg)", transform: 'rotate(180deg)',
}, },
}, },
], ],

View File

@ -1,11 +1,11 @@
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useState, useRef } from 'react';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import PropagateLoader from "react-spinners/PropagateLoader"; import PropagateLoader from 'react-spinners/PropagateLoader';
import { Quote } from "components/Quote"; import { Quote } from 'components/Quote';
import { BackstoryElementProps } from "components/BackstoryTab"; import { BackstoryElementProps } from 'components/BackstoryTab';
import { Candidate, ChatSession } from "types/types"; import { Candidate, ChatSession } from 'types/types';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
interface GenerateImageProps extends BackstoryElementProps { interface GenerateImageProps extends BackstoryElementProps {
prompt: string; prompt: string;
@ -17,26 +17,23 @@ const GenerateImage = (props: GenerateImageProps) => {
const { chatSession, prompt } = props; const { chatSession, prompt } = props;
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [status, setStatus] = useState<string>(""); const [status, setStatus] = useState<string>('');
const [image, setImage] = useState<string>(""); const [image, setImage] = useState<string>('');
const name = const name = (user?.userType === 'candidate' ? (user as Candidate).username : user?.email) || '';
(user?.userType === "candidate"
? (user as Candidate).username
: user?.email) || "";
// Only keep refs that are truly necessary // Only keep refs that are truly necessary
const controllerRef = useRef<string>(null); const controllerRef = useRef<string>(null);
// Effect to trigger profile generation when user data is ready // Effect to trigger profile generation when user data is ready
useEffect(() => { useEffect(() => {
if (controllerRef.current) { if (controllerRef.current) {
console.log("Controller already active, skipping profile generation"); console.log('Controller already active, skipping profile generation');
return; return;
} }
if (!prompt) { if (!prompt) {
return; return;
} }
setStatus("Starting image generation..."); setStatus('Starting image generation...');
setProcessing(true); setProcessing(true);
const start = Date.now(); const start = Date.now();
@ -95,39 +92,39 @@ const GenerateImage = (props: GenerateImageProps) => {
<Box <Box
className="GenerateImage" className="GenerateImage"
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
gap: 1, gap: 1,
maxWidth: { xs: "100%", md: "700px", lg: "1024px" }, maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
minHeight: "max-content", minHeight: 'max-content',
}} }}
> >
{image !== "" && <img alt={prompt} src={`${image}/${chatSession.id}`} />} {image !== '' && <img alt={prompt} src={`${image}/${chatSession.id}`} />}
{prompt && ( {prompt && (
<Quote <Quote
size={processing ? "normal" : "small"} size={processing ? 'normal' : 'small'}
quote={prompt} quote={prompt}
sx={{ "& *": { color: "#2E2E2E !important" } }} sx={{ '& *': { color: '#2E2E2E !important' } }}
/> />
)} )}
{processing && ( {processing && (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
m: 0, m: 0,
gap: 1, gap: 1,
minHeight: "min-content", minHeight: 'min-content',
mb: 2, mb: 2,
}} }}
> >
{status && ( {status && (
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box sx={{ fontSize: "0.5rem" }}>Generation status</Box> <Box sx={{ fontSize: '0.5rem' }}>Generation status</Box>
<Box sx={{ fontWeight: "bold" }}>{status}</Box> <Box sx={{ fontWeight: 'bold' }}>{status}</Box>
</Box> </Box>
)} )}
<PropagateLoader <PropagateLoader

View File

@ -1,4 +1,4 @@
import React, { useState, useRef, JSX } from "react"; import React, { useState, useRef, JSX } from 'react';
import { import {
Box, Box,
Button, Button,
@ -14,7 +14,7 @@ import {
LinearProgress, LinearProgress,
Stack, Stack,
Paper, Paper,
} from "@mui/material"; } from '@mui/material';
import { import {
SyncAlt, SyncAlt,
Favorite, Favorite,
@ -31,30 +31,30 @@ import {
Work, Work,
CheckCircle, CheckCircle,
Star, Star,
} from "@mui/icons-material"; } from '@mui/icons-material';
import { styled } from "@mui/material/styles"; import { styled } from '@mui/material/styles';
import FileUploadIcon from "@mui/icons-material/FileUpload"; import FileUploadIcon from '@mui/icons-material/FileUpload';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedJob } from "hooks/GlobalContext"; import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
import { BackstoryElementProps } from "./BackstoryTab"; import { BackstoryElementProps } from './BackstoryTab';
import { LoginRequired } from "components/ui/LoginRequired"; import { LoginRequired } from 'components/ui/LoginRequired';
import * as Types from "types/types"; import * as Types from 'types/types';
import { StyledMarkdown } from "./StyledMarkdown"; import { StyledMarkdown } from './StyledMarkdown';
import { JobInfo } from "./ui/JobInfo"; import { JobInfo } from './ui/JobInfo';
import { Scrollable } from "./Scrollable"; import { Scrollable } from './Scrollable';
import { StatusIcon, StatusBox } from "components/ui/StatusIcon"; import { StatusIcon, StatusBox } from 'components/ui/StatusIcon';
const VisuallyHiddenInput = styled("input")({ const VisuallyHiddenInput = styled('input')({
clip: "rect(0 0 0 0)", clip: 'rect(0 0 0 0)',
clipPath: "inset(50%)", clipPath: 'inset(50%)',
height: 1, height: 1,
overflow: "hidden", overflow: 'hidden',
position: "absolute", position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
whiteSpace: "nowrap", whiteSpace: 'nowrap',
width: 1, width: 1,
}); });
@ -62,11 +62,11 @@ const UploadBox = styled(Box)(({ theme }) => ({
border: `2px dashed ${theme.palette.primary.main}`, border: `2px dashed ${theme.palette.primary.main}`,
borderRadius: theme.shape.borderRadius * 2, borderRadius: theme.shape.borderRadius * 2,
padding: theme.spacing(4), padding: theme.spacing(4),
textAlign: "center", textAlign: 'center',
backgroundColor: theme.palette.action.hover, backgroundColor: theme.palette.action.hover,
transition: "all 0.3s ease", transition: 'all 0.3s ease',
cursor: "pointer", cursor: 'pointer',
"&:hover": { '&:hover': {
backgroundColor: theme.palette.action.selected, backgroundColor: theme.palette.action.selected,
borderColor: theme.palette.primary.dark, borderColor: theme.palette.primary.dark,
}, },
@ -81,48 +81,46 @@ const JobCreator = (props: JobCreatorProps) => {
const { selectedJob, setSelectedJob } = useSelectedJob(); const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [jobDescription, setJobDescription] = useState<string>(""); const [jobDescription, setJobDescription] = useState<string>('');
const [jobRequirements, setJobRequirements] = const [jobRequirements, setJobRequirements] = useState<Types.JobRequirements | null>(null);
useState<Types.JobRequirements | null>(null); const [jobTitle, setJobTitle] = useState<string>('');
const [jobTitle, setJobTitle] = useState<string>(""); const [company, setCompany] = useState<string>('');
const [company, setCompany] = useState<string>(""); const [summary, setSummary] = useState<string>('');
const [summary, setSummary] = useState<string>("");
const [job, setJob] = useState<Types.Job | null>(null); const [job, setJob] = useState<Types.Job | null>(null);
const [jobStatus, setJobStatus] = useState<string>(""); const [jobStatus, setJobStatus] = useState<string>('');
const [jobStatusType, setJobStatusType] = const [jobStatusType, setJobStatusType] = useState<Types.ApiActivityType | null>(null);
useState<Types.ApiActivityType | null>(null);
const [isProcessing, setIsProcessing] = useState<boolean>(false); const [isProcessing, setIsProcessing] = useState<boolean>(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const jobStatusHandlers = { const jobStatusHandlers = {
onStatus: (status: Types.ChatMessageStatus) => { onStatus: (status: Types.ChatMessageStatus) => {
console.log("status:", status.content); console.log('status:', status.content);
setJobStatusType(status.activity); setJobStatusType(status.activity);
setJobStatus(status.content); setJobStatus(status.content);
}, },
onMessage: (jobMessage: Types.JobRequirementsMessage) => { onMessage: (jobMessage: Types.JobRequirementsMessage) => {
const job: Types.Job = jobMessage.job; const job: Types.Job = jobMessage.job;
console.log("onMessage - job", job); console.log('onMessage - job', job);
setJob(job); setJob(job);
setCompany(job.company || ""); setCompany(job.company || '');
setJobDescription(job.description); setJobDescription(job.description);
setSummary(job.summary || ""); setSummary(job.summary || '');
setJobTitle(job.title || ""); setJobTitle(job.title || '');
setJobRequirements(job.requirements || null); setJobRequirements(job.requirements || null);
setJobStatusType(null); setJobStatusType(null);
setJobStatus(""); setJobStatus('');
}, },
onError: (error: Types.ChatMessageError) => { onError: (error: Types.ChatMessageError) => {
console.log("onError", error); console.log('onError', error);
setSnack(error.content, "error"); setSnack(error.content, 'error');
setIsProcessing(false); setIsProcessing(false);
}, },
onComplete: () => { onComplete: () => {
setJobStatusType(null); setJobStatusType(null);
setJobStatus(""); setJobStatus('');
setIsProcessing(false); setIsProcessing(false);
}, },
}; };
@ -130,47 +128,44 @@ const JobCreator = (props: JobCreatorProps) => {
const handleJobUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleJobUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) { if (e.target.files && e.target.files[0]) {
const file = e.target.files[0]; const file = e.target.files[0];
const fileExtension = "." + file.name.split(".").pop()?.toLowerCase(); const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
let docType: Types.DocumentType | null = null; let docType: Types.DocumentType | null = null;
switch (fileExtension.substring(1)) { switch (fileExtension.substring(1)) {
case "pdf": case 'pdf':
docType = "pdf"; docType = 'pdf';
break; break;
case "docx": case 'docx':
docType = "docx"; docType = 'docx';
break; break;
case "md": case 'md':
docType = "markdown"; docType = 'markdown';
break; break;
case "txt": case 'txt':
docType = "txt"; docType = 'txt';
break; break;
} }
if (!docType) { if (!docType) {
setSnack( setSnack('Invalid file type. Please upload .txt, .md, .docx, or .pdf files only.', 'error');
"Invalid file type. Please upload .txt, .md, .docx, or .pdf files only.",
"error"
);
return; return;
} }
try { try {
setIsProcessing(true); setIsProcessing(true);
setJobDescription(""); setJobDescription('');
setJobTitle(""); setJobTitle('');
setJobRequirements(null); setJobRequirements(null);
setSummary(""); setSummary('');
const controller = apiClient.createJobFromFile(file, jobStatusHandlers); const controller = apiClient.createJobFromFile(file, jobStatusHandlers);
const job = await controller.promise; const job = await controller.promise;
if (!job) { if (!job) {
return; return;
} }
console.log(`Job id: ${job.id}`); console.log(`Job id: ${job.id}`);
e.target.value = ""; e.target.value = '';
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setSnack("Failed to upload document", "error"); setSnack('Failed to upload document', 'error');
setIsProcessing(false); setIsProcessing(false);
} }
} }
@ -190,24 +185,16 @@ const JobCreator = (props: JobCreatorProps) => {
return ( return (
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Box sx={{ display: "flex", alignItems: "center", mb: 1.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}>
{icon} {icon}
<Typography variant="subtitle1" sx={{ ml: 1, fontWeight: 600 }}> <Typography variant="subtitle1" sx={{ ml: 1, fontWeight: 600 }}>
{title} {title}
</Typography> </Typography>
{required && ( {required && <Chip label="Required" size="small" color="error" sx={{ ml: 1 }} />}
<Chip label="Required" size="small" color="error" sx={{ ml: 1 }} />
)}
</Box> </Box>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap> <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{items.map((item, index) => ( {items.map((item, index) => (
<Chip <Chip key={index} label={item} variant="outlined" size="small" sx={{ mb: 1 }} />
key={index}
label={item}
variant="outlined"
size="small"
sx={{ mb: 1 }}
/>
))} ))}
</Stack> </Stack>
</Box> </Box>
@ -226,49 +213,49 @@ const JobCreator = (props: JobCreatorProps) => {
/> />
<CardContent sx={{ pt: 0 }}> <CardContent sx={{ pt: 0 }}>
{renderRequirementSection( {renderRequirementSection(
"Technical Skills (Required)", 'Technical Skills (Required)',
jobRequirements.technicalSkills.required, jobRequirements.technicalSkills.required,
<Build color="primary" />, <Build color="primary" />,
true true
)} )}
{renderRequirementSection( {renderRequirementSection(
"Technical Skills (Preferred)", 'Technical Skills (Preferred)',
jobRequirements.technicalSkills.preferred, jobRequirements.technicalSkills.preferred,
<Build color="action" /> <Build color="action" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience Requirements (Required)", 'Experience Requirements (Required)',
jobRequirements.experienceRequirements.required, jobRequirements.experienceRequirements.required,
<Work color="primary" />, <Work color="primary" />,
true true
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience Requirements (Preferred)", 'Experience Requirements (Preferred)',
jobRequirements.experienceRequirements.preferred, jobRequirements.experienceRequirements.preferred,
<Work color="action" /> <Work color="action" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Soft Skills", 'Soft Skills',
jobRequirements.softSkills, jobRequirements.softSkills,
<Psychology color="secondary" /> <Psychology color="secondary" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience", 'Experience',
jobRequirements.experience, jobRequirements.experience,
<Star color="warning" /> <Star color="warning" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Education", 'Education',
jobRequirements.education, jobRequirements.education,
<Description color="info" /> <Description color="info" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Certifications", 'Certifications',
jobRequirements.certifications, jobRequirements.certifications,
<CheckCircle color="success" /> <CheckCircle color="success" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Preferred Attributes", 'Preferred Attributes',
jobRequirements.preferredAttributes, jobRequirements.preferredAttributes,
<Star color="secondary" /> <Star color="secondary" />
)} )}
@ -279,8 +266,8 @@ const JobCreator = (props: JobCreatorProps) => {
const handleSave = async () => { const handleSave = async () => {
const newJob: Types.Job = { const newJob: Types.Job = {
ownerId: user?.id || "", ownerId: user?.id || '',
ownerType: "candidate", ownerType: 'candidate',
description: jobDescription, description: jobDescription,
company: company, company: company,
summary: summary, summary: summary,
@ -293,7 +280,7 @@ const JobCreator = (props: JobCreatorProps) => {
const job = await apiClient.createJob(newJob); const job = await apiClient.createJob(newJob);
setIsProcessing(false); setIsProcessing(false);
if (!job) { if (!job) {
setSnack("Failed to save job", "error"); setSnack('Failed to save job', 'error');
return; return;
} }
onSave && onSave(job); onSave && onSave(job);
@ -302,10 +289,7 @@ const JobCreator = (props: JobCreatorProps) => {
const handleExtractRequirements = async () => { const handleExtractRequirements = async () => {
try { try {
setIsProcessing(true); setIsProcessing(true);
const controller = apiClient.createJobFromDescription( const controller = apiClient.createJobFromDescription(jobDescription, jobStatusHandlers);
jobDescription,
jobStatusHandlers
);
const job = await controller.promise; const job = await controller.promise;
if (!job) { if (!job) {
setIsProcessing(false); setIsProcessing(false);
@ -314,7 +298,7 @@ const JobCreator = (props: JobCreatorProps) => {
console.log(`Job id: ${job.id}`); console.log(`Job id: ${job.id}`);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setSnack("Failed to upload document", "error"); setSnack('Failed to upload document', 'error');
setIsProcessing(false); setIsProcessing(false);
} }
setIsProcessing(false); setIsProcessing(false);
@ -324,7 +308,7 @@ const JobCreator = (props: JobCreatorProps) => {
return ( return (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
p: 1, p: 1,
}} }}
> >
@ -341,23 +325,17 @@ const JobCreator = (props: JobCreatorProps) => {
<Typography <Typography
variant="h6" variant="h6"
gutterBottom gutterBottom
sx={{ display: "flex", alignItems: "center" }} sx={{ display: 'flex', alignItems: 'center' }}
> >
<CloudUpload sx={{ mr: 1 }} /> <CloudUpload sx={{ mr: 1 }} />
Upload Job Description Upload Job Description
</Typography> </Typography>
<UploadBox onClick={handleUploadClick}> <UploadBox onClick={handleUploadClick}>
<CloudUpload <CloudUpload sx={{ fontSize: 48, color: 'primary.main', mb: 2 }} />
sx={{ fontSize: 48, color: "primary.main", mb: 2 }}
/>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Drop your job description here Drop your job description here
</Typography> </Typography>
<Typography <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
variant="body2"
color="text.secondary"
sx={{ mb: 2 }}
>
Supported formats: PDF, DOCX, TXT, MD Supported formats: PDF, DOCX, TXT, MD
</Typography> </Typography>
<Button <Button
@ -381,7 +359,7 @@ const JobCreator = (props: JobCreatorProps) => {
<Typography <Typography
variant="h6" variant="h6"
gutterBottom gutterBottom
sx={{ display: "flex", alignItems: "center" }} sx={{ display: 'flex', alignItems: 'center' }}
> >
<Description sx={{ mr: 1 }} /> <Description sx={{ mr: 1 }} />
Or Enter Manually Or Enter Manually
@ -393,7 +371,7 @@ const JobCreator = (props: JobCreatorProps) => {
placeholder="Paste or type the job description here..." placeholder="Paste or type the job description here..."
variant="outlined" variant="outlined"
value={jobDescription} value={jobDescription}
onChange={(e) => setJobDescription(e.target.value)} onChange={e => setJobDescription(e.target.value)}
disabled={isProcessing} disabled={isProcessing}
sx={{ mb: 2 }} sx={{ mb: 2 }}
/> />
@ -416,7 +394,7 @@ const JobCreator = (props: JobCreatorProps) => {
<StatusBox> <StatusBox>
{jobStatusType && <StatusIcon type={jobStatusType} />} {jobStatusType && <StatusIcon type={jobStatusType} />}
<Typography variant="body2" sx={{ ml: 1 }}> <Typography variant="body2" sx={{ ml: 1 }}>
{jobStatus || "Processing..."} {jobStatus || 'Processing...'}
</Typography> </Typography>
</StatusBox> </StatusBox>
{isProcessing && <LinearProgress sx={{ mt: 1 }} />} {isProcessing && <LinearProgress sx={{ mt: 1 }} />}
@ -440,13 +418,11 @@ const JobCreator = (props: JobCreatorProps) => {
label="Job Title" label="Job Title"
variant="outlined" variant="outlined"
value={jobTitle} value={jobTitle}
onChange={(e) => setJobTitle(e.target.value)} onChange={e => setJobTitle(e.target.value)}
required required
disabled={isProcessing} disabled={isProcessing}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: <Work sx={{ mr: 1, color: 'text.secondary' }} />,
<Work sx={{ mr: 1, color: "text.secondary" }} />
),
}} }}
/> />
</Grid> </Grid>
@ -457,13 +433,11 @@ const JobCreator = (props: JobCreatorProps) => {
label="Company" label="Company"
variant="outlined" variant="outlined"
value={company} value={company}
onChange={(e) => setCompany(e.target.value)} onChange={e => setCompany(e.target.value)}
required required
disabled={isProcessing} disabled={isProcessing}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: <Business sx={{ mr: 1, color: 'text.secondary' }} />,
<Business sx={{ mr: 1, color: "text.secondary" }} />
),
}} }}
/> />
</Grid> </Grid>
@ -486,7 +460,7 @@ const JobCreator = (props: JobCreatorProps) => {
</Card> </Card>
{/* Job Summary */} {/* Job Summary */}
{summary !== "" && ( {summary !== '' && (
<Card elevation={2} sx={{ mt: 3 }}> <Card elevation={2} sx={{ mt: 3 }}>
<CardHeader <CardHeader
title="Job Summary" title="Job Summary"
@ -507,70 +481,68 @@ const JobCreator = (props: JobCreatorProps) => {
<Box <Box
className="JobManagement" className="JobManagement"
sx={{ sx={{
background: "white", background: 'white',
p: 0, p: 0,
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
}} }}
> >
{job === null && renderJobCreation()} {job === null && renderJobCreation()}
{job && ( {job && (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
height: "100%" /* Restrict to main-container's height */, height: '100%' /* Restrict to main-container's height */,
width: "100%", width: '100%',
minHeight: 0 /* Prevent flex overflow */, minHeight: 0 /* Prevent flex overflow */,
maxHeight: "min-content", maxHeight: 'min-content',
position: "relative", position: 'relative',
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
flexGrow: 1, flexGrow: 1,
gap: 1, gap: 1,
height: "100%" /* Restrict to main-container's height */, height: '100%' /* Restrict to main-container's height */,
width: "100%", width: '100%',
minHeight: 0 /* Prevent flex overflow */, minHeight: 0 /* Prevent flex overflow */,
maxHeight: "min-content", maxHeight: 'min-content',
"& > *:not(.Scrollable)": { '& > *:not(.Scrollable)': {
flexShrink: 0 /* Prevent shrinking */, flexShrink: 0 /* Prevent shrinking */,
}, },
position: "relative", position: 'relative',
}} }}
> >
<Scrollable <Scrollable
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
position: "relative", position: 'relative',
maxHeight: "30rem", maxHeight: '30rem',
}} }}
> >
<JobInfo job={job} /> <JobInfo job={job} />
</Scrollable> </Scrollable>
<Scrollable <Scrollable
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
position: "relative", position: 'relative',
maxHeight: "30rem", maxHeight: '30rem',
}} }}
> >
<StyledMarkdown content={job.description} /> <StyledMarkdown content={job.description} />
</Scrollable> </Scrollable>
</Box> </Box>
<Box sx={{ display: "flex", gap: 2, alignItems: "flex-end" }}> <Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-end' }}>
<Button <Button
variant="contained" variant="contained"
onClick={handleSave} onClick={handleSave}
disabled={ disabled={!jobTitle || !company || !jobDescription || isProcessing}
!jobTitle || !company || !jobDescription || isProcessing
}
fullWidth={isMobile} fullWidth={isMobile}
size="large" size="large"
startIcon={<CheckCircle />} startIcon={<CheckCircle />}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { import {
Box, Box,
Typography, Typography,
@ -16,12 +16,12 @@ import {
LinearProgress, LinearProgress,
useMediaQuery, useMediaQuery,
Button, Button,
} from "@mui/material"; } from '@mui/material';
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from "@mui/icons-material/Error"; import ErrorIcon from '@mui/icons-material/Error';
import PendingIcon from "@mui/icons-material/Pending"; import PendingIcon from '@mui/icons-material/Pending';
import WarningIcon from "@mui/icons-material/Warning"; import WarningIcon from '@mui/icons-material/Warning';
import { import {
Candidate, Candidate,
ChatMessage, ChatMessage,
@ -34,32 +34,32 @@ import {
JobRequirements, JobRequirements,
SkillAssessment, SkillAssessment,
SkillStatus, SkillStatus,
} from "types/types"; } from 'types/types';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { BackstoryPageProps } from "./BackstoryTab"; import { BackstoryPageProps } from './BackstoryTab';
import { Job } from "types/types"; import { Job } from 'types/types';
import { StyledMarkdown } from "./StyledMarkdown"; import { StyledMarkdown } from './StyledMarkdown';
import { Scrollable } from "./Scrollable"; import { Scrollable } from './Scrollable';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
import * as Types from "types/types"; import * as Types from 'types/types';
import JsonView from "@uiw/react-json-view"; import JsonView from '@uiw/react-json-view';
import { VectorVisualizer } from "./VectorVisualizer"; import { VectorVisualizer } from './VectorVisualizer';
import { JobInfo } from "./ui/JobInfo"; import { JobInfo } from './ui/JobInfo';
interface JobAnalysisProps extends BackstoryPageProps { interface JobAnalysisProps extends BackstoryPageProps {
job: Job; job: Job;
candidate: Candidate; candidate: Candidate;
variant?: "small" | "normal"; variant?: 'small' | 'normal';
onAnalysisComplete: (skills: SkillAssessment[]) => void; onAnalysisComplete: (skills: SkillAssessment[]) => void;
} }
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
status: "done", status: 'done',
type: "text", type: 'text',
sessionId: "", sessionId: '',
timestamp: new Date(), timestamp: new Date(),
content: "", content: '',
role: "assistant", role: 'assistant',
metadata: null as any, metadata: null as any,
}; };
@ -69,32 +69,25 @@ interface SkillMatch extends SkillAssessment {
matchScore: number; matchScore: number;
} }
const JobMatchAnalysis: React.FC<JobAnalysisProps> = ( const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) => {
props: JobAnalysisProps const { job, candidate, onAnalysisComplete, variant = 'normal' } = props;
) => {
const { job, candidate, onAnalysisComplete, variant = "normal" } = props;
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const theme = useTheme(); const theme = useTheme();
const [requirements, setRequirements] = useState< const [requirements, setRequirements] = useState<{ requirement: string; domain: string }[]>([]);
{ requirement: string; domain: string }[]
>([]);
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]); const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
const [creatingSession, setCreatingSession] = useState<boolean>(false); const [creatingSession, setCreatingSession] = useState<boolean>(false);
const [loadingRequirements, setLoadingRequirements] = const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false);
useState<boolean>(false);
const [expanded, setExpanded] = useState<string | false>(false); const [expanded, setExpanded] = useState<string | false>(false);
const [overallScore, setOverallScore] = useState<number>(0); const [overallScore, setOverallScore] = useState<number>(0);
const [requirementsSession, setRequirementsSession] = const [requirementsSession, setRequirementsSession] = useState<ChatSession | null>(null);
useState<ChatSession | null>(null);
const [statusMessage, setStatusMessage] = useState<ChatMessage | null>(null); const [statusMessage, setStatusMessage] = useState<ChatMessage | null>(null);
const [startAnalysis, setStartAnalysis] = useState<boolean>(false); const [startAnalysis, setStartAnalysis] = useState<boolean>(false);
const [analyzing, setAnalyzing] = useState<boolean>(false); const [analyzing, setAnalyzing] = useState<boolean>(false);
const [matchStatus, setMatchStatus] = useState<string>(""); const [matchStatus, setMatchStatus] = useState<string>('');
const [matchStatusType, setMatchStatusType] = const [matchStatusType, setMatchStatusType] = useState<Types.ApiActivityType | null>(null);
useState<Types.ApiActivityType | null>(null);
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
// Handle accordion expansion // Handle accordion expansion
const handleAccordionChange = const handleAccordionChange =
@ -108,66 +101,66 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
} }
const requirements: { requirement: string; domain: string }[] = []; const requirements: { requirement: string; domain: string }[] = [];
if (job.requirements?.technicalSkills) { if (job.requirements?.technicalSkills) {
job.requirements.technicalSkills.required?.forEach((req) => job.requirements.technicalSkills.required?.forEach(req =>
requirements.push({ requirements.push({
requirement: req, requirement: req,
domain: "Technical Skills (required)", domain: 'Technical Skills (required)',
}) })
); );
job.requirements.technicalSkills.preferred?.forEach((req) => job.requirements.technicalSkills.preferred?.forEach(req =>
requirements.push({ requirements.push({
requirement: req, requirement: req,
domain: "Technical Skills (preferred)", domain: 'Technical Skills (preferred)',
}) })
); );
} }
if (job.requirements?.experienceRequirements) { if (job.requirements?.experienceRequirements) {
job.requirements.experienceRequirements.required?.forEach((req) => job.requirements.experienceRequirements.required?.forEach(req =>
requirements.push({ requirement: req, domain: "Experience (required)" }) requirements.push({ requirement: req, domain: 'Experience (required)' })
); );
job.requirements.experienceRequirements.preferred?.forEach((req) => job.requirements.experienceRequirements.preferred?.forEach(req =>
requirements.push({ requirements.push({
requirement: req, requirement: req,
domain: "Experience (preferred)", domain: 'Experience (preferred)',
}) })
); );
} }
if (job.requirements?.softSkills) { if (job.requirements?.softSkills) {
job.requirements.softSkills.forEach((req) => job.requirements.softSkills.forEach(req =>
requirements.push({ requirement: req, domain: "Soft Skills" }) requirements.push({ requirement: req, domain: 'Soft Skills' })
); );
} }
if (job.requirements?.experience) { if (job.requirements?.experience) {
job.requirements.experience.forEach((req) => job.requirements.experience.forEach(req =>
requirements.push({ requirement: req, domain: "Experience" }) requirements.push({ requirement: req, domain: 'Experience' })
); );
} }
if (job.requirements?.education) { if (job.requirements?.education) {
job.requirements.education.forEach((req) => job.requirements.education.forEach(req =>
requirements.push({ requirement: req, domain: "Education" }) requirements.push({ requirement: req, domain: 'Education' })
); );
} }
if (job.requirements?.certifications) { if (job.requirements?.certifications) {
job.requirements.certifications.forEach((req) => job.requirements.certifications.forEach(req =>
requirements.push({ requirement: req, domain: "Certifications" }) requirements.push({ requirement: req, domain: 'Certifications' })
); );
} }
if (job.requirements?.preferredAttributes) { if (job.requirements?.preferredAttributes) {
job.requirements.preferredAttributes.forEach((req) => job.requirements.preferredAttributes.forEach(req =>
requirements.push({ requirement: req, domain: "Preferred Attributes" }) requirements.push({ requirement: req, domain: 'Preferred Attributes' })
); );
} }
const initialSkillMatches: SkillMatch[] = requirements.map((req) => ({ const initialSkillMatches: SkillMatch[] = requirements.map(req => ({
skill: req.requirement, skill: req.requirement,
skillModified: req.requirement, skillModified: req.requirement,
candidateId: candidate.id || "", candidateId: candidate.id || '',
domain: req.domain, domain: req.domain,
status: "waiting" as const, status: 'waiting' as const,
assessment: "", assessment: '',
description: "", description: '',
evidenceFound: false, evidenceFound: false,
evidenceStrength: "none", evidenceStrength: 'none',
evidenceDetails: [], evidenceDetails: [],
matchScore: 0, matchScore: 0,
})); }));
@ -202,38 +195,38 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
// Process requirements one by one // Process requirements one by one
for (let i = 0; i < requirements.length; i++) { for (let i = 0; i < requirements.length; i++) {
try { try {
setSkillMatches((prev) => { setSkillMatches(prev => {
const updated = [...prev]; const updated = [...prev];
updated[i] = { ...updated[i], status: "pending" }; updated[i] = { ...updated[i], status: 'pending' };
return updated; return updated;
}); });
const request: any = await apiClient.candidateMatchForRequirement( const request: any = await apiClient.candidateMatchForRequirement(
candidate.id || "", candidate.id || '',
requirements[i].requirement, requirements[i].requirement,
skillMatchHandlers skillMatchHandlers
); );
const result = await request.promise; const result = await request.promise;
const skillMatch = result.skillAssessment; const skillMatch = result.skillAssessment;
skills.push(skillMatch); skills.push(skillMatch);
setMatchStatus(""); setMatchStatus('');
let matchScore = 0; let matchScore = 0;
switch (skillMatch.evidenceStrength.toUpperCase()) { switch (skillMatch.evidenceStrength.toUpperCase()) {
case "STRONG": case 'STRONG':
matchScore = 100; matchScore = 100;
break; break;
case "MODERATE": case 'MODERATE':
matchScore = 75; matchScore = 75;
break; break;
case "WEAK": case 'WEAK':
matchScore = 50; matchScore = 50;
break; break;
case "NONE": case 'NONE':
matchScore = 0; matchScore = 0;
break; break;
} }
if ( if (
skillMatch.evidenceStrength == "NONE" && skillMatch.evidenceStrength == 'NONE' &&
skillMatch.citations && skillMatch.citations &&
skillMatch.citations.length > 3 skillMatch.citations.length > 3
) { ) {
@ -241,42 +234,35 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
} }
const match: SkillMatch = { const match: SkillMatch = {
...skillMatch, ...skillMatch,
status: "complete", status: 'complete',
matchScore, matchScore,
domain: requirements[i].domain, domain: requirements[i].domain,
}; };
setSkillMatches((prev) => { setSkillMatches(prev => {
const updated = [...prev]; const updated = [...prev];
updated[i] = match; updated[i] = match;
return updated; return updated;
}); });
// Update overall score // Update overall score
setSkillMatches((current) => { setSkillMatches(current => {
const completedMatches = current.filter( const completedMatches = current.filter(match => match.status === 'complete');
(match) => match.status === "complete"
);
if (completedMatches.length > 0) { if (completedMatches.length > 0) {
const newOverallScore = const newOverallScore =
completedMatches.reduce( completedMatches.reduce((sum, match) => sum + match.matchScore, 0) /
(sum, match) => sum + match.matchScore, completedMatches.length;
0
) / completedMatches.length;
setOverallScore(newOverallScore); setOverallScore(newOverallScore);
} }
return current; return current;
}); });
} catch (error) { } catch (error) {
console.error( console.error(`Error fetching match for requirement ${requirements[i]}:`, error);
`Error fetching match for requirement ${requirements[i]}:`, setSkillMatches(prev => {
error
);
setSkillMatches((prev) => {
const updated = [...prev]; const updated = [...prev];
updated[i] = { updated[i] = {
...updated[i], ...updated[i],
status: "error", status: 'error',
assessment: "Failed to analyze this requirement.", assessment: 'Failed to analyze this requirement.',
}; };
return updated; return updated;
}); });
@ -291,14 +277,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
setStartAnalysis(false); setStartAnalysis(false);
onAnalysisComplete && onAnalysisComplete(skills); onAnalysisComplete && onAnalysisComplete(skills);
}); });
}, [ }, [job, onAnalysisComplete, startAnalysis, analyzing, requirements, loadingRequirements]);
job,
onAnalysisComplete,
startAnalysis,
analyzing,
requirements,
loadingRequirements,
]);
// Get color based on match score // Get color based on match score
const getMatchColor = (score: number): string => { const getMatchColor = (score: number): string => {
@ -310,8 +289,8 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
// Get icon based on status // Get icon based on status
const getStatusIcon = (status: string, score: number) => { const getStatusIcon = (status: string, score: number) => {
if (status === "pending" || status === "waiting") return <PendingIcon />; if (status === 'pending' || status === 'waiting') return <PendingIcon />;
if (status === "error") return <ErrorIcon color="error" />; if (status === 'error') return <ErrorIcon color="error" />;
if (score >= 70) return <CheckCircleIcon color="success" />; if (score >= 70) return <CheckCircleIcon color="success" />;
if (score >= 40) return <WarningIcon color="warning" />; if (score >= 40) return <WarningIcon color="warning" />;
return <ErrorIcon color="error" />; return <ErrorIcon color="error" />;
@ -323,23 +302,23 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
}; };
return ( return (
<Box sx={{ display: "flex", flexDirection: "column", m: 0, p: 0 }}> <Box sx={{ display: 'flex', flexDirection: 'column', m: 0, p: 0 }}>
{variant !== "small" && <JobInfo job={job} variant="normal" />} {variant !== 'small' && <JobInfo job={job} variant="normal" />}
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
alignItems: "center", alignItems: 'center',
mb: isMobile ? 1 : 2, mb: isMobile ? 1 : 2,
gap: 1, gap: 1,
justifyContent: "space-between", justifyContent: 'space-between',
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: isMobile ? "column" : "row", flexDirection: isMobile ? 'column' : 'row',
flexGrow: 1, flexGrow: 1,
gap: 1, gap: 1,
}} }}
@ -351,8 +330,8 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
</Typography> </Typography>
<Box <Box
sx={{ sx={{
position: "relative", position: 'relative',
display: "inline-flex", display: 'inline-flex',
mr: 2, mr: 2,
}} }}
> >
@ -371,17 +350,13 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
left: 0, left: 0,
bottom: 0, bottom: 0,
right: 0, right: 0,
position: "absolute", position: 'absolute',
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
<Typography <Typography variant="caption" component="div" sx={{ fontWeight: 'bold' }}>
variant="caption"
component="div"
sx={{ fontWeight: "bold" }}
>
{`${Math.round(overallScore)}%`} {`${Math.round(overallScore)}%`}
</Typography> </Typography>
</Box> </Box>
@ -389,34 +364,34 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
<Chip <Chip
label={ label={
overallScore >= 80 overallScore >= 80
? "Excellent Match" ? 'Excellent Match'
: overallScore >= 60 : overallScore >= 60
? "Good Match" ? 'Good Match'
: overallScore >= 40 : overallScore >= 40
? "Partial Match" ? 'Partial Match'
: "Low Match" : 'Low Match'
} }
sx={{ sx={{
bgcolor: getMatchColor(overallScore), bgcolor: getMatchColor(overallScore),
color: "white", color: 'white',
fontWeight: "bold", fontWeight: 'bold',
}} }}
/> />
</> </>
)} )}
</Box> </Box>
<Button <Button
sx={{ marginLeft: "auto" }} sx={{ marginLeft: 'auto' }}
disabled={analyzing || startAnalysis} disabled={analyzing || startAnalysis}
onClick={beginAnalysis} onClick={beginAnalysis}
variant="contained" variant="contained"
> >
{analyzing ? "Assessment in Progress" : "Start Skill Assessment"} {analyzing ? 'Assessment in Progress' : 'Start Skill Assessment'}
</Button> </Button>
</Box> </Box>
{loadingRequirements ? ( {loadingRequirements ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
<CircularProgress /> <CircularProgress />
<Typography variant="h6" sx={{ ml: 2 }}> <Typography variant="h6" sx={{ ml: 2 }}>
Analyzing job requirements... Analyzing job requirements...
@ -435,9 +410,9 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
onChange={handleAccordionChange(`panel${index}`)} onChange={handleAccordionChange(`panel${index}`)}
sx={{ sx={{
mb: 2, mb: 2,
border: "1px solid", border: '1px solid',
borderColor: borderColor:
match.status === "complete" match.status === 'complete'
? getMatchColor(match.matchScore) ? getMatchColor(match.matchScore)
: theme.palette.divider, : theme.palette.divider,
}} }}
@ -448,25 +423,25 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
id={`panel${index}bh-header`} id={`panel${index}bh-header`}
sx={{ sx={{
bgcolor: bgcolor:
match.status === "complete" match.status === 'complete'
? `${getMatchColor(match.matchScore)}22` // Add transparency ? `${getMatchColor(match.matchScore)}22` // Add transparency
: "inherit", : 'inherit',
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
width: "100%", width: '100%',
justifyContent: "space-between", justifyContent: 'space-between',
}} }}
> >
<Box sx={{ display: "flex", alignItems: "center" }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
{getStatusIcon(match.status, match.matchScore)} {getStatusIcon(match.status, match.matchScore)}
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: 0, gap: 0,
p: 0, p: 0,
m: 0, m: 0,
@ -476,48 +451,45 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
sx={{ sx={{
ml: 1, ml: 1,
mb: 0, mb: 0,
fontWeight: "medium", fontWeight: 'medium',
marginBottom: "0px !important", marginBottom: '0px !important',
}} }}
> >
{match.skill} {match.skill}
</Typography> </Typography>
<Typography <Typography variant="caption" sx={{ ml: 1, fontWeight: 'light' }}>
variant="caption"
sx={{ ml: 1, fontWeight: "light" }}
>
{match.domain} {match.domain}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
{match.status === "complete" ? ( {match.status === 'complete' ? (
<Chip <Chip
label={`${match.matchScore}% Match`} label={`${match.matchScore}% Match`}
size="small" size="small"
sx={{ sx={{
bgcolor: getMatchColor(match.matchScore), bgcolor: getMatchColor(match.matchScore),
color: "white", color: 'white',
minWidth: 90, minWidth: 90,
}} }}
/> />
) : match.status === "waiting" ? ( ) : match.status === 'waiting' ? (
<Chip <Chip
label="Waiting..." label="Waiting..."
size="small" size="small"
sx={{ sx={{
bgcolor: "rgb(189, 173, 85)", bgcolor: 'rgb(189, 173, 85)',
color: "white", color: 'white',
minWidth: 90, minWidth: 90,
}} }}
/> />
) : match.status === "pending" ? ( ) : match.status === 'pending' ? (
<Chip <Chip
label="Analyzing..." label="Analyzing..."
size="small" size="small"
sx={{ sx={{
bgcolor: theme.palette.grey[400], bgcolor: theme.palette.grey[400],
color: "white", color: 'white',
minWidth: 90, minWidth: 90,
}} }}
/> />
@ -527,7 +499,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
size="small" size="small"
sx={{ sx={{
bgcolor: theme.palette.error.main, bgcolor: theme.palette.error.main,
color: "white", color: 'white',
minWidth: 90, minWidth: 90,
}} }}
/> />
@ -536,18 +508,16 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
{match.status === "pending" ? ( {match.status === 'pending' ? (
<Box sx={{ width: "100%", p: 2 }}> <Box sx={{ width: '100%', p: 2 }}>
<LinearProgress /> <LinearProgress />
<Typography sx={{ mt: 2 }}> <Typography sx={{ mt: 2 }}>
Analyzing candidate's match for this requirement...{" "} Analyzing candidate's match for this requirement... {matchStatus}
{matchStatus}
</Typography> </Typography>
</Box> </Box>
) : match.status === "error" ? ( ) : match.status === 'error' ? (
<Typography color="error"> <Typography color="error">
{match.assessment || {match.assessment || 'An error occurred while analyzing this requirement.'}
"An error occurred while analyzing this requirement."}
</Typography> </Typography>
) : ( ) : (
<Box> <Box>
@ -562,15 +532,14 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Supporting Evidence Supporting Evidence
</Typography> </Typography>
{match.evidenceDetails && {match.evidenceDetails && match.evidenceDetails.length > 0 ? (
match.evidenceDetails.length > 0 ? (
match.evidenceDetails.map((evidence, evndex) => ( match.evidenceDetails.map((evidence, evndex) => (
<Card <Card
key={evndex} key={evndex}
variant="outlined" variant="outlined"
sx={{ sx={{
mb: 2, mb: 2,
borderLeft: "4px solid", borderLeft: '4px solid',
borderColor: theme.palette.primary.main, borderColor: theme.palette.primary.main,
}} }}
> >
@ -578,28 +547,22 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (
<Typography <Typography
variant="body1" variant="body1"
component="div" component="div"
sx={{ mb: 1, fontStyle: "italic" }} sx={{ mb: 1, fontStyle: 'italic' }}
> >
"{evidence.quote}" "{evidence.quote}"
</Typography> </Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "flex-start", alignItems: 'flex-start',
flexDirection: "column", flexDirection: 'column',
}} }}
> >
<Typography <Typography variant="body2" color="text.secondary">
variant="body2"
color="text.secondary"
>
Relevance: {evidence.context} Relevance: {evidence.context}
</Typography> </Typography>
<Typography <Typography variant="caption" color="text.secondary">
variant="caption"
color="text.secondary"
>
Source: {evidence.source} Source: {evidence.source}
</Typography> </Typography>
{/* <Chip {/* <Chip

View File

@ -1,20 +1,13 @@
import React from "react"; import React from 'react';
import { import { Box, CircularProgress, Typography, Grid, LinearProgress, Fade } from '@mui/material';
Box, import { styled } from '@mui/material/styles';
CircularProgress,
Typography,
Grid,
LinearProgress,
Fade,
} from "@mui/material";
import { styled } from "@mui/material/styles";
// Types for props // Types for props
interface LoadingComponentProps { interface LoadingComponentProps {
/** Text to display while loading */ /** Text to display while loading */
loadingText?: string; loadingText?: string;
/** Type of loader to show */ /** Type of loader to show */
loaderType?: "circular" | "linear"; loaderType?: 'circular' | 'linear';
/** Whether to show with fade-in animation */ /** Whether to show with fade-in animation */
withFade?: boolean; withFade?: boolean;
/** Duration of fade-in animation in ms */ /** Duration of fade-in animation in ms */
@ -23,37 +16,37 @@ interface LoadingComponentProps {
// Styled components // Styled components
const LoadingContainer = styled(Box)(({ theme }) => ({ const LoadingContainer = styled(Box)(({ theme }) => ({
width: "100%", width: '100%',
padding: theme.spacing(3), padding: theme.spacing(3),
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
})); }));
/** /**
* A loading component to display at the top of pages while content is loading * A loading component to display at the top of pages while content is loading
*/ */
const LoadingComponent: React.FC<LoadingComponentProps> = ({ const LoadingComponent: React.FC<LoadingComponentProps> = ({
loadingText = "Loading content...", loadingText = 'Loading content...',
loaderType = "circular", loaderType = 'circular',
withFade = true, withFade = true,
fadeDuration = 800, fadeDuration = 800,
}) => { }) => {
const content = ( const content = (
<LoadingContainer> <LoadingContainer>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{ xs: 12 }} sx={{ textAlign: "center", mb: 2 }}> <Grid size={{ xs: 12 }} sx={{ textAlign: 'center', mb: 2 }}>
{loaderType === "circular" ? ( {loaderType === 'circular' ? (
<CircularProgress color="primary" /> <CircularProgress color="primary" />
) : ( ) : (
<Box sx={{ width: "100%", maxWidth: 400 }}> <Box sx={{ width: '100%', maxWidth: 400 }}>
<LinearProgress color="primary" /> <LinearProgress color="primary" />
</Box> </Box>
)} )}
</Grid> </Grid>
<Grid size={{ xs: 12 }} sx={{ textAlign: "center" }}> <Grid size={{ xs: 12 }} sx={{ textAlign: 'center' }}>
<Typography variant="body1" color="textSecondary"> <Typography variant="body1" color="textSecondary">
{loadingText} {loadingText}
</Typography> </Typography>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { import {
Box, Box,
TextField, TextField,
@ -8,12 +8,12 @@ import {
Chip, Chip,
FormControlLabel, FormControlLabel,
Checkbox, Checkbox,
} from "@mui/material"; } from '@mui/material';
import { LocationOn, Public, Home } from "@mui/icons-material"; import { LocationOn, Public, Home } from '@mui/icons-material';
import { Country, State, City } from "country-state-city"; import { Country, State, City } from 'country-state-city';
import type { ICountry, IState, ICity } from "country-state-city"; import type { ICountry, IState, ICity } from 'country-state-city';
// Import from your types file - adjust path as needed // Import from your types file - adjust path as needed
import type { Location } from "types/types"; import type { Location } from 'types/types';
interface LocationInputProps { interface LocationInputProps {
value?: Partial<Location>; value?: Partial<Location>;
@ -38,18 +38,14 @@ const LocationInput: React.FC<LocationInputProps> = ({
const allCountries = Country.getAllCountries(); const allCountries = Country.getAllCountries();
const [selectedCountry, setSelectedCountry] = useState<ICountry | null>( const [selectedCountry, setSelectedCountry] = useState<ICountry | null>(
value.country value.country ? allCountries.find(c => c.name === value.country) || null : null
? allCountries.find((c) => c.name === value.country) || null
: null
); );
const [selectedState, setSelectedState] = useState<IState | null>(null); const [selectedState, setSelectedState] = useState<IState | null>(null);
const [selectedCity, setSelectedCity] = useState<ICity | null>(null); const [selectedCity, setSelectedCity] = useState<ICity | null>(null);
const [isRemote, setIsRemote] = useState<boolean>(value.remote || false); const [isRemote, setIsRemote] = useState<boolean>(value.remote || false);
// Get states for selected country // Get states for selected country
const availableStates = selectedCountry const availableStates = selectedCountry ? State.getStatesOfCountry(selectedCountry.isoCode) : [];
? State.getStatesOfCountry(selectedCountry.isoCode)
: [];
// Get cities for selected state // Get cities for selected state
const availableCities = const availableCities =
@ -60,14 +56,14 @@ const LocationInput: React.FC<LocationInputProps> = ({
// Initialize state and city from value prop // Initialize state and city from value prop
useEffect(() => { useEffect(() => {
if (selectedCountry && value.state) { if (selectedCountry && value.state) {
const stateMatch = availableStates.find((s) => s.name === value.state); const stateMatch = availableStates.find(s => s.name === value.state);
setSelectedState(stateMatch || null); setSelectedState(stateMatch || null);
} }
}, [selectedCountry, value.state, availableStates]); }, [selectedCountry, value.state, availableStates]);
useEffect(() => { useEffect(() => {
if (selectedCountry && selectedState && value.city && showCity) { if (selectedCountry && selectedState && value.city && showCity) {
const cityMatch = availableCities.find((c) => c.name === value.city); const cityMatch = availableCities.find(c => c.name === value.city);
setSelectedCity(cityMatch || null); setSelectedCity(cityMatch || null);
} }
}, [selectedCountry, selectedState, value.city, availableCities, showCity]); }, [selectedCountry, selectedState, value.city, availableCities, showCity]);
@ -93,12 +89,7 @@ const LocationInput: React.FC<LocationInputProps> = ({
} }
// Only call onChange if there's actual data or if clearing // Only call onChange if there's actual data or if clearing
if ( if (Object.keys(newLocation).length > 0 || value.country || value.state || value.city) {
Object.keys(newLocation).length > 0 ||
value.country ||
value.state ||
value.city
) {
onChange(newLocation); onChange(newLocation);
} }
}, [ }, [
@ -136,13 +127,9 @@ const LocationInput: React.FC<LocationInputProps> = ({
return ( return (
<Box> <Box>
<Typography <Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
variant="h6"
gutterBottom
sx={{ display: "flex", alignItems: "center", gap: 1 }}
>
<LocationOn color="primary" /> <LocationOn color="primary" />
Location {required && <span style={{ color: "red" }}>*</span>} Location {required && <span style={{ color: 'red' }}>*</span>}
</Typography> </Typography>
<Grid container spacing={2}> <Grid container spacing={2}>
@ -152,9 +139,9 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedCountry} value={selectedCountry}
onChange={handleCountryChange} onChange={handleCountryChange}
options={allCountries} options={allCountries}
getOptionLabel={(option) => option.name} getOptionLabel={option => option.name}
disabled={disabled} disabled={disabled}
renderInput={(params) => ( renderInput={params => (
<TextField <TextField
{...params} {...params}
label="Country" label="Country"
@ -162,15 +149,11 @@ const LocationInput: React.FC<LocationInputProps> = ({
required={required} required={required}
error={error && required && !selectedCountry} error={error && required && !selectedCountry}
helperText={ helperText={
error && required && !selectedCountry error && required && !selectedCountry ? 'Country is required' : helperText
? "Country is required"
: helperText
} }
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
startAdornment: ( startAdornment: <Public sx={{ mr: 1, color: 'text.secondary' }} />,
<Public sx={{ mr: 1, color: "text.secondary" }} />
),
}} }}
/> />
)} )}
@ -197,17 +180,15 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedState} value={selectedState}
onChange={handleStateChange} onChange={handleStateChange}
options={availableStates} options={availableStates}
getOptionLabel={(option) => option.name} getOptionLabel={option => option.name}
disabled={disabled || availableStates.length === 0} disabled={disabled || availableStates.length === 0}
renderInput={(params) => ( renderInput={params => (
<TextField <TextField
{...params} {...params}
label="State/Region" label="State/Region"
variant="outlined" variant="outlined"
placeholder={ placeholder={
availableStates.length > 0 availableStates.length > 0 ? 'Select state/region' : 'No states available'
? "Select state/region"
: "No states available"
} }
/> />
)} )}
@ -222,23 +203,17 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedCity} value={selectedCity}
onChange={handleCityChange} onChange={handleCityChange}
options={availableCities} options={availableCities}
getOptionLabel={(option) => option.name} getOptionLabel={option => option.name}
disabled={disabled || availableCities.length === 0} disabled={disabled || availableCities.length === 0}
renderInput={(params) => ( renderInput={params => (
<TextField <TextField
{...params} {...params}
label="City" label="City"
variant="outlined" variant="outlined"
placeholder={ placeholder={availableCities.length > 0 ? 'Select city' : 'No cities available'}
availableCities.length > 0
? "Select city"
: "No cities available"
}
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
startAdornment: ( startAdornment: <Home sx={{ mr: 1, color: 'text.secondary' }} />,
<Home sx={{ mr: 1, color: "text.secondary" }} />
),
}} }}
/> />
)} )}
@ -264,7 +239,7 @@ const LocationInput: React.FC<LocationInputProps> = ({
{/* Location Summary Chips */} {/* Location Summary Chips */}
{(selectedCountry || selectedState || selectedCity || isRemote) && ( {(selectedCountry || selectedState || selectedCity || isRemote) && (
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1, mt: 1 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 1 }}>
{selectedCountry && ( {selectedCountry && (
<Chip <Chip
icon={<Public />} icon={<Public />}
@ -291,14 +266,7 @@ const LocationInput: React.FC<LocationInputProps> = ({
size="small" size="small"
/> />
)} )}
{isRemote && ( {isRemote && <Chip label="Remote" variant="filled" color="success" size="small" />}
<Chip
label="Remote"
variant="filled"
color="success"
size="small"
/>
)}
</Box> </Box>
</Grid> </Grid>
)} )}
@ -314,29 +282,23 @@ const LocationInputDemo: React.FC = () => {
const handleLocationChange = (newLocation: Partial<Location>) => { const handleLocationChange = (newLocation: Partial<Location>) => {
setLocation(newLocation); setLocation(newLocation);
console.log("Location updated:", newLocation); console.log('Location updated:', newLocation);
}; };
// Show some stats about the data // Show some stats about the data
const totalCountries = Country.getAllCountries().length; const totalCountries = Country.getAllCountries().length;
const usStates = State.getStatesOfCountry("US").length; const usStates = State.getStatesOfCountry('US').length;
const canadaProvinces = State.getStatesOfCountry("CA").length; const canadaProvinces = State.getStatesOfCountry('CA').length;
return ( return (
<Box sx={{ p: 3, maxWidth: 800, mx: "auto" }}> <Box sx={{ p: 3, maxWidth: 800, mx: 'auto' }}>
<Typography variant="h4" gutterBottom align="center" color="primary"> <Typography variant="h4" gutterBottom align="center" color="primary">
Location Input with Real Data Location Input with Real Data
</Typography> </Typography>
<Typography <Typography variant="body2" color="text.secondary" align="center" sx={{ mb: 3 }}>
variant="body2"
color="text.secondary"
align="center"
sx={{ mb: 3 }}
>
Using country-state-city library with {totalCountries} countries, Using country-state-city library with {totalCountries} countries,
{usStates} US states, {canadaProvinces} Canadian provinces, and {usStates} US states, {canadaProvinces} Canadian provinces, and thousands of cities
thousands of cities
</Typography> </Typography>
<Grid container spacing={4}> <Grid container spacing={4}>
@ -344,11 +306,7 @@ const LocationInputDemo: React.FC = () => {
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
Basic Location Input Basic Location Input
</Typography> </Typography>
<LocationInput <LocationInput value={location} onChange={handleLocationChange} required />
value={location}
onChange={handleLocationChange}
required
/>
</Grid> </Grid>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
@ -356,7 +314,7 @@ const LocationInputDemo: React.FC = () => {
control={ control={
<Checkbox <Checkbox
checked={showAdvanced} checked={showAdvanced}
onChange={(e) => setShowAdvanced(e.target.checked)} onChange={e => setShowAdvanced(e.target.checked)}
color="primary" color="primary"
/> />
} }
@ -385,11 +343,11 @@ const LocationInputDemo: React.FC = () => {
<Box <Box
component="pre" component="pre"
sx={{ sx={{
bgcolor: "grey.100", bgcolor: 'grey.100',
p: 2, p: 2,
borderRadius: 1, borderRadius: 1,
overflow: "auto", overflow: 'auto',
fontSize: "0.875rem", fontSize: '0.875rem',
}} }}
> >
{JSON.stringify(location, null, 2)} {JSON.stringify(location, null, 2)}
@ -398,9 +356,8 @@ const LocationInputDemo: React.FC = () => {
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
💡 This component uses the country-state-city library which is 💡 This component uses the country-state-city library which is regularly updated and
regularly updated and includes ISO codes, flags, and comprehensive includes ISO codes, flags, and comprehensive location data.
location data.
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -1,13 +1,13 @@
import React, { useEffect, useRef, useState, useCallback } from "react"; import React, { useEffect, useRef, useState, useCallback } from 'react';
import mermaid, { MermaidConfig } from "mermaid"; import mermaid, { MermaidConfig } from 'mermaid';
import { SxProps } from "@mui/material/styles"; import { SxProps } from '@mui/material/styles';
import { Box } from "@mui/material"; import { Box } from '@mui/material';
import { useResizeObserverAndMutationObserver } from "../hooks/useAutoScrollToBottom"; import { useResizeObserverAndMutationObserver } from '../hooks/useAutoScrollToBottom';
const defaultMermaidConfig: MermaidConfig = { const defaultMermaidConfig: MermaidConfig = {
startOnLoad: true, startOnLoad: true,
securityLevel: "loose", securityLevel: 'loose',
fontFamily: "Fira Code", fontFamily: 'Fira Code',
}; };
interface MermaidProps { interface MermaidProps {
@ -38,7 +38,7 @@ const Mermaid: React.FC<MermaidProps> = (props: MermaidProps) => {
await mermaid.initialize(mermaidConfig || defaultMermaidConfig); await mermaid.initialize(mermaidConfig || defaultMermaidConfig);
await mermaid.run({ nodes: [containerRef.current] }); await mermaid.run({ nodes: [containerRef.current] });
} catch (e) { } catch (e) {
console.error("Mermaid render error:", e, containerRef.current); console.error('Mermaid render error:', e, containerRef.current);
} }
} }
}; };
@ -50,10 +50,10 @@ const Mermaid: React.FC<MermaidProps> = (props: MermaidProps) => {
return ( return (
<Box <Box
className={className || "Mermaid"} className={className || 'Mermaid'}
ref={containerRef} ref={containerRef}
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
...sx, ...sx,
}} }}

View File

@ -1,42 +1,37 @@
import { useState, useRef } from "react"; import { useState, useRef } from 'react';
import Divider from "@mui/material/Divider"; import Divider from '@mui/material/Divider';
import Accordion from "@mui/material/Accordion"; import Accordion from '@mui/material/Accordion';
import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from "@mui/material/AccordionDetails"; import AccordionDetails from '@mui/material/AccordionDetails';
import Card from "@mui/material/Card"; import Card from '@mui/material/Card';
import Table from "@mui/material/Table"; import Table from '@mui/material/Table';
import TableBody from "@mui/material/TableBody"; import TableBody from '@mui/material/TableBody';
import TableCell from "@mui/material/TableCell"; import TableCell from '@mui/material/TableCell';
import TableContainer from "@mui/material/TableContainer"; import TableContainer from '@mui/material/TableContainer';
import TableHead from "@mui/material/TableHead"; import TableHead from '@mui/material/TableHead';
import TableRow from "@mui/material/TableRow"; import TableRow from '@mui/material/TableRow';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import CardContent from "@mui/material/CardContent"; import CardContent from '@mui/material/CardContent';
import CardActions from "@mui/material/CardActions"; import CardActions from '@mui/material/CardActions';
import Collapse from "@mui/material/Collapse"; import Collapse from '@mui/material/Collapse';
import { ExpandMore } from "./ExpandMore"; import { ExpandMore } from './ExpandMore';
import JsonView from "@uiw/react-json-view"; import JsonView from '@uiw/react-json-view';
import React from "react"; import React from 'react';
import { Box } from "@mui/material"; import { Box } from '@mui/material';
import { useTheme } from "@mui/material/styles"; import { useTheme } from '@mui/material/styles';
import { SxProps, Theme } from "@mui/material"; import { SxProps, Theme } from '@mui/material';
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import LocationSearchingIcon from "@mui/icons-material/LocationSearching"; import LocationSearchingIcon from '@mui/icons-material/LocationSearching';
import { import { ErrorOutline, InfoOutline, Memory, Psychology /* Stream, */ } from '@mui/icons-material';
ErrorOutline,
InfoOutline,
Memory,
Psychology /* Stream, */,
} from "@mui/icons-material";
import { StyledMarkdown } from "./StyledMarkdown"; import { StyledMarkdown } from './StyledMarkdown';
import { VectorVisualizer } from "./VectorVisualizer"; import { VectorVisualizer } from './VectorVisualizer';
import { SetSnackType } from "./Snack"; import { SetSnackType } from './Snack';
import { CopyBubble } from "./CopyBubble"; import { CopyBubble } from './CopyBubble';
import { Scrollable } from "./Scrollable"; import { Scrollable } from './Scrollable';
import { BackstoryElementProps } from "./BackstoryTab"; import { BackstoryElementProps } from './BackstoryTab';
import { import {
ChatMessage, ChatMessage,
ChatSession, ChatSession,
@ -47,26 +42,23 @@ import {
ChatMessageError, ChatMessageError,
ChatMessageStatus, ChatMessageStatus,
ChatSenderType, ChatSenderType,
} from "types/types"; } from 'types/types';
const getStyle = ( const getStyle = (theme: Theme, type: ApiActivityType | ChatSenderType | 'error'): any => {
theme: Theme, const defaultRadius = '16px';
type: ApiActivityType | ChatSenderType | "error"
): any => {
const defaultRadius = "16px";
const defaultStyle = { const defaultStyle = {
padding: theme.spacing(1, 2), padding: theme.spacing(1, 2),
fontSize: "0.875rem", fontSize: '0.875rem',
alignSelf: "flex-start", alignSelf: 'flex-start',
maxWidth: "100%", maxWidth: '100%',
minWidth: "100%", minWidth: '100%',
height: "fit-content", height: 'fit-content',
"& > *": { '& > *': {
color: "inherit", color: 'inherit',
overflow: "hidden", overflow: 'hidden',
m: 0, m: 0,
}, },
"& > :last-child": { '& > :last-child': {
mb: 0, mb: 0,
m: 0, m: 0,
p: 0, p: 0,
@ -83,45 +75,45 @@ const getStyle = (
}, },
content: { content: {
...defaultStyle, ...defaultStyle,
backgroundColor: "#F5F2EA", backgroundColor: '#F5F2EA',
border: `1px solid ${theme.palette.custom.highlight}`, border: `1px solid ${theme.palette.custom.highlight}`,
borderRadius: 0, borderRadius: 0,
alignSelf: "center", alignSelf: 'center',
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: "8px 8px", padding: '8px 8px',
marginBottom: "0px", marginBottom: '0px',
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.05)", boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)',
fontSize: "0.9rem", fontSize: '0.9rem',
lineHeight: "1.3", lineHeight: '1.3',
fontFamily: theme.typography.fontFamily, fontFamily: theme.typography.fontFamily,
}, },
error: { error: {
...defaultStyle, ...defaultStyle,
backgroundColor: "#F8E7E7", backgroundColor: '#F8E7E7',
border: `1px solid #D83A3A`, border: `1px solid #D83A3A`,
borderRadius: defaultRadius, borderRadius: defaultRadius,
maxWidth: "90%", maxWidth: '90%',
minWidth: "90%", minWidth: '90%',
alignSelf: "center", alignSelf: 'center',
color: "#8B2525", color: '#8B2525',
padding: "10px 16px", padding: '10px 16px',
boxShadow: "0 1px 3px rgba(216, 58, 58, 0.15)", boxShadow: '0 1px 3px rgba(216, 58, 58, 0.15)',
}, },
"fact-check": "qualifications", 'fact-check': 'qualifications',
generating: "status", generating: 'status',
"job-description": "content", 'job-description': 'content',
"job-requirements": "qualifications", 'job-requirements': 'qualifications',
information: { information: {
...defaultStyle, ...defaultStyle,
backgroundColor: "#BFD8D8", backgroundColor: '#BFD8D8',
border: `1px solid ${theme.palette.secondary.main}`, border: `1px solid ${theme.palette.secondary.main}`,
borderRadius: defaultRadius, borderRadius: defaultRadius,
color: theme.palette.text.primary, color: theme.palette.text.primary,
opacity: 0.95, opacity: 0.95,
}, },
info: "information", info: 'information',
preparing: "status", preparing: 'status',
processing: "status", processing: 'status',
qualifications: { qualifications: {
...defaultStyle, ...defaultStyle,
backgroundColor: theme.palette.primary.light, backgroundColor: theme.palette.primary.light,
@ -129,49 +121,49 @@ const getStyle = (
borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`, borderRadius: `${defaultRadius} ${defaultRadius} ${defaultRadius} 0`,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
}, },
resume: "content", resume: 'content',
searching: "status", searching: 'status',
status: { status: {
...defaultStyle, ...defaultStyle,
backgroundColor: "rgba(74, 122, 125, 0.15)", backgroundColor: 'rgba(74, 122, 125, 0.15)',
border: `1px solid ${theme.palette.secondary.light}`, border: `1px solid ${theme.palette.secondary.light}`,
borderRadius: "4px", borderRadius: '4px',
maxWidth: "75%", maxWidth: '75%',
minWidth: "75%", minWidth: '75%',
alignSelf: "center", alignSelf: 'center',
color: theme.palette.secondary.dark, color: theme.palette.secondary.dark,
fontWeight: 500, fontWeight: 500,
fontSize: "0.95rem", fontSize: '0.95rem',
padding: "8px 12px", padding: '8px 12px',
opacity: 0.9, opacity: 0.9,
transition: "opacity 0.3s ease-in-out", transition: 'opacity 0.3s ease-in-out',
}, },
streaming: "response", streaming: 'response',
system: { system: {
...defaultStyle, ...defaultStyle,
backgroundColor: "#EDEAE0", backgroundColor: '#EDEAE0',
border: `1px dashed ${theme.palette.custom.highlight}`, border: `1px dashed ${theme.palette.custom.highlight}`,
borderRadius: defaultRadius, borderRadius: defaultRadius,
maxWidth: "90%", maxWidth: '90%',
minWidth: "90%", minWidth: '90%',
alignSelf: "center", alignSelf: 'center',
color: theme.palette.text.primary, color: theme.palette.text.primary,
fontStyle: "italic", fontStyle: 'italic',
}, },
thinking: "status", thinking: 'status',
user: { user: {
...defaultStyle, ...defaultStyle,
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.custom.highlight}`, border: `1px solid ${theme.palette.custom.highlight}`,
borderRadius: `${defaultRadius} ${defaultRadius} 0 ${defaultRadius}`, borderRadius: `${defaultRadius} ${defaultRadius} 0 ${defaultRadius}`,
alignSelf: "flex-end", alignSelf: 'flex-end',
color: theme.palette.primary.main, color: theme.palette.primary.main,
}, },
}; };
// Resolve string references in styles // Resolve string references in styles
for (const [key, value] of Object.entries(styles)) { for (const [key, value] of Object.entries(styles)) {
if (typeof value === "string") { if (typeof value === 'string') {
styles[key] = styles[value]; styles[key] = styles[value];
} }
} }
@ -184,7 +176,7 @@ const getStyle = (
}; };
const getIcon = ( const getIcon = (
activityType: ApiActivityType | ChatSenderType | "error" activityType: ApiActivityType | ChatSenderType | 'error'
): React.ReactNode | null => { ): React.ReactNode | null => {
const icons: any = { const icons: any = {
error: <ErrorOutline color="error" />, error: <ErrorOutline color="error" />,
@ -227,19 +219,15 @@ const MessageMeta = (props: MessageMetaProps) => {
} = props.metadata || {}; } = props.metadata || {};
const message: any = props.messageProps.message; const message: any = props.messageProps.message;
let llm_submission = "<|system|>\n"; let llm_submission = '<|system|>\n';
llm_submission += message.system_prompt + "\n\n"; llm_submission += message.system_prompt + '\n\n';
llm_submission += message.context_prompt; llm_submission += message.context_prompt;
return ( return (
<> <>
{promptEvalDuration !== 0 && evalDuration !== 0 && ( {promptEvalDuration !== 0 && evalDuration !== 0 && (
<> <>
<TableContainer <TableContainer component={Card} className="PromptStats" sx={{ mb: 1 }}>
component={Card}
className="PromptStats"
sx={{ mb: 1 }}
>
<Table aria-label="prompt stats" size="small"> <Table aria-label="prompt stats" size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
@ -250,10 +238,7 @@ const MessageMeta = (props: MessageMetaProps) => {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow <TableRow key="prompt" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
key="prompt"
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
Prompt Prompt
</TableCell> </TableCell>
@ -262,39 +247,26 @@ const MessageMeta = (props: MessageMetaProps) => {
{Math.round(promptEvalDuration / 10 ** 7) / 100} {Math.round(promptEvalDuration / 10 ** 7) / 100}
</TableCell> </TableCell>
<TableCell align="right"> <TableCell align="right">
{Math.round( {Math.round((promptEvalCount * 10 ** 9) / promptEvalDuration)}
(promptEvalCount * 10 ** 9) / promptEvalDuration
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow <TableRow key="response" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
key="response"
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
Response Response
</TableCell> </TableCell>
<TableCell align="right">{evalCount}</TableCell> <TableCell align="right">{evalCount}</TableCell>
<TableCell align="right"> <TableCell align="right">{Math.round(evalDuration / 10 ** 7) / 100}</TableCell>
{Math.round(evalDuration / 10 ** 7) / 100}
</TableCell>
<TableCell align="right"> <TableCell align="right">
{Math.round((evalCount * 10 ** 9) / evalDuration)} {Math.round((evalCount * 10 ** 9) / evalDuration)}
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow <TableRow key="total" sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
key="total"
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
Total Total
</TableCell> </TableCell>
<TableCell align="right">{promptEvalCount + evalCount}</TableCell>
<TableCell align="right"> <TableCell align="right">
{promptEvalCount + evalCount} {Math.round((promptEvalDuration + evalDuration) / 10 ** 7) / 100}
</TableCell>
<TableCell align="right">
{Math.round((promptEvalDuration + evalDuration) / 10 ** 7) /
100}
</TableCell> </TableCell>
<TableCell align="right"> <TableCell align="right">
{Math.round( {Math.round(
@ -309,9 +281,9 @@ const MessageMeta = (props: MessageMetaProps) => {
</> </>
)} )}
{tools && tools.tool_calls && tools.tool_calls.length !== 0 && ( {tools && tools.tool_calls && tools.tool_calls.length !== 0 && (
<Accordion sx={{ boxSizing: "border-box" }}> <Accordion sx={{ boxSizing: 'border-box' }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: "0.8rem" }}>Tools queried</Box> <Box sx={{ fontSize: '0.8rem' }}>Tools queried</Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
{tools.tool_calls.map((tool: any, index: number) => ( {tools.tool_calls.map((tool: any, index: number) => (
@ -321,48 +293,45 @@ const MessageMeta = (props: MessageMetaProps) => {
m: 0, m: 0,
p: 1, p: 1,
pt: 0, pt: 0,
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
border: "1px solid #e0e0e0", border: '1px solid #e0e0e0',
}} }}
> >
{index !== 0 && <Divider />} {index !== 0 && <Divider />}
<Box <Box
sx={{ sx={{
fontSize: "0.75rem", fontSize: '0.75rem',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
mt: 1, mt: 1,
mb: 1, mb: 1,
fontWeight: "bold", fontWeight: 'bold',
}} }}
> >
{tool.name} {tool.name}
</Box> </Box>
{tool.content !== "null" && ( {tool.content !== 'null' && (
<JsonView <JsonView
displayDataTypes={false} displayDataTypes={false}
objectSortKeys={true} objectSortKeys={true}
collapsed={1} collapsed={1}
value={JSON.parse(tool.content)} value={JSON.parse(tool.content)}
style={{ style={{
fontSize: "0.8rem", fontSize: '0.8rem',
maxHeight: "20rem", maxHeight: '20rem',
overflow: "auto", overflow: 'auto',
}} }}
> >
<JsonView.String <JsonView.String
render={({ children, ...reset }) => { render={({ children, ...reset }) => {
if ( if (typeof children === 'string' && children.match('\n')) {
typeof children === "string" &&
children.match("\n")
) {
return ( return (
<pre <pre
{...reset} {...reset}
style={{ style={{
display: "flex", display: 'flex',
border: "none", border: 'none',
...reset.style, ...reset.style,
}} }}
> >
@ -374,7 +343,7 @@ const MessageMeta = (props: MessageMetaProps) => {
/> />
</JsonView> </JsonView>
)} )}
{tool.content === "null" && "No response from tool call"} {tool.content === 'null' && 'No response from tool call'}
</Box> </Box>
))} ))}
</AccordionDetails> </AccordionDetails>
@ -383,19 +352,13 @@ const MessageMeta = (props: MessageMetaProps) => {
{ragResults.map((collection: ChromaDBGetResponse) => ( {ragResults.map((collection: ChromaDBGetResponse) => (
<Accordion key={collection.name}> <Accordion key={collection.name}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: '0.8rem' }}>
Top {collection.ids?.length} RAG matches from {collection.size}{" "} Top {collection.ids?.length} RAG matches from {collection.size} entries using an
entries using an embedding vector of{" "} embedding vector of {collection.queryEmbedding?.length} dimensions
{collection.queryEmbedding?.length} dimensions
</Box> </Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<VectorVisualizer <VectorVisualizer inline {...props.messageProps} {...props.metadata} rag={collection} />
inline
{...props.messageProps}
{...props.metadata}
rag={collection}
/>
{/* { ...rag, query: message.prompt }} /> */} {/* { ...rag, query: message.prompt }} /> */}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
@ -403,7 +366,7 @@ const MessageMeta = (props: MessageMetaProps) => {
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: "0.8rem" }}>Full Response Details</Box> <Box sx={{ fontSize: '0.8rem' }}>Full Response Details</Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Box sx={{ pb: 1 }}> <Box sx={{ pb: 1 }}>
@ -414,17 +377,17 @@ const MessageMeta = (props: MessageMetaProps) => {
objectSortKeys={true} objectSortKeys={true}
collapsed={1} collapsed={1}
value={message} value={message}
style={{ fontSize: "0.8rem", maxHeight: "20rem", overflow: "auto" }} style={{ fontSize: '0.8rem', maxHeight: '20rem', overflow: 'auto' }}
> >
<JsonView.String <JsonView.String
render={({ children, ...reset }) => { render={({ children, ...reset }) => {
if (typeof children === "string" && children.match("\n")) { if (typeof children === 'string' && children.match('\n')) {
return ( return (
<pre <pre
{...reset} {...reset}
style={{ style={{
display: "inline", display: 'inline',
border: "none", border: 'none',
...reset.style, ...reset.style,
}} }}
> >
@ -442,7 +405,7 @@ const MessageMeta = (props: MessageMetaProps) => {
}; };
interface MessageContainerProps { interface MessageContainerProps {
type: ApiActivityType | ChatSenderType | "error"; type: ApiActivityType | ChatSenderType | 'error';
metadataView?: React.ReactNode | null; metadataView?: React.ReactNode | null;
messageView?: React.ReactNode | null; messageView?: React.ReactNode | null;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
@ -457,20 +420,20 @@ const MessageContainer = (props: MessageContainerProps) => {
<Box <Box
className={`Message Message-${type}`} className={`Message Message-${type}`}
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
m: 0, m: 0,
mt: 1, mt: 1,
marginBottom: "0px !important", // Remove whitespace from expanded Accordion marginBottom: '0px !important', // Remove whitespace from expanded Accordion
gap: 1, gap: 1,
...sx, ...sx,
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
alignItems: "center", alignItems: 'center',
gap: 1, gap: 1,
}} }}
> >
@ -479,18 +442,15 @@ const MessageContainer = (props: MessageContainerProps) => {
</Box> </Box>
<Box <Box
flex={{ flex={{
display: "flex", display: 'flex',
position: "relative", position: 'relative',
flexDirection: "row", flexDirection: 'row',
justifyContent: "flex-end", justifyContent: 'flex-end',
alignItems: "center", alignItems: 'center',
}} }}
> >
{copyContent && ( {copyContent && (
<CopyBubble <CopyBubble content={copyContent} sx={{ position: 'absolute', top: '11px', left: 0 }} />
content={copyContent}
sx={{ position: "absolute", top: "11px", left: 0 }}
/>
)} )}
{metadataView} {metadataView}
</Box> </Box>
@ -499,23 +459,14 @@ const MessageContainer = (props: MessageContainerProps) => {
}; };
const Message = (props: MessageProps) => { const Message = (props: MessageProps) => {
const { const { message, title, sx, className, chatSession, onExpand, expanded, expandable } = props;
message,
title,
sx,
className,
chatSession,
onExpand,
expanded,
expandable,
} = props;
const [metaExpanded, setMetaExpanded] = useState<boolean>(false); const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
const theme = useTheme(); const theme = useTheme();
const type: ApiActivityType | ChatSenderType | "error" = const type: ApiActivityType | ChatSenderType | 'error' =
"activity" in message 'activity' in message
? message.activity ? message.activity
: "error" in message : 'error' in message
? "error" ? 'error'
: (message as ChatMessage).role; : (message as ChatMessage).role;
const style: any = getStyle(theme, type); const style: any = getStyle(theme, type);
@ -524,12 +475,10 @@ const Message = (props: MessageProps) => {
}; };
let content; let content;
if (typeof message.content === "string") { if (typeof message.content === 'string') {
content = message.content.trim(); content = message.content.trim();
} else { } else {
console.error( console.error(`message content is not a string, it is a ${typeof message.content}`);
`message content is not a string, it is a ${typeof message.content}`
);
return <></>; return <></>;
} }
@ -540,35 +489,33 @@ const Message = (props: MessageProps) => {
const messageView = ( const messageView = (
<StyledMarkdown <StyledMarkdown
chatSession={chatSession} chatSession={chatSession}
streaming={message.status === "streaming"} streaming={message.status === 'streaming'}
content={content} content={content}
/> />
); );
let metadataView = <></>; let metadataView = <></>;
let metadata: ChatMessageMetaData | null = let metadata: ChatMessageMetaData | null =
"metadata" in message 'metadata' in message ? (message.metadata as ChatMessageMetaData) || null : null;
? (message.metadata as ChatMessageMetaData) || null if ('role' in message && message.role === 'user') {
: null;
if ("role" in message && message.role === "user") {
metadata = null; metadata = null;
} }
if (metadata) { if (metadata) {
metadataView = ( metadataView = (
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}> <Box sx={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: 1, gap: 1,
flexDirection: "row", flexDirection: 'row',
}} }}
> >
<Box sx={{ display: "flex", flexGrow: 1 }} /> <Box sx={{ display: 'flex', flexGrow: 1 }} />
<Button <Button
variant="text" variant="text"
onClick={handleMetaExpandClick} onClick={handleMetaExpandClick}
sx={{ flexShrink: 1, color: "darkgrey", p: 0 }} sx={{ flexShrink: 1, color: 'darkgrey', p: 0 }}
> >
LLM information for this query LLM information for this query
</Button> </Button>
@ -591,7 +538,7 @@ const Message = (props: MessageProps) => {
); );
} }
const copyContent = type === "assistant" ? message.content : undefined; const copyContent = type === 'assistant' ? message.content : undefined;
if (!expandable) { if (!expandable) {
/* When not expandable, the styles are applied directly to MessageContainer */ /* When not expandable, the styles are applied directly to MessageContainer */
@ -610,8 +557,7 @@ const Message = (props: MessageProps) => {
} }
// Determine if Accordion is controlled // Determine if Accordion is controlled
const isControlled = const isControlled = typeof expanded === 'boolean' && typeof onExpand === 'function';
typeof expanded === "boolean" && typeof onExpand === "function";
return ( return (
<Accordion <Accordion
expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled
@ -627,17 +573,17 @@ const Message = (props: MessageProps) => {
slotProps={{ slotProps={{
content: { content: {
sx: { sx: {
display: "flex", display: 'flex',
justifyItems: "center", justifyItems: 'center',
m: 0, m: 0,
p: 0, p: 0,
fontWeight: "bold", fontWeight: 'bold',
fontSize: "1.1rem", fontSize: '1.1rem',
}, },
}, },
}} }}
> >
{title || ""} {title || ''}
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}> <AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}>
<MessageContainer <MessageContainer

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useState, useRef } from 'react';
import { SxProps } from "@mui/material"; import { SxProps } from '@mui/material';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
interface PulseProps { interface PulseProps {
timestamp: number | string; timestamp: number | string;
@ -15,7 +15,7 @@ const Pulse: React.FC<PulseProps> = ({ timestamp, sx }) => {
useEffect(() => { useEffect(() => {
if (timestamp && timestamp !== previousTimestamp.current) { if (timestamp && timestamp !== previousTimestamp.current) {
previousTimestamp.current = timestamp; previousTimestamp.current = timestamp;
setAnimationKey((prev) => prev + 1); setAnimationKey(prev => prev + 1);
setIsAnimating(true); setIsAnimating(true);
// Reset animation state after animation completes // Reset animation state after animation completes
@ -28,68 +28,68 @@ const Pulse: React.FC<PulseProps> = ({ timestamp, sx }) => {
}, [timestamp]); }, [timestamp]);
const containerStyle: React.CSSProperties = { const containerStyle: React.CSSProperties = {
position: "relative", position: 'relative',
width: 80, width: 80,
height: 80, height: 80,
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
}; };
const baseCoreStyle: React.CSSProperties = { const baseCoreStyle: React.CSSProperties = {
width: 0, width: 0,
height: 0, height: 0,
borderRadius: "50%", borderRadius: '50%',
backgroundColor: "#2196f3", backgroundColor: '#2196f3',
position: "relative", position: 'relative',
zIndex: 3, zIndex: 3,
}; };
const coreStyle: React.CSSProperties = { const coreStyle: React.CSSProperties = {
...baseCoreStyle, ...baseCoreStyle,
animation: isAnimating ? "pulse-glow 1s ease-out" : "none", animation: isAnimating ? 'pulse-glow 1s ease-out' : 'none',
}; };
const pulseRing1Style: React.CSSProperties = { const pulseRing1Style: React.CSSProperties = {
position: "absolute", position: 'absolute',
width: 24, width: 24,
height: 24, height: 24,
borderRadius: "50%", borderRadius: '50%',
backgroundColor: "#2196f3", backgroundColor: '#2196f3',
zIndex: 2, zIndex: 2,
animation: "pulse-expand 1s ease-out forwards", animation: 'pulse-expand 1s ease-out forwards',
}; };
const pulseRing2Style: React.CSSProperties = { const pulseRing2Style: React.CSSProperties = {
position: "absolute", position: 'absolute',
width: 24, width: 24,
height: 24, height: 24,
borderRadius: "50%", borderRadius: '50%',
backgroundColor: "#64b5f6", backgroundColor: '#64b5f6',
zIndex: 1, zIndex: 1,
animation: "pulse-expand 1s ease-out 0.2s forwards", animation: 'pulse-expand 1s ease-out 0.2s forwards',
}; };
const rippleStyle: React.CSSProperties = { const rippleStyle: React.CSSProperties = {
position: "absolute", position: 'absolute',
width: 32, width: 32,
height: 32, height: 32,
borderRadius: "50%", borderRadius: '50%',
border: "2px solid #2196f3", border: '2px solid #2196f3',
backgroundColor: "transparent", backgroundColor: 'transparent',
zIndex: 0, zIndex: 0,
animation: "ripple-expand 1s ease-out forwards", animation: 'ripple-expand 1s ease-out forwards',
}; };
const outerRippleStyle: React.CSSProperties = { const outerRippleStyle: React.CSSProperties = {
position: "absolute", position: 'absolute',
width: 40, width: 40,
height: 40, height: 40,
borderRadius: "50%", borderRadius: '50%',
border: "1px solid #90caf9", border: '1px solid #90caf9',
backgroundColor: "transparent", backgroundColor: 'transparent',
zIndex: 0, zIndex: 0,
animation: "ripple-expand 1s ease-out 0.3s forwards", animation: 'ripple-expand 1s ease-out 0.3s forwards',
}; };
return ( return (
@ -153,10 +153,7 @@ const Pulse: React.FC<PulseProps> = ({ timestamp, sx }) => {
<div key={`ripple-${animationKey}`} style={rippleStyle} /> <div key={`ripple-${animationKey}`} style={rippleStyle} />
{/* Outer ripple */} {/* Outer ripple */}
<div <div key={`ripple-outer-${animationKey}`} style={outerRippleStyle} />
key={`ripple-outer-${animationKey}`}
style={outerRippleStyle}
/>
</> </>
)} )}
</Box> </Box>

View File

@ -1,115 +1,111 @@
import React from "react"; import React from 'react';
import { Box, Typography, Paper, SxProps } from "@mui/material"; import { Box, Typography, Paper, SxProps } from '@mui/material';
import { styled } from "@mui/material/styles"; import { styled } from '@mui/material/styles';
interface QuoteContainerProps { interface QuoteContainerProps {
size?: "normal" | "small"; size?: 'normal' | 'small';
} }
const QuoteContainer = styled(Paper, { const QuoteContainer = styled(Paper, {
shouldForwardProp: (prop) => prop !== "size", shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = "normal" }) => ({ })<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
position: "relative", position: 'relative',
padding: size === "small" ? theme.spacing(1) : theme.spacing(4), padding: size === 'small' ? theme.spacing(1) : theme.spacing(4),
margin: size === "small" ? theme.spacing(0.5) : theme.spacing(2), margin: size === 'small' ? theme.spacing(0.5) : theme.spacing(2),
background: "linear-gradient(135deg, #FFFFFF 0%, #D3CDBF 100%)", background: 'linear-gradient(135deg, #FFFFFF 0%, #D3CDBF 100%)',
borderRadius: size === "small" ? theme.spacing(1) : theme.spacing(2), borderRadius: size === 'small' ? theme.spacing(1) : theme.spacing(2),
boxShadow: "0 8px 32px rgba(26, 37, 54, 0.15)", boxShadow: '0 8px 32px rgba(26, 37, 54, 0.15)',
overflow: "hidden", overflow: 'hidden',
border: "1px solid rgba(74, 122, 125, 0.2)", border: '1px solid rgba(74, 122, 125, 0.2)',
"&::before": { '&::before': {
content: '""', content: '""',
position: "absolute", position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
height: size === "small" ? "2px" : "4px", height: size === 'small' ? '2px' : '4px',
background: "linear-gradient(90deg, #4A7A7D 0%, #D4A017 100%)", background: 'linear-gradient(90deg, #4A7A7D 0%, #D4A017 100%)',
}, },
})); }));
const QuoteText = styled(Typography, { const QuoteText = styled(Typography, {
shouldForwardProp: (prop) => prop !== "size", shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = "normal" }) => ({ })<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
fontSize: size === "small" ? "0.9rem" : "1.2rem", fontSize: size === 'small' ? '0.9rem' : '1.2rem',
lineHeight: size === "small" ? 1.4 : 1.6, lineHeight: size === 'small' ? 1.4 : 1.6,
fontStyle: "italic", fontStyle: 'italic',
color: "#2E2E2E", // Charcoal Black color: '#2E2E2E', // Charcoal Black
position: "relative", position: 'relative',
zIndex: 2, zIndex: 2,
textAlign: "center", textAlign: 'center',
fontFamily: '"Georgia", "Times New Roman", serif', fontFamily: '"Georgia", "Times New Roman", serif',
fontWeight: 400, fontWeight: 400,
})); }));
const QuoteMark = styled(Typography, { const QuoteMark = styled(Typography, {
shouldForwardProp: (prop) => prop !== "size", shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = "normal" }) => ({ })<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
fontSize: size === "small" ? "2.5rem" : "4rem", fontSize: size === 'small' ? '2.5rem' : '4rem',
fontFamily: '"Georgia", "Times New Roman", serif', fontFamily: '"Georgia", "Times New Roman", serif',
fontWeight: "bold", fontWeight: 'bold',
opacity: 0.15, opacity: 0.15,
position: "absolute", position: 'absolute',
zIndex: 1, zIndex: 1,
color: "#4A7A7D", // Dusty Teal color: '#4A7A7D', // Dusty Teal
userSelect: "none", userSelect: 'none',
})); }));
const OpeningQuote = styled(QuoteMark)( const OpeningQuote = styled(QuoteMark)(({ size = 'normal' }: QuoteContainerProps) => ({
({ size = "normal" }: QuoteContainerProps) => ({ top: size === 'small' ? '5px' : '10px',
top: size === "small" ? "5px" : "10px", left: size === 'small' ? '8px' : '15px',
left: size === "small" ? "8px" : "15px", }));
})
);
const ClosingQuote = styled(QuoteMark)( const ClosingQuote = styled(QuoteMark)(({ size = 'normal' }: QuoteContainerProps) => ({
({ size = "normal" }: QuoteContainerProps) => ({ bottom: size === 'small' ? '5px' : '10px',
bottom: size === "small" ? "5px" : "10px", right: size === 'small' ? '8px' : '15px',
right: size === "small" ? "8px" : "15px", transform: 'rotate(180deg)',
transform: "rotate(180deg)", }));
})
);
const AuthorText = styled(Typography, { const AuthorText = styled(Typography, {
shouldForwardProp: (prop) => prop !== "size", shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = "normal" }) => ({ })<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
marginTop: size === "small" ? theme.spacing(1) : theme.spacing(2), marginTop: size === 'small' ? theme.spacing(1) : theme.spacing(2),
textAlign: "right", textAlign: 'right',
fontStyle: "normal", fontStyle: 'normal',
fontWeight: 500, fontWeight: 500,
color: "#1A2536", // Midnight Blue color: '#1A2536', // Midnight Blue
fontSize: size === "small" ? "0.8rem" : "0.95rem", fontSize: size === 'small' ? '0.8rem' : '0.95rem',
"&::before": { '&::before': {
content: '"— "', content: '"— "',
color: "#D4A017", // Golden Ochre dash color: '#D4A017', // Golden Ochre dash
}, },
})); }));
const AccentLine = styled(Box, { const AccentLine = styled(Box, {
shouldForwardProp: (prop) => prop !== "size", shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = "normal" }) => ({ })<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
width: size === "small" ? "40px" : "60px", width: size === 'small' ? '40px' : '60px',
height: size === "small" ? "1px" : "2px", height: size === 'small' ? '1px' : '2px',
background: "linear-gradient(90deg, #D4A017 0%, #4A7A7D 100%)", // Golden Ochre to Dusty Teal background: 'linear-gradient(90deg, #D4A017 0%, #4A7A7D 100%)', // Golden Ochre to Dusty Teal
margin: size === "small" ? "0.5rem auto" : "1rem auto", margin: size === 'small' ? '0.5rem auto' : '1rem auto',
borderRadius: "1px", borderRadius: '1px',
})); }));
interface QuoteProps { interface QuoteProps {
quote?: string; quote?: string;
author?: string; author?: string;
size?: "small" | "normal"; size?: 'small' | 'normal';
sx?: SxProps; sx?: SxProps;
} }
const Quote = (props: QuoteProps) => { const Quote = (props: QuoteProps) => {
const { quote, author, size = "normal", sx } = props; const { quote, author, size = 'normal', sx } = props;
return ( return (
<QuoteContainer size={size} elevation={0} sx={sx}> <QuoteContainer size={size} elevation={0} sx={sx}>
<OpeningQuote size={size}>"</OpeningQuote> <OpeningQuote size={size}>"</OpeningQuote>
<ClosingQuote size={size}>"</ClosingQuote> <ClosingQuote size={size}>"</ClosingQuote>
<Box sx={{ position: "relative", zIndex: 2 }}> <Box sx={{ position: 'relative', zIndex: 2 }}>
<QuoteText size={size} variant="body1"> <QuoteText size={size} variant="body1">
{quote} {quote}
</QuoteText> </QuoteText>

View File

@ -1,27 +1,19 @@
import React, { useState, useCallback, useRef, useEffect } from "react"; import React, { useState, useCallback, useRef, useEffect } from 'react';
import { import { Tabs, Tab, Box, Button, Paper, Typography, LinearProgress } from '@mui/material';
Tabs, import { Job, Candidate, SkillAssessment } from 'types/types';
Tab, import { Scrollable } from './Scrollable';
Box, import { useAuth } from 'hooks/AuthContext';
Button, import * as Types from 'types/types';
Paper, import { StyledMarkdown } from './StyledMarkdown';
Typography, import { Message } from './Message';
LinearProgress, import InputIcon from '@mui/icons-material/Input';
} from "@mui/material"; import TuneIcon from '@mui/icons-material/Tune';
import { Job, Candidate, SkillAssessment } from "types/types"; import ArticleIcon from '@mui/icons-material/Article';
import { Scrollable } from "./Scrollable"; import { StatusBox, StatusIcon } from './ui/StatusIcon';
import { useAuth } from "hooks/AuthContext"; import { CopyBubble } from './CopyBubble';
import * as Types from "types/types"; import { useAppState } from 'hooks/GlobalContext';
import { StyledMarkdown } from "./StyledMarkdown"; import { StreamingOptions } from 'services/api-client';
import { Message } from "./Message"; import { setDefaultResultOrder } from 'dns';
import InputIcon from "@mui/icons-material/Input";
import TuneIcon from "@mui/icons-material/Tune";
import ArticleIcon from "@mui/icons-material/Article";
import { StatusBox, StatusIcon } from "./ui/StatusIcon";
import { CopyBubble } from "./CopyBubble";
import { useAppState } from "hooks/GlobalContext";
import { StreamingOptions } from "services/api-client";
import { setDefaultResultOrder } from "dns";
interface ResumeGeneratorProps { interface ResumeGeneratorProps {
job: Job; job: Job;
@ -31,29 +23,25 @@ interface ResumeGeneratorProps {
} }
const defaultMessage: Types.ChatMessageStatus = { const defaultMessage: Types.ChatMessageStatus = {
status: "done", status: 'done',
type: "text", type: 'text',
sessionId: "", sessionId: '',
timestamp: new Date(), timestamp: new Date(),
content: "", content: '',
activity: "info", activity: 'info',
}; };
const ResumeGenerator: React.FC<ResumeGeneratorProps> = ( const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => {
props: ResumeGeneratorProps
) => {
const { job, candidate, skills, onComplete } = props; const { job, candidate, skills, onComplete } = props;
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const { apiClient, user } = useAuth(); const { apiClient, user } = useAuth();
const [resume, setResume] = useState<string>(""); const [resume, setResume] = useState<string>('');
const [prompt, setPrompt] = useState<string>(""); const [prompt, setPrompt] = useState<string>('');
const [systemPrompt, setSystemPrompt] = useState<string>(""); const [systemPrompt, setSystemPrompt] = useState<string>('');
const [generated, setGenerated] = useState<boolean>(false); const [generated, setGenerated] = useState<boolean>(false);
const [tabValue, setTabValue] = useState<string>("resume"); const [tabValue, setTabValue] = useState<string>('resume');
const [status, setStatus] = useState<string>(""); const [status, setStatus] = useState<string>('');
const [statusType, setStatusType] = useState<Types.ApiActivityType | null>( const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null);
null
);
const [error, setError] = useState<Types.ChatMessageError | null>(null); const [error, setError] = useState<Types.ChatMessageError | null>(null);
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
@ -67,25 +55,25 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (
setGenerated(true); setGenerated(true);
setStatusType("thinking"); setStatusType('thinking');
setStatus("Starting resume generation..."); setStatus('Starting resume generation...');
const generateResumeHandlers: StreamingOptions<Types.ChatMessageResume> = { const generateResumeHandlers: StreamingOptions<Types.ChatMessageResume> = {
onMessage: (message: Types.ChatMessageResume) => { onMessage: (message: Types.ChatMessageResume) => {
setSystemPrompt(message.systemPrompt || ""); setSystemPrompt(message.systemPrompt || '');
setPrompt(message.prompt || ""); setPrompt(message.prompt || '');
setResume(message.resume || ""); setResume(message.resume || '');
setStatus(""); setStatus('');
}, },
onStreaming: (chunk: Types.ChatMessageStreaming) => { onStreaming: (chunk: Types.ChatMessageStreaming) => {
if (status === "") { if (status === '') {
setStatus("Generating resume..."); setStatus('Generating resume...');
setStatusType("generating"); setStatusType('generating');
} }
setResume(chunk.content); setResume(chunk.content);
}, },
onStatus: (status: Types.ChatMessageStatus) => { onStatus: (status: Types.ChatMessageStatus) => {
console.log("status:", status.content); console.log('status:', status.content);
setStatusType(status.activity); setStatusType(status.activity);
setStatus(status.content); setStatus(status.content);
}, },
@ -93,7 +81,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (
onComplete && onComplete(resume); onComplete && onComplete(resume);
}, },
onError: (error: Types.ChatMessageError) => { onError: (error: Types.ChatMessageError) => {
console.log("error:", error); console.log('error:', error);
setStatusType(null); setStatusType(null);
setStatus(error.content); setStatus(error.content);
setError(error); setError(error);
@ -102,45 +90,35 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (
const generateResume = async () => { const generateResume = async () => {
const request: any = await apiClient.generateResume( const request: any = await apiClient.generateResume(
candidate.id || "", candidate.id || '',
job.id || "", job.id || '',
generateResumeHandlers generateResumeHandlers
); );
const result = await request.promise; const result = await request.promise;
}; };
generateResume(); generateResume();
}, [ }, [job, candidate, apiClient, resume, skills, generated, setSystemPrompt, setPrompt, setResume]);
job,
candidate,
apiClient,
resume,
skills,
generated,
setSystemPrompt,
setPrompt,
setResume,
]);
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
if (!resume) { if (!resume) {
setSnack("No resume to save!"); setSnack('No resume to save!');
return; return;
} }
try { try {
if (!candidate.id || !job.id) { if (!candidate.id || !job.id) {
setSnack("Candidate or job ID is missing."); setSnack('Candidate or job ID is missing.');
return; return;
} }
const controller = apiClient.saveResume(candidate.id, job.id, resume); const controller = apiClient.saveResume(candidate.id, job.id, resume);
const result = await controller.promise; const result = await controller.promise;
if (result.resume.id) { if (result.resume.id) {
setSnack("Resume saved successfully!"); setSnack('Resume saved successfully!');
} }
} catch (error) { } catch (error) {
console.error("Error saving resume:", error); console.error('Error saving resume:', error);
setSnack("Error saving resume."); setSnack('Error saving resume.');
} }
}, [apiClient, candidate.id, job.id, resume, setSnack]); }, [apiClient, candidate.id, job.id, resume, setSnack]);
@ -148,31 +126,16 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (
<Box <Box
className="ResumeGenerator" className="ResumeGenerator"
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
}} }}
> >
{user?.isAdmin && ( {user?.isAdmin && (
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 1 }}> <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 1 }}>
<Tabs value={tabValue} onChange={handleTabChange} centered> <Tabs value={tabValue} onChange={handleTabChange} centered>
<Tab <Tab disabled={systemPrompt === ''} value="system" icon={<TuneIcon />} label="System" />
disabled={systemPrompt === ""} <Tab disabled={prompt === ''} value="prompt" icon={<InputIcon />} label="Prompt" />
value="system" <Tab disabled={resume === ''} value="resume" icon={<ArticleIcon />} label="Resume" />
icon={<TuneIcon />}
label="System"
/>
<Tab
disabled={prompt === ""}
value="prompt"
icon={<InputIcon />}
label="Prompt"
/>
<Tab
disabled={resume === ""}
value="resume"
icon={<ArticleIcon />}
label="Resume"
/>
</Tabs> </Tabs>
</Box> </Box>
)} )}
@ -182,7 +145,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (
<StatusBox> <StatusBox>
{statusType && <StatusIcon type={statusType} />} {statusType && <StatusIcon type={statusType} />}
<Typography variant="body2" sx={{ ml: 1 }}> <Typography variant="body2" sx={{ ml: 1 }}>
{status || "Processing..."} {status || 'Processing...'}
</Typography> </Typography>
</StatusBox> </StatusBox>
{status && !error && <LinearProgress sx={{ mt: 1 }} />} {status && !error && <LinearProgress sx={{ mt: 1 }} />}
@ -190,19 +153,16 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (
)} )}
<Paper elevation={3} sx={{ p: 3, m: 1, mt: 0 }}> <Paper elevation={3} sx={{ p: 3, m: 1, mt: 0 }}>
<Scrollable <Scrollable autoscroll sx={{ display: 'flex', flexGrow: 1, position: 'relative' }}>
autoscroll {tabValue === 'system' && <pre>{systemPrompt}</pre>}
sx={{ display: "flex", flexGrow: 1, position: "relative" }} {tabValue === 'prompt' && <pre>{prompt}</pre>}
> {tabValue === 'resume' && (
{tabValue === "system" && <pre>{systemPrompt}</pre>}
{tabValue === "prompt" && <pre>{prompt}</pre>}
{tabValue === "resume" && (
<> <>
<CopyBubble <CopyBubble
onClick={() => { onClick={() => {
setSnack("Resume copied to clipboard!"); setSnack('Resume copied to clipboard!');
}} }}
sx={{ position: "absolute", top: 0, right: 0 }} sx={{ position: 'absolute', top: 0, right: 0 }}
content={resume} content={resume}
/> />
<StyledMarkdown content={resume} /> <StyledMarkdown content={resume} />
@ -212,12 +172,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (
</Paper> </Paper>
{resume && !status && !error && ( {resume && !status && !error && (
<Button <Button onClick={handleSave} variant="contained" color="primary" sx={{ mt: 2 }}>
onClick={handleSave}
variant="contained"
color="primary"
sx={{ mt: 2 }}
>
Save Resume Save Resume
</Button> </Button>
)} )}

View File

@ -1,7 +1,7 @@
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import { SxProps, Theme } from "@mui/material"; import { SxProps, Theme } from '@mui/material';
import { RefObject, useRef, forwardRef, useImperativeHandle } from "react"; import { RefObject, useRef, forwardRef, useImperativeHandle } from 'react';
import { useAutoScrollToBottom } from "../hooks/useAutoScrollToBottom"; import { useAutoScrollToBottom } from '../hooks/useAutoScrollToBottom';
interface ScrollableProps { interface ScrollableProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -34,15 +34,15 @@ const Scrollable = forwardRef((props: ScrollableProps, ref) => {
return ( return (
<Box <Box
className={`Scrollable ${className || ""}`} className={`Scrollable ${className || ''}`}
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
margin: "0 auto", margin: '0 auto',
p: 0, p: 0,
flexGrow: 1, flexGrow: 1,
overflow: "auto", overflow: 'auto',
position: "relative", position: 'relative',
// backgroundColor: '#F5F5F5', // backgroundColor: '#F5F5F5',
...sx, ...sx,
}} }}

View File

@ -1,16 +1,11 @@
import React, { import React, { useState, useCallback, useImperativeHandle, forwardRef } from 'react';
useState, import { SxProps, Theme } from '@mui/material';
useCallback, import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar';
useImperativeHandle, import Alert from '@mui/material/Alert';
forwardRef,
} from "react";
import { SxProps, Theme } from "@mui/material";
import Snackbar, { SnackbarCloseReason } from "@mui/material/Snackbar";
import Alert from "@mui/material/Alert";
import "./Snack.css"; import './Snack.css';
type SeverityType = "error" | "info" | "success" | "warning" | undefined; type SeverityType = 'error' | 'info' | 'success' | 'warning' | undefined;
type SetSnackType = (message: string, severity?: SeverityType) => void; type SetSnackType = (message: string, severity?: SeverityType) => void;
interface SnackHandle { interface SnackHandle {
@ -22,63 +17,51 @@ interface SnackProps {
className?: string; className?: string;
} }
const Snack = forwardRef<SnackHandle, SnackProps>( const Snack = forwardRef<SnackHandle, SnackProps>(({ className, sx }: SnackProps, ref) => {
({ className, sx }: SnackProps, ref) => { const [open, setOpen] = useState(false);
const [open, setOpen] = useState(false); const [message, setMessage] = useState('');
const [message, setMessage] = useState(""); const [severity, setSeverity] = useState<SeverityType>('success');
const [severity, setSeverity] = useState<SeverityType>("success");
// Set the snack pop-up and open it // Set the snack pop-up and open it
const setSnack: SetSnackType = useCallback<SetSnackType>( const setSnack: SetSnackType = useCallback<SetSnackType>(
(message: string, severity: SeverityType = "success") => { (message: string, severity: SeverityType = 'success') => {
setTimeout(() => { setTimeout(() => {
setMessage(message); setMessage(message);
setSeverity(severity); setSeverity(severity);
setOpen(true); setOpen(true);
}); });
}, },
[setMessage, setSeverity, setOpen] [setMessage, setSeverity, setOpen]
); );
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
setSnack: (message: string, severity?: SeverityType) => { setSnack: (message: string, severity?: SeverityType) => {
setSnack(message, severity); setSnack(message, severity);
}, },
})); }));
const handleSnackClose = ( const handleSnackClose = (event: React.SyntheticEvent | Event, reason?: SnackbarCloseReason) => {
event: React.SyntheticEvent | Event, if (reason === 'clickaway') {
reason?: SnackbarCloseReason return;
) => { }
if (reason === "clickaway") {
return;
}
setOpen(false); setOpen(false);
}; };
return ( return (
<Snackbar <Snackbar
className={className || "Snack"} className={className || 'Snack'}
sx={{ ...sx }} sx={{ ...sx }}
open={open} open={open}
autoHideDuration={ autoHideDuration={severity === 'success' || severity === 'info' ? 1500 : 6000}
severity === "success" || severity === "info" ? 1500 : 6000 onClose={handleSnackClose}
} >
onClose={handleSnackClose} <Alert onClose={handleSnackClose} severity={severity} variant="filled" sx={{ width: '100%' }}>
> {message}
<Alert </Alert>
onClose={handleSnackClose} </Snackbar>
severity={severity} );
variant="filled" });
sx={{ width: "100%" }}
>
{message}
</Alert>
</Snackbar>
);
}
);
export type { SeverityType, SetSnackType }; export type { SeverityType, SetSnackType };

View File

@ -1,23 +1,20 @@
import React from "react"; import React from 'react';
import { MuiMarkdown } from "mui-markdown"; import { MuiMarkdown } from 'mui-markdown';
import { useTheme } from "@mui/material/styles"; import { useTheme } from '@mui/material/styles';
import { Link } from "@mui/material"; import { Link } from '@mui/material';
import { import { BackstoryQuery, BackstoryQueryInterface } from 'components/BackstoryQuery';
BackstoryQuery, import Box from '@mui/material/Box';
BackstoryQueryInterface, import JsonView from '@uiw/react-json-view';
} from "components/BackstoryQuery"; import { vscodeTheme } from '@uiw/react-json-view/vscode';
import Box from "@mui/material/Box"; import { Mermaid } from 'components/Mermaid';
import JsonView from "@uiw/react-json-view"; import { Scrollable } from 'components/Scrollable';
import { vscodeTheme } from "@uiw/react-json-view/vscode"; import { jsonrepair } from 'jsonrepair';
import { Mermaid } from "components/Mermaid"; import { GenerateImage } from 'components/GenerateImage';
import { Scrollable } from "components/Scrollable";
import { jsonrepair } from "jsonrepair";
import { GenerateImage } from "components/GenerateImage";
import "./StyledMarkdown.css"; import './StyledMarkdown.css';
import { BackstoryElementProps } from "./BackstoryTab"; import { BackstoryElementProps } from './BackstoryTab';
import { CandidateQuestion, ChatQuery, ChatSession } from "types/types"; import { CandidateQuestion, ChatQuery, ChatSession } from 'types/types';
import { ChatSubmitQueryInterface } from "components/BackstoryQuery"; import { ChatSubmitQueryInterface } from 'components/BackstoryQuery';
interface StyledMarkdownProps extends BackstoryElementProps { interface StyledMarkdownProps extends BackstoryElementProps {
className?: string; className?: string;
@ -27,9 +24,7 @@ interface StyledMarkdownProps extends BackstoryElementProps {
submitQuery?: ChatSubmitQueryInterface; submitQuery?: ChatSubmitQueryInterface;
} }
const StyledMarkdown: React.FC<StyledMarkdownProps> = ( const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => {
props: StyledMarkdownProps
) => {
const { className, content, chatSession, submitQuery, sx, streaming } = props; const { className, content, chatSession, submitQuery, sx, streaming } = props;
const theme = useTheme(); const theme = useTheme();
@ -42,14 +37,14 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (
pre: { pre: {
component: (element: any) => { component: (element: any) => {
const { className } = element.children.props; const { className } = element.children.props;
const content = element.children?.props?.children || ""; const content = element.children?.props?.children || '';
if (className === "lang-mermaid" && !streaming) { if (className === 'lang-mermaid' && !streaming) {
return <Mermaid className="Mermaid" chart={content} />; return <Mermaid className="Mermaid" chart={content} />;
} }
if (className === "lang-markdown") { if (className === 'lang-markdown') {
return <MuiMarkdown children={content} />; return <MuiMarkdown children={content} />;
} }
if (className === "lang-json" && !streaming) { if (className === 'lang-json' && !streaming) {
try { try {
const fixed = JSON.parse(jsonrepair(content)); const fixed = JSON.parse(jsonrepair(content));
return ( return (
@ -58,13 +53,13 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (
className="JsonView" className="JsonView"
style={{ style={{
...vscodeTheme, ...vscodeTheme,
fontSize: "0.8rem", fontSize: '0.8rem',
maxHeight: "10rem", maxHeight: '10rem',
padding: "14px 0", padding: '14px 0',
overflow: "hidden", overflow: 'hidden',
width: "100%", width: '100%',
minHeight: "max-content", minHeight: 'max-content',
backgroundColor: "transparent", backgroundColor: 'transparent',
}} }}
displayDataTypes={false} displayDataTypes={false}
objectSortKeys={false} objectSortKeys={false}
@ -74,16 +69,13 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (
> >
<JsonView.String <JsonView.String
render={({ children, ...reset }) => { render={({ children, ...reset }) => {
if ( if (typeof children === 'string' && children.match('\n')) {
typeof children === "string" &&
children.match("\n")
) {
return ( return (
<pre <pre
{...reset} {...reset}
style={{ style={{
display: "flex", display: 'flex',
border: "none", border: 'none',
...reset.style, ...reset.style,
}} }}
> >
@ -106,7 +98,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (
} }
return ( return (
<pre> <pre>
<code className={className || ""}>{element.children}</code> <code className={className || ''}>{element.children}</code>
</pre> </pre>
); );
}, },
@ -115,22 +107,22 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (
component: Link, component: Link,
props: { props: {
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => { onClick: (event: React.MouseEvent<HTMLAnchorElement>) => {
const href = event.currentTarget.getAttribute("href"); const href = event.currentTarget.getAttribute('href');
console.log("StyledMarkdown onClick:", href); console.log('StyledMarkdown onClick:', href);
if (href) { if (href) {
if (href.match(/^\//)) { if (href.match(/^\//)) {
event.preventDefault(); event.preventDefault();
window.history.replaceState({}, "", `${href}`); window.history.replaceState({}, '', `${href}`);
} }
} }
}, },
sx: { sx: {
wordBreak: "break-all", wordBreak: 'break-all',
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
textDecoration: "none", textDecoration: 'none',
"&:hover": { '&:hover': {
color: theme.palette.custom.highlight, color: theme.palette.custom.highlight,
textDecoration: "underline", textDecoration: 'underline',
}, },
}, },
}, },
@ -150,7 +142,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (
query.question query.question
); );
} catch (e) { } catch (e) {
console.log("StyledMarkdown error:", queryString, e); console.log('StyledMarkdown error:', queryString, e);
return props.query; return props.query;
} }
}, },
@ -164,7 +156,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (
try { try {
return <GenerateImage {...{ chatSession, prompt }} />; return <GenerateImage {...{ chatSession, prompt }} />;
} catch (e) { } catch (e) {
console.log("StyledMarkdown error:", prompt, e); console.log('StyledMarkdown error:', prompt, e);
return props.prompt; return props.prompt;
} }
}, },
@ -173,14 +165,14 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (
return ( return (
<Box <Box
className={`MuiMarkdown ${className || ""}`} className={`MuiMarkdown ${className || ''}`}
sx={{ sx={{
display: "flex", display: 'flex',
m: 0, m: 0,
p: 0, p: 0,
boxSizing: "border-box", boxSizing: 'border-box',
flexGrow: 1, flexGrow: 1,
height: "auto", height: 'auto',
...sx, ...sx,
}} }}
> >

View File

@ -1,29 +1,29 @@
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useState, useRef } from 'react';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import Paper from "@mui/material/Paper"; import Paper from '@mui/material/Paper';
import Plot from "react-plotly.js"; import Plot from 'react-plotly.js';
import TextField from "@mui/material/TextField"; import TextField from '@mui/material/TextField';
import Tooltip from "@mui/material/Tooltip"; import Tooltip from '@mui/material/Tooltip';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import SendIcon from "@mui/icons-material/Send"; import SendIcon from '@mui/icons-material/Send';
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from "@mui/material/Switch"; import Switch from '@mui/material/Switch';
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from '@mui/material/useMediaQuery';
import { SxProps, useTheme } from "@mui/material/styles"; import { SxProps, useTheme } from '@mui/material/styles';
import Table from "@mui/material/Table"; import Table from '@mui/material/Table';
import TableBody from "@mui/material/TableBody"; import TableBody from '@mui/material/TableBody';
import TableCell from "@mui/material/TableCell"; import TableCell from '@mui/material/TableCell';
import TableContainer from "@mui/material/TableContainer"; import TableContainer from '@mui/material/TableContainer';
import TableRow from "@mui/material/TableRow"; import TableRow from '@mui/material/TableRow';
import { Scrollable } from "./Scrollable"; import { Scrollable } from './Scrollable';
import "./VectorVisualizer.css"; import './VectorVisualizer.css';
import { BackstoryPageProps } from "./BackstoryTab"; import { BackstoryPageProps } from './BackstoryTab';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import * as Types from "types/types"; import * as Types from 'types/types';
import { useAppState, useSelectedCandidate } from "hooks/GlobalContext"; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
interface VectorVisualizerProps extends BackstoryPageProps { interface VectorVisualizerProps extends BackstoryPageProps {
inline?: boolean; inline?: boolean;
@ -43,10 +43,10 @@ const emptyQuerySet: Types.ChromaDBGetResponse = {
metadatas: [], metadatas: [],
embeddings: [], embeddings: [],
distances: [], distances: [],
name: "Empty", name: 'Empty',
size: 0, size: 0,
dimensions: 2, dimensions: 2,
query: "", query: '',
}; };
interface PlotData { interface PlotData {
@ -105,32 +105,32 @@ const config: Partial<Plotly.Config> = {
// | "hovercompare" // | "hovercompare"
// | "hoverclosest" // | "hoverclosest"
// | "v1hovermode"; // | "v1hovermode";
modeBarButtonsToRemove: ["lasso2d", "select2d"], modeBarButtonsToRemove: ['lasso2d', 'select2d'],
}; };
const layout: Partial<Plotly.Layout> = { const layout: Partial<Plotly.Layout> = {
autosize: false, autosize: false,
clickmode: "event+select", clickmode: 'event+select',
paper_bgcolor: "#FFFFFF", // white paper_bgcolor: '#FFFFFF', // white
plot_bgcolor: "#FFFFFF", // white plot background plot_bgcolor: '#FFFFFF', // white plot background
font: { font: {
family: "Roboto, sans-serif", family: 'Roboto, sans-serif',
color: "#2E2E2E", // charcoal black color: '#2E2E2E', // charcoal black
}, },
hovermode: "closest", hovermode: 'closest',
scene: { scene: {
bgcolor: "#FFFFFF", // 3D plot background bgcolor: '#FFFFFF', // 3D plot background
zaxis: { title: "Z", gridcolor: "#cccccc", zerolinecolor: "#aaaaaa" }, zaxis: { title: 'Z', gridcolor: '#cccccc', zerolinecolor: '#aaaaaa' },
}, },
xaxis: { title: "X", gridcolor: "#cccccc", zerolinecolor: "#aaaaaa" }, xaxis: { title: 'X', gridcolor: '#cccccc', zerolinecolor: '#aaaaaa' },
yaxis: { title: "Y", gridcolor: "#cccccc", zerolinecolor: "#aaaaaa" }, yaxis: { title: 'Y', gridcolor: '#cccccc', zerolinecolor: '#aaaaaa' },
margin: { r: 0, b: 0, l: 0, t: 0 }, margin: { r: 0, b: 0, l: 0, t: 0 },
legend: { legend: {
x: 0.8, // Horizontal position (0 to 1, 0 is left, 1 is right) x: 0.8, // Horizontal position (0 to 1, 0 is left, 1 is right)
y: 0, // Vertical position (0 to 1, 0 is bottom, 1 is top) y: 0, // Vertical position (0 to 1, 0 is bottom, 1 is top)
xanchor: "left", xanchor: 'left',
yanchor: "top", yanchor: 'top',
orientation: "h", // 'v' for horizontal legend orientation: 'h', // 'v' for horizontal legend
}, },
showlegend: true, // Show the legend showlegend: true, // Show the legend
}; };
@ -140,25 +140,25 @@ const normalizeDimension = (arr: number[]): number[] => {
const max = Math.max(...arr); const max = Math.max(...arr);
const range = max - min; const range = max - min;
if (range === 0) return arr.map(() => 0.5); // flat dimension if (range === 0) return arr.map(() => 0.5); // flat dimension
return arr.map((v) => (v - min) / range); return arr.map(v => (v - min) / range);
}; };
const emojiMap: Record<string, string> = { const emojiMap: Record<string, string> = {
query: "🔍", query: '🔍',
resume: "📄", resume: '📄',
projects: "📁", projects: '📁',
jobs: "📁", jobs: '📁',
"performance-reviews": "📄", 'performance-reviews': '📄',
news: "📰", news: '📰',
}; };
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {
query: "#D4A017", // Golden Ochre — strong highlight query: '#D4A017', // Golden Ochre — strong highlight
resume: "#4A7A7D", // Dusty Teal — secondary theme color resume: '#4A7A7D', // Dusty Teal — secondary theme color
projects: "#1A2536", // Midnight Blue — rich and deep projects: '#1A2536', // Midnight Blue — rich and deep
news: "#D3CDBF", // Warm Gray — soft and neutral news: '#D3CDBF', // Warm Gray — soft and neutral
"performance-reviews": "#8FD0D0", // Light red 'performance-reviews': '#8FD0D0', // Light red
jobs: "#F3aD8F", // Warm Gray — soft and neutral jobs: '#F3aD8F', // Warm Gray — soft and neutral
}; };
const DEFAULT_SIZE = 6; const DEFAULT_SIZE = 6;
@ -180,29 +180,25 @@ type Node = {
sx: SxProps; sx: SxProps;
}; };
const VectorVisualizer: React.FC<VectorVisualizerProps> = ( const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
props: VectorVisualizerProps
) => {
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const { rag, inline, sx } = props; const { rag, inline, sx } = props;
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const [plotData, setPlotData] = useState<PlotData | null>(null); const [plotData, setPlotData] = useState<PlotData | null>(null);
const [newQuery, setNewQuery] = useState<string>(""); const [newQuery, setNewQuery] = useState<string>('');
const [querySet, setQuerySet] = useState<Types.ChromaDBGetResponse>( const [querySet, setQuerySet] = useState<Types.ChromaDBGetResponse>(rag || emptyQuerySet);
rag || emptyQuerySet
);
const [result, setResult] = useState<Types.ChromaDBGetResponse | null>(null); const [result, setResult] = useState<Types.ChromaDBGetResponse | null>(null);
const [view2D, setView2D] = useState<boolean>(true); const [view2D, setView2D] = useState<boolean>(true);
const plotlyRef = useRef(null); const plotlyRef = useRef(null);
const boxRef = useRef<HTMLElement>(null); const boxRef = useRef<HTMLElement>(null);
const [node, setNode] = useState<Node | null>(null); const [node, setNode] = useState<Node | null>(null);
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [plotDimensions, setPlotDimensions] = useState({ width: 0, height: 0 }); const [plotDimensions, setPlotDimensions] = useState({ width: 0, height: 0 });
const navigate = useNavigate(); const navigate = useNavigate();
const candidate: Types.Candidate | null = const candidate: Types.Candidate | null =
user?.userType === "candidate" ? (user as Types.Candidate) : null; user?.userType === 'candidate' ? (user as Types.Candidate) : null;
/* Force resize of Plotly as it tends to not be the correct size if it is initially rendered /* Force resize of Plotly as it tends to not be the correct size if it is initially rendered
* off screen (eg., the VectorVisualizer is not on the tab the app loads to) */ * off screen (eg., the VectorVisualizer is not on the tab the app loads to) */
@ -212,12 +208,8 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
} }
const resize = () => { const resize = () => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const plotContainer = document.querySelector( const plotContainer = document.querySelector('.plot-container') as HTMLElement;
".plot-container" const svgContainer = document?.querySelector('.svg-container') as HTMLElement;
) as HTMLElement;
const svgContainer = document?.querySelector(
".svg-container"
) as HTMLElement;
if (plotContainer && svgContainer) { if (plotContainer && svgContainer) {
const plotContainerRect = plotContainer.getBoundingClientRect(); const plotContainerRect = plotContainer.getBoundingClientRect();
svgContainer.style.width = `${plotContainerRect.width}px`; svgContainer.style.width = `${plotContainerRect.width}px`;
@ -250,8 +242,8 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
const result = await apiClient.getCandidateVectors(view2D ? 2 : 3); const result = await apiClient.getCandidateVectors(view2D ? 2 : 3);
setResult(result); setResult(result);
} catch (error) { } catch (error) {
console.error("Error obtaining collection information:", error); console.error('Error obtaining collection information:', error);
setSnack("Unable to obtain collection information.", "error"); setSnack('Unable to obtain collection information.', 'error');
} }
}; };
@ -276,7 +268,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
} }
if (!is2D && !is3D) { if (!is2D && !is3D) {
console.warn("Modified vectors are neither 2D nor 3D"); console.warn('Modified vectors are neither 2D nor 3D');
return; return;
} }
@ -286,10 +278,10 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
embeddings: [], embeddings: [],
metadatas: [], metadatas: [],
distances: [], distances: [],
query: "", query: '',
size: 0, size: 0,
dimensions: 2, dimensions: 2,
name: "", name: '',
}; };
const filtered: Types.ChromaDBGetResponse = { const filtered: Types.ChromaDBGetResponse = {
ids: [], ids: [],
@ -297,10 +289,10 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
embeddings: [], embeddings: [],
metadatas: [], metadatas: [],
distances: [], distances: [],
query: "", query: '',
size: 0, size: 0,
dimensions: 2, dimensions: 2,
name: "", name: '',
}; };
/* Loop through all items and divide into two groups: /* Loop through all items and divide into two groups:
@ -332,102 +324,83 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
}); });
if (view2D && querySet.umapEmbedding2D && querySet.umapEmbedding2D.length) { if (view2D && querySet.umapEmbedding2D && querySet.umapEmbedding2D.length) {
query.ids.unshift("query"); query.ids.unshift('query');
query.metadatas.unshift({ query.metadatas.unshift({
id: "query", id: 'query',
docType: "query", docType: 'query',
content: querySet.query || "", content: querySet.query || '',
distance: 0, distance: 0,
}); });
query.embeddings.unshift(querySet.umapEmbedding2D); query.embeddings.unshift(querySet.umapEmbedding2D);
} }
if ( if (!view2D && querySet.umapEmbedding3D && querySet.umapEmbedding3D.length) {
!view2D && query.ids.unshift('query');
querySet.umapEmbedding3D &&
querySet.umapEmbedding3D.length
) {
query.ids.unshift("query");
query.metadatas.unshift({ query.metadatas.unshift({
id: "query", id: 'query',
docType: "query", docType: 'query',
content: querySet.query || "", content: querySet.query || '',
distance: 0, distance: 0,
}); });
query.embeddings.unshift(querySet.umapEmbedding3D); query.embeddings.unshift(querySet.umapEmbedding3D);
} }
const filtered_docTypes = filtered.metadatas.map( const filtered_docTypes = filtered.metadatas.map(m => m.docType || 'unknown');
(m) => m.docType || "unknown" const query_docTypes = query.metadatas.map(m => m.docType || 'unknown');
);
const query_docTypes = query.metadatas.map((m) => m.docType || "unknown");
const has_query = query.metadatas.length > 0; const has_query = query.metadatas.length > 0;
const filtered_sizes = filtered.metadatas.map((m) => const filtered_sizes = filtered.metadatas.map(m =>
has_query ? DEFAULT_UNFOCUS_SIZE : DEFAULT_SIZE has_query ? DEFAULT_UNFOCUS_SIZE : DEFAULT_SIZE
); );
const filtered_colors = filtered_docTypes.map( const filtered_colors = filtered_docTypes.map(type => colorMap[type] || '#4d4d4d');
(type) => colorMap[type] || "#4d4d4d" const filtered_x = normalizeDimension(filtered.embeddings.map((v: number[]) => v[0]));
); const filtered_y = normalizeDimension(filtered.embeddings.map((v: number[]) => v[1]));
const filtered_x = normalizeDimension(
filtered.embeddings.map((v: number[]) => v[0])
);
const filtered_y = normalizeDimension(
filtered.embeddings.map((v: number[]) => v[1])
);
const filtered_z = is3D const filtered_z = is3D
? normalizeDimension(filtered.embeddings.map((v: number[]) => v[2])) ? normalizeDimension(filtered.embeddings.map((v: number[]) => v[2]))
: undefined; : undefined;
const query_sizes = query.metadatas.map( const query_sizes = query.metadatas.map(
(m) => m => DEFAULT_SIZE + 2 * DEFAULT_SIZE * Math.pow(1 - (m.distance || 1), 3)
DEFAULT_SIZE + 2 * DEFAULT_SIZE * Math.pow(1 - (m.distance || 1), 3)
);
const query_colors = query_docTypes.map(
(type) => colorMap[type] || "#4d4d4d"
);
const query_x = normalizeDimension(
query.embeddings.map((v: number[]) => v[0])
);
const query_y = normalizeDimension(
query.embeddings.map((v: number[]) => v[1])
); );
const query_colors = query_docTypes.map(type => colorMap[type] || '#4d4d4d');
const query_x = normalizeDimension(query.embeddings.map((v: number[]) => v[0]));
const query_y = normalizeDimension(query.embeddings.map((v: number[]) => v[1]));
const query_z = is3D const query_z = is3D
? normalizeDimension(query.embeddings.map((v: number[]) => v[2])) ? normalizeDimension(query.embeddings.map((v: number[]) => v[2]))
: undefined; : undefined;
const data: any = [ const data: any = [
{ {
name: "All data", name: 'All data',
x: filtered_x, x: filtered_x,
y: filtered_y, y: filtered_y,
mode: "markers", mode: 'markers',
marker: { marker: {
size: filtered_sizes, size: filtered_sizes,
symbol: "circle", symbol: 'circle',
color: filtered_colors, color: filtered_colors,
opacity: 1, opacity: 1,
}, },
text: filtered.ids, text: filtered.ids,
customdata: filtered.metadatas, customdata: filtered.metadatas,
type: is3D ? "scatter3d" : "scatter", type: is3D ? 'scatter3d' : 'scatter',
hovertemplate: "&nbsp;", hovertemplate: '&nbsp;',
}, },
{ {
name: "Query", name: 'Query',
x: query_x, x: query_x,
y: query_y, y: query_y,
mode: "markers", mode: 'markers',
marker: { marker: {
size: query_sizes, size: query_sizes,
symbol: "circle", symbol: 'circle',
color: query_colors, color: query_colors,
opacity: 1, opacity: 1,
}, },
text: query.ids, text: query.ids,
customdata: query.metadatas, customdata: query.metadatas,
type: is3D ? "scatter3d" : "scatter", type: is3D ? 'scatter3d' : 'scatter',
hovertemplate: "%{text}", hovertemplate: '%{text}',
}, },
]; ];
@ -440,14 +413,14 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
}, [result, querySet, view2D]); }, [result, querySet, view2D]);
const handleKeyPress = (event: any) => { const handleKeyPress = (event: any) => {
if (event.key === "Enter") { if (event.key === 'Enter') {
sendQuery(newQuery); sendQuery(newQuery);
} }
}; };
const sendQuery = async (query: string) => { const sendQuery = async (query: string) => {
if (!query.trim()) return; if (!query.trim()) return;
setNewQuery(""); setNewQuery('');
try { try {
const result = await apiClient.getCandidateSimilarContent(query); const result = await apiClient.getCandidateSimilarContent(query);
@ -455,7 +428,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
setQuerySet(result); setQuerySet(result);
} catch (error) { } catch (error) {
const msg = `Error obtaining similar content to ${query}.`; const msg = `Error obtaining similar content to ${query}.`;
setSnack(msg, "error"); setSnack(msg, 'error');
} }
}; };
@ -463,10 +436,10 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
}} }}
> >
<div>Loading visualization...</div> <div>Loading visualization...</div>
@ -477,18 +450,15 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
}} }}
> >
<div> <div>
No candidate selected. Please{" "} No candidate selected. Please{' '}
<Button onClick={() => navigate("/find-a-candidate")}> <Button onClick={() => navigate('/find-a-candidate')}>select a candidate</Button> first.
select a candidate
</Button>{" "}
first.
</div> </div>
</Box> </Box>
); );
@ -504,33 +474,33 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (
} catch (error) { } catch (error) {
const msg = `Error obtaining content for ${node.id}.`; const msg = `Error obtaining content for ${node.id}.`;
console.error(msg, error); console.error(msg, error);
setSnack(msg, "error"); setSnack(msg, 'error');
} }
}; };
const onNodeSelected = (metadata: any) => { const onNodeSelected = (metadata: any) => {
let node: Node; let node: Node;
console.log(metadata); console.log(metadata);
if (metadata.docType === "query") { if (metadata.docType === 'query') {
node = { node = {
...metadata, ...metadata,
content: `Similarity results for the query **${querySet.query || ""}** content: `Similarity results for the query **${querySet.query || ''}**
The scatter graph shows the query in N-dimensional space, mapped to ${ The scatter graph shows the query in N-dimensional space, mapped to ${
view2D ? "2" : "3" view2D ? '2' : '3'
}-dimensional space. Larger dots represent relative similarity in N-dimensional space. }-dimensional space. Larger dots represent relative similarity in N-dimensional space.
`, `,
emoji: emojiMap[metadata.docType], emoji: emojiMap[metadata.docType],
sx: { sx: {
m: 0.5, m: 0.5,
p: 2, p: 2,
width: "3rem", width: '3rem',
display: "flex", display: 'flex',
alignContent: "center", alignContent: 'center',
justifyContent: "center", justifyContent: 'center',
flexGrow: 0, flexGrow: 0,
flexWrap: "wrap", flexWrap: 'wrap',
backgroundColor: colorMap[metadata.docType] || "#ff8080", backgroundColor: colorMap[metadata.docType] || '#ff8080',
}, },
}; };
setNode(node); setNode(node);
@ -540,7 +510,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
node = { node = {
content: `Loading...`, content: `Loading...`,
...metadata, ...metadata,
emoji: emojiMap[metadata.docType] || "❓", emoji: emojiMap[metadata.docType] || '❓',
}; };
setNode(node); setNode(node);
@ -561,22 +531,22 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
sx={{ sx={{
p: 0.5, p: 0.5,
m: 0, m: 0,
display: "flex", display: 'flex',
flexGrow: 0, flexGrow: 0,
height: isMobile ? "auto" : "auto", //"320px", height: isMobile ? 'auto' : 'auto', //"320px",
minHeight: isMobile ? "auto" : "auto", //"320px", minHeight: isMobile ? 'auto' : 'auto', //"320px",
maxHeight: isMobile ? "auto" : "auto", //"320px", maxHeight: isMobile ? 'auto' : 'auto', //"320px",
position: "relative", position: 'relative',
flexDirection: "column", flexDirection: 'column',
}} }}
> >
<FormControlLabel <FormControlLabel
sx={{ sx={{
display: "flex", display: 'flex',
position: "relative", position: 'relative',
width: "fit-content", width: 'fit-content',
ml: 1, ml: 1,
mb: "-2.5rem", mb: '-2.5rem',
zIndex: 100, zIndex: 100,
flexBasis: 0, flexBasis: 0,
flexGrow: 0, flexGrow: 0,
@ -597,14 +567,14 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
useResizeHandler={true} useResizeHandler={true}
config={config} config={config}
style={{ style={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
minHeight: "240px", minHeight: '240px',
padding: 0, padding: 0,
margin: 0, margin: 0,
width: "100%", width: '100%',
height: "100%", height: '100%',
overflow: "hidden", overflow: 'hidden',
}} }}
layout={{ layout={{
...layout, ...layout,
@ -616,37 +586,34 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
<Paper <Paper
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: isMobile ? "column" : "row", flexDirection: isMobile ? 'column' : 'row',
mt: 0.5, mt: 0.5,
p: 0.5, p: 0.5,
flexGrow: 1, flexGrow: 1,
minHeight: "fit-content", minHeight: 'fit-content',
}} }}
> >
{node !== null && ( {node !== null && (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
fontSize: "0.75rem", fontSize: '0.75rem',
flexDirection: "column", flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
maxWidth: "100%", maxWidth: '100%',
flexBasis: 1, flexBasis: 1,
maxHeight: "min-content", maxHeight: 'min-content',
}} }}
> >
<TableContainer <TableContainer component={Paper} sx={{ mb: isMobile ? 1 : 0, mr: isMobile ? 0 : 1 }}>
component={Paper} <Table size="small" sx={{ tableLayout: 'fixed' }}>
sx={{ mb: isMobile ? 1 : 0, mr: isMobile ? 0 : 1 }}
>
<Table size="small" sx={{ tableLayout: "fixed" }}>
<TableBody <TableBody
sx={{ sx={{
"& td": { verticalAlign: "top", fontSize: "0.75rem" }, '& td': { verticalAlign: 'top', fontSize: '0.75rem' },
"& td:first-of-type": { '& td:first-of-type': {
whiteSpace: "nowrap", whiteSpace: 'nowrap',
width: "1rem", width: '1rem',
}, },
}} }}
> >
@ -659,9 +626,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
{node.source_file !== undefined && ( {node.source_file !== undefined && (
<TableRow> <TableRow>
<TableCell>File</TableCell> <TableCell>File</TableCell>
<TableCell> <TableCell>{node.source_file.replace(/^.*\//, '')}</TableCell>
{node.source_file.replace(/^.*\//, "")}
</TableCell>
</TableRow> </TableRow>
)} )}
{node.path !== undefined && ( {node.path !== undefined && (
@ -679,30 +644,28 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
{node.content !== "" && node.content !== undefined && ( {node.content !== '' && node.content !== undefined && (
<Paper <Paper
elevation={6} elevation={6}
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
border: "1px solid #808080", border: '1px solid #808080',
minHeight: "fit-content", minHeight: 'fit-content',
mt: 1, mt: 1,
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
background: "#404040", background: '#404040',
p: 1, p: 1,
color: "white", color: 'white',
}} }}
> >
Vector Embedded Content Vector Embedded Content
</Box> </Box>
<Box sx={{ display: "flex", p: 1, flexGrow: 1 }}> <Box sx={{ display: 'flex', p: 1, flexGrow: 1 }}>{node.content}</Box>
{node.content}
</Box>
</Paper> </Paper>
)} )}
</Box> </Box>
@ -710,8 +673,8 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
flexGrow: 2, flexGrow: 2,
flexBasis: 0, flexBasis: 0,
flexShrink: 1, flexShrink: 1,
@ -719,49 +682,46 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
> >
{node === null && ( {node === null && (
<Paper sx={{ m: 0.5, p: 2, flexGrow: 1 }}> <Paper sx={{ m: 0.5, p: 2, flexGrow: 1 }}>
Click a point in the scatter-graph to see information about that Click a point in the scatter-graph to see information about that node.
node.
</Paper> </Paper>
)} )}
{node !== null && node.fullContent && ( {node !== null && node.fullContent && (
<Scrollable <Scrollable
autoscroll={false} autoscroll={false}
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
m: 0, m: 0,
p: 0.5, p: 0.5,
pl: 1, pl: 1,
flexShrink: 1, flexShrink: 1,
position: "relative", position: 'relative',
maxWidth: "100%", maxWidth: '100%',
}} }}
> >
{node.fullContent.split("\n").map((line, index) => { {node.fullContent.split('\n').map((line, index) => {
index += 1 + node.chunkBegin; index += 1 + node.chunkBegin;
const bgColor = const bgColor =
index > node.lineBegin && index <= node.lineEnd index > node.lineBegin && index <= node.lineEnd ? '#f0f0f0' : 'auto';
? "#f0f0f0"
: "auto";
return ( return (
<Box <Box
key={index} key={index}
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
borderBottom: "1px solid #d0d0d0", borderBottom: '1px solid #d0d0d0',
":first-of-type": { borderTop: "1px solid #d0d0d0" }, ':first-of-type': { borderTop: '1px solid #d0d0d0' },
backgroundColor: bgColor, backgroundColor: bgColor,
}} }}
> >
<Box <Box
sx={{ sx={{
fontFamily: "courier", fontFamily: 'courier',
fontSize: "0.8rem", fontSize: '0.8rem',
minWidth: "2rem", minWidth: '2rem',
pt: "0.1rem", pt: '0.1rem',
align: "left", align: 'left',
verticalAlign: "top", verticalAlign: 'top',
}} }}
> >
{index} {index}
@ -770,60 +730,52 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
style={{ style={{
margin: 0, margin: 0,
padding: 0, padding: 0,
border: "none", border: 'none',
minHeight: "1rem", minHeight: '1rem',
overflow: "hidden", overflow: 'hidden',
}} }}
> >
{line || " "} {line || ' '}
</pre> </pre>
</Box> </Box>
); );
})} })}
{!node.lineBegin && ( {!node.lineBegin && (
<pre style={{ margin: 0, padding: 0, border: "none" }}> <pre style={{ margin: 0, padding: 0, border: 'none' }}>{node.content}</pre>
{node.content}
</pre>
)} )}
</Scrollable> </Scrollable>
)} )}
</Box> </Box>
</Paper> </Paper>
{!inline && querySet.query !== undefined && querySet.query !== "" && ( {!inline && querySet.query !== undefined && querySet.query !== '' && (
<Paper <Paper
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
justifyContent: "center", justifyContent: 'center',
flexGrow: 0, flexGrow: 0,
minHeight: "2.5rem", minHeight: '2.5rem',
maxHeight: "2.5rem", maxHeight: '2.5rem',
height: "2.5rem", height: '2.5rem',
alignItems: "center", alignItems: 'center',
mt: 1, mt: 1,
pb: 0, pb: 0,
}} }}
> >
{querySet.query !== undefined && {querySet.query !== undefined && querySet.query !== '' && `Query: ${querySet.query}`}
querySet.query !== "" && {querySet.ids.length === 0 && 'Enter query below to perform a similarity search.'}
`Query: ${querySet.query}`}
{querySet.ids.length === 0 &&
"Enter query below to perform a similarity search."}
</Paper> </Paper>
)} )}
{!inline && ( {!inline && (
<Box <Box className="Query" sx={{ display: 'flex', flexDirection: 'row', p: 1 }}>
className="Query"
sx={{ display: "flex", flexDirection: "row", p: 1 }}
>
<TextField <TextField
variant="outlined" variant="outlined"
fullWidth fullWidth
type="text" type="text"
value={newQuery} value={newQuery}
onChange={(e) => setNewQuery(e.target.value)} onChange={e => setNewQuery(e.target.value)}
onKeyDown={handleKeyPress} onKeyDown={handleKeyPress}
placeholder="Enter query to find related documents..." placeholder="Enter query to find related documents..."
id="QueryInput" id="QueryInput"

View File

@ -1,20 +1,20 @@
// components/layout/BackstoryLayout.tsx // components/layout/BackstoryLayout.tsx
import React, { ReactElement, useEffect, useState } from "react"; import React, { ReactElement, useEffect, useState } from 'react';
import { Outlet, useLocation, Routes, Route } from "react-router-dom"; import { Outlet, useLocation, Routes, Route } from 'react-router-dom';
import { Box, Container, Paper } from "@mui/material"; import { Box, Container, Paper } from '@mui/material';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { SxProps, Theme } from "@mui/material"; import { SxProps, Theme } from '@mui/material';
import { darken } from "@mui/material/styles"; import { darken } from '@mui/material/styles';
import { Header } from "components/layout/Header"; import { Header } from 'components/layout/Header';
import { Scrollable } from "components/Scrollable"; import { Scrollable } from 'components/Scrollable';
import { Footer } from "components/layout/Footer"; import { Footer } from 'components/layout/Footer';
import { Snack, SetSnackType } from "components/Snack"; import { Snack, SetSnackType } from 'components/Snack';
import { User } from "types/types"; import { User } from 'types/types';
import { LoadingComponent } from "components/LoadingComponent"; import { LoadingComponent } from 'components/LoadingComponent';
import { AuthProvider, useAuth, ProtectedRoute } from "hooks/AuthContext"; import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext';
import { useAppState, useSelectedCandidate } from "hooks/GlobalContext"; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import { getMainNavigationItems, getAllRoutes } from "config/navigationConfig"; import { getMainNavigationItems, getAllRoutes } from 'config/navigationConfig';
import { NavigationItem } from "types/navigation"; import { NavigationItem } from 'types/navigation';
// Legacy type for backward compatibility // Legacy type for backward compatibility
export type NavigationLinkType = { export type NavigationLinkType = {
@ -34,37 +34,37 @@ const BackstoryPageContainer = (props: BackstoryPageContainerProps) => {
<Container <Container
className="BackstoryPageContainer" className="BackstoryPageContainer"
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
flexGrow: 1, flexGrow: 1,
p: "0 !important", p: '0 !important',
m: "0 auto !important", m: '0 auto !important',
maxWidth: "1024px", maxWidth: '1024px',
height: "100%", height: '100%',
minHeight: 0, minHeight: 0,
...sx, ...sx,
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
p: { xs: 0, sm: 0.5 }, p: { xs: 0, sm: 0.5 },
flexGrow: 1, flexGrow: 1,
minHeight: "min-content", minHeight: 'min-content',
}} }}
> >
<Paper <Paper
elevation={2} elevation={2}
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
m: 0, m: 0,
p: 0.5, p: 0.5,
minHeight: "min-content", minHeight: 'min-content',
backgroundColor: "background.paper", backgroundColor: 'background.paper',
borderRadius: 0.5, borderRadius: 0.5,
maxWidth: "100%", maxWidth: '100%',
flexDirection: "column", flexDirection: 'column',
}} }}
> >
{children} {children}
@ -79,9 +79,7 @@ interface BackstoryLayoutProps {
chatRef: React.Ref<any>; chatRef: React.Ref<any>;
} }
const BackstoryLayout: React.FC<BackstoryLayoutProps> = ( const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => {
props: BackstoryLayoutProps
) => {
const { page, chatRef } = props; const { page, chatRef } = props;
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const navigate = useNavigate(); const navigate = useNavigate();
@ -91,9 +89,7 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (
useEffect(() => { useEffect(() => {
const userType = user?.userType || null; const userType = user?.userType || null;
setNavigationItems( setNavigationItems(getMainNavigationItems(userType, user?.isAdmin ? true : false));
getMainNavigationItems(userType, user?.isAdmin ? true : false)
);
}, [user]); }, [user]);
// Generate dynamic routes from navigation config // Generate dynamic routes from navigation config
@ -109,20 +105,13 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (
if (!route.path || !route.component) return null; if (!route.path || !route.component) return null;
// Clone the component and pass necessary props if it's a page component // Clone the component and pass necessary props if it's a page component
const componentWithProps = React.cloneElement( const componentWithProps = React.cloneElement(route.component as ReactElement, {
route.component as ReactElement, ...(route.id === 'chat' && { ref: chatRef }),
{ ...(route.component.props || {}),
...(route.id === "chat" && { ref: chatRef }), });
...(route.component.props || {}),
}
);
return ( return (
<Route <Route key={`${route.id}-${index}`} path={route.path} element={componentWithProps} />
key={`${route.id}-${index}`}
path={route.path}
element={componentWithProps}
/>
); );
}) })
.filter(Boolean); .filter(Boolean);
@ -131,28 +120,24 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (
return ( return (
<Box <Box
sx={{ sx={{
height: "100%", height: '100%',
maxHeight: "100%", maxHeight: '100%',
minHeight: "100%", minHeight: '100%',
flexDirection: "column", flexDirection: 'column',
}} }}
> >
<Header <Header currentPath={page} navigate={navigate} navigationItems={navigationItems} />
currentPath={page}
navigate={navigate}
navigationItems={navigationItems}
/>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
width: "100%", width: '100%',
maxHeight: "100%", maxHeight: '100%',
minHeight: "100%", minHeight: '100%',
flex: 1, flex: 1,
m: 0, m: 0,
p: 0, p: 0,
flexDirection: "column", flexDirection: 'column',
backgroundColor: "#D3CDBF" /* Warm Gray */, backgroundColor: '#D3CDBF' /* Warm Gray */,
}} }}
> >
<Scrollable <Scrollable
@ -160,15 +145,14 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (
sx={{ sx={{
m: 0, m: 0,
p: 0, p: 0,
mt: "72px" /* Needs to be kept in sync with the height of Header */, mt: '72px' /* Needs to be kept in sync with the height of Header */,
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
backgroundColor: (theme) => backgroundColor: theme => darken(theme.palette.background.default, 0.4),
darken(theme.palette.background.default, 0.4), height: '100%',
height: "100%", maxHeight: '100%',
maxHeight: "100%", minHeight: '100%',
minHeight: "100%", minWidth: 'min-content',
minWidth: "min-content",
}} }}
> >
<BackstoryPageContainer> <BackstoryPageContainer>
@ -188,7 +172,7 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (
<Routes>{generateRoutes()}</Routes> <Routes>{generateRoutes()}</Routes>
</> </>
)} )}
{location.pathname === "/" && <Footer />} {location.pathname === '/' && <Footer />}
</BackstoryPageContainer> </BackstoryPageContainer>
</Scrollable> </Scrollable>
</Box> </Box>

View File

@ -1,22 +1,20 @@
// components/layout/BackstoryRoutes.tsx // components/layout/BackstoryRoutes.tsx
import React, { Ref, ReactNode } from "react"; import React, { Ref, ReactNode } from 'react';
import { Route } from "react-router-dom"; import { Route } from 'react-router-dom';
import { BackstoryPageProps } from "../BackstoryTab"; import { BackstoryPageProps } from '../BackstoryTab';
import { ConversationHandle } from "../Conversation"; import { ConversationHandle } from '../Conversation';
import { User } from "types/types"; import { User } from 'types/types';
import { getAllRoutes } from "config/navigationConfig"; import { getAllRoutes } from 'config/navigationConfig';
import { NavigationItem } from "types/navigation"; import { NavigationItem } from 'types/navigation';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
interface BackstoryDynamicRoutesProps extends BackstoryPageProps { interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
chatRef: Ref<ConversationHandle>; chatRef: Ref<ConversationHandle>;
user?: User | null; user?: User | null;
} }
const getBackstoryDynamicRoutes = ( const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNode => {
props: BackstoryDynamicRoutesProps
): ReactNode => {
const { user, chatRef } = props; const { user, chatRef } = props;
const userType = user?.userType || null; const userType = user?.userType || null;
const isAdmin = user?.isAdmin ? true : false; const isAdmin = user?.isAdmin ? true : false;
@ -29,23 +27,14 @@ const getBackstoryDynamicRoutes = (
if (!route.path || !route.component) return null; if (!route.path || !route.component) return null;
// Clone the component and pass necessary props // Clone the component and pass necessary props
const componentWithProps = React.cloneElement( const componentWithProps = React.cloneElement(route.component as React.ReactElement, {
route.component as React.ReactElement, // Special handling for chat component ref
{ ...(route.id === 'chat' && { ref: chatRef }),
// Special handling for chat component ref // Preserve any existing props
...(route.id === "chat" && { ref: chatRef }), ...(route.component.props || {}),
// Preserve any existing props });
...(route.component.props || {}),
}
);
return ( return <Route key={`${route.id}-${index}`} path={route.path} element={componentWithProps} />;
<Route
key={`${route.id}-${index}`}
path={route.path}
element={componentWithProps}
/>
);
}) })
.filter(Boolean); .filter(Boolean);
}; };

View File

@ -1,4 +1,4 @@
import React from "react"; import React from 'react';
import { import {
Paper, Paper,
Box, Box,
@ -10,8 +10,8 @@ import {
IconButton, IconButton,
Stack, Stack,
useMediaQuery, useMediaQuery,
} from "@mui/material"; } from '@mui/material';
import { styled, useTheme } from "@mui/material/styles"; import { styled, useTheme } from '@mui/material/styles';
import { import {
Facebook, Facebook,
Twitter, Twitter,
@ -21,7 +21,7 @@ import {
Email, Email,
LocationOn, LocationOn,
Copyright, Copyright,
} from "@mui/icons-material"; } from '@mui/icons-material';
// Styled components // Styled components
const FooterContainer = styled(Paper)(({ theme }) => ({ const FooterContainer = styled(Paper)(({ theme }) => ({
@ -34,12 +34,12 @@ const FooterContainer = styled(Paper)(({ theme }) => ({
const FooterLink = styled(Link)(({ theme }) => ({ const FooterLink = styled(Link)(({ theme }) => ({
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
textDecoration: "none", textDecoration: 'none',
"&:hover": { '&:hover': {
textDecoration: "underline", textDecoration: 'underline',
color: theme.palette.action.active, color: theme.palette.action.active,
}, },
display: "block", display: 'block',
marginBottom: theme.spacing(1), marginBottom: theme.spacing(1),
})); }));
@ -50,15 +50,15 @@ const FooterHeading = styled(Typography)(({ theme }) => ({
})); }));
const ContactItem = styled(Box)(({ theme }) => ({ const ContactItem = styled(Box)(({ theme }) => ({
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
marginBottom: theme.spacing(1.5), marginBottom: theme.spacing(1.5),
})); }));
// Footer component // Footer component
const Footer = () => { const Footer = () => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
return ( return (
@ -73,15 +73,15 @@ const Footer = () => {
component="div" component="div"
sx={{ sx={{
fontWeight: 700, fontWeight: 700,
letterSpacing: ".2rem", letterSpacing: '.2rem',
marginBottom: 2, marginBottom: 2,
}} }}
> >
BACKSTORY BACKSTORY
</Typography> </Typography>
<Typography variant="body2" sx={{ mb: 2, color: "white" }}> <Typography variant="body2" sx={{ mb: 2, color: 'white' }}>
Helping candidates share their professional journey and connect Helping candidates share their professional journey and connect with the right
with the right employers through compelling backstories. employers through compelling backstories.
</Typography> </Typography>
<Stack direction="row"> <Stack direction="row">
{/* <IconButton {/* <IconButton
@ -120,16 +120,13 @@ const Footer = () => {
sx={{ sx={{
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
mr: 1, mr: 1,
"&:hover": { '&:hover': {
backgroundColor: "rgba(211, 205, 191, 0.1)", backgroundColor: 'rgba(211, 205, 191, 0.1)',
color: theme.palette.action.active, color: theme.palette.action.active,
}, },
}} }}
onClick={() => onClick={() =>
window.open( window.open('https://www.linkedin.com/in/james-ketrenos/', '_blank')
"https://www.linkedin.com/in/james-ketrenos/",
"_blank"
)
} }
> >
<LinkedIn /> <LinkedIn />
@ -172,17 +169,11 @@ const Footer = () => {
{false && ( {false && (
<> <>
<Grid size={{ xs: 12, sm: 6, md: 2 }}> <Grid size={{ xs: 12, sm: 6, md: 2 }}>
<FooterHeading variant="subtitle1"> <FooterHeading variant="subtitle1">For Candidates</FooterHeading>
For Candidates
</FooterHeading>
<FooterLink href="/create-profile">Create Profile</FooterLink> <FooterLink href="/create-profile">Create Profile</FooterLink>
<FooterLink href="/backstory-editor"> <FooterLink href="/backstory-editor">Backstory Editor</FooterLink>
Backstory Editor
</FooterLink>
<FooterLink href="/resume-builder">Resume Builder</FooterLink> <FooterLink href="/resume-builder">Resume Builder</FooterLink>
<FooterLink href="/career-resources"> <FooterLink href="/career-resources">Career Resources</FooterLink>
Career Resources
</FooterLink>
<FooterLink href="/interview-tips">Interview Tips</FooterLink> <FooterLink href="/interview-tips">Interview Tips</FooterLink>
</Grid> </Grid>
</> </>
@ -194,13 +185,9 @@ const Footer = () => {
<Grid size={{ xs: 12, sm: 6, md: 2 }}> <Grid size={{ xs: 12, sm: 6, md: 2 }}>
<FooterHeading variant="subtitle1">For Employers</FooterHeading> <FooterHeading variant="subtitle1">For Employers</FooterHeading>
<FooterLink href="/post-job">Post a Job</FooterLink> <FooterLink href="/post-job">Post a Job</FooterLink>
<FooterLink href="/search-candidates"> <FooterLink href="/search-candidates">Search Candidates</FooterLink>
Search Candidates
</FooterLink>
<FooterLink href="/company-profile">Company Profile</FooterLink> <FooterLink href="/company-profile">Company Profile</FooterLink>
<FooterLink href="/recruiting-tools"> <FooterLink href="/recruiting-tools">Recruiting Tools</FooterLink>
Recruiting Tools
</FooterLink>
<FooterLink href="/pricing-plans">Pricing Plans</FooterLink> <FooterLink href="/pricing-plans">Pricing Plans</FooterLink>
</Grid> </Grid>
</> </>
@ -223,9 +210,7 @@ const Footer = () => {
<Grid size={{ xs: 12, sm: 6, md: 3 }}> <Grid size={{ xs: 12, sm: 6, md: 3 }}>
<ContactItem> <ContactItem>
<Email sx={{ mr: 1, fontSize: 20 }} /> <Email sx={{ mr: 1, fontSize: 20 }} />
<FooterLink href="mailto:james_backstory@ketrenos.com"> <FooterLink href="mailto:james_backstory@ketrenos.com">Email</FooterLink>
Email
</FooterLink>
</ContactItem> </ContactItem>
{/* <ContactItem> {/* <ContactItem>
<Phone sx={{ mr: 1, fontSize: 20 }} /> <Phone sx={{ mr: 1, fontSize: 20 }} />
@ -233,21 +218,21 @@ const Footer = () => {
</ContactItem> */} </ContactItem> */}
<ContactItem> <ContactItem>
<LocationOn sx={{ mr: 1, fontSize: 20 }} /> <LocationOn sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="body2" sx={{ color: "white" }}> <Typography variant="body2" sx={{ color: 'white' }}>
Beaverton, OR 97003 Beaverton, OR 97003
</Typography> </Typography>
</ContactItem> </ContactItem>
</Grid> </Grid>
</Grid> </Grid>
<Divider sx={{ my: 4, backgroundColor: "rgba(211, 205, 191, 0.2)" }} /> <Divider sx={{ my: 4, backgroundColor: 'rgba(211, 205, 191, 0.2)' }} />
{/* Bottom Footer */} {/* Bottom Footer */}
<Grid container spacing={2} alignItems="center"> <Grid container spacing={2} alignItems="center">
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Copyright sx={{ fontSize: 16, mr: 1, color: "white" }} /> <Copyright sx={{ fontSize: 16, mr: 1, color: 'white' }} />
<Typography variant="body2" sx={{ color: "white" }}> <Typography variant="body2" sx={{ color: 'white' }}>
{currentYear} James P. Ketrenos. All rights reserved. {currentYear} James P. Ketrenos. All rights reserved.
</Typography> </Typography>
</Box> </Box>
@ -256,9 +241,9 @@ const Footer = () => {
{false && ( {false && (
<> <>
<Stack <Stack
direction={isMobile ? "column" : "row"} direction={isMobile ? 'column' : 'row'}
spacing={isMobile ? 1 : 3} spacing={isMobile ? 1 : 3}
sx={{ textAlign: { xs: "left", md: "right" } }} sx={{ textAlign: { xs: 'left', md: 'right' } }}
> >
<FooterLink href="/terms" sx={{ mb: 0 }}> <FooterLink href="/terms" sx={{ mb: 0 }}>
Terms of Service Terms of Service

View File

@ -1,6 +1,6 @@
// components/layout/Header.tsx // components/layout/Header.tsx
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { NavigateFunction, useLocation } from "react-router-dom"; import { NavigateFunction, useLocation } from 'react-router-dom';
import { import {
AppBar, AppBar,
Toolbar, Toolbar,
@ -27,8 +27,8 @@ import {
ListItem, ListItem,
ListItemButton, ListItemButton,
SxProps, SxProps,
} from "@mui/material"; } from '@mui/material';
import { styled, useTheme } from "@mui/material/styles"; import { styled, useTheme } from '@mui/material/styles';
import { import {
Menu as MenuIcon, Menu as MenuIcon,
Dashboard, Dashboard,
@ -38,60 +38,60 @@ import {
ExpandMore, ExpandMore,
ExpandLess, ExpandLess,
KeyboardArrowDown, KeyboardArrowDown,
} from "@mui/icons-material"; } from '@mui/icons-material';
import FaceRetouchingNaturalIcon from "@mui/icons-material/FaceRetouchingNatural"; import FaceRetouchingNaturalIcon from '@mui/icons-material/FaceRetouchingNatural';
import { getUserMenuItemsByGroup } from "config/navigationConfig"; import { getUserMenuItemsByGroup } from 'config/navigationConfig';
import { NavigationItem } from "types/navigation"; import { NavigationItem } from 'types/navigation';
import { Beta } from "components/ui/Beta"; import { Beta } from 'components/ui/Beta';
import { Candidate, Employer } from "types/types"; import { Candidate, Employer } from 'types/types';
import { SetSnackType } from "components/Snack"; import { SetSnackType } from 'components/Snack';
import { CopyBubble } from "components/CopyBubble"; import { CopyBubble } from 'components/CopyBubble';
import "components/layout/Header.css"; import 'components/layout/Header.css';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
// Styled components // Styled components
const StyledAppBar = styled(AppBar, { const StyledAppBar = styled(AppBar, {
shouldForwardProp: (prop) => prop !== "transparent", shouldForwardProp: prop => prop !== 'transparent',
})<{ transparent?: boolean }>(({ theme, transparent }) => ({ })<{ transparent?: boolean }>(({ theme, transparent }) => ({
backgroundColor: transparent ? "transparent" : theme.palette.primary.main, backgroundColor: transparent ? 'transparent' : theme.palette.primary.main,
boxShadow: transparent ? "none" : "", boxShadow: transparent ? 'none' : '',
transition: "background-color 0.3s ease", transition: 'background-color 0.3s ease',
borderRadius: 0, borderRadius: 0,
padding: 0, padding: 0,
})); }));
const NavLinksContainer = styled(Box)(({ theme }) => ({ const NavLinksContainer = styled(Box)(({ theme }) => ({
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
flex: 1, flex: 1,
[theme.breakpoints.down("md")]: { [theme.breakpoints.down('md')]: {
display: "none", display: 'none',
}, },
})); }));
const UserActionsContainer = styled(Box)(({ theme }) => ({ const UserActionsContainer = styled(Box)(({ theme }) => ({
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: theme.spacing(1), gap: theme.spacing(1),
})); }));
const UserButton = styled(Button)(({ theme }) => ({ const UserButton = styled(Button)(({ theme }) => ({
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
textTransform: "none", textTransform: 'none',
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: theme.spacing(1), gap: theme.spacing(1),
padding: theme.spacing(0.5, 1.5), padding: theme.spacing(0.5, 1.5),
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,
"&:hover": { '&:hover': {
backgroundColor: theme.palette.action.hover, backgroundColor: theme.palette.action.hover,
}, },
})); }));
const MobileDrawer = styled(Drawer)(({ theme }) => ({ const MobileDrawer = styled(Drawer)(({ theme }) => ({
"& .MuiDrawer-paper": { '& .MuiDrawer-paper': {
width: 320, width: 320,
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
}, },
@ -99,9 +99,9 @@ const MobileDrawer = styled(Drawer)(({ theme }) => ({
const DropdownButton = styled(Button)(({ theme }) => ({ const DropdownButton = styled(Button)(({ theme }) => ({
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
textTransform: "none", textTransform: 'none',
minHeight: 48, minHeight: 48,
"&:hover": { '&:hover': {
backgroundColor: theme.palette.action.hover, backgroundColor: theme.palette.action.hover,
}, },
})); }));
@ -110,7 +110,7 @@ const UserMenuContainer = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[8], boxShadow: theme.shadows[8],
overflow: "hidden", overflow: 'hidden',
minWidth: 200, minWidth: 200,
})); }));
@ -126,17 +126,11 @@ interface HeaderProps {
const Header: React.FC<HeaderProps> = (props: HeaderProps) => { const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const { const { transparent = false, className, navigate, navigationItems, sessionId } = props;
transparent = false,
className,
navigate,
navigationItems,
sessionId,
} = props;
const theme = useTheme(); const theme = useTheme();
const location = useLocation(); const location = useLocation();
const name = user?.firstName || user?.email || ""; const name = user?.firstName || user?.email || '';
// State for desktop dropdown menus // State for desktop dropdown menus
const [dropdownAnchors, setDropdownAnchors] = useState<{ const [dropdownAnchors, setDropdownAnchors] = useState<{
@ -150,18 +144,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
}>({}); }>({});
// State for user menu // State for user menu
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>( const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
null
);
const userMenuOpen = Boolean(userMenuAnchor); const userMenuOpen = Boolean(userMenuAnchor);
const isAdmin = user?.isAdmin || false; const isAdmin = user?.isAdmin || false;
// Get user menu items from navigation config // Get user menu items from navigation config
const userMenuGroups = getUserMenuItemsByGroup( const userMenuGroups = getUserMenuItemsByGroup(user?.userType || null, isAdmin);
user?.userType || null,
isAdmin
);
// Create user menu items array with proper actions // Create user menu items array with proper actions
const createUserMenuItems = () => { const createUserMenuItems = () => {
@ -174,14 +163,14 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
}> = []; }> = [];
// Add profile group items // Add profile group items
userMenuGroups.profile.forEach((item) => { userMenuGroups.profile.forEach(item => {
if (!item.divider) { if (!item.divider) {
items.push({ items.push({
id: item.id, id: item.id,
label: item.label as string, label: item.label as string,
icon: item.icon || null, icon: item.icon || null,
action: () => item.path && navigate(item.path.replace(/:.*$/, "")), action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
group: "profile", group: 'profile',
}); });
} }
}); });
@ -189,23 +178,23 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
// Add divider if we have items before system group // Add divider if we have items before system group
if (items.length > 0 && userMenuGroups.system.length > 0) { if (items.length > 0 && userMenuGroups.system.length > 0) {
items.push({ items.push({
id: "divider", id: 'divider',
label: "", label: '',
icon: null, icon: null,
action: () => {}, action: () => {},
group: "divider", group: 'divider',
}); });
} }
// Add account group items // Add account group items
userMenuGroups.account.forEach((item) => { userMenuGroups.account.forEach(item => {
if (!item.divider) { if (!item.divider) {
items.push({ items.push({
id: item.id, id: item.id,
label: item.label as string, label: item.label as string,
icon: item.icon || null, icon: item.icon || null,
action: () => item.path && navigate(item.path.replace(/:.*$/, "")), action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
group: "account", group: 'account',
}); });
} }
}); });
@ -213,23 +202,23 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
// Add divider if we have items before system group // Add divider if we have items before system group
if (items.length > 0 && userMenuGroups.system.length > 0) { if (items.length > 0 && userMenuGroups.system.length > 0) {
items.push({ items.push({
id: "divider", id: 'divider',
label: "", label: '',
icon: null, icon: null,
action: () => {}, action: () => {},
group: "divider", group: 'divider',
}); });
} }
// Add admin group items // Add admin group items
userMenuGroups.admin.forEach((item) => { userMenuGroups.admin.forEach(item => {
if (!item.divider) { if (!item.divider) {
items.push({ items.push({
id: item.id, id: item.id,
label: item.label as string, label: item.label as string,
icon: item.icon || null, icon: item.icon || null,
action: () => item.path && navigate(item.path.replace(/:.*$/, "")), action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
group: "admin", group: 'admin',
}); });
} }
}); });
@ -237,47 +226,47 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
// Add divider if we have items before system group // Add divider if we have items before system group
if (items.length > 0 && userMenuGroups.admin.length > 0) { if (items.length > 0 && userMenuGroups.admin.length > 0) {
items.push({ items.push({
id: "divider", id: 'divider',
label: "", label: '',
icon: null, icon: null,
action: () => {}, action: () => {},
group: "divider", group: 'divider',
}); });
} }
// Add system group items with special handling for logout // Add system group items with special handling for logout
userMenuGroups.system.forEach((item) => { userMenuGroups.system.forEach(item => {
if (item.id === "logout") { if (item.id === 'logout') {
items.push({ items.push({
id: "logout", id: 'logout',
label: "Logout", label: 'Logout',
icon: <Logout fontSize="small" />, icon: <Logout fontSize="small" />,
action: () => { action: () => {
logout(); logout();
navigate("/"); navigate('/');
}, },
group: "system", group: 'system',
}); });
} else if (!item.divider) { } else if (!item.divider) {
items.push({ items.push({
id: item.id, id: item.id,
label: item.label as string, label: item.label as string,
icon: item.icon || null, icon: item.icon || null,
action: () => item.path && navigate(item.path.replace(/:.*$/, "")), action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
group: "system", group: 'system',
}); });
} }
}); });
// Add other group items // Add other group items
userMenuGroups.other.forEach((item) => { userMenuGroups.other.forEach(item => {
if (!item.divider) { if (!item.divider) {
items.push({ items.push({
id: item.id, id: item.id,
label: item.label as string, label: item.label as string,
icon: item.icon || null, icon: item.icon || null,
action: () => item.path && navigate(item.path.replace(/:.*$/, "")), action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
group: "other", group: 'other',
}); });
} }
}); });
@ -299,26 +288,21 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
// Helper function to check if any child is current path // Helper function to check if any child is current path
const hasActiveChild = (item: NavigationItem): boolean => { const hasActiveChild = (item: NavigationItem): boolean => {
if (!item.children) return false; if (!item.children) return false;
return item.children.some( return item.children.some(child => isCurrentPath(child) || hasActiveChild(child));
(child) => isCurrentPath(child) || hasActiveChild(child)
);
}; };
// Desktop dropdown handlers // Desktop dropdown handlers
const handleDropdownOpen = ( const handleDropdownOpen = (event: React.MouseEvent<HTMLElement>, itemId: string) => {
event: React.MouseEvent<HTMLElement>, setDropdownAnchors(prev => ({ ...prev, [itemId]: event.currentTarget }));
itemId: string
) => {
setDropdownAnchors((prev) => ({ ...prev, [itemId]: event.currentTarget }));
}; };
const handleDropdownClose = (itemId: string) => { const handleDropdownClose = (itemId: string) => {
setDropdownAnchors((prev) => ({ ...prev, [itemId]: null })); setDropdownAnchors(prev => ({ ...prev, [itemId]: null }));
}; };
// Mobile accordion handlers // Mobile accordion handlers
const handleMobileToggle = (itemId: string) => { const handleMobileToggle = (itemId: string) => {
setMobileExpanded((prev) => ({ ...prev, [itemId]: !prev[itemId] })); setMobileExpanded(prev => ({ ...prev, [itemId]: !prev[itemId] }));
}; };
const handleDrawerToggle = () => { const handleDrawerToggle = () => {
@ -340,7 +324,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
action: () => void; action: () => void;
group?: string; group?: string;
}) => { }) => {
if (item.group !== "divider") { if (item.group !== 'divider') {
item.action(); item.action();
handleUserMenuClose(); handleUserMenuClose();
} }
@ -348,7 +332,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
// Navigation handlers // Navigation handlers
const handleNavigate = (path: string) => { const handleNavigate = (path: string) => {
navigate(path.replace(/:.*$/, "")); navigate(path.replace(/:.*$/, ''));
setMobileOpen(false); setMobileOpen(false);
// Close all dropdowns // Close all dropdowns
setDropdownAnchors({}); setDropdownAnchors({});
@ -359,10 +343,10 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
width: "100%", width: '100%',
justifyContent: "space-between", justifyContent: 'space-between',
}} }}
> >
{navigationItems.map((item, index) => { {navigationItems.map((item, index) => {
@ -374,45 +358,38 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<Box <Box
key={item.id} key={item.id}
sx={{ sx={{
mr: mr: index === 0 || index === navigationItems.length - 1 ? 'auto' : 'unset',
index === 0 || index === navigationItems.length - 1
? "auto"
: "unset",
}} }}
> >
<DropdownButton <DropdownButton
onClick={(e) => handleDropdownOpen(e, item.id)} onClick={e => handleDropdownOpen(e, item.id)}
endIcon={<KeyboardArrowDown />} endIcon={<KeyboardArrowDown />}
sx={{ sx={{
backgroundColor: isActive backgroundColor: isActive ? 'action.selected' : 'transparent',
? "action.selected" color: isActive ? 'secondary.main' : 'primary.contrastText',
: "transparent",
color: isActive ? "secondary.main" : "primary.contrastText",
}} }}
> >
{item.icon && ( {item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
<Box sx={{ mr: 1, display: "flex" }}>{item.icon}</Box>
)}
{item.label} {item.label}
</DropdownButton> </DropdownButton>
<Menu <Menu
anchorEl={dropdownAnchors[item.id]} anchorEl={dropdownAnchors[item.id]}
open={Boolean(dropdownAnchors[item.id])} open={Boolean(dropdownAnchors[item.id])}
onClose={() => handleDropdownClose(item.id)} onClose={() => handleDropdownClose(item.id)}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: "top", horizontal: "left" }} transformOrigin={{ vertical: 'top', horizontal: 'left' }}
TransitionComponent={Fade} TransitionComponent={Fade}
> >
{item.children?.map((child) => ( {item.children?.map(child => (
<MenuItem <MenuItem
key={child.id} key={child.id}
onClick={() => child.path && handleNavigate(child.path)} onClick={() => child.path && handleNavigate(child.path)}
selected={isCurrentPath(child)} selected={isCurrentPath(child)}
disabled={!child.path} disabled={!child.path}
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
"& *": { m: 0, p: 0 }, '& *': { m: 0, p: 0 },
m: 0, m: 0,
}} }}
> >
@ -429,17 +406,12 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
key={item.id} key={item.id}
onClick={() => item.path && handleNavigate(item.path)} onClick={() => item.path && handleNavigate(item.path)}
sx={{ sx={{
backgroundColor: isActive ? "action.selected" : "transparent", backgroundColor: isActive ? 'action.selected' : 'transparent',
color: isActive ? "secondary.main" : "primary.contrastText", color: isActive ? 'secondary.main' : 'primary.contrastText',
mr: mr: index === 0 || index === navigationItems.length - 1 ? 'auto' : 'unset',
index === 0 || index === navigationItems.length - 1
? "auto"
: "unset",
}} }}
> >
{item.icon && ( {item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
<Box sx={{ mr: 1, display: "flex" }}>{item.icon}</Box>
)}
{item.label} {item.label}
</DropdownButton> </DropdownButton>
); );
@ -469,41 +441,35 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
}} }}
selected={isActive} selected={isActive}
sx={{ sx={{
backgroundColor: isActive ? "action.selected" : "transparent", backgroundColor: isActive ? 'action.selected' : 'transparent',
"&.Mui-selected": { '&.Mui-selected': {
backgroundColor: "primary.main", backgroundColor: 'primary.main',
color: "primary.contrastText", color: 'primary.contrastText',
"& .MuiListItemIcon-root": { '& .MuiListItemIcon-root': {
color: "primary.contrastText", color: 'primary.contrastText',
}, },
}, },
}} }}
> >
{item.icon && ( {item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
<ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>
)}
<ListItemText <ListItemText
primary={item.label} primary={item.label}
sx={{ sx={{
"& .MuiTypography-root": { '& .MuiTypography-root': {
fontSize: depth > 0 ? "0.875rem" : "1rem", fontSize: depth > 0 ? '0.875rem' : '1rem',
fontWeight: depth === 0 ? 500 : 400, fontWeight: depth === 0 ? 500 : 400,
}, },
}} }}
/> />
{hasChildren && ( {hasChildren && (
<IconButton size="small"> <IconButton size="small">{isExpanded ? <ExpandLess /> : <ExpandMore />}</IconButton>
{isExpanded ? <ExpandLess /> : <ExpandMore />}
</IconButton>
)} )}
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
{hasChildren && ( {hasChildren && (
<Collapse in={isExpanded} timeout="auto" unmountOnExit> <Collapse in={isExpanded} timeout="auto" unmountOnExit>
<List disablePadding> <List disablePadding>
{item.children?.map((child) => {item.children?.map(child => renderNavigationItem(child, depth + 1))}
renderNavigationItem(child, depth + 1)
)}
</List> </List>
</Collapse> </Collapse>
)} )}
@ -513,11 +479,11 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
return ( return (
<List sx={{ pt: 0 }}> <List sx={{ pt: 0 }}>
{navigationItems.map((item) => renderNavigationItem(item))} {navigationItems.map(item => renderNavigationItem(item))}
<Divider sx={{ my: 1 }} /> <Divider sx={{ my: 1 }} />
{!user && ( {!user && (
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton onClick={() => handleNavigate("/login")}> <ListItemButton onClick={() => handleNavigate('/login')}>
<ListItemText primary="Login" /> <ListItemText primary="Login" />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
@ -532,16 +498,12 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<UserMenuContainer> <UserMenuContainer>
<List dense> <List dense>
{userMenuItems.map((item, index) => {userMenuItems.map((item, index) =>
item.group === "divider" ? ( item.group === 'divider' ? (
<Divider key={`divider-${index}`} /> <Divider key={`divider-${index}`} />
) : ( ) : (
<ListItem key={item.id}> <ListItem key={item.id}>
<ListItemButton onClick={() => handleUserMenuAction(item)}> <ListItemButton onClick={() => handleUserMenuAction(item)}>
{item.icon && ( {item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
<ListItemIcon sx={{ minWidth: 36 }}>
{item.icon}
</ListItemIcon>
)}
<ListItemText primary={item.label} /> <ListItemText primary={item.label} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
@ -559,9 +521,9 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<Button <Button
color="info" color="info"
variant="contained" variant="contained"
onClick={() => navigate("/login")} onClick={() => navigate('/login')}
sx={{ sx={{
display: { xs: "none", sm: "block" }, display: { xs: 'none', sm: 'block' },
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
}} }}
> >
@ -574,9 +536,9 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<> <>
<UserButton <UserButton
onClick={handleUserMenuOpen} onClick={handleUserMenuOpen}
aria-controls={userMenuOpen ? "user-menu" : undefined} aria-controls={userMenuOpen ? 'user-menu' : undefined}
aria-haspopup="true" aria-haspopup="true"
aria-expanded={userMenuOpen ? "true" : undefined} aria-expanded={userMenuOpen ? 'true' : undefined}
> >
<Avatar <Avatar
sx={{ sx={{
@ -587,7 +549,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
> >
{name.charAt(0).toUpperCase()} {name.charAt(0).toUpperCase()}
</Avatar> </Avatar>
<Box sx={{ display: { xs: "none", sm: "block" } }}>{name}</Box> <Box sx={{ display: { xs: 'none', sm: 'block' } }}>{name}</Box>
<ExpandMore fontSize="small" /> <ExpandMore fontSize="small" />
</UserButton> </UserButton>
@ -597,12 +559,12 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
anchorEl={userMenuAnchor} anchorEl={userMenuAnchor}
onClose={handleUserMenuClose} onClose={handleUserMenuClose}
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: 'bottom',
horizontal: "right", horizontal: 'right',
}} }}
transformOrigin={{ transformOrigin={{
vertical: "top", vertical: 'top',
horizontal: "right", horizontal: 'right',
}} }}
TransitionComponent={Fade} TransitionComponent={Fade}
> >
@ -617,7 +579,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
position="fixed" position="fixed"
transparent={transparent} transparent={transparent}
className={className} className={className}
sx={{ overflow: "hidden" }} sx={{ overflow: 'hidden' }}
> >
<Container maxWidth="xl"> <Container maxWidth="xl">
<Toolbar disableGutters> <Toolbar disableGutters>
@ -635,7 +597,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
aria-label="open drawer" aria-label="open drawer"
edge="end" edge="end"
onClick={handleDrawerToggle} onClick={handleDrawerToggle}
sx={{ display: { md: "none" } }} sx={{ display: { md: 'none' } }}
> >
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
@ -651,13 +613,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
width: 36, width: 36,
height: 36, height: 36,
opacity: 1, opacity: 1,
bgcolor: "inherit", bgcolor: 'inherit',
"&:hover": { bgcolor: "action.hover", opacity: 1 }, '&:hover': { bgcolor: 'action.hover', opacity: 1 },
}} }}
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`} content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
onClick={() => { onClick={() => {
navigate(`${window.location.pathname}?id=${sessionId}`); navigate(`${window.location.pathname}?id=${sessionId}`);
setSnack("Link copied!"); setSnack('Link copied!');
}} }}
size="large" size="large"
/> />
@ -679,9 +641,9 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
</Toolbar> </Toolbar>
</Container> </Container>
<Beta <Beta
sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} sx={{ left: '-90px', '& .mobile': { right: '-72px' } }}
onClick={() => { onClick={() => {
navigate("/docs/beta"); navigate('/docs/beta');
}} }}
/> />
</StyledAppBar> </StyledAppBar>

View File

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

View File

@ -1,8 +1,8 @@
import React from "react"; import React from 'react';
import { Typography, Avatar } from "@mui/material"; import { Typography, Avatar } from '@mui/material';
import { useTheme } from "@mui/material/styles"; import { useTheme } from '@mui/material/styles';
import "components/layout/Header.css"; import 'components/layout/Header.css';
const BackstoryLogo = () => { const BackstoryLogo = () => {
const theme = useTheme(); const theme = useTheme();
@ -12,17 +12,17 @@ const BackstoryLogo = () => {
className="BackstoryLogo" className="BackstoryLogo"
noWrap noWrap
sx={{ sx={{
cursor: "pointer", cursor: 'pointer',
fontWeight: 700, fontWeight: 700,
letterSpacing: ".2rem", letterSpacing: '.2rem',
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
textDecoration: "none", textDecoration: 'none',
display: "inline-flex", display: 'inline-flex',
flexDirection: "row", flexDirection: 'row',
alignItems: "center", alignItems: 'center',
verticalAlign: "center", verticalAlign: 'center',
gap: 1, gap: 1,
textTransform: "uppercase", textTransform: 'uppercase',
}} }}
> >
<Avatar <Avatar

View File

@ -1,9 +1,9 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from 'react';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from '@mui/material/useMediaQuery';
import { SxProps, useTheme } from "@mui/material/styles"; import { SxProps, useTheme } from '@mui/material/styles';
import "./Beta.css"; import './Beta.css';
type BetaProps = { type BetaProps = {
adaptive?: boolean; adaptive?: boolean;
@ -15,7 +15,7 @@ const Beta: React.FC<BetaProps> = (props: BetaProps) => {
const { onClick, adaptive = true, sx = {} } = props; const { onClick, adaptive = true, sx = {} } = props;
const betaRef = useRef<HTMLElement | null>(null); const betaRef = useRef<HTMLElement | null>(null);
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [animationKey, setAnimationKey] = useState<number>(0); const [animationKey, setAnimationKey] = useState<number>(0);
const [firstPass, setFirstPass] = useState<boolean>(true); const [firstPass, setFirstPass] = useState<boolean>(true);
@ -31,24 +31,21 @@ const Beta: React.FC<BetaProps> = (props: BetaProps) => {
if (!betaRef.current) return; if (!betaRef.current) return;
// Increment animation key to force React to recreate the element // Increment animation key to force React to recreate the element
setAnimationKey((prevKey) => prevKey + 1); setAnimationKey(prevKey => prevKey + 1);
// Ensure the animate class is present // Ensure the animate class is present
betaRef.current.classList.add("animate"); betaRef.current.classList.add('animate');
}; };
return ( return (
<Box <Box
sx={sx} sx={sx}
className={`beta-clipper ${adaptive && isMobile && "mobile"}`} className={`beta-clipper ${adaptive && isMobile && 'mobile'}`}
onClick={(e) => { onClick={e => {
onClick && onClick(e); onClick && onClick(e);
}} }}
> >
<Box <Box ref={betaRef} className={`beta-label ${adaptive && isMobile && 'mobile'}`}>
ref={betaRef}
className={`beta-label ${adaptive && isMobile && "mobile"}`}
>
<Box key={animationKey} className="particles"></Box> <Box key={animationKey} className="particles"></Box>
<Box>BETA</Box> <Box>BETA</Box>
</Box> </Box>

View File

@ -1,43 +1,30 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from 'react';
import { import { Box, Link, Typography, Avatar, Grid, SxProps, Tooltip, IconButton } from '@mui/material';
Box, import { Card, CardContent, Divider, useTheme } from '@mui/material';
Link, import DeleteIcon from '@mui/icons-material/Delete';
Typography, import { useMediaQuery } from '@mui/material';
Avatar, import { Candidate, CandidateAI } from 'types/types';
Grid, import { CopyBubble } from 'components/CopyBubble';
SxProps, import { rest } from 'lodash';
Tooltip, import { AIBanner } from 'components/ui/AIBanner';
IconButton, import { useAuth } from 'hooks/AuthContext';
} from "@mui/material"; import { DeleteConfirmation } from '../DeleteConfirmation';
import { Card, CardContent, Divider, useTheme } from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import { useMediaQuery } from "@mui/material";
import { Candidate, CandidateAI } from "types/types";
import { CopyBubble } from "components/CopyBubble";
import { rest } from "lodash";
import { AIBanner } from "components/ui/AIBanner";
import { useAuth } from "hooks/AuthContext";
import { DeleteConfirmation } from "../DeleteConfirmation";
interface CandidateInfoProps { interface CandidateInfoProps {
candidate: Candidate; candidate: Candidate;
sx?: SxProps; sx?: SxProps;
action?: string; action?: string;
elevation?: number; elevation?: number;
variant?: "minimal" | "small" | "normal" | undefined; variant?: 'minimal' | 'small' | 'normal' | undefined;
} }
const CandidateInfo: React.FC<CandidateInfoProps> = ( const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
props: CandidateInfoProps
) => {
const { candidate } = props; const { candidate } = props;
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const { sx, action = "", elevation = 1, variant = "normal" } = props; const { sx, action = '', elevation = 1, variant = 'normal' } = props;
const theme = useTheme(); const theme = useTheme();
const isMobile = const isMobile = useMediaQuery(theme.breakpoints.down('md')) || variant === 'minimal';
useMediaQuery(theme.breakpoints.down("md")) || variant === "minimal"; const ai: CandidateAI | null = 'isAI' in candidate ? (candidate as CandidateAI) : null;
const ai: CandidateAI | null =
"isAI" in candidate ? (candidate as CandidateAI) : null;
const isAdmin = user?.isAdmin; const isAdmin = user?.isAdmin;
// State for description expansion // State for description expansion
@ -67,76 +54,68 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
transition: "all 0.3s ease", transition: 'all 0.3s ease',
flexGrow: 1, flexGrow: 1,
p: isMobile ? 1 : 2, p: isMobile ? 1 : 2,
height: "100%", height: '100%',
flexDirection: "column", flexDirection: 'column',
alignItems: "stretch", alignItems: 'stretch',
position: "relative", position: 'relative',
overflow: "hidden", overflow: 'hidden',
...sx, ...sx,
}} }}
{...rest} {...rest}
> >
{ai && <AIBanner variant={variant} />} {ai && <AIBanner variant={variant} />}
<Box sx={{ display: "flex", flexDirection: "row" }}> <Box sx={{ display: 'flex', flexDirection: 'row' }}>
<Avatar <Avatar
src={ src={candidate.profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''}
candidate.profileImage
? `/api/1.0/candidates/profile/${candidate.username}`
: ""
}
alt={`${candidate.fullName}'s profile`} alt={`${candidate.fullName}'s profile`}
sx={{ sx={{
alignSelf: "flex-start", alignSelf: 'flex-start',
width: isMobile ? 40 : 80, width: isMobile ? 40 : 80,
height: isMobile ? 40 : 80, height: isMobile ? 40 : 80,
border: "2px solid #e0e0e0", border: '2px solid #e0e0e0',
}} }}
/> />
<Box sx={{ ml: 1 }}> <Box sx={{ ml: 1 }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "flex-start", alignItems: 'flex-start',
mb: 1, mb: 1,
}} }}
> >
<Box> <Box>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: isMobile ? "column" : "row", flexDirection: isMobile ? 'column' : 'row',
alignItems: "left", alignItems: 'left',
gap: 1, gap: 1,
"& > .MuiTypography-root": { m: 0 }, '& > .MuiTypography-root': { m: 0 },
}} }}
> >
{action !== "" && ( {action !== '' && <Typography variant="body1">{action}</Typography>}
<Typography variant="body1">{action}</Typography> {action === '' && (
)}
{action === "" && (
<Typography <Typography
variant="h5" variant="h5"
component="h1" component="h1"
sx={{ sx={{
fontWeight: "bold", fontWeight: 'bold',
whiteSpace: "nowrap", whiteSpace: 'nowrap',
}} }}
> >
{candidate.fullName} {candidate.fullName}
</Typography> </Typography>
)} )}
</Box> </Box>
<Box sx={{ fontSize: "0.75rem", alignItems: "center" }}> <Box sx={{ fontSize: '0.75rem', alignItems: 'center' }}>
<Link <Link href={`/u/${candidate.username}`}>{`/u/${candidate.username}`}</Link>
href={`/u/${candidate.username}`}
>{`/u/${candidate.username}`}</Link>
<CopyBubble <CopyBubble
onClick={(event: any) => { onClick={(event: any) => {
event.stopPropagation(); event.stopPropagation();
@ -151,20 +130,20 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (
</Box> </Box>
<Box> <Box>
{!isMobile && variant === "normal" && ( {!isMobile && variant === 'normal' && (
<Box sx={{ minHeight: "5rem" }}> <Box sx={{ minHeight: '5rem' }}>
<Typography <Typography
ref={descriptionRef} ref={descriptionRef}
variant="body1" variant="body1"
color="text.secondary" color="text.secondary"
sx={{ sx={{
display: "-webkit-box", display: '-webkit-box',
WebkitLineClamp: isDescriptionExpanded ? "unset" : 3, WebkitLineClamp: isDescriptionExpanded ? 'unset' : 3,
WebkitBoxOrient: "vertical", WebkitBoxOrient: 'vertical',
overflow: "hidden", overflow: 'hidden',
textOverflow: "ellipsis", textOverflow: 'ellipsis',
lineHeight: 1.5, lineHeight: 1.5,
fontSize: "0.8rem !important", fontSize: '0.8rem !important',
}} }}
> >
{candidate.description} {candidate.description}
@ -173,37 +152,37 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (
<Link <Link
component="button" component="button"
variant="body2" variant="body2"
onClick={(e) => { onClick={e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsDescriptionExpanded(!isDescriptionExpanded); setIsDescriptionExpanded(!isDescriptionExpanded);
}} }}
sx={{ sx={{
color: theme.palette.primary.main, color: theme.palette.primary.main,
textDecoration: "none", textDecoration: 'none',
cursor: "pointer", cursor: 'pointer',
fontSize: "0.725rem", fontSize: '0.725rem',
fontWeight: 500, fontWeight: 500,
mt: 0.5, mt: 0.5,
display: "block", display: 'block',
"&:hover": { '&:hover': {
textDecoration: "underline", textDecoration: 'underline',
}, },
}} }}
> >
[{isDescriptionExpanded ? "less" : "more"}] [{isDescriptionExpanded ? 'less' : 'more'}]
</Link> </Link>
)} )}
</Box> </Box>
)} )}
{variant !== "small" && variant !== "minimal" && ( {variant !== 'small' && variant !== 'minimal' && (
<> <>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
{candidate.location && ( {candidate.location && (
<Typography variant="body2" sx={{ mb: 1 }}> <Typography variant="body2" sx={{ mb: 1 }}>
<strong>Location:</strong> {candidate.location.city},{" "} <strong>Location:</strong> {candidate.location.city},{' '}
{candidate.location.state || candidate.location.country} {candidate.location.state || candidate.location.country}
</Typography> </Typography>
)} )}
@ -223,15 +202,15 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (
{isAdmin && ai && ( {isAdmin && ai && (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
justifyContent: "flex-start", justifyContent: 'flex-start',
}} }}
> >
<Tooltip title="Delete Job"> <Tooltip title="Delete Job">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
deleteCandidate(candidate.id); deleteCandidate(candidate.id);
}} }}

View File

@ -1,14 +1,14 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import { BackstoryElementProps } from "components/BackstoryTab"; import { BackstoryElementProps } from 'components/BackstoryTab';
import { CandidateInfo } from "components/ui/CandidateInfo"; import { CandidateInfo } from 'components/ui/CandidateInfo';
import { Candidate, CandidateAI } from "types/types"; import { Candidate, CandidateAI } from 'types/types';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedCandidate } from "hooks/GlobalContext"; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import { Paper } from "@mui/material"; import { Paper } from '@mui/material';
interface CandidatePickerProps extends BackstoryElementProps { interface CandidatePickerProps extends BackstoryElementProps {
onSelect?: (candidate: Candidate) => void; onSelect?: (candidate: Candidate) => void;
@ -31,8 +31,8 @@ const CandidatePicker = (props: CandidatePickerProps) => {
const results = await apiClient.getCandidates(); const results = await apiClient.getCandidates();
const candidates: Candidate[] = results.data; const candidates: Candidate[] = results.data;
candidates.sort((a, b) => { candidates.sort((a, b) => {
const aIsAi = "isAI" in a ? 1 : 0; const aIsAi = 'isAI' in a ? 1 : 0;
const bIsAi = "isAI" in b ? 1 : 0; const bIsAi = 'isAI' in b ? 1 : 0;
let result = aIsAi - bIsAi; let result = aIsAi - bIsAi;
if (result === 0) { if (result === 0) {
result = a.lastName.localeCompare(b.lastName); result = a.lastName.localeCompare(b.lastName);
@ -47,7 +47,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
}); });
setCandidates(candidates); setCandidates(candidates);
} catch (err) { } catch (err) {
setSnack("" + err); setSnack('' + err);
} }
}; };
@ -55,13 +55,13 @@ const CandidatePicker = (props: CandidatePickerProps) => {
}, [candidates, setSnack]); }, [candidates, setSnack]);
return ( return (
<Box sx={{ display: "flex", flexDirection: "column", ...sx }}> <Box sx={{ display: 'flex', flexDirection: 'column', ...sx }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
gap: 1, gap: 1,
flexWrap: "wrap", flexWrap: 'wrap',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
{candidates?.map((u, i) => ( {candidates?.map((u, i) => (
@ -70,20 +70,19 @@ const CandidatePicker = (props: CandidatePickerProps) => {
onClick={() => { onClick={() => {
onSelect ? onSelect(u) : setSelectedCandidate(u); onSelect ? onSelect(u) : setSelectedCandidate(u);
}} }}
sx={{ cursor: "pointer" }} sx={{ cursor: 'pointer' }}
> >
<CandidateInfo <CandidateInfo
variant="small" variant="small"
sx={{ sx={{
maxWidth: "100%", maxWidth: '100%',
minWidth: "320px", minWidth: '320px',
width: "320px", width: '320px',
cursor: "pointer", cursor: 'pointer',
backgroundColor: backgroundColor: selectedCandidate?.id === u.id ? '#f0f0f0' : 'inherit',
selectedCandidate?.id === u.id ? "#f0f0f0" : "inherit", border: '2px solid transparent',
border: "2px solid transparent", '&:hover': {
"&:hover": { border: '2px solid orange',
border: "2px solid orange",
}, },
}} }}
candidate={u} candidate={u}

View File

@ -1,9 +1,9 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from 'react';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from '@mui/material/useMediaQuery';
import { SxProps, useTheme } from "@mui/material/styles"; import { SxProps, useTheme } from '@mui/material/styles';
import "./ComingSoon.css"; import './ComingSoon.css';
type ComingSoonProps = { type ComingSoonProps = {
children?: React.ReactNode; children?: React.ReactNode;

View File

@ -1,4 +1,4 @@
import React, { JSX, useActionState, useEffect, useRef, useState } from "react"; import React, { JSX, useActionState, useEffect, useRef, useState } from 'react';
import { import {
Box, Box,
Link, Link,
@ -15,52 +15,43 @@ import {
LinearProgress, LinearProgress,
IconButton, IconButton,
Tooltip, Tooltip,
} from "@mui/material"; } from '@mui/material';
import { Card, CardContent, Divider, useTheme } from "@mui/material"; import { Card, CardContent, Divider, useTheme } from '@mui/material';
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from '@mui/icons-material/Delete';
import { useMediaQuery } from "@mui/material"; import { useMediaQuery } from '@mui/material';
import { Job } from "types/types"; import { Job } from 'types/types';
import { CopyBubble } from "components/CopyBubble"; import { CopyBubble } from 'components/CopyBubble';
import { rest } from "lodash"; import { rest } from 'lodash';
import { AIBanner } from "components/ui/AIBanner"; import { AIBanner } from 'components/ui/AIBanner';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { DeleteConfirmation } from "../DeleteConfirmation"; import { DeleteConfirmation } from '../DeleteConfirmation';
import { import { Build, CheckCircle, Description, Psychology, Star, Work } from '@mui/icons-material';
Build, import ModelTrainingIcon from '@mui/icons-material/ModelTraining';
CheckCircle, import { StatusIcon, StatusBox } from 'components/ui/StatusIcon';
Description, import RestoreIcon from '@mui/icons-material/Restore';
Psychology, import SaveIcon from '@mui/icons-material/Save';
Star, import * as Types from 'types/types';
Work, import { useAppState } from 'hooks/GlobalContext';
} from "@mui/icons-material"; import { StyledMarkdown } from 'components/StyledMarkdown';
import ModelTrainingIcon from "@mui/icons-material/ModelTraining";
import { StatusIcon, StatusBox } from "components/ui/StatusIcon";
import RestoreIcon from "@mui/icons-material/Restore";
import SaveIcon from "@mui/icons-material/Save";
import * as Types from "types/types";
import { useAppState } from "hooks/GlobalContext";
import { StyledMarkdown } from "components/StyledMarkdown";
interface JobInfoProps { interface JobInfoProps {
job: Job; job: Job;
sx?: SxProps; sx?: SxProps;
action?: string; action?: string;
elevation?: number; elevation?: number;
variant?: "minimal" | "small" | "normal" | "all" | null; variant?: 'minimal' | 'small' | 'normal' | 'all' | null;
} }
const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => { const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const { job } = props; const { job } = props;
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const { sx, action = "", elevation = 1, variant = "normal" } = props; const { sx, action = '', elevation = 1, variant = 'normal' } = props;
const theme = useTheme(); const theme = useTheme();
const isMobile = const isMobile = useMediaQuery(theme.breakpoints.down('md')) || variant === 'minimal';
useMediaQuery(theme.breakpoints.down("md")) || variant === "minimal";
const isAdmin = user?.isAdmin; const isAdmin = user?.isAdmin;
const [adminStatus, setAdminStatus] = useState<string | null>(null); const [adminStatus, setAdminStatus] = useState<string | null>(null);
const [adminStatusType, setAdminStatusType] = const [adminStatusType, setAdminStatusType] = useState<Types.ApiActivityType | null>(null);
useState<Types.ApiActivityType | null>(null);
const [activeJob, setActiveJob] = useState<Types.Job>({ const [activeJob, setActiveJob] = useState<Types.Job>({
...job, ...job,
}); /* Copy of job */ }); /* Copy of job */
@ -100,36 +91,33 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
} }
const handleSave = async () => { const handleSave = async () => {
const newJob = await apiClient.updateJob(job.id || "", { const newJob = await apiClient.updateJob(job.id || '', {
description: activeJob.description, description: activeJob.description,
requirements: activeJob.requirements, requirements: activeJob.requirements,
}); });
job.updatedAt = newJob.updatedAt; job.updatedAt = newJob.updatedAt;
setActiveJob(newJob); setActiveJob(newJob);
setSnack("Job updated."); setSnack('Job updated.');
}; };
const handleRefresh = () => { const handleRefresh = () => {
setAdminStatus("Re-extracting Job information..."); setAdminStatus('Re-extracting Job information...');
const jobStatusHandlers = { const jobStatusHandlers = {
onStatus: (status: Types.ChatMessageStatus) => { onStatus: (status: Types.ChatMessageStatus) => {
console.log("status:", status.content); console.log('status:', status.content);
setAdminStatusType(status.activity); setAdminStatusType(status.activity);
setAdminStatus(status.content); setAdminStatus(status.content);
}, },
onMessage: async (jobMessage: Types.JobRequirementsMessage) => { onMessage: async (jobMessage: Types.JobRequirementsMessage) => {
const newJob: Types.Job = jobMessage.job; const newJob: Types.Job = jobMessage.job;
console.log("onMessage - job", newJob); console.log('onMessage - job', newJob);
newJob.id = job.id; newJob.id = job.id;
newJob.createdAt = job.createdAt; newJob.createdAt = job.createdAt;
const updatedJob: Types.Job = await apiClient.updateJob( const updatedJob: Types.Job = await apiClient.updateJob(job.id || '', newJob);
job.id || "",
newJob
);
setActiveJob(updatedJob); setActiveJob(updatedJob);
}, },
onError: (error: Types.ChatMessageError) => { onError: (error: Types.ChatMessageError) => {
console.log("onError", error); console.log('onError', error);
setAdminStatusType(null); setAdminStatusType(null);
setAdminStatus(null); setAdminStatus(null);
}, },
@ -138,10 +126,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
setAdminStatus(null); setAdminStatus(null);
}, },
}; };
apiClient.createJobFromDescription( apiClient.createJobFromDescription(activeJob.description, jobStatusHandlers);
activeJob.description,
jobStatusHandlers
);
}; };
const renderRequirementSection = ( const renderRequirementSection = (
@ -154,11 +139,11 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
return ( return (
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2 }}>
<Box sx={{ display: "flex", alignItems: "center", mb: 1.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}>
{icon} {icon}
<Typography <Typography
variant="subtitle1" variant="subtitle1"
sx={{ ml: 1, fontWeight: 600, fontSize: "0.85rem !important" }} sx={{ ml: 1, fontWeight: 600, fontSize: '0.85rem !important' }}
> >
{title} {title}
</Typography> </Typography>
@ -167,7 +152,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
label="Required" label="Required"
size="small" size="small"
color="error" color="error"
sx={{ ml: 1, fontSize: "0.75rem !important" }} sx={{ ml: 1, fontSize: '0.75rem !important' }}
/> />
)} )}
</Box> </Box>
@ -178,7 +163,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
label={item} label={item}
variant="outlined" variant="outlined"
size="small" size="small"
sx={{ mb: 1, fontSize: "0.75rem !important" }} sx={{ mb: 1, fontSize: '0.75rem !important' }}
/> />
))} ))}
</Stack> </Stack>
@ -190,10 +175,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
if (!activeJob.requirements) return null; if (!activeJob.requirements) return null;
return ( return (
<Card <Card elevation={0} sx={{ m: 0, p: 0, mt: 2, background: 'transparent !important' }}>
elevation={0}
sx={{ m: 0, p: 0, mt: 2, background: "transparent !important" }}
>
<CardHeader <CardHeader
title="Job Requirements Analysis" title="Job Requirements Analysis"
avatar={<CheckCircle color="success" />} avatar={<CheckCircle color="success" />}
@ -201,49 +183,49 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
/> />
<CardContent sx={{ p: 0 }}> <CardContent sx={{ p: 0 }}>
{renderRequirementSection( {renderRequirementSection(
"Technical Skills (Required)", 'Technical Skills (Required)',
activeJob.requirements.technicalSkills.required, activeJob.requirements.technicalSkills.required,
<Build color="primary" />, <Build color="primary" />,
true true
)} )}
{renderRequirementSection( {renderRequirementSection(
"Technical Skills (Preferred)", 'Technical Skills (Preferred)',
activeJob.requirements.technicalSkills.preferred, activeJob.requirements.technicalSkills.preferred,
<Build color="action" /> <Build color="action" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience Requirements (Required)", 'Experience Requirements (Required)',
activeJob.requirements.experienceRequirements.required, activeJob.requirements.experienceRequirements.required,
<Work color="primary" />, <Work color="primary" />,
true true
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience Requirements (Preferred)", 'Experience Requirements (Preferred)',
activeJob.requirements.experienceRequirements.preferred, activeJob.requirements.experienceRequirements.preferred,
<Work color="action" /> <Work color="action" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Soft Skills", 'Soft Skills',
activeJob.requirements.softSkills, activeJob.requirements.softSkills,
<Psychology color="secondary" /> <Psychology color="secondary" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience", 'Experience',
activeJob.requirements.experience, activeJob.requirements.experience,
<Star color="warning" /> <Star color="warning" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Education", 'Education',
activeJob.requirements.education, activeJob.requirements.education,
<Description color="info" /> <Description color="info" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Certifications", 'Certifications',
activeJob.requirements.certifications, activeJob.requirements.certifications,
<CheckCircle color="success" /> <CheckCircle color="success" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Preferred Attributes", 'Preferred Attributes',
activeJob.requirements.preferredAttributes, activeJob.requirements.preferredAttributes,
<Star color="secondary" /> <Star color="secondary" />
)} )}
@ -255,61 +237,61 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
borderColor: "transparent", borderColor: 'transparent',
borderWidth: 2, borderWidth: 2,
borderStyle: "solid", borderStyle: 'solid',
transition: "all 0.3s ease", transition: 'all 0.3s ease',
flexDirection: "column", flexDirection: 'column',
minWidth: 0, minWidth: 0,
opacity: deleted ? 0.5 : 1.0, opacity: deleted ? 0.5 : 1.0,
backgroundColor: deleted backgroundColor: deleted
? theme.palette.action.disabledBackground ? theme.palette.action.disabledBackground
: theme.palette.background.paper, : theme.palette.background.paper,
pointerEvents: deleted ? "none" : "auto", pointerEvents: deleted ? 'none' : 'auto',
...sx, ...sx,
}} }}
{...rest} {...rest}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
p: 1, p: 1,
pb: 0, pb: 0,
height: "100%", height: '100%',
flexDirection: "column", flexDirection: 'column',
alignItems: "stretch", alignItems: 'stretch',
position: "relative", position: 'relative',
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: isMobile || variant === "small" ? "column" : "row", flexDirection: isMobile || variant === 'small' ? 'column' : 'row',
"& > div > div > :first-of-type": { '& > div > div > :first-of-type': {
fontWeight: "bold", fontWeight: 'bold',
whiteSpace: "nowrap", whiteSpace: 'nowrap',
}, },
"& > div > div > :last-of-type": { mb: 0.75, mr: 1 }, '& > div > div > :last-of-type': { mb: 0.75, mr: 1 },
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: isMobile ? "row" : "column", flexDirection: isMobile ? 'row' : 'column',
flexGrow: 1, flexGrow: 1,
gap: 1, gap: 1,
}} }}
> >
{activeJob.company && ( {activeJob.company && (
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: '0.8rem' }}>
<Box>Company</Box> <Box>Company</Box>
<Box sx={{ whiteSpace: "nowrap" }}>{activeJob.company}</Box> <Box sx={{ whiteSpace: 'nowrap' }}>{activeJob.company}</Box>
</Box> </Box>
)} )}
{activeJob.title && ( {activeJob.title && (
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: '0.8rem' }}>
<Box>Title</Box> <Box>Title</Box>
<Box>{activeJob.title}</Box> <Box>{activeJob.title}</Box>
</Box> </Box>
@ -317,30 +299,27 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
width: width: variant !== 'small' && variant !== 'minimal' ? '75%' : '100%',
variant !== "small" && variant !== "minimal" ? "75%" : "100%",
}} }}
> >
{!isMobile && activeJob.summary && ( {!isMobile && activeJob.summary && (
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: '0.8rem' }}>
<Box>Summary</Box> <Box>Summary</Box>
<Box <Box sx={{ minHeight: variant === 'small' ? '5rem' : 'inherit' }}>
sx={{ minHeight: variant === "small" ? "5rem" : "inherit" }}
>
<Typography <Typography
ref={descriptionRef} ref={descriptionRef}
variant="body1" variant="body1"
color="text.secondary" color="text.secondary"
sx={{ sx={{
display: "-webkit-box", display: '-webkit-box',
WebkitLineClamp: isDescriptionExpanded ? "unset" : 3, WebkitLineClamp: isDescriptionExpanded ? 'unset' : 3,
WebkitBoxOrient: "vertical", WebkitBoxOrient: 'vertical',
overflow: "hidden", overflow: 'hidden',
textOverflow: "ellipsis", textOverflow: 'ellipsis',
lineHeight: 1.5, lineHeight: 1.5,
fontSize: "0.8rem !important", fontSize: '0.8rem !important',
}} }}
> >
{activeJob.summary} {activeJob.summary}
@ -349,25 +328,25 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Link <Link
component="button" component="button"
variant="body2" variant="body2"
onClick={(e) => { onClick={e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsDescriptionExpanded(!isDescriptionExpanded); setIsDescriptionExpanded(!isDescriptionExpanded);
}} }}
sx={{ sx={{
color: theme.palette.primary.main, color: theme.palette.primary.main,
textDecoration: "none", textDecoration: 'none',
cursor: "pointer", cursor: 'pointer',
fontSize: "0.725rem", fontSize: '0.725rem',
fontWeight: 500, fontWeight: 500,
mt: 0.5, mt: 0.5,
display: "block", display: 'block',
"&:hover": { '&:hover': {
textDecoration: "underline", textDecoration: 'underline',
}, },
}} }}
> >
[{isDescriptionExpanded ? "less" : "more"}] [{isDescriptionExpanded ? 'less' : 'more'}]
</Link> </Link>
)} )}
</Box> </Box>
@ -376,13 +355,12 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
</Box> </Box>
</Box> </Box>
{variant !== "small" && variant !== "minimal" && ( {variant !== 'small' && variant !== 'minimal' && (
<> <>
{activeJob.details && ( {activeJob.details && (
<Typography variant="body2" sx={{ mb: 1 }}> <Typography variant="body2" sx={{ mb: 1 }}>
<strong>Location:</strong> {activeJob.details.location.city},{" "} <strong>Location:</strong> {activeJob.details.location.city},{' '}
{activeJob.details.location.state || {activeJob.details.location.state || activeJob.details.location.country}
activeJob.details.location.country}
</Typography> </Typography>
)} )}
{activeJob.owner && ( {activeJob.owner && (
@ -403,14 +381,11 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Typography variant="caption">Job ID: {job.id}</Typography> <Typography variant="caption">Job ID: {job.id}</Typography>
</> </>
)} )}
{variant === "all" && ( {variant === 'all' && (
<StyledMarkdown <StyledMarkdown sx={{ display: 'flex' }} content={activeJob.description} />
sx={{ display: "flex" }}
content={activeJob.description}
/>
)} )}
{variant !== "small" && variant !== "minimal" && ( {variant !== 'small' && variant !== 'minimal' && (
<Box> <Box>
<Divider /> <Divider />
{renderJobRequirements()} {renderJobRequirements()}
@ -418,16 +393,16 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
)} )}
{isAdmin && ( {isAdmin && (
<Box sx={{ display: "flex", flexDirection: "column", p: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', p: 1 }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
pl: 1, pl: 1,
pr: 1, pr: 1,
gap: 1, gap: 1,
alignContent: "center", alignContent: 'center',
height: "32px", height: '32px',
}} }}
> >
{(job.updatedAt && job.updatedAt.toISOString()) !== {(job.updatedAt && job.updatedAt.toISOString()) !==
@ -435,7 +410,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Tooltip title="Save Job"> <Tooltip title="Save Job">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
handleSave(); handleSave();
}} }}
@ -447,7 +422,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Tooltip title="Delete Job"> <Tooltip title="Delete Job">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
deleteJob(job.id); deleteJob(job.id);
setDeleted(true); setDeleted(true);
@ -459,7 +434,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Tooltip title="Reset Job"> <Tooltip title="Reset Job">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
handleReset(); handleReset();
}} }}
@ -470,7 +445,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Tooltip title="Reprocess Job"> <Tooltip title="Reprocess Job">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
handleRefresh(); handleRefresh();
}} }}
@ -484,7 +459,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<StatusBox> <StatusBox>
{adminStatusType && <StatusIcon type={adminStatusType} />} {adminStatusType && <StatusIcon type={adminStatusType} />}
<Typography variant="body2" sx={{ ml: 1 }}> <Typography variant="body2" sx={{ ml: 1 }}>
{adminStatus || "Processing..."} {adminStatus || 'Processing...'}
</Typography> </Typography>
</StatusBox> </StatusBox>
{adminStatus && <LinearProgress sx={{ mt: 1 }} />} {adminStatus && <LinearProgress sx={{ mt: 1 }} />}

View File

@ -1,14 +1,14 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import { BackstoryElementProps } from "components/BackstoryTab"; import { BackstoryElementProps } from 'components/BackstoryTab';
import { JobInfo } from "components/ui/JobInfo"; import { JobInfo } from 'components/ui/JobInfo';
import { Job } from "types/types"; import { Job } from 'types/types';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedJob } from "hooks/GlobalContext"; import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
import { Paper } from "@mui/material"; import { Paper } from '@mui/material';
interface JobPickerProps extends BackstoryElementProps { interface JobPickerProps extends BackstoryElementProps {
onSelect?: (job: Job) => void; onSelect?: (job: Job) => void;
@ -30,15 +30,15 @@ const JobPicker = (props: JobPickerProps) => {
const results = await apiClient.getJobs(); const results = await apiClient.getJobs();
const jobs: Job[] = results.data; const jobs: Job[] = results.data;
jobs.sort((a, b) => { jobs.sort((a, b) => {
let result = a.company?.localeCompare(b.company || ""); let result = a.company?.localeCompare(b.company || '');
if (result === 0) { if (result === 0) {
result = a.title?.localeCompare(b.title || ""); result = a.title?.localeCompare(b.title || '');
} }
return result || 0; return result || 0;
}); });
setJobs(jobs); setJobs(jobs);
} catch (err) { } catch (err) {
setSnack("" + err); setSnack('' + err);
} }
}; };
@ -46,36 +46,35 @@ const JobPicker = (props: JobPickerProps) => {
}, [jobs, setSnack]); }, [jobs, setSnack]);
return ( return (
<Box sx={{ display: "flex", flexDirection: "column", mb: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', mb: 1 }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
gap: 1, gap: 1,
flexWrap: "wrap", flexWrap: 'wrap',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
{jobs?.map((j, i) => ( {jobs?.map((j, i) => (
<Paper <Paper
key={`${j.id}`} key={`${j.id}`}
onClick={() => { onClick={() => {
console.log("Selected job", j); console.log('Selected job', j);
onSelect && onSelect(j); onSelect && onSelect(j);
}} }}
sx={{ cursor: "pointer" }} sx={{ cursor: 'pointer' }}
> >
<JobInfo <JobInfo
variant="small" variant="small"
sx={{ sx={{
maxWidth: "100%", maxWidth: '100%',
minWidth: "320px", minWidth: '320px',
width: "320px", width: '320px',
cursor: "pointer", cursor: 'pointer',
backgroundColor: backgroundColor: selectedJob?.id === j.id ? '#f0f0f0' : 'inherit',
selectedJob?.id === j.id ? "#f0f0f0" : "inherit", border: '2px solid transparent',
border: "2px solid transparent", '&:hover': {
"&:hover": { border: '2px solid orange',
border: "2px solid orange",
}, },
}} }}
job={j} job={j}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { import {
Box, Box,
Paper, Paper,
@ -21,7 +21,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
Slide, Slide,
} from "@mui/material"; } from '@mui/material';
import { import {
KeyboardArrowUp as ArrowUpIcon, KeyboardArrowUp as ArrowUpIcon,
KeyboardArrowDown as ArrowDownIcon, KeyboardArrowDown as ArrowDownIcon,
@ -30,16 +30,16 @@ import {
Schedule as ScheduleIcon, Schedule as ScheduleIcon,
Close as CloseIcon, Close as CloseIcon,
ArrowBack as ArrowBackIcon, ArrowBack as ArrowBackIcon,
} from "@mui/icons-material"; } from '@mui/icons-material';
import { TransitionProps } from "@mui/material/transitions"; import { TransitionProps } from '@mui/material/transitions';
import { JobInfo } from "components/ui/JobInfo"; import { JobInfo } from 'components/ui/JobInfo';
import { Job } from "types/types"; import { Job } from 'types/types';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedJob } from "hooks/GlobalContext"; import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
import { Navigate, useNavigate, useParams } from "react-router-dom"; import { Navigate, useNavigate, useParams } from 'react-router-dom';
type SortField = "updatedAt" | "createdAt" | "company" | "title"; type SortField = 'updatedAt' | 'createdAt' | 'company' | 'title';
type SortOrder = "asc" | "desc"; type SortOrder = 'asc' | 'desc';
interface JobViewerProps { interface JobViewerProps {
onSelect?: (job: Job) => void; onSelect?: (job: Job) => void;
@ -57,16 +57,16 @@ const Transition = React.forwardRef(function Transition(
const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => { const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const isSmall = useMediaQuery(theme.breakpoints.down("sm")); const isSmall = useMediaQuery(theme.breakpoints.down('sm'));
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { selectedJob, setSelectedJob } = useSelectedJob(); const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const [jobs, setJobs] = useState<Job[]>([]); const [jobs, setJobs] = useState<Job[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [sortField, setSortField] = useState<SortField>("updatedAt"); const [sortField, setSortField] = useState<SortField>('updatedAt');
const [sortOrder, setSortOrder] = useState<SortOrder>("desc"); const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
const [mobileDialogOpen, setMobileDialogOpen] = useState(false); const [mobileDialogOpen, setMobileDialogOpen] = useState(false);
const { jobId } = useParams<{ jobId?: string }>(); const { jobId } = useParams<{ jobId?: string }>();
@ -79,7 +79,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
setJobs(jobsData); setJobs(jobsData);
if (jobId) { if (jobId) {
const job = jobsData.find((j) => j.id === jobId); const job = jobsData.find(j => j.id === jobId);
if (job) { if (job) {
setSelectedJob(job); setSelectedJob(job);
onSelect?.(job); onSelect?.(job);
@ -95,7 +95,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
onSelect?.(firstJob); onSelect?.(firstJob);
} }
} catch (err) { } catch (err) {
setSnack("Failed to load jobs: " + err); setSnack('Failed to load jobs: ' + err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -104,48 +104,44 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
getJobs(); getJobs();
}, [apiClient, setSnack]); }, [apiClient, setSnack]);
const sortJobs = ( const sortJobs = (jobsList: Job[], field: SortField, order: SortOrder): Job[] => {
jobsList: Job[],
field: SortField,
order: SortOrder
): Job[] => {
return [...jobsList].sort((a, b) => { return [...jobsList].sort((a, b) => {
let aValue: any; let aValue: any;
let bValue: any; let bValue: any;
switch (field) { switch (field) {
case "updatedAt": case 'updatedAt':
aValue = a.updatedAt?.getTime() || 0; aValue = a.updatedAt?.getTime() || 0;
bValue = b.updatedAt?.getTime() || 0; bValue = b.updatedAt?.getTime() || 0;
break; break;
case "createdAt": case 'createdAt':
aValue = a.createdAt?.getTime() || 0; aValue = a.createdAt?.getTime() || 0;
bValue = b.createdAt?.getTime() || 0; bValue = b.createdAt?.getTime() || 0;
break; break;
case "company": case 'company':
aValue = a.company?.toLowerCase() || ""; aValue = a.company?.toLowerCase() || '';
bValue = b.company?.toLowerCase() || ""; bValue = b.company?.toLowerCase() || '';
break; break;
case "title": case 'title':
aValue = a.title?.toLowerCase() || ""; aValue = a.title?.toLowerCase() || '';
bValue = b.title?.toLowerCase() || ""; bValue = b.title?.toLowerCase() || '';
break; break;
default: default:
return 0; return 0;
} }
if (aValue < bValue) return order === "asc" ? -1 : 1; if (aValue < bValue) return order === 'asc' ? -1 : 1;
if (aValue > bValue) return order === "asc" ? 1 : -1; if (aValue > bValue) return order === 'asc' ? 1 : -1;
return 0; return 0;
}); });
}; };
const handleSort = (field: SortField) => { const handleSort = (field: SortField) => {
if (sortField === field) { if (sortField === field) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc"); setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else { } else {
setSortField(field); setSortField(field);
setSortOrder("desc"); setSortOrder('desc');
} }
}; };
@ -163,18 +159,18 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
const sortedJobs = sortJobs(jobs, sortField, sortOrder); const sortedJobs = sortJobs(jobs, sortField, sortOrder);
const formatDate = (date: Date | undefined) => { const formatDate = (date: Date | undefined) => {
if (!date) return "N/A"; if (!date) return 'N/A';
return new Intl.DateTimeFormat("en-US", { return new Intl.DateTimeFormat('en-US', {
month: "short", month: 'short',
day: "numeric", day: 'numeric',
...(isMobile ? {} : { year: "numeric" }), ...(isMobile ? {} : { year: 'numeric' }),
...(isSmall ? {} : { hour: "2-digit", minute: "2-digit" }), ...(isSmall ? {} : { hour: '2-digit', minute: '2-digit' }),
}).format(date); }).format(date);
}; };
const getSortIcon = (field: SortField) => { const getSortIcon = (field: SortField) => {
if (sortField !== field) return null; if (sortField !== field) return null;
return sortOrder === "asc" ? ( return sortOrder === 'asc' ? (
<ArrowUpIcon fontSize="small" /> <ArrowUpIcon fontSize="small" />
) : ( ) : (
<ArrowDownIcon fontSize="small" /> <ArrowDownIcon fontSize="small" />
@ -185,42 +181,36 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
<Paper <Paper
elevation={isMobile ? 0 : 1} elevation={isMobile ? 0 : 1}
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
width: "100%", width: '100%',
boxShadow: "none", boxShadow: 'none',
backgroundColor: "transparent", backgroundColor: 'transparent',
}} }}
> >
<Box <Box
sx={{ sx={{
p: isMobile ? 0.5 : 1, p: isMobile ? 0.5 : 1,
borderBottom: 1, borderBottom: 1,
borderColor: "divider", borderColor: 'divider',
backgroundColor: isMobile ? "background.paper" : "inherit", backgroundColor: isMobile ? 'background.paper' : 'inherit',
}} }}
> >
<Typography <Typography
variant={isSmall ? "subtitle2" : isMobile ? "subtitle1" : "h6"} variant={isSmall ? 'subtitle2' : isMobile ? 'subtitle1' : 'h6'}
gutterBottom gutterBottom
sx={{ mb: isMobile ? 0.5 : 1, fontWeight: 600 }} sx={{ mb: isMobile ? 0.5 : 1, fontWeight: 600 }}
> >
Jobs ({jobs.length}) Jobs ({jobs.length})
</Typography> </Typography>
<FormControl <FormControl size="small" sx={{ minWidth: isSmall ? 120 : isMobile ? 150 : 200 }}>
size="small"
sx={{ minWidth: isSmall ? 120 : isMobile ? 150 : 200 }}
>
<InputLabel>Sort by</InputLabel> <InputLabel>Sort by</InputLabel>
<Select <Select
value={`${sortField}-${sortOrder}`} value={`${sortField}-${sortOrder}`}
label="Sort by" label="Sort by"
onChange={(e) => { onChange={e => {
const [field, order] = e.target.value.split("-") as [ const [field, order] = e.target.value.split('-') as [SortField, SortOrder];
SortField,
SortOrder
];
setSortField(field); setSortField(field);
setSortOrder(order); setSortOrder(order);
}} }}
@ -240,9 +230,9 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
<TableContainer <TableContainer
sx={{ sx={{
flex: 1, flex: 1,
overflow: "auto", overflow: 'auto',
"& .MuiTable-root": { '& .MuiTable-root': {
tableLayout: isMobile ? "fixed" : "auto", tableLayout: isMobile ? 'fixed' : 'auto',
}, },
}} }}
> >
@ -251,59 +241,59 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
<TableRow> <TableRow>
<TableCell <TableCell
sx={{ sx={{
cursor: "pointer", cursor: 'pointer',
userSelect: "none", userSelect: 'none',
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? "25%" : "auto", width: isMobile ? '25%' : 'auto',
backgroundColor: "background.paper", backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort("company")} onClick={() => handleSort('company')}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<BusinessIcon fontSize={isMobile ? "small" : "medium"} /> <BusinessIcon fontSize={isMobile ? 'small' : 'medium'} />
<Typography variant="caption" fontWeight="bold" noWrap> <Typography variant="caption" fontWeight="bold" noWrap>
{isSmall ? "Co." : isMobile ? "Company" : "Company"} {isSmall ? 'Co.' : isMobile ? 'Company' : 'Company'}
</Typography> </Typography>
{getSortIcon("company")} {getSortIcon('company')}
</Box> </Box>
</TableCell> </TableCell>
<TableCell <TableCell
sx={{ sx={{
cursor: "pointer", cursor: 'pointer',
userSelect: "none", userSelect: 'none',
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? "45%" : "auto", width: isMobile ? '45%' : 'auto',
backgroundColor: "background.paper", backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort("title")} onClick={() => handleSort('title')}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<WorkIcon fontSize={isMobile ? "small" : "medium"} /> <WorkIcon fontSize={isMobile ? 'small' : 'medium'} />
<Typography variant="caption" fontWeight="bold" noWrap> <Typography variant="caption" fontWeight="bold" noWrap>
Title Title
</Typography> </Typography>
{getSortIcon("title")} {getSortIcon('title')}
</Box> </Box>
</TableCell> </TableCell>
{!isMobile && ( {!isMobile && (
<TableCell <TableCell
sx={{ sx={{
cursor: "pointer", cursor: 'pointer',
userSelect: "none", userSelect: 'none',
py: 0.5, py: 0.5,
px: 1, px: 1,
backgroundColor: "background.paper", backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort("updatedAt")} onClick={() => handleSort('updatedAt')}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<ScheduleIcon fontSize="medium" /> <ScheduleIcon fontSize="medium" />
<Typography variant="caption" fontWeight="bold"> <Typography variant="caption" fontWeight="bold">
Updated Updated
</Typography> </Typography>
{getSortIcon("updatedAt")} {getSortIcon('updatedAt')}
</Box> </Box>
</TableCell> </TableCell>
)} )}
@ -311,31 +301,31 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
sx={{ sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? "30%" : "auto", width: isMobile ? '30%' : 'auto',
backgroundColor: "background.paper", backgroundColor: 'background.paper',
}} }}
> >
<Typography variant="caption" fontWeight="bold" noWrap> <Typography variant="caption" fontWeight="bold" noWrap>
{isMobile ? "Status" : "Status"} {isMobile ? 'Status' : 'Status'}
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{sortedJobs.map((job) => ( {sortedJobs.map(job => (
<TableRow <TableRow
key={job.id} key={job.id}
hover hover
selected={selectedJob?.id === job.id} selected={selectedJob?.id === job.id}
onClick={() => handleJobSelect(job)} onClick={() => handleJobSelect(job)}
sx={{ sx={{
cursor: "pointer", cursor: 'pointer',
height: isMobile ? 48 : "auto", height: isMobile ? 48 : 'auto',
"&.Mui-selected": { '&.Mui-selected': {
backgroundColor: "action.selected", backgroundColor: 'action.selected',
}, },
"&:hover": { '&:hover': {
backgroundColor: "action.hover", backgroundColor: 'action.hover',
}, },
}} }}
> >
@ -343,27 +333,26 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
sx={{ sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: "hidden", overflow: 'hidden',
}} }}
> >
<Typography <Typography
variant={isMobile ? "caption" : "body2"} variant={isMobile ? 'caption' : 'body2'}
fontWeight="medium" fontWeight="medium"
noWrap noWrap
sx={{ fontSize: isMobile ? "0.75rem" : "0.875rem" }} sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }}
> >
{job.company || "N/A"} {job.company || 'N/A'}
</Typography> </Typography>
{!isMobile && job.details?.location && ( {!isMobile && job.details?.location && (
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
noWrap noWrap
sx={{ display: "block", fontSize: "0.7rem" }} sx={{ display: 'block', fontSize: '0.7rem' }}
> >
{job.details.location.city},{" "} {job.details.location.city},{' '}
{job.details.location.state || {job.details.location.state || job.details.location.country}
job.details.location.country}
</Typography> </Typography>
)} )}
</TableCell> </TableCell>
@ -371,16 +360,16 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
sx={{ sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: "hidden", overflow: 'hidden',
}} }}
> >
<Typography <Typography
variant={isMobile ? "caption" : "body2"} variant={isMobile ? 'caption' : 'body2'}
fontWeight="medium" fontWeight="medium"
noWrap noWrap
sx={{ fontSize: isMobile ? "0.75rem" : "0.875rem" }} sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }}
> >
{job.title || "N/A"} {job.title || 'N/A'}
</Typography> </Typography>
{!isMobile && job.details?.employmentType && ( {!isMobile && job.details?.employmentType && (
<Chip <Chip
@ -389,23 +378,23 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
variant="outlined" variant="outlined"
sx={{ sx={{
mt: 0.25, mt: 0.25,
fontSize: "0.6rem", fontSize: '0.6rem',
height: 16, height: 16,
"& .MuiChip-label": { px: 0.5 }, '& .MuiChip-label': { px: 0.5 },
}} }}
/> />
)} )}
</TableCell> </TableCell>
{!isMobile && ( {!isMobile && (
<TableCell sx={{ py: 0.5, px: 1 }}> <TableCell sx={{ py: 0.5, px: 1 }}>
<Typography variant="body2" sx={{ fontSize: "0.8rem" }}> <Typography variant="body2" sx={{ fontSize: '0.8rem' }}>
{formatDate(job.updatedAt)} {formatDate(job.updatedAt)}
</Typography> </Typography>
{job.createdAt && ( {job.createdAt && (
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
sx={{ display: "block", fontSize: "0.7rem" }} sx={{ display: 'block', fontSize: '0.7rem' }}
> >
Created: {formatDate(job.createdAt)} Created: {formatDate(job.createdAt)}
</Typography> </Typography>
@ -416,18 +405,18 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
sx={{ sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: "hidden", overflow: 'hidden',
}} }}
> >
<Chip <Chip
label={job.details?.isActive ? "Active" : "Inactive"} label={job.details?.isActive ? 'Active' : 'Inactive'}
color={job.details?.isActive ? "success" : "default"} color={job.details?.isActive ? 'success' : 'default'}
size="small" size="small"
variant="outlined" variant="outlined"
sx={{ sx={{
fontSize: isMobile ? "0.65rem" : "0.7rem", fontSize: isMobile ? '0.65rem' : '0.7rem',
height: isMobile ? 20 : 22, height: isMobile ? 20 : 22,
"& .MuiChip-label": { '& .MuiChip-label': {
px: isMobile ? 0.5 : 0.75, px: isMobile ? 0.5 : 0.75,
py: 0, py: 0,
}, },
@ -446,9 +435,9 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
<Box <Box
sx={{ sx={{
flex: 1, flex: 1,
overflow: "auto", overflow: 'auto',
p: inDialog ? 1.5 : 0.75, p: inDialog ? 1.5 : 0.75,
height: inDialog ? "100%" : "auto", height: inDialog ? '100%' : 'auto',
}} }}
> >
{selectedJob ? ( {selectedJob ? (
@ -456,23 +445,23 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
job={selectedJob} job={selectedJob}
variant="all" variant="all"
sx={{ sx={{
border: "none", border: 'none',
boxShadow: "none", boxShadow: 'none',
backgroundColor: "transparent", backgroundColor: 'transparent',
"& .MuiTypography-h6": { '& .MuiTypography-h6': {
fontSize: inDialog ? "1.25rem" : "1.1rem", fontSize: inDialog ? '1.25rem' : '1.1rem',
}, },
}} }}
/> />
) : ( ) : (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
height: "100%", height: '100%',
color: "text.secondary", color: 'text.secondary',
textAlign: "center", textAlign: 'center',
p: 2, p: 2,
}} }}
> >
@ -485,9 +474,9 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
return ( return (
<Box <Box
sx={{ sx={{
height: "100%", height: '100%',
p: 0.5, p: 0.5,
backgroundColor: "background.default", backgroundColor: 'background.default',
}} }}
> >
<JobList /> <JobList />
@ -499,7 +488,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
TransitionComponent={Transition} TransitionComponent={Transition}
TransitionProps={{ timeout: 300 }} TransitionProps={{ timeout: 300 }}
> >
<AppBar sx={{ position: "relative", elevation: 1 }}> <AppBar sx={{ position: 'relative', elevation: 1 }}>
<Toolbar variant="dense" sx={{ minHeight: 48 }}> <Toolbar variant="dense" sx={{ minHeight: 48 }}>
<IconButton <IconButton
edge="start" edge="start"
@ -511,18 +500,13 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
<Box sx={{ ml: 1, flex: 1, minWidth: 0 }}> <Box sx={{ ml: 1, flex: 1, minWidth: 0 }}>
<Typography <Typography variant="h6" component="div" noWrap sx={{ fontSize: '1rem' }}>
variant="h6"
component="div"
noWrap
sx={{ fontSize: "1rem" }}
>
{selectedJob?.title} {selectedJob?.title}
</Typography> </Typography>
<Typography <Typography
variant="caption" variant="caption"
component="div" component="div"
sx={{ color: "rgba(255, 255, 255, 0.7)" }} sx={{ color: 'rgba(255, 255, 255, 0.7)' }}
noWrap noWrap
> >
{selectedJob?.company} {selectedJob?.company}

View File

@ -1,6 +1,6 @@
import React from "react"; import React from 'react';
import { Button, Typography, Paper, Container } from "@mui/material"; import { Button, Typography, Paper, Container } from '@mui/material';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
interface LoginRequiredProps { interface LoginRequiredProps {
asset: string; asset: string;
@ -11,14 +11,14 @@ const LoginRequired = (props: LoginRequiredProps) => {
return ( return (
<Container maxWidth="md"> <Container maxWidth="md">
<Paper elevation={3} sx={{ p: 4, mt: 4, textAlign: "center" }}> <Paper elevation={3} sx={{ p: 4, mt: 4, textAlign: 'center' }}>
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
Please log in to access {asset} Please log in to access {asset}
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
onClick={() => { onClick={() => {
navigate("/login"); navigate('/login');
}} }}
color="primary" color="primary"
sx={{ mt: 2 }} sx={{ mt: 2 }}

View File

@ -1,5 +1,5 @@
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import "./LoginRestricted.css"; import './LoginRestricted.css';
interface LoginRestrictedProps { interface LoginRestrictedProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -9,9 +9,7 @@ const LoginRestricted = (props: LoginRestrictedProps) => {
const { children } = props; const { children } = props;
return ( return (
<Box className="LoginRestricted"> <Box className="LoginRestricted">
<Box className="LoginRestricted-label"> <Box className="LoginRestricted-label">You must login to access this feature</Box>
You must login to access this feature
</Box>
{children} {children}
</Box> </Box>
); );

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from 'react';
import { import {
Box, Box,
Link, Link,
@ -26,8 +26,8 @@ import {
DialogActions, DialogActions,
Tabs, Tabs,
Tab, Tab,
} from "@mui/material"; } from '@mui/material';
import PrintIcon from "@mui/icons-material/Print"; import PrintIcon from '@mui/icons-material/Print';
import { import {
Delete as DeleteIcon, Delete as DeleteIcon,
Restore as RestoreIcon, Restore as RestoreIcon,
@ -39,35 +39,34 @@ import {
Schedule as ScheduleIcon, Schedule as ScheduleIcon,
Visibility as VisibilityIcon, Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon, VisibilityOff as VisibilityOffIcon,
} from "@mui/icons-material"; } from '@mui/icons-material';
import PreviewIcon from "@mui/icons-material/Preview"; import PreviewIcon from '@mui/icons-material/Preview';
import EditDocumentIcon from "@mui/icons-material/EditDocument"; import EditDocumentIcon from '@mui/icons-material/EditDocument';
import { useReactToPrint } from "react-to-print"; import { useReactToPrint } from 'react-to-print';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
import { StyledMarkdown } from "components/StyledMarkdown"; import { StyledMarkdown } from 'components/StyledMarkdown';
import { Resume } from "types/types"; import { Resume } from 'types/types';
import { BackstoryTextField } from "components/BackstoryTextField"; import { BackstoryTextField } from 'components/BackstoryTextField';
import { JobInfo } from "./JobInfo"; import { JobInfo } from './JobInfo';
interface ResumeInfoProps { interface ResumeInfoProps {
resume: Resume; resume: Resume;
sx?: SxProps; sx?: SxProps;
action?: string; action?: string;
elevation?: number; elevation?: number;
variant?: "minimal" | "small" | "normal" | "all" | null; variant?: 'minimal' | 'small' | 'normal' | 'all' | null;
} }
const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => { const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const { resume } = props; const { resume } = props;
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const { sx, action = "", elevation = 1, variant = "normal" } = props; const { sx, action = '', elevation = 1, variant = 'normal' } = props;
const theme = useTheme(); const theme = useTheme();
const isMobile = const isMobile = useMediaQuery(theme.breakpoints.down('md')) || variant === 'minimal';
useMediaQuery(theme.breakpoints.down("md")) || variant === "minimal";
const isAdmin = user?.isAdmin; const isAdmin = user?.isAdmin;
const [activeResume, setActiveResume] = useState<Resume>({ ...resume }); const [activeResume, setActiveResume] = useState<Resume>({ ...resume });
const [isContentExpanded, setIsContentExpanded] = useState(false); const [isContentExpanded, setIsContentExpanded] = useState(false);
@ -75,14 +74,14 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
const [deleted, setDeleted] = useState<boolean>(false); const [deleted, setDeleted] = useState<boolean>(false);
const [editDialogOpen, setEditDialogOpen] = useState<boolean>(false); const [editDialogOpen, setEditDialogOpen] = useState<boolean>(false);
const [printDialogOpen, setPrintDialogOpen] = useState<boolean>(false); const [printDialogOpen, setPrintDialogOpen] = useState<boolean>(false);
const [editContent, setEditContent] = useState<string>(""); const [editContent, setEditContent] = useState<string>('');
const [saving, setSaving] = useState<boolean>(false); const [saving, setSaving] = useState<boolean>(false);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const [tabValue, setTabValue] = useState("markdown"); const [tabValue, setTabValue] = useState('markdown');
const printContentRef = useRef<HTMLDivElement>(null); const printContentRef = useRef<HTMLDivElement>(null);
const reactToPrintFn = useReactToPrint({ const reactToPrintFn = useReactToPrint({
contentRef: printContentRef, contentRef: printContentRef,
pageStyle: "@page { margin: 10px; }", pageStyle: '@page { margin: 10px; }',
}); });
useEffect(() => { useEffect(() => {
@ -104,9 +103,9 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
try { try {
await apiClient.deleteResume(id); await apiClient.deleteResume(id);
setDeleted(true); setDeleted(true);
setSnack("Resume deleted successfully."); setSnack('Resume deleted successfully.');
} catch (error) { } catch (error) {
setSnack("Failed to delete resume."); setSnack('Failed to delete resume.');
} }
} }
}; };
@ -118,19 +117,16 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
const handleSave = async () => { const handleSave = async () => {
setSaving(true); setSaving(true);
try { try {
const result = await apiClient.updateResume( const result = await apiClient.updateResume(activeResume.id || '', editContent);
activeResume.id || "",
editContent
);
const updatedResume = { const updatedResume = {
...activeResume, ...activeResume,
resume: editContent, resume: editContent,
updatedAt: new Date(), updatedAt: new Date(),
}; };
setActiveResume(updatedResume); setActiveResume(updatedResume);
setSnack("Resume updated successfully."); setSnack('Resume updated successfully.');
} catch (error) { } catch (error) {
setSnack("Failed to update resume."); setSnack('Failed to update resume.');
} finally { } finally {
setSaving(false); setSaving(false);
} }
@ -146,18 +142,18 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
} }
const formatDate = (date: Date | undefined) => { const formatDate = (date: Date | undefined) => {
if (!date) return "N/A"; if (!date) return 'N/A';
return new Intl.DateTimeFormat("en-US", { return new Intl.DateTimeFormat('en-US', {
month: "short", month: 'short',
day: "numeric", day: 'numeric',
year: "numeric", year: 'numeric',
hour: "2-digit", hour: '2-digit',
minute: "2-digit", minute: '2-digit',
}).format(date); }).format(date);
}; };
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
if (newValue === "print") { if (newValue === 'print') {
reactToPrintFn(); reactToPrintFn();
return; return;
} }
@ -167,38 +163,38 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
borderColor: "transparent", borderColor: 'transparent',
borderWidth: 2, borderWidth: 2,
borderStyle: "solid", borderStyle: 'solid',
transition: "all 0.3s ease", transition: 'all 0.3s ease',
flexDirection: "column", flexDirection: 'column',
minWidth: 0, minWidth: 0,
opacity: deleted ? 0.5 : 1.0, opacity: deleted ? 0.5 : 1.0,
backgroundColor: deleted backgroundColor: deleted
? theme.palette.action.disabledBackground ? theme.palette.action.disabledBackground
: theme.palette.background.paper, : theme.palette.background.paper,
pointerEvents: deleted ? "none" : "auto", pointerEvents: deleted ? 'none' : 'auto',
...sx, ...sx,
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
p: 1, p: 1,
pb: 0, pb: 0,
height: "100%", height: '100%',
flexDirection: "column", flexDirection: 'column',
alignItems: "stretch", alignItems: 'stretch',
position: "relative", position: 'relative',
}} }}
> >
{/* Header Information */} {/* Header Information */}
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: isMobile ? "column" : "row", flexDirection: isMobile ? 'column' : 'row',
gap: 2, gap: 2,
mb: 2, mb: 2,
}} }}
@ -207,7 +203,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Stack spacing={1}> <Stack spacing={1}>
{activeResume.candidate && ( {activeResume.candidate && (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<PersonIcon color="primary" fontSize="small" /> <PersonIcon color="primary" fontSize="small" />
<Typography variant="subtitle2" fontWeight="bold"> <Typography variant="subtitle2" fontWeight="bold">
Candidate Candidate
@ -222,8 +218,8 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<> <>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: 1, gap: 1,
mt: 1, mt: 1,
}} }}
@ -243,7 +239,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Stack spacing={1}> <Stack spacing={1}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ScheduleIcon color="primary" fontSize="small" /> <ScheduleIcon color="primary" fontSize="small" />
<Typography variant="subtitle2" fontWeight="bold"> <Typography variant="subtitle2" fontWeight="bold">
Timeline Timeline
@ -267,10 +263,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{/* Resume Content */} {/* Resume Content */}
{activeResume.resume && ( {activeResume.resume && (
<Card <Card elevation={0} sx={{ m: 0, p: 0, background: 'transparent !important' }}>
elevation={0}
sx={{ m: 0, p: 0, background: "transparent !important" }}
>
<CardHeader <CardHeader
title="Resume Content" title="Resume Content"
avatar={<DescriptionIcon color="success" />} avatar={<DescriptionIcon color="success" />}
@ -286,27 +279,27 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
} }
/> />
<CardContent sx={{ p: 0 }}> <CardContent sx={{ p: 0 }}>
<Box sx={{ position: "relative" }}> <Box sx={{ position: 'relative' }}>
<Typography <Typography
ref={contentRef} ref={contentRef}
variant="body2" variant="body2"
component="div" component="div"
sx={{ sx={{
display: "-webkit-box", display: '-webkit-box',
WebkitLineClamp: isContentExpanded WebkitLineClamp: isContentExpanded
? "unset" ? 'unset'
: variant === "small" : variant === 'small'
? 5 ? 5
: variant === "minimal" : variant === 'minimal'
? 3 ? 3
: 10, : 10,
WebkitBoxOrient: "vertical", WebkitBoxOrient: 'vertical',
overflow: "hidden", overflow: 'hidden',
textOverflow: "ellipsis", textOverflow: 'ellipsis',
lineHeight: 1.6, lineHeight: 1.6,
fontSize: "0.875rem !important", fontSize: '0.875rem !important',
whiteSpace: "pre-wrap", whiteSpace: 'pre-wrap',
fontFamily: "monospace", fontFamily: 'monospace',
backgroundColor: theme.palette.action.hover, backgroundColor: theme.palette.action.hover,
p: 2, p: 2,
borderRadius: 1, borderRadius: 1,
@ -316,24 +309,16 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{activeResume.resume} {activeResume.resume}
</Typography> </Typography>
{shouldShowMoreButton && variant !== "all" && ( {shouldShowMoreButton && variant !== 'all' && (
<Box <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
sx={{ display: "flex", justifyContent: "center", mt: 1 }}
>
<Button <Button
variant="text" variant="text"
size="small" size="small"
onClick={() => setIsContentExpanded(!isContentExpanded)} onClick={() => setIsContentExpanded(!isContentExpanded)}
startIcon={ startIcon={isContentExpanded ? <VisibilityOffIcon /> : <VisibilityIcon />}
isContentExpanded ? ( sx={{ fontSize: '0.75rem' }}
<VisibilityOffIcon />
) : (
<VisibilityIcon />
)
}
sx={{ fontSize: "0.75rem" }}
> >
{isContentExpanded ? "Show Less" : "Show More"} {isContentExpanded ? 'Show Less' : 'Show More'}
</Button> </Button>
</Box> </Box>
)} )}
@ -342,7 +327,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
</Card> </Card>
)} )}
{variant === "all" && activeResume.resume && ( {variant === 'all' && activeResume.resume && (
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<StyledMarkdown content={activeResume.resume} /> <StyledMarkdown content={activeResume.resume} />
</Box> </Box>
@ -351,22 +336,22 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{/* Admin Controls */} {/* Admin Controls */}
{isAdmin && ( {isAdmin && (
<Box sx={{ display: "flex", flexDirection: "column", p: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', p: 1 }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
pl: 1, pl: 1,
pr: 1, pr: 1,
gap: 1, gap: 1,
alignContent: "center", alignContent: 'center',
height: "32px", height: '32px',
}} }}
> >
<Tooltip title="Edit Resume"> <Tooltip title="Edit Resume">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
handleEditOpen(); handleEditOpen();
}} }}
@ -378,7 +363,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<Tooltip title="Delete Resume"> <Tooltip title="Delete Resume">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
deleteResume(activeResume.id); deleteResume(activeResume.id);
}} }}
@ -390,7 +375,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<Tooltip title="Reset Resume"> <Tooltip title="Reset Resume">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
handleReset(); handleReset();
}} }}
@ -423,13 +408,13 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
content={activeResume.resume} content={activeResume.resume}
sx={{ sx={{
p: 2, p: 2,
position: "relative", position: 'relative',
maxHeight: "100%", maxHeight: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
flex: 1 /* Take remaining space in some-container */, flex: 1 /* Take remaining space in some-container */,
overflowY: "auto" /* Scroll if content overflows */, overflowY: 'auto' /* Scroll if content overflows */,
}} }}
/> />
</Dialog> </Dialog>
@ -446,35 +431,28 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<DialogTitle> <DialogTitle>
Edit Resume Content Edit Resume Content
<Typography variant="caption" display="block" color="text.secondary"> <Typography variant="caption" display="block" color="text.secondary">
Resume for{" "} Resume for {activeResume.candidate?.fullName || activeResume.candidateId},{' '}
{activeResume.candidate?.fullName || activeResume.candidateId},{" "} {activeResume.job?.title || 'No Job Title Assigned'},{' '}
{activeResume.job?.title || "No Job Title Assigned"},{" "} {activeResume.job?.company || 'No Company Assigned'}
{activeResume.job?.company || "No Company Assigned"}
</Typography> </Typography>
<Typography variant="caption" display="block" color="text.secondary"> <Typography variant="caption" display="block" color="text.secondary">
Resume ID: # {activeResume.id} Resume ID: # {activeResume.id}
</Typography> </Typography>
<Typography variant="caption" display="block" color="text.secondary"> <Typography variant="caption" display="block" color="text.secondary">
Last saved:{" "} Last saved:{' '}
{activeResume.updatedAt {activeResume.updatedAt ? new Date(activeResume.updatedAt).toLocaleString() : 'N/A'}
? new Date(activeResume.updatedAt).toLocaleString()
: "N/A"}
</Typography> </Typography>
</DialogTitle> </DialogTitle>
<DialogContent <DialogContent
sx={{ sx={{
position: "relative", position: 'relative',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
height: "100%", height: '100%',
}} }}
> >
<Tabs value={tabValue} onChange={handleTabChange} centered> <Tabs value={tabValue} onChange={handleTabChange} centered>
<Tab <Tab value="markdown" icon={<EditDocumentIcon />} label="Markdown" />
value="markdown"
icon={<EditDocumentIcon />}
label="Markdown"
/>
<Tab value="preview" icon={<PreviewIcon />} label="Preview" /> <Tab value="preview" icon={<PreviewIcon />} label="Preview" />
<Tab value="job" icon={<WorkIcon />} label="Job" /> <Tab value="job" icon={<WorkIcon />} label="Job" />
<Tab value="print" icon={<PrintIcon />} label="Print" /> <Tab value="print" icon={<PrintIcon />} label="Print" />
@ -482,68 +460,68 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<Box <Box
ref={printContentRef} ref={printContentRef}
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
height: "100%" /* Restrict to main-container's height */, height: '100%' /* Restrict to main-container's height */,
width: "100%", width: '100%',
minHeight: 0 /* Prevent flex overflow */, minHeight: 0 /* Prevent flex overflow */,
//maxHeight: "min-content", //maxHeight: "min-content",
"& > *:not(.Scrollable)": { '& > *:not(.Scrollable)': {
flexShrink: 0 /* Prevent shrinking */, flexShrink: 0 /* Prevent shrinking */,
}, },
position: "relative", position: 'relative',
}} }}
> >
{tabValue === "markdown" && ( {tabValue === 'markdown' && (
<BackstoryTextField <BackstoryTextField
value={editContent} value={editContent}
onChange={(value) => setEditContent(value)} onChange={value => setEditContent(value)}
style={{ style={{
position: "relative", position: 'relative',
// maxHeight: "100%", // maxHeight: "100%",
height: "100%", height: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
minHeight: "100%", minHeight: '100%',
flexGrow: 1, flexGrow: 1,
flex: 1 /* Take remaining space in some-container */, flex: 1 /* Take remaining space in some-container */,
overflowY: "auto" /* Scroll if content overflows */, overflowY: 'auto' /* Scroll if content overflows */,
}} }}
placeholder="Enter resume content..." placeholder="Enter resume content..."
/> />
)} )}
{tabValue === "preview" && ( {tabValue === 'preview' && (
<> <>
<StyledMarkdown <StyledMarkdown
sx={{ sx={{
p: 2, p: 2,
position: "relative", position: 'relative',
maxHeight: "100%", maxHeight: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
flex: 1 /* Take remaining space in some-container */, flex: 1 /* Take remaining space in some-container */,
overflowY: "auto" /* Scroll if content overflows */, overflowY: 'auto' /* Scroll if content overflows */,
}} }}
content={editContent} content={editContent}
/> />
<Box sx={{ pb: 2 }}></Box> <Box sx={{ pb: 2 }}></Box>
</> </>
)} )}
{tabValue === "job" && activeResume.job && ( {tabValue === 'job' && activeResume.job && (
<JobInfo <JobInfo
variant="all" variant="all"
job={activeResume.job} job={activeResume.job}
sx={{ sx={{
p: 2, p: 2,
position: "relative", position: 'relative',
maxHeight: "100%", maxHeight: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
flex: 1 /* Take remaining space in some-container */, flex: 1 /* Take remaining space in some-container */,
overflowY: "auto" /* Scroll if content overflows */, overflowY: 'auto' /* Scroll if content overflows */,
}} }}
/> />
)} )}
@ -557,7 +535,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
disabled={saving} disabled={saving}
startIcon={<SaveIcon />} startIcon={<SaveIcon />}
> >
{saving ? "Saving..." : "Save"} {saving ? 'Saving...' : 'Save'}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from 'react';
import { import {
Box, Box,
Paper, Paper,
@ -23,7 +23,7 @@ import {
Slide, Slide,
TextField, TextField,
InputAdornment, InputAdornment,
} from "@mui/material"; } from '@mui/material';
import { import {
KeyboardArrowUp as ArrowUpIcon, KeyboardArrowUp as ArrowUpIcon,
KeyboardArrowDown as ArrowDownIcon, KeyboardArrowDown as ArrowDownIcon,
@ -35,16 +35,16 @@ import {
ArrowBack as ArrowBackIcon, ArrowBack as ArrowBackIcon,
Search as SearchIcon, Search as SearchIcon,
Clear as ClearIcon, Clear as ClearIcon,
} from "@mui/icons-material"; } from '@mui/icons-material';
import { TransitionProps } from "@mui/material/transitions"; import { TransitionProps } from '@mui/material/transitions';
import { ResumeInfo } from "components/ui/ResumeInfo"; import { ResumeInfo } from 'components/ui/ResumeInfo';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedResume } from "hooks/GlobalContext"; // Assuming similar context exists import { useAppState, useSelectedResume } from 'hooks/GlobalContext'; // Assuming similar context exists
import { Navigate, useNavigate, useParams } from "react-router-dom"; import { Navigate, useNavigate, useParams } from 'react-router-dom';
import { Resume } from "types/types"; import { Resume } from 'types/types';
type SortField = "updatedAt" | "createdAt" | "candidateId" | "jobId"; type SortField = 'updatedAt' | 'createdAt' | 'candidateId' | 'jobId';
type SortOrder = "asc" | "desc"; type SortOrder = 'asc' | 'desc';
interface ResumeViewerProps { interface ResumeViewerProps {
onSelect?: (resume: Resume) => void; onSelect?: (resume: Resume) => void;
@ -61,25 +61,21 @@ const Transition = React.forwardRef(function Transition(
return <Slide direction="up" ref={ref} {...props} />; return <Slide direction="up" ref={ref} {...props} />;
}); });
const ResumeViewer: React.FC<ResumeViewerProps> = ({ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobId }) => {
onSelect,
candidateId,
jobId,
}) => {
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const isSmall = useMediaQuery(theme.breakpoints.down("sm")); const isSmall = useMediaQuery(theme.breakpoints.down('sm'));
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { selectedResume, setSelectedResume } = useSelectedResume(); // Assuming similar context const { selectedResume, setSelectedResume } = useSelectedResume(); // Assuming similar context
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const [resumes, setResumes] = useState<Resume[]>([]); const [resumes, setResumes] = useState<Resume[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [sortField, setSortField] = useState<SortField>("updatedAt"); const [sortField, setSortField] = useState<SortField>('updatedAt');
const [sortOrder, setSortOrder] = useState<SortOrder>("desc"); const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
const [mobileDialogOpen, setMobileDialogOpen] = useState(false); const [mobileDialogOpen, setMobileDialogOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState('');
const [filteredResumes, setFilteredResumes] = useState<Resume[]>([]); const [filteredResumes, setFilteredResumes] = useState<Resume[]>([]);
const { resumeId } = useParams<{ resumeId?: string }>(); const { resumeId } = useParams<{ resumeId?: string }>();
@ -102,7 +98,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
setFilteredResumes(resumesData); setFilteredResumes(resumesData);
if (resumeId) { if (resumeId) {
const resume = resumesData.find((r) => r.id === resumeId); const resume = resumesData.find(r => r.id === resumeId);
if (resume) { if (resume) {
setSelectedResume(resume); setSelectedResume(resume);
onSelect?.(resume); onSelect?.(resume);
@ -117,8 +113,8 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
onSelect?.(firstResume); onSelect?.(firstResume);
} }
} catch (err) { } catch (err) {
console.error("Failed to load resumes:", err); console.error('Failed to load resumes:', err);
setSnack("Failed to load resumes: " + err, "error"); setSnack('Failed to load resumes: ' + err, 'error');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -133,16 +129,10 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
setFilteredResumes(resumes); setFilteredResumes(resumes);
} else { } else {
const filtered = resumes.filter( const filtered = resumes.filter(
(resume) => resume =>
resume.candidate?.fullName resume.candidate?.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
?.toLowerCase() resume.job?.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
.includes(searchQuery.toLowerCase()) || resume.job?.company?.toLowerCase().includes(searchQuery.toLowerCase()) ||
resume.job?.title
?.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
resume.job?.company
?.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
resume.resume?.toLowerCase().includes(searchQuery.toLowerCase()) || resume.resume?.toLowerCase().includes(searchQuery.toLowerCase()) ||
resume.id?.toLowerCase().includes(searchQuery.toLowerCase()) resume.id?.toLowerCase().includes(searchQuery.toLowerCase())
); );
@ -150,54 +140,44 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
} }
}, [searchQuery, resumes]); }, [searchQuery, resumes]);
const sortResumes = ( const sortResumes = (resumesList: Resume[], field: SortField, order: SortOrder): Resume[] => {
resumesList: Resume[],
field: SortField,
order: SortOrder
): Resume[] => {
return [...resumesList].sort((a, b) => { return [...resumesList].sort((a, b) => {
let aValue: any; let aValue: any;
let bValue: any; let bValue: any;
switch (field) { switch (field) {
case "updatedAt": case 'updatedAt':
aValue = a.updatedAt?.getTime() || 0; aValue = a.updatedAt?.getTime() || 0;
bValue = b.updatedAt?.getTime() || 0; bValue = b.updatedAt?.getTime() || 0;
break; break;
case "createdAt": case 'createdAt':
aValue = a.createdAt?.getTime() || 0; aValue = a.createdAt?.getTime() || 0;
bValue = b.createdAt?.getTime() || 0; bValue = b.createdAt?.getTime() || 0;
break; break;
case "candidateId": case 'candidateId':
aValue = aValue = a.candidate?.fullName?.toLowerCase() || a.candidateId?.toLowerCase() || '';
a.candidate?.fullName?.toLowerCase() || bValue = b.candidate?.fullName?.toLowerCase() || b.candidateId?.toLowerCase() || '';
a.candidateId?.toLowerCase() ||
"";
bValue =
b.candidate?.fullName?.toLowerCase() ||
b.candidateId?.toLowerCase() ||
"";
break; break;
case "jobId": case 'jobId':
aValue = a.job?.title?.toLowerCase() || a.jobId?.toLowerCase() || ""; aValue = a.job?.title?.toLowerCase() || a.jobId?.toLowerCase() || '';
bValue = b.job?.title?.toLowerCase() || b.jobId?.toLowerCase() || ""; bValue = b.job?.title?.toLowerCase() || b.jobId?.toLowerCase() || '';
break; break;
default: default:
return 0; return 0;
} }
if (aValue < bValue) return order === "asc" ? -1 : 1; if (aValue < bValue) return order === 'asc' ? -1 : 1;
if (aValue > bValue) return order === "asc" ? 1 : -1; if (aValue > bValue) return order === 'asc' ? 1 : -1;
return 0; return 0;
}); });
}; };
const handleSort = (field: SortField) => { const handleSort = (field: SortField) => {
if (sortField === field) { if (sortField === field) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc"); setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else { } else {
setSortField(field); setSortField(field);
setSortOrder("desc"); setSortOrder('desc');
} }
}; };
@ -216,24 +196,24 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
}; };
const handleSearchClear = () => { const handleSearchClear = () => {
setSearchQuery(""); setSearchQuery('');
}; };
const sortedResumes = sortResumes(filteredResumes, sortField, sortOrder); const sortedResumes = sortResumes(filteredResumes, sortField, sortOrder);
const formatDate = (date: Date | undefined) => { const formatDate = (date: Date | undefined) => {
if (!date) return "N/A"; if (!date) return 'N/A';
return new Intl.DateTimeFormat("en-US", { return new Intl.DateTimeFormat('en-US', {
month: "short", month: 'short',
day: "numeric", day: 'numeric',
...(isMobile ? {} : { year: "numeric" }), ...(isMobile ? {} : { year: 'numeric' }),
...(isSmall ? {} : { hour: "2-digit", minute: "2-digit" }), ...(isSmall ? {} : { hour: '2-digit', minute: '2-digit' }),
}).format(date); }).format(date);
}; };
const getSortIcon = (field: SortField) => { const getSortIcon = (field: SortField) => {
if (sortField !== field) return null; if (sortField !== field) return null;
return sortOrder === "asc" ? ( return sortOrder === 'asc' ? (
<ArrowUpIcon fontSize="small" /> <ArrowUpIcon fontSize="small" />
) : ( ) : (
<ArrowDownIcon fontSize="small" /> <ArrowDownIcon fontSize="small" />
@ -250,27 +230,27 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
<Paper <Paper
elevation={isMobile ? 0 : 1} elevation={isMobile ? 0 : 1}
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
...(isMobile ...(isMobile
? { ? {
width: "100%", width: '100%',
boxShadow: "none", boxShadow: 'none',
backgroundColor: "transparent", backgroundColor: 'transparent',
} }
: { width: "50%" }), : { width: '50%' }),
}} }}
> >
<Box <Box
sx={{ sx={{
p: isMobile ? 0.5 : 1, p: isMobile ? 0.5 : 1,
borderBottom: 1, borderBottom: 1,
borderColor: "divider", borderColor: 'divider',
backgroundColor: isMobile ? "background.paper" : "inherit", backgroundColor: isMobile ? 'background.paper' : 'inherit',
}} }}
> >
<Typography <Typography
variant={isSmall ? "subtitle2" : isMobile ? "subtitle1" : "h6"} variant={isSmall ? 'subtitle2' : isMobile ? 'subtitle1' : 'h6'}
gutterBottom gutterBottom
sx={{ mb: isMobile ? 0.5 : 1, fontWeight: 600 }} sx={{ mb: isMobile ? 0.5 : 1, fontWeight: 600 }}
> >
@ -279,17 +259,17 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
gap: 1, gap: 1,
flexDirection: isSmall ? "column" : "row", flexDirection: isSmall ? 'column' : 'row',
alignItems: isSmall ? "stretch" : "center", alignItems: isSmall ? 'stretch' : 'center',
}} }}
> >
<TextField <TextField
size="small" size="small"
placeholder="Search resumes..." placeholder="Search resumes..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={e => setSearchQuery(e.target.value)}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
@ -304,19 +284,16 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
</InputAdornment> </InputAdornment>
), ),
}} }}
sx={{ flexGrow: 1, minWidth: isSmall ? "100%" : 200 }} sx={{ flexGrow: 1, minWidth: isSmall ? '100%' : 200 }}
/> />
<FormControl size="small" sx={{ minWidth: isSmall ? "100%" : 180 }}> <FormControl size="small" sx={{ minWidth: isSmall ? '100%' : 180 }}>
<InputLabel>Sort by</InputLabel> <InputLabel>Sort by</InputLabel>
<Select <Select
value={`${sortField}-${sortOrder}`} value={`${sortField}-${sortOrder}`}
label="Sort by" label="Sort by"
onChange={(e) => { onChange={e => {
const [field, order] = e.target.value.split("-") as [ const [field, order] = e.target.value.split('-') as [SortField, SortOrder];
SortField,
SortOrder
];
setSortField(field); setSortField(field);
setSortOrder(order); setSortOrder(order);
}} }}
@ -337,9 +314,9 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
<TableContainer <TableContainer
sx={{ sx={{
flex: 1, flex: 1,
overflow: "auto", overflow: 'auto',
"& .MuiTable-root": { '& .MuiTable-root': {
tableLayout: isMobile ? "fixed" : "auto", tableLayout: isMobile ? 'fixed' : 'auto',
}, },
}} }}
> >
@ -348,59 +325,59 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
<TableRow> <TableRow>
<TableCell <TableCell
sx={{ sx={{
cursor: "pointer", cursor: 'pointer',
userSelect: "none", userSelect: 'none',
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? "35%" : "auto", width: isMobile ? '35%' : 'auto',
backgroundColor: "background.paper", backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort("candidateId")} onClick={() => handleSort('candidateId')}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<PersonIcon fontSize={isMobile ? "small" : "medium"} /> <PersonIcon fontSize={isMobile ? 'small' : 'medium'} />
<Typography variant="caption" fontWeight="bold" noWrap> <Typography variant="caption" fontWeight="bold" noWrap>
{isSmall ? "Candidate" : "Candidate"} {isSmall ? 'Candidate' : 'Candidate'}
</Typography> </Typography>
{getSortIcon("candidateId")} {getSortIcon('candidateId')}
</Box> </Box>
</TableCell> </TableCell>
<TableCell <TableCell
sx={{ sx={{
cursor: "pointer", cursor: 'pointer',
userSelect: "none", userSelect: 'none',
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? "35%" : "auto", width: isMobile ? '35%' : 'auto',
backgroundColor: "background.paper", backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort("jobId")} onClick={() => handleSort('jobId')}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<WorkIcon fontSize={isMobile ? "small" : "medium"} /> <WorkIcon fontSize={isMobile ? 'small' : 'medium'} />
<Typography variant="caption" fontWeight="bold" noWrap> <Typography variant="caption" fontWeight="bold" noWrap>
Job Job
</Typography> </Typography>
{getSortIcon("jobId")} {getSortIcon('jobId')}
</Box> </Box>
</TableCell> </TableCell>
{!isMobile && ( {!isMobile && (
<TableCell <TableCell
sx={{ sx={{
cursor: "pointer", cursor: 'pointer',
userSelect: "none", userSelect: 'none',
py: 0.5, py: 0.5,
px: 1, px: 1,
backgroundColor: "background.paper", backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort("updatedAt")} onClick={() => handleSort('updatedAt')}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<ScheduleIcon fontSize="medium" /> <ScheduleIcon fontSize="medium" />
<Typography variant="caption" fontWeight="bold"> <Typography variant="caption" fontWeight="bold">
Updated Updated
</Typography> </Typography>
{getSortIcon("updatedAt")} {getSortIcon('updatedAt')}
</Box> </Box>
</TableCell> </TableCell>
)} )}
@ -408,8 +385,8 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
sx={{ sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? "30%" : "auto", width: isMobile ? '30%' : 'auto',
backgroundColor: "background.paper", backgroundColor: 'background.paper',
}} }}
> >
<Typography variant="caption" fontWeight="bold" noWrap> <Typography variant="caption" fontWeight="bold" noWrap>
@ -419,20 +396,20 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{sortedResumes.map((resume) => ( {sortedResumes.map(resume => (
<TableRow <TableRow
key={resume.id} key={resume.id}
hover hover
selected={selectedResume?.id === resume.id} selected={selectedResume?.id === resume.id}
onClick={() => handleResumeSelect(resume)} onClick={() => handleResumeSelect(resume)}
sx={{ sx={{
cursor: "pointer", cursor: 'pointer',
height: isMobile ? 48 : "auto", height: isMobile ? 48 : 'auto',
"&.Mui-selected": { '&.Mui-selected': {
backgroundColor: "action.selected", backgroundColor: 'action.selected',
}, },
"&:hover": { '&:hover': {
backgroundColor: "action.hover", backgroundColor: 'action.hover',
}, },
}} }}
> >
@ -440,14 +417,14 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
sx={{ sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: "hidden", overflow: 'hidden',
}} }}
> >
<Typography <Typography
variant={isMobile ? "caption" : "body2"} variant={isMobile ? 'caption' : 'body2'}
fontWeight="medium" fontWeight="medium"
noWrap noWrap
sx={{ fontSize: isMobile ? "0.75rem" : "0.875rem" }} sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }}
> >
{resume.candidate?.fullName || resume.candidateId} {resume.candidate?.fullName || resume.candidateId}
</Typography> </Typography>
@ -456,7 +433,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
noWrap noWrap
sx={{ display: "block", fontSize: "0.7rem" }} sx={{ display: 'block', fontSize: '0.7rem' }}
> >
{formatDate(resume.updatedAt)} {formatDate(resume.updatedAt)}
</Typography> </Typography>
@ -466,23 +443,23 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
sx={{ sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: "hidden", overflow: 'hidden',
}} }}
> >
<Typography <Typography
variant={isMobile ? "caption" : "body2"} variant={isMobile ? 'caption' : 'body2'}
fontWeight="medium" fontWeight="medium"
noWrap noWrap
sx={{ fontSize: isMobile ? "0.75rem" : "0.875rem" }} sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }}
> >
{resume.job?.title || "Unknown Job"} {resume.job?.title || 'Unknown Job'}
</Typography> </Typography>
{!isMobile && resume.job?.company && ( {!isMobile && resume.job?.company && (
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
noWrap noWrap
sx={{ display: "block", fontSize: "0.7rem" }} sx={{ display: 'block', fontSize: '0.7rem' }}
> >
{resume.job.company} {resume.job.company}
</Typography> </Typography>
@ -490,14 +467,14 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
</TableCell> </TableCell>
{!isMobile && ( {!isMobile && (
<TableCell sx={{ py: 0.5, px: 1 }}> <TableCell sx={{ py: 0.5, px: 1 }}>
<Typography variant="body2" sx={{ fontSize: "0.8rem" }}> <Typography variant="body2" sx={{ fontSize: '0.8rem' }}>
{formatDate(resume.updatedAt)} {formatDate(resume.updatedAt)}
</Typography> </Typography>
{resume.createdAt && ( {resume.createdAt && (
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
sx={{ display: "block", fontSize: "0.7rem" }} sx={{ display: 'block', fontSize: '0.7rem' }}
> >
Created: {formatDate(resume.createdAt)} Created: {formatDate(resume.createdAt)}
</Typography> </Typography>
@ -508,14 +485,14 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
sx={{ sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: "hidden", overflow: 'hidden',
}} }}
> >
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
noWrap noWrap
sx={{ fontSize: isMobile ? "0.65rem" : "0.7rem" }} sx={{ fontSize: isMobile ? '0.65rem' : '0.7rem' }}
> >
{resume.id} {resume.id}
</Typography> </Typography>
@ -532,9 +509,9 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
<Box <Box
sx={{ sx={{
flex: 1, flex: 1,
overflow: "auto", overflow: 'auto',
p: inDialog ? 1.5 : 0.75, p: inDialog ? 1.5 : 0.75,
height: inDialog ? "100%" : "auto", height: inDialog ? '100%' : 'auto',
}} }}
> >
{selectedResume ? ( {selectedResume ? (
@ -542,29 +519,27 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
resume={selectedResume} resume={selectedResume}
variant="all" variant="all"
sx={{ sx={{
border: "none", border: 'none',
boxShadow: "none", boxShadow: 'none',
backgroundColor: "transparent", backgroundColor: 'transparent',
"& .MuiTypography-h6": { '& .MuiTypography-h6': {
fontSize: inDialog ? "1.25rem" : "1.1rem", fontSize: inDialog ? '1.25rem' : '1.1rem',
}, },
}} }}
/> />
) : ( ) : (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
height: "100%", height: '100%',
color: "text.secondary", color: 'text.secondary',
textAlign: "center", textAlign: 'center',
p: 2, p: 2,
}} }}
> >
<Typography variant="body2"> <Typography variant="body2">Select a resume to view details</Typography>
Select a resume to view details
</Typography>
</Box> </Box>
)} )}
</Box> </Box>
@ -574,9 +549,9 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
return ( return (
<Box <Box
sx={{ sx={{
height: "100%", height: '100%',
p: 0.5, p: 0.5,
backgroundColor: "background.default", backgroundColor: 'background.default',
}} }}
> >
<ResumeList /> <ResumeList />
@ -588,7 +563,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
TransitionComponent={Transition} TransitionComponent={Transition}
TransitionProps={{ timeout: 300 }} TransitionProps={{ timeout: 300 }}
> >
<AppBar sx={{ position: "relative", elevation: 1 }}> <AppBar sx={{ position: 'relative', elevation: 1 }}>
<Toolbar variant="dense" sx={{ minHeight: 48 }}> <Toolbar variant="dense" sx={{ minHeight: 48 }}>
<IconButton <IconButton
edge="start" edge="start"
@ -600,22 +575,16 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
<Box sx={{ ml: 1, flex: 1, minWidth: 0 }}> <Box sx={{ ml: 1, flex: 1, minWidth: 0 }}>
<Typography <Typography variant="h6" component="div" noWrap sx={{ fontSize: '1rem' }}>
variant="h6"
component="div"
noWrap
sx={{ fontSize: "1rem" }}
>
Resume Details Resume Details
</Typography> </Typography>
<Typography <Typography
variant="caption" variant="caption"
component="div" component="div"
sx={{ color: "rgba(255, 255, 255, 0.7)" }} sx={{ color: 'rgba(255, 255, 255, 0.7)' }}
noWrap noWrap
> >
{selectedResume?.candidate?.fullName || {selectedResume?.candidate?.fullName || selectedResume?.candidateId}
selectedResume?.candidateId}
</Typography> </Typography>
</Box> </Box>
</Toolbar> </Toolbar>
@ -629,20 +598,20 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
height: "100%", height: '100%',
gap: 0.75, gap: 0.75,
p: 0.75, p: 0.75,
backgroundColor: "background.default", backgroundColor: 'background.default',
}} }}
> >
<ResumeList /> <ResumeList />
<Paper <Paper
sx={{ sx={{
width: "50%", width: '50%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
elevation: 1, elevation: 1,
}} }}
> >
@ -650,11 +619,11 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({
sx={{ sx={{
p: 0.75, p: 0.75,
borderBottom: 1, borderBottom: 1,
borderColor: "divider", borderColor: 'divider',
backgroundColor: "background.paper", backgroundColor: 'background.paper',
}} }}
> >
<Typography variant="h6" sx={{ fontSize: "1.1rem", fontWeight: 600 }}> <Typography variant="h6" sx={{ fontSize: '1.1rem', fontWeight: 600 }}>
Resume Details Resume Details
</Typography> </Typography>
</Box> </Box>

View File

@ -1,4 +1,4 @@
import React from "react"; import React from 'react';
import { import {
SyncAlt, SyncAlt,
Favorite, Favorite,
@ -9,18 +9,18 @@ import {
Image, Image,
Psychology, Psychology,
Build, Build,
} from "@mui/icons-material"; } from '@mui/icons-material';
import { styled } from "@mui/material/styles"; import { styled } from '@mui/material/styles';
import * as Types from "types/types"; import * as Types from 'types/types';
import { Box } from "@mui/material"; import { Box } from '@mui/material';
interface StatusIconProps { interface StatusIconProps {
type: Types.ApiActivityType; type: Types.ApiActivityType;
} }
const StatusBox = styled(Box)(({ theme }) => ({ const StatusBox = styled(Box)(({ theme }) => ({
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: theme.spacing(1), gap: theme.spacing(1),
padding: theme.spacing(1, 2), padding: theme.spacing(1, 2),
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
@ -33,23 +33,23 @@ const StatusIcon = (props: StatusIconProps) => {
const { type } = props; const { type } = props;
switch (type) { switch (type) {
case "converting": case 'converting':
return <SyncAlt color="primary" />; return <SyncAlt color="primary" />;
case "heartbeat": case 'heartbeat':
return <Favorite color="error" />; return <Favorite color="error" />;
case "system": case 'system':
return <Settings color="action" />; return <Settings color="action" />;
case "info": case 'info':
return <Info color="info" />; return <Info color="info" />;
case "searching": case 'searching':
return <Search color="primary" />; return <Search color="primary" />;
case "generating": case 'generating':
return <AutoFixHigh color="secondary" />; return <AutoFixHigh color="secondary" />;
case "generating_image": case 'generating_image':
return <Image color="primary" />; return <Image color="primary" />;
case "thinking": case 'thinking':
return <Psychology color="secondary" />; return <Psychology color="secondary" />;
case "tooling": case 'tooling':
return <Build color="action" />; return <Build color="action" />;
default: default:
return <Info color="action" />; return <Info color="action" />;

View File

@ -1,4 +1,4 @@
import React from "react"; import React from 'react';
import { import {
Chat as ChatIcon, Chat as ChatIcon,
Dashboard as DashboardIcon, Dashboard as DashboardIcon,
@ -18,35 +18,35 @@ import {
Analytics as AnalyticsIcon, Analytics as AnalyticsIcon,
BubbleChart, BubbleChart,
AutoFixHigh, AutoFixHigh,
} from "@mui/icons-material"; } from '@mui/icons-material';
import EditDocumentIcon from "@mui/icons-material/EditDocument"; import EditDocumentIcon from '@mui/icons-material/EditDocument';
import { BackstoryLogo } from "components/ui/BackstoryLogo"; import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import { HomePage } from "pages/HomePage"; import { HomePage } from 'pages/HomePage';
import { CandidateChatPage } from "pages/CandidateChatPage"; import { CandidateChatPage } from 'pages/CandidateChatPage';
import { DocsPage } from "pages/DocsPage"; import { DocsPage } from 'pages/DocsPage';
import { CreateProfilePage } from "pages/candidate/ProfileWizard"; import { CreateProfilePage } from 'pages/candidate/ProfileWizard';
import { VectorVisualizerPage } from "pages/VectorVisualizerPage"; import { VectorVisualizerPage } from 'pages/VectorVisualizerPage';
import { BetaPage } from "pages/BetaPage"; import { BetaPage } from 'pages/BetaPage';
import { CandidateListingPage } from "pages/FindCandidatePage"; import { CandidateListingPage } from 'pages/FindCandidatePage';
import { JobAnalysisPage } from "pages/JobAnalysisPage"; import { JobAnalysisPage } from 'pages/JobAnalysisPage';
import { GenerateCandidate } from "pages/GenerateCandidate"; import { GenerateCandidate } from 'pages/GenerateCandidate';
import { LoginPage } from "pages/LoginPage"; import { LoginPage } from 'pages/LoginPage';
import { EmailVerificationPage } from "components/EmailVerificationComponents"; import { EmailVerificationPage } from 'components/EmailVerificationComponents';
import { Box, Typography } from "@mui/material"; import { Box, Typography } from '@mui/material';
import { CandidateDashboard } from "pages/candidate/Dashboard"; import { CandidateDashboard } from 'pages/candidate/Dashboard';
import { NavigationConfig, NavigationItem } from "types/navigation"; import { NavigationConfig, NavigationItem } from 'types/navigation';
import { HowItWorks } from "pages/HowItWorks"; import { HowItWorks } from 'pages/HowItWorks';
import SchoolIcon from "@mui/icons-material/School"; import SchoolIcon from '@mui/icons-material/School';
import { CandidateProfile } from "pages/candidate/Profile"; import { CandidateProfile } from 'pages/candidate/Profile';
import { Settings } from "pages/candidate/Settings"; import { Settings } from 'pages/candidate/Settings';
import { VectorVisualizer } from "components/VectorVisualizer"; import { VectorVisualizer } from 'components/VectorVisualizer';
import { DocumentManager } from "components/DocumentManager"; import { DocumentManager } from 'components/DocumentManager';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { JobViewer } from "components/ui/JobViewer"; import { JobViewer } from 'components/ui/JobViewer';
import { CandidatePicker } from "components/ui/CandidatePicker"; import { CandidatePicker } from 'components/ui/CandidatePicker';
import { ResumeViewer } from "components/ui/ResumeViewer"; import { ResumeViewer } from 'components/ui/ResumeViewer';
// Beta page components for placeholder routes // Beta page components for placeholder routes
const BackstoryPage = () => ( const BackstoryPage = () => (
@ -89,7 +89,7 @@ const LogoutPage = () => {
const { logout } = useAuth(); const { logout } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
logout().then(() => { logout().then(() => {
navigate("/"); navigate('/');
}); });
return <Typography variant="h4">Logging out...</Typography>; return <Typography variant="h4">Logging out...</Typography>;
}; };
@ -107,28 +107,28 @@ const SettingsPage = () => (
export const navigationConfig: NavigationConfig = { export const navigationConfig: NavigationConfig = {
items: [ items: [
{ {
id: "home", id: 'home',
label: <BackstoryLogo />, label: <BackstoryLogo />,
path: "/", path: '/',
component: <HowItWorks />, component: <HowItWorks />,
userTypes: ["guest", "candidate", "employer"], userTypes: ['guest', 'candidate', 'employer'],
exact: true, exact: true,
}, },
{ {
id: "job-analysis", id: 'job-analysis',
label: "Job Analysis", label: 'Job Analysis',
path: "/job-analysis", path: '/job-analysis',
icon: <WorkIcon />, icon: <WorkIcon />,
component: <JobAnalysisPage />, component: <JobAnalysisPage />,
userTypes: ["guest", "candidate", "employer"], userTypes: ['guest', 'candidate', 'employer'],
}, },
{ {
id: "chat", id: 'chat',
label: "Candidate Chat", label: 'Candidate Chat',
path: "/chat", path: '/chat',
icon: <ChatIcon />, icon: <ChatIcon />,
component: <CandidateChatPage />, component: <CandidateChatPage />,
userTypes: ["guest", "candidate", "employer"], userTypes: ['guest', 'candidate', 'employer'],
}, },
// { // {
// id: "explore", // id: "explore",
@ -151,74 +151,74 @@ export const navigationConfig: NavigationConfig = {
// }, // },
{ {
id: "generate-candidate", id: 'generate-candidate',
label: "Generate Candidate", label: 'Generate Candidate',
path: "/admin/generate-candidate", path: '/admin/generate-candidate',
icon: <AutoFixHigh />, icon: <AutoFixHigh />,
component: <GenerateCandidate />, component: <GenerateCandidate />,
userTypes: ["admin"], userTypes: ['admin'],
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
userMenuGroup: "admin", userMenuGroup: 'admin',
}, },
// User menu only items (not shown in main navigation) // User menu only items (not shown in main navigation)
{ {
id: "candidate-profile", id: 'candidate-profile',
label: "Profile", label: 'Profile',
icon: <PersonIcon />, icon: <PersonIcon />,
path: "/candidate/profile", path: '/candidate/profile',
component: <CandidateProfile />, component: <CandidateProfile />,
userTypes: ["candidate"], userTypes: ['candidate'],
userMenuGroup: "profile", userMenuGroup: 'profile',
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
}, },
{ {
id: "candidate-dashboard", id: 'candidate-dashboard',
label: "Dashboard", label: 'Dashboard',
path: "/candidate/dashboard", path: '/candidate/dashboard',
icon: <DashboardIcon />, icon: <DashboardIcon />,
component: <CandidateDashboard />, component: <CandidateDashboard />,
userTypes: ["candidate"], userTypes: ['candidate'],
userMenuGroup: "profile", userMenuGroup: 'profile',
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
}, },
{ {
id: "explore-jobs", id: 'explore-jobs',
label: "Jobs", label: 'Jobs',
path: "/candidate/jobs/:jobId?", path: '/candidate/jobs/:jobId?',
icon: <WorkIcon />, icon: <WorkIcon />,
component: <JobViewer />, component: <JobViewer />,
userTypes: ["candidate", "guest", "employer"], userTypes: ['candidate', 'guest', 'employer'],
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
userMenuGroup: "profile", userMenuGroup: 'profile',
}, },
{ {
id: "explore-resumes", id: 'explore-resumes',
label: "Resumes", label: 'Resumes',
path: "/candidate/resumes/:resumeId?", path: '/candidate/resumes/:resumeId?',
icon: <EditDocumentIcon />, icon: <EditDocumentIcon />,
component: <ResumeViewer />, component: <ResumeViewer />,
userTypes: ["candidate", "guest", "employer"], userTypes: ['candidate', 'guest', 'employer'],
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
userMenuGroup: "profile", userMenuGroup: 'profile',
}, },
{ {
id: "candidate-docs", id: 'candidate-docs',
label: "Content", label: 'Content',
icon: <BubbleChart />, icon: <BubbleChart />,
path: "/candidate/documents", path: '/candidate/documents',
component: ( component: (
<Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}> <Box sx={{ display: 'flex', width: '100%', flexDirection: 'column' }}>
<VectorVisualizer /> <VectorVisualizer />
<DocumentManager /> <DocumentManager />
</Box> </Box>
), ),
userTypes: ["candidate"], userTypes: ['candidate'],
userMenuGroup: "profile", userMenuGroup: 'profile',
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
}, },
@ -265,65 +265,65 @@ export const navigationConfig: NavigationConfig = {
// showInUserMenu: true, // showInUserMenu: true,
// }, // },
{ {
id: "candidate-settings", id: 'candidate-settings',
label: "System Information", label: 'System Information',
path: "/candidate/settings", path: '/candidate/settings',
icon: <SettingsIcon />, icon: <SettingsIcon />,
component: <Settings />, component: <Settings />,
userTypes: ["candidate"], userTypes: ['candidate'],
userMenuGroup: "account", userMenuGroup: 'account',
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
}, },
{ {
id: "logout", id: 'logout',
label: "Logout", label: 'Logout',
icon: <PersonIcon />, // This will be handled specially in Header icon: <PersonIcon />, // This will be handled specially in Header
userTypes: ["candidate", "employer"], userTypes: ['candidate', 'employer'],
showInNavigation: false, showInNavigation: false,
showInUserMenu: true, showInUserMenu: true,
userMenuGroup: "system", userMenuGroup: 'system',
}, },
// Auth routes (special handling) // Auth routes (special handling)
{ {
id: "auth", id: 'auth',
label: "Auth", label: 'Auth',
userTypes: ["guest", "candidate", "employer"], userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false, showInNavigation: false,
children: [ children: [
{ {
id: "verify-email", id: 'verify-email',
label: "Verify Email", label: 'Verify Email',
path: "/login/verify-email", path: '/login/verify-email',
component: <EmailVerificationPage />, component: <EmailVerificationPage />,
userTypes: ["guest", "candidate", "employer"], userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false, showInNavigation: false,
}, },
{ {
id: "login", id: 'login',
label: "Login", label: 'Login',
path: "/login/:tab?", path: '/login/:tab?',
component: <LoginPage />, component: <LoginPage />,
userTypes: ["guest", "candidate", "employer"], userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false, showInNavigation: false,
}, },
{ {
id: "logout-page", id: 'logout-page',
label: "Logout", label: 'Logout',
path: "/logout", path: '/logout',
component: <LogoutPage />, component: <LogoutPage />,
userTypes: ["candidate", "employer"], userTypes: ['candidate', 'employer'],
showInNavigation: false, showInNavigation: false,
}, },
], ],
}, },
// Catch-all route // Catch-all route
{ {
id: "catch-all", id: 'catch-all',
label: "Not Found", label: 'Not Found',
path: "*", path: '*',
component: <BetaPage />, component: <BetaPage />,
userTypes: ["guest", "candidate", "employer"], userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false, showInNavigation: false,
}, },
], ],
@ -331,46 +331,44 @@ export const navigationConfig: NavigationConfig = {
// Utility functions for working with navigation config // Utility functions for working with navigation config
export const getNavigationItemsForUser = ( export const getNavigationItemsForUser = (
userType: "guest" | "candidate" | "employer" | null, userType: 'guest' | 'candidate' | 'employer' | null,
isAdmin: boolean isAdmin: boolean
): NavigationItem[] => { ): NavigationItem[] => {
const currentUserType = userType || "guest"; const currentUserType = userType || 'guest';
const filterItems = (items: NavigationItem[]): NavigationItem[] => { const filterItems = (items: NavigationItem[]): NavigationItem[] => {
return items return items
.filter( .filter(
(item) => item =>
!item.userTypes || !item.userTypes ||
item.userTypes.includes(currentUserType) || item.userTypes.includes(currentUserType) ||
(item.userTypes.includes("admin") && isAdmin) (item.userTypes.includes('admin') && isAdmin)
) )
.filter((item) => item.showInNavigation !== false) // Default to true if not specified .filter(item => item.showInNavigation !== false) // Default to true if not specified
.map((item) => ({ .map(item => ({
...item, ...item,
children: item.children ? filterItems(item.children) : undefined, children: item.children ? filterItems(item.children) : undefined,
})) }))
.filter( .filter(item => item.path || (item.children && item.children.length > 0));
(item) => item.path || (item.children && item.children.length > 0)
);
}; };
return filterItems(navigationConfig.items); return filterItems(navigationConfig.items);
}; };
export const getAllRoutes = ( export const getAllRoutes = (
userType: "guest" | "candidate" | "employer" | null, userType: 'guest' | 'candidate' | 'employer' | null,
isAdmin: boolean isAdmin: boolean
): NavigationItem[] => { ): NavigationItem[] => {
const currentUserType = userType || "guest"; const currentUserType = userType || 'guest';
const extractRoutes = (items: NavigationItem[]): NavigationItem[] => { const extractRoutes = (items: NavigationItem[]): NavigationItem[] => {
const routes: NavigationItem[] = []; const routes: NavigationItem[] = [];
items.forEach((item) => { items.forEach(item => {
if ( if (
!item.userTypes || !item.userTypes ||
item.userTypes.includes(currentUserType) || item.userTypes.includes(currentUserType) ||
(item.userTypes.includes("admin") && isAdmin) (item.userTypes.includes('admin') && isAdmin)
) { ) {
if (item.path && item.component) { if (item.path && item.component) {
routes.push(item); routes.push(item);
@ -388,20 +386,20 @@ export const getAllRoutes = (
}; };
export const getMainNavigationItems = ( export const getMainNavigationItems = (
userType: "guest" | "candidate" | "employer" | null, userType: 'guest' | 'candidate' | 'employer' | null,
isAdmin: boolean isAdmin: boolean
): NavigationItem[] => { ): NavigationItem[] => {
return getNavigationItemsForUser(userType, isAdmin).filter( return getNavigationItemsForUser(userType, isAdmin).filter(
(item) => item =>
item.id !== "auth" && item.id !== 'auth' &&
item.id !== "catch-all" && item.id !== 'catch-all' &&
item.showInNavigation !== false && item.showInNavigation !== false &&
(item.path || (item.children && item.children.length > 0)) (item.path || (item.children && item.children.length > 0))
); );
}; };
export const getUserMenuItems = ( export const getUserMenuItems = (
userType: "candidate" | "employer" | "guest" | null, userType: 'candidate' | 'employer' | 'guest' | null,
isAdmin: boolean isAdmin: boolean
): NavigationItem[] => { ): NavigationItem[] => {
if (!userType) return []; if (!userType) return [];
@ -409,7 +407,7 @@ export const getUserMenuItems = (
const extractUserMenuItems = (items: NavigationItem[]): NavigationItem[] => { const extractUserMenuItems = (items: NavigationItem[]): NavigationItem[] => {
const menuItems: NavigationItem[] = []; const menuItems: NavigationItem[] = [];
items.forEach((item) => { items.forEach(item => {
if (!item.userTypes || item.userTypes.includes(userType) || isAdmin) { if (!item.userTypes || item.userTypes.includes(userType) || isAdmin) {
if (item.showInUserMenu) { if (item.showInUserMenu) {
menuItems.push(item); menuItems.push(item);
@ -427,7 +425,7 @@ export const getUserMenuItems = (
}; };
export const getUserMenuItemsByGroup = ( export const getUserMenuItemsByGroup = (
userType: "candidate" | "employer" | "guest" | null, userType: 'candidate' | 'employer' | 'guest' | null,
isAdmin: boolean isAdmin: boolean
): { [key: string]: NavigationItem[] } => { ): { [key: string]: NavigationItem[] } => {
const menuItems = getUserMenuItems(userType, isAdmin); const menuItems = getUserMenuItems(userType, isAdmin);
@ -439,8 +437,8 @@ export const getUserMenuItemsByGroup = (
other: [], other: [],
}; };
menuItems.forEach((item) => { menuItems.forEach(item => {
const group = item.userMenuGroup || "other"; const group = item.userMenuGroup || 'other';
if (!grouped[group]) { if (!grouped[group]) {
grouped[group] = []; grouped[group] = [];
} }

View File

@ -1 +1 @@
declare module "react-plotly.js"; declare module 'react-plotly.js';

View File

@ -1,21 +1,15 @@
import React from "react"; import React from 'react';
import { Box, Typography, Paper, Container } from "@mui/material"; import { Box, Typography, Paper, Container } from '@mui/material';
// Import the backstoryTheme // Import the backstoryTheme
// BackstoryAnalysisDisplay component // BackstoryAnalysisDisplay component
const BackstoryAppAnalysisPage = () => { const BackstoryAppAnalysisPage = () => {
return ( return (
<Box <Box sx={{ backgroundColor: 'background.default', minHeight: '100%', py: 4 }}>
sx={{ backgroundColor: "background.default", minHeight: "100%", py: 4 }}
>
<Container maxWidth="lg"> <Container maxWidth="lg">
<Paper sx={{ p: 4, boxShadow: 2 }}> <Paper sx={{ p: 4, boxShadow: 2 }}>
<Typography <Typography variant="h1" component="h1" sx={{ mb: 3, color: 'primary.main' }}>
variant="h1"
component="h1"
sx={{ mb: 3, color: "primary.main" }}
>
Backstory Application Analysis Backstory Application Analysis
</Typography> </Typography>
@ -23,9 +17,9 @@ const BackstoryAppAnalysisPage = () => {
Core Concept Core Concept
</Typography> </Typography>
<Typography variant="body1"> <Typography variant="body1">
Backstory is a dual-purpose platform designed to bridge the gap Backstory is a dual-purpose platform designed to bridge the gap between job candidates
between job candidates and employers/recruiters with an AI-powered and employers/recruiters with an AI-powered approach to professional profiles and resume
approach to professional profiles and resume generation. generation.
</Typography> </Typography>
<Typography variant="h3" component="h3" sx={{ mt: 3 }}> <Typography variant="h3" component="h3" sx={{ mt: 3 }}>
@ -34,16 +28,15 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}> <Box component="ol" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Job Candidates</strong> - Upload and manage <strong>Job Candidates</strong> - Upload and manage comprehensive professional
comprehensive professional histories and generate tailored histories and generate tailored resumes for specific positions
resumes for specific positions
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Employers/Recruiters</strong> - Search for candidates, <strong>Employers/Recruiters</strong> - Search for candidates, directly interact
directly interact with AI assistants about candidate with AI assistants about candidate experiences, and generate position-specific
experiences, and generate position-specific resumes resumes
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -58,32 +51,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}> <Box component="ul" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Complete Profile Management</strong> - Create detailed <strong>Complete Profile Management</strong> - Create detailed professional
professional histories beyond typical resume constraints histories beyond typical resume constraints
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>AI-Assisted Q&A Setup</strong> - Configure an AI <strong>AI-Assisted Q&A Setup</strong> - Configure an AI assistant to answer
assistant to answer employer questions about your experience employer questions about your experience
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Smart Resume Generator</strong> - Create tailored <strong>Smart Resume Generator</strong> - Create tailored resumes for specific
resumes for specific positions using AI positions using AI
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Analytics Dashboard</strong> - Track profile views, <strong>Analytics Dashboard</strong> - Track profile views, resume downloads, and
resume downloads, and employer engagement employer engagement
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Privacy Controls</strong> - Manage visibility and access <strong>Privacy Controls</strong> - Manage visibility and access to your
to your professional information professional information
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -94,32 +87,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}> <Box component="ul" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Advanced Candidate Search</strong> - Find candidates <strong>Advanced Candidate Search</strong> - Find candidates with specific skills,
with specific skills, experience levels, and qualifications experience levels, and qualifications
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Interactive Q&A</strong> - Ask questions directly to <strong>Interactive Q&A</strong> - Ask questions directly to candidate AI assistants
candidate AI assistants to learn more about their experience to learn more about their experience
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Resume Generation</strong> - Generate candidate resumes <strong>Resume Generation</strong> - Generate candidate resumes tailored to specific
tailored to specific job requirements job requirements
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Talent Pool Management</strong> - Create and manage <strong>Talent Pool Management</strong> - Create and manage groups of candidates for
groups of candidates for different positions different positions
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Job Posting Management</strong> - Create, manage, and <strong>Job Posting Management</strong> - Create, manage, and track applications for
track applications for job postings job postings
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -137,20 +130,20 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}> <Box component="ul" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Public Navigation</strong> - Home, Docs, Pricing, <strong>Public Navigation</strong> - Home, Docs, Pricing, Login/Register accessible
Login/Register accessible to all users to all users
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Candidate Dashboard Navigation</strong> - Profile, <strong>Candidate Dashboard Navigation</strong> - Profile, Backstory, Resumes, Q&A
Backstory, Resumes, Q&A Setup, Analytics, Settings Setup, Analytics, Settings
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Employer Dashboard Navigation</strong> - Dashboard, <strong>Employer Dashboard Navigation</strong> - Dashboard, Search, Saved, Jobs,
Search, Saved, Jobs, Company, Analytics, Settings Company, Analytics, Settings
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -161,38 +154,38 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}> <Box component="ol" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Dashboard Cards</strong> - Both user types have <strong>Dashboard Cards</strong> - Both user types have dashboards with card-based
dashboards with card-based information displays information displays
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Tab-Based Content Organization</strong> - Many screens <strong>Tab-Based Content Organization</strong> - Many screens use horizontal tabs
use horizontal tabs to organize related content to organize related content
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Form-Based Editors</strong> - Profile and content <strong>Form-Based Editors</strong> - Profile and content editors use structured
editors use structured forms with varied input types forms with varied input types
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Three-Column Layouts</strong> - Many screens follow a <strong>Three-Column Layouts</strong> - Many screens follow a left sidebar, main
left sidebar, main content, right sidebar pattern content, right sidebar pattern
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Preview/Edit Toggle</strong> - Resume and profile <strong>Preview/Edit Toggle</strong> - Resume and profile editing screens offer both
editing screens offer both editing and preview modes editing and preview modes
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Filter-Based Search</strong> - Employer search uses <strong>Filter-Based Search</strong> - Employer search uses multiple filter
multiple filter categories to refine candidate results categories to refine candidate results
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -201,9 +194,8 @@ const BackstoryAppAnalysisPage = () => {
Mobile Adaptations Mobile Adaptations
</Typography> </Typography>
<Typography variant="body1"> <Typography variant="body1">
The mobile designs show a simplified navigation structure with The mobile designs show a simplified navigation structure with bottom tabs and a
bottom tabs and a hamburger menu, maintaining the core functionality hamburger menu, maintaining the core functionality while adapting to smaller screens.
while adapting to smaller screens.
</Typography> </Typography>
<Typography variant="h2" component="h2" sx={{ mt: 4 }}> <Typography variant="h2" component="h2" sx={{ mt: 4 }}>
@ -216,26 +208,26 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}> <Box component="ul" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>LLM Integration</strong> - Supports multiple AI models <strong>LLM Integration</strong> - Supports multiple AI models (Claude, GPT-4,
(Claude, GPT-4, self-hosted models) self-hosted models)
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Candidate AI Assistant</strong> - Personalized AI <strong>Candidate AI Assistant</strong> - Personalized AI chatbot that answers
chatbot that answers questions about candidate experience questions about candidate experience
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Resume Generation</strong> - AI-powered resume creation <strong>Resume Generation</strong> - AI-powered resume creation based on job
based on job requirements requirements
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Skills Matching</strong> - Automated matching between <strong>Skills Matching</strong> - Automated matching between candidate skills and
candidate skills and job requirements job requirements
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -246,26 +238,25 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}> <Box component="ul" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Authentication</strong> - OAuth with Google, LinkedIn, <strong>Authentication</strong> - OAuth with Google, LinkedIn, GitHub
GitHub
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Data Import</strong> - LinkedIn profile import, resume <strong>Data Import</strong> - LinkedIn profile import, resume parsing (PDF, DOCX),
parsing (PDF, DOCX), CSV/JSON import CSV/JSON import
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>ATS Compatibility</strong> - Integration with employer <strong>ATS Compatibility</strong> - Integration with employer Applicant Tracking
Applicant Tracking Systems Systems
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Vector Databases</strong> - Semantic search capabilities <strong>Vector Databases</strong> - Semantic search capabilities for candidate
for candidate matching matching
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -276,32 +267,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}> <Box component="ol" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Beyond the Resume</strong> - Focuses on comprehensive <strong>Beyond the Resume</strong> - Focuses on comprehensive professional stories
professional stories rather than just resume highlights rather than just resume highlights
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>AI-Mediated Communication</strong> - Uses AI to <strong>AI-Mediated Communication</strong> - Uses AI to facilitate deeper
facilitate deeper understanding of candidate experiences understanding of candidate experiences
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Bidirectional Resume Generation</strong> - Both <strong>Bidirectional Resume Generation</strong> - Both candidates and employers can
candidates and employers can generate tailored resumes generate tailored resumes
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Configurable AI Personalities</strong> - Candidates can <strong>Configurable AI Personalities</strong> - Candidates can customize how their
customize how their AI assistant responds to questions AI assistant responds to questions
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Deep Analytics</strong> - Both candidates and employers <strong>Deep Analytics</strong> - Both candidates and employers receive insights
receive insights about their engagement about their engagement
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -320,9 +311,7 @@ const BackstoryAppAnalysisPage = () => {
<Typography variant="body1">Cloud-hosted (SaaS model)</Typography> <Typography variant="body1">Cloud-hosted (SaaS model)</Typography>
</li> </li>
<li> <li>
<Typography variant="body1"> <Typography variant="body1">Hybrid deployment (mixed cloud/on-premises)</Typography>
Hybrid deployment (mixed cloud/on-premises)
</Typography>
</li> </li>
</Box> </Box>
@ -331,14 +320,10 @@ const BackstoryAppAnalysisPage = () => {
</Typography> </Typography>
<Box component="ul" sx={{ pl: 4 }}> <Box component="ul" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1"> <Typography variant="body1">Granular candidate privacy controls</Typography>
Granular candidate privacy controls
</Typography>
</li> </li>
<li> <li>
<Typography variant="body1"> <Typography variant="body1">Role-based access for employer teams</Typography>
Role-based access for employer teams
</Typography>
</li> </li>
<li> <li>
<Typography variant="body1"> <Typography variant="body1">

View File

@ -1,10 +1,10 @@
import React from "react"; import React from 'react';
import { backstoryTheme } from "../BackstoryTheme"; import { backstoryTheme } from '../BackstoryTheme';
import { Box, Paper, Container } from "@mui/material"; import { Box, Paper, Container } from '@mui/material';
// This component provides a visual demonstration of the theme colors // This component provides a visual demonstration of the theme colors
const BackstoryThemeVisualizerPage = () => { const BackstoryThemeVisualizerPage = () => {
const colorSwatch = (color: string, name: string, textColor = "#fff") => ( const colorSwatch = (color: string, name: string, textColor = '#fff') => (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div <div
className="w-20 h-20 rounded-lg shadow-md flex items-center justify-center mb-2" className="w-20 h-20 rounded-lg shadow-md flex items-center justify-center mb-2"
@ -17,9 +17,7 @@ const BackstoryThemeVisualizerPage = () => {
); );
return ( return (
<Box <Box sx={{ backgroundColor: 'background.default', minHeight: '100%', py: 4 }}>
sx={{ backgroundColor: "background.default", minHeight: "100%", py: 4 }}
>
<Container maxWidth="lg"> <Container maxWidth="lg">
<Paper sx={{ p: 4, boxShadow: 2 }}> <Paper sx={{ p: 4, boxShadow: 2 }}>
<div className="p-8"> <div className="p-8">
@ -31,70 +29,41 @@ const BackstoryThemeVisualizerPage = () => {
</h1> </h1>
<div className="mb-8"> <div className="mb-8">
<h2 <h2 className="text-xl mb-4" style={{ color: backstoryTheme.palette.text.primary }}>
className="text-xl mb-4"
style={{ color: backstoryTheme.palette.text.primary }}
>
Primary Colors Primary Colors
</h2> </h2>
<div className="flex space-x-4"> <div className="flex space-x-4">
{colorSwatch( {colorSwatch(
backstoryTheme.palette.primary.main, backstoryTheme.palette.primary.main,
"Primary", 'Primary',
backstoryTheme.palette.primary.contrastText backstoryTheme.palette.primary.contrastText
)} )}
{colorSwatch( {colorSwatch(
backstoryTheme.palette.secondary.main, backstoryTheme.palette.secondary.main,
"Secondary", 'Secondary',
backstoryTheme.palette.secondary.contrastText backstoryTheme.palette.secondary.contrastText
)} )}
{colorSwatch( {colorSwatch(backstoryTheme.palette.custom.highlight, 'Highlight', '#fff')}
backstoryTheme.palette.custom.highlight,
"Highlight",
"#fff"
)}
</div> </div>
</div> </div>
<div className="mb-8"> <div className="mb-8">
<h2 <h2 className="text-xl mb-4" style={{ color: backstoryTheme.palette.text.primary }}>
className="text-xl mb-4"
style={{ color: backstoryTheme.palette.text.primary }}
>
Background Colors Background Colors
</h2> </h2>
<div className="flex space-x-4"> <div className="flex space-x-4">
{colorSwatch( {colorSwatch(backstoryTheme.palette.background.default, 'Default', '#000')}
backstoryTheme.palette.background.default, {colorSwatch(backstoryTheme.palette.background.paper, 'Paper', '#000')}
"Default",
"#000"
)}
{colorSwatch(
backstoryTheme.palette.background.paper,
"Paper",
"#000"
)}
</div> </div>
</div> </div>
<div className="mb-8"> <div className="mb-8">
<h2 <h2 className="text-xl mb-4" style={{ color: backstoryTheme.palette.text.primary }}>
className="text-xl mb-4"
style={{ color: backstoryTheme.palette.text.primary }}
>
Text Colors Text Colors
</h2> </h2>
<div className="flex space-x-4"> <div className="flex space-x-4">
{colorSwatch( {colorSwatch(backstoryTheme.palette.text.primary, 'Primary', '#fff')}
backstoryTheme.palette.text.primary, {colorSwatch(backstoryTheme.palette.text.secondary, 'Secondary', '#fff')}
"Primary",
"#fff"
)}
{colorSwatch(
backstoryTheme.palette.text.secondary,
"Secondary",
"#fff"
)}
</div> </div>
</div> </div>
@ -104,10 +73,7 @@ const BackstoryThemeVisualizerPage = () => {
backgroundColor: backstoryTheme.palette.background.paper, backgroundColor: backstoryTheme.palette.background.paper,
}} }}
> >
<h2 <h2 className="text-xl mb-4" style={{ color: backstoryTheme.palette.text.primary }}>
className="text-xl mb-4"
style={{ color: backstoryTheme.palette.text.primary }}
>
Typography Examples Typography Examples
</h2> </h2>
@ -132,10 +98,9 @@ const BackstoryThemeVisualizerPage = () => {
color: backstoryTheme.typography.body1.color, color: backstoryTheme.typography.body1.color,
}} }}
> >
Body Text - This is how the regular text content will appear Body Text - This is how the regular text content will appear in the Backstory
in the Backstory application. The application uses Roboto as application. The application uses Roboto as its primary font family, with
its primary font family, with carefully selected sizing and carefully selected sizing and colors.
colors.
</p> </p>
</div> </div>
@ -150,10 +115,7 @@ const BackstoryThemeVisualizerPage = () => {
</div> </div>
<div className="mb-8"> <div className="mb-8">
<h2 <h2 className="text-xl mb-4" style={{ color: backstoryTheme.palette.text.primary }}>
className="text-xl mb-4"
style={{ color: backstoryTheme.palette.text.primary }}
>
UI Component Examples UI Component Examples
</h2> </h2>
@ -180,12 +142,12 @@ const BackstoryThemeVisualizerPage = () => {
<div <div
style={{ style={{
padding: "8px 16px", padding: '8px 16px',
backgroundColor: backstoryTheme.palette.primary.main, backgroundColor: backstoryTheme.palette.primary.main,
color: backstoryTheme.palette.primary.contrastText, color: backstoryTheme.palette.primary.contrastText,
display: "inline-block", display: 'inline-block',
borderRadius: "4px", borderRadius: '4px',
cursor: "pointer", cursor: 'pointer',
fontFamily: backstoryTheme.typography.fontFamily, fontFamily: backstoryTheme.typography.fontFamily,
}} }}
> >
@ -195,12 +157,12 @@ const BackstoryThemeVisualizerPage = () => {
<div <div
className="mt-4" className="mt-4"
style={{ style={{
padding: "8px 16px", padding: '8px 16px',
backgroundColor: backstoryTheme.palette.secondary.main, backgroundColor: backstoryTheme.palette.secondary.main,
color: backstoryTheme.palette.secondary.contrastText, color: backstoryTheme.palette.secondary.contrastText,
display: "inline-block", display: 'inline-block',
borderRadius: "4px", borderRadius: '4px',
cursor: "pointer", cursor: 'pointer',
fontFamily: backstoryTheme.typography.fontFamily, fontFamily: backstoryTheme.typography.fontFamily,
}} }}
> >
@ -210,12 +172,12 @@ const BackstoryThemeVisualizerPage = () => {
<div <div
className="mt-4" className="mt-4"
style={{ style={{
padding: "8px 16px", padding: '8px 16px',
backgroundColor: backstoryTheme.palette.action.active, backgroundColor: backstoryTheme.palette.action.active,
color: "#fff", color: '#fff',
display: "inline-block", display: 'inline-block',
borderRadius: "4px", borderRadius: '4px',
cursor: "pointer", cursor: 'pointer',
fontFamily: backstoryTheme.typography.fontFamily, fontFamily: backstoryTheme.typography.fontFamily,
}} }}
> >
@ -225,10 +187,7 @@ const BackstoryThemeVisualizerPage = () => {
</div> </div>
<div> <div>
<h2 <h2 className="text-xl mb-4" style={{ color: backstoryTheme.palette.text.primary }}>
className="text-xl mb-4"
style={{ color: backstoryTheme.palette.text.primary }}
>
Theme Color Breakdown Theme Color Breakdown
</h2> </h2>
<table className="border-collapse"> <table className="border-collapse">
@ -237,8 +196,7 @@ const BackstoryThemeVisualizerPage = () => {
<th <th
className="border p-2 text-left" className="border p-2 text-left"
style={{ style={{
backgroundColor: backgroundColor: backstoryTheme.palette.background.default,
backstoryTheme.palette.background.default,
color: backstoryTheme.palette.text.primary, color: backstoryTheme.palette.text.primary,
}} }}
> >
@ -247,8 +205,7 @@ const BackstoryThemeVisualizerPage = () => {
<th <th
className="border p-2 text-left" className="border p-2 text-left"
style={{ style={{
backgroundColor: backgroundColor: backstoryTheme.palette.background.default,
backstoryTheme.palette.background.default,
color: backstoryTheme.palette.text.primary, color: backstoryTheme.palette.text.primary,
}} }}
> >
@ -257,8 +214,7 @@ const BackstoryThemeVisualizerPage = () => {
<th <th
className="border p-2 text-left" className="border p-2 text-left"
style={{ style={{
backgroundColor: backgroundColor: backstoryTheme.palette.background.default,
backstoryTheme.palette.background.default,
color: backstoryTheme.palette.text.primary, color: backstoryTheme.palette.text.primary,
}} }}
> >
@ -284,8 +240,7 @@ const BackstoryThemeVisualizerPage = () => {
className="border p-2" className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }} style={{ color: backstoryTheme.palette.text.primary }}
> >
Midnight Blue - Used for main headers and primary UI Midnight Blue - Used for main headers and primary UI elements
elements
</td> </td>
</tr> </tr>
<tr> <tr>
@ -345,8 +300,7 @@ const BackstoryThemeVisualizerPage = () => {
className="border p-2" className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }} style={{ color: backstoryTheme.palette.text.primary }}
> >
Golden Ochre - Used for highlights, accents, and important Golden Ochre - Used for highlights, accents, and important actions
actions
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,27 +1,20 @@
import React from "react"; import React from 'react';
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from '@mui/material/styles';
import { import { Box, Container, Paper, Typography, Grid, CssBaseline } from '@mui/material';
Box, import { backstoryTheme } from 'BackstoryTheme';
Container,
Paper,
Typography,
Grid,
CssBaseline,
} from "@mui/material";
import { backstoryTheme } from "BackstoryTheme";
const BackstoryUIOverviewPage: React.FC = () => { const BackstoryUIOverviewPage: React.FC = () => {
return ( return (
<ThemeProvider theme={backstoryTheme}> <ThemeProvider theme={backstoryTheme}>
<CssBaseline /> <CssBaseline />
<Box sx={{ bgcolor: "background.default", overflow: "hidden", py: 4 }}> <Box sx={{ bgcolor: 'background.default', overflow: 'hidden', py: 4 }}>
<Container maxWidth="lg"> <Container maxWidth="lg">
<Paper sx={{ p: 4, borderRadius: 2, boxShadow: 2 }}> <Paper sx={{ p: 4, borderRadius: 2, boxShadow: 2 }}>
{/* Header */} {/* Header */}
<Box <Box
sx={{ sx={{
p: 3, p: 3,
bgcolor: "background.paper", bgcolor: 'background.paper',
borderRadius: 2, borderRadius: 2,
mb: 4, mb: 4,
boxShadow: 1, boxShadow: 1,
@ -30,13 +23,12 @@ const BackstoryUIOverviewPage: React.FC = () => {
<Typography <Typography
variant="h4" variant="h4"
component="h1" component="h1"
sx={{ fontWeight: "bold", color: "primary.main", mb: 1 }} sx={{ fontWeight: 'bold', color: 'primary.main', mb: 1 }}
> >
Backstory UI Architecture Backstory UI Architecture
</Typography> </Typography>
<Typography variant="body1" color="text.secondary"> <Typography variant="body1" color="text.secondary">
A visual overview of the dual-purpose application serving A visual overview of the dual-purpose application serving candidates and employers
candidates and employers
</Typography> </Typography>
</Box> </Box>
@ -46,38 +38,33 @@ const BackstoryUIOverviewPage: React.FC = () => {
<Box <Box
sx={{ sx={{
p: 3, p: 3,
bgcolor: "rgba(74, 122, 125, 0.1)", bgcolor: 'rgba(74, 122, 125, 0.1)',
borderRadius: 2, borderRadius: 2,
border: "1px solid", border: '1px solid',
borderColor: "rgba(74, 122, 125, 0.3)", borderColor: 'rgba(74, 122, 125, 0.3)',
height: "100%", height: '100%',
}} }}
> >
<Typography <Typography
variant="h6" variant="h6"
sx={{ color: "secondary.main", mb: 2, fontWeight: "bold" }} sx={{ color: 'secondary.main', mb: 2, fontWeight: 'bold' }}
> >
Candidate Experience Candidate Experience
</Typography> </Typography>
<Box <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}
>
{[ {[
"Create comprehensive professional profiles", 'Create comprehensive professional profiles',
"Configure AI assistant for employer Q&A", 'Configure AI assistant for employer Q&A',
"Generate tailored resumes for specific jobs", 'Generate tailored resumes for specific jobs',
"Track profile engagement metrics", 'Track profile engagement metrics',
].map((item, index) => ( ].map((item, index) => (
<Box <Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
key={index}
sx={{ display: "flex", alignItems: "center", gap: 1.5 }}
>
<Box <Box
sx={{ sx={{
width: 8, width: 8,
height: 8, height: 8,
borderRadius: "50%", borderRadius: '50%',
bgcolor: "secondary.main", bgcolor: 'secondary.main',
}} }}
/> />
<Typography variant="body2">{item}</Typography> <Typography variant="body2">{item}</Typography>
@ -91,38 +78,33 @@ const BackstoryUIOverviewPage: React.FC = () => {
<Box <Box
sx={{ sx={{
p: 3, p: 3,
bgcolor: "rgba(26, 37, 54, 0.1)", bgcolor: 'rgba(26, 37, 54, 0.1)',
borderRadius: 2, borderRadius: 2,
border: "1px solid", border: '1px solid',
borderColor: "rgba(26, 37, 54, 0.3)", borderColor: 'rgba(26, 37, 54, 0.3)',
height: "100%", height: '100%',
}} }}
> >
<Typography <Typography
variant="h6" variant="h6"
sx={{ color: "primary.main", mb: 2, fontWeight: "bold" }} sx={{ color: 'primary.main', mb: 2, fontWeight: 'bold' }}
> >
Employer Experience Employer Experience
</Typography> </Typography>
<Box <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}
>
{[ {[
"Search for candidates with specific skills", 'Search for candidates with specific skills',
"Interact with candidate AI assistants", 'Interact with candidate AI assistants',
"Generate position-specific candidate resumes", 'Generate position-specific candidate resumes',
"Manage talent pools and job listings", 'Manage talent pools and job listings',
].map((item, index) => ( ].map((item, index) => (
<Box <Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
key={index}
sx={{ display: "flex", alignItems: "center", gap: 1.5 }}
>
<Box <Box
sx={{ sx={{
width: 8, width: 8,
height: 8, height: 8,
borderRadius: "50%", borderRadius: '50%',
bgcolor: "primary.main", bgcolor: 'primary.main',
}} }}
/> />
<Typography variant="body2">{item}</Typography> <Typography variant="body2">{item}</Typography>
@ -137,64 +119,56 @@ const BackstoryUIOverviewPage: React.FC = () => {
<Box <Box
sx={{ sx={{
p: 3, p: 3,
bgcolor: "background.paper", bgcolor: 'background.paper',
borderRadius: 2, borderRadius: 2,
mb: 4, mb: 4,
boxShadow: 1, boxShadow: 1,
}} }}
> >
<Typography <Typography variant="h5" sx={{ color: 'text.primary', mb: 3, fontWeight: 'bold' }}>
variant="h5"
sx={{ color: "text.primary", mb: 3, fontWeight: "bold" }}
>
Key UI Components Key UI Components
</Typography> </Typography>
<Grid container spacing={2}> <Grid container spacing={2}>
{[ {[
{ {
title: "Dashboards", title: 'Dashboards',
description: description:
"Role-specific dashboards with card-based metrics and action items", 'Role-specific dashboards with card-based metrics and action items',
}, },
{ {
title: "Profile Editors", title: 'Profile Editors',
description: description: 'Comprehensive forms for managing professional information',
"Comprehensive forms for managing professional information",
}, },
{ {
title: "Resume Builder", title: 'Resume Builder',
description: description: 'AI-powered tools for creating tailored resumes',
"AI-powered tools for creating tailored resumes",
}, },
{ {
title: "Q&A Interface", title: 'Q&A Interface',
description: description: 'Chat-like interface for employer-candidate AI interaction',
"Chat-like interface for employer-candidate AI interaction",
}, },
{ {
title: "Search & Filters", title: 'Search & Filters',
description: description: 'Advanced search with multiple filter categories',
"Advanced search with multiple filter categories",
}, },
{ {
title: "Analytics Dashboards", title: 'Analytics Dashboards',
description: description: 'Visual metrics for tracking engagement and performance',
"Visual metrics for tracking engagement and performance",
}, },
].map((component, index) => ( ].map((component, index) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}> <Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<Box <Box
sx={{ sx={{
p: 2, p: 2,
border: "1px solid", border: '1px solid',
borderColor: "divider", borderColor: 'divider',
borderRadius: 1, borderRadius: 1,
height: "100%", height: '100%',
transition: "all 0.2s ease-in-out", transition: 'all 0.2s ease-in-out',
"&:hover": { '&:hover': {
bgcolor: "rgba(212, 160, 23, 0.05)", bgcolor: 'rgba(212, 160, 23, 0.05)',
borderColor: "action.active", borderColor: 'action.active',
transform: "translateY(-2px)", transform: 'translateY(-2px)',
boxShadow: 1, boxShadow: 1,
}, },
}} }}
@ -202,9 +176,9 @@ const BackstoryUIOverviewPage: React.FC = () => {
<Typography <Typography
variant="h6" variant="h6"
sx={{ sx={{
color: "secondary.main", color: 'secondary.main',
mb: 1, mb: 1,
fontWeight: "medium", fontWeight: 'medium',
}} }}
> >
{component.title} {component.title}
@ -222,53 +196,53 @@ const BackstoryUIOverviewPage: React.FC = () => {
<Grid container spacing={3} sx={{ mb: 4 }}> <Grid container spacing={3} sx={{ mb: 4 }}>
{[ {[
{ {
title: "Candidate Navigation", title: 'Candidate Navigation',
items: [ items: [
"Dashboard", 'Dashboard',
"Profile", 'Profile',
"Backstory", 'Backstory',
"Resumes", 'Resumes',
"Q&A Setup", 'Q&A Setup',
"Analytics", 'Analytics',
"Settings", 'Settings',
], ],
color: "secondary.main", color: 'secondary.main',
borderColor: "secondary.main", borderColor: 'secondary.main',
}, },
{ {
title: "Employer Navigation", title: 'Employer Navigation',
items: [ items: [
"Dashboard", 'Dashboard',
"Search", 'Search',
"Saved", 'Saved',
"Jobs", 'Jobs',
"Company", 'Company',
"Analytics", 'Analytics',
"Settings", 'Settings',
], ],
color: "primary.main", color: 'primary.main',
borderColor: "primary.main", borderColor: 'primary.main',
}, },
{ {
title: "Public Navigation", title: 'Public Navigation',
items: ["Home", "Docs", "Pricing", "Login", "Register"], items: ['Home', 'Docs', 'Pricing', 'Login', 'Register'],
color: "custom.highlight", color: 'custom.highlight',
borderColor: "custom.highlight", borderColor: 'custom.highlight',
}, },
].map((nav, index) => ( ].map((nav, index) => (
<Grid size={{ xs: 12, md: 4 }} key={index}> <Grid size={{ xs: 12, md: 4 }} key={index}>
<Box <Box
sx={{ sx={{
p: 3, p: 3,
bgcolor: "background.paper", bgcolor: 'background.paper',
borderRadius: 2, borderRadius: 2,
boxShadow: 1, boxShadow: 1,
height: "100%", height: '100%',
}} }}
> >
<Typography <Typography
variant="h6" variant="h6"
sx={{ color: "text.primary", mb: 2, fontWeight: "bold" }} sx={{ color: 'text.primary', mb: 2, fontWeight: 'bold' }}
> >
{nav.title} {nav.title}
</Typography> </Typography>
@ -278,16 +252,13 @@ const BackstoryUIOverviewPage: React.FC = () => {
borderColor: nav.borderColor, borderColor: nav.borderColor,
pl: 2, pl: 2,
py: 1, py: 1,
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: 1.5, gap: 1.5,
}} }}
> >
{nav.items.map((item, idx) => ( {nav.items.map((item, idx) => (
<Typography <Typography key={idx} sx={{ color: nav.color, fontWeight: 'medium' }}>
key={idx}
sx={{ color: nav.color, fontWeight: "medium" }}
>
{item} {item}
</Typography> </Typography>
))} ))}
@ -301,68 +272,65 @@ const BackstoryUIOverviewPage: React.FC = () => {
<Box <Box
sx={{ sx={{
p: 3, p: 3,
bgcolor: "background.paper", bgcolor: 'background.paper',
borderRadius: 2, borderRadius: 2,
mb: 4, mb: 4,
boxShadow: 1, boxShadow: 1,
}} }}
> >
<Typography <Typography variant="h5" sx={{ color: 'text.primary', mb: 3, fontWeight: 'bold' }}>
variant="h5"
sx={{ color: "text.primary", mb: 3, fontWeight: "bold" }}
>
System Connection Points System Connection Points
</Typography> </Typography>
<Box sx={{ position: "relative", py: 2 }}> <Box sx={{ position: 'relative', py: 2 }}>
{/* Connection line */} {/* Connection line */}
<Box <Box
sx={{ sx={{
position: "absolute", position: 'absolute',
left: "50%", left: '50%',
top: 0, top: 0,
bottom: 0, bottom: 0,
width: 1, width: 1,
borderColor: "divider", borderColor: 'divider',
zIndex: 0, zIndex: 0,
borderLeft: "1px solid", borderLeft: '1px solid',
overflow: "hidden", overflow: 'hidden',
}} }}
/> />
{/* Connection points */} {/* Connection points */}
{[ {[
{ left: "Candidate Profile", right: "Employer Search" }, { left: 'Candidate Profile', right: 'Employer Search' },
{ left: "Q&A Setup", right: "Q&A Interface" }, { left: 'Q&A Setup', right: 'Q&A Interface' },
{ left: "Resume Generator", right: "Job Posts" }, { left: 'Resume Generator', right: 'Job Posts' },
].map((connection, index) => ( ].map((connection, index) => (
<Box <Box
key={index} key={index}
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
mb: index < 2 ? 5 : 0, mb: index < 2 ? 5 : 0,
position: "relative", position: 'relative',
zIndex: 1, zIndex: 1,
}} }}
> >
<Box <Box
sx={{ sx={{
flex: 1, flex: 1,
display: "flex", display: 'flex',
justifyContent: "flex-end", justifyContent: 'flex-end',
pr: 3, pr: 3,
}} }}
> >
<Box <Box
sx={{ sx={{
display: "inline-block", display: 'inline-block',
bgcolor: "rgba(74, 122, 125, 0.1)", bgcolor: 'rgba(74, 122, 125, 0.1)',
p: 2, p: 2,
borderRadius: 2, borderRadius: 2,
color: "secondary.main", color: 'secondary.main',
fontWeight: "medium", fontWeight: 'medium',
border: "1px solid", border: '1px solid',
borderColor: "rgba(74, 122, 125, 0.3)", borderColor: 'rgba(74, 122, 125, 0.3)',
}} }}
> >
{connection.left} {connection.left}
@ -373,8 +341,8 @@ const BackstoryUIOverviewPage: React.FC = () => {
sx={{ sx={{
width: 16, width: 16,
height: 16, height: 16,
borderRadius: "50%", borderRadius: '50%',
bgcolor: "custom.highlight", bgcolor: 'custom.highlight',
zIndex: 2, zIndex: 2,
boxShadow: 2, boxShadow: 2,
}} }}
@ -388,14 +356,14 @@ const BackstoryUIOverviewPage: React.FC = () => {
> >
<Box <Box
sx={{ sx={{
display: "inline-block", display: 'inline-block',
bgcolor: "rgba(26, 37, 54, 0.1)", bgcolor: 'rgba(26, 37, 54, 0.1)',
p: 2, p: 2,
borderRadius: 2, borderRadius: 2,
color: "primary.main", color: 'primary.main',
fontWeight: "medium", fontWeight: 'medium',
border: "1px solid", border: '1px solid',
borderColor: "rgba(26, 37, 54, 0.3)", borderColor: 'rgba(26, 37, 54, 0.3)',
}} }}
> >
{connection.right} {connection.right}
@ -410,54 +378,49 @@ const BackstoryUIOverviewPage: React.FC = () => {
<Box <Box
sx={{ sx={{
p: 3, p: 3,
bgcolor: "background.paper", bgcolor: 'background.paper',
borderRadius: 2, borderRadius: 2,
boxShadow: 1, boxShadow: 1,
}} }}
> >
<Typography <Typography variant="h5" sx={{ color: 'text.primary', mb: 3, fontWeight: 'bold' }}>
variant="h5"
sx={{ color: "text.primary", mb: 3, fontWeight: "bold" }}
>
Mobile Adaptation Mobile Adaptation
</Typography> </Typography>
<Box sx={{ display: "flex", justifyContent: "center" }}> <Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Box <Box
sx={{ sx={{
width: 200, width: 200,
height: 400, height: 400,
border: "4px solid", border: '4px solid',
borderColor: "text.primary", borderColor: 'text.primary',
borderRadius: 5, borderRadius: 5,
p: 1, p: 1,
bgcolor: "background.default", bgcolor: 'background.default',
}} }}
> >
<Box <Box
sx={{ sx={{
height: "100%", height: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
border: "1px solid", border: '1px solid',
borderColor: "divider", borderColor: 'divider',
borderRadius: 4, borderRadius: 4,
overflow: "hidden", overflow: 'hidden',
}} }}
> >
{/* Mobile header */} {/* Mobile header */}
<Box <Box
sx={{ sx={{
bgcolor: "primary.main", bgcolor: 'primary.main',
color: "primary.contrastText", color: 'primary.contrastText',
p: 1, p: 1,
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Typography <Typography sx={{ fontWeight: 'bold', fontSize: '0.875rem' }}>
sx={{ fontWeight: "bold", fontSize: "0.875rem" }}
>
BACKSTORY BACKSTORY
</Typography> </Typography>
<Box></Box> <Box></Box>
@ -468,96 +431,86 @@ const BackstoryUIOverviewPage: React.FC = () => {
sx={{ sx={{
flex: 1, flex: 1,
p: 1.5, p: 1.5,
overflow: "auto", overflow: 'auto',
fontSize: "0.75rem", fontSize: '0.75rem',
}} }}
> >
<Typography sx={{ mb: 1, fontWeight: "medium" }}> <Typography sx={{ mb: 1, fontWeight: 'medium' }}>
Welcome back, [Name]! Welcome back, [Name]!
</Typography> </Typography>
<Typography sx={{ fontSize: "0.675rem", mb: 2 }}> <Typography sx={{ fontSize: '0.675rem', mb: 2 }}>
Profile: 75% complete Profile: 75% complete
</Typography> </Typography>
<Box <Box
sx={{ sx={{
border: "1px solid", border: '1px solid',
borderColor: "divider", borderColor: 'divider',
borderRadius: 1, borderRadius: 1,
p: 1.5, p: 1.5,
mb: 2, mb: 2,
bgcolor: "background.paper", bgcolor: 'background.paper',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontWeight: "bold", fontWeight: 'bold',
fontSize: "0.75rem", fontSize: '0.75rem',
mb: 0.5, mb: 0.5,
}} }}
> >
Resume Builder Resume Builder
</Typography> </Typography>
<Typography sx={{ fontSize: "0.675rem" }}> <Typography sx={{ fontSize: '0.675rem' }}>3 custom resumes</Typography>
3 custom resumes
</Typography>
</Box> </Box>
<Box <Box
sx={{ sx={{
border: "1px solid", border: '1px solid',
borderColor: "divider", borderColor: 'divider',
borderRadius: 1, borderRadius: 1,
p: 1.5, p: 1.5,
bgcolor: "background.paper", bgcolor: 'background.paper',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontWeight: "bold", fontWeight: 'bold',
fontSize: "0.75rem", fontSize: '0.75rem',
mb: 0.5, mb: 0.5,
}} }}
> >
Recent Activity Recent Activity
</Typography> </Typography>
<Typography sx={{ fontSize: "0.675rem" }}> <Typography sx={{ fontSize: '0.675rem' }}> 5 profile views</Typography>
5 profile views <Typography sx={{ fontSize: '0.675rem' }}> 2 downloads</Typography>
</Typography>
<Typography sx={{ fontSize: "0.675rem" }}>
2 downloads
</Typography>
</Box> </Box>
</Box> </Box>
{/* Mobile footer */} {/* Mobile footer */}
<Box <Box
sx={{ sx={{
bgcolor: "background.default", bgcolor: 'background.default',
p: 1, p: 1,
display: "flex", display: 'flex',
justifyContent: "space-around", justifyContent: 'space-around',
borderTop: "1px solid", borderTop: '1px solid',
borderColor: "divider", borderColor: 'divider',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontWeight: "bold", fontWeight: 'bold',
fontSize: "0.75rem", fontSize: '0.75rem',
color: "secondary.main", color: 'secondary.main',
}} }}
> >
Home Home
</Typography> </Typography>
<Typography <Typography sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
sx={{ fontSize: "0.75rem", color: "text.secondary" }}
>
Profile Profile
</Typography> </Typography>
<Typography <Typography sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
sx={{ fontSize: "0.75rem", color: "text.secondary" }}
>
More More
</Typography> </Typography>
</Box> </Box>

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { import {
AppBar, AppBar,
Box, Box,
@ -18,7 +18,7 @@ import {
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from '@mui/material';
import { import {
Menu as MenuIcon, Menu as MenuIcon,
Search as SearchIcon, Search as SearchIcon,
@ -31,7 +31,7 @@ import {
Save as SaveIcon, Save as SaveIcon,
Delete as TrashIcon, Delete as TrashIcon,
AccessTime as ClockIcon, AccessTime as ClockIcon,
} from "@mui/icons-material"; } from '@mui/icons-material';
interface Resume { interface Resume {
id: number; id: number;
@ -42,8 +42,8 @@ interface Resume {
const MockupPage = () => { const MockupPage = () => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [activeTab, setActiveTab] = useState<string>("resume"); const [activeTab, setActiveTab] = useState<string>('resume');
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState<boolean>(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState<boolean>(false);
const [selectedResume, setSelectedResume] = useState<number | null>(null); const [selectedResume, setSelectedResume] = useState<number | null>(null);
@ -51,20 +51,20 @@ const MockupPage = () => {
const savedResumes: Resume[] = [ const savedResumes: Resume[] = [
{ {
id: 1, id: 1,
name: "Software Engineer - Tech Co", name: 'Software Engineer - Tech Co',
date: "May 15, 2025", date: 'May 15, 2025',
isRecent: true, isRecent: true,
}, },
{ {
id: 2, id: 2,
name: "Product Manager - StartupX", name: 'Product Manager - StartupX',
date: "May 10, 2025", date: 'May 10, 2025',
isRecent: false, isRecent: false,
}, },
{ {
id: 3, id: 3,
name: "Data Scientist - AI Corp", name: 'Data Scientist - AI Corp',
date: "May 5, 2025", date: 'May 5, 2025',
isRecent: false, isRecent: false,
}, },
]; ];
@ -72,10 +72,10 @@ const MockupPage = () => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
height: "100%", height: '100%',
bgcolor: "background.default", bgcolor: 'background.default',
}} }}
> >
{/* Header */} {/* Header */}
@ -83,24 +83,19 @@ const MockupPage = () => {
position="static" position="static"
color="default" color="default"
elevation={1} elevation={1}
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }} sx={{ zIndex: theme => theme.zIndex.drawer + 1 }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
px: 2, px: 2,
py: 1, py: 1,
}} }}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography <Typography variant="h6" component="h1" fontWeight="bold" color="text.primary">
variant="h6"
component="h1"
fontWeight="bold"
color="text.primary"
>
Backstory Backstory
</Typography> </Typography>
{isMobile && ( {isMobile && (
@ -113,20 +108,20 @@ const MockupPage = () => {
</IconButton> </IconButton>
)} )}
</Box> </Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{!isMobile && ( {!isMobile && (
<Button startIcon={<PlusIcon />} color="primary" size="small"> <Button startIcon={<PlusIcon />} color="primary" size="small">
New Resume New Resume
</Button> </Button>
)} )}
<IconButton sx={{ bgcolor: "action.hover", borderRadius: "50%" }}> <IconButton sx={{ bgcolor: 'action.hover', borderRadius: '50%' }}>
<UserIcon /> <UserIcon />
</IconButton> </IconButton>
</Box> </Box>
</Box> </Box>
</AppBar> </AppBar>
<Box sx={{ display: "flex", flex: 1, overflow: "hidden" }}> <Box sx={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Sidebar - hidden on mobile */} {/* Sidebar - hidden on mobile */}
{!isMobile && ( {!isMobile && (
<Drawer <Drawer
@ -136,25 +131,21 @@ const MockupPage = () => {
flexShrink: 0, flexShrink: 0,
[`& .MuiDrawer-paper`]: { [`& .MuiDrawer-paper`]: {
width: 240, width: 240,
boxSizing: "border-box", boxSizing: 'border-box',
position: "relative", position: 'relative',
}, },
}} }}
> >
<Box <Box
sx={{ sx={{
p: 2, p: 2,
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
height: "100%", height: '100%',
}} }}
> >
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography <Typography variant="overline" color="text.secondary" gutterBottom>
variant="overline"
color="text.secondary"
gutterBottom
>
Main Main
</Typography> </Typography>
<List disablePadding> <List disablePadding>
@ -170,13 +161,11 @@ const MockupPage = () => {
<ListItemButton <ListItemButton
sx={{ sx={{
borderRadius: 1, borderRadius: 1,
bgcolor: "primary.lighter", bgcolor: 'primary.lighter',
color: "primary.main", color: 'primary.main',
}} }}
> >
<ListItemIcon <ListItemIcon sx={{ minWidth: 36, color: 'primary.main' }}>
sx={{ minWidth: 36, color: "primary.main" }}
>
<FileTextIcon fontSize="small" /> <FileTextIcon fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Resume Builder" /> <ListItemText primary="Resume Builder" />
@ -186,11 +175,7 @@ const MockupPage = () => {
</Box> </Box>
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography <Typography variant="overline" color="text.secondary" gutterBottom>
variant="overline"
color="text.secondary"
gutterBottom
>
My Content My Content
</Typography> </Typography>
<List disablePadding> <List disablePadding>
@ -218,27 +203,16 @@ const MockupPage = () => {
</List> </List>
</Box> </Box>
<Box sx={{ mt: "auto" }}> <Box sx={{ mt: 'auto' }}>
<Paper <Paper variant="outlined" sx={{ p: 2, bgcolor: 'background.default' }}>
variant="outlined" <Typography variant="subtitle2" color="text.primary" gutterBottom>
sx={{ p: 2, bgcolor: "background.default" }}
>
<Typography
variant="subtitle2"
color="text.primary"
gutterBottom
>
Recent Activity Recent Activity
</Typography> </Typography>
<List dense disablePadding> <List dense disablePadding>
{savedResumes {savedResumes
.filter((r) => r.isRecent) .filter(r => r.isRecent)
.map((resume) => ( .map(resume => (
<ListItem <ListItem key={resume.id} disablePadding sx={{ mb: 0.5 }}>
key={resume.id}
disablePadding
sx={{ mb: 0.5 }}
>
<ListItemIcon sx={{ minWidth: 24 }}> <ListItemIcon sx={{ minWidth: 24 }}>
<ClockIcon fontSize="small" /> <ClockIcon fontSize="small" />
</ListItemIcon> </ListItemIcon>
@ -260,24 +234,20 @@ const MockupPage = () => {
open={isMobileMenuOpen} open={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)} onClose={() => setIsMobileMenuOpen(false)}
sx={{ sx={{
display: { xs: "block", md: "none" }, display: { xs: 'block', md: 'none' },
"& .MuiDrawer-paper": { width: 240 }, '& .MuiDrawer-paper': { width: 240 },
}} }}
> >
<Box <Box
sx={{ sx={{
p: 2, p: 2,
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
height: "100%", height: '100%',
}} }}
> >
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography <Typography variant="overline" color="text.secondary" gutterBottom>
variant="overline"
color="text.secondary"
gutterBottom
>
Main Main
</Typography> </Typography>
<List disablePadding> <List disablePadding>
@ -292,9 +262,9 @@ const MockupPage = () => {
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton <ListItemButton
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
sx={{ bgcolor: "primary.lighter", color: "primary.main" }} sx={{ bgcolor: 'primary.lighter', color: 'primary.main' }}
> >
<ListItemIcon sx={{ color: "primary.main" }}> <ListItemIcon sx={{ color: 'primary.main' }}>
<FileTextIcon fontSize="small" /> <FileTextIcon fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Resume Builder" /> <ListItemText primary="Resume Builder" />
@ -304,11 +274,7 @@ const MockupPage = () => {
</Box> </Box>
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography <Typography variant="overline" color="text.secondary" gutterBottom>
variant="overline"
color="text.secondary"
gutterBottom
>
My Content My Content
</Typography> </Typography>
<List disablePadding> <List disablePadding>
@ -339,15 +305,10 @@ const MockupPage = () => {
</Drawer> </Drawer>
{/* Main content */} {/* Main content */}
<Box sx={{ flex: 1, overflow: "auto", p: 3 }}> <Box sx={{ flex: 1, overflow: 'auto', p: 3 }}>
{/* Resume Builder content */} {/* Resume Builder content */}
<Box sx={{ mb: 4 }}> <Box sx={{ mb: 4 }}>
<Typography <Typography variant="h5" component="h2" fontWeight="bold" gutterBottom>
variant="h5"
component="h2"
fontWeight="bold"
gutterBottom
>
Resume Builder Resume Builder
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
@ -355,13 +316,13 @@ const MockupPage = () => {
</Typography> </Typography>
</Box> </Box>
{/* Tabs */} {/* Tabs */}
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 3 }}> <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs <Tabs
value={activeTab} value={activeTab}
onChange={(_, newValue) => setActiveTab(newValue)} onChange={(_, newValue) => setActiveTab(newValue)}
aria-label="Resume builder tabs" aria-label="Resume builder tabs"
variant={isMobile ? "scrollable" : "standard"} variant={isMobile ? 'scrollable' : 'standard'}
scrollButtons={isMobile ? "auto" : undefined} scrollButtons={isMobile ? 'auto' : undefined}
> >
<Tab label="Job Description" value="job" /> <Tab label="Job Description" value="job" />
<Tab label="Resume" value="resume" /> <Tab label="Resume" value="resume" />
@ -370,7 +331,7 @@ const MockupPage = () => {
</Tabs> </Tabs>
</Box> </Box>
{/* Tab content */} {/* Tab content */}
{activeTab === "job" && ( {activeTab === 'job' && (
<Paper variant="outlined" sx={{ p: 3 }}> <Paper variant="outlined" sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Job Description Job Description
@ -389,44 +350,36 @@ const MockupPage = () => {
</Box> </Box>
</Paper> </Paper>
)} )}
{activeTab === "resume" && ( {activeTab === 'resume' && (
<Paper variant="outlined" sx={{ p: 3 }}> <Paper variant="outlined" sx={{ p: 3 }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
mb: 3, mb: 3,
}} }}
> >
<Typography variant="h6">Resume Editor</Typography> <Typography variant="h6">Resume Editor</Typography>
<Box sx={{ display: "flex", gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<Button <Button variant="outlined" size="small" startIcon={<SaveIcon />}>
variant="outlined"
size="small"
startIcon={<SaveIcon />}
>
Save Save
</Button> </Button>
<Button <Button variant="outlined" size="small" startIcon={<EyeIcon />}>
variant="outlined"
size="small"
startIcon={<EyeIcon />}
>
Preview Preview
</Button> </Button>
</Box> </Box>
</Box> </Box>
{/* Resume content editor with sections */} {/* Resume content editor with sections */}
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Contact information */} {/* Contact information */}
<Paper variant="outlined" sx={{ p: 2 }}> <Paper variant="outlined" sx={{ p: 2 }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
mb: 2, mb: 2,
}} }}
> >
@ -439,8 +392,8 @@ const MockupPage = () => {
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "grid", display: 'grid',
gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
gap: 2, gap: 2,
}} }}
> >
@ -453,11 +406,7 @@ const MockupPage = () => {
> >
Full Name Full Name
</Typography> </Typography>
<TextField <TextField size="small" fullWidth defaultValue="John Doe" />
size="small"
fullWidth
defaultValue="John Doe"
/>
</Box> </Box>
<Box> <Box>
<Typography <Typography
@ -468,11 +417,7 @@ const MockupPage = () => {
> >
Email Email
</Typography> </Typography>
<TextField <TextField size="small" fullWidth defaultValue="john@example.com" />
size="small"
fullWidth
defaultValue="john@example.com"
/>
</Box> </Box>
</Box> </Box>
</Paper> </Paper>
@ -481,9 +426,9 @@ const MockupPage = () => {
<Paper variant="outlined" sx={{ p: 2 }}> <Paper variant="outlined" sx={{ p: 2 }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
mb: 2, mb: 2,
}} }}
> >
@ -507,36 +452,27 @@ const MockupPage = () => {
<Paper variant="outlined" sx={{ p: 2 }}> <Paper variant="outlined" sx={{ p: 2 }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
mb: 2, mb: 2,
}} }}
> >
<Typography variant="subtitle1" fontWeight="medium"> <Typography variant="subtitle1" fontWeight="medium">
Work Experience Work Experience
</Typography> </Typography>
<Button <Button startIcon={<PlusIcon />} color="primary" size="small">
startIcon={<PlusIcon />}
color="primary"
size="small"
>
Add Position Add Position
</Button> </Button>
</Box> </Box>
{/* Job entry */} {/* Job entry */}
<Paper <Paper variant="outlined" sx={{ p: 2, mb: 2, bgcolor: 'background.default' }}>
variant="outlined" <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
sx={{ p: 2, mb: 2, bgcolor: "background.default" }}
>
<Box
sx={{ display: "flex", justifyContent: "space-between" }}
>
<Typography variant="subtitle2" fontWeight="medium"> <Typography variant="subtitle2" fontWeight="medium">
Senior Developer Senior Developer
</Typography> </Typography>
<Box sx={{ display: "flex", gap: 0.5 }}> <Box sx={{ display: 'flex', gap: 0.5 }}>
<IconButton size="small"> <IconButton size="small">
<EditIcon fontSize="small" /> <EditIcon fontSize="small" />
</IconButton> </IconButton>
@ -564,10 +500,10 @@ const MockupPage = () => {
variant="outlined" variant="outlined"
fullWidth fullWidth
sx={{ sx={{
borderStyle: "dashed", borderStyle: 'dashed',
p: 1.5, p: 1.5,
color: "text.secondary", color: 'text.secondary',
"&:hover": { bgcolor: "background.default" }, '&:hover': { bgcolor: 'background.default' },
}} }}
startIcon={<PlusIcon />} startIcon={<PlusIcon />}
> >
@ -575,53 +511,40 @@ const MockupPage = () => {
</Button> </Button>
</Box> </Box>
</Paper> </Paper>
)}{" "} )}{' '}
{activeTab === "saved" && ( {activeTab === 'saved' && (
<Paper variant="outlined" sx={{ p: 3 }}> <Paper variant="outlined" sx={{ p: 3 }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
mb: 3, mb: 3,
}} }}
> >
<Typography variant="h6">Saved Resumes</Typography> <Typography variant="h6">Saved Resumes</Typography>
<Button <Button variant="contained" color="primary" size="small" startIcon={<PlusIcon />}>
variant="contained"
color="primary"
size="small"
startIcon={<PlusIcon />}
>
New Resume New Resume
</Button> </Button>
</Box> </Box>
{/* Resume list */} {/* Resume list */}
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{savedResumes.map((resume) => ( {savedResumes.map(resume => (
<Paper <Paper
key={resume.id} key={resume.id}
variant="outlined" variant="outlined"
sx={{ sx={{
p: 2, p: 2,
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
cursor: "pointer", cursor: 'pointer',
bgcolor: bgcolor:
selectedResume === resume.id selectedResume === resume.id ? 'primary.lighter' : 'background.paper',
? "primary.lighter" borderColor: selectedResume === resume.id ? 'primary.light' : 'divider',
: "background.paper", '&:hover': {
borderColor: bgcolor: selectedResume === resume.id ? 'primary.lighter' : 'action.hover',
selectedResume === resume.id
? "primary.light"
: "divider",
"&:hover": {
bgcolor:
selectedResume === resume.id
? "primary.lighter"
: "action.hover",
}, },
}} }}
onClick={() => setSelectedResume(resume.id)} onClick={() => setSelectedResume(resume.id)}
@ -632,7 +555,7 @@ const MockupPage = () => {
Last edited: {resume.date} Last edited: {resume.date}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: "flex", gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<IconButton size="small"> <IconButton size="small">
<EditIcon fontSize="small" /> <EditIcon fontSize="small" />
</IconButton> </IconButton>
@ -645,26 +568,18 @@ const MockupPage = () => {
</Box> </Box>
</Paper> </Paper>
)} )}
{activeTab === "fact" && ( {activeTab === 'fact' && (
<Paper variant="outlined" sx={{ p: 3 }}> <Paper variant="outlined" sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Fact Check Fact Check
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" paragraph> <Typography variant="body2" color="text.secondary" paragraph>
This tab shows how your resume content compares to your This tab shows how your resume content compares to your employment history data.
employment history data.
</Typography> </Typography>
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Paper <Paper variant="outlined" sx={{ p: 2, mb: 2, bgcolor: 'success.lighter' }}>
variant="outlined" <Typography variant="subtitle2" fontWeight="medium" color="success.dark">
sx={{ p: 2, mb: 2, bgcolor: "success.lighter" }}
>
<Typography
variant="subtitle2"
fontWeight="medium"
color="success.dark"
>
Work History Verification Work History Verification
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
@ -672,20 +587,13 @@ const MockupPage = () => {
</Typography> </Typography>
</Paper> </Paper>
<Paper <Paper variant="outlined" sx={{ p: 2, mb: 2, bgcolor: 'warning.lighter' }}>
variant="outlined" <Typography variant="subtitle2" fontWeight="medium" color="warning.dark">
sx={{ p: 2, mb: 2, bgcolor: "warning.lighter" }}
>
<Typography
variant="subtitle2"
fontWeight="medium"
color="warning.dark"
>
Skills Verification Skills Verification
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
Some skills listed (React Native, Flutter) are not strongly Some skills listed (React Native, Flutter) are not strongly supported by your
supported by your experience documents. experience documents.
</Typography> </Typography>
</Paper> </Paper>
</Box> </Box>
@ -698,26 +606,26 @@ const MockupPage = () => {
{isMobile && ( {isMobile && (
<Paper <Paper
sx={{ sx={{
position: "fixed", position: 'fixed',
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
display: "flex", display: 'flex',
justifyContent: "space-around", justifyContent: 'space-around',
borderTop: 1, borderTop: 1,
borderColor: "divider", borderColor: 'divider',
zIndex: 1100, zIndex: 1100,
}} }}
elevation={3} elevation={3}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
py: 1, py: 1,
px: 2, px: 2,
color: "text.secondary", color: 'text.secondary',
}} }}
component="button" component="button"
> >
@ -726,12 +634,12 @@ const MockupPage = () => {
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
py: 1, py: 1,
px: 2, px: 2,
color: "primary.main", color: 'primary.main',
}} }}
component="button" component="button"
> >
@ -740,12 +648,12 @@ const MockupPage = () => {
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
py: 1, py: 1,
px: 2, px: 2,
color: "text.secondary", color: 'text.secondary',
}} }}
component="button" component="button"
> >

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { import {
Box, Box,
Typography, Typography,
@ -23,8 +23,8 @@ import {
FormControl, FormControl,
InputLabel, InputLabel,
Grid, Grid,
} from "@mui/material"; } from '@mui/material';
import { Person, Business, AssignmentInd } from "@mui/icons-material"; import { Person, Business, AssignmentInd } from '@mui/icons-material';
// Interfaces from the data model // Interfaces from the data model
interface BaseUser { interface BaseUser {
@ -37,7 +37,7 @@ interface BaseUser {
} }
interface Candidate extends BaseUser { interface Candidate extends BaseUser {
type: "candidate"; type: 'candidate';
firstName: string; firstName: string;
lastName: string; lastName: string;
skills: { id: string; name: string; level: string }[]; skills: { id: string; name: string; level: string }[];
@ -45,7 +45,7 @@ interface Candidate extends BaseUser {
} }
interface Employer extends BaseUser { interface Employer extends BaseUser {
type: "employer"; type: 'employer';
companyName: string; companyName: string;
industry: string; industry: string;
companySize: string; companySize: string;
@ -58,58 +58,58 @@ type User = Candidate | Employer;
// Mock data // Mock data
const mockUsers: User[] = [ const mockUsers: User[] = [
{ {
id: "1", id: '1',
email: "john.doe@example.com", email: 'john.doe@example.com',
createdAt: new Date("2023-08-15"), createdAt: new Date('2023-08-15'),
lastLogin: new Date("2023-10-22"), lastLogin: new Date('2023-10-22'),
isActive: true, isActive: true,
type: "candidate", type: 'candidate',
firstName: "John", firstName: 'John',
lastName: "Doe", lastName: 'Doe',
skills: [ skills: [
{ id: "s1", name: "React", level: "advanced" }, { id: 's1', name: 'React', level: 'advanced' },
{ id: "s2", name: "TypeScript", level: "intermediate" }, { id: 's2', name: 'TypeScript', level: 'intermediate' },
], ],
location: { city: "Austin", country: "USA" }, location: { city: 'Austin', country: 'USA' },
}, },
{ {
id: "2", id: '2',
email: "sarah.smith@example.com", email: 'sarah.smith@example.com',
createdAt: new Date("2023-09-10"), createdAt: new Date('2023-09-10'),
lastLogin: new Date("2023-10-24"), lastLogin: new Date('2023-10-24'),
isActive: true, isActive: true,
type: "candidate", type: 'candidate',
firstName: "Sarah", firstName: 'Sarah',
lastName: "Smith", lastName: 'Smith',
skills: [ skills: [
{ id: "s3", name: "Python", level: "expert" }, { id: 's3', name: 'Python', level: 'expert' },
{ id: "s4", name: "Data Science", level: "advanced" }, { id: 's4', name: 'Data Science', level: 'advanced' },
], ],
location: { city: "Seattle", country: "USA", remote: true }, location: { city: 'Seattle', country: 'USA', remote: true },
}, },
{ {
id: "3", id: '3',
email: "tech@acme.com", email: 'tech@acme.com',
createdAt: new Date("2023-07-05"), createdAt: new Date('2023-07-05'),
lastLogin: new Date("2023-10-23"), lastLogin: new Date('2023-10-23'),
isActive: true, isActive: true,
type: "employer", type: 'employer',
companyName: "Acme Tech", companyName: 'Acme Tech',
industry: "Software", industry: 'Software',
companySize: "50-200", companySize: '50-200',
location: { city: "San Francisco", country: "USA" }, location: { city: 'San Francisco', country: 'USA' },
}, },
{ {
id: "4", id: '4',
email: "careers@globex.com", email: 'careers@globex.com',
createdAt: new Date("2023-08-20"), createdAt: new Date('2023-08-20'),
lastLogin: new Date("2023-10-20"), lastLogin: new Date('2023-10-20'),
isActive: false, isActive: false,
type: "employer", type: 'employer',
companyName: "Globex Corporation", companyName: 'Globex Corporation',
industry: "Manufacturing", industry: 'Manufacturing',
companySize: "1000+", companySize: '1000+',
location: { city: "Chicago", country: "USA" }, location: { city: 'Chicago', country: 'USA' },
}, },
]; ];
@ -127,10 +127,10 @@ const UserManagement: React.FC = () => {
}; };
// Filter users based on tab value // Filter users based on tab value
const filteredUsers = users.filter((user) => { const filteredUsers = users.filter(user => {
if (tabValue === 0) return true; if (tabValue === 0) return true;
if (tabValue === 1) return user.type === "candidate"; if (tabValue === 1) return user.type === 'candidate';
if (tabValue === 2) return user.type === "employer"; if (tabValue === 2) return user.type === 'employer';
return false; return false;
}); });
@ -159,7 +159,7 @@ const UserManagement: React.FC = () => {
// Helper function to get user's name for display // Helper function to get user's name for display
const getUserDisplayName = (user: User) => { const getUserDisplayName = (user: User) => {
if (user.type === "candidate") { if (user.type === 'candidate') {
return `${user.firstName} ${user.lastName}`; return `${user.firstName} ${user.lastName}`;
} else { } else {
return user.companyName; return user.companyName;
@ -172,8 +172,8 @@ const UserManagement: React.FC = () => {
}; };
return ( return (
<Box sx={{ width: "100%", p: 3 }}> <Box sx={{ width: '100%', p: 3 }}>
<Paper sx={{ width: "100%", mb: 2 }}> <Paper sx={{ width: '100%', mb: 2 }}>
<Tabs <Tabs
value={tabValue} value={tabValue}
onChange={handleTabChange} onChange={handleTabChange}
@ -200,17 +200,14 @@ const UserManagement: React.FC = () => {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{filteredUsers.map((user) => ( {filteredUsers.map(user => (
<TableRow <TableRow key={user.id} sx={{ '& > td': { whiteSpace: 'nowrap' } }}>
key={user.id}
sx={{ "& > td": { whiteSpace: "nowrap" } }}
>
<TableCell> <TableCell>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "flex-start", alignItems: 'flex-start',
flexDirection: "column", flexDirection: 'column',
}} }}
> >
<Typography>{getUserDisplayName(user)}</Typography> <Typography>{getUserDisplayName(user)}</Typography>
@ -218,12 +215,8 @@ const UserManagement: React.FC = () => {
</TableCell> </TableCell>
<TableCell> <TableCell>
<Chip <Chip
label={ label={user.type === 'candidate' ? 'Candidate' : 'Employer'}
user.type === "candidate" ? "Candidate" : "Employer" color={user.type === 'candidate' ? 'primary' : 'secondary'}
}
color={
user.type === "candidate" ? "primary" : "secondary"
}
size="small" size="small"
/> />
</TableCell> </TableCell>
@ -237,8 +230,8 @@ const UserManagement: React.FC = () => {
<TableCell>{formatDate(user.lastLogin)}</TableCell> <TableCell>{formatDate(user.lastLogin)}</TableCell>
<TableCell> <TableCell>
<Chip <Chip
label={user.isActive ? "Active" : "Inactive"} label={user.isActive ? 'Active' : 'Inactive'}
color={user.isActive ? "success" : "error"} color={user.isActive ? 'success' : 'error'}
size="small" size="small"
/> />
</TableCell> </TableCell>
@ -268,26 +261,17 @@ const UserManagement: React.FC = () => {
</Paper> </Paper>
{/* User Details Dialog */} {/* User Details Dialog */}
<Dialog <Dialog open={openDialog} onClose={handleCloseDialog} maxWidth="md" fullWidth>
open={openDialog}
onClose={handleCloseDialog}
maxWidth="md"
fullWidth
>
{selectedUser && ( {selectedUser && (
<> <>
<DialogTitle> <DialogTitle>
{selectedUser.type === "candidate" {selectedUser.type === 'candidate' ? 'Candidate Details' : 'Employer Details'}
? "Candidate Details"
: "Employer Details"}
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
{selectedUser.type === "candidate" ? ( {selectedUser.type === 'candidate' ? (
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="subtitle1"> <Typography variant="subtitle1">Personal Information</Typography>
Personal Information
</Typography>
<TextField <TextField
label="First Name" label="First Name"
value={selectedUser.firstName} value={selectedUser.firstName}
@ -313,7 +297,7 @@ const UserManagement: React.FC = () => {
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="subtitle1">Skills</Typography> <Typography variant="subtitle1">Skills</Typography>
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
{selectedUser.skills.map((skill) => ( {selectedUser.skills.map(skill => (
<Chip <Chip
key={skill.id} key={skill.id}
label={`${skill.name} (${skill.level})`} label={`${skill.name} (${skill.level})`}
@ -326,9 +310,7 @@ const UserManagement: React.FC = () => {
) : ( ) : (
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="subtitle1"> <Typography variant="subtitle1">Company Information</Typography>
Company Information
</Typography>
<TextField <TextField
label="Company Name" label="Company Name"
value={selectedUser.companyName} value={selectedUser.companyName}
@ -352,9 +334,7 @@ const UserManagement: React.FC = () => {
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="subtitle1"> <Typography variant="subtitle1">Contact Information</Typography>
Contact Information
</Typography>
<TextField <TextField
label="Email" label="Email"
value={selectedUser.email} value={selectedUser.email}
@ -381,26 +361,17 @@ const UserManagement: React.FC = () => {
</Dialog> </Dialog>
{/* AI Config Dialog */} {/* AI Config Dialog */}
<Dialog <Dialog open={aiConfigOpen} onClose={handleCloseAiConfig} maxWidth="md" fullWidth>
open={aiConfigOpen}
onClose={handleCloseAiConfig}
maxWidth="md"
fullWidth
>
{selectedUser && ( {selectedUser && (
<> <>
<DialogTitle> <DialogTitle>AI Configuration for {getUserDisplayName(selectedUser)}</DialogTitle>
AI Configuration for {getUserDisplayName(selectedUser)}
</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
RAG Database Configuration RAG Database Configuration
</Typography> </Typography>
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel id="embedding-model-label"> <InputLabel id="embedding-model-label">Embedding Model</InputLabel>
Embedding Model
</InputLabel>
<Select <Select
labelId="embedding-model-label" labelId="embedding-model-label"
label="Embedding Model" label="Embedding Model"
@ -414,11 +385,7 @@ const UserManagement: React.FC = () => {
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel id="vector-store-label">Vector Store</InputLabel> <InputLabel id="vector-store-label">Vector Store</InputLabel>
<Select <Select labelId="vector-store-label" label="Vector Store" defaultValue="pinecone">
labelId="vector-store-label"
label="Vector Store"
defaultValue="pinecone"
>
<MenuItem value="pinecone">Pinecone</MenuItem> <MenuItem value="pinecone">Pinecone</MenuItem>
<MenuItem value="qdrant">Qdrant</MenuItem> <MenuItem value="qdrant">Qdrant</MenuItem>
<MenuItem value="faiss">FAISS</MenuItem> <MenuItem value="faiss">FAISS</MenuItem>
@ -433,11 +400,7 @@ const UserManagement: React.FC = () => {
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel id="model-label">AI Model</InputLabel> <InputLabel id="model-label">AI Model</InputLabel>
<Select <Select labelId="model-label" label="AI Model" defaultValue="gpt-4">
labelId="model-label"
label="AI Model"
defaultValue="gpt-4"
>
<MenuItem value="gpt-4">GPT-4</MenuItem> <MenuItem value="gpt-4">GPT-4</MenuItem>
<MenuItem value="claude-3">Claude 3</MenuItem> <MenuItem value="claude-3">Claude 3</MenuItem>
<MenuItem value="custom">Custom</MenuItem> <MenuItem value="custom">Custom</MenuItem>
@ -482,9 +445,9 @@ const UserManagement: React.FC = () => {
fullWidth fullWidth
margin="normal" margin="normal"
defaultValue={`You are an AI assistant helping ${ defaultValue={`You are an AI assistant helping ${
selectedUser.type === "candidate" selectedUser.type === 'candidate'
? "job candidates find relevant positions" ? 'job candidates find relevant positions'
: "employers find qualified candidates" : 'employers find qualified candidates'
}. Be professional, helpful, and concise in your responses.`} }. Be professional, helpful, and concise in your responses.`}
/> />

View File

@ -1,21 +1,14 @@
// Replace the existing AuthContext.tsx with these enhancements // Replace the existing AuthContext.tsx with these enhancements
import React, { import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
createContext, import * as Types from '../types/types';
useContext,
useState,
useCallback,
useEffect,
useRef,
} from "react";
import * as Types from "../types/types";
import { import {
ApiClient, ApiClient,
CreateCandidateRequest, CreateCandidateRequest,
CreateEmployerRequest, CreateEmployerRequest,
GuestConversionRequest, GuestConversionRequest,
} from "services/api-client"; } from 'services/api-client';
import { formatApiRequest, toCamelCase } from "types/conversion"; import { formatApiRequest, toCamelCase } from 'types/conversion';
// ============================ // ============================
// Enhanced Types and Interfaces // Enhanced Types and Interfaces
@ -54,13 +47,13 @@ interface PasswordResetRequest {
// ============================ // ============================
const TOKEN_STORAGE = { const TOKEN_STORAGE = {
ACCESS_TOKEN: "accessToken", ACCESS_TOKEN: 'accessToken',
REFRESH_TOKEN: "refreshToken", REFRESH_TOKEN: 'refreshToken',
USER_DATA: "userData", USER_DATA: 'userData',
TOKEN_EXPIRY: "tokenExpiry", TOKEN_EXPIRY: 'tokenExpiry',
USER_TYPE: "userType", USER_TYPE: 'userType',
IS_GUEST: "isGuest", IS_GUEST: 'isGuest',
PENDING_VERIFICATION_EMAIL: "pendingVerificationEmail", PENDING_VERIFICATION_EMAIL: 'pendingVerificationEmail',
} as const; } as const;
// ============================ // ============================
@ -69,17 +62,17 @@ const TOKEN_STORAGE = {
function parseJwtPayload(token: string): any { function parseJwtPayload(token: string): any {
try { try {
const base64Url = token.split(".")[1]; const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent( const jsonPayload = decodeURIComponent(
atob(base64) atob(base64)
.split("") .split('')
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)) .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join("") .join('')
); );
return JSON.parse(jsonPayload); return JSON.parse(jsonPayload);
} catch (error) { } catch (error) {
console.error("Failed to parse JWT token:", error); console.error('Failed to parse JWT token:', error);
return null; return null;
} }
} }
@ -117,7 +110,7 @@ function prepareUserDataForStorage(user: Types.User): string {
const userForStorage = formatApiRequest(user); const userForStorage = formatApiRequest(user);
return JSON.stringify(userForStorage); return JSON.stringify(userForStorage);
} catch (error) { } catch (error) {
console.error("Failed to prepare user data for storage:", error); console.error('Failed to prepare user data for storage:', error);
return JSON.stringify(user); // Fallback to direct serialization return JSON.stringify(user); // Fallback to direct serialization
} }
} }
@ -131,36 +124,24 @@ function parseStoredUserData(userDataStr: string): Types.User | null {
return convertedData; return convertedData;
} catch (error) { } catch (error) {
console.error("Failed to parse stored user data:", error); console.error('Failed to parse stored user data:', error);
return null; return null;
} }
} }
function updateStoredUserData(user: Types.User): void { function updateStoredUserData(user: Types.User): void {
try { try {
localStorage.setItem( localStorage.setItem(TOKEN_STORAGE.USER_DATA, prepareUserDataForStorage(user));
TOKEN_STORAGE.USER_DATA,
prepareUserDataForStorage(user)
);
} catch (error) { } catch (error) {
console.error("Failed to update stored user data:", error); console.error('Failed to update stored user data:', error);
} }
} }
function storeAuthData( function storeAuthData(authResponse: Types.AuthResponse, isGuest = false): void {
authResponse: Types.AuthResponse,
isGuest = false
): void {
localStorage.setItem(TOKEN_STORAGE.ACCESS_TOKEN, authResponse.accessToken); localStorage.setItem(TOKEN_STORAGE.ACCESS_TOKEN, authResponse.accessToken);
localStorage.setItem(TOKEN_STORAGE.REFRESH_TOKEN, authResponse.refreshToken); localStorage.setItem(TOKEN_STORAGE.REFRESH_TOKEN, authResponse.refreshToken);
localStorage.setItem( localStorage.setItem(TOKEN_STORAGE.USER_DATA, prepareUserDataForStorage(authResponse.user));
TOKEN_STORAGE.USER_DATA, localStorage.setItem(TOKEN_STORAGE.TOKEN_EXPIRY, authResponse.expiresAt.toString());
prepareUserDataForStorage(authResponse.user)
);
localStorage.setItem(
TOKEN_STORAGE.TOKEN_EXPIRY,
authResponse.expiresAt.toString()
);
localStorage.setItem(TOKEN_STORAGE.USER_TYPE, authResponse.user.userType); localStorage.setItem(TOKEN_STORAGE.USER_TYPE, authResponse.user.userType);
localStorage.setItem(TOKEN_STORAGE.IS_GUEST, isGuest.toString()); localStorage.setItem(TOKEN_STORAGE.IS_GUEST, isGuest.toString());
} }
@ -191,7 +172,7 @@ function getStoredAuthData(): {
expiresAt = parseInt(expiryStr, 10); expiresAt = parseInt(expiryStr, 10);
} }
} catch (error) { } catch (error) {
console.error("Failed to parse stored auth data:", error); console.error('Failed to parse stored auth data:', error);
clearStoredAuth(); clearStoredAuth();
} }
@ -201,7 +182,7 @@ function getStoredAuthData(): {
userData, userData,
expiresAt, expiresAt,
userType, userType,
isGuest: isGuestStr === "true", isGuest: isGuestStr === 'true',
}; };
} }
@ -232,7 +213,7 @@ function useAuthenticationLogic() {
const response = await apiClient.refreshToken(refreshToken); const response = await apiClient.refreshToken(refreshToken);
return response; return response;
} catch (error) { } catch (error) {
console.error("Token refresh failed:", error); console.error('Token refresh failed:', error);
return null; return null;
} }
}, },
@ -248,10 +229,10 @@ function useAuthenticationLogic() {
guestCreationAttempted.current = true; guestCreationAttempted.current = true;
try { try {
console.log("🔄 Creating guest session..."); console.log('🔄 Creating guest session...');
const guestAuth = await apiClient.createGuestSession(); const guestAuth = await apiClient.createGuestSession();
if (guestAuth && guestAuth.user && guestAuth.user.userType === "guest") { if (guestAuth && guestAuth.user && guestAuth.user.userType === 'guest') {
storeAuthData(guestAuth, true); storeAuthData(guestAuth, true);
apiClient.setAuthToken(guestAuth.accessToken); apiClient.setAuthToken(guestAuth.accessToken);
@ -266,17 +247,17 @@ function useAuthenticationLogic() {
mfaResponse: null, mfaResponse: null,
}); });
console.log("👤 Guest session created successfully:", guestAuth.user); console.log('👤 Guest session created successfully:', guestAuth.user);
return true; return true;
} }
return false; return false;
} catch (error) { } catch (error) {
console.error("❌ Failed to create guest session:", error); console.error('❌ Failed to create guest session:', error);
guestCreationAttempted.current = false; guestCreationAttempted.current = false;
// Set to unauthenticated state if guest creation fails // Set to unauthenticated state if guest creation fails
setAuthState((prev) => ({ setAuthState(prev => ({
...prev, ...prev,
user: null, user: null,
guest: null, guest: null,
@ -284,7 +265,7 @@ function useAuthenticationLogic() {
isGuest: false, isGuest: false,
isLoading: false, isLoading: false,
isInitializing: false, isInitializing: false,
error: "Failed to create guest session", error: 'Failed to create guest session',
})); }));
return false; return false;
@ -302,35 +283,28 @@ function useAuthenticationLogic() {
// If no stored tokens, create guest session // If no stored tokens, create guest session
if (!stored.accessToken || !stored.refreshToken || !stored.userData) { if (!stored.accessToken || !stored.refreshToken || !stored.userData) {
console.log("🔄 No stored auth found, creating guest session..."); console.log('🔄 No stored auth found, creating guest session...');
await createGuestSession(); await createGuestSession();
return; return;
} }
// For guests, always verify the session exists on server // For guests, always verify the session exists on server
if (stored.userType === "guest" && stored.userData) { if (stored.userType === 'guest' && stored.userData) {
console.log(stored.userData); console.log(stored.userData);
try { try {
// Make a quick API call to verify guest still exists // Make a quick API call to verify guest still exists
const response = await fetch( const response = await fetch(`${apiClient.getBaseUrl()}/users/${stored.userData.id}`, {
`${apiClient.getBaseUrl()}/users/${stored.userData.id}`, headers: { Authorization: `Bearer ${stored.accessToken}` },
{ });
headers: { Authorization: `Bearer ${stored.accessToken}` },
}
);
if (!response.ok) { if (!response.ok) {
console.log( console.log('🔄 Guest session invalid, creating new guest session...');
"🔄 Guest session invalid, creating new guest session..."
);
clearStoredAuth(); clearStoredAuth();
await createGuestSession(); await createGuestSession();
return; return;
} }
} catch (error) { } catch (error) {
console.log( console.log('🔄 Guest verification failed, creating new guest session...');
"🔄 Guest verification failed, creating new guest session..."
);
clearStoredAuth(); clearStoredAuth();
await createGuestSession(); await createGuestSession();
return; return;
@ -339,12 +313,12 @@ function useAuthenticationLogic() {
// Check if access token is expired // Check if access token is expired
if (isTokenExpired(stored.accessToken)) { if (isTokenExpired(stored.accessToken)) {
console.log("🔄 Access token expired, attempting refresh..."); console.log('🔄 Access token expired, attempting refresh...');
const refreshResult = await refreshAccessToken(stored.refreshToken); const refreshResult = await refreshAccessToken(stored.refreshToken);
if (refreshResult) { if (refreshResult) {
const isGuest = stored.userType === "guest"; const isGuest = stored.userType === 'guest';
storeAuthData(refreshResult, isGuest); storeAuthData(refreshResult, isGuest);
apiClient.setAuthToken(refreshResult.accessToken); apiClient.setAuthToken(refreshResult.accessToken);
@ -359,9 +333,9 @@ function useAuthenticationLogic() {
mfaResponse: null, mfaResponse: null,
}); });
console.log("✅ Token refreshed successfully"); console.log('✅ Token refreshed successfully');
} else { } else {
console.log("❌ Token refresh failed, creating new guest session..."); console.log('❌ Token refresh failed, creating new guest session...');
clearStoredAuth(); clearStoredAuth();
apiClient.clearAuthToken(); apiClient.clearAuthToken();
await createGuestSession(); await createGuestSession();
@ -369,7 +343,7 @@ function useAuthenticationLogic() {
} else { } else {
// Access token is still valid // Access token is still valid
apiClient.setAuthToken(stored.accessToken); apiClient.setAuthToken(stored.accessToken);
const isGuest = stored.userType === "guest"; const isGuest = stored.userType === 'guest';
setAuthState({ setAuthState({
user: isGuest ? null : stored.userData, user: isGuest ? null : stored.userData,
@ -382,10 +356,10 @@ function useAuthenticationLogic() {
mfaResponse: null, mfaResponse: null,
}); });
console.log("✅ Restored authentication from stored tokens"); console.log('✅ Restored authentication from stored tokens');
} }
} catch (error) { } catch (error) {
console.error("❌ Error initializing auth:", error); console.error('❌ Error initializing auth:', error);
clearStoredAuth(); clearStoredAuth();
apiClient.clearAuthToken(); apiClient.clearAuthToken();
await createGuestSession(); await createGuestSession();
@ -420,7 +394,7 @@ function useAuthenticationLogic() {
} }
const refreshTimer = setTimeout(() => { const refreshTimer = setTimeout(() => {
console.log("🔄 Auto-refreshing token before expiry..."); console.log('🔄 Auto-refreshing token before expiry...');
initializeAuth(); initializeAuth();
}, timeUntilExpiry); }, timeUntilExpiry);
@ -430,7 +404,7 @@ function useAuthenticationLogic() {
// Enhanced login with MFA support // Enhanced login with MFA support
const login = useCallback( const login = useCallback(
async (loginData: LoginRequest): Promise<boolean> => { async (loginData: LoginRequest): Promise<boolean> => {
setAuthState((prev) => ({ setAuthState(prev => ({
...prev, ...prev,
isLoading: true, isLoading: true,
error: null, error: null,
@ -443,9 +417,9 @@ function useAuthenticationLogic() {
password: loginData.password, password: loginData.password,
}); });
if ("mfaRequired" in result) { if ('mfaRequired' in result) {
// MFA required for new device // MFA required for new device
setAuthState((prev) => ({ setAuthState(prev => ({
...prev, ...prev,
isLoading: false, isLoading: false,
mfaResponse: result, mfaResponse: result,
@ -457,7 +431,7 @@ function useAuthenticationLogic() {
storeAuthData(authResponse, false); storeAuthData(authResponse, false);
apiClient.setAuthToken(authResponse.accessToken); apiClient.setAuthToken(authResponse.accessToken);
setAuthState((prev) => ({ setAuthState(prev => ({
...prev, ...prev,
user: authResponse.user, user: authResponse.user,
guest: null, guest: null,
@ -468,17 +442,13 @@ function useAuthenticationLogic() {
mfaResponse: null, mfaResponse: null,
})); }));
console.log( console.log('✅ Login successful, converted from guest to authenticated user');
"✅ Login successful, converted from guest to authenticated user"
);
return true; return true;
} }
} catch (error: any) { } catch (error: any) {
const errorMessage = const errorMessage =
error instanceof Error error instanceof Error ? error.message : 'Network error occurred. Please try again.';
? error.message setAuthState(prev => ({
: "Network error occurred. Please try again.";
setAuthState((prev) => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage, error: errorMessage,
@ -494,10 +464,10 @@ function useAuthenticationLogic() {
const convertGuestToUser = useCallback( const convertGuestToUser = useCallback(
async (registrationData: GuestConversionRequest): Promise<boolean> => { async (registrationData: GuestConversionRequest): Promise<boolean> => {
if (!authState.isGuest || !authState.guest) { if (!authState.isGuest || !authState.guest) {
throw new Error("Not currently a guest user"); throw new Error('Not currently a guest user');
} }
setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
const result = await apiClient.convertGuestToUser(registrationData); const result = await apiClient.convertGuestToUser(registrationData);
@ -506,7 +476,7 @@ function useAuthenticationLogic() {
storeAuthData(result.auth, false); storeAuthData(result.auth, false);
apiClient.setAuthToken(result.auth.accessToken); apiClient.setAuthToken(result.auth.accessToken);
setAuthState((prev) => ({ setAuthState(prev => ({
...prev, ...prev,
user: result.auth.user, user: result.auth.user,
guest: null, guest: null,
@ -516,14 +486,12 @@ function useAuthenticationLogic() {
error: null, error: null,
})); }));
console.log("✅ Guest successfully converted to permanent user"); console.log('✅ Guest successfully converted to permanent user');
return true; return true;
} catch (error: any) { } catch (error: any) {
const errorMessage = const errorMessage =
error instanceof Error error instanceof Error ? error.message : 'Failed to convert guest account';
? error.message setAuthState(prev => ({
: "Failed to convert guest account";
setAuthState((prev) => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage, error: errorMessage,
@ -537,7 +505,7 @@ function useAuthenticationLogic() {
// MFA verification // MFA verification
const verifyMFA = useCallback( const verifyMFA = useCallback(
async (mfaData: Types.MFAVerifyRequest): Promise<boolean> => { async (mfaData: Types.MFAVerifyRequest): Promise<boolean> => {
setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
const result = await apiClient.verifyMFA(mfaData); const result = await apiClient.verifyMFA(mfaData);
@ -547,7 +515,7 @@ function useAuthenticationLogic() {
storeAuthData(authResponse, false); storeAuthData(authResponse, false);
apiClient.setAuthToken(authResponse.accessToken); apiClient.setAuthToken(authResponse.accessToken);
setAuthState((prev) => ({ setAuthState(prev => ({
...prev, ...prev,
user: authResponse.user, user: authResponse.user,
guest: null, guest: null,
@ -558,15 +526,14 @@ function useAuthenticationLogic() {
mfaResponse: null, mfaResponse: null,
})); }));
console.log("✅ MFA verification successful, converted from guest"); console.log('✅ MFA verification successful, converted from guest');
return true; return true;
} }
return false; return false;
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : 'MFA verification failed';
error instanceof Error ? error.message : "MFA verification failed"; setAuthState(prev => ({
setAuthState((prev) => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage, error: errorMessage,
@ -587,14 +554,12 @@ function useAuthenticationLogic() {
try { try {
await apiClient.logout(stored.accessToken, stored.refreshToken); await apiClient.logout(stored.accessToken, stored.refreshToken);
} catch (error) { } catch (error) {
console.warn( console.warn('Logout request failed, proceeding with local cleanup');
"Logout request failed, proceeding with local cleanup"
);
} }
} }
} }
} catch (error) { } catch (error) {
console.warn("Error during logout:", error); console.warn('Error during logout:', error);
} finally { } finally {
// Always clear stored auth and create new guest session // Always clear stored auth and create new guest session
clearStoredAuth(); clearStoredAuth();
@ -604,25 +569,20 @@ function useAuthenticationLogic() {
// Create new guest session // Create new guest session
await createGuestSession(); await createGuestSession();
console.log("🔄 Logged out, created new guest session"); console.log('🔄 Logged out, created new guest session');
} }
}, [ }, [apiClient, authState.isAuthenticated, authState.isGuest, createGuestSession]);
apiClient,
authState.isAuthenticated,
authState.isGuest,
createGuestSession,
]);
// Update user data // Update user data
const updateUserData = useCallback( const updateUserData = useCallback(
(updatedUser: Types.User) => { (updatedUser: Types.User) => {
updateStoredUserData(updatedUser); updateStoredUserData(updatedUser);
setAuthState((prev) => ({ setAuthState(prev => ({
...prev, ...prev,
user: authState.isGuest ? null : updatedUser, user: authState.isGuest ? null : updatedUser,
guest: authState.isGuest ? (updatedUser as Types.Guest) : prev.guest, guest: authState.isGuest ? (updatedUser as Types.Guest) : prev.guest,
})); }));
console.log("✅ User data updated"); console.log('✅ User data updated');
}, },
[authState.isGuest] [authState.isGuest]
); );
@ -630,19 +590,18 @@ function useAuthenticationLogic() {
// Email verification functions (unchanged) // Email verification functions (unchanged)
const verifyEmail = useCallback( const verifyEmail = useCallback(
async (verificationData: EmailVerificationRequest) => { async (verificationData: EmailVerificationRequest) => {
setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
const result = await apiClient.verifyEmail(verificationData); const result = await apiClient.verifyEmail(verificationData);
setAuthState((prev) => ({ ...prev, isLoading: false })); setAuthState(prev => ({ ...prev, isLoading: false }));
return { return {
message: result.message || "Email verified successfully", message: result.message || 'Email verified successfully',
userType: result.userType || "user", userType: result.userType || 'user',
}; };
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : 'Email verification failed';
error instanceof Error ? error.message : "Email verification failed"; setAuthState(prev => ({
setAuthState((prev) => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage, error: errorMessage,
@ -656,18 +615,16 @@ function useAuthenticationLogic() {
// Other existing methods remain the same... // Other existing methods remain the same...
const resendEmailVerification = useCallback( const resendEmailVerification = useCallback(
async (email: string): Promise<boolean> => { async (email: string): Promise<boolean> => {
setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
await apiClient.resendVerificationEmail({ email }); await apiClient.resendVerificationEmail({ email });
setAuthState((prev) => ({ ...prev, isLoading: false })); setAuthState(prev => ({ ...prev, isLoading: false }));
return true; return true;
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error instanceof Error error instanceof Error ? error.message : 'Failed to resend verification email';
? error.message setAuthState(prev => ({
: "Failed to resend verification email";
setAuthState((prev) => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage, error: errorMessage,
@ -688,20 +645,19 @@ function useAuthenticationLogic() {
const createEmployerAccount = useCallback( const createEmployerAccount = useCallback(
async (employerData: CreateEmployerRequest): Promise<boolean> => { async (employerData: CreateEmployerRequest): Promise<boolean> => {
setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
const employer = await apiClient.createEmployer(employerData); const employer = await apiClient.createEmployer(employerData);
console.log("✅ Employer created:", employer); console.log('✅ Employer created:', employer);
setPendingVerificationEmail(employerData.email); setPendingVerificationEmail(employerData.email);
setAuthState((prev) => ({ ...prev, isLoading: false })); setAuthState(prev => ({ ...prev, isLoading: false }));
return true; return true;
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : 'Account creation failed';
error instanceof Error ? error.message : "Account creation failed"; setAuthState(prev => ({
setAuthState((prev) => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage, error: errorMessage,
@ -714,18 +670,16 @@ function useAuthenticationLogic() {
const requestPasswordReset = useCallback( const requestPasswordReset = useCallback(
async (email: string): Promise<boolean> => { async (email: string): Promise<boolean> => {
setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
await apiClient.requestPasswordReset({ email }); await apiClient.requestPasswordReset({ email });
setAuthState((prev) => ({ ...prev, isLoading: false })); setAuthState(prev => ({ ...prev, isLoading: false }));
return true; return true;
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error instanceof Error error instanceof Error ? error.message : 'Password reset request failed';
? error.message setAuthState(prev => ({
: "Password reset request failed";
setAuthState((prev) => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage, error: errorMessage,
@ -742,16 +696,16 @@ function useAuthenticationLogic() {
return false; return false;
} }
setAuthState((prev) => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
const refreshResult = await refreshAccessToken(stored.refreshToken); const refreshResult = await refreshAccessToken(stored.refreshToken);
if (refreshResult) { if (refreshResult) {
const isGuest = stored.userType === "guest"; const isGuest = stored.userType === 'guest';
storeAuthData(refreshResult, isGuest); storeAuthData(refreshResult, isGuest);
apiClient.setAuthToken(refreshResult.accessToken); apiClient.setAuthToken(refreshResult.accessToken);
setAuthState((prev) => ({ setAuthState(prev => ({
...prev, ...prev,
user: isGuest ? null : refreshResult.user, user: isGuest ? null : refreshResult.user,
guest: isGuest ? (refreshResult.user as Types.Guest) : null, guest: isGuest ? (refreshResult.user as Types.Guest) : null,
@ -770,27 +724,22 @@ function useAuthenticationLogic() {
// Resend MFA code // Resend MFA code
const resendMFACode = useCallback( const resendMFACode = useCallback(
async ( async (email: string, deviceId: string, deviceName: string): Promise<boolean> => {
email: string, setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
deviceId: string,
deviceName: string
): Promise<boolean> => {
setAuthState((prev) => ({ ...prev, isLoading: true, error: null }));
try { try {
await apiClient.requestMFA({ await apiClient.requestMFA({
email, email,
password: "", // This would need to be stored securely or re-entered password: '', // This would need to be stored securely or re-entered
deviceId, deviceId,
deviceName, deviceName,
}); });
setAuthState((prev) => ({ ...prev, isLoading: false })); setAuthState(prev => ({ ...prev, isLoading: false }));
return true; return true;
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : 'Failed to resend MFA code';
error instanceof Error ? error.message : "Failed to resend MFA code"; setAuthState(prev => ({
setAuthState((prev) => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage, error: errorMessage,
@ -803,7 +752,7 @@ function useAuthenticationLogic() {
// Clear MFA state // Clear MFA state
const clearMFA = useCallback(() => { const clearMFA = useCallback(() => {
setAuthState((prev) => ({ setAuthState(prev => ({
...prev, ...prev,
mfaResponse: null, mfaResponse: null,
error: null, error: null,
@ -835,9 +784,7 @@ function useAuthenticationLogic() {
// Enhanced Context Provider // Enhanced Context Provider
// ============================ // ============================
const AuthContext = createContext<ReturnType< const AuthContext = createContext<ReturnType<typeof useAuthenticationLogic> | null>(null);
typeof useAuthenticationLogic
> | null>(null);
function AuthProvider({ children }: { children: React.ReactNode }) { function AuthProvider({ children }: { children: React.ReactNode }) {
const auth = useAuthenticationLogic(); const auth = useAuthenticationLogic();
@ -848,7 +795,7 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
function useAuth() { function useAuth() {
const context = useContext(AuthContext); const context = useContext(AuthContext);
if (!context) { if (!context) {
throw new Error("useAuth must be used within an AuthProvider"); throw new Error('useAuth must be used within an AuthProvider');
} }
return context; return context;
} }
@ -904,9 +851,6 @@ export type {
GuestConversionRequest, GuestConversionRequest,
}; };
export type { export type { CreateCandidateRequest, CreateEmployerRequest } from '../services/api-client';
CreateCandidateRequest,
CreateEmployerRequest,
} from "../services/api-client";
export { useAuthenticationLogic, AuthProvider, useAuth, ProtectedRoute }; export { useAuthenticationLogic, AuthProvider, useAuth, ProtectedRoute };

View File

@ -1,30 +1,23 @@
import React, { import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
createContext, import * as Types from 'types/types';
useContext,
useState,
useCallback,
useEffect,
useRef,
} from "react";
import * as Types from "types/types";
// Assuming you're using React Router // Assuming you're using React Router
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { SetSnackType, SeverityType, Snack } from "components/Snack"; import { SetSnackType, SeverityType, Snack } from 'components/Snack';
// ============================ // ============================
// Storage Keys // Storage Keys
// ============================ // ============================
const STORAGE_KEYS = { const STORAGE_KEYS = {
SELECTED_CANDIDATE_ID: "selectedCandidateId", SELECTED_CANDIDATE_ID: 'selectedCandidateId',
SELECTED_JOB_ID: "selectedJobId", SELECTED_JOB_ID: 'selectedJobId',
SELECTED_EMPLOYER_ID: "selectedEmployerId", SELECTED_EMPLOYER_ID: 'selectedEmployerId',
LAST_ROUTE: "lastVisitedRoute", LAST_ROUTE: 'lastVisitedRoute',
ROUTE_STATE: "routeState", ROUTE_STATE: 'routeState',
ACTIVE_TAB: "activeTab", ACTIVE_TAB: 'activeTab',
APPLIED_FILTERS: "appliedFilters", APPLIED_FILTERS: 'appliedFilters',
SIDEBAR_COLLAPSED: "sidebarCollapsed", SIDEBAR_COLLAPSED: 'sidebarCollapsed',
} as const; } as const;
// ============================ // ============================
@ -81,7 +74,7 @@ function getStoredId(key: string): string | null {
try { try {
return localStorage.getItem(key); return localStorage.getItem(key);
} catch (error) { } catch (error) {
console.warn("Failed to read from localStorage:", error); console.warn('Failed to read from localStorage:', error);
return null; return null;
} }
} }
@ -94,7 +87,7 @@ function setStoredId(key: string, id: string | null): void {
localStorage.removeItem(key); localStorage.removeItem(key);
} }
} catch (error) { } catch (error) {
console.warn("Failed to write to localStorage:", error); console.warn('Failed to write to localStorage:', error);
} }
} }
@ -146,19 +139,14 @@ export function useAppStateLogic(): AppStateContextType {
const { apiClient } = useAuth(); const { apiClient } = useAuth();
// Entity state // Entity state
const [selectedCandidate, setSelectedCandidateState] = const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null);
useState<Types.Candidate | null>(null);
const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null); const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null);
const [selectedEmployer, setSelectedEmployerState] = const [selectedEmployer, setSelectedEmployerState] = useState<Types.Employer | null>(null);
useState<Types.Employer | null>(null); const [selectedResume, setSelectedResume] = useState<Types.Resume | null>(null);
const [selectedResume, setSelectedResume] = useState<Types.Resume | null>(
null
);
const [isInitializing, setIsInitializing] = useState<boolean>(true); const [isInitializing, setIsInitializing] = useState<boolean>(true);
// Route state // Route state
const [routeState, setRouteStateState] = const [routeState, setRouteStateState] = useState<RouteState>(getInitialRouteState);
useState<RouteState>(getInitialRouteState);
// ============================ // ============================
// Initialization Effect // Initialization Effect
@ -184,13 +172,13 @@ export function useAppStateLogic(): AppStateContextType {
const candidate = await apiClient.getCandidate(candidateId); const candidate = await apiClient.getCandidate(candidateId);
if (candidate) { if (candidate) {
setSelectedCandidateState(candidate); setSelectedCandidateState(candidate);
console.log("Restored candidate from storage:", candidate); console.log('Restored candidate from storage:', candidate);
} else { } else {
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null); setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
console.log("Candidate not found, cleared from storage"); console.log('Candidate not found, cleared from storage');
} }
} catch (error) { } catch (error) {
console.warn("Failed to restore candidate:", error); console.warn('Failed to restore candidate:', error);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null); setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
} }
})() })()
@ -204,13 +192,13 @@ export function useAppStateLogic(): AppStateContextType {
const job = await apiClient.getJob(jobId); const job = await apiClient.getJob(jobId);
if (job) { if (job) {
setSelectedJobState(job); setSelectedJobState(job);
console.log("Restored job from storage:", job); console.log('Restored job from storage:', job);
} else { } else {
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null); setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
console.log("Job not found, cleared from storage"); console.log('Job not found, cleared from storage');
} }
} catch (error) { } catch (error) {
console.warn("Failed to restore job:", error); console.warn('Failed to restore job:', error);
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null); setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
} }
})() })()
@ -224,13 +212,13 @@ export function useAppStateLogic(): AppStateContextType {
const employer = await apiClient.getEmployer(employerId); const employer = await apiClient.getEmployer(employerId);
if (employer) { if (employer) {
setSelectedEmployerState(employer); setSelectedEmployerState(employer);
console.log("Restored employer from storage:", employer); console.log('Restored employer from storage:', employer);
} else { } else {
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null); setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
console.log("Employer not found, cleared from storage"); console.log('Employer not found, cleared from storage');
} }
} catch (error) { } catch (error) {
console.warn("Failed to restore employer:", error); console.warn('Failed to restore employer:', error);
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null); setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
} }
})() })()
@ -240,7 +228,7 @@ export function useAppStateLogic(): AppStateContextType {
// Wait for all restoration attempts to complete // Wait for all restoration attempts to complete
await Promise.all(promises); await Promise.all(promises);
} catch (error) { } catch (error) {
console.error("Error during app state initialization:", error); console.error('Error during app state initialization:', error);
} finally { } finally {
setIsInitializing(false); setIsInitializing(false);
} }
@ -255,18 +243,11 @@ export function useAppStateLogic(): AppStateContextType {
useEffect(() => { useEffect(() => {
// Don't save certain routes (login, register, etc.) // Don't save certain routes (login, register, etc.)
const excludedRoutes = [ const excludedRoutes = ['/login', '/register', '/verify-email', '/reset-password'];
"/login", const shouldSaveRoute = !excludedRoutes.some(route => location.pathname.startsWith(route));
"/register",
"/verify-email",
"/reset-password",
];
const shouldSaveRoute = !excludedRoutes.some((route) =>
location.pathname.startsWith(route)
);
if (shouldSaveRoute && !isInitializing) { if (shouldSaveRoute && !isInitializing) {
setRouteStateState((prev) => { setRouteStateState(prev => {
const newState = { ...prev, lastRoute: location.pathname }; const newState = { ...prev, lastRoute: location.pathname };
persistRouteState(newState); persistRouteState(newState);
return newState; return newState;
@ -278,28 +259,25 @@ export function useAppStateLogic(): AppStateContextType {
// Entity State Setters with Persistence // Entity State Setters with Persistence
// ============================ // ============================
const setSelectedCandidate = useCallback( const setSelectedCandidate = useCallback((candidate: Types.Candidate | null) => {
(candidate: Types.Candidate | null) => { setSelectedCandidateState(candidate);
setSelectedCandidateState(candidate); setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
if (candidate) { if (candidate) {
console.log("Selected candidate:", candidate); console.log('Selected candidate:', candidate);
} else { } else {
console.log("Cleared selected candidate"); console.log('Cleared selected candidate');
} }
}, }, []);
[]
);
const setSelectedJob = useCallback((job: Types.Job | null) => { const setSelectedJob = useCallback((job: Types.Job | null) => {
setSelectedJobState(job); setSelectedJobState(job);
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null); setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null);
if (job) { if (job) {
console.log("Selected job:", job); console.log('Selected job:', job);
} else { } else {
console.log("Cleared selected job"); console.log('Cleared selected job');
} }
}, []); }, []);
@ -308,9 +286,9 @@ export function useAppStateLogic(): AppStateContextType {
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, employer?.id || null); setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, employer?.id || null);
if (employer) { if (employer) {
console.log("Selected employer:", employer); console.log('Selected employer:', employer);
} else { } else {
console.log("Cleared selected employer"); console.log('Cleared selected employer');
} }
}, []); }, []);
@ -323,7 +301,7 @@ export function useAppStateLogic(): AppStateContextType {
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null); setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null); setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
console.log("Cleared all selections"); console.log('Cleared all selections');
}, []); }, []);
// ============================ // ============================
@ -331,7 +309,7 @@ export function useAppStateLogic(): AppStateContextType {
// ============================ // ============================
const saveCurrentRoute = useCallback(() => { const saveCurrentRoute = useCallback(() => {
setRouteStateState((prev) => { setRouteStateState(prev => {
const newState = { ...prev, lastRoute: location.pathname }; const newState = { ...prev, lastRoute: location.pathname };
persistRouteState(newState); persistRouteState(newState);
return newState; return newState;
@ -345,7 +323,7 @@ export function useAppStateLogic(): AppStateContextType {
}, [routeState.lastRoute, location.pathname, navigate]); }, [routeState.lastRoute, location.pathname, navigate]);
const setActiveTab = useCallback((tab: string) => { const setActiveTab = useCallback((tab: string) => {
setRouteStateState((prev) => { setRouteStateState(prev => {
const newState = { ...prev, activeTab: tab }; const newState = { ...prev, activeTab: tab };
persistRouteState(newState); persistRouteState(newState);
return newState; return newState;
@ -353,7 +331,7 @@ export function useAppStateLogic(): AppStateContextType {
}, []); }, []);
const setFilters = useCallback((filters: Record<string, any>) => { const setFilters = useCallback((filters: Record<string, any>) => {
setRouteStateState((prev) => { setRouteStateState(prev => {
const newState = { ...prev, appliedFilters: filters }; const newState = { ...prev, appliedFilters: filters };
persistRouteState(newState); persistRouteState(newState);
return newState; return newState;
@ -361,7 +339,7 @@ export function useAppStateLogic(): AppStateContextType {
}, []); }, []);
const setSidebarCollapsed = useCallback((collapsed: boolean) => { const setSidebarCollapsed = useCallback((collapsed: boolean) => {
setRouteStateState((prev) => { setRouteStateState(prev => {
const newState = { ...prev, sidebarCollapsed: collapsed }; const newState = { ...prev, sidebarCollapsed: collapsed };
persistRouteState(newState); persistRouteState(newState);
return newState; return newState;
@ -384,13 +362,10 @@ export function useAppStateLogic(): AppStateContextType {
localStorage.removeItem(STORAGE_KEYS.APPLIED_FILTERS); localStorage.removeItem(STORAGE_KEYS.APPLIED_FILTERS);
localStorage.removeItem(STORAGE_KEYS.SIDEBAR_COLLAPSED); localStorage.removeItem(STORAGE_KEYS.SIDEBAR_COLLAPSED);
console.log("Cleared all route state"); console.log('Cleared all route state');
}, []); }, []);
const emptySetSnack: SetSnackType = ( const emptySetSnack: SetSnackType = (message: string, severity?: SeverityType) => {
message: string,
severity?: SeverityType
) => {
return; return;
}; };
@ -445,7 +420,7 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) {
export function useAppState() { export function useAppState() {
const context = useContext(AppStateContext); const context = useContext(AppStateContext);
if (!context) { if (!context) {
throw new Error("useAppState must be used within an AppStateProvider"); throw new Error('useAppState must be used within an AppStateProvider');
} }
return context; return context;
} }

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, RefObject, useCallback } from "react"; import { useEffect, useRef, RefObject, useCallback } from 'react';
const debug = false; const debug = false;
@ -69,10 +69,10 @@ const useResizeObserverAndMutationObserver = (
}, 500); }, 500);
const resizeObserver = new ResizeObserver((e: any) => { const resizeObserver = new ResizeObserver((e: any) => {
debouncedCallback("resize"); debouncedCallback('resize');
}); });
const mutationObserver = new MutationObserver((e: any) => { const mutationObserver = new MutationObserver((e: any) => {
debouncedCallback("mutation"); debouncedCallback('mutation');
}); });
// Observe container size // Observe container size
@ -124,7 +124,7 @@ const useAutoScrollToBottom = (
const scrollTo = scrollToRef.current; const scrollTo = scrollToRef.current;
if (isPasteEvent && !scrollTo) { if (isPasteEvent && !scrollTo) {
console.error("Paste Event triggered without scrollTo"); console.error('Paste Event triggered without scrollTo');
} }
if (scrollTo) { if (scrollTo) {
@ -136,16 +136,14 @@ const useAutoScrollToBottom = (
// Check if TextField is fully or partially visible (for non-paste events) // Check if TextField is fully or partially visible (for non-paste events)
const isTextFieldVisible = const isTextFieldVisible =
scrollToRect.top < containerBottom && scrollToRect.top < containerBottom && scrollToRect.bottom > containerTop;
scrollToRect.bottom > containerTop;
// Scroll on paste or if TextField is visible and user isn't scrolling up // Scroll on paste or if TextField is visible and user isn't scrolling up
shouldScroll = shouldScroll = isPasteEvent || (isTextFieldVisible && !isUserScrollingUpRef.current);
isPasteEvent || (isTextFieldVisible && !isUserScrollingUpRef.current);
if (shouldScroll) { if (shouldScroll) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
debug && debug &&
console.debug("Scrolling to container bottom:", { console.debug('Scrolling to container bottom:', {
scrollHeight: container.scrollHeight, scrollHeight: container.scrollHeight,
scrollToHeight: scrollToRect.height, scrollToHeight: scrollToRect.height,
containerHeight: container.clientHeight, containerHeight: container.clientHeight,
@ -155,7 +153,7 @@ const useAutoScrollToBottom = (
}); });
container.scrollTo({ container.scrollTo({
top: container.scrollHeight, top: container.scrollHeight,
behavior: smooth ? "smooth" : "auto", behavior: smooth ? 'smooth' : 'auto',
}); });
}); });
} }
@ -170,12 +168,12 @@ const useAutoScrollToBottom = (
if (shouldScroll) { if (shouldScroll) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
debug && debug &&
console.debug("Scrolling to container bottom (fallback):", { console.debug('Scrolling to container bottom (fallback):', {
scrollHeight, scrollHeight,
}); });
container.scrollTo({ container.scrollTo({
top: container.scrollHeight, top: container.scrollHeight,
behavior: smooth ? "smooth" : "auto", behavior: smooth ? 'smooth' : 'auto',
}); });
}); });
} }
@ -195,74 +193,60 @@ const useAutoScrollToBottom = (
* they may be zooming in a region; pause scrolling */ * they may be zooming in a region; pause scrolling */
isUserScrollingUpRef.current = isUserScrollingUpRef.current =
currentScrollTop <= lastScrollTop.current || pause ? true : false; currentScrollTop <= lastScrollTop.current || pause ? true : false;
debug && debug && console.debug(`Scrolling up or paused: ${isUserScrollingUpRef.current} ${pause}`);
console.debug(
`Scrolling up or paused: ${isUserScrollingUpRef.current} ${pause}`
);
lastScrollTop.current = currentScrollTop; lastScrollTop.current = currentScrollTop;
if (scrollTimeout.current) clearTimeout(scrollTimeout.current); if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
scrollTimeout.current = setTimeout( scrollTimeout.current = setTimeout(
() => { () => {
isUserScrollingUpRef.current = false; isUserScrollingUpRef.current = false;
debug && debug && console.debug(`Scrolling up: ${isUserScrollingUpRef.current}`);
console.debug(`Scrolling up: ${isUserScrollingUpRef.current}`);
}, },
pause ? pause : 500 pause ? pause : 500
); );
}; };
const pauseScroll = (ev: Event) => { const pauseScroll = (ev: Event) => {
debug && console.log("Pausing for mouse movement"); debug && console.log('Pausing for mouse movement');
handleScroll(ev, 500); handleScroll(ev, 500);
}; };
const pauseClick = (ev: Event) => { const pauseClick = (ev: Event) => {
debug && console.log("Pausing for mouse click"); debug && console.log('Pausing for mouse click');
handleScroll(ev, 1000); handleScroll(ev, 1000);
}; };
const handlePaste = () => { const handlePaste = () => {
console.log("handlePaste"); console.log('handlePaste');
// Delay scroll check to ensure DOM updates // Delay scroll check to ensure DOM updates
setTimeout(() => { setTimeout(() => {
console.log("scrolling for handlePaste"); console.log('scrolling for handlePaste');
requestAnimationFrame(() => checkAndScrollToBottom(true)); requestAnimationFrame(() => checkAndScrollToBottom(true));
}, 100); }, 100);
}; };
window.addEventListener("mousemove", pauseScroll); window.addEventListener('mousemove', pauseScroll);
window.addEventListener("mousedown", pauseClick); window.addEventListener('mousedown', pauseClick);
container.addEventListener("scroll", handleScroll); container.addEventListener('scroll', handleScroll);
if (scrollTo) { if (scrollTo) {
scrollTo.addEventListener("paste", handlePaste); scrollTo.addEventListener('paste', handlePaste);
} }
checkAndScrollToBottom(); checkAndScrollToBottom();
return () => { return () => {
window.removeEventListener("mousedown", pauseClick); window.removeEventListener('mousedown', pauseClick);
window.removeEventListener("mousemove", pauseScroll); window.removeEventListener('mousemove', pauseScroll);
container.removeEventListener("scroll", handleScroll); container.removeEventListener('scroll', handleScroll);
if (scrollTo) { if (scrollTo) {
scrollTo.removeEventListener("paste", handlePaste); scrollTo.removeEventListener('paste', handlePaste);
} }
if (scrollTimeout.current) clearTimeout(scrollTimeout.current); if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
}; };
}, [ }, [smooth, scrollToRef, fallbackThreshold, contentUpdateTrigger, checkAndScrollToBottom]);
smooth,
scrollToRef,
fallbackThreshold,
contentUpdateTrigger,
checkAndScrollToBottom,
]);
// Observe container and TextField size, plus DOM changes // Observe container and TextField size, plus DOM changes
useResizeObserverAndMutationObserver( useResizeObserverAndMutationObserver(containerRef, scrollToRef, checkAndScrollToBottom);
containerRef,
scrollToRef,
checkAndScrollToBottom
);
return containerRef; return containerRef;
}; };

View File

@ -1,16 +1,14 @@
import React from "react"; import React from 'react';
import ReactDOM from "react-dom/client"; import ReactDOM from 'react-dom/client';
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from '@mui/material/styles';
import { backstoryTheme } from "./BackstoryTheme"; import { backstoryTheme } from './BackstoryTheme';
import { BrowserRouter as Router } from "react-router-dom"; import { BrowserRouter as Router } from 'react-router-dom';
import { BackstoryApp } from "./BackstoryApp"; import { BackstoryApp } from './BackstoryApp';
// import { BackstoryTestApp } from 'TestApp'; // import { BackstoryTestApp } from 'TestApp';
import "./index.css"; import './index.css';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
document.getElementById("root") as HTMLElement
);
root.render( root.render(
<React.StrictMode> <React.StrictMode>

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from 'react-router-dom';
import { import {
Box, Box,
Container, Container,
@ -9,11 +9,11 @@ import {
Button, Button,
alpha, alpha,
GlobalStyles, GlobalStyles,
} from "@mui/material"; } from '@mui/material';
import { useTheme } from "@mui/material/styles"; import { useTheme } from '@mui/material/styles';
import ConstructionIcon from "@mui/icons-material/Construction"; import ConstructionIcon from '@mui/icons-material/Construction';
import RocketLaunchIcon from "@mui/icons-material/RocketLaunch"; import RocketLaunchIcon from '@mui/icons-material/RocketLaunch';
import { Beta } from "../components/ui/Beta"; import { Beta } from '../components/ui/Beta';
interface BetaPageProps { interface BetaPageProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -26,10 +26,10 @@ interface BetaPageProps {
const BetaPage: React.FC<BetaPageProps> = ({ const BetaPage: React.FC<BetaPageProps> = ({
children, children,
title = "Coming Soon", title = 'Coming Soon',
subtitle = "This page is currently in development", subtitle = 'This page is currently in development',
returnPath = "/", returnPath = '/',
returnLabel = "Return to Backstory", returnLabel = 'Return to Backstory',
onReturn, onReturn,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
@ -39,10 +39,9 @@ const BetaPage: React.FC<BetaPageProps> = ({
if (!children) { if (!children) {
children = ( children = (
<Box sx={{ width: "100%", display: "flex", justifyContent: "center" }}> <Box sx={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<Typography> <Typography>
The page you requested (<b>{location.pathname.replace(/^\//, "")}</b>) The page you requested (<b>{location.pathname.replace(/^\//, '')}</b>) is not yet ready.
is not yet ready.
</Typography> </Typography>
</Box> </Box>
); );
@ -94,10 +93,10 @@ const BetaPage: React.FC<BetaPageProps> = ({
return ( return (
<Box <Box
sx={{ sx={{
minHeight: "100%", minHeight: '100%',
width: "100%", width: '100%',
position: "relative", position: 'relative',
overflow: "hidden", overflow: 'hidden',
bgcolor: theme.palette.background.default, bgcolor: theme.palette.background.default,
pt: 8, pt: 8,
pb: 6, pb: 6,
@ -106,25 +105,25 @@ const BetaPage: React.FC<BetaPageProps> = ({
{/* Animated background elements */} {/* Animated background elements */}
<Box <Box
sx={{ sx={{
position: "absolute", position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
zIndex: 0, zIndex: 0,
overflow: "hidden", overflow: 'hidden',
}} }}
> >
{sparkles.map((sparkle) => ( {sparkles.map(sparkle => (
<Box <Box
key={sparkle.id} key={sparkle.id}
sx={{ sx={{
position: "absolute", position: 'absolute',
left: `${sparkle.x}%`, left: `${sparkle.x}%`,
top: `${sparkle.y}%`, top: `${sparkle.y}%`,
width: sparkle.size, width: sparkle.size,
height: sparkle.size, height: sparkle.size,
borderRadius: "50%", borderRadius: '50%',
bgcolor: alpha(theme.palette.primary.main, sparkle.opacity), bgcolor: alpha(theme.palette.primary.main, sparkle.opacity),
boxShadow: `0 0 ${sparkle.size * 2}px ${alpha( boxShadow: `0 0 ${sparkle.size * 2}px ${alpha(
theme.palette.primary.main, theme.palette.primary.main,
@ -136,34 +135,24 @@ const BetaPage: React.FC<BetaPageProps> = ({
))} ))}
</Box> </Box>
<Container maxWidth="lg" sx={{ position: "relative", zIndex: 2 }}> <Container maxWidth="lg" sx={{ position: 'relative', zIndex: 2 }}>
<Grid container spacing={4} direction="column" alignItems="center"> <Grid container spacing={4} direction="column" alignItems="center">
<Grid size={{ xs: 12 }} sx={{ textAlign: "center", mb: 2 }}> <Grid size={{ xs: 12 }} sx={{ textAlign: 'center', mb: 2 }}>
<Typography <Typography
variant="h2" variant="h2"
component="h1" component="h1"
gutterBottom gutterBottom
sx={{ sx={{
fontWeight: "bold", fontWeight: 'bold',
color: theme.palette.primary.main, color: theme.palette.primary.main,
textShadow: `0 0 10px ${alpha( textShadow: `0 0 10px ${alpha(theme.palette.primary.main, 0.3)}`,
theme.palette.primary.main, animation: showSparkle ? 'titleGlow 3s ease-in-out infinite alternate' : 'none',
0.3
)}`,
animation: showSparkle
? "titleGlow 3s ease-in-out infinite alternate"
: "none",
}} }}
> >
{title} {title}
</Typography> </Typography>
<Typography <Typography variant="h5" component="h2" color="textSecondary" sx={{ mb: 6 }}>
variant="h5"
component="h2"
color="textSecondary"
sx={{ mb: 6 }}
>
{subtitle} {subtitle}
</Typography> </Typography>
</Grid> </Grid>
@ -175,28 +164,25 @@ const BetaPage: React.FC<BetaPageProps> = ({
p: { xs: 3, md: 5 }, p: { xs: 3, md: 5 },
borderRadius: 2, borderRadius: 2,
bgcolor: alpha(theme.palette.background.paper, 0.8), bgcolor: alpha(theme.palette.background.paper, 0.8),
backdropFilter: "blur(8px)", backdropFilter: 'blur(8px)',
boxShadow: `0 8px 32px ${alpha( boxShadow: `0 8px 32px ${alpha(theme.palette.primary.main, 0.15)}`,
theme.palette.primary.main,
0.15
)}`,
border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}`, border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}`,
position: "relative", position: 'relative',
overflow: "hidden", overflow: 'hidden',
}} }}
> >
{/* Construction icon */} {/* Construction icon */}
<Box <Box
sx={{ sx={{
position: "absolute", position: 'absolute',
top: -15, top: -15,
right: -15, right: -15,
bgcolor: theme.palette.warning.main, bgcolor: theme.palette.warning.main,
color: theme.palette.warning.contrastText, color: theme.palette.warning.contrastText,
borderRadius: "50%", borderRadius: '50%',
p: 2, p: 2,
boxShadow: 3, boxShadow: 3,
transform: "rotate(15deg)", transform: 'rotate(15deg)',
}} }}
> >
<ConstructionIcon fontSize="large" /> <ConstructionIcon fontSize="large" />
@ -205,14 +191,14 @@ const BetaPage: React.FC<BetaPageProps> = ({
{/* Content */} {/* Content */}
<Box sx={{ mt: 3, mb: 3 }}> <Box sx={{ mt: 3, mb: 3 }}>
{children || ( {children || (
<Box sx={{ textAlign: "center", py: 4 }}> <Box sx={{ textAlign: 'center', py: 4 }}>
<RocketLaunchIcon <RocketLaunchIcon
fontSize="large" fontSize="large"
color="primary" color="primary"
sx={{ sx={{
fontSize: 80, fontSize: 80,
mb: 2, mb: 2,
animation: "rocketWobble 3s ease-in-out infinite", animation: 'rocketWobble 3s ease-in-out infinite',
}} }}
/> />
<Typography> <Typography>
@ -227,21 +213,21 @@ const BetaPage: React.FC<BetaPageProps> = ({
adaptive={false} adaptive={false}
sx={{ sx={{
opacity: 0.5, opacity: 0.5,
left: "-72px", left: '-72px',
"& > div": { '& > div': {
paddingRight: "30px", paddingRight: '30px',
background: "gold", background: 'gold',
color: "#808080", color: '#808080',
}, },
}} }}
onClick={() => { onClick={() => {
navigate("/docs/beta"); navigate('/docs/beta');
}} }}
/> />
</Box> </Box>
{/* Return button */} {/* Return button */}
<Box sx={{ mt: 4, textAlign: "center" }}> <Box sx={{ mt: 4, textAlign: 'center' }}>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
@ -251,15 +237,9 @@ const BetaPage: React.FC<BetaPageProps> = ({
px: 4, px: 4,
py: 1, py: 1,
borderRadius: 4, borderRadius: 4,
boxShadow: `0 4px 14px ${alpha( boxShadow: `0 4px 14px ${alpha(theme.palette.primary.main, 0.4)}`,
theme.palette.primary.main, '&:hover': {
0.4 boxShadow: `0 6px 20px ${alpha(theme.palette.primary.main, 0.6)}`,
)}`,
"&:hover": {
boxShadow: `0 6px 20px ${alpha(
theme.palette.primary.main,
0.6
)}`,
}, },
}} }}
> >
@ -274,47 +254,47 @@ const BetaPage: React.FC<BetaPageProps> = ({
{/* Global styles added with MUI's GlobalStyles component */} {/* Global styles added with MUI's GlobalStyles component */}
<GlobalStyles <GlobalStyles
styles={{ styles={{
"@keyframes float": { '@keyframes float': {
"0%": { '0%': {
transform: "translateY(0) scale(1)", transform: 'translateY(0) scale(1)',
}, },
"100%": { '100%': {
transform: "translateY(-20px) scale(1.1)", transform: 'translateY(-20px) scale(1.1)',
}, },
}, },
"@keyframes sparkleFloat": { '@keyframes sparkleFloat': {
"0%": { '0%': {
transform: "translateY(0) scale(1)", transform: 'translateY(0) scale(1)',
opacity: 0.7, opacity: 0.7,
}, },
"50%": { '50%': {
opacity: 1, opacity: 1,
}, },
"100%": { '100%': {
transform: "translateY(-15px) scale(1.2)", transform: 'translateY(-15px) scale(1.2)',
opacity: 0.7, opacity: 0.7,
}, },
}, },
"@keyframes titleGlow": { '@keyframes titleGlow': {
"0%": { '0%': {
textShadow: `0 0 10px ${alpha(theme.palette.primary.main, 0.3)}`, textShadow: `0 0 10px ${alpha(theme.palette.primary.main, 0.3)}`,
}, },
"100%": { '100%': {
textShadow: `0 0 25px ${alpha( textShadow: `0 0 25px ${alpha(theme.palette.primary.main, 0.7)}, 0 0 40px ${alpha(
theme.palette.primary.main, theme.palette.primary.main,
0.7 0.4
)}, 0 0 40px ${alpha(theme.palette.primary.main, 0.4)}`, )}`,
}, },
}, },
"@keyframes rocketWobble": { '@keyframes rocketWobble': {
"0%": { '0%': {
transform: "translateY(0) rotate(0deg)", transform: 'translateY(0) rotate(0deg)',
}, },
"50%": { '50%': {
transform: "translateY(-10px) rotate(3deg)", transform: 'translateY(-10px) rotate(3deg)',
}, },
"100%": { '100%': {
transform: "translateY(0) rotate(-2deg)", transform: 'translateY(0) rotate(-2deg)',
}, },
}, },
}} }}

View File

@ -1,15 +1,7 @@
import React, { forwardRef, useState, useEffect, useRef } from "react"; import React, { forwardRef, useState, useEffect, useRef } from 'react';
import { import { Box, Paper, Button, Divider, useTheme, useMediaQuery, Tooltip } from '@mui/material';
Box, import { Send as SendIcon } from '@mui/icons-material';
Paper, import { useAuth } from 'hooks/AuthContext';
Button,
Divider,
useTheme,
useMediaQuery,
Tooltip,
} from "@mui/material";
import { Send as SendIcon } from "@mui/icons-material";
import { useAuth } from "hooks/AuthContext";
import { import {
ChatMessage, ChatMessage,
ChatSession, ChatSession,
@ -17,30 +9,27 @@ import {
ChatMessageError, ChatMessageError,
ChatMessageStreaming, ChatMessageStreaming,
ChatMessageStatus, ChatMessageStatus,
} from "types/types"; } from 'types/types';
import { ConversationHandle } from "components/Conversation"; import { ConversationHandle } from 'components/Conversation';
import { BackstoryPageProps } from "components/BackstoryTab"; import { BackstoryPageProps } from 'components/BackstoryTab';
import { Message } from "components/Message"; import { Message } from 'components/Message';
import { DeleteConfirmation } from "components/DeleteConfirmation"; import { DeleteConfirmation } from 'components/DeleteConfirmation';
import { CandidateInfo } from "components/ui/CandidateInfo"; import { CandidateInfo } from 'components/ui/CandidateInfo';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { useAppState, useSelectedCandidate } from "hooks/GlobalContext"; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import PropagateLoader from "react-spinners/PropagateLoader"; import PropagateLoader from 'react-spinners/PropagateLoader';
import { import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
BackstoryTextField, import { BackstoryQuery } from 'components/BackstoryQuery';
BackstoryTextFieldRef, import { CandidatePicker } from 'components/ui/CandidatePicker';
} from "components/BackstoryTextField"; import { Scrollable } from 'components/Scrollable';
import { BackstoryQuery } from "components/BackstoryQuery";
import { CandidatePicker } from "components/ui/CandidatePicker";
import { Scrollable } from "components/Scrollable";
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
status: "done", status: 'done',
type: "text", type: 'text',
sessionId: "", sessionId: '',
timestamp: new Date(), timestamp: new Date(),
content: "", content: '',
role: "user", role: 'user',
metadata: null as any, metadata: null as any,
}; };
@ -53,8 +42,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
const [processingMessage, setProcessingMessage] = useState< const [processingMessage, setProcessingMessage] = useState<
ChatMessageStatus | ChatMessageError | null ChatMessageStatus | ChatMessageError | null
>(null); >(null);
const [streamingMessage, setStreamingMessage] = const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
useState<ChatMessage | null>(null);
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null); const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
const { setSnack } = useAppState(); const { setSnack } = useAppState();
@ -75,12 +63,9 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
setMessages(chatMessages); setMessages(chatMessages);
setProcessingMessage(null); setProcessingMessage(null);
setStreamingMessage(null); setStreamingMessage(null);
console.log( console.log(`getChatMessages returned ${chatMessages.length} messages.`, chatMessages);
`getChatMessages returned ${chatMessages.length} messages.`,
chatMessages
);
} catch (error) { } catch (error) {
console.error("Failed to load messages:", error); console.error('Failed to load messages:', error);
} }
}; };
@ -92,43 +77,37 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
await apiClient.resetChatSession(session.id); await apiClient.resetChatSession(session.id);
// If we're deleting the currently selected session, clear it // If we're deleting the currently selected session, clear it
setMessages([]); setMessages([]);
setSnack("Session reset succeeded", "success"); setSnack('Session reset succeeded', 'success');
} catch (error) { } catch (error) {
console.error("Failed to delete session:", error); console.error('Failed to delete session:', error);
setSnack("Failed to delete session", "error"); setSnack('Failed to delete session', 'error');
} }
}; };
// Send message // Send message
const sendMessage = async (message: string) => { const sendMessage = async (message: string) => {
if ( if (!message.trim() || !chatSession?.id || streaming || !selectedCandidate) return;
!message.trim() ||
!chatSession?.id ||
streaming ||
!selectedCandidate
)
return;
const messageContent = message; const messageContent = message;
setStreaming(true); setStreaming(true);
const chatMessage: ChatMessageUser = { const chatMessage: ChatMessageUser = {
sessionId: chatSession.id, sessionId: chatSession.id,
role: "user", role: 'user',
content: messageContent, content: messageContent,
status: "done", status: 'done',
type: "text", type: 'text',
timestamp: new Date(), timestamp: new Date(),
}; };
setProcessingMessage({ setProcessingMessage({
...defaultMessage, ...defaultMessage,
status: "status", status: 'status',
activity: "info", activity: 'info',
content: `Establishing connection with ${selectedCandidate.firstName}'s chat session.`, content: `Establishing connection with ${selectedCandidate.firstName}'s chat session.`,
}); });
setMessages((prev) => { setMessages(prev => {
const filtered = prev.filter((m: any) => m.id !== chatMessage.id); const filtered = prev.filter((m: any) => m.id !== chatMessage.id);
return [...filtered, chatMessage] as any; return [...filtered, chatMessage] as any;
}); });
@ -136,7 +115,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
try { try {
apiClient.sendMessageStream(chatMessage, { apiClient.sendMessageStream(chatMessage, {
onMessage: (msg: ChatMessage) => { onMessage: (msg: ChatMessage) => {
setMessages((prev) => { setMessages(prev => {
const filtered = prev.filter((m: any) => m.id !== msg.id); const filtered = prev.filter((m: any) => m.id !== msg.id);
return [...filtered, msg] as any; return [...filtered, msg] as any;
}); });
@ -144,20 +123,16 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
setProcessingMessage(null); setProcessingMessage(null);
}, },
onError: (error: string | ChatMessageError) => { onError: (error: string | ChatMessageError) => {
console.log("onError:", error); console.log('onError:', error);
let message: string; let message: string;
// Type-guard to determine if this is a ChatMessageBase or a string // Type-guard to determine if this is a ChatMessageBase or a string
if ( if (typeof error === 'object' && error !== null && 'content' in error) {
typeof error === "object" &&
error !== null &&
"content" in error
) {
setProcessingMessage(error); setProcessingMessage(error);
message = error.content as string; message = error.content as string;
} else { } else {
setProcessingMessage({ setProcessingMessage({
...defaultMessage, ...defaultMessage,
status: "error", status: 'error',
content: error, content: error,
}); });
} }
@ -167,7 +142,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
// console.log("onStreaming:", chunk); // console.log("onStreaming:", chunk);
setStreamingMessage({ setStreamingMessage({
...chunk, ...chunk,
role: "assistant", role: 'assistant',
metadata: null as any, metadata: null as any,
}); });
}, },
@ -175,21 +150,21 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
setProcessingMessage(status); setProcessingMessage(status);
}, },
onComplete: () => { onComplete: () => {
console.log("onComplete"); console.log('onComplete');
setStreamingMessage(null); setStreamingMessage(null);
setProcessingMessage(null); setProcessingMessage(null);
setStreaming(false); setStreaming(false);
}, },
}); });
} catch (error) { } catch (error) {
console.error("Failed to send message:", error); console.error('Failed to send message:', error);
setStreaming(false); setStreaming(false);
} }
}; };
// Auto-scroll to bottom when new messages arrive // Auto-scroll to bottom when new messages arrive
useEffect(() => { useEffect(() => {
(messagesEndRef.current as any)?.scrollIntoView({ behavior: "smooth" }); (messagesEndRef.current as any)?.scrollIntoView({ behavior: 'smooth' });
}, [messages]); }, [messages]);
// Load sessions when username changes // Load sessions when username changes
@ -201,14 +176,14 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
.getOrCreateChatSession( .getOrCreateChatSession(
selectedCandidate, selectedCandidate,
`Backstory chat with ${selectedCandidate.fullName}`, `Backstory chat with ${selectedCandidate.fullName}`,
"candidate_chat" 'candidate_chat'
) )
.then((session) => { .then(session => {
setChatSession(session); setChatSession(session);
setLoading(false); setLoading(false);
}); });
} catch (error) { } catch (error) {
setSnack("Unable to load chat session", "error"); setSnack('Unable to load chat session', 'error');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -226,10 +201,10 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
} }
const welcomeMessage: ChatMessage = { const welcomeMessage: ChatMessage = {
sessionId: chatSession?.id || "", sessionId: chatSession?.id || '',
role: "information", role: 'information',
type: "text", type: 'text',
status: "done", status: 'done',
timestamp: new Date(), timestamp: new Date(),
content: `Welcome to the Backstory Chat about ${selectedCandidate.fullName}. Ask any questions you have about ${selectedCandidate.firstName}.`, content: `Welcome to the Backstory Chat about ${selectedCandidate.fullName}. Ask any questions you have about ${selectedCandidate.firstName}.`,
metadata: null as any, metadata: null as any,
@ -239,16 +214,16 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
<Box <Box
ref={ref} ref={ref}
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
height: "100%" /* Restrict to main-container's height */, height: '100%' /* Restrict to main-container's height */,
width: "100%", width: '100%',
minHeight: 0 /* Prevent flex overflow */, minHeight: 0 /* Prevent flex overflow */,
maxHeight: "min-content", maxHeight: 'min-content',
"& > *:not(.Scrollable)": { '& > *:not(.Scrollable)': {
flexShrink: 0 /* Prevent shrinking */, flexShrink: 0 /* Prevent shrinking */,
}, },
position: "relative", position: 'relative',
}} }}
> >
<Paper elevation={2} sx={{ m: 1, p: 1 }}> <Paper elevation={2} sx={{ m: 1, p: 1 }}>
@ -260,13 +235,13 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
variant="small" variant="small"
sx={{ sx={{
flexShrink: 1, flexShrink: 1,
width: "100%", width: '100%',
maxHeight: 0, maxHeight: 0,
minHeight: "min-content", minHeight: 'min-content',
}} // Prevent header from shrinking }} // Prevent header from shrinking
/> />
<Button <Button
sx={{ maxWidth: "max-content" }} sx={{ maxWidth: 'max-content' }}
onClick={() => { onClick={() => {
setSelectedCandidate(null); setSelectedCandidate(null);
}} }}
@ -280,22 +255,20 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
{chatSession && ( {chatSession && (
<Scrollable <Scrollable
sx={{ sx={{
position: "relative", position: 'relative',
maxHeight: "100%", maxHeight: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
flex: 1 /* Take remaining space in some-container */, flex: 1 /* Take remaining space in some-container */,
overflowY: "auto" /* Scroll if content overflows */, overflowY: 'auto' /* Scroll if content overflows */,
pt: 2, pt: 2,
pl: 1, pl: 1,
pr: 1, pr: 1,
pb: 2, pb: 2,
}} }}
> >
{messages.length === 0 && ( {messages.length === 0 && <Message {...{ chatSession, message: welcomeMessage }} />}
<Message {...{ chatSession, message: welcomeMessage }} />
)}
{messages.map((message: ChatMessage) => ( {messages.map((message: ChatMessage) => (
<Message key={message.id} {...{ chatSession, message }} /> <Message key={message.id} {...{ chatSession, message }} />
))} ))}
@ -308,10 +281,10 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
{streaming && ( {streaming && (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
m: 1, m: 1,
}} }}
> >
@ -327,17 +300,15 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
</Scrollable> </Scrollable>
)} )}
{selectedCandidate.questions?.length !== 0 && {selectedCandidate.questions?.length !== 0 &&
selectedCandidate.questions?.map((q) => ( selectedCandidate.questions?.map(q => <BackstoryQuery question={q} />)}
<BackstoryQuery question={q} />
))}
{/* Fixed Message Input */} {/* Fixed Message Input */}
<Box sx={{ display: "flex", flexShrink: 1, gap: 1 }}> <Box sx={{ display: 'flex', flexShrink: 1, gap: 1 }}>
<DeleteConfirmation <DeleteConfirmation
onDelete={() => { onDelete={() => {
chatSession && onDelete(chatSession); chatSession && onDelete(chatSession);
}} }}
disabled={!chatSession} disabled={!chatSession}
sx={{ minWidth: "auto", px: 2, maxHeight: "min-content" }} sx={{ minWidth: 'auto', px: 2, maxHeight: 'min-content' }}
action="reset" action="reset"
label="chat session" label="chat session"
title="Reset Chat Session" title="Reset Chat Session"
@ -352,18 +323,16 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
<Tooltip title="Send"> <Tooltip title="Send">
<span <span
style={{ style={{
minWidth: "auto", minWidth: 'auto',
maxHeight: "min-content", maxHeight: 'min-content',
alignSelf: "center", alignSelf: 'center',
}} }}
> >
<Button <Button
variant="contained" variant="contained"
onClick={() => { onClick={() => {
sendMessage( sendMessage(
(backstoryTextRef.current && (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ''
backstoryTextRef.current.getAndResetValue()) ||
""
); );
}} }}
disabled={streaming || loading} disabled={streaming || loading}

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation, useParams } from "react-router-dom"; import { useNavigate, useLocation, useParams } from 'react-router-dom';
import { import {
Box, Box,
Drawer, Drawer,
@ -19,27 +19,27 @@ import {
CardActionArea, CardActionArea,
useTheme, useTheme,
useMediaQuery, useMediaQuery,
} from "@mui/material"; } from '@mui/material';
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from '@mui/icons-material/Menu';
import PersonIcon from "@mui/icons-material/Person"; import PersonIcon from '@mui/icons-material/Person';
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from '@mui/icons-material/Close';
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import DescriptionIcon from "@mui/icons-material/Description"; import DescriptionIcon from '@mui/icons-material/Description';
import CodeIcon from "@mui/icons-material/Code"; import CodeIcon from '@mui/icons-material/Code';
import LayersIcon from "@mui/icons-material/Layers"; import LayersIcon from '@mui/icons-material/Layers';
import DashboardIcon from "@mui/icons-material/Dashboard"; import DashboardIcon from '@mui/icons-material/Dashboard';
import PaletteIcon from "@mui/icons-material/Palette"; import PaletteIcon from '@mui/icons-material/Palette';
import AnalyticsIcon from "@mui/icons-material/Analytics"; import AnalyticsIcon from '@mui/icons-material/Analytics';
import ViewQuiltIcon from "@mui/icons-material/ViewQuilt"; import ViewQuiltIcon from '@mui/icons-material/ViewQuilt';
import { Document } from "../components/Document"; import { Document } from '../components/Document';
import { BackstoryPageProps } from "../components/BackstoryTab"; import { BackstoryPageProps } from '../components/BackstoryTab';
import { BackstoryUIOverviewPage } from "documents/BackstoryUIOverviewPage"; import { BackstoryUIOverviewPage } from 'documents/BackstoryUIOverviewPage';
import { BackstoryAppAnalysisPage } from "documents/BackstoryAppAnalysisPage"; import { BackstoryAppAnalysisPage } from 'documents/BackstoryAppAnalysisPage';
import { BackstoryThemeVisualizerPage } from "documents/BackstoryThemeVisualizerPage"; import { BackstoryThemeVisualizerPage } from 'documents/BackstoryThemeVisualizerPage';
import { UserManagement } from "documents/UserManagement"; import { UserManagement } from 'documents/UserManagement';
import { MockupPage } from "documents/MockupPage"; import { MockupPage } from 'documents/MockupPage';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
// Sidebar navigation component using MUI components // Sidebar navigation component using MUI components
const Sidebar: React.FC<{ const Sidebar: React.FC<{
@ -60,26 +60,22 @@ const Sidebar: React.FC<{
}; };
return ( return (
<Box sx={{ height: "100%", display: "flex", flexDirection: "column" }}> <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box <Box
sx={{ sx={{
p: 2, p: 2,
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "space-between", justifyContent: 'space-between',
borderBottom: 1, borderBottom: 1,
borderColor: "divider", borderColor: 'divider',
}} }}
> >
<Typography variant="h6" component="h2" fontWeight="bold"> <Typography variant="h6" component="h2" fontWeight="bold">
Documentation Documentation
</Typography> </Typography>
{isMobile && onClose && ( {isMobile && onClose && (
<IconButton <IconButton onClick={onClose} size="small" aria-label="Close navigation">
onClick={onClose}
size="small"
aria-label="Close navigation"
>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
)} )}
@ -88,7 +84,7 @@ const Sidebar: React.FC<{
<Box <Box
sx={{ sx={{
flexGrow: 1, flexGrow: 1,
overflow: "auto", overflow: 'auto',
p: 1, p: 1,
}} }}
> >
@ -96,9 +92,7 @@ const Sidebar: React.FC<{
{documents.map((doc, index) => ( {documents.map((doc, index) => (
<ListItem key={index} disablePadding> <ListItem key={index} disablePadding>
<ListItemButton <ListItemButton
onClick={() => onClick={() => (doc.route ? handleItemClick(doc.route) : navigate('/'))}
doc.route ? handleItemClick(doc.route) : navigate("/")
}
selected={currentPage === doc.route} selected={currentPage === doc.route}
sx={{ sx={{
borderRadius: 1, borderRadius: 1,
@ -107,10 +101,7 @@ const Sidebar: React.FC<{
> >
<ListItemIcon <ListItemIcon
sx={{ sx={{
color: color: currentPage === doc.route ? 'primary.main' : 'text.secondary',
currentPage === doc.route
? "primary.main"
: "text.secondary",
minWidth: 40, minWidth: 40,
}} }}
> >
@ -120,8 +111,7 @@ const Sidebar: React.FC<{
primary={doc.title} primary={doc.title}
slotProps={{ slotProps={{
primary: { primary: {
fontWeight: fontWeight: currentPage === doc.route ? 'medium' : 'regular',
currentPage === doc.route ? "medium" : "regular",
}, },
}} }}
/> />
@ -135,9 +125,7 @@ const Sidebar: React.FC<{
}; };
const getDocumentIcon = (title: string): React.ReactNode => { const getDocumentIcon = (title: string): React.ReactNode => {
const item = documents.find( const item = documents.find(d => d.title.toLocaleLowerCase() === title.toLocaleLowerCase());
(d) => d.title.toLocaleLowerCase() === title.toLocaleLowerCase()
);
if (!item) { if (!item) {
throw Error(`${title} does not exist in documents`); throw Error(`${title} does not exist in documents`);
} }
@ -153,87 +141,86 @@ type DocType = {
const documents: DocType[] = [ const documents: DocType[] = [
{ {
title: "Backstory", title: 'Backstory',
route: null, route: null,
description: "Backstory", description: 'Backstory',
icon: <ArrowBackIcon />, icon: <ArrowBackIcon />,
}, },
{ {
title: "About", title: 'About',
route: "about", route: 'about',
description: "General information about the application and its purpose", description: 'General information about the application and its purpose',
icon: <DescriptionIcon />, icon: <DescriptionIcon />,
}, },
{ {
title: "BETA", title: 'BETA',
route: "beta", route: 'beta',
description: "Details about the current beta version and upcoming features", description: 'Details about the current beta version and upcoming features',
icon: <CodeIcon />, icon: <CodeIcon />,
}, },
{ {
title: "Resume Generation Architecture", title: 'Resume Generation Architecture',
route: "resume-generation", route: 'resume-generation',
description: description: 'Technical overview of how resumes are processed and generated',
"Technical overview of how resumes are processed and generated",
icon: <LayersIcon />, icon: <LayersIcon />,
}, },
{ {
title: "Application Architecture", title: 'Application Architecture',
route: "about-app", route: 'about-app',
description: "System design and technical stack information", description: 'System design and technical stack information',
icon: <LayersIcon />, icon: <LayersIcon />,
}, },
{ {
title: "Authentication Architecture", title: 'Authentication Architecture',
route: "authentication.md", route: 'authentication.md',
description: "Complete authentication architecture", description: 'Complete authentication architecture',
icon: <LayersIcon />, icon: <LayersIcon />,
}, },
{ {
title: "UI Overview", title: 'UI Overview',
route: "ui-overview", route: 'ui-overview',
description: "Guide to the user interface components and interactions", description: 'Guide to the user interface components and interactions',
icon: <DashboardIcon />, icon: <DashboardIcon />,
}, },
{ {
title: "UI Mockup", title: 'UI Mockup',
route: "ui-mockup", route: 'ui-mockup',
description: "Visual previews of interfaces and layout concepts", description: 'Visual previews of interfaces and layout concepts',
icon: <DashboardIcon />, icon: <DashboardIcon />,
}, },
{ {
title: "Theme Visualizer", title: 'Theme Visualizer',
route: "theme-visualizer", route: 'theme-visualizer',
description: "Explore and customize application themes and visual styles", description: 'Explore and customize application themes and visual styles',
icon: <PaletteIcon />, icon: <PaletteIcon />,
}, },
{ {
title: "App Analysis", title: 'App Analysis',
route: "app-analysis", route: 'app-analysis',
description: "Statistics and performance metrics of the application", description: 'Statistics and performance metrics of the application',
icon: <AnalyticsIcon />, icon: <AnalyticsIcon />,
}, },
{ {
title: "Text Mockups", title: 'Text Mockups',
route: "backstory-ui-mockups", route: 'backstory-ui-mockups',
description: "Early text mockups of many of the interaction points.", description: 'Early text mockups of many of the interaction points.',
}, },
{ {
title: "User Management", title: 'User Management',
route: "user-management", route: 'user-management',
description: "User management.", description: 'User management.',
icon: <PersonIcon />, icon: <PersonIcon />,
}, },
{ {
title: "Type Safety", title: 'Type Safety',
route: "type-safety", route: 'type-safety',
description: "Overview of front/back-end type synchronization.", description: 'Overview of front/back-end type synchronization.',
icon: <CodeIcon />, icon: <CodeIcon />,
}, },
]; ];
const documentFromRoute = (route: string): DocType | null => { const documentFromRoute = (route: string): DocType | null => {
const index = documents.findIndex((v) => v.route === route); const index = documents.findIndex(v => v.route === route);
if (index === -1) { if (index === -1) {
return null; return null;
} }
@ -244,7 +231,7 @@ const documentFromRoute = (route: string): DocType | null => {
const documentTitleFromRoute = (route: string): string => { const documentTitleFromRoute = (route: string): string => {
const doc = documentFromRoute(route); const doc = documentFromRoute(route);
if (doc === null) { if (doc === null) {
return "Documentation"; return 'Documentation';
} }
return doc.title; return doc.title;
}; };
@ -253,20 +240,20 @@ const DocsPage = (props: BackstoryPageProps) => {
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { paramPage = "" } = useParams(); const { paramPage = '' } = useParams();
const [page, setPage] = useState<string>(paramPage); const [page, setPage] = useState<string>(paramPage);
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Track location changes // Track location changes
useEffect(() => { useEffect(() => {
const parts = location.pathname.split("/"); const parts = location.pathname.split('/');
if (parts.length > 2) { if (parts.length > 2) {
setPage(parts[2]); setPage(parts[2]);
} else { } else {
setPage(""); setPage('');
} }
}, [location]); }, [location]);
@ -279,21 +266,21 @@ const DocsPage = (props: BackstoryPageProps) => {
// Handle document navigation // Handle document navigation
const onDocumentExpand = (docName: string, open: boolean) => { const onDocumentExpand = (docName: string, open: boolean) => {
console.log("Document expanded:", { docName, open, location }); console.log('Document expanded:', { docName, open, location });
if (open) { if (open) {
const parts = location.pathname.split("/"); const parts = location.pathname.split('/');
if (docName === "backstory") { if (docName === 'backstory') {
navigate("/"); navigate('/');
return; return;
} }
if (parts.length > 2) { if (parts.length > 2) {
const basePath = parts.slice(0, -1).join("/"); const basePath = parts.slice(0, -1).join('/');
navigate(`${basePath}/${docName}`); navigate(`${basePath}/${docName}`);
} else { } else {
navigate(docName); navigate(docName);
} }
} else { } else {
const basePath = location.pathname.split("/").slice(0, -1).join("/"); const basePath = location.pathname.split('/').slice(0, -1).join('/');
navigate(`${basePath}`); navigate(`${basePath}`);
} }
}; };
@ -312,7 +299,7 @@ const DocsPage = (props: BackstoryPageProps) => {
page: string; page: string;
} }
const DocView = (props: DocViewProps) => { const DocView = (props: DocViewProps) => {
const { page = "about" } = props; const { page = 'about' } = props;
const title = documentTitleFromRoute(page); const title = documentTitleFromRoute(page);
const icon = getDocumentIcon(title); const icon = getDocumentIcon(title);
@ -321,13 +308,13 @@ const DocsPage = (props: BackstoryPageProps) => {
<CardContent> <CardContent>
<Box <Box
sx={{ sx={{
color: "inherit", color: 'inherit',
fontSize: "1.75rem", fontSize: '1.75rem',
fontWeight: "bold", fontWeight: 'bold',
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
gap: 1, gap: 1,
alignItems: "center", alignItems: 'center',
mr: 1.5, mr: 1.5,
}} }}
> >
@ -343,19 +330,19 @@ const DocsPage = (props: BackstoryPageProps) => {
// Render the appropriate content based on current page // Render the appropriate content based on current page
function renderContent() { function renderContent() {
switch (page) { switch (page) {
case "ui-overview": case 'ui-overview':
return <BackstoryUIOverviewPage />; return <BackstoryUIOverviewPage />;
case "theme-visualizer": case 'theme-visualizer':
return ( return (
<Paper sx={{ m: 0, p: 1 }}> <Paper sx={{ m: 0, p: 1 }}>
<BackstoryThemeVisualizerPage /> <BackstoryThemeVisualizerPage />
</Paper> </Paper>
); );
case "app-analysis": case 'app-analysis':
return <BackstoryAppAnalysisPage />; return <BackstoryAppAnalysisPage />;
case "ui-mockup": case 'ui-mockup':
return <MockupPage />; return <MockupPage />;
case "user-management": case 'user-management':
return <UserManagement />; return <UserManagement />;
default: default:
if (documentFromRoute(page)) { if (documentFromRoute(page)) {
@ -369,45 +356,39 @@ const DocsPage = (props: BackstoryPageProps) => {
Documentation Documentation
</Typography> </Typography>
<Typography variant="body1" color="text.secondary"> <Typography variant="body1" color="text.secondary">
Select a document from the sidebar to view detailed technical Select a document from the sidebar to view detailed technical information about the
information about the application. application.
</Typography> </Typography>
</Box> </Box>
<Grid container spacing={1}> <Grid container spacing={1}>
{documents.map((doc, index) => { {documents.map((doc, index) => {
if (doc.route === null) return <></>; if (doc.route === null) return <></>;
return ( return (
<Grid <Grid sx={{ minWidth: '164px' }} size={{ xs: 12, sm: 6, md: 4 }} key={index}>
sx={{ minWidth: "164px" }} <Card sx={{ minHeight: '180px' }}>
size={{ xs: 12, sm: 6, md: 4 }}
key={index}
>
<Card sx={{ minHeight: "180px" }}>
<CardActionArea <CardActionArea
onClick={() => onClick={() =>
doc.route doc.route ? onDocumentExpand(doc.route, true) : navigate('/')
? onDocumentExpand(doc.route, true)
: navigate("/")
} }
> >
<CardContent <CardContent
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
m: 0, m: 0,
p: 1, p: 1,
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
gap: 1, gap: 1,
verticalAlign: "top", verticalAlign: 'top',
}} }}
> >
{getDocumentIcon(doc.title)} {getDocumentIcon(doc.title)}
<Typography variant="h3" sx={{ m: "0 !important" }}> <Typography variant="h3" sx={{ m: '0 !important' }}>
{doc.title} {doc.title}
</Typography> </Typography>
</Box> </Box>
@ -430,7 +411,7 @@ const DocsPage = (props: BackstoryPageProps) => {
const drawerWidth = 240; const drawerWidth = 240;
return ( return (
<Box sx={{ display: "flex", height: "100%" }}> <Box sx={{ display: 'flex', height: '100%' }}>
{/* Mobile App Bar */} {/* Mobile App Bar */}
{isMobile && ( {isMobile && (
<AppBar <AppBar
@ -438,27 +419,17 @@ const DocsPage = (props: BackstoryPageProps) => {
sx={{ sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` }, width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` }, ml: { sm: `${drawerWidth}px` },
display: { md: "none" }, display: { md: 'none' },
}} }}
elevation={0} elevation={0}
color="default" color="default"
> >
<Toolbar> <Toolbar>
<IconButton <IconButton aria-label="open drawer" edge="start" onClick={toggleDrawer} sx={{ mr: 2 }}>
aria-label="open drawer"
edge="start"
onClick={toggleDrawer}
sx={{ mr: 2 }}
>
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography <Typography variant="h6" noWrap component="div" sx={{ color: 'white' }}>
variant="h6" {page ? documentTitleFromRoute(page) : 'Documentation'}
noWrap
component="div"
sx={{ color: "white" }}
>
{page ? documentTitleFromRoute(page) : "Documentation"}
</Typography> </Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
@ -482,9 +453,9 @@ const DocsPage = (props: BackstoryPageProps) => {
keepMounted: true, // Better open performance on mobile keepMounted: true, // Better open performance on mobile
}} }}
sx={{ sx={{
display: { xs: "block", md: "none" }, display: { xs: 'block', md: 'none' },
"& .MuiDrawer-paper": { '& .MuiDrawer-paper': {
boxSizing: "border-box", boxSizing: 'border-box',
width: drawerWidth, width: drawerWidth,
}, },
}} }}
@ -501,21 +472,17 @@ const DocsPage = (props: BackstoryPageProps) => {
<Drawer <Drawer
variant="permanent" variant="permanent"
sx={{ sx={{
display: { xs: "none", md: "block" }, display: { xs: 'none', md: 'block' },
"& .MuiDrawer-paper": { '& .MuiDrawer-paper': {
boxSizing: "border-box", boxSizing: 'border-box',
width: drawerWidth, width: drawerWidth,
position: "relative", position: 'relative',
height: "100%", height: '100%',
}, },
}} }}
open open
> >
<Sidebar <Sidebar currentPage={page} onDocumentSelect={onDocumentExpand} isMobile={false} />
currentPage={page}
onDocumentSelect={onDocumentExpand}
isMobile={false}
/>
</Drawer> </Drawer>
)} )}
</Box> </Box>
@ -528,8 +495,8 @@ const DocsPage = (props: BackstoryPageProps) => {
p: 3, p: 3,
width: { md: `calc(100% - ${drawerWidth}px)` }, width: { md: `calc(100% - ${drawerWidth}px)` },
pt: isMobile ? { xs: 8, sm: 9 } : 3, // Add padding top on mobile to account for AppBar pt: isMobile ? { xs: 8, sm: 9 } : 3, // Add padding top on mobile to account for AppBar
height: "100%", height: '100%',
overflow: "auto", overflow: 'auto',
}} }}
> >
{renderContent()} {renderContent()}

View File

@ -1,7 +1,7 @@
import React from "react"; import React from 'react';
import { BackstoryPageProps } from "../components/BackstoryTab"; import { BackstoryPageProps } from '../components/BackstoryTab';
import { CandidatePicker } from "components/ui/CandidatePicker"; import { CandidatePicker } from 'components/ui/CandidatePicker';
const CandidateListingPage = (props: BackstoryPageProps) => { const CandidateListingPage = (props: BackstoryPageProps) => {
return <CandidatePicker {...props} />; return <CandidatePicker {...props} />;

View File

@ -1,25 +1,22 @@
import React, { useEffect, useState, useRef, useCallback } from "react"; import React, { useEffect, useState, useRef, useCallback } from 'react';
import Avatar from "@mui/material/Avatar"; import Avatar from '@mui/material/Avatar';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import Tooltip from "@mui/material/Tooltip"; import Tooltip from '@mui/material/Tooltip';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import Paper from "@mui/material/Paper"; import Paper from '@mui/material/Paper';
import IconButton from "@mui/material/IconButton"; import IconButton from '@mui/material/IconButton';
import CancelIcon from "@mui/icons-material/Cancel"; import CancelIcon from '@mui/icons-material/Cancel';
import SendIcon from "@mui/icons-material/Send"; import SendIcon from '@mui/icons-material/Send';
import PropagateLoader from "react-spinners/PropagateLoader"; import PropagateLoader from 'react-spinners/PropagateLoader';
import { CandidateInfo } from "../components/ui/CandidateInfo"; import { CandidateInfo } from '../components/ui/CandidateInfo';
import { Quote } from "components/Quote"; import { Quote } from 'components/Quote';
import { BackstoryElementProps } from "components/BackstoryTab"; import { BackstoryElementProps } from 'components/BackstoryTab';
import { import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
BackstoryTextField, import { StyledMarkdown } from 'components/StyledMarkdown';
BackstoryTextFieldRef, import { Scrollable } from '../components/Scrollable';
} from "components/BackstoryTextField"; import { Pulse } from 'components/Pulse';
import { StyledMarkdown } from "components/StyledMarkdown"; import { StreamingResponse } from 'services/api-client';
import { Scrollable } from "../components/Scrollable";
import { Pulse } from "components/Pulse";
import { StreamingResponse } from "services/api-client";
import { import {
ChatMessage, ChatMessage,
ChatMessageUser, ChatMessageUser,
@ -27,34 +24,32 @@ import {
CandidateAI, CandidateAI,
ChatMessageStatus, ChatMessageStatus,
ChatMessageError, ChatMessageError,
} from "types/types"; } from 'types/types';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { Message } from "components/Message"; import { Message } from 'components/Message';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
status: "done", status: 'done',
type: "text", type: 'text',
sessionId: "", sessionId: '',
timestamp: new Date(), timestamp: new Date(),
content: "", content: '',
role: "user", role: 'user',
metadata: null as any, metadata: null as any,
}; };
const GenerateCandidate = (props: BackstoryElementProps) => { const GenerateCandidate = (props: BackstoryElementProps) => {
const { apiClient, user } = useAuth(); const { apiClient, user } = useAuth();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const [processingMessage, setProcessingMessage] = const [processingMessage, setProcessingMessage] = useState<ChatMessage | null>(null);
useState<ChatMessage | null>(null);
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [generatedUser, setGeneratedUser] = useState<CandidateAI | null>(null); const [generatedUser, setGeneratedUser] = useState<CandidateAI | null>(null);
const [prompt, setPrompt] = useState<string>(""); const [prompt, setPrompt] = useState<string>('');
const [resume, setResume] = useState<string | null>(null); const [resume, setResume] = useState<string | null>(null);
const [canGenImage, setCanGenImage] = useState<boolean>(false); const [canGenImage, setCanGenImage] = useState<boolean>(false);
const [timestamp, setTimestamp] = useState<string>(""); const [timestamp, setTimestamp] = useState<string>('');
const [shouldGenerateProfile, setShouldGenerateProfile] = const [shouldGenerateProfile, setShouldGenerateProfile] = useState<boolean>(false);
useState<boolean>(false);
const [chatSession, setChatSession] = useState<ChatSession | null>(null); const [chatSession, setChatSession] = useState<ChatSession | null>(null);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@ -74,26 +69,18 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
.getOrCreateChatSession( .getOrCreateChatSession(
generatedUser, generatedUser,
`Profile image generator for ${generatedUser.fullName}`, `Profile image generator for ${generatedUser.fullName}`,
"generate_image" 'generate_image'
) )
.then((session) => { .then(session => {
setChatSession(session); setChatSession(session);
setLoading(false); setLoading(false);
}); });
} catch (error) { } catch (error) {
setSnack("Unable to load chat session", "error"); setSnack('Unable to load chat session', 'error');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [ }, [generatedUser, chatSession, loading, setChatSession, setLoading, setSnack, apiClient]);
generatedUser,
chatSession,
loading,
setChatSession,
setLoading,
setSnack,
apiClient,
]);
const cancelQuery = useCallback(() => { const cancelQuery = useCallback(() => {
if (controllerRef.current) { if (controllerRef.current) {
@ -111,18 +98,18 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
const generatePersona = async (prompt: string) => { const generatePersona = async (prompt: string) => {
const userMessage: ChatMessageUser = { const userMessage: ChatMessageUser = {
type: "text", type: 'text',
role: "user", role: 'user',
content: prompt, content: prompt,
sessionId: "", sessionId: '',
status: "done", status: 'done',
timestamp: new Date(), timestamp: new Date(),
}; };
setPrompt(prompt || ""); setPrompt(prompt || '');
setProcessing(true); setProcessing(true);
setProcessingMessage({ setProcessingMessage({
...defaultMessage, ...defaultMessage,
content: "Generating persona...", content: 'Generating persona...',
}); });
try { try {
const result = await apiClient.createCandidateAI(userMessage); const result = await apiClient.createCandidateAI(userMessage);
@ -133,11 +120,11 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
setShouldGenerateProfile(true); // Reset the flag setShouldGenerateProfile(true); // Reset the flag
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setPrompt(""); setPrompt('');
setResume(null); setResume(null);
setProcessing(false); setProcessing(false);
setProcessingMessage(null); setProcessingMessage(null);
setSnack("Unable to generate AI persona", "error"); setSnack('Unable to generate AI persona', 'error');
} }
}; };
@ -147,10 +134,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
); );
const handleSendClick = useCallback(() => { const handleSendClick = useCallback(() => {
const value = const value = (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || '';
(backstoryTextRef.current &&
backstoryTextRef.current.getAndResetValue()) ||
"";
onEnter(value); onEnter(value);
}, [onEnter]); }, [onEnter]);
@ -163,29 +147,29 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
if ( if (
!shouldGenerateProfile || !shouldGenerateProfile ||
username === "[blank]" || username === '[blank]' ||
generatedUser?.firstName === "[blank]" generatedUser?.firstName === '[blank]'
) { ) {
return; return;
} }
if (controllerRef.current) { if (controllerRef.current) {
console.log("Controller already active, skipping profile generation"); console.log('Controller already active, skipping profile generation');
return; return;
} }
setProcessingMessage({ setProcessingMessage({
...defaultMessage, ...defaultMessage,
content: "Starting image generation...", content: 'Starting image generation...',
}); });
setProcessing(true); setProcessing(true);
setCanGenImage(false); setCanGenImage(false);
const chatMessage: ChatMessageUser = { const chatMessage: ChatMessageUser = {
sessionId: chatSession.id || "", sessionId: chatSession.id || '',
role: "user", role: 'user',
status: "done", status: 'done',
type: "text", type: 'text',
timestamp: new Date(), timestamp: new Date(),
content: prompt, content: prompt,
}; };
@ -195,20 +179,18 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
console.log(`onMessage: ${msg.type} ${msg.content}`, msg); console.log(`onMessage: ${msg.type} ${msg.content}`, msg);
controllerRef.current = null; controllerRef.current = null;
try { try {
await apiClient.updateCandidate(generatedUser.id || "", { await apiClient.updateCandidate(generatedUser.id || '', {
profileImage: "profile.png", profileImage: 'profile.png',
}); });
const { success, message } = await apiClient.deleteChatSession( const { success, message } = await apiClient.deleteChatSession(chatSession.id || '');
chatSession.id || ""
);
console.log( console.log(
`Profile generated for ${username} and chat session was ${ `Profile generated for ${username} and chat session was ${
!success ? "not " : "" !success ? 'not ' : ''
} deleted: ${message}}` } deleted: ${message}}`
); );
setGeneratedUser({ setGeneratedUser({
...generatedUser, ...generatedUser,
profileImage: "profile.png", profileImage: 'profile.png',
} as CandidateAI); } as CandidateAI);
setCanGenImage(true); setCanGenImage(true);
setShouldGenerateProfile(false); setShouldGenerateProfile(false);
@ -216,20 +198,17 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
console.error(error); console.error(error);
setSnack( setSnack(
`Unable to update ${username} to indicate they have a profile picture.`, `Unable to update ${username} to indicate they have a profile picture.`,
"error" 'error'
); );
} }
}, },
onError: (error: string | ChatMessageError) => { onError: (error: string | ChatMessageError) => {
console.log("onError:", error); console.log('onError:', error);
// Type-guard to determine if this is a ChatMessageBase or a string // Type-guard to determine if this is a ChatMessageBase or a string
if (typeof error === "object" && error !== null && "content" in error) { if (typeof error === 'object' && error !== null && 'content' in error) {
setSnack( setSnack(error.content || 'Unknown error generating profile image', 'error');
error.content || "Unknown error generating profile image",
"error"
);
} else { } else {
setSnack(error as string, "error"); setSnack(error as string, 'error');
} }
setProcessingMessage(null); setProcessingMessage(null);
setProcessing(false); setProcessing(false);
@ -245,51 +224,40 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
setShouldGenerateProfile(false); setShouldGenerateProfile(false);
}, },
onStatus: (status: ChatMessageStatus) => { onStatus: (status: ChatMessageStatus) => {
if (status.activity === "heartbeat" && status.content) { if (status.activity === 'heartbeat' && status.content) {
setTimestamp(status.timestamp?.toISOString() || ""); setTimestamp(status.timestamp?.toISOString() || '');
} else if (status.content) { } else if (status.content) {
setProcessingMessage({ ...defaultMessage, content: status.content }); setProcessingMessage({ ...defaultMessage, content: status.content });
} }
console.log(`onStatusChange: ${status}`); console.log(`onStatusChange: ${status}`);
}, },
}); });
}, [ }, [chatSession, shouldGenerateProfile, generatedUser, prompt, setSnack, apiClient]);
chatSession,
shouldGenerateProfile,
generatedUser,
prompt,
setSnack,
apiClient,
]);
if (!user?.isAdmin) { if (!user?.isAdmin) {
return ( return <Box>You must be logged in as an admin to generate AI candidates.</Box>;
<Box>You must be logged in as an admin to generate AI candidates.</Box>
);
} }
return ( return (
<Box <Box
className="GenerateCandidate" className="GenerateCandidate"
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
gap: 1, gap: 1,
maxWidth: { xs: "100%", md: "700px", lg: "1024px" }, maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
}} }}
> >
{generatedUser && ( {generatedUser && <CandidateInfo candidate={generatedUser} sx={{ flexShrink: 1 }} />}
<CandidateInfo candidate={generatedUser} sx={{ flexShrink: 1 }} />
)}
{prompt && <Quote quote={prompt} />} {prompt && <Quote quote={prompt} />}
{processing && ( {processing && (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
m: 2, m: 2,
}} }}
> >
@ -304,62 +272,58 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
/> />
</Box> </Box>
)} )}
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
position: "relative", position: 'relative',
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
position: "relative", position: 'relative',
width: "min-content", width: 'min-content',
height: "min-content", height: 'min-content',
}} }}
> >
<Avatar <Avatar
src={ src={
generatedUser?.profileImage generatedUser?.profileImage
? `/api/1.0/candidates/profile/${generatedUser.username}` ? `/api/1.0/candidates/profile/${generatedUser.username}`
: "" : ''
} }
alt={`${generatedUser?.fullName}'s profile`} alt={`${generatedUser?.fullName}'s profile`}
sx={{ sx={{
width: 80, width: 80,
height: 80, height: 80,
border: "2px solid #e0e0e0", border: '2px solid #e0e0e0',
}} }}
/> />
{processing && ( {processing && (
<Pulse <Pulse
sx={{ sx={{
position: "relative", position: 'relative',
left: "-80px", left: '-80px',
top: "0px", top: '0px',
mr: "-80px", mr: '-80px',
}} }}
timestamp={timestamp} timestamp={timestamp}
/> />
)} )}
</Box> </Box>
<Tooltip <Tooltip title={`${generatedUser?.profileImage ? 'Re-' : ''}Generate Picture`}>
title={`${ <span style={{ display: 'flex', flexGrow: 1 }}>
generatedUser?.profileImage ? "Re-" : ""
}Generate Picture`}
>
<span style={{ display: "flex", flexGrow: 1 }}>
<Button <Button
sx={{ sx={{
m: 1, m: 1,
gap: 1, gap: 1,
justifySelf: "flex-start", justifySelf: 'flex-start',
alignSelf: "center", alignSelf: 'center',
flexGrow: 0, flexGrow: 0,
maxHeight: "min-content", maxHeight: 'min-content',
}} }}
variant="contained" variant="contained"
disabled={processing || !canGenImage} disabled={processing || !canGenImage}
@ -367,7 +331,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
setShouldGenerateProfile(true); setShouldGenerateProfile(true);
}} }}
> >
{generatedUser?.profileImage ? "Re-" : ""}Generate Picture {generatedUser?.profileImage ? 'Re-' : ''}Generate Picture
<SendIcon /> <SendIcon />
</Button> </Button>
</span> </span>
@ -388,11 +352,9 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
onEnter={onEnter} onEnter={onEnter}
placeholder='Specify any characteristics you would like the persona to have. For example, "This person likes yo-yos."' placeholder='Specify any characteristics you would like the persona to have. For example, "This person likes yo-yos."'
/> />
<Box <Box sx={{ display: 'flex', justifyContent: 'center', flexDirection: 'row' }}>
sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }} <Tooltip title={'Send'}>
> <span style={{ display: 'flex', flexGrow: 1 }}>
<Tooltip title={"Send"}>
<span style={{ display: "flex", flexGrow: 1 }}>
<Button <Button
sx={{ m: 1, gap: 1, flexGrow: 1 }} sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained" variant="contained"
@ -405,13 +367,13 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
</span> </span>
</Tooltip> </Tooltip>
<Tooltip title="Cancel"> <Tooltip title="Cancel">
<span style={{ display: "flex" }}> <span style={{ display: 'flex' }}>
{" "} {' '}
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} {/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton <IconButton
aria-label="cancel" aria-label="cancel"
onClick={cancelQuery} onClick={cancelQuery}
sx={{ display: "flex", margin: "auto 0px" }} sx={{ display: 'flex', margin: 'auto 0px' }}
size="large" size="large"
edge="start" edge="start"
disabled={controllerRef.current === null || processing === false} disabled={controllerRef.current === null || processing === false}
@ -421,7 +383,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
</span> </span>
</Tooltip> </Tooltip>
</Box> </Box>
<Box sx={{ display: "flex", flexGrow: 1 }} /> <Box sx={{ display: 'flex', flexGrow: 1 }} />
</Box> </Box>
); );
}; };

View File

@ -1,5 +1,5 @@
import React from "react"; import React from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { import {
Box, Box,
Button, Button,
@ -11,16 +11,16 @@ import {
Card, Card,
CardContent, CardContent,
CardActions, CardActions,
} from "@mui/material"; } from '@mui/material';
import { styled } from "@mui/material/styles"; import { styled } from '@mui/material/styles';
import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import PersonSearchIcon from "@mui/icons-material/PersonSearch"; import PersonSearchIcon from '@mui/icons-material/PersonSearch';
import WorkHistoryIcon from "@mui/icons-material/WorkHistory"; import WorkHistoryIcon from '@mui/icons-material/WorkHistory';
import QuestionAnswerIcon from "@mui/icons-material/QuestionAnswer"; import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer';
import DescriptionIcon from "@mui/icons-material/Description"; import DescriptionIcon from '@mui/icons-material/Description';
import professionalConversationPng from "assets/Conversation.png"; import professionalConversationPng from 'assets/Conversation.png';
import { ComingSoon } from "components/ui/ComingSoon"; import { ComingSoon } from 'components/ui/ComingSoon';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
// Placeholder for Testimonials component // Placeholder for Testimonials component
const Testimonials = () => { const Testimonials = () => {
@ -36,7 +36,7 @@ const HeroSection = styled(Box)(({ theme }) => ({
padding: theme.spacing(8, 0), padding: theme.spacing(8, 0),
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
[theme.breakpoints.down("md")]: { [theme.breakpoints.down('md')]: {
padding: theme.spacing(6, 0), padding: theme.spacing(6, 0),
}, },
})); }));
@ -50,7 +50,7 @@ const HeroButton = (props: HeroButtonProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const handleClick = () => { const handleClick = () => {
const path = children?.replace(/ /g, "-").toLocaleLowerCase() || "/"; const path = children?.replace(/ /g, '-').toLocaleLowerCase() || '/';
navigate(path); navigate(path);
}; };
@ -60,7 +60,7 @@ const HeroButton = (props: HeroButtonProps) => {
fontWeight: 500, fontWeight: 500,
backgroundColor: theme.palette.action.active, backgroundColor: theme.palette.action.active,
color: theme.palette.background.paper, color: theme.palette.background.paper,
"&:hover": { '&:hover': {
backgroundColor: theme.palette.action.active, backgroundColor: theme.palette.action.active,
opacity: 0.9, opacity: 0.9,
}, },
@ -81,7 +81,7 @@ const ActionButton = (props: ActionButtonProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const handleClick = () => { const handleClick = () => {
const path = children?.replace(/ /g, "-").toLocaleLowerCase() || "/"; const path = children?.replace(/ /g, '-').toLocaleLowerCase() || '/';
navigate(path); navigate(path);
}; };
@ -95,11 +95,11 @@ const ActionButton = (props: ActionButtonProps) => {
const FeatureIcon = styled(Box)(({ theme }) => ({ const FeatureIcon = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.action.active, backgroundColor: theme.palette.action.active,
color: theme.palette.background.paper, color: theme.palette.background.paper,
borderRadius: "50%", borderRadius: '50%',
padding: theme.spacing(2), padding: theme.spacing(2),
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
width: 64, width: 64,
height: 64, height: 64,
@ -116,7 +116,7 @@ const FeatureCard = ({
description: string; description: string;
}) => { }) => {
return ( return (
<Card sx={{ height: "100%", display: "flex", flexDirection: "column" }}> <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<CardContent sx={{ flexGrow: 1 }}> <CardContent sx={{ flexGrow: 1 }}>
<Box display="flex" justifyContent="center" mb={2}> <Box display="flex" justifyContent="center" mb={2}>
{icon} {icon}
@ -128,7 +128,7 @@ const FeatureCard = ({
{description} {description}
</Typography> </Typography>
</CardContent> </CardContent>
<CardActions sx={{ justifyContent: "center", pb: 2 }}> <CardActions sx={{ justifyContent: 'center', pb: 2 }}>
<Button size="small" endIcon={<ArrowForwardIcon />}> <Button size="small" endIcon={<ArrowForwardIcon />}>
Learn more Learn more
</Button> </Button>
@ -143,25 +143,25 @@ const HomePage = () => {
if (isGuest) { if (isGuest) {
// Show guest-specific UI // Show guest-specific UI
console.log("Guest session:", guest?.sessionId || "No guest"); console.log('Guest session:', guest?.sessionId || 'No guest');
} else { } else {
// Show authenticated user UI // Show authenticated user UI
console.log("Authenticated user:", user?.email || "No user"); console.log('Authenticated user:', user?.email || 'No user');
} }
return ( return (
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
{/* Hero Section */} {/* Hero Section */}
<HeroSection> <HeroSection>
<Container> <Container>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: { xs: "column", md: "row" }, flexDirection: { xs: 'column', md: 'row' },
gap: 4, gap: 4,
alignItems: "center", alignItems: 'center',
flexGrow: 1, flexGrow: 1,
maxWidth: "1024px", maxWidth: '1024px',
}} }}
> >
<Box sx={{ flex: 1, flexGrow: 1 }}> <Box sx={{ flex: 1, flexGrow: 1 }}>
@ -170,18 +170,18 @@ const HomePage = () => {
component="h1" component="h1"
sx={{ sx={{
fontWeight: 700, fontWeight: 700,
fontSize: { xs: "2rem", md: "3rem" }, fontSize: { xs: '2rem', md: '3rem' },
mb: 2, mb: 2,
color: "white", color: 'white',
}} }}
> >
Your complete professional story, beyond a single page Your complete professional story, beyond a single page
</Typography> </Typography>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}> <Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}>
Let potential employers discover the depth of your experience Let potential employers discover the depth of your experience through interactive
through interactive Q&A and tailored resumes Q&A and tailored resumes
</Typography> </Typography>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<HeroButton variant="contained" size="large"> <HeroButton variant="contained" size="large">
Get Started as Candidate Get Started as Candidate
</HeroButton> </HeroButton>
@ -189,9 +189,9 @@ const HomePage = () => {
variant="outlined" variant="outlined"
size="large" size="large"
sx={{ sx={{
backgroundColor: "transparent", backgroundColor: 'transparent',
border: "2px solid", border: '2px solid',
borderColor: "action.active", borderColor: 'action.active',
}} }}
> >
Recruit Talent Recruit Talent
@ -200,8 +200,8 @@ const HomePage = () => {
</Box> </Box>
<Box <Box
sx={{ sx={{
justifyContent: "center", justifyContent: 'center',
display: { xs: "none", md: "block" }, display: { xs: 'none', md: 'block' },
}} }}
> >
<Box <Box
@ -209,9 +209,9 @@ const HomePage = () => {
src={professionalConversationPng} src={professionalConversationPng}
alt="Professional conversation" alt="Professional conversation"
sx={{ sx={{
width: "100%", width: '100%',
maxWidth: 200, maxWidth: 200,
height: "auto", height: 'auto',
borderRadius: 2, borderRadius: 2,
boxShadow: 3, boxShadow: 3,
}} }}
@ -235,25 +235,19 @@ const HomePage = () => {
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: { xs: "column", md: "row" }, flexDirection: { xs: 'column', md: 'row' },
gap: 4, gap: 4,
}} }}
> >
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<Typography <Typography variant="h4" component="h3" gutterBottom sx={{ color: 'primary.main' }}>
variant="h4"
component="h3"
gutterBottom
sx={{ color: "primary.main" }}
>
For Job Seekers For Job Seekers
</Typography> </Typography>
<Box sx={{ my: 3 }}> <Box sx={{ my: 3 }}>
<Typography variant="body1" paragraph> <Typography variant="body1" paragraph>
Backstory helps you tell your complete professional story, Backstory helps you tell your complete professional story, highlight your
highlight your achievements, and showcase your skills beyond achievements, and showcase your skills beyond what fits on a traditional resume.
what fits on a traditional resume.
</Typography> </Typography>
</Box> </Box>
@ -261,18 +255,18 @@ const HomePage = () => {
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box <Box
sx={{ sx={{
backgroundColor: "primary.main", backgroundColor: 'primary.main',
color: "primary.contrastText", color: 'primary.contrastText',
borderRadius: "50%", borderRadius: '50%',
width: 40, width: 40,
height: 40, height: 40,
minWidth: 40, minWidth: 40,
minHeight: 40, minHeight: 40,
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
mr: 2, mr: 2,
fontWeight: "bold", fontWeight: 'bold',
}} }}
> >
1 1
@ -285,43 +279,42 @@ const HomePage = () => {
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box <Box
sx={{ sx={{
backgroundColor: "primary.main", backgroundColor: 'primary.main',
color: "primary.contrastText", color: 'primary.contrastText',
borderRadius: "50%", borderRadius: '50%',
width: 40, width: 40,
height: 40, height: 40,
minWidth: 40, minWidth: 40,
minHeight: 40, minHeight: 40,
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
mr: 2, mr: 2,
fontWeight: "bold", fontWeight: 'bold',
}} }}
> >
2 2
</Box> </Box>
<Typography variant="body1"> <Typography variant="body1">
Configure your AI assistant to answer questions about your Configure your AI assistant to answer questions about your experience
experience
</Typography> </Typography>
</Box> </Box>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box <Box
sx={{ sx={{
backgroundColor: "primary.main", backgroundColor: 'primary.main',
color: "primary.contrastText", color: 'primary.contrastText',
borderRadius: "50%", borderRadius: '50%',
width: 40, width: 40,
height: 40, height: 40,
minWidth: 40, minWidth: 40,
minHeight: 40, minHeight: 40,
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
mr: 2, mr: 2,
fontWeight: "bold", fontWeight: 'bold',
}} }}
> >
3 3
@ -344,19 +337,13 @@ const HomePage = () => {
<ComingSoon> <ComingSoon>
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<Typography <Typography variant="h4" component="h3" gutterBottom sx={{ color: 'primary.main' }}>
variant="h4"
component="h3"
gutterBottom
sx={{ color: "primary.main" }}
>
For Employers For Employers
</Typography> </Typography>
<Box sx={{ my: 3 }}> <Box sx={{ my: 3 }}>
<Typography variant="body1" paragraph> <Typography variant="body1" paragraph>
Discover candidates with the perfect skills and experience for Discover candidates with the perfect skills and experience for your positions by
your positions by engaging in meaningful Q&A to learn more engaging in meaningful Q&A to learn more about their background.
about their background.
</Typography> </Typography>
</Box> </Box>
@ -364,68 +351,66 @@ const HomePage = () => {
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box <Box
sx={{ sx={{
backgroundColor: "secondary.main", backgroundColor: 'secondary.main',
color: "secondary.contrastText", color: 'secondary.contrastText',
borderRadius: "50%", borderRadius: '50%',
width: 40, width: 40,
height: 40, height: 40,
minWidth: 40, minWidth: 40,
minHeight: 40, minHeight: 40,
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
mr: 2, mr: 2,
fontWeight: "bold", fontWeight: 'bold',
}} }}
> >
1 1
</Box> </Box>
<Typography variant="body1"> <Typography variant="body1">
Search the candidate pool based on skills, experience, and Search the candidate pool based on skills, experience, and location
location
</Typography> </Typography>
</Box> </Box>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box <Box
sx={{ sx={{
backgroundColor: "secondary.main", backgroundColor: 'secondary.main',
color: "secondary.contrastText", color: 'secondary.contrastText',
borderRadius: "50%", borderRadius: '50%',
width: 40, width: 40,
height: 40, height: 40,
minWidth: 40, minWidth: 40,
minHeight: 40, minHeight: 40,
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
mr: 2, mr: 2,
fontWeight: "bold", fontWeight: 'bold',
}} }}
> >
2 2
</Box> </Box>
<Typography variant="body1"> <Typography variant="body1">
Ask personalized questions about candidates' experience and Ask personalized questions about candidates' experience and skills
skills
</Typography> </Typography>
</Box> </Box>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box <Box
sx={{ sx={{
backgroundColor: "secondary.main", backgroundColor: 'secondary.main',
color: "secondary.contrastText", color: 'secondary.contrastText',
borderRadius: "50%", borderRadius: '50%',
width: 40, width: 40,
height: 40, height: 40,
minWidth: 40, minWidth: 40,
minHeight: 40, minHeight: 40,
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
mr: 2, mr: 2,
fontWeight: "bold", fontWeight: 'bold',
}} }}
> >
3 3
@ -450,7 +435,7 @@ const HomePage = () => {
</Container> </Container>
{/* Features Section */} {/* Features Section */}
<Box sx={{ backgroundColor: "background.paper", py: 8 }}> <Box sx={{ backgroundColor: 'background.paper', py: 8 }}>
<Container> <Container>
<Typography <Typography
variant="h3" variant="h3"
@ -462,14 +447,14 @@ const HomePage = () => {
Key Features Key Features
</Typography> </Typography>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 4 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
<Box <Box
sx={{ sx={{
flex: "1 1 250px", flex: '1 1 250px',
minWidth: { minWidth: {
xs: "100%", xs: '100%',
sm: "calc(50% - 16px)", sm: 'calc(50% - 16px)',
md: "calc(25% - 16px)", md: 'calc(25% - 16px)',
}, },
}} }}
> >
@ -485,11 +470,11 @@ const HomePage = () => {
</Box> </Box>
<Box <Box
sx={{ sx={{
flex: "1 1 250px", flex: '1 1 250px',
minWidth: { minWidth: {
xs: "100%", xs: '100%',
sm: "calc(50% - 16px)", sm: 'calc(50% - 16px)',
md: "calc(25% - 16px)", md: 'calc(25% - 16px)',
}, },
}} }}
> >
@ -505,11 +490,11 @@ const HomePage = () => {
</Box> </Box>
<Box <Box
sx={{ sx={{
flex: "1 1 250px", flex: '1 1 250px',
minWidth: { minWidth: {
xs: "100%", xs: '100%',
sm: "calc(50% - 16px)", sm: 'calc(50% - 16px)',
md: "calc(25% - 16px)", md: 'calc(25% - 16px)',
}, },
}} }}
> >
@ -525,11 +510,11 @@ const HomePage = () => {
</Box> </Box>
<Box <Box
sx={{ sx={{
flex: "1 1 250px", flex: '1 1 250px',
minWidth: { minWidth: {
xs: "100%", xs: '100%',
sm: "calc(50% - 16px)", sm: 'calc(50% - 16px)',
md: "calc(25% - 16px)", md: 'calc(25% - 16px)',
}, },
}} }}
> >
@ -559,13 +544,8 @@ const HomePage = () => {
> >
Success Stories Success Stories
</Typography> </Typography>
<Typography <Typography variant="body1" align="center" sx={{ mb: 6, maxWidth: 800, mx: 'auto' }}>
variant="body1" See how Backstory has transformed the hiring process for both candidates and employers.
align="center"
sx={{ mb: 6, maxWidth: 800, mx: "auto" }}
>
See how Backstory has transformed the hiring process for both
candidates and employers.
</Typography> </Typography>
<Testimonials /> <Testimonials />
@ -575,39 +555,29 @@ const HomePage = () => {
{/* CTA Section */} {/* CTA Section */}
<Box <Box
sx={{ sx={{
backgroundColor: "primary.main", backgroundColor: 'primary.main',
color: "primary.contrastText", color: 'primary.contrastText',
py: 8, py: 8,
}} }}
> >
<Container> <Container>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
textAlign: "center", textAlign: 'center',
maxWidth: 800, maxWidth: 800,
mx: "auto", mx: 'auto',
}} }}
> >
<Typography <Typography variant="h3" component="h2" gutterBottom sx={{ color: 'white' }}>
variant="h3"
component="h2"
gutterBottom
sx={{ color: "white" }}
>
Ready to transform your hiring process? Ready to transform your hiring process?
</Typography> </Typography>
<Typography variant="h6" sx={{ mb: 4 }}> <Typography variant="h6" sx={{ mb: 4 }}>
Join Backstory today and discover a better way to connect talent Join Backstory today and discover a better way to connect talent with opportunity.
with opportunity.
</Typography> </Typography>
<Stack <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} justifyContent="center">
direction={{ xs: "column", sm: "row" }}
spacing={2}
justifyContent="center"
>
<HeroButton variant="contained" size="large"> <HeroButton variant="contained" size="large">
Sign Up as Candidate Sign Up as Candidate
</HeroButton> </HeroButton>
@ -615,9 +585,9 @@ const HomePage = () => {
variant="outlined" variant="outlined"
size="large" size="large"
sx={{ sx={{
backgroundColor: "transparent", backgroundColor: 'transparent',
border: "2px solid", border: '2px solid',
borderColor: "action.active", borderColor: 'action.active',
}} }}
> >
Sign Up as Employer Sign Up as Employer

View File

@ -1,5 +1,5 @@
import React from "react"; import React from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { import {
Box, Box,
Button, Button,
@ -17,37 +17,37 @@ import {
ButtonProps, ButtonProps,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from '@mui/material';
import { styled } from "@mui/material/styles"; import { styled } from '@mui/material/styles';
import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import AssessmentIcon from "@mui/icons-material/Assessment"; import AssessmentIcon from '@mui/icons-material/Assessment';
import PersonIcon from "@mui/icons-material/Person"; import PersonIcon from '@mui/icons-material/Person';
import WorkIcon from "@mui/icons-material/Work"; import WorkIcon from '@mui/icons-material/Work';
import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome"; import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import DescriptionIcon from "@mui/icons-material/Description"; import DescriptionIcon from '@mui/icons-material/Description';
import professionalConversationPng from "assets/Conversation.png"; import professionalConversationPng from 'assets/Conversation.png';
import selectAJobPng from "assets/select-a-job.png"; import selectAJobPng from 'assets/select-a-job.png';
import selectJobAnalysisPng from "assets/select-job-analysis.png"; import selectJobAnalysisPng from 'assets/select-job-analysis.png';
import selectACandidatePng from "assets/select-a-candidate.png"; import selectACandidatePng from 'assets/select-a-candidate.png';
import selectStartAnalysisPng from "assets/select-start-analysis.png"; import selectStartAnalysisPng from 'assets/select-start-analysis.png';
import waitPng from "assets/wait.png"; import waitPng from 'assets/wait.png';
import finalResumePng from "assets/final-resume.png"; import finalResumePng from 'assets/final-resume.png';
import { Beta } from "components/ui/Beta"; import { Beta } from 'components/ui/Beta';
// Styled components matching HomePage patterns // Styled components matching HomePage patterns
const HeroSection = styled(Box)(({ theme }) => ({ const HeroSection = styled(Box)(({ theme }) => ({
padding: theme.spacing(3, 0), padding: theme.spacing(3, 0),
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
[theme.breakpoints.down("md")]: { [theme.breakpoints.down('md')]: {
padding: theme.spacing(2, 0), padding: theme.spacing(2, 0),
}, },
})); }));
const StepSection = styled(Box)(({ theme }) => ({ const StepSection = styled(Box)(({ theme }) => ({
padding: theme.spacing(6, 0), padding: theme.spacing(6, 0),
"&:nth-of-type(even)": { '&:nth-of-type(even)': {
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
}, },
})); }));
@ -55,25 +55,25 @@ const StepSection = styled(Box)(({ theme }) => ({
const StepNumber = styled(Box)(({ theme }) => ({ const StepNumber = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.action.active, backgroundColor: theme.palette.action.active,
color: theme.palette.background.paper, color: theme.palette.background.paper,
borderRadius: "50%", borderRadius: '50%',
width: 60, width: 60,
height: 60, height: 60,
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
fontSize: "1.5rem", fontSize: '1.5rem',
fontWeight: "bold", fontWeight: 'bold',
margin: "0 auto 1rem auto", margin: '0 auto 1rem auto',
[theme.breakpoints.up("md")]: { [theme.breakpoints.up('md')]: {
margin: 0, margin: 0,
}, },
})); }));
const ImageContainer = styled(Box)(({ theme }) => ({ const ImageContainer = styled(Box)(({ theme }) => ({
textAlign: "center", textAlign: 'center',
"& img": { '& img': {
maxWidth: "100%", maxWidth: '100%',
height: "auto", height: 'auto',
borderRadius: theme.spacing(1), borderRadius: theme.spacing(1),
boxShadow: theme.shadows[3], boxShadow: theme.shadows[3],
border: `2px solid ${theme.palette.action.active}`, border: `2px solid ${theme.palette.action.active}`,
@ -81,22 +81,22 @@ const ImageContainer = styled(Box)(({ theme }) => ({
})); }));
const StepCard = styled(Card)(({ theme }) => ({ const StepCard = styled(Card)(({ theme }) => ({
height: "100%", height: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
border: `1px solid ${theme.palette.action.active}`, border: `1px solid ${theme.palette.action.active}`,
"&:hover": { '&:hover': {
boxShadow: theme.shadows[4], boxShadow: theme.shadows[4],
}, },
})); }));
const steps = [ const steps = [
"Select Job Analysis", 'Select Job Analysis',
"Choose a Job", 'Choose a Job',
"Select a Candidate", 'Select a Candidate',
"Start Assessment", 'Start Assessment',
"Review Results", 'Review Results',
"Generate Resume", 'Generate Resume',
]; ];
interface StepContentProps { interface StepContentProps {
@ -126,24 +126,18 @@ const StepContent: React.FC<StepContentProps> = ({
}) => { }) => {
const textContent = ( const textContent = (
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ display: "flex", alignItems: "center", mb: 3 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<StepNumber>{stepNumber}</StepNumber> <StepNumber>{stepNumber}</StepNumber>
<Box <Box sx={{ ml: { xs: 0, md: 3 }, textAlign: { xs: 'center', md: 'left' } }}>
sx={{ ml: { xs: 0, md: 3 }, textAlign: { xs: "center", md: "left" } }} <Typography variant="h3" component="h2" sx={{ color: 'primary.main', mb: 1 }}>
>
<Typography
variant="h3"
component="h2"
sx={{ color: "primary.main", mb: 1 }}
>
{title} {title}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
gap: 1, gap: 1,
alignItems: "center", alignItems: 'center',
justifyContent: { xs: "center", md: "flex-start" }, justifyContent: { xs: 'center', md: 'flex-start' },
}} }}
> >
{icon} {icon}
@ -162,13 +156,13 @@ const StepContent: React.FC<StepContentProps> = ({
<Paper <Paper
sx={{ sx={{
p: 2, p: 2,
backgroundColor: "action.hover", backgroundColor: 'action.hover',
border: "1px solid", border: '1px solid',
borderColor: "action.active", borderColor: 'action.active',
mt: 2, mt: 2,
}} }}
> >
<Typography variant="body2" sx={{ fontStyle: "italic" }}> <Typography variant="body2" sx={{ fontStyle: 'italic' }}>
<strong>Note:</strong> {note} <strong>Note:</strong> {note}
</Typography> </Typography>
</Paper> </Paper>
@ -177,12 +171,12 @@ const StepContent: React.FC<StepContentProps> = ({
<Paper <Paper
sx={{ sx={{
p: 2, p: 2,
backgroundColor: "secondary.main", backgroundColor: 'secondary.main',
color: "secondary.contrastText", color: 'secondary.contrastText',
mt: 2, mt: 2,
}} }}
> >
<Typography variant="body1" sx={{ fontWeight: "bold" }}> <Typography variant="body1" sx={{ fontWeight: 'bold' }}>
🎉 {success} 🎉 {success}
</Typography> </Typography>
</Paper> </Paper>
@ -233,7 +227,7 @@ const HeroButton = (props: HeroButtonProps) => {
fontWeight: 500, fontWeight: 500,
backgroundColor: theme.palette.action.active, backgroundColor: theme.palette.action.active,
color: theme.palette.background.paper, color: theme.palette.background.paper,
"&:hover": { '&:hover': {
backgroundColor: theme.palette.action.active, backgroundColor: theme.palette.action.active,
opacity: 0.9, opacity: 0.9,
}, },
@ -248,26 +242,26 @@ const HeroButton = (props: HeroButtonProps) => {
const HowItWorks: React.FC = () => { const HowItWorks: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const handleGetStarted = () => { const handleGetStarted = () => {
navigate("/job-analysis"); navigate('/job-analysis');
}; };
return ( return (
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
{/* Hero Section */} {/* Hero Section */}
{/* Hero Section */} {/* Hero Section */}
<HeroSection> <HeroSection>
<Container> <Container>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: { xs: "column", md: "row" }, flexDirection: { xs: 'column', md: 'row' },
gap: 4, gap: 4,
alignItems: "center", alignItems: 'center',
flexGrow: 1, flexGrow: 1,
maxWidth: "1024px", maxWidth: '1024px',
}} }}
> >
<Box sx={{ flex: 1, flexGrow: 1 }}> <Box sx={{ flex: 1, flexGrow: 1 }}>
@ -276,23 +270,19 @@ const HowItWorks: React.FC = () => {
component="h1" component="h1"
sx={{ sx={{
fontWeight: 700, fontWeight: 700,
fontSize: { xs: "2rem", md: "3rem" }, fontSize: { xs: '2rem', md: '3rem' },
mb: 2, mb: 2,
color: "white", color: 'white',
}} }}
> >
Your complete professional story, beyond a single page Your complete professional story, beyond a single page
</Typography> </Typography>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}> <Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}>
Let potential employers discover the depth of your experience Let potential employers discover the depth of your experience through interactive
through interactive Q&A and tailored resumes Q&A and tailored resumes
</Typography> </Typography>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<HeroButton <HeroButton variant="contained" size="large" path="/login/register">
variant="contained"
size="large"
path="/login/register"
>
Get Started as Candidate Get Started as Candidate
</HeroButton> </HeroButton>
{/* <HeroButton {/* <HeroButton
@ -310,8 +300,8 @@ const HowItWorks: React.FC = () => {
</Box> </Box>
<Box <Box
sx={{ sx={{
justifyContent: "center", justifyContent: 'center',
display: { xs: "none", md: "block" }, display: { xs: 'none', md: 'block' },
}} }}
> >
<Box <Box
@ -319,9 +309,9 @@ const HowItWorks: React.FC = () => {
src={professionalConversationPng} src={professionalConversationPng}
alt="Professional conversation" alt="Professional conversation"
sx={{ sx={{
width: "100%", width: '100%',
maxWidth: 200, maxWidth: 200,
height: "auto", height: 'auto',
borderRadius: 2, borderRadius: 2,
boxShadow: 3, boxShadow: 3,
}} }}
@ -332,22 +322,22 @@ const HowItWorks: React.FC = () => {
</HeroSection> </HeroSection>
<HeroSection <HeroSection
sx={{ sx={{
display: "flex", display: 'flex',
position: "relative", position: 'relative',
overflow: "hidden", overflow: 'hidden',
border: "2px solid orange", border: '2px solid orange',
}} }}
> >
<Beta adaptive={false} sx={{ left: "-90px" }} /> <Beta adaptive={false} sx={{ left: '-90px' }} />
<Container sx={{ display: "flex", position: "relative" }}> <Container sx={{ display: 'flex', position: 'relative' }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
textAlign: "center", textAlign: 'center',
maxWidth: 800, maxWidth: 800,
mx: "auto", mx: 'auto',
position: "relative", position: 'relative',
}} }}
> >
<Typography <Typography
@ -355,9 +345,9 @@ const HowItWorks: React.FC = () => {
component="h1" component="h1"
sx={{ sx={{
fontWeight: 700, fontWeight: 700,
fontSize: { xs: "2rem", md: "2.5rem" }, fontSize: { xs: '2rem', md: '2.5rem' },
mb: 2, mb: 2,
color: "white", color: 'white',
}} }}
> >
Welcome to the Backstory Beta! Welcome to the Backstory Beta!
@ -371,9 +361,9 @@ const HowItWorks: React.FC = () => {
{/* Progress Overview */} {/* Progress Overview */}
<Container sx={{ py: 4 }}> <Container sx={{ py: 4 }}>
<Box sx={{ display: { xs: "none", md: "block" } }}> <Box sx={{ display: { xs: 'none', md: 'block' } }}>
<Stepper alternativeLabel sx={{ mb: 4 }}> <Stepper alternativeLabel sx={{ mb: 4 }}>
{steps.map((label) => ( {steps.map(label => (
<Step key={label}> <Step key={label}>
<StepLabel>{label}</StepLabel> <StepLabel>{label}</StepLabel>
</Step> </Step>
@ -389,7 +379,7 @@ const HowItWorks: React.FC = () => {
stepNumber={1} stepNumber={1}
title="Select Job Analysis" title="Select Job Analysis"
subtitle="Navigate to the main feature" subtitle="Navigate to the main feature"
icon={<AssessmentIcon sx={{ color: "action.active" }} />} icon={<AssessmentIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"Select 'Job Analysis' from the menu. This takes you to the interactive Job Analysis page, where you will get to evaluate a candidate for a selected job.", "Select 'Job Analysis' from the menu. This takes you to the interactive Job Analysis page, where you will get to evaluate a candidate for a selected job.",
]} ]}
@ -406,9 +396,9 @@ const HowItWorks: React.FC = () => {
stepNumber={2} stepNumber={2}
title="Choose a Job" title="Choose a Job"
subtitle="Pick from existing job postings" subtitle="Pick from existing job postings"
icon={<WorkIcon sx={{ color: "action.active" }} />} icon={<WorkIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"Once on the Job Analysis Page, explore a little bit and then select one of the jobs. The requirements and information provided on Backstory are extracted from job postings that users have pasted as a job description or uploaded from a PDF.", 'Once on the Job Analysis Page, explore a little bit and then select one of the jobs. The requirements and information provided on Backstory are extracted from job postings that users have pasted as a job description or uploaded from a PDF.',
]} ]}
imageSrc={selectAJobPng} imageSrc={selectAJobPng}
imageAlt="Select a job from the available options" imageAlt="Select a job from the available options"
@ -425,9 +415,9 @@ const HowItWorks: React.FC = () => {
stepNumber={3} stepNumber={3}
title="Select a Candidate" title="Select a Candidate"
subtitle="Choose from available profiles" subtitle="Choose from available profiles"
icon={<PersonIcon sx={{ color: "action.active" }} />} icon={<PersonIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"Now that you have a Job selected, you need to select a candidate. In addition to myself (James), there are several candidates which AI has generated. Each has a unique skillset and can be used to test out the system.", 'Now that you have a Job selected, you need to select a candidate. In addition to myself (James), there are several candidates which AI has generated. Each has a unique skillset and can be used to test out the system.',
]} ]}
imageSrc={selectACandidatePng} imageSrc={selectACandidatePng}
imageAlt="Select a candidate from the available profiles" imageAlt="Select a candidate from the available profiles"
@ -443,10 +433,10 @@ const HowItWorks: React.FC = () => {
stepNumber={4} stepNumber={4}
title="Start Assessment" title="Start Assessment"
subtitle="Begin the AI analysis" subtitle="Begin the AI analysis"
icon={<PlayArrowIcon sx={{ color: "action.active" }} />} icon={<PlayArrowIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"After selecting a candidate, you are ready to have Backstory perform the Job Analysis. During this phase, Backstory will take each of requirements extracted from the Job and match it against information about the selected candidate.", 'After selecting a candidate, you are ready to have Backstory perform the Job Analysis. During this phase, Backstory will take each of requirements extracted from the Job and match it against information about the selected candidate.',
"This could be as little as a simple resume, or as complete as a full work history. Backstory performs similarity searches to identify key elements from the candidate that pertain to a given skill and provides a graded response.", 'This could be as little as a simple resume, or as complete as a full work history. Backstory performs similarity searches to identify key elements from the candidate that pertain to a given skill and provides a graded response.',
'To see that in action, click the "Start Skill Assessment" button.', 'To see that in action, click the "Start Skill Assessment" button.',
]} ]}
imageSrc={selectStartAnalysisPng} imageSrc={selectStartAnalysisPng}
@ -463,10 +453,10 @@ const HowItWorks: React.FC = () => {
stepNumber={5} stepNumber={5}
title="Review Results" title="Review Results"
subtitle="Watch the magic happen" subtitle="Watch the magic happen"
icon={<AutoAwesomeIcon sx={{ color: "action.active" }} />} icon={<AutoAwesomeIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"Once you begin that action, the Start Skill Assessment button will grey out and the page will begin updating as it collates information about the candidate. As Backstory performs its magic, you can monitor the progress and explore the different identified skills to see how or why a candidate does or does not have that skill.", 'Once you begin that action, the Start Skill Assessment button will grey out and the page will begin updating as it collates information about the candidate. As Backstory performs its magic, you can monitor the progress and explore the different identified skills to see how or why a candidate does or does not have that skill.',
"Once it is done, you can see the final Overall Match. This is a weighted score based on amount of evidence a skill had, whether the skill was required or preferred, and other metrics.", 'Once it is done, you can see the final Overall Match. This is a weighted score based on amount of evidence a skill had, whether the skill was required or preferred, and other metrics.',
]} ]}
imageSrc={waitPng} imageSrc={waitPng}
imageAlt="Wait for the analysis to complete and review results" imageAlt="Wait for the analysis to complete and review results"
@ -481,7 +471,7 @@ const HowItWorks: React.FC = () => {
stepNumber={6} stepNumber={6}
title="Generate Resume" title="Generate Resume"
subtitle="Create your tailored resume" subtitle="Create your tailored resume"
icon={<DescriptionIcon sx={{ color: "action.active" }} />} icon={<DescriptionIcon sx={{ color: 'action.active' }} />}
description={[ description={[
'The final step is creating the custom resume for the Candidate tailored to the particular Job. On the bottom right you can click "Next" to have Backstory generate the custom resume.', 'The final step is creating the custom resume for the Candidate tailored to the particular Job. On the bottom right you can click "Next" to have Backstory generate the custom resume.',
"Note that the resume focuses on identifying key areas from the Candidate's work history that align with skills which were extracted from the original job posting.", "Note that the resume focuses on identifying key areas from the Candidate's work history that align with skills which were extracted from the original job posting.",
@ -497,28 +487,23 @@ const HowItWorks: React.FC = () => {
{/* CTA Section */} {/* CTA Section */}
<Box <Box
sx={{ sx={{
backgroundColor: "primary.main", backgroundColor: 'primary.main',
color: "primary.contrastText", color: 'primary.contrastText',
py: 6, py: 6,
}} }}
> >
<Container> <Container>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
textAlign: "center", textAlign: 'center',
maxWidth: 600, maxWidth: 600,
mx: "auto", mx: 'auto',
}} }}
> >
<Typography <Typography variant="h3" component="h2" gutterBottom sx={{ color: 'white' }}>
variant="h3"
component="h2"
gutterBottom
sx={{ color: "white" }}
>
Ready to try Backstory? Ready to try Backstory?
</Typography> </Typography>
<Typography variant="h6" sx={{ mb: 4 }}> <Typography variant="h6" sx={{ mb: 4 }}>
@ -530,13 +515,13 @@ const HowItWorks: React.FC = () => {
startIcon={<PlayArrowIcon />} startIcon={<PlayArrowIcon />}
onClick={handleGetStarted} onClick={handleGetStarted}
sx={{ sx={{
backgroundColor: "action.active", backgroundColor: 'action.active',
color: "background.paper", color: 'background.paper',
fontWeight: "bold", fontWeight: 'bold',
px: 4, px: 4,
py: 1.5, py: 1.5,
"&:hover": { '&:hover': {
backgroundColor: "action.active", backgroundColor: 'action.active',
opacity: 0.9, opacity: 0.9,
}, },
}} }}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useRef } from "react"; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { import {
Box, Box,
Stepper, Stepper,
@ -15,32 +15,28 @@ import {
Avatar, Avatar,
useMediaQuery, useMediaQuery,
Divider, Divider,
} from "@mui/material"; } from '@mui/material';
import { Add, WorkOutline } from "@mui/icons-material"; import { Add, WorkOutline } from '@mui/icons-material';
import PersonIcon from "@mui/icons-material/Person"; import PersonIcon from '@mui/icons-material/Person';
import WorkIcon from "@mui/icons-material/Work"; import WorkIcon from '@mui/icons-material/Work';
import AssessmentIcon from "@mui/icons-material/Assessment"; import AssessmentIcon from '@mui/icons-material/Assessment';
import { JobMatchAnalysis } from "components/JobMatchAnalysis"; import { JobMatchAnalysis } from 'components/JobMatchAnalysis';
import { Candidate, Job, SkillAssessment } from "types/types"; import { Candidate, Job, SkillAssessment } from 'types/types';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { BackstoryPageProps } from "components/BackstoryTab"; import { BackstoryPageProps } from 'components/BackstoryTab';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
useAppState, import { CandidateInfo } from 'components/ui/CandidateInfo';
useSelectedCandidate, import { ComingSoon } from 'components/ui/ComingSoon';
useSelectedJob, import { LoginRequired } from 'components/ui/LoginRequired';
} from "hooks/GlobalContext"; import { Scrollable } from 'components/Scrollable';
import { CandidateInfo } from "components/ui/CandidateInfo"; import { CandidatePicker } from 'components/ui/CandidatePicker';
import { ComingSoon } from "components/ui/ComingSoon"; import { JobPicker } from 'components/ui/JobPicker';
import { LoginRequired } from "components/ui/LoginRequired"; import { JobCreator } from 'components/JobCreator';
import { Scrollable } from "components/Scrollable"; import { LoginRestricted } from 'components/ui/LoginRestricted';
import { CandidatePicker } from "components/ui/CandidatePicker"; import JsonView from '@uiw/react-json-view';
import { JobPicker } from "components/ui/JobPicker"; import { ResumeGenerator } from 'components/ResumeGenerator';
import { JobCreator } from "components/JobCreator"; import { JobInfo } from 'components/ui/JobInfo';
import { LoginRestricted } from "components/ui/LoginRestricted";
import JsonView from "@uiw/react-json-view";
import { ResumeGenerator } from "components/ResumeGenerator";
import { JobInfo } from "components/ui/JobInfo";
function WorkAddIcon() { function WorkAddIcon() {
return ( return (
@ -48,19 +44,19 @@ function WorkAddIcon() {
position="relative" position="relative"
display="inline-flex" display="inline-flex"
sx={{ sx={{
lineHeight: "30px", lineHeight: '30px',
mb: "6px", mb: '6px',
}} }}
> >
<WorkOutline sx={{ fontSize: 24 }} /> <WorkOutline sx={{ fontSize: 24 }} />
<Add <Add
sx={{ sx={{
position: "absolute", position: 'absolute',
bottom: -2, bottom: -2,
right: -2, right: -2,
fontSize: 14, fontSize: 14,
bgcolor: "background.paper", bgcolor: 'background.paper',
borderRadius: "50%", borderRadius: '50%',
boxShadow: 1, boxShadow: 1,
}} }}
color="primary" color="primary"
@ -93,20 +89,20 @@ const initialState: AnalysisState = {
// Steps in our process // Steps in our process
const steps: Step[] = [ const steps: Step[] = [
{ requiredState: [], title: "Job Selection", icon: <WorkIcon /> }, { requiredState: [], title: 'Job Selection', icon: <WorkIcon /> },
{ requiredState: ["job"], title: "Select Candidate", icon: <PersonIcon /> }, { requiredState: ['job'], title: 'Select Candidate', icon: <PersonIcon /> },
{ {
requiredState: ["job", "candidate"], requiredState: ['job', 'candidate'],
title: "Job Analysis", title: 'Job Analysis',
icon: <WorkIcon />, icon: <WorkIcon />,
}, },
{ {
requiredState: ["job", "candidate", "analysis"], requiredState: ['job', 'candidate', 'analysis'],
title: "Generated Resume", title: 'Generated Resume',
icon: <AssessmentIcon />, icon: <AssessmentIcon />,
}, },
].map((item, index) => { ].map((item, index) => {
return { ...item, index, label: item.title.toLowerCase().replace(/ /g, "-") }; return { ...item, index, label: item.title.toLowerCase().replace(/ /g, '-') };
}); });
const capitalize = (str: string) => { const capitalize = (str: string) => {
@ -114,9 +110,7 @@ const capitalize = (str: string) => {
}; };
// Main component // Main component
const JobAnalysisPage: React.FC<BackstoryPageProps> = ( const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
props: BackstoryPageProps
) => {
const theme = useTheme(); const theme = useTheme();
const { user, guest } = useAuth(); const { user, guest } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate(); const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
@ -124,22 +118,18 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
const [activeStep, setActiveStep] = useState<Step>(steps[0]); const [activeStep, setActiveStep] = useState<Step>(steps[0]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [jobTab, setJobTab] = useState<string>("select"); const [jobTab, setJobTab] = useState<string>('select');
const [analysisState, setAnalysisState] = useState<AnalysisState | null>( const [analysisState, setAnalysisState] = useState<AnalysisState | null>(null);
null
);
const [canAdvance, setCanAdvance] = useState<boolean>(false); const [canAdvance, setCanAdvance] = useState<boolean>(false);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const canAccessStep = useCallback( const canAccessStep = useCallback(
(step: Step) => { (step: Step) => {
if (!analysisState) { if (!analysisState) {
return; return;
} }
const missing = step.requiredState.find( const missing = step.requiredState.find(f => !(analysisState as any)[f]);
(f) => !(analysisState as any)[f]
);
return missing; return missing;
}, },
[analysisState] [analysisState]
@ -157,19 +147,13 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
}; };
setAnalysisState(analysis); setAnalysisState(analysis);
for (let i = steps.length - 1; i >= 0; i--) { for (let i = steps.length - 1; i >= 0; i--) {
const missing = steps[i].requiredState.find((f) => !(analysis as any)[f]); const missing = steps[i].requiredState.find(f => !(analysis as any)[f]);
if (!missing) { if (!missing) {
setActiveStep(steps[i]); setActiveStep(steps[i]);
return; return;
} }
} }
}, [ }, [analysisState, selectedCandidate, selectedJob, setActiveStep, canAccessStep]);
analysisState,
selectedCandidate,
selectedJob,
setActiveStep,
canAccessStep,
]);
useEffect(() => { useEffect(() => {
if (activeStep.index === steps.length - 1) { if (activeStep.index === steps.length - 1) {
@ -185,7 +169,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollTo({ scrollRef.current.scrollTo({
top: 0, top: 0,
behavior: "smooth", behavior: 'smooth',
}); });
} }
}, [setCanAdvance, analysisState, activeStep]); }, [setCanAdvance, analysisState, activeStep]);
@ -201,7 +185,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
} }
if (activeStep.index < steps.length - 1) { if (activeStep.index < steps.length - 1) {
setActiveStep((prevActiveStep) => steps[prevActiveStep.index + 1]); setActiveStep(prevActiveStep => steps[prevActiveStep.index + 1]);
} }
}; };
@ -210,7 +194,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
return; return;
} }
setActiveStep((prevActiveStep) => steps[prevActiveStep.index - 1]); setActiveStep(prevActiveStep => steps[prevActiveStep.index - 1]);
}; };
const moveToStep = (step: number) => { const moveToStep = (step: number) => {
@ -254,17 +238,17 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
// Render function for the job description step // Render function for the job description step
const renderJobDescription = () => { const renderJobDescription = () => {
return ( return (
<Box sx={{ mt: 3, width: "100%" }}> <Box sx={{ mt: 3, width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 3 }}> <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={jobTab} onChange={handleTabChange} centered> <Tabs value={jobTab} onChange={handleTabChange} centered>
<Tab value="select" icon={<WorkOutline />} label="Select Job" /> <Tab value="select" icon={<WorkOutline />} label="Select Job" />
<Tab value="create" icon={<WorkAddIcon />} label="Create Job" /> <Tab value="create" icon={<WorkAddIcon />} label="Create Job" />
</Tabs> </Tabs>
</Box> </Box>
{jobTab === "select" && <JobPicker onSelect={onJobSelect} />} {jobTab === 'select' && <JobPicker onSelect={onJobSelect} />}
{jobTab === "create" && user && <JobCreator onSave={onJobSelect} />} {jobTab === 'create' && user && <JobCreator onSave={onJobSelect} />}
{jobTab === "create" && guest && ( {jobTab === 'create' && guest && (
<LoginRestricted> <LoginRestricted>
<JobCreator onSave={onJobSelect} /> <JobCreator onSave={onJobSelect} />
</LoginRestricted> </LoginRestricted>
@ -312,11 +296,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
if (!analysisState) { if (!analysisState) {
return; return;
} }
if ( if (!analysisState.job || !analysisState.candidate || !analysisState.analysis) {
!analysisState.job ||
!analysisState.candidate ||
!analysisState.analysis
) {
return <></>; return <></>;
} }
@ -334,28 +314,24 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
height: "100%" /* Restrict to main-container's height */, height: '100%' /* Restrict to main-container's height */,
width: "100%", width: '100%',
minHeight: 0 /* Prevent flex overflow */, minHeight: 0 /* Prevent flex overflow */,
maxHeight: "min-content", maxHeight: 'min-content',
"& > *:not(.Scrollable)": { '& > *:not(.Scrollable)': {
flexShrink: 0 /* Prevent shrinking */, flexShrink: 0 /* Prevent shrinking */,
}, },
position: "relative", position: 'relative',
}} }}
> >
<Paper elevation={4} sx={{ m: 0, borderRadius: 0, mb: 1, p: 0, gap: 1 }}> <Paper elevation={4} sx={{ m: 0, borderRadius: 0, mb: 1, p: 0, gap: 1 }}>
<Stepper <Stepper activeStep={activeStep.index} alternativeLabel sx={{ mt: 2, mb: 2 }}>
activeStep={activeStep.index}
alternativeLabel
sx={{ mt: 2, mb: 2 }}
>
{steps.map((step, index) => ( {steps.map((step, index) => (
<Step> <Step>
<StepLabel <StepLabel
sx={{ cursor: "pointer" }} sx={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
moveToStep(index); moveToStep(index);
}} }}
@ -368,7 +344,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
activeStep.index >= step.index activeStep.index >= step.index
? theme.palette.primary.main ? theme.palette.primary.main
: theme.palette.grey[300], : theme.palette.grey[300],
color: "white", color: 'white',
}} }}
> >
{step.icon} {step.icon}
@ -381,18 +357,16 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
</Step> </Step>
))} ))}
</Stepper> </Stepper>
<Box <Box sx={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row' }}>
sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}
>
{analysisState && analysisState.job && ( {analysisState && analysisState.job && (
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}> <Box sx={{ display: 'flex', flexDirection: 'row', width: '100%' }}>
{!isMobile && ( {!isMobile && (
<Avatar <Avatar
sx={{ sx={{
ml: 1, ml: 1,
mt: 1, mt: 1,
bgcolor: theme.palette.primary.main, bgcolor: theme.palette.primary.main,
color: "white", color: 'white',
}} }}
> >
<WorkIcon /> <WorkIcon />
@ -401,21 +375,11 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
<JobInfo variant="minimal" job={analysisState.job} /> <JobInfo variant="minimal" job={analysisState.job} />
</Box> </Box>
)} )}
{isMobile && ( {isMobile && <Box sx={{ display: 'flex', borderBottom: '1px solid lightgrey' }} />}
<Box {!isMobile && <Box sx={{ display: 'flex', borderLeft: '1px solid lightgrey' }} />}
sx={{ display: "flex", borderBottom: "1px solid lightgrey" }}
/>
)}
{!isMobile && (
<Box sx={{ display: "flex", borderLeft: "1px solid lightgrey" }} />
)}
{analysisState && analysisState.candidate && ( {analysisState && analysisState.candidate && (
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}> <Box sx={{ display: 'flex', flexDirection: 'row', width: '100%' }}>
<CandidateInfo <CandidateInfo variant="minimal" candidate={analysisState.candidate} sx={{}} />
variant="minimal"
candidate={analysisState.candidate}
sx={{}}
/>
</Box> </Box>
)} )}
</Box> </Box>
@ -423,22 +387,22 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
<Scrollable <Scrollable
ref={scrollRef} ref={scrollRef}
sx={{ sx={{
position: "relative", position: 'relative',
maxHeight: "100%", maxHeight: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
flex: 1 /* Take remaining space in some-container */, flex: 1 /* Take remaining space in some-container */,
overflowY: "auto" /* Scroll if content overflows */, overflowY: 'auto' /* Scroll if content overflows */,
}} }}
> >
{activeStep.label === "job-selection" && renderJobDescription()} {activeStep.label === 'job-selection' && renderJobDescription()}
{activeStep.label === "select-candidate" && renderCandidateSelection()} {activeStep.label === 'select-candidate' && renderCandidateSelection()}
{activeStep.label === "job-analysis" && renderAnalysis()} {activeStep.label === 'job-analysis' && renderAnalysis()}
{activeStep.label === "generated-resume" && renderResume()} {activeStep.label === 'generated-resume' && renderResume()}
</Scrollable> </Scrollable>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Button <Button
color="inherit" color="inherit"
disabled={activeStep.index === steps[0].index} disabled={activeStep.index === steps[0].index}
@ -447,7 +411,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
> >
Back Back
</Button> </Button>
<Box sx={{ flex: "1 1 auto" }} /> <Box sx={{ flex: '1 1 auto' }} />
{activeStep.index === steps[steps.length - 1].index ? ( {activeStep.index === steps[steps.length - 1].index ? (
<Button <Button
@ -460,12 +424,8 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
Start New Analysis Start New Analysis
</Button> </Button>
) : ( ) : (
<Button <Button disabled={!canAdvance} onClick={handleNext} variant="contained">
disabled={!canAdvance} {activeStep.index === steps.length - 1 ? 'Done' : 'Next'}
onClick={handleNext}
variant="contained"
>
{activeStep.index === steps.length - 1 ? "Done" : "Next"}
</Button> </Button>
)} )}
</Box> </Box>
@ -475,13 +435,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
open={!!error} open={!!error}
autoHideDuration={6000} autoHideDuration={6000}
onClose={() => setError(null)} onClose={() => setError(null)}
anchorOrigin={{ vertical: "bottom", horizontal: "center" }} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
> >
<Alert <Alert onClose={() => setError(null)} severity="error" sx={{ width: '100%' }}>
onClose={() => setError(null)}
severity="error"
sx={{ width: "100%" }}
>
{error} {error}
</Alert> </Alert>
</Snackbar> </Snackbar>

View File

@ -1,15 +1,15 @@
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import { BackstoryPageProps } from "../components/BackstoryTab"; import { BackstoryPageProps } from '../components/BackstoryTab';
import { Message } from "../components/Message"; import { Message } from '../components/Message';
import { ChatMessage } from "types/types"; import { ChatMessage } from 'types/types';
const LoadingPage = (props: BackstoryPageProps) => { const LoadingPage = (props: BackstoryPageProps) => {
const preamble: ChatMessage = { const preamble: ChatMessage = {
role: "assistant", role: 'assistant',
type: "text", type: 'text',
status: "done", status: 'done',
sessionId: "", sessionId: '',
content: "Please wait while connecting to Backstory...", content: 'Please wait while connecting to Backstory...',
timestamp: new Date(), timestamp: new Date(),
metadata: null as any, metadata: null as any,
}; };
@ -17,10 +17,10 @@ const LoadingPage = (props: BackstoryPageProps) => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
maxWidth: "1024px", maxWidth: '1024px',
margin: "0 auto", margin: '0 auto',
}} }}
> >
<Message message={preamble} {...props} /> <Message message={preamble} {...props} />

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { import {
Box, Box,
Container, Container,
@ -11,36 +11,34 @@ import {
CardContent, CardContent,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from '@mui/material';
import { Person, PersonAdd } from "@mui/icons-material"; import { Person, PersonAdd } from '@mui/icons-material';
import "react-phone-number-input/style.css"; import 'react-phone-number-input/style.css';
import "./LoginPage.css"; import './LoginPage.css';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { BackstoryLogo } from "components/ui/BackstoryLogo"; import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import { BackstoryPageProps } from "components/BackstoryTab"; import { BackstoryPageProps } from 'components/BackstoryTab';
import { LoginForm } from "components/EmailVerificationComponents"; import { LoginForm } from 'components/EmailVerificationComponents';
import { CandidateRegistrationForm } from "pages/candidate/RegistrationForms"; import { CandidateRegistrationForm } from 'pages/candidate/RegistrationForms';
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from 'react-router-dom';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
import * as Types from "types/types"; import * as Types from 'types/types';
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => { const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const [tabValue, setTabValue] = useState<string>("login"); const [tabValue, setTabValue] = useState<string>('login');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
const { guest, user, login, isLoading, error } = useAuth(); const { guest, user, login, isLoading, error } = useAuth();
const name = const name =
user?.userType === "candidate" user?.userType === 'candidate' ? (user as Types.Candidate).username : user?.email || '';
? (user as Types.Candidate).username
: user?.email || "";
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const showGuest = false; const showGuest = false;
const { tab } = useParams(); const { tab } = useParams();
@ -50,10 +48,10 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
} }
if (loading && error) { if (loading && error) {
/* Remove 'HTTP .*: ' from error string */ /* Remove 'HTTP .*: ' from error string */
const jsonStr = error.replace(/^[^{]*/, ""); const jsonStr = error.replace(/^[^{]*/, '');
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
setErrorMessage(data.error.message); setErrorMessage(data.error.message);
setSnack(data.error.message, "error"); setSnack(data.error.message, 'error');
setTimeout(() => { setTimeout(() => {
setErrorMessage(null); setErrorMessage(null);
setLoading(false); setLoading(false);
@ -62,7 +60,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
}, [error, loading]); }, [error, loading]);
useEffect(() => { useEffect(() => {
if (tab === "register") { if (tab === 'register') {
setTabValue(tab); setTabValue(tab);
} }
}, [tab, setTabValue]); }, [tab, setTabValue]);
@ -74,7 +72,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
// If user is logged in, navigate to the profile page // If user is logged in, navigate to the profile page
if (user) { if (user) {
navigate("/candidate/profile"); navigate('/candidate/profile');
return <></>; return <></>;
} }
@ -83,7 +81,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
<BackstoryLogo /> <BackstoryLogo />
{showGuest && guest && ( {showGuest && guest && (
<Card sx={{ mb: 3, bgcolor: "grey.50" }} elevation={1}> <Card sx={{ mb: 3, bgcolor: 'grey.50' }} elevation={1}>
<CardContent> <CardContent>
<Typography variant="h6" gutterBottom color="primary"> <Typography variant="h6" gutterBottom color="primary">
Guest Session Active Guest Session Active
@ -98,7 +96,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
</Card> </Card>
)} )}
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 2 }}> <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
<Tabs value={tabValue} onChange={handleTabChange} centered> <Tabs value={tabValue} onChange={handleTabChange} centered>
<Tab value="login" icon={<Person />} label="Login" /> <Tab value="login" icon={<Person />} label="Login" />
<Tab value="register" icon={<PersonAdd />} label="Register" /> <Tab value="register" icon={<PersonAdd />} label="Register" />
@ -117,9 +115,9 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
</Alert> </Alert>
)} )}
{tabValue === "login" && <LoginForm />} {tabValue === 'login' && <LoginForm />}
{tabValue === "register" && <CandidateRegistrationForm />} {tabValue === 'register' && <CandidateRegistrationForm />}
</Paper> </Paper>
); );
}; };

View File

@ -1,27 +1,27 @@
import React, { forwardRef, useEffect, useState } from "react"; import React, { forwardRef, useEffect, useState } from 'react';
import useMediaQuery from "@mui/material/useMediaQuery"; import useMediaQuery from '@mui/material/useMediaQuery';
import Box from "@mui/material/Box"; import Box from '@mui/material/Box';
import { useTheme } from "@mui/material/styles"; import { useTheme } from '@mui/material/styles';
import MuiMarkdown from "mui-markdown"; import MuiMarkdown from 'mui-markdown';
import { BackstoryPageProps } from "../components/BackstoryTab"; import { BackstoryPageProps } from '../components/BackstoryTab';
import { Conversation, ConversationHandle } from "../components/Conversation"; import { Conversation, ConversationHandle } from '../components/Conversation';
import { BackstoryQuery } from "../components/BackstoryQuery"; import { BackstoryQuery } from '../components/BackstoryQuery';
import { CandidateInfo } from "components/ui/CandidateInfo"; import { CandidateInfo } from 'components/ui/CandidateInfo';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { Candidate } from "types/types"; import { Candidate } from 'types/types';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
import * as Types from "types/types"; import * as Types from 'types/types';
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>( const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
(props: BackstoryPageProps, ref) => { (props: BackstoryPageProps, ref) => {
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const { user } = useAuth(); const { user } = useAuth();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [questions, setQuestions] = useState<React.ReactElement[]>([]); const [questions, setQuestions] = useState<React.ReactElement[]>([]);
const candidate: Candidate | null = const candidate: Candidate | null =
user?.userType === "candidate" ? (user as Types.Candidate) : null; user?.userType === 'candidate' ? (user as Types.Candidate) : null;
// console.log("ChatPage candidate =>", candidate); // console.log("ChatPage candidate =>", candidate);
useEffect(() => { useEffect(() => {
@ -30,9 +30,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
} }
setQuestions([ setQuestions([
<Box <Box sx={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row' }}>
sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}
>
{candidate.questions?.map((q, i: number) => ( {candidate.questions?.map((q, i: number) => (
<BackstoryQuery key={i} question={q} /> <BackstoryQuery key={i} question={q} />
))} ))}
@ -50,17 +48,14 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
} }
return ( return (
<Box> <Box>
<CandidateInfo <CandidateInfo candidate={candidate} action="Chat with Backstory AI about " />
candidate={candidate}
action="Chat with Backstory AI about "
/>
<Conversation <Conversation
ref={ref} ref={ref}
{...{ {...{
multiline: true, multiline: true,
type: "chat", type: 'chat',
placeholder: `What would you like to know about ${candidate?.firstName}?`, placeholder: `What would you like to know about ${candidate?.firstName}?`,
resetLabel: "chat", resetLabel: 'chat',
defaultPrompts: questions, defaultPrompts: questions,
}} }}
/> />

View File

@ -1,18 +1,16 @@
import React from "react"; import React from 'react';
import { VectorVisualizer } from "../components/VectorVisualizer"; import { VectorVisualizer } from '../components/VectorVisualizer';
import { BackstoryPageProps } from "../components/BackstoryTab"; import { BackstoryPageProps } from '../components/BackstoryTab';
import "./VectorVisualizerPage.css"; import './VectorVisualizerPage.css';
interface VectorVisualizerProps extends BackstoryPageProps { interface VectorVisualizerProps extends BackstoryPageProps {
inline?: boolean; inline?: boolean;
rag?: any; rag?: any;
} }
const VectorVisualizerPage: React.FC<VectorVisualizerProps> = ( const VectorVisualizerPage: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
props: VectorVisualizerProps
) => {
return <VectorVisualizer inline={false} {...props} />; return <VectorVisualizer inline={false} {...props} />;
}; };

View File

@ -1,13 +1,5 @@
import React from "react"; import React from 'react';
import { import { Box, Card, CardContent, Typography, Button, LinearProgress, Stack } from '@mui/material';
Box,
Card,
CardContent,
Typography,
Button,
LinearProgress,
Stack,
} from "@mui/material";
import { import {
Add as AddIcon, Add as AddIcon,
Visibility as VisibilityIcon, Visibility as VisibilityIcon,
@ -15,13 +7,13 @@ import {
ContactMail as ContactMailIcon, ContactMail as ContactMailIcon,
Edit as EditIcon, Edit as EditIcon,
TipsAndUpdates as TipsIcon, TipsAndUpdates as TipsIcon,
} from "@mui/icons-material"; } from '@mui/icons-material';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { LoginRequired } from "components/ui/LoginRequired"; import { LoginRequired } from 'components/ui/LoginRequired';
import { BackstoryElementProps } from "components/BackstoryTab"; import { BackstoryElementProps } from 'components/BackstoryTab';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { ComingSoon } from "components/ui/ComingSoon"; import { ComingSoon } from 'components/ui/ComingSoon';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
type CandidateDashboardProps = BackstoryElementProps; type CandidateDashboardProps = BackstoryElementProps;
@ -35,12 +27,12 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
return <LoginRequired asset="candidate dashboard" />; return <LoginRequired asset="candidate dashboard" />;
} }
if (user?.userType !== "candidate") { if (user?.userType !== 'candidate') {
setSnack( setSnack(
`The page you were on is only available for candidates (you are a ${user.userType}`, `The page you were on is only available for candidates (you are a ${user.userType}`,
"warning" 'warning'
); );
navigate("/"); navigate('/');
return <></>; return <></>;
} }
@ -51,7 +43,7 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
<Box sx={{ flex: 1, p: 3 }}> <Box sx={{ flex: 1, p: 3 }}>
{/* Welcome Section */} {/* Welcome Section */}
<Box sx={{ mb: 4 }}> <Box sx={{ mb: 4 }}>
<Typography variant="h4" sx={{ mb: 2, fontWeight: "bold" }}> <Typography variant="h4" sx={{ mb: 2, fontWeight: 'bold' }}>
Welcome back, {user.firstName}! Welcome back, {user.firstName}!
</Typography> </Typography>
@ -65,9 +57,9 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
sx={{ sx={{
height: 8, height: 8,
borderRadius: 4, borderRadius: 4,
backgroundColor: "#e0e0e0", backgroundColor: '#e0e0e0',
"& .MuiLinearProgress-bar": { '& .MuiLinearProgress-bar': {
backgroundColor: "#4caf50", backgroundColor: '#4caf50',
}, },
}} }}
/> />
@ -77,9 +69,9 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
variant="contained" variant="contained"
color="primary" color="primary"
sx={{ mt: 1 }} sx={{ mt: 1 }}
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
navigate("/candidate/profile"); navigate('/candidate/profile');
}} }}
> >
Complete Your Profile Complete Your Profile
@ -87,21 +79,21 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
</Box> </Box>
{/* Cards Grid */} {/* Cards Grid */}
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Top Row */} {/* Top Row */}
<Box sx={{ display: "flex", gap: 3 }}> <Box sx={{ display: 'flex', gap: 3 }}>
{/* Resume Builder Card */} {/* Resume Builder Card */}
<Card sx={{ flex: 1, minHeight: 200 }}> <Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent> <CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: "bold" }}> <Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Resume Builder Resume Builder
</Typography> </Typography>
<Typography variant="body2" sx={{ mb: 1, color: "#666" }}> <Typography variant="body2" sx={{ mb: 1, color: '#666' }}>
3 custom resumes 3 custom resumes
</Typography> </Typography>
<Typography variant="body2" sx={{ mb: 3, color: "#666" }}> <Typography variant="body2" sx={{ mb: 3, color: '#666' }}>
Last created: May 15, 2025 Last created: May 15, 2025
</Typography> </Typography>
@ -114,25 +106,23 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
{/* Recent Activity Card */} {/* Recent Activity Card */}
<Card sx={{ flex: 1, minHeight: 200 }}> <Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent> <CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: "bold" }}> <Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Recent Activity Recent Activity
</Typography> </Typography>
<Stack spacing={1} sx={{ mb: 3 }}> <Stack spacing={1} sx={{ mb: 3 }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<VisibilityIcon sx={{ fontSize: 16, color: "#666" }} /> <VisibilityIcon sx={{ fontSize: 16, color: '#666' }} />
<Typography variant="body2">5 profile views</Typography> <Typography variant="body2">5 profile views</Typography>
</Box> </Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<DownloadIcon sx={{ fontSize: 16, color: "#666" }} /> <DownloadIcon sx={{ fontSize: 16, color: '#666' }} />
<Typography variant="body2"> <Typography variant="body2">2 resume downloads</Typography>
2 resume downloads
</Typography>
</Box> </Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ContactMailIcon sx={{ fontSize: 16, color: "#666" }} /> <ContactMailIcon sx={{ fontSize: 16, color: '#666' }} />
<Typography variant="body2">1 direct contact</Typography> <Typography variant="body2">1 direct contact</Typography>
</Box> </Box>
</Stack> </Stack>
@ -145,31 +135,22 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
</Box> </Box>
{/* Bottom Row */} {/* Bottom Row */}
<Box sx={{ display: "flex", gap: 3 }}> <Box sx={{ display: 'flex', gap: 3 }}>
{/* Complete Your Backstory Card */} {/* Complete Your Backstory Card */}
<Card sx={{ flex: 1, minHeight: 200 }}> <Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent> <CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: "bold" }}> <Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Complete Your Backstory Complete Your Backstory
</Typography> </Typography>
<Stack spacing={1} sx={{ mb: 3 }}> <Stack spacing={1} sx={{ mb: 3 }}>
<Typography <Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
variant="body2"
sx={{ display: "flex", alignItems: "center" }}
>
Add projects Add projects
</Typography> </Typography>
<Typography <Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
variant="body2"
sx={{ display: "flex", alignItems: "center" }}
>
Detail skills Detail skills
</Typography> </Typography>
<Typography <Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
variant="body2"
sx={{ display: "flex", alignItems: "center" }}
>
Work history Work history
</Typography> </Typography>
</Stack> </Stack>
@ -183,21 +164,15 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
{/* Improvement Suggestions Card */} {/* Improvement Suggestions Card */}
<Card sx={{ flex: 1, minHeight: 200 }}> <Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent> <CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: "bold" }}> <Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Improvement Suggestions Improvement Suggestions
</Typography> </Typography>
<Stack spacing={1} sx={{ mb: 3 }}> <Stack spacing={1} sx={{ mb: 3 }}>
<Typography <Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
variant="body2"
sx={{ display: "flex", alignItems: "center" }}
>
Add certifications Add certifications
</Typography> </Typography>
<Typography <Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
variant="body2"
sx={{ display: "flex", alignItems: "center" }}
>
Enhance your project details Enhance your project details
</Typography> </Typography>
</Stack> </Stack>

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { import {
Box, Box,
Button, Button,
@ -16,10 +16,10 @@ import {
CircularProgress, CircularProgress,
Snackbar, Snackbar,
Alert, Alert,
} from "@mui/material"; } from '@mui/material';
import { styled } from "@mui/material/styles"; import { styled } from '@mui/material/styles';
import { CloudUpload, PhotoCamera } from "@mui/icons-material"; import { CloudUpload, PhotoCamera } from '@mui/icons-material';
import { useTheme } from "@mui/material/styles"; import { useTheme } from '@mui/material/styles';
// import { Beta } from '../components/Beta'; // import { Beta } from '../components/Beta';
// Interfaces // Interfaces
@ -34,21 +34,21 @@ interface ProfileFormData {
} }
// Styled components // Styled components
const VisuallyHiddenInput = styled("input")({ const VisuallyHiddenInput = styled('input')({
clip: "rect(0 0 0 0)", clip: 'rect(0 0 0 0)',
clipPath: "inset(50%)", clipPath: 'inset(50%)',
height: 1, height: 1,
overflow: "hidden", overflow: 'hidden',
position: "absolute", position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
whiteSpace: "nowrap", whiteSpace: 'nowrap',
width: 1, width: 1,
}); });
const CreateProfilePage: React.FC = () => { const CreateProfilePage: React.FC = () => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
// State management // State management
const [activeStep, setActiveStep] = useState<number>(0); const [activeStep, setActiveStep] = useState<number>(0);
@ -58,29 +58,25 @@ const CreateProfilePage: React.FC = () => {
const [snackbar, setSnackbar] = useState<{ const [snackbar, setSnackbar] = useState<{
open: boolean; open: boolean;
message: string; message: string;
severity: "success" | "error"; severity: 'success' | 'error';
}>({ }>({
open: false, open: false,
message: "", message: '',
severity: "success", severity: 'success',
}); });
const [formData, setFormData] = useState<ProfileFormData>({ const [formData, setFormData] = useState<ProfileFormData>({
firstName: "", firstName: '',
lastName: "", lastName: '',
email: "", email: '',
phoneNumber: "", phoneNumber: '',
jobTitle: "", jobTitle: '',
location: "", location: '',
bio: "", bio: '',
}); });
// Steps for the profile creation process // Steps for the profile creation process
const steps = [ const steps = ['Personal Information', 'Professional Details', 'Resume Upload'];
"Personal Information",
"Professional Details",
"Resume Upload",
];
// Handle form input changes // Handle form input changes
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -95,7 +91,7 @@ const CreateProfilePage: React.FC = () => {
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) { if (e.target.files && e.target.files[0]) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (event) => { reader.onload = event => {
if (event.target?.result) { if (event.target?.result) {
setProfileImage(event.target.result.toString()); setProfileImage(event.target.result.toString());
} }
@ -111,7 +107,7 @@ const CreateProfilePage: React.FC = () => {
setSnackbar({ setSnackbar({
open: true, open: true,
message: `Resume uploaded: ${e.target.files[0].name}`, message: `Resume uploaded: ${e.target.files[0].name}`,
severity: "success", severity: 'success',
}); });
} }
}; };
@ -121,12 +117,12 @@ const CreateProfilePage: React.FC = () => {
if (activeStep === steps.length - 1) { if (activeStep === steps.length - 1) {
handleSubmit(); handleSubmit();
} else { } else {
setActiveStep((prevStep) => prevStep + 1); setActiveStep(prevStep => prevStep + 1);
} }
}; };
const handleBack = () => { const handleBack = () => {
setActiveStep((prevStep) => prevStep - 1); setActiveStep(prevStep => prevStep - 1);
}; };
// Form submission // Form submission
@ -138,8 +134,8 @@ const CreateProfilePage: React.FC = () => {
setLoading(false); setLoading(false);
setSnackbar({ setSnackbar({
open: true, open: true,
message: "Profile created successfully! Redirecting to dashboard...", message: 'Profile created successfully! Redirecting to dashboard...',
severity: "success", severity: 'success',
}); });
// Redirect would happen here in a real application // Redirect would happen here in a real application
@ -152,12 +148,12 @@ const CreateProfilePage: React.FC = () => {
switch (activeStep) { switch (activeStep) {
case 0: case 0:
return ( return (
formData.firstName.trim() !== "" && formData.firstName.trim() !== '' &&
formData.lastName.trim() !== "" && formData.lastName.trim() !== '' &&
formData.email.trim() !== "" formData.email.trim() !== ''
); );
case 1: case 1:
return formData.jobTitle.trim() !== ""; return formData.jobTitle.trim() !== '';
case 2: case 2:
return resumeFile !== null; return resumeFile !== null;
default: default:
@ -171,16 +167,16 @@ const CreateProfilePage: React.FC = () => {
case 0: case 0:
return ( return (
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid size={{ xs: 12 }} sx={{ textAlign: "center", mb: 2 }}> <Grid size={{ xs: 12 }} sx={{ textAlign: 'center', mb: 2 }}>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Avatar <Avatar
src={profileImage || ""} src={profileImage || ''}
sx={{ sx={{
width: 120, width: 120,
height: 120, height: 120,
@ -188,17 +184,9 @@ const CreateProfilePage: React.FC = () => {
border: `2px solid ${theme.palette.primary.main}`, border: `2px solid ${theme.palette.primary.main}`,
}} }}
/> />
<IconButton <IconButton color="primary" aria-label="upload picture" component="label">
color="primary"
aria-label="upload picture"
component="label"
>
<PhotoCamera /> <PhotoCamera />
<VisuallyHiddenInput <VisuallyHiddenInput type="file" accept="image/*" onChange={handleImageUpload} />
type="file"
accept="image/*"
onChange={handleImageUpload}
/>
</IconButton> </IconButton>
<Typography variant="caption" color="textSecondary"> <Typography variant="caption" color="textSecondary">
Add profile photo Add profile photo
@ -296,11 +284,10 @@ const CreateProfilePage: React.FC = () => {
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<Typography variant="body1" component="p"> <Typography variant="body1" component="p">
Upload your resume to complete your profile. We'll analyze it to Upload your resume to complete your profile. We'll analyze it to better understand
better understand your skills and experience. (Supported your skills and experience. (Supported formats: .pdf, .docx, .md, and .txt)
formats: .pdf, .docx, .md, and .txt)
</Typography> </Typography>
<Box sx={{ textAlign: "center", mt: 2 }}> <Box sx={{ textAlign: 'center', mt: 2 }}>
<Button <Button
component="label" component="label"
variant="contained" variant="contained"
@ -316,11 +303,7 @@ const CreateProfilePage: React.FC = () => {
</Button> </Button>
{resumeFile && ( {resumeFile && (
<Typography <Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
variant="body2"
color="textSecondary"
sx={{ mt: 1 }}
>
File uploaded: {resumeFile.name} File uploaded: {resumeFile.name}
</Typography> </Typography>
)} )}
@ -329,7 +312,7 @@ const CreateProfilePage: React.FC = () => {
</Grid> </Grid>
); );
default: default:
return "Unknown step"; return 'Unknown step';
} }
}; };
@ -350,10 +333,10 @@ const CreateProfilePage: React.FC = () => {
<Stepper <Stepper
activeStep={activeStep} activeStep={activeStep}
alternativeLabel={!isMobile} alternativeLabel={!isMobile}
orientation={isMobile ? "vertical" : "horizontal"} orientation={isMobile ? 'vertical' : 'horizontal'}
sx={{ mt: 3, mb: 5 }} sx={{ mt: 3, mb: 5 }}
> >
{steps.map((label) => ( {steps.map(label => (
<Step key={label}> <Step key={label}>
<StepLabel>{label}</StepLabel> <StepLabel>{label}</StepLabel>
</Step> </Step>
@ -362,23 +345,17 @@ const CreateProfilePage: React.FC = () => {
<Box sx={{ mt: 2, mb: 4 }}>{getStepContent(activeStep)}</Box> <Box sx={{ mt: 2, mb: 4 }}>{getStepContent(activeStep)}</Box>
<Box sx={{ display: "flex", justifyContent: "space-between", mt: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 4 }}>
<Button <Button disabled={activeStep === 0} onClick={handleBack} variant="outlined">
disabled={activeStep === 0}
onClick={handleBack}
variant="outlined"
>
Back Back
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
onClick={handleNext} onClick={handleNext}
disabled={!isStepValid() || loading} disabled={!isStepValid() || loading}
startIcon={ startIcon={loading ? <CircularProgress size={20} color="inherit" /> : null}
loading ? <CircularProgress size={20} color="inherit" /> : null
}
> >
{activeStep === steps.length - 1 ? "Create Profile" : "Next"} {activeStep === steps.length - 1 ? 'Create Profile' : 'Next'}
</Button> </Button>
</Box> </Box>
</Paper> </Paper>
@ -391,7 +368,7 @@ const CreateProfilePage: React.FC = () => {
<Alert <Alert
onClose={() => setSnackbar({ ...snackbar, open: false })} onClose={() => setSnackbar({ ...snackbar, open: false })}
severity={snackbar.severity} severity={snackbar.severity}
sx={{ width: "100%" }} sx={{ width: '100%' }}
> >
{snackbar.message} {snackbar.message}
</Alert> </Alert>

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { import {
Paper, Paper,
Box, Box,
@ -22,28 +22,28 @@ import {
useTheme, useTheme,
IconButton, IconButton,
InputAdornment, InputAdornment,
} from "@mui/material"; } from '@mui/material';
import { Visibility, VisibilityOff } from "@mui/icons-material"; import { Visibility, VisibilityOff } from '@mui/icons-material';
import { ApiClient } from "services/api-client"; import { ApiClient } from 'services/api-client';
import { RegistrationSuccessDialog } from "components/EmailVerificationComponents"; import { RegistrationSuccessDialog } from 'components/EmailVerificationComponents';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
// Candidate Registration Form // Candidate Registration Form
const CandidateRegistrationForm = () => { const CandidateRegistrationForm = () => {
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
email: "", email: '',
username: "", username: '',
password: "", password: '',
confirmPassword: "", confirmPassword: '',
firstName: "", firstName: '',
lastName: "", lastName: '',
phone: "", phone: '',
}); });
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
@ -60,50 +60,46 @@ const CandidateRegistrationForm = () => {
// Email validation // Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email) { if (!formData.email) {
newErrors.email = "Email is required"; newErrors.email = 'Email is required';
} else if (!emailRegex.test(formData.email)) { } else if (!emailRegex.test(formData.email)) {
newErrors.email = "Please enter a valid email address"; newErrors.email = 'Please enter a valid email address';
} }
// Username validation // Username validation
if (!formData.username) { if (!formData.username) {
newErrors.username = "Username is required"; newErrors.username = 'Username is required';
} else if (formData.username.length < 3) { } else if (formData.username.length < 3) {
newErrors.username = "Username must be at least 3 characters"; newErrors.username = 'Username must be at least 3 characters';
} else if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) { } else if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) {
newErrors.username = newErrors.username = 'Username can only contain letters, numbers, and underscores';
"Username can only contain letters, numbers, and underscores";
} }
// Password validation // Password validation
if (!formData.password) { if (!formData.password) {
newErrors.password = "Password is required"; newErrors.password = 'Password is required';
} else { } else {
const passwordErrors = validatePassword(formData.password); const passwordErrors = validatePassword(formData.password);
if (passwordErrors.length > 0) { if (passwordErrors.length > 0) {
newErrors.password = passwordErrors.join(", "); newErrors.password = passwordErrors.join(', ');
} }
} }
// Confirm password // Confirm password
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = "Passwords do not match"; newErrors.confirmPassword = 'Passwords do not match';
} }
// Name validation // Name validation
if (!formData.firstName.trim()) { if (!formData.firstName.trim()) {
newErrors.firstName = "First name is required"; newErrors.firstName = 'First name is required';
} }
if (!formData.lastName.trim()) { if (!formData.lastName.trim()) {
newErrors.lastName = "Last name is required"; newErrors.lastName = 'Last name is required';
} }
// Phone validation (optional but must be valid if provided) // Phone validation (optional but must be valid if provided)
if ( if (formData.phone && !/^[+]?[1-9][\d]{0,15}$/.test(formData.phone.replace(/\s/g, ''))) {
formData.phone && newErrors.phone = 'Please enter a valid phone number';
!/^[+]?[1-9][\d]{0,15}$/.test(formData.phone.replace(/\s/g, ""))
) {
newErrors.phone = "Please enter a valid phone number";
} }
setErrors(newErrors); setErrors(newErrors);
@ -114,32 +110,30 @@ const CandidateRegistrationForm = () => {
const errors: string[] = []; const errors: string[] = [];
if (password.length < 8) { if (password.length < 8) {
errors.push("at least 8 characters"); errors.push('at least 8 characters');
} }
if (!/[a-z]/.test(password)) { if (!/[a-z]/.test(password)) {
errors.push("one lowercase letter"); errors.push('one lowercase letter');
} }
if (!/[A-Z]/.test(password)) { if (!/[A-Z]/.test(password)) {
errors.push("one uppercase letter"); errors.push('one uppercase letter');
} }
if (!/\d/.test(password)) { if (!/\d/.test(password)) {
errors.push("one number"); errors.push('one number');
} }
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push("one special character"); errors.push('one special character');
} }
return errors.length > 0 return errors.length > 0 ? [`Password must contain ${errors.join(', ')}`] : [];
? [`Password must contain ${errors.join(", ")}`]
: [];
}; };
const handleInputChange = (field: string, value: string) => { const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value })); setFormData(prev => ({ ...prev, [field]: value }));
// Clear error when user starts typing // Clear error when user starts typing
if (errors[field]) { if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" })); setErrors(prev => ({ ...prev, [field]: '' }));
} }
}; };
@ -165,15 +159,15 @@ const CandidateRegistrationForm = () => {
setRegistrationResult(result); setRegistrationResult(result);
setShowSuccess(true); setShowSuccess(true);
} catch (error: any) { } catch (error: any) {
if (error.message.includes("already exists")) { if (error.message.includes('already exists')) {
if (error.message.includes("email")) { if (error.message.includes('email')) {
setErrors({ email: "An account with this email already exists" }); setErrors({ email: 'An account with this email already exists' });
} else if (error.message.includes("username")) { } else if (error.message.includes('username')) {
setErrors({ username: "This username is already taken" }); setErrors({ username: 'This username is already taken' });
} }
} else { } else {
setErrors({ setErrors({
general: error.message || "Registration failed. Please try again.", general: error.message || 'Registration failed. Please try again.',
}); });
} }
} finally { } finally {
@ -192,18 +186,16 @@ const CandidateRegistrationForm = () => {
const strength = validations.filter(Boolean).length; const strength = validations.filter(Boolean).length;
if (strength < 2) return { level: "weak", color: "error", value: 20 }; if (strength < 2) return { level: 'weak', color: 'error', value: 20 };
if (strength < 4) return { level: "medium", color: "warning", value: 60 }; if (strength < 4) return { level: 'medium', color: 'warning', value: 60 };
return { level: "strong", color: "success", value: 100 }; return { level: 'strong', color: 'success', value: 100 };
}; };
const passwordStrength = formData.password const passwordStrength = formData.password ? getPasswordStrength(formData.password) : null;
? getPasswordStrength(formData.password)
: null;
return ( return (
<Box sx={{ p: isMobile ? 1 : 5 }}> <Box sx={{ p: isMobile ? 1 : 5 }}>
<Box sx={{ textAlign: "center", mb: 4 }}> <Box sx={{ textAlign: 'center', mb: 4 }}>
<Typography variant="h4" component="h1" sx={{ mb: 1 }}> <Typography variant="h4" component="h1" sx={{ mb: 1 }}>
Join as a Candidate Join as a Candidate
</Typography> </Typography>
@ -218,7 +210,7 @@ const CandidateRegistrationForm = () => {
label="Email Address" label="Email Address"
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)} onChange={e => handleInputChange('email', e.target.value)}
placeholder="your.email@example.com" placeholder="your.email@example.com"
error={!!errors.email} error={!!errors.email}
helperText={errors.email} helperText={errors.email}
@ -229,9 +221,7 @@ const CandidateRegistrationForm = () => {
fullWidth fullWidth
label="Username" label="Username"
value={formData.username} value={formData.username}
onChange={(e) => onChange={e => handleInputChange('username', e.target.value.toLowerCase())}
handleInputChange("username", e.target.value.toLowerCase())
}
placeholder="johndoe123" placeholder="johndoe123"
error={!!errors.username} error={!!errors.username}
helperText={errors.username} helperText={errors.username}
@ -243,7 +233,7 @@ const CandidateRegistrationForm = () => {
fullWidth fullWidth
label="First Name" label="First Name"
value={formData.firstName} value={formData.firstName}
onChange={(e) => handleInputChange("firstName", e.target.value)} onChange={e => handleInputChange('firstName', e.target.value)}
placeholder="John" placeholder="John"
error={!!errors.firstName} error={!!errors.firstName}
helperText={errors.firstName} helperText={errors.firstName}
@ -253,7 +243,7 @@ const CandidateRegistrationForm = () => {
fullWidth fullWidth
label="Last Name" label="Last Name"
value={formData.lastName} value={formData.lastName}
onChange={(e) => handleInputChange("lastName", e.target.value)} onChange={e => handleInputChange('lastName', e.target.value)}
placeholder="Doe" placeholder="Doe"
error={!!errors.lastName} error={!!errors.lastName}
helperText={errors.lastName} helperText={errors.lastName}
@ -266,19 +256,19 @@ const CandidateRegistrationForm = () => {
label="Phone Number" label="Phone Number"
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)} onChange={e => handleInputChange('phone', e.target.value)}
placeholder="+1 (555) 123-4567" placeholder="+1 (555) 123-4567"
error={!!errors.phone} error={!!errors.phone}
helperText={errors.phone || "Optional"} helperText={errors.phone || 'Optional'}
/> />
<Box> <Box>
<TextField <TextField
fullWidth fullWidth
label="Password" label="Password"
type={showPassword ? "text" : "password"} type={showPassword ? 'text' : 'password'}
value={formData.password} value={formData.password}
onChange={(e) => handleInputChange("password", e.target.value)} onChange={e => handleInputChange('password', e.target.value)}
placeholder="Create a strong password" placeholder="Create a strong password"
error={!!errors.password} error={!!errors.password}
helperText={errors.password} helperText={errors.password}
@ -289,7 +279,7 @@ const CandidateRegistrationForm = () => {
<IconButton <IconButton
aria-label="toggle password visibility" aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
onMouseDown={(e) => e.preventDefault()} onMouseDown={e => e.preventDefault()}
edge="end" edge="end"
> >
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
@ -309,7 +299,7 @@ const CandidateRegistrationForm = () => {
<Typography <Typography
variant="caption" variant="caption"
color={`${passwordStrength.color}.main`} color={`${passwordStrength.color}.main`}
sx={{ mt: 0.5, display: "block", textTransform: "capitalize" }} sx={{ mt: 0.5, display: 'block', textTransform: 'capitalize' }}
> >
Password strength: {passwordStrength.level} Password strength: {passwordStrength.level}
</Typography> </Typography>
@ -320,9 +310,9 @@ const CandidateRegistrationForm = () => {
<TextField <TextField
fullWidth fullWidth
label="Confirm Password" label="Confirm Password"
type={showConfirmPassword ? "text" : "password"} type={showConfirmPassword ? 'text' : 'password'}
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={(e) => handleInputChange("confirmPassword", e.target.value)} onChange={e => handleInputChange('confirmPassword', e.target.value)}
placeholder="Confirm your password" placeholder="Confirm your password"
error={!!errors.confirmPassword} error={!!errors.confirmPassword}
helperText={errors.confirmPassword} helperText={errors.confirmPassword}
@ -333,7 +323,7 @@ const CandidateRegistrationForm = () => {
<IconButton <IconButton
aria-label="toggle confirm password visibility" aria-label="toggle confirm password visibility"
onClick={() => setShowConfirmPassword(!showConfirmPassword)} onClick={() => setShowConfirmPassword(!showConfirmPassword)}
onMouseDown={(e) => e.preventDefault()} onMouseDown={e => e.preventDefault()}
edge="end" edge="end"
> >
{showConfirmPassword ? <VisibilityOff /> : <Visibility />} {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
@ -359,18 +349,18 @@ const CandidateRegistrationForm = () => {
<Typography>Creating Account...</Typography> <Typography>Creating Account...</Typography>
</Stack> </Stack>
) : ( ) : (
"Create Account" 'Create Account'
)} )}
</Button> </Button>
<Box sx={{ textAlign: "center" }}> <Box sx={{ textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Already have an account?{" "} Already have an account?{' '}
<Link <Link
component="button" component="button"
onClick={(e) => { onClick={e => {
e.preventDefault(); e.preventDefault();
navigate("/login"); navigate('/login');
}} }}
sx={{ fontWeight: 600 }} sx={{ fontWeight: 600 }}
> >
@ -394,16 +384,16 @@ const CandidateRegistrationForm = () => {
// Employer Registration Form // Employer Registration Form
const EmployerRegistrationForm = () => { const EmployerRegistrationForm = () => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
email: "", email: '',
username: "", username: '',
password: "", password: '',
confirmPassword: "", confirmPassword: '',
companyName: "", companyName: '',
industry: "", industry: '',
companySize: "", companySize: '',
companyDescription: "", companyDescription: '',
websiteUrl: "", websiteUrl: '',
phone: "", phone: '',
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -418,26 +408,26 @@ const EmployerRegistrationForm = () => {
const apiClient = new ApiClient(); const apiClient = new ApiClient();
const industryOptions = [ const industryOptions = [
"Technology", 'Technology',
"Healthcare", 'Healthcare',
"Finance", 'Finance',
"Education", 'Education',
"Manufacturing", 'Manufacturing',
"Retail", 'Retail',
"Consulting", 'Consulting',
"Media", 'Media',
"Non-profit", 'Non-profit',
"Government", 'Government',
"Other", 'Other',
]; ];
const companySizeOptions = [ const companySizeOptions = [
"1-10 employees", '1-10 employees',
"11-50 employees", '11-50 employees',
"51-200 employees", '51-200 employees',
"201-500 employees", '201-500 employees',
"501-1000 employees", '501-1000 employees',
"1000+ employees", '1000+ employees',
]; ];
const validateForm = () => { const validateForm = () => {
@ -446,48 +436,47 @@ const EmployerRegistrationForm = () => {
// Email validation // Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!formData.email) { if (!formData.email) {
newErrors.email = "Email is required"; newErrors.email = 'Email is required';
} else if (!emailRegex.test(formData.email)) { } else if (!emailRegex.test(formData.email)) {
newErrors.email = "Please enter a valid email address"; newErrors.email = 'Please enter a valid email address';
} }
// Username validation // Username validation
if (!formData.username) { if (!formData.username) {
newErrors.username = "Username is required"; newErrors.username = 'Username is required';
} else if (formData.username.length < 3) { } else if (formData.username.length < 3) {
newErrors.username = "Username must be at least 3 characters"; newErrors.username = 'Username must be at least 3 characters';
} }
// Password validation // Password validation
if (!formData.password) { if (!formData.password) {
newErrors.password = "Password is required"; newErrors.password = 'Password is required';
} else { } else {
const passwordErrors = validatePassword(formData.password); const passwordErrors = validatePassword(formData.password);
if (passwordErrors.length > 0) { if (passwordErrors.length > 0) {
newErrors.password = passwordErrors.join(", "); newErrors.password = passwordErrors.join(', ');
} }
} }
// Confirm password // Confirm password
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = "Passwords do not match"; newErrors.confirmPassword = 'Passwords do not match';
} }
// Company validation // Company validation
if (!formData.companyName.trim()) { if (!formData.companyName.trim()) {
newErrors.companyName = "Company name is required"; newErrors.companyName = 'Company name is required';
} }
if (!formData.industry) { if (!formData.industry) {
newErrors.industry = "Industry is required"; newErrors.industry = 'Industry is required';
} }
if (!formData.companySize) { if (!formData.companySize) {
newErrors.companySize = "Company size is required"; newErrors.companySize = 'Company size is required';
} }
if (!formData.companyDescription.trim()) { if (!formData.companyDescription.trim()) {
newErrors.companyDescription = "Company description is required"; newErrors.companyDescription = 'Company description is required';
} else if (formData.companyDescription.length < 50) { } else if (formData.companyDescription.length < 50) {
newErrors.companyDescription = newErrors.companyDescription = 'Company description must be at least 50 characters';
"Company description must be at least 50 characters";
} }
// Website URL validation (optional but must be valid if provided) // Website URL validation (optional but must be valid if provided)
@ -495,7 +484,7 @@ const EmployerRegistrationForm = () => {
try { try {
new URL(formData.websiteUrl); new URL(formData.websiteUrl);
} catch { } catch {
newErrors.websiteUrl = "Please enter a valid website URL"; newErrors.websiteUrl = 'Please enter a valid website URL';
} }
} }
@ -507,32 +496,30 @@ const EmployerRegistrationForm = () => {
const errors: string[] = []; const errors: string[] = [];
if (password.length < 8) { if (password.length < 8) {
errors.push("at least 8 characters"); errors.push('at least 8 characters');
} }
if (!/[a-z]/.test(password)) { if (!/[a-z]/.test(password)) {
errors.push("one lowercase letter"); errors.push('one lowercase letter');
} }
if (!/[A-Z]/.test(password)) { if (!/[A-Z]/.test(password)) {
errors.push("one uppercase letter"); errors.push('one uppercase letter');
} }
if (!/\d/.test(password)) { if (!/\d/.test(password)) {
errors.push("one number"); errors.push('one number');
} }
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push("one special character"); errors.push('one special character');
} }
return errors.length > 0 return errors.length > 0 ? [`Password must contain ${errors.join(', ')}`] : [];
? [`Password must contain ${errors.join(", ")}`]
: [];
}; };
const handleInputChange = (field: string, value: string) => { const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value })); setFormData(prev => ({ ...prev, [field]: value }));
// Clear error when user starts typing // Clear error when user starts typing
if (errors[field]) { if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: "" })); setErrors(prev => ({ ...prev, [field]: '' }));
} }
}; };
@ -561,15 +548,15 @@ const EmployerRegistrationForm = () => {
setRegistrationResult(result); setRegistrationResult(result);
setShowSuccess(true); setShowSuccess(true);
} catch (error: any) { } catch (error: any) {
if (error.message.includes("already exists")) { if (error.message.includes('already exists')) {
if (error.message.includes("email")) { if (error.message.includes('email')) {
setErrors({ email: "An account with this email already exists" }); setErrors({ email: 'An account with this email already exists' });
} else if (error.message.includes("username")) { } else if (error.message.includes('username')) {
setErrors({ username: "This username is already taken" }); setErrors({ username: 'This username is already taken' });
} }
} else { } else {
setErrors({ setErrors({
general: error.message || "Registration failed. Please try again.", general: error.message || 'Registration failed. Please try again.',
}); });
} }
} finally { } finally {
@ -580,7 +567,7 @@ const EmployerRegistrationForm = () => {
return ( return (
<Paper elevation={3}> <Paper elevation={3}>
<Box sx={{ p: 5 }}> <Box sx={{ p: 5 }}>
<Box sx={{ textAlign: "center", mb: 4 }}> <Box sx={{ textAlign: 'center', mb: 4 }}>
<Typography variant="h4" component="h1" sx={{ mb: 1 }}> <Typography variant="h4" component="h1" sx={{ mb: 1 }}>
Join as an Employer Join as an Employer
</Typography> </Typography>
@ -591,7 +578,7 @@ const EmployerRegistrationForm = () => {
<Stack spacing={4}> <Stack spacing={4}>
{/* Account Information Section */} {/* Account Information Section */}
<Box sx={{ bgcolor: "grey.50", p: 3, borderRadius: 2 }}> <Box sx={{ bgcolor: 'grey.50', p: 3, borderRadius: 2 }}>
<Typography variant="h6" sx={{ mb: 2 }}> <Typography variant="h6" sx={{ mb: 2 }}>
Account Information Account Information
</Typography> </Typography>
@ -603,7 +590,7 @@ const EmployerRegistrationForm = () => {
label="Email Address" label="Email Address"
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)} onChange={e => handleInputChange('email', e.target.value)}
placeholder="company@example.com" placeholder="company@example.com"
error={!!errors.email} error={!!errors.email}
helperText={errors.email} helperText={errors.email}
@ -613,9 +600,7 @@ const EmployerRegistrationForm = () => {
fullWidth fullWidth
label="Username" label="Username"
value={formData.username} value={formData.username}
onChange={(e) => onChange={e => handleInputChange('username', e.target.value.toLowerCase())}
handleInputChange("username", e.target.value.toLowerCase())
}
placeholder="company123" placeholder="company123"
error={!!errors.username} error={!!errors.username}
helperText={errors.username} helperText={errors.username}
@ -627,11 +612,9 @@ const EmployerRegistrationForm = () => {
<TextField <TextField
fullWidth fullWidth
label="Password" label="Password"
type={showPassword ? "text" : "password"} type={showPassword ? 'text' : 'password'}
value={formData.password} value={formData.password}
onChange={(e) => onChange={e => handleInputChange('password', e.target.value)}
handleInputChange("password", e.target.value)
}
placeholder="Create a strong password" placeholder="Create a strong password"
error={!!errors.password} error={!!errors.password}
helperText={errors.password} helperText={errors.password}
@ -642,7 +625,7 @@ const EmployerRegistrationForm = () => {
<IconButton <IconButton
aria-label="toggle password visibility" aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
onMouseDown={(e) => e.preventDefault()} onMouseDown={e => e.preventDefault()}
edge="end" edge="end"
> >
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
@ -654,11 +637,9 @@ const EmployerRegistrationForm = () => {
<TextField <TextField
fullWidth fullWidth
label="Confirm Password" label="Confirm Password"
type={showConfirmPassword ? "text" : "password"} type={showConfirmPassword ? 'text' : 'password'}
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={(e) => onChange={e => handleInputChange('confirmPassword', e.target.value)}
handleInputChange("confirmPassword", e.target.value)
}
placeholder="Confirm your password" placeholder="Confirm your password"
error={!!errors.confirmPassword} error={!!errors.confirmPassword}
helperText={errors.confirmPassword} helperText={errors.confirmPassword}
@ -668,17 +649,11 @@ const EmployerRegistrationForm = () => {
<InputAdornment position="end"> <InputAdornment position="end">
<IconButton <IconButton
aria-label="toggle confirm password visibility" aria-label="toggle confirm password visibility"
onClick={() => onClick={() => setShowConfirmPassword(!showConfirmPassword)}
setShowConfirmPassword(!showConfirmPassword) onMouseDown={e => e.preventDefault()}
}
onMouseDown={(e) => e.preventDefault()}
edge="end" edge="end"
> >
{showConfirmPassword ? ( {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
<VisibilityOff />
) : (
<Visibility />
)}
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
), ),
@ -689,7 +664,7 @@ const EmployerRegistrationForm = () => {
</Box> </Box>
{/* Company Information Section */} {/* Company Information Section */}
<Box sx={{ bgcolor: "primary.50", p: 3, borderRadius: 2 }}> <Box sx={{ bgcolor: 'primary.50', p: 3, borderRadius: 2 }}>
<Typography variant="h6" sx={{ mb: 2 }}> <Typography variant="h6" sx={{ mb: 2 }}>
Company Information Company Information
</Typography> </Typography>
@ -699,9 +674,7 @@ const EmployerRegistrationForm = () => {
fullWidth fullWidth
label="Company Name" label="Company Name"
value={formData.companyName} value={formData.companyName}
onChange={(e) => onChange={e => handleInputChange('companyName', e.target.value)}
handleInputChange("companyName", e.target.value)
}
placeholder="Your Company Inc." placeholder="Your Company Inc."
error={!!errors.companyName} error={!!errors.companyName}
helperText={errors.companyName} helperText={errors.companyName}
@ -713,40 +686,32 @@ const EmployerRegistrationForm = () => {
<InputLabel>Industry</InputLabel> <InputLabel>Industry</InputLabel>
<Select <Select
value={formData.industry} value={formData.industry}
onChange={(e) => onChange={e => handleInputChange('industry', e.target.value)}
handleInputChange("industry", e.target.value)
}
label="Industry" label="Industry"
> >
{industryOptions.map((industry) => ( {industryOptions.map(industry => (
<MenuItem key={industry} value={industry}> <MenuItem key={industry} value={industry}>
{industry} {industry}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
{errors.industry && ( {errors.industry && <FormHelperText>{errors.industry}</FormHelperText>}
<FormHelperText>{errors.industry}</FormHelperText>
)}
</FormControl> </FormControl>
<FormControl fullWidth error={!!errors.companySize} required> <FormControl fullWidth error={!!errors.companySize} required>
<InputLabel>Company Size</InputLabel> <InputLabel>Company Size</InputLabel>
<Select <Select
value={formData.companySize} value={formData.companySize}
onChange={(e) => onChange={e => handleInputChange('companySize', e.target.value)}
handleInputChange("companySize", e.target.value)
}
label="Company Size" label="Company Size"
> >
{companySizeOptions.map((size) => ( {companySizeOptions.map(size => (
<MenuItem key={size} value={size}> <MenuItem key={size} value={size}>
{size} {size}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
{errors.companySize && ( {errors.companySize && <FormHelperText>{errors.companySize}</FormHelperText>}
<FormHelperText>{errors.companySize}</FormHelperText>
)}
</FormControl> </FormControl>
</Stack> </Stack>
@ -757,9 +722,7 @@ const EmployerRegistrationForm = () => {
multiline multiline
rows={4} rows={4}
value={formData.companyDescription} value={formData.companyDescription}
onChange={(e) => onChange={e => handleInputChange('companyDescription', e.target.value)}
handleInputChange("companyDescription", e.target.value)
}
placeholder="Tell us about your company, what you do, your mission, and what makes you unique..." placeholder="Tell us about your company, what you do, your mission, and what makes you unique..."
error={!!errors.companyDescription} error={!!errors.companyDescription}
helperText={ helperText={
@ -776,22 +739,20 @@ const EmployerRegistrationForm = () => {
label="Website URL" label="Website URL"
type="url" type="url"
value={formData.websiteUrl} value={formData.websiteUrl}
onChange={(e) => onChange={e => handleInputChange('websiteUrl', e.target.value)}
handleInputChange("websiteUrl", e.target.value)
}
placeholder="https://www.yourcompany.com" placeholder="https://www.yourcompany.com"
error={!!errors.websiteUrl} error={!!errors.websiteUrl}
helperText={errors.websiteUrl || "Optional"} helperText={errors.websiteUrl || 'Optional'}
/> />
<TextField <TextField
fullWidth fullWidth
label="Phone Number" label="Phone Number"
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleInputChange("phone", e.target.value)} onChange={e => handleInputChange('phone', e.target.value)}
placeholder="+1 (555) 123-4567" placeholder="+1 (555) 123-4567"
error={!!errors.phone} error={!!errors.phone}
helperText={errors.phone || "Optional"} helperText={errors.phone || 'Optional'}
/> />
</Stack> </Stack>
</Stack> </Stack>
@ -813,13 +774,13 @@ const EmployerRegistrationForm = () => {
<Typography>Creating Company Account...</Typography> <Typography>Creating Company Account...</Typography>
</Stack> </Stack>
) : ( ) : (
"Create Company Account" 'Create Company Account'
)} )}
</Button> </Button>
<Box sx={{ textAlign: "center" }}> <Box sx={{ textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Already have an account?{" "} Already have an account?{' '}
<Link href="/login" sx={{ fontWeight: 600 }}> <Link href="/login" sx={{ fontWeight: 600 }}>
Sign in here Sign in here
</Link> </Link>
@ -845,7 +806,7 @@ export function RegistrationTypeSelector() {
return ( return (
<Paper elevation={3}> <Paper elevation={3}>
<Box sx={{ p: 5 }}> <Box sx={{ p: 5 }}>
<Box sx={{ textAlign: "center", mb: 5 }}> <Box sx={{ textAlign: 'center', mb: 5 }}>
<Typography variant="h3" component="h1" sx={{ mb: 2 }}> <Typography variant="h3" component="h1" sx={{ mb: 2 }}>
Join Backstory Join Backstory
</Typography> </Typography>
@ -859,18 +820,18 @@ export function RegistrationTypeSelector() {
<Card <Card
sx={{ sx={{
flex: 1, flex: 1,
cursor: "pointer", cursor: 'pointer',
transition: "all 0.3s ease", transition: 'all 0.3s ease',
border: "2px solid transparent", border: '2px solid transparent',
"&:hover": { '&:hover': {
transform: "translateY(-4px)", transform: 'translateY(-4px)',
boxShadow: 6, boxShadow: 6,
borderColor: "primary.main", borderColor: 'primary.main',
}, },
}} }}
onClick={() => (window.location.href = "/register/candidate")} onClick={() => (window.location.href = '/register/candidate')}
> >
<CardContent sx={{ textAlign: "center", py: 4 }}> <CardContent sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h1" sx={{ mb: 2 }}> <Typography variant="h1" sx={{ mb: 2 }}>
👤 👤
</Typography> </Typography>
@ -881,7 +842,7 @@ export function RegistrationTypeSelector() {
Create a candidate profile to find your next opportunity Create a candidate profile to find your next opportunity
</Typography> </Typography>
</CardContent> </CardContent>
<CardActions sx={{ justifyContent: "center", pb: 3 }}> <CardActions sx={{ justifyContent: 'center', pb: 3 }}>
<Button variant="contained" size="large"> <Button variant="contained" size="large">
Join as Candidate Join as Candidate
</Button> </Button>
@ -892,18 +853,18 @@ export function RegistrationTypeSelector() {
<Card <Card
sx={{ sx={{
flex: 1, flex: 1,
cursor: "pointer", cursor: 'pointer',
transition: "all 0.3s ease", transition: 'all 0.3s ease',
border: "2px solid transparent", border: '2px solid transparent',
"&:hover": { '&:hover': {
transform: "translateY(-4px)", transform: 'translateY(-4px)',
boxShadow: 6, boxShadow: 6,
borderColor: "primary.main", borderColor: 'primary.main',
}, },
}} }}
onClick={() => (window.location.href = "/register/employer")} onClick={() => (window.location.href = '/register/employer')}
> >
<CardContent sx={{ textAlign: "center", py: 4 }}> <CardContent sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h1" sx={{ mb: 2 }}> <Typography variant="h1" sx={{ mb: 2 }}>
🏢 🏢
</Typography> </Typography>
@ -914,7 +875,7 @@ export function RegistrationTypeSelector() {
Create a company account to find and hire talent Create a company account to find and hire talent
</Typography> </Typography>
</CardContent> </CardContent>
<CardActions sx={{ justifyContent: "center", pb: 3 }}> <CardActions sx={{ justifyContent: 'center', pb: 3 }}>
<Button variant="contained" size="large"> <Button variant="contained" size="large">
Join as Employer Join as Employer
</Button> </Button>
@ -922,9 +883,9 @@ export function RegistrationTypeSelector() {
</Card> </Card>
</Stack> </Stack>
<Box sx={{ textAlign: "center", mt: 4 }}> <Box sx={{ textAlign: 'center', mt: 4 }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Already have an account?{" "} Already have an account?{' '}
<Link href="/login" sx={{ fontWeight: 600 }}> <Link href="/login" sx={{ fontWeight: 600 }}>
Sign in here Sign in here
</Link> </Link>

View File

@ -1,23 +1,23 @@
import React, { useState, useEffect, ReactElement } from "react"; import React, { useState, useEffect, ReactElement } from 'react';
// import FormGroup from '@mui/material/FormGroup'; // import FormGroup from '@mui/material/FormGroup';
// import FormControlLabel from '@mui/material/FormControlLabel'; // import FormControlLabel from '@mui/material/FormControlLabel';
// import Switch from '@mui/material/Switch'; // import Switch from '@mui/material/Switch';
// import Divider from '@mui/material/Divider'; // import Divider from '@mui/material/Divider';
// import TextField from '@mui/material/TextField'; // import TextField from '@mui/material/TextField';
import Accordion from "@mui/material/Accordion"; import Accordion from '@mui/material/Accordion';
import AccordionActions from "@mui/material/AccordionActions"; import AccordionActions from '@mui/material/AccordionActions';
import AccordionSummary from "@mui/material/AccordionSummary"; import AccordionSummary from '@mui/material/AccordionSummary';
import AccordionDetails from "@mui/material/AccordionDetails"; import AccordionDetails from '@mui/material/AccordionDetails';
import Typography from "@mui/material/Typography"; import Typography from '@mui/material/Typography';
// import Button from '@mui/material/Button'; // import Button from '@mui/material/Button';
// import Box from '@mui/material/Box'; // import Box from '@mui/material/Box';
// import ResetIcon from '@mui/icons-material/History'; // import ResetIcon from '@mui/icons-material/History';
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { BackstoryPageProps } from "../../components/BackstoryTab"; import { BackstoryPageProps } from '../../components/BackstoryTab';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import * as Types from "types/types"; import * as Types from 'types/types';
// interface ServerTunables { // interface ServerTunables {
// system_prompt: string, // system_prompt: string,
@ -41,9 +41,9 @@ const SystemInfoComponent: React.FC<{
const convertToSymbols = (text: string) => { const convertToSymbols = (text: string) => {
return text return text
.replace(/\(R\)/g, "®") // Replace (R) with the ® symbol .replace(/\(R\)/g, '®') // Replace (R) with the ® symbol
.replace(/\(C\)/g, "©") // Replace (C) with the © symbol .replace(/\(C\)/g, '©') // Replace (C) with the © symbol
.replace(/\(TM\)/g, "™"); // Replace (TM) with the ™ symbol .replace(/\(TM\)/g, '™'); // Replace (TM) with the ™ symbol
}; };
useEffect(() => { useEffect(() => {
@ -59,10 +59,10 @@ const SystemInfoComponent: React.FC<{
{convertToSymbols(k)} {index} {convertToSymbols(k)} {index}
</div> </div>
<div> <div>
{convertToSymbols(card.name)}{" "} {convertToSymbols(card.name)}{' '}
{card.discrete {card.discrete
? `w/ ${Math.round(card.memory / (1024 * 1024 * 1024))}GB RAM` ? `w/ ${Math.round(card.memory / (1024 * 1024 * 1024))}GB RAM`
: "(integrated)"} : '(integrated)'}
</div> </div>
</div> </div>
)); ));
@ -87,9 +87,7 @@ const Settings = (props: BackstoryPageProps) => {
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
// const [editSystemPrompt, setEditSystemPrompt] = useState<string>(""); // const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
const [systemInfo, setSystemInfo] = useState<Types.SystemInfo | undefined>( const [systemInfo, setSystemInfo] = useState<Types.SystemInfo | undefined>(undefined);
undefined
);
// const [tools, setTools] = useState<Tool[]>([]); // const [tools, setTools] = useState<Tool[]>([]);
// const [rags, setRags] = useState<Tool[]>([]); // const [rags, setRags] = useState<Tool[]>([]);
// const [systemPrompt, setSystemPrompt] = useState<string>(""); // const [systemPrompt, setSystemPrompt] = useState<string>("");
@ -183,8 +181,8 @@ const Settings = (props: BackstoryPageProps) => {
const response: Types.SystemInfo = await apiClient.getSystemInfo(); const response: Types.SystemInfo = await apiClient.getSystemInfo();
setSystemInfo(response); setSystemInfo(response);
} catch (error) { } catch (error) {
console.error("Error obtaining system information:", error); console.error('Error obtaining system information:', error);
setSnack("Unable to obtain system information.", "error"); setSnack('Unable to obtain system information.', 'error');
} }
}; };
@ -397,9 +395,7 @@ const Settings = (props: BackstoryPageProps) => {
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography component="span">System Information</Typography> <Typography component="span">System Information</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>The server is running on the following hardware:</AccordionDetails>
The server is running on the following hardware:
</AccordionDetails>
<AccordionActions> <AccordionActions>
<SystemInfoComponent systemInfo={systemInfo} /> <SystemInfoComponent systemInfo={systemInfo} />
</AccordionActions> </AccordionActions>

View File

@ -1,21 +1,19 @@
import React, { useEffect } from "react"; import React, { useEffect } from 'react';
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from 'react-router-dom';
import { Box } from "@mui/material"; import { Box } from '@mui/material';
import { SetSnackType } from "../components/Snack"; import { SetSnackType } from '../components/Snack';
import { LoadingComponent } from "../components/LoadingComponent"; import { LoadingComponent } from '../components/LoadingComponent';
import { User, Guest, Candidate } from "types/types"; import { User, Guest, Candidate } from 'types/types';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedCandidate } from "hooks/GlobalContext"; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
interface CandidateRouteProps { interface CandidateRouteProps {
guest?: Guest | null; guest?: Guest | null;
user?: User | null; user?: User | null;
} }
const CandidateRoute: React.FC<CandidateRouteProps> = ( const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProps) => {
props: CandidateRouteProps
) => {
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate(); const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
@ -30,22 +28,15 @@ const CandidateRoute: React.FC<CandidateRouteProps> = (
try { try {
const result: Candidate = await apiClient.getCandidate(reference); const result: Candidate = await apiClient.getCandidate(reference);
setSelectedCandidate(result); setSelectedCandidate(result);
navigate("/chat"); navigate('/chat');
} catch { } catch {
setSnack(`Unable to obtain information for ${username}.`, "error"); setSnack(`Unable to obtain information for ${username}.`, 'error');
navigate("/"); navigate('/');
} }
}; };
getCandidate(username); getCandidate(username);
}, [ }, [setSelectedCandidate, selectedCandidate, username, navigate, setSnack, apiClient]);
setSelectedCandidate,
selectedCandidate,
username,
navigate,
setSnack,
apiClient,
]);
if (selectedCandidate?.username !== username) { if (selectedCandidate?.username !== username) {
return ( return (

File diff suppressed because it is too large Load Diff

View File

@ -12,13 +12,11 @@
/** /**
* Converts a camelCase object to snake_case for sending to the Python backend * Converts a camelCase object to snake_case for sending to the Python backend
*/ */
export function toSnakeCase<T extends Record<string, any>>( export function toSnakeCase<T extends Record<string, any>>(obj: T): Record<string, any> {
obj: T if (!obj || typeof obj !== 'object') return obj;
): Record<string, any> {
if (!obj || typeof obj !== "object") return obj;
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return obj.map((item) => toSnakeCase(item)); return obj.map(item => toSnakeCase(item));
} }
const result: Record<string, any> = {}; const result: Record<string, any> = {};
@ -29,13 +27,13 @@ export function toSnakeCase<T extends Record<string, any>>(
if (value === null || value === undefined) { if (value === null || value === undefined) {
result[snakeCaseKey] = value; result[snakeCaseKey] = value;
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
result[snakeCaseKey] = value.map((item) => result[snakeCaseKey] = value.map(item =>
typeof item === "object" && item !== null ? toSnakeCase(item) : item typeof item === 'object' && item !== null ? toSnakeCase(item) : item
); );
} else if (value instanceof Date) { } else if (value instanceof Date) {
// Convert Date to ISO string for Python datetime // Convert Date to ISO string for Python datetime
result[snakeCaseKey] = value.toISOString(); result[snakeCaseKey] = value.toISOString();
} else if (typeof value === "object") { } else if (typeof value === 'object') {
result[snakeCaseKey] = toSnakeCase(value); result[snakeCaseKey] = toSnakeCase(value);
} else { } else {
result[snakeCaseKey] = value; result[snakeCaseKey] = value;
@ -49,10 +47,10 @@ export function toSnakeCase<T extends Record<string, any>>(
* Converts a snake_case object to camelCase for TypeScript/JavaScript * Converts a snake_case object to camelCase for TypeScript/JavaScript
*/ */
export function toCamelCase<T>(obj: Record<string, any>): T { export function toCamelCase<T>(obj: Record<string, any>): T {
if (!obj || typeof obj !== "object") return obj as T; if (!obj || typeof obj !== 'object') return obj as T;
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return obj.map((item) => toCamelCase(item)) as T; return obj.map(item => toCamelCase(item)) as T;
} }
const result: Record<string, any> = {}; const result: Record<string, any> = {};
@ -63,13 +61,13 @@ export function toCamelCase<T>(obj: Record<string, any>): T {
if (value === null || value === undefined) { if (value === null || value === undefined) {
result[camelCaseKey] = value; result[camelCaseKey] = value;
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
result[camelCaseKey] = value.map((item) => result[camelCaseKey] = value.map(item =>
typeof item === "object" && item !== null ? toCamelCase(item) : item typeof item === 'object' && item !== null ? toCamelCase(item) : item
); );
} else if (typeof value === "string" && isIsoDateString(value)) { } else if (typeof value === 'string' && isIsoDateString(value)) {
// Convert ISO date string to Date object // Convert ISO date string to Date object
result[camelCaseKey] = new Date(value); result[camelCaseKey] = new Date(value);
} else if (typeof value === "object") { } else if (typeof value === 'object') {
result[camelCaseKey] = toCamelCase(value); result[camelCaseKey] = toCamelCase(value);
} else { } else {
result[camelCaseKey] = value; result[camelCaseKey] = value;
@ -83,7 +81,7 @@ export function toCamelCase<T>(obj: Record<string, any>): T {
* Helper function to convert camelCase to snake_case * Helper function to convert camelCase to snake_case
*/ */
function camelToSnake(str: string): string { function camelToSnake(str: string): string {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
} }
/** /**
@ -97,10 +95,8 @@ function snakeToCamel(str: string): string {
* Checks if a string is an ISO date format * Checks if a string is an ISO date format
*/ */
function isIsoDateString(value: string): boolean { function isIsoDateString(value: string): boolean {
if (typeof value !== "string") return false; if (typeof value !== 'string') return false;
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test( return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test(value);
value
);
} }
// ============================ // ============================
@ -110,9 +106,7 @@ function isIsoDateString(value: string): boolean {
/** /**
* Format data for API requests (converts to format expected by Python backend) * Format data for API requests (converts to format expected by Python backend)
*/ */
export function formatApiRequest<T extends Record<string, any>>( export function formatApiRequest<T extends Record<string, any>>(data: T): Record<string, any> {
data: T
): Record<string, any> {
if (!data) return data; if (!data) return data;
// Create a new object to avoid mutating the original // Create a new object to avoid mutating the original
@ -123,15 +117,15 @@ export function formatApiRequest<T extends Record<string, any>>(
if (value instanceof Date) { if (value instanceof Date) {
formatted[key] = value.toISOString(); formatted[key] = value.toISOString();
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
formatted[key] = value.map((item) => { formatted[key] = value.map(item => {
if (item instanceof Date) { if (item instanceof Date) {
return item.toISOString(); return item.toISOString();
} else if (typeof item === "object" && item !== null) { } else if (typeof item === 'object' && item !== null) {
return formatApiRequest(item); return formatApiRequest(item);
} }
return item; return item;
}); });
} else if (typeof value === "object" && value !== null) { } else if (typeof value === 'object' && value !== null) {
formatted[key] = formatApiRequest(value); formatted[key] = formatApiRequest(value);
} else { } else {
formatted[key] = value; formatted[key] = value;
@ -145,12 +139,12 @@ export function formatApiRequest<T extends Record<string, any>>(
* Parse API responses and convert to TypeScript format * Parse API responses and convert to TypeScript format
*/ */
export function parseApiResponse<T>(data: any): ApiResponse<T> { export function parseApiResponse<T>(data: any): ApiResponse<T> {
if (!data || typeof data !== "object") { if (!data || typeof data !== 'object') {
return { return {
success: false, success: false,
error: { error: {
code: "INVALID_RESPONSE", code: 'INVALID_RESPONSE',
message: "Invalid response format", message: 'Invalid response format',
}, },
}; };
} }
@ -204,12 +198,12 @@ export function toUrlParams(obj: Record<string, any>): URLSearchParams {
if (value !== null && value !== undefined) { if (value !== null && value !== undefined) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// Handle arrays by adding multiple params with same key // Handle arrays by adding multiple params with same key
value.forEach((item) => { value.forEach(item => {
params.append(key, String(item)); params.append(key, String(item));
}); });
} else if (value instanceof Date) { } else if (value instanceof Date) {
params.append(key, value.toISOString()); params.append(key, value.toISOString());
} else if (typeof value === "object") { } else if (typeof value === 'object') {
// For nested objects, we could flatten or JSON stringify // For nested objects, we could flatten or JSON stringify
params.append(key, JSON.stringify(value)); params.append(key, JSON.stringify(value));
} else { } else {
@ -228,17 +222,15 @@ export function toUrlParams(obj: Record<string, any>): URLSearchParams {
/** /**
* Check if response is a successful API response * Check if response is a successful API response
*/ */
export function isSuccessResponse<T>( export function isSuccessResponse<T>(response: any): response is SuccessApiResponse<T> {
response: any return response && typeof response === 'object' && response.success === true;
): response is SuccessApiResponse<T> {
return response && typeof response === "object" && response.success === true;
} }
/** /**
* Check if response is an error API response * Check if response is an error API response
*/ */
export function isErrorResponse(response: any): response is ErrorApiResponse { export function isErrorResponse(response: any): response is ErrorApiResponse {
return response && typeof response === "object" && response.success === false; return response && typeof response === 'object' && response.success === false;
} }
/** /**
@ -250,8 +242,8 @@ export function extractApiData<T>(response: ApiResponse<T>): T {
} }
const errorMessage = isErrorResponse(response) const errorMessage = isErrorResponse(response)
? response.error?.message || "Unknown API error" ? response.error?.message || 'Unknown API error'
: "Invalid API response format"; : 'Invalid API response format';
throw new Error(errorMessage); throw new Error(errorMessage);
} }
@ -298,7 +290,7 @@ export interface PaginatedRequest {
page?: number; page?: number;
limit?: number; limit?: number;
sortBy?: string; sortBy?: string;
sortOrder?: "asc" | "desc"; sortOrder?: 'asc' | 'desc';
filters?: Record<string, any>; filters?: Record<string, any>;
} }
@ -309,13 +301,11 @@ export interface PaginatedRequest {
/** /**
* Create a paginated request with defaults * Create a paginated request with defaults
*/ */
export function createPaginatedRequest( export function createPaginatedRequest(params: Partial<PaginatedRequest> = {}): PaginatedRequest {
params: Partial<PaginatedRequest> = {}
): PaginatedRequest {
return { return {
page: 1, page: 1,
limit: 20, limit: 20,
sortOrder: "desc", sortOrder: 'desc',
...params, ...params,
}; };
} }
@ -360,12 +350,12 @@ export async function handlePaginatedApiResponse<T>(
/** /**
* Log conversion for debugging * Log conversion for debugging
*/ */
export function debugConversion<T>(obj: T, label = "Object"): T { export function debugConversion<T>(obj: T, label = 'Object'): T {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === 'development') {
console.group(`🔄 ${label} Conversion`); console.group(`🔄 ${label} Conversion`);
console.log("Original:", obj); console.log('Original:', obj);
if (typeof obj === "object" && obj !== null) { if (typeof obj === 'object' && obj !== null) {
console.log("Formatted for API:", formatApiRequest(obj as any)); console.log('Formatted for API:', formatApiRequest(obj as any));
} }
console.groupEnd(); console.groupEnd();
} }

View File

@ -1,4 +1,4 @@
import { ReactElement } from "react"; import { ReactElement } from 'react';
export interface NavigationItem { export interface NavigationItem {
id: string; id: string;
@ -7,12 +7,12 @@ export interface NavigationItem {
icon?: ReactElement; icon?: ReactElement;
children?: NavigationItem[]; children?: NavigationItem[];
component?: ReactElement; component?: ReactElement;
userTypes?: ("candidate" | "employer" | "guest" | "admin")[]; userTypes?: ('candidate' | 'employer' | 'guest' | 'admin')[];
exact?: boolean; exact?: boolean;
divider?: boolean; divider?: boolean;
showInNavigation?: boolean; // Controls if item appears in main navigation showInNavigation?: boolean; // Controls if item appears in main navigation
showInUserMenu?: boolean; // Controls if item appears in user menu showInUserMenu?: boolean; // Controls if item appears in user menu
userMenuGroup?: "profile" | "account" | "system" | "admin"; // Groups items in user menu userMenuGroup?: 'profile' | 'account' | 'system' | 'admin'; // Groups items in user menu
} }
export interface NavigationConfig { export interface NavigationConfig {

View File

@ -1,6 +1,6 @@
import { Palette, PaletteOptions } from "@mui/material/styles"; import { Palette, PaletteOptions } from '@mui/material/styles';
declare module "@mui/material/styles" { declare module '@mui/material/styles' {
interface Palette { interface Palette {
custom: { custom: {
highlight: string; highlight: string;

File diff suppressed because it is too large Load Diff