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": {
"browser": true,
"es2021": true,
"jest": true
},
"extends": [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { useState } from "react";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import CheckIcon from "@mui/icons-material/Check";
import IconButton, { IconButtonProps } from "@mui/material/IconButton";
import { Tooltip } from "@mui/material";
import { SxProps, Theme } from "@mui/material";
import { useState } from 'react';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import CheckIcon from '@mui/icons-material/Check';
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
import { Tooltip } from '@mui/material';
import { SxProps, Theme } from '@mui/material';
interface CopyBubbleProps extends IconButtonProps {
content: string | undefined;
@ -14,7 +14,7 @@ interface CopyBubbleProps extends IconButtonProps {
const CopyBubble = ({
content,
sx,
tooltip = "Copy to clipboard",
tooltip = 'Copy to clipboard',
onClick,
...rest
}: CopyBubbleProps) => {
@ -38,19 +38,19 @@ const CopyBubble = ({
return (
<Tooltip title={tooltip} placement="top" arrow>
<IconButton
onClick={(e) => {
onClick={e => {
handleCopy(e);
}}
sx={{
width: 24,
height: 24,
opacity: 0.75,
bgcolor: "background.paper",
"&:hover": { bgcolor: "action.hover", opacity: 1 },
bgcolor: 'background.paper',
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
...sx,
}}
size="small"
color={copied ? "success" : "default"}
color={copied ? 'success' : 'default'}
{...rest}
>
{copied ? (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import React from "react";
import { Button, Typography, Paper, Container } from "@mui/material";
import { useNavigate } from "react-router-dom";
import React from 'react';
import { Button, Typography, Paper, Container } from '@mui/material';
import { useNavigate } from 'react-router-dom';
interface LoginRequiredProps {
asset: string;
@ -11,14 +11,14 @@ const LoginRequired = (props: LoginRequiredProps) => {
return (
<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>
Please log in to access {asset}
</Typography>
<Button
variant="contained"
onClick={() => {
navigate("/login");
navigate('/login');
}}
color="primary"
sx={{ mt: 2 }}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React from "react";
import React from 'react';
import {
Chat as ChatIcon,
Dashboard as DashboardIcon,
@ -18,35 +18,35 @@ import {
Analytics as AnalyticsIcon,
BubbleChart,
AutoFixHigh,
} from "@mui/icons-material";
import EditDocumentIcon from "@mui/icons-material/EditDocument";
} from '@mui/icons-material';
import EditDocumentIcon from '@mui/icons-material/EditDocument';
import { BackstoryLogo } from "components/ui/BackstoryLogo";
import { HomePage } from "pages/HomePage";
import { CandidateChatPage } from "pages/CandidateChatPage";
import { DocsPage } from "pages/DocsPage";
import { CreateProfilePage } from "pages/candidate/ProfileWizard";
import { VectorVisualizerPage } from "pages/VectorVisualizerPage";
import { BetaPage } from "pages/BetaPage";
import { CandidateListingPage } from "pages/FindCandidatePage";
import { JobAnalysisPage } from "pages/JobAnalysisPage";
import { GenerateCandidate } from "pages/GenerateCandidate";
import { LoginPage } from "pages/LoginPage";
import { EmailVerificationPage } from "components/EmailVerificationComponents";
import { Box, Typography } from "@mui/material";
import { CandidateDashboard } from "pages/candidate/Dashboard";
import { NavigationConfig, NavigationItem } from "types/navigation";
import { HowItWorks } from "pages/HowItWorks";
import SchoolIcon from "@mui/icons-material/School";
import { CandidateProfile } from "pages/candidate/Profile";
import { Settings } from "pages/candidate/Settings";
import { VectorVisualizer } from "components/VectorVisualizer";
import { DocumentManager } from "components/DocumentManager";
import { useAuth } from "hooks/AuthContext";
import { useNavigate } from "react-router-dom";
import { JobViewer } from "components/ui/JobViewer";
import { CandidatePicker } from "components/ui/CandidatePicker";
import { ResumeViewer } from "components/ui/ResumeViewer";
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import { HomePage } from 'pages/HomePage';
import { CandidateChatPage } from 'pages/CandidateChatPage';
import { DocsPage } from 'pages/DocsPage';
import { CreateProfilePage } from 'pages/candidate/ProfileWizard';
import { VectorVisualizerPage } from 'pages/VectorVisualizerPage';
import { BetaPage } from 'pages/BetaPage';
import { CandidateListingPage } from 'pages/FindCandidatePage';
import { JobAnalysisPage } from 'pages/JobAnalysisPage';
import { GenerateCandidate } from 'pages/GenerateCandidate';
import { LoginPage } from 'pages/LoginPage';
import { EmailVerificationPage } from 'components/EmailVerificationComponents';
import { Box, Typography } from '@mui/material';
import { CandidateDashboard } from 'pages/candidate/Dashboard';
import { NavigationConfig, NavigationItem } from 'types/navigation';
import { HowItWorks } from 'pages/HowItWorks';
import SchoolIcon from '@mui/icons-material/School';
import { CandidateProfile } from 'pages/candidate/Profile';
import { Settings } from 'pages/candidate/Settings';
import { VectorVisualizer } from 'components/VectorVisualizer';
import { DocumentManager } from 'components/DocumentManager';
import { useAuth } from 'hooks/AuthContext';
import { useNavigate } from 'react-router-dom';
import { JobViewer } from 'components/ui/JobViewer';
import { CandidatePicker } from 'components/ui/CandidatePicker';
import { ResumeViewer } from 'components/ui/ResumeViewer';
// Beta page components for placeholder routes
const BackstoryPage = () => (
@ -89,7 +89,7 @@ const LogoutPage = () => {
const { logout } = useAuth();
const navigate = useNavigate();
logout().then(() => {
navigate("/");
navigate('/');
});
return <Typography variant="h4">Logging out...</Typography>;
};
@ -107,28 +107,28 @@ const SettingsPage = () => (
export const navigationConfig: NavigationConfig = {
items: [
{
id: "home",
id: 'home',
label: <BackstoryLogo />,
path: "/",
path: '/',
component: <HowItWorks />,
userTypes: ["guest", "candidate", "employer"],
userTypes: ['guest', 'candidate', 'employer'],
exact: true,
},
{
id: "job-analysis",
label: "Job Analysis",
path: "/job-analysis",
id: 'job-analysis',
label: 'Job Analysis',
path: '/job-analysis',
icon: <WorkIcon />,
component: <JobAnalysisPage />,
userTypes: ["guest", "candidate", "employer"],
userTypes: ['guest', 'candidate', 'employer'],
},
{
id: "chat",
label: "Candidate Chat",
path: "/chat",
id: 'chat',
label: 'Candidate Chat',
path: '/chat',
icon: <ChatIcon />,
component: <CandidateChatPage />,
userTypes: ["guest", "candidate", "employer"],
userTypes: ['guest', 'candidate', 'employer'],
},
// {
// id: "explore",
@ -151,74 +151,74 @@ export const navigationConfig: NavigationConfig = {
// },
{
id: "generate-candidate",
label: "Generate Candidate",
path: "/admin/generate-candidate",
id: 'generate-candidate',
label: 'Generate Candidate',
path: '/admin/generate-candidate',
icon: <AutoFixHigh />,
component: <GenerateCandidate />,
userTypes: ["admin"],
userTypes: ['admin'],
showInNavigation: false,
showInUserMenu: true,
userMenuGroup: "admin",
userMenuGroup: 'admin',
},
// User menu only items (not shown in main navigation)
{
id: "candidate-profile",
label: "Profile",
id: 'candidate-profile',
label: 'Profile',
icon: <PersonIcon />,
path: "/candidate/profile",
path: '/candidate/profile',
component: <CandidateProfile />,
userTypes: ["candidate"],
userMenuGroup: "profile",
userTypes: ['candidate'],
userMenuGroup: 'profile',
showInNavigation: false,
showInUserMenu: true,
},
{
id: "candidate-dashboard",
label: "Dashboard",
path: "/candidate/dashboard",
id: 'candidate-dashboard',
label: 'Dashboard',
path: '/candidate/dashboard',
icon: <DashboardIcon />,
component: <CandidateDashboard />,
userTypes: ["candidate"],
userMenuGroup: "profile",
userTypes: ['candidate'],
userMenuGroup: 'profile',
showInNavigation: false,
showInUserMenu: true,
},
{
id: "explore-jobs",
label: "Jobs",
path: "/candidate/jobs/:jobId?",
id: 'explore-jobs',
label: 'Jobs',
path: '/candidate/jobs/:jobId?',
icon: <WorkIcon />,
component: <JobViewer />,
userTypes: ["candidate", "guest", "employer"],
userTypes: ['candidate', 'guest', 'employer'],
showInNavigation: false,
showInUserMenu: true,
userMenuGroup: "profile",
userMenuGroup: 'profile',
},
{
id: "explore-resumes",
label: "Resumes",
path: "/candidate/resumes/:resumeId?",
id: 'explore-resumes',
label: 'Resumes',
path: '/candidate/resumes/:resumeId?',
icon: <EditDocumentIcon />,
component: <ResumeViewer />,
userTypes: ["candidate", "guest", "employer"],
userTypes: ['candidate', 'guest', 'employer'],
showInNavigation: false,
showInUserMenu: true,
userMenuGroup: "profile",
userMenuGroup: 'profile',
},
{
id: "candidate-docs",
label: "Content",
id: 'candidate-docs',
label: 'Content',
icon: <BubbleChart />,
path: "/candidate/documents",
path: '/candidate/documents',
component: (
<Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}>
<Box sx={{ display: 'flex', width: '100%', flexDirection: 'column' }}>
<VectorVisualizer />
<DocumentManager />
</Box>
),
userTypes: ["candidate"],
userMenuGroup: "profile",
userTypes: ['candidate'],
userMenuGroup: 'profile',
showInNavigation: false,
showInUserMenu: true,
},
@ -265,65 +265,65 @@ export const navigationConfig: NavigationConfig = {
// showInUserMenu: true,
// },
{
id: "candidate-settings",
label: "System Information",
path: "/candidate/settings",
id: 'candidate-settings',
label: 'System Information',
path: '/candidate/settings',
icon: <SettingsIcon />,
component: <Settings />,
userTypes: ["candidate"],
userMenuGroup: "account",
userTypes: ['candidate'],
userMenuGroup: 'account',
showInNavigation: false,
showInUserMenu: true,
},
{
id: "logout",
label: "Logout",
id: 'logout',
label: 'Logout',
icon: <PersonIcon />, // This will be handled specially in Header
userTypes: ["candidate", "employer"],
userTypes: ['candidate', 'employer'],
showInNavigation: false,
showInUserMenu: true,
userMenuGroup: "system",
userMenuGroup: 'system',
},
// Auth routes (special handling)
{
id: "auth",
label: "Auth",
userTypes: ["guest", "candidate", "employer"],
id: 'auth',
label: 'Auth',
userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false,
children: [
{
id: "verify-email",
label: "Verify Email",
path: "/login/verify-email",
id: 'verify-email',
label: 'Verify Email',
path: '/login/verify-email',
component: <EmailVerificationPage />,
userTypes: ["guest", "candidate", "employer"],
userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false,
},
{
id: "login",
label: "Login",
path: "/login/:tab?",
id: 'login',
label: 'Login',
path: '/login/:tab?',
component: <LoginPage />,
userTypes: ["guest", "candidate", "employer"],
userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false,
},
{
id: "logout-page",
label: "Logout",
path: "/logout",
id: 'logout-page',
label: 'Logout',
path: '/logout',
component: <LogoutPage />,
userTypes: ["candidate", "employer"],
userTypes: ['candidate', 'employer'],
showInNavigation: false,
},
],
},
// Catch-all route
{
id: "catch-all",
label: "Not Found",
path: "*",
id: 'catch-all',
label: 'Not Found',
path: '*',
component: <BetaPage />,
userTypes: ["guest", "candidate", "employer"],
userTypes: ['guest', 'candidate', 'employer'],
showInNavigation: false,
},
],
@ -331,46 +331,44 @@ export const navigationConfig: NavigationConfig = {
// Utility functions for working with navigation config
export const getNavigationItemsForUser = (
userType: "guest" | "candidate" | "employer" | null,
userType: 'guest' | 'candidate' | 'employer' | null,
isAdmin: boolean
): NavigationItem[] => {
const currentUserType = userType || "guest";
const currentUserType = userType || 'guest';
const filterItems = (items: NavigationItem[]): NavigationItem[] => {
return items
.filter(
(item) =>
item =>
!item.userTypes ||
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
.map((item) => ({
.filter(item => item.showInNavigation !== false) // Default to true if not specified
.map(item => ({
...item,
children: item.children ? filterItems(item.children) : undefined,
}))
.filter(
(item) => item.path || (item.children && item.children.length > 0)
);
.filter(item => item.path || (item.children && item.children.length > 0));
};
return filterItems(navigationConfig.items);
};
export const getAllRoutes = (
userType: "guest" | "candidate" | "employer" | null,
userType: 'guest' | 'candidate' | 'employer' | null,
isAdmin: boolean
): NavigationItem[] => {
const currentUserType = userType || "guest";
const currentUserType = userType || 'guest';
const extractRoutes = (items: NavigationItem[]): NavigationItem[] => {
const routes: NavigationItem[] = [];
items.forEach((item) => {
items.forEach(item => {
if (
!item.userTypes ||
item.userTypes.includes(currentUserType) ||
(item.userTypes.includes("admin") && isAdmin)
(item.userTypes.includes('admin') && isAdmin)
) {
if (item.path && item.component) {
routes.push(item);
@ -388,20 +386,20 @@ export const getAllRoutes = (
};
export const getMainNavigationItems = (
userType: "guest" | "candidate" | "employer" | null,
userType: 'guest' | 'candidate' | 'employer' | null,
isAdmin: boolean
): NavigationItem[] => {
return getNavigationItemsForUser(userType, isAdmin).filter(
(item) =>
item.id !== "auth" &&
item.id !== "catch-all" &&
item =>
item.id !== 'auth' &&
item.id !== 'catch-all' &&
item.showInNavigation !== false &&
(item.path || (item.children && item.children.length > 0))
);
};
export const getUserMenuItems = (
userType: "candidate" | "employer" | "guest" | null,
userType: 'candidate' | 'employer' | 'guest' | null,
isAdmin: boolean
): NavigationItem[] => {
if (!userType) return [];
@ -409,7 +407,7 @@ export const getUserMenuItems = (
const extractUserMenuItems = (items: NavigationItem[]): NavigationItem[] => {
const menuItems: NavigationItem[] = [];
items.forEach((item) => {
items.forEach(item => {
if (!item.userTypes || item.userTypes.includes(userType) || isAdmin) {
if (item.showInUserMenu) {
menuItems.push(item);
@ -427,7 +425,7 @@ export const getUserMenuItems = (
};
export const getUserMenuItemsByGroup = (
userType: "candidate" | "employer" | "guest" | null,
userType: 'candidate' | 'employer' | 'guest' | null,
isAdmin: boolean
): { [key: string]: NavigationItem[] } => {
const menuItems = getUserMenuItems(userType, isAdmin);
@ -439,8 +437,8 @@ export const getUserMenuItemsByGroup = (
other: [],
};
menuItems.forEach((item) => {
const group = item.userMenuGroup || "other";
menuItems.forEach(item => {
const group = item.userMenuGroup || 'other';
if (!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 { Box, Typography, Paper, Container } from "@mui/material";
import React from 'react';
import { Box, Typography, Paper, Container } from '@mui/material';
// Import the backstoryTheme
// BackstoryAnalysisDisplay component
const BackstoryAppAnalysisPage = () => {
return (
<Box
sx={{ backgroundColor: "background.default", minHeight: "100%", py: 4 }}
>
<Box sx={{ backgroundColor: 'background.default', minHeight: '100%', py: 4 }}>
<Container maxWidth="lg">
<Paper sx={{ p: 4, boxShadow: 2 }}>
<Typography
variant="h1"
component="h1"
sx={{ mb: 3, color: "primary.main" }}
>
<Typography variant="h1" component="h1" sx={{ mb: 3, color: 'primary.main' }}>
Backstory Application Analysis
</Typography>
@ -23,9 +17,9 @@ const BackstoryAppAnalysisPage = () => {
Core Concept
</Typography>
<Typography variant="body1">
Backstory is a dual-purpose platform designed to bridge the gap
between job candidates and employers/recruiters with an AI-powered
approach to professional profiles and resume generation.
Backstory is a dual-purpose platform designed to bridge the gap between job candidates
and employers/recruiters with an AI-powered approach to professional profiles and resume
generation.
</Typography>
<Typography variant="h3" component="h3" sx={{ mt: 3 }}>
@ -34,16 +28,15 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Job Candidates</strong> - Upload and manage
comprehensive professional histories and generate tailored
resumes for specific positions
<strong>Job Candidates</strong> - Upload and manage comprehensive professional
histories and generate tailored resumes for specific positions
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Employers/Recruiters</strong> - Search for candidates,
directly interact with AI assistants about candidate
experiences, and generate position-specific resumes
<strong>Employers/Recruiters</strong> - Search for candidates, directly interact
with AI assistants about candidate experiences, and generate position-specific
resumes
</Typography>
</li>
</Box>
@ -58,32 +51,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Complete Profile Management</strong> - Create detailed
professional histories beyond typical resume constraints
<strong>Complete Profile Management</strong> - Create detailed professional
histories beyond typical resume constraints
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>AI-Assisted Q&A Setup</strong> - Configure an AI
assistant to answer employer questions about your experience
<strong>AI-Assisted Q&A Setup</strong> - Configure an AI assistant to answer
employer questions about your experience
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Smart Resume Generator</strong> - Create tailored
resumes for specific positions using AI
<strong>Smart Resume Generator</strong> - Create tailored resumes for specific
positions using AI
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Analytics Dashboard</strong> - Track profile views,
resume downloads, and employer engagement
<strong>Analytics Dashboard</strong> - Track profile views, resume downloads, and
employer engagement
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Privacy Controls</strong> - Manage visibility and access
to your professional information
<strong>Privacy Controls</strong> - Manage visibility and access to your
professional information
</Typography>
</li>
</Box>
@ -94,32 +87,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Advanced Candidate Search</strong> - Find candidates
with specific skills, experience levels, and qualifications
<strong>Advanced Candidate Search</strong> - Find candidates with specific skills,
experience levels, and qualifications
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Interactive Q&A</strong> - Ask questions directly to
candidate AI assistants to learn more about their experience
<strong>Interactive Q&A</strong> - Ask questions directly to candidate AI assistants
to learn more about their experience
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Resume Generation</strong> - Generate candidate resumes
tailored to specific job requirements
<strong>Resume Generation</strong> - Generate candidate resumes tailored to specific
job requirements
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Talent Pool Management</strong> - Create and manage
groups of candidates for different positions
<strong>Talent Pool Management</strong> - Create and manage groups of candidates for
different positions
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Job Posting Management</strong> - Create, manage, and
track applications for job postings
<strong>Job Posting Management</strong> - Create, manage, and track applications for
job postings
</Typography>
</li>
</Box>
@ -137,20 +130,20 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Public Navigation</strong> - Home, Docs, Pricing,
Login/Register accessible to all users
<strong>Public Navigation</strong> - Home, Docs, Pricing, Login/Register accessible
to all users
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Candidate Dashboard Navigation</strong> - Profile,
Backstory, Resumes, Q&A Setup, Analytics, Settings
<strong>Candidate Dashboard Navigation</strong> - Profile, Backstory, Resumes, Q&A
Setup, Analytics, Settings
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Employer Dashboard Navigation</strong> - Dashboard,
Search, Saved, Jobs, Company, Analytics, Settings
<strong>Employer Dashboard Navigation</strong> - Dashboard, Search, Saved, Jobs,
Company, Analytics, Settings
</Typography>
</li>
</Box>
@ -161,38 +154,38 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Dashboard Cards</strong> - Both user types have
dashboards with card-based information displays
<strong>Dashboard Cards</strong> - Both user types have dashboards with card-based
information displays
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Tab-Based Content Organization</strong> - Many screens
use horizontal tabs to organize related content
<strong>Tab-Based Content Organization</strong> - Many screens use horizontal tabs
to organize related content
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Form-Based Editors</strong> - Profile and content
editors use structured forms with varied input types
<strong>Form-Based Editors</strong> - Profile and content editors use structured
forms with varied input types
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Three-Column Layouts</strong> - Many screens follow a
left sidebar, main content, right sidebar pattern
<strong>Three-Column Layouts</strong> - Many screens follow a left sidebar, main
content, right sidebar pattern
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Preview/Edit Toggle</strong> - Resume and profile
editing screens offer both editing and preview modes
<strong>Preview/Edit Toggle</strong> - Resume and profile editing screens offer both
editing and preview modes
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Filter-Based Search</strong> - Employer search uses
multiple filter categories to refine candidate results
<strong>Filter-Based Search</strong> - Employer search uses multiple filter
categories to refine candidate results
</Typography>
</li>
</Box>
@ -201,9 +194,8 @@ const BackstoryAppAnalysisPage = () => {
Mobile Adaptations
</Typography>
<Typography variant="body1">
The mobile designs show a simplified navigation structure with
bottom tabs and a hamburger menu, maintaining the core functionality
while adapting to smaller screens.
The mobile designs show a simplified navigation structure with bottom tabs and a
hamburger menu, maintaining the core functionality while adapting to smaller screens.
</Typography>
<Typography variant="h2" component="h2" sx={{ mt: 4 }}>
@ -216,26 +208,26 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>LLM Integration</strong> - Supports multiple AI models
(Claude, GPT-4, self-hosted models)
<strong>LLM Integration</strong> - Supports multiple AI models (Claude, GPT-4,
self-hosted models)
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Candidate AI Assistant</strong> - Personalized AI
chatbot that answers questions about candidate experience
<strong>Candidate AI Assistant</strong> - Personalized AI chatbot that answers
questions about candidate experience
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Resume Generation</strong> - AI-powered resume creation
based on job requirements
<strong>Resume Generation</strong> - AI-powered resume creation based on job
requirements
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Skills Matching</strong> - Automated matching between
candidate skills and job requirements
<strong>Skills Matching</strong> - Automated matching between candidate skills and
job requirements
</Typography>
</li>
</Box>
@ -246,26 +238,25 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Authentication</strong> - OAuth with Google, LinkedIn,
GitHub
<strong>Authentication</strong> - OAuth with Google, LinkedIn, GitHub
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Data Import</strong> - LinkedIn profile import, resume
parsing (PDF, DOCX), CSV/JSON import
<strong>Data Import</strong> - LinkedIn profile import, resume parsing (PDF, DOCX),
CSV/JSON import
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>ATS Compatibility</strong> - Integration with employer
Applicant Tracking Systems
<strong>ATS Compatibility</strong> - Integration with employer Applicant Tracking
Systems
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Vector Databases</strong> - Semantic search capabilities
for candidate matching
<strong>Vector Databases</strong> - Semantic search capabilities for candidate
matching
</Typography>
</li>
</Box>
@ -276,32 +267,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Beyond the Resume</strong> - Focuses on comprehensive
professional stories rather than just resume highlights
<strong>Beyond the Resume</strong> - Focuses on comprehensive professional stories
rather than just resume highlights
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>AI-Mediated Communication</strong> - Uses AI to
facilitate deeper understanding of candidate experiences
<strong>AI-Mediated Communication</strong> - Uses AI to facilitate deeper
understanding of candidate experiences
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Bidirectional Resume Generation</strong> - Both
candidates and employers can generate tailored resumes
<strong>Bidirectional Resume Generation</strong> - Both candidates and employers can
generate tailored resumes
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Configurable AI Personalities</strong> - Candidates can
customize how their AI assistant responds to questions
<strong>Configurable AI Personalities</strong> - Candidates can customize how their
AI assistant responds to questions
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Deep Analytics</strong> - Both candidates and employers
receive insights about their engagement
<strong>Deep Analytics</strong> - Both candidates and employers receive insights
about their engagement
</Typography>
</li>
</Box>
@ -320,9 +311,7 @@ const BackstoryAppAnalysisPage = () => {
<Typography variant="body1">Cloud-hosted (SaaS model)</Typography>
</li>
<li>
<Typography variant="body1">
Hybrid deployment (mixed cloud/on-premises)
</Typography>
<Typography variant="body1">Hybrid deployment (mixed cloud/on-premises)</Typography>
</li>
</Box>
@ -331,14 +320,10 @@ const BackstoryAppAnalysisPage = () => {
</Typography>
<Box component="ul" sx={{ pl: 4 }}>
<li>
<Typography variant="body1">
Granular candidate privacy controls
</Typography>
<Typography variant="body1">Granular candidate privacy controls</Typography>
</li>
<li>
<Typography variant="body1">
Role-based access for employer teams
</Typography>
<Typography variant="body1">Role-based access for employer teams</Typography>
</li>
<li>
<Typography variant="body1">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +1,23 @@
import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
useRef,
} from "react";
import * as Types from "types/types";
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
import * as Types from 'types/types';
// Assuming you're using React Router
import { useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "hooks/AuthContext";
import { SetSnackType, SeverityType, Snack } from "components/Snack";
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from 'hooks/AuthContext';
import { SetSnackType, SeverityType, Snack } from 'components/Snack';
// ============================
// Storage Keys
// ============================
const STORAGE_KEYS = {
SELECTED_CANDIDATE_ID: "selectedCandidateId",
SELECTED_JOB_ID: "selectedJobId",
SELECTED_EMPLOYER_ID: "selectedEmployerId",
LAST_ROUTE: "lastVisitedRoute",
ROUTE_STATE: "routeState",
ACTIVE_TAB: "activeTab",
APPLIED_FILTERS: "appliedFilters",
SIDEBAR_COLLAPSED: "sidebarCollapsed",
SELECTED_CANDIDATE_ID: 'selectedCandidateId',
SELECTED_JOB_ID: 'selectedJobId',
SELECTED_EMPLOYER_ID: 'selectedEmployerId',
LAST_ROUTE: 'lastVisitedRoute',
ROUTE_STATE: 'routeState',
ACTIVE_TAB: 'activeTab',
APPLIED_FILTERS: 'appliedFilters',
SIDEBAR_COLLAPSED: 'sidebarCollapsed',
} as const;
// ============================
@ -81,7 +74,7 @@ function getStoredId(key: string): string | null {
try {
return localStorage.getItem(key);
} catch (error) {
console.warn("Failed to read from localStorage:", error);
console.warn('Failed to read from localStorage:', error);
return null;
}
}
@ -94,7 +87,7 @@ function setStoredId(key: string, id: string | null): void {
localStorage.removeItem(key);
}
} 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();
// Entity state
const [selectedCandidate, setSelectedCandidateState] =
useState<Types.Candidate | null>(null);
const [selectedCandidate, setSelectedCandidateState] = useState<Types.Candidate | null>(null);
const [selectedJob, setSelectedJobState] = useState<Types.Job | null>(null);
const [selectedEmployer, setSelectedEmployerState] =
useState<Types.Employer | null>(null);
const [selectedResume, setSelectedResume] = useState<Types.Resume | null>(
null
);
const [selectedEmployer, setSelectedEmployerState] = useState<Types.Employer | null>(null);
const [selectedResume, setSelectedResume] = useState<Types.Resume | null>(null);
const [isInitializing, setIsInitializing] = useState<boolean>(true);
// Route state
const [routeState, setRouteStateState] =
useState<RouteState>(getInitialRouteState);
const [routeState, setRouteStateState] = useState<RouteState>(getInitialRouteState);
// ============================
// Initialization Effect
@ -184,13 +172,13 @@ export function useAppStateLogic(): AppStateContextType {
const candidate = await apiClient.getCandidate(candidateId);
if (candidate) {
setSelectedCandidateState(candidate);
console.log("Restored candidate from storage:", candidate);
console.log('Restored candidate from storage:', candidate);
} else {
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) {
console.warn("Failed to restore candidate:", error);
console.warn('Failed to restore candidate:', error);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, null);
}
})()
@ -204,13 +192,13 @@ export function useAppStateLogic(): AppStateContextType {
const job = await apiClient.getJob(jobId);
if (job) {
setSelectedJobState(job);
console.log("Restored job from storage:", job);
console.log('Restored job from storage:', job);
} else {
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) {
console.warn("Failed to restore job:", error);
console.warn('Failed to restore job:', error);
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, null);
}
})()
@ -224,13 +212,13 @@ export function useAppStateLogic(): AppStateContextType {
const employer = await apiClient.getEmployer(employerId);
if (employer) {
setSelectedEmployerState(employer);
console.log("Restored employer from storage:", employer);
console.log('Restored employer from storage:', employer);
} else {
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) {
console.warn("Failed to restore employer:", error);
console.warn('Failed to restore employer:', error);
setStoredId(STORAGE_KEYS.SELECTED_EMPLOYER_ID, null);
}
})()
@ -240,7 +228,7 @@ export function useAppStateLogic(): AppStateContextType {
// Wait for all restoration attempts to complete
await Promise.all(promises);
} catch (error) {
console.error("Error during app state initialization:", error);
console.error('Error during app state initialization:', error);
} finally {
setIsInitializing(false);
}
@ -255,18 +243,11 @@ export function useAppStateLogic(): AppStateContextType {
useEffect(() => {
// Don't save certain routes (login, register, etc.)
const excludedRoutes = [
"/login",
"/register",
"/verify-email",
"/reset-password",
];
const shouldSaveRoute = !excludedRoutes.some((route) =>
location.pathname.startsWith(route)
);
const excludedRoutes = ['/login', '/register', '/verify-email', '/reset-password'];
const shouldSaveRoute = !excludedRoutes.some(route => location.pathname.startsWith(route));
if (shouldSaveRoute && !isInitializing) {
setRouteStateState((prev) => {
setRouteStateState(prev => {
const newState = { ...prev, lastRoute: location.pathname };
persistRouteState(newState);
return newState;
@ -278,28 +259,25 @@ export function useAppStateLogic(): AppStateContextType {
// Entity State Setters with Persistence
// ============================
const setSelectedCandidate = useCallback(
(candidate: Types.Candidate | null) => {
setSelectedCandidateState(candidate);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
const setSelectedCandidate = useCallback((candidate: Types.Candidate | null) => {
setSelectedCandidateState(candidate);
setStoredId(STORAGE_KEYS.SELECTED_CANDIDATE_ID, candidate?.id || null);
if (candidate) {
console.log("Selected candidate:", candidate);
} else {
console.log("Cleared selected candidate");
}
},
[]
);
if (candidate) {
console.log('Selected candidate:', candidate);
} else {
console.log('Cleared selected candidate');
}
}, []);
const setSelectedJob = useCallback((job: Types.Job | null) => {
setSelectedJobState(job);
setStoredId(STORAGE_KEYS.SELECTED_JOB_ID, job?.id || null);
if (job) {
console.log("Selected job:", job);
console.log('Selected job:', job);
} 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);
if (employer) {
console.log("Selected employer:", employer);
console.log('Selected employer:', employer);
} 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_EMPLOYER_ID, null);
console.log("Cleared all selections");
console.log('Cleared all selections');
}, []);
// ============================
@ -331,7 +309,7 @@ export function useAppStateLogic(): AppStateContextType {
// ============================
const saveCurrentRoute = useCallback(() => {
setRouteStateState((prev) => {
setRouteStateState(prev => {
const newState = { ...prev, lastRoute: location.pathname };
persistRouteState(newState);
return newState;
@ -345,7 +323,7 @@ export function useAppStateLogic(): AppStateContextType {
}, [routeState.lastRoute, location.pathname, navigate]);
const setActiveTab = useCallback((tab: string) => {
setRouteStateState((prev) => {
setRouteStateState(prev => {
const newState = { ...prev, activeTab: tab };
persistRouteState(newState);
return newState;
@ -353,7 +331,7 @@ export function useAppStateLogic(): AppStateContextType {
}, []);
const setFilters = useCallback((filters: Record<string, any>) => {
setRouteStateState((prev) => {
setRouteStateState(prev => {
const newState = { ...prev, appliedFilters: filters };
persistRouteState(newState);
return newState;
@ -361,7 +339,7 @@ export function useAppStateLogic(): AppStateContextType {
}, []);
const setSidebarCollapsed = useCallback((collapsed: boolean) => {
setRouteStateState((prev) => {
setRouteStateState(prev => {
const newState = { ...prev, sidebarCollapsed: collapsed };
persistRouteState(newState);
return newState;
@ -384,13 +362,10 @@ export function useAppStateLogic(): AppStateContextType {
localStorage.removeItem(STORAGE_KEYS.APPLIED_FILTERS);
localStorage.removeItem(STORAGE_KEYS.SIDEBAR_COLLAPSED);
console.log("Cleared all route state");
console.log('Cleared all route state');
}, []);
const emptySetSnack: SetSnackType = (
message: string,
severity?: SeverityType
) => {
const emptySetSnack: SetSnackType = (message: string, severity?: SeverityType) => {
return;
};
@ -445,7 +420,7 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) {
export function useAppState() {
const context = useContext(AppStateContext);
if (!context) {
throw new Error("useAppState must be used within an AppStateProvider");
throw new Error('useAppState must be used within an AppStateProvider');
}
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;
@ -69,10 +69,10 @@ const useResizeObserverAndMutationObserver = (
}, 500);
const resizeObserver = new ResizeObserver((e: any) => {
debouncedCallback("resize");
debouncedCallback('resize');
});
const mutationObserver = new MutationObserver((e: any) => {
debouncedCallback("mutation");
debouncedCallback('mutation');
});
// Observe container size
@ -124,7 +124,7 @@ const useAutoScrollToBottom = (
const scrollTo = scrollToRef.current;
if (isPasteEvent && !scrollTo) {
console.error("Paste Event triggered without scrollTo");
console.error('Paste Event triggered without scrollTo');
}
if (scrollTo) {
@ -136,16 +136,14 @@ const useAutoScrollToBottom = (
// Check if TextField is fully or partially visible (for non-paste events)
const isTextFieldVisible =
scrollToRect.top < containerBottom &&
scrollToRect.bottom > containerTop;
scrollToRect.top < containerBottom && scrollToRect.bottom > containerTop;
// Scroll on paste or if TextField is visible and user isn't scrolling up
shouldScroll =
isPasteEvent || (isTextFieldVisible && !isUserScrollingUpRef.current);
shouldScroll = isPasteEvent || (isTextFieldVisible && !isUserScrollingUpRef.current);
if (shouldScroll) {
requestAnimationFrame(() => {
debug &&
console.debug("Scrolling to container bottom:", {
console.debug('Scrolling to container bottom:', {
scrollHeight: container.scrollHeight,
scrollToHeight: scrollToRect.height,
containerHeight: container.clientHeight,
@ -155,7 +153,7 @@ const useAutoScrollToBottom = (
});
container.scrollTo({
top: container.scrollHeight,
behavior: smooth ? "smooth" : "auto",
behavior: smooth ? 'smooth' : 'auto',
});
});
}
@ -170,12 +168,12 @@ const useAutoScrollToBottom = (
if (shouldScroll) {
requestAnimationFrame(() => {
debug &&
console.debug("Scrolling to container bottom (fallback):", {
console.debug('Scrolling to container bottom (fallback):', {
scrollHeight,
});
container.scrollTo({
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 */
isUserScrollingUpRef.current =
currentScrollTop <= lastScrollTop.current || pause ? true : false;
debug &&
console.debug(
`Scrolling up or paused: ${isUserScrollingUpRef.current} ${pause}`
);
debug && console.debug(`Scrolling up or paused: ${isUserScrollingUpRef.current} ${pause}`);
lastScrollTop.current = currentScrollTop;
if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
scrollTimeout.current = setTimeout(
() => {
isUserScrollingUpRef.current = false;
debug &&
console.debug(`Scrolling up: ${isUserScrollingUpRef.current}`);
debug && console.debug(`Scrolling up: ${isUserScrollingUpRef.current}`);
},
pause ? pause : 500
);
};
const pauseScroll = (ev: Event) => {
debug && console.log("Pausing for mouse movement");
debug && console.log('Pausing for mouse movement');
handleScroll(ev, 500);
};
const pauseClick = (ev: Event) => {
debug && console.log("Pausing for mouse click");
debug && console.log('Pausing for mouse click');
handleScroll(ev, 1000);
};
const handlePaste = () => {
console.log("handlePaste");
console.log('handlePaste');
// Delay scroll check to ensure DOM updates
setTimeout(() => {
console.log("scrolling for handlePaste");
console.log('scrolling for handlePaste');
requestAnimationFrame(() => checkAndScrollToBottom(true));
}, 100);
};
window.addEventListener("mousemove", pauseScroll);
window.addEventListener("mousedown", pauseClick);
window.addEventListener('mousemove', pauseScroll);
window.addEventListener('mousedown', pauseClick);
container.addEventListener("scroll", handleScroll);
container.addEventListener('scroll', handleScroll);
if (scrollTo) {
scrollTo.addEventListener("paste", handlePaste);
scrollTo.addEventListener('paste', handlePaste);
}
checkAndScrollToBottom();
return () => {
window.removeEventListener("mousedown", pauseClick);
window.removeEventListener("mousemove", pauseScroll);
container.removeEventListener("scroll", handleScroll);
window.removeEventListener('mousedown', pauseClick);
window.removeEventListener('mousemove', pauseScroll);
container.removeEventListener('scroll', handleScroll);
if (scrollTo) {
scrollTo.removeEventListener("paste", handlePaste);
scrollTo.removeEventListener('paste', handlePaste);
}
if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
};
}, [
smooth,
scrollToRef,
fallbackThreshold,
contentUpdateTrigger,
checkAndScrollToBottom,
]);
}, [smooth, scrollToRef, fallbackThreshold, contentUpdateTrigger, checkAndScrollToBottom]);
// Observe container and TextField size, plus DOM changes
useResizeObserverAndMutationObserver(
containerRef,
scrollToRef,
checkAndScrollToBottom
);
useResizeObserverAndMutationObserver(containerRef, scrollToRef, checkAndScrollToBottom);
return containerRef;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import React from 'react';
import { useNavigate } from 'react-router-dom';
import {
Box,
Button,
@ -17,37 +17,37 @@ import {
ButtonProps,
useMediaQuery,
useTheme,
} from "@mui/material";
import { styled } from "@mui/material/styles";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import AssessmentIcon from "@mui/icons-material/Assessment";
import PersonIcon from "@mui/icons-material/Person";
import WorkIcon from "@mui/icons-material/Work";
import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
import DescriptionIcon from "@mui/icons-material/Description";
import professionalConversationPng from "assets/Conversation.png";
import selectAJobPng from "assets/select-a-job.png";
import selectJobAnalysisPng from "assets/select-job-analysis.png";
import selectACandidatePng from "assets/select-a-candidate.png";
import selectStartAnalysisPng from "assets/select-start-analysis.png";
import waitPng from "assets/wait.png";
import finalResumePng from "assets/final-resume.png";
} from '@mui/material';
import { styled } from '@mui/material/styles';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import AssessmentIcon from '@mui/icons-material/Assessment';
import PersonIcon from '@mui/icons-material/Person';
import WorkIcon from '@mui/icons-material/Work';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import DescriptionIcon from '@mui/icons-material/Description';
import professionalConversationPng from 'assets/Conversation.png';
import selectAJobPng from 'assets/select-a-job.png';
import selectJobAnalysisPng from 'assets/select-job-analysis.png';
import selectACandidatePng from 'assets/select-a-candidate.png';
import selectStartAnalysisPng from 'assets/select-start-analysis.png';
import waitPng from 'assets/wait.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
const HeroSection = styled(Box)(({ theme }) => ({
padding: theme.spacing(3, 0),
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
[theme.breakpoints.down("md")]: {
[theme.breakpoints.down('md')]: {
padding: theme.spacing(2, 0),
},
}));
const StepSection = styled(Box)(({ theme }) => ({
padding: theme.spacing(6, 0),
"&:nth-of-type(even)": {
'&:nth-of-type(even)': {
backgroundColor: theme.palette.background.default,
},
}));
@ -55,25 +55,25 @@ const StepSection = styled(Box)(({ theme }) => ({
const StepNumber = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.action.active,
color: theme.palette.background.paper,
borderRadius: "50%",
borderRadius: '50%',
width: 60,
height: 60,
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: "1.5rem",
fontWeight: "bold",
margin: "0 auto 1rem auto",
[theme.breakpoints.up("md")]: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '1.5rem',
fontWeight: 'bold',
margin: '0 auto 1rem auto',
[theme.breakpoints.up('md')]: {
margin: 0,
},
}));
const ImageContainer = styled(Box)(({ theme }) => ({
textAlign: "center",
"& img": {
maxWidth: "100%",
height: "auto",
textAlign: 'center',
'& img': {
maxWidth: '100%',
height: 'auto',
borderRadius: theme.spacing(1),
boxShadow: theme.shadows[3],
border: `2px solid ${theme.palette.action.active}`,
@ -81,22 +81,22 @@ const ImageContainer = styled(Box)(({ theme }) => ({
}));
const StepCard = styled(Card)(({ theme }) => ({
height: "100%",
display: "flex",
flexDirection: "column",
height: '100%',
display: 'flex',
flexDirection: 'column',
border: `1px solid ${theme.palette.action.active}`,
"&:hover": {
'&:hover': {
boxShadow: theme.shadows[4],
},
}));
const steps = [
"Select Job Analysis",
"Choose a Job",
"Select a Candidate",
"Start Assessment",
"Review Results",
"Generate Resume",
'Select Job Analysis',
'Choose a Job',
'Select a Candidate',
'Start Assessment',
'Review Results',
'Generate Resume',
];
interface StepContentProps {
@ -126,24 +126,18 @@ const StepContent: React.FC<StepContentProps> = ({
}) => {
const textContent = (
<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>
<Box
sx={{ ml: { xs: 0, md: 3 }, textAlign: { xs: "center", md: "left" } }}
>
<Typography
variant="h3"
component="h2"
sx={{ color: "primary.main", mb: 1 }}
>
<Box sx={{ ml: { xs: 0, md: 3 }, textAlign: { xs: 'center', md: 'left' } }}>
<Typography variant="h3" component="h2" sx={{ color: 'primary.main', mb: 1 }}>
{title}
</Typography>
<Box
sx={{
display: "flex",
display: 'flex',
gap: 1,
alignItems: "center",
justifyContent: { xs: "center", md: "flex-start" },
alignItems: 'center',
justifyContent: { xs: 'center', md: 'flex-start' },
}}
>
{icon}
@ -162,13 +156,13 @@ const StepContent: React.FC<StepContentProps> = ({
<Paper
sx={{
p: 2,
backgroundColor: "action.hover",
border: "1px solid",
borderColor: "action.active",
backgroundColor: 'action.hover',
border: '1px solid',
borderColor: 'action.active',
mt: 2,
}}
>
<Typography variant="body2" sx={{ fontStyle: "italic" }}>
<Typography variant="body2" sx={{ fontStyle: 'italic' }}>
<strong>Note:</strong> {note}
</Typography>
</Paper>
@ -177,12 +171,12 @@ const StepContent: React.FC<StepContentProps> = ({
<Paper
sx={{
p: 2,
backgroundColor: "secondary.main",
color: "secondary.contrastText",
backgroundColor: 'secondary.main',
color: 'secondary.contrastText',
mt: 2,
}}
>
<Typography variant="body1" sx={{ fontWeight: "bold" }}>
<Typography variant="body1" sx={{ fontWeight: 'bold' }}>
🎉 {success}
</Typography>
</Paper>
@ -233,7 +227,7 @@ const HeroButton = (props: HeroButtonProps) => {
fontWeight: 500,
backgroundColor: theme.palette.action.active,
color: theme.palette.background.paper,
"&:hover": {
'&:hover': {
backgroundColor: theme.palette.action.active,
opacity: 0.9,
},
@ -248,26 +242,26 @@ const HeroButton = (props: HeroButtonProps) => {
const HowItWorks: React.FC = () => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const handleGetStarted = () => {
navigate("/job-analysis");
navigate('/job-analysis');
};
return (
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{/* Hero Section */}
{/* Hero Section */}
<HeroSection>
<Container>
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", md: "row" },
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
gap: 4,
alignItems: "center",
alignItems: 'center',
flexGrow: 1,
maxWidth: "1024px",
maxWidth: '1024px',
}}
>
<Box sx={{ flex: 1, flexGrow: 1 }}>
@ -276,23 +270,19 @@ const HowItWorks: React.FC = () => {
component="h1"
sx={{
fontWeight: 700,
fontSize: { xs: "2rem", md: "3rem" },
fontSize: { xs: '2rem', md: '3rem' },
mb: 2,
color: "white",
color: 'white',
}}
>
Your complete professional story, beyond a single page
</Typography>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}>
Let potential employers discover the depth of your experience
through interactive Q&A and tailored resumes
Let potential employers discover the depth of your experience through interactive
Q&A and tailored resumes
</Typography>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<HeroButton
variant="contained"
size="large"
path="/login/register"
>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<HeroButton variant="contained" size="large" path="/login/register">
Get Started as Candidate
</HeroButton>
{/* <HeroButton
@ -310,8 +300,8 @@ const HowItWorks: React.FC = () => {
</Box>
<Box
sx={{
justifyContent: "center",
display: { xs: "none", md: "block" },
justifyContent: 'center',
display: { xs: 'none', md: 'block' },
}}
>
<Box
@ -319,9 +309,9 @@ const HowItWorks: React.FC = () => {
src={professionalConversationPng}
alt="Professional conversation"
sx={{
width: "100%",
width: '100%',
maxWidth: 200,
height: "auto",
height: 'auto',
borderRadius: 2,
boxShadow: 3,
}}
@ -332,22 +322,22 @@ const HowItWorks: React.FC = () => {
</HeroSection>
<HeroSection
sx={{
display: "flex",
position: "relative",
overflow: "hidden",
border: "2px solid orange",
display: 'flex',
position: 'relative',
overflow: 'hidden',
border: '2px solid orange',
}}
>
<Beta adaptive={false} sx={{ left: "-90px" }} />
<Container sx={{ display: "flex", position: "relative" }}>
<Beta adaptive={false} sx={{ left: '-90px' }} />
<Container sx={{ display: 'flex', position: 'relative' }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
textAlign: "center",
display: 'flex',
flexDirection: 'column',
textAlign: 'center',
maxWidth: 800,
mx: "auto",
position: "relative",
mx: 'auto',
position: 'relative',
}}
>
<Typography
@ -355,9 +345,9 @@ const HowItWorks: React.FC = () => {
component="h1"
sx={{
fontWeight: 700,
fontSize: { xs: "2rem", md: "2.5rem" },
fontSize: { xs: '2rem', md: '2.5rem' },
mb: 2,
color: "white",
color: 'white',
}}
>
Welcome to the Backstory Beta!
@ -371,9 +361,9 @@ const HowItWorks: React.FC = () => {
{/* Progress Overview */}
<Container sx={{ py: 4 }}>
<Box sx={{ display: { xs: "none", md: "block" } }}>
<Box sx={{ display: { xs: 'none', md: 'block' } }}>
<Stepper alternativeLabel sx={{ mb: 4 }}>
{steps.map((label) => (
{steps.map(label => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
@ -389,7 +379,7 @@ const HowItWorks: React.FC = () => {
stepNumber={1}
title="Select Job Analysis"
subtitle="Navigate to the main feature"
icon={<AssessmentIcon sx={{ color: "action.active" }} />}
icon={<AssessmentIcon sx={{ color: 'action.active' }} />}
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.",
]}
@ -406,9 +396,9 @@ const HowItWorks: React.FC = () => {
stepNumber={2}
title="Choose a Job"
subtitle="Pick from existing job postings"
icon={<WorkIcon sx={{ color: "action.active" }} />}
icon={<WorkIcon sx={{ color: 'action.active' }} />}
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}
imageAlt="Select a job from the available options"
@ -425,9 +415,9 @@ const HowItWorks: React.FC = () => {
stepNumber={3}
title="Select a Candidate"
subtitle="Choose from available profiles"
icon={<PersonIcon sx={{ color: "action.active" }} />}
icon={<PersonIcon sx={{ color: 'action.active' }} />}
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}
imageAlt="Select a candidate from the available profiles"
@ -443,10 +433,10 @@ const HowItWorks: React.FC = () => {
stepNumber={4}
title="Start Assessment"
subtitle="Begin the AI analysis"
icon={<PlayArrowIcon sx={{ color: "action.active" }} />}
icon={<PlayArrowIcon sx={{ color: 'action.active' }} />}
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.",
"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.",
'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.',
'To see that in action, click the "Start Skill Assessment" button.',
]}
imageSrc={selectStartAnalysisPng}
@ -463,10 +453,10 @@ const HowItWorks: React.FC = () => {
stepNumber={5}
title="Review Results"
subtitle="Watch the magic happen"
icon={<AutoAwesomeIcon sx={{ color: "action.active" }} />}
icon={<AutoAwesomeIcon sx={{ color: 'action.active' }} />}
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 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 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.',
]}
imageSrc={waitPng}
imageAlt="Wait for the analysis to complete and review results"
@ -481,7 +471,7 @@ const HowItWorks: React.FC = () => {
stepNumber={6}
title="Generate Resume"
subtitle="Create your tailored resume"
icon={<DescriptionIcon sx={{ color: "action.active" }} />}
icon={<DescriptionIcon sx={{ color: 'action.active' }} />}
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.',
"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 */}
<Box
sx={{
backgroundColor: "primary.main",
color: "primary.contrastText",
backgroundColor: 'primary.main',
color: 'primary.contrastText',
py: 6,
}}
>
<Container>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center',
maxWidth: 600,
mx: "auto",
mx: 'auto',
}}
>
<Typography
variant="h3"
component="h2"
gutterBottom
sx={{ color: "white" }}
>
<Typography variant="h3" component="h2" gutterBottom sx={{ color: 'white' }}>
Ready to try Backstory?
</Typography>
<Typography variant="h6" sx={{ mb: 4 }}>
@ -530,13 +515,13 @@ const HowItWorks: React.FC = () => {
startIcon={<PlayArrowIcon />}
onClick={handleGetStarted}
sx={{
backgroundColor: "action.active",
color: "background.paper",
fontWeight: "bold",
backgroundColor: 'action.active',
color: 'background.paper',
fontWeight: 'bold',
px: 4,
py: 1.5,
"&:hover": {
backgroundColor: "action.active",
'&:hover': {
backgroundColor: 'action.active',
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 {
Box,
Stepper,
@ -15,32 +15,28 @@ import {
Avatar,
useMediaQuery,
Divider,
} from "@mui/material";
import { Add, WorkOutline } from "@mui/icons-material";
import PersonIcon from "@mui/icons-material/Person";
import WorkIcon from "@mui/icons-material/Work";
import AssessmentIcon from "@mui/icons-material/Assessment";
import { JobMatchAnalysis } from "components/JobMatchAnalysis";
import { Candidate, Job, SkillAssessment } from "types/types";
import { useNavigate } from "react-router-dom";
import { BackstoryPageProps } from "components/BackstoryTab";
import { useAuth } from "hooks/AuthContext";
import {
useAppState,
useSelectedCandidate,
useSelectedJob,
} from "hooks/GlobalContext";
import { CandidateInfo } from "components/ui/CandidateInfo";
import { ComingSoon } from "components/ui/ComingSoon";
import { LoginRequired } from "components/ui/LoginRequired";
import { Scrollable } from "components/Scrollable";
import { CandidatePicker } from "components/ui/CandidatePicker";
import { JobPicker } from "components/ui/JobPicker";
import { JobCreator } from "components/JobCreator";
import { LoginRestricted } from "components/ui/LoginRestricted";
import JsonView from "@uiw/react-json-view";
import { ResumeGenerator } from "components/ResumeGenerator";
import { JobInfo } from "components/ui/JobInfo";
} from '@mui/material';
import { Add, WorkOutline } from '@mui/icons-material';
import PersonIcon from '@mui/icons-material/Person';
import WorkIcon from '@mui/icons-material/Work';
import AssessmentIcon from '@mui/icons-material/Assessment';
import { JobMatchAnalysis } from 'components/JobMatchAnalysis';
import { Candidate, Job, SkillAssessment } from 'types/types';
import { useNavigate } from 'react-router-dom';
import { BackstoryPageProps } from 'components/BackstoryTab';
import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
import { CandidateInfo } from 'components/ui/CandidateInfo';
import { ComingSoon } from 'components/ui/ComingSoon';
import { LoginRequired } from 'components/ui/LoginRequired';
import { Scrollable } from 'components/Scrollable';
import { CandidatePicker } from 'components/ui/CandidatePicker';
import { JobPicker } from 'components/ui/JobPicker';
import { JobCreator } from 'components/JobCreator';
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() {
return (
@ -48,19 +44,19 @@ function WorkAddIcon() {
position="relative"
display="inline-flex"
sx={{
lineHeight: "30px",
mb: "6px",
lineHeight: '30px',
mb: '6px',
}}
>
<WorkOutline sx={{ fontSize: 24 }} />
<Add
sx={{
position: "absolute",
position: 'absolute',
bottom: -2,
right: -2,
fontSize: 14,
bgcolor: "background.paper",
borderRadius: "50%",
bgcolor: 'background.paper',
borderRadius: '50%',
boxShadow: 1,
}}
color="primary"
@ -93,20 +89,20 @@ const initialState: AnalysisState = {
// Steps in our process
const steps: Step[] = [
{ requiredState: [], title: "Job Selection", icon: <WorkIcon /> },
{ requiredState: ["job"], title: "Select Candidate", icon: <PersonIcon /> },
{ requiredState: [], title: 'Job Selection', icon: <WorkIcon /> },
{ requiredState: ['job'], title: 'Select Candidate', icon: <PersonIcon /> },
{
requiredState: ["job", "candidate"],
title: "Job Analysis",
requiredState: ['job', 'candidate'],
title: 'Job Analysis',
icon: <WorkIcon />,
},
{
requiredState: ["job", "candidate", "analysis"],
title: "Generated Resume",
requiredState: ['job', 'candidate', 'analysis'],
title: 'Generated Resume',
icon: <AssessmentIcon />,
},
].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) => {
@ -114,9 +110,7 @@ const capitalize = (str: string) => {
};
// Main component
const JobAnalysisPage: React.FC<BackstoryPageProps> = (
props: BackstoryPageProps
) => {
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const theme = useTheme();
const { user, guest } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
@ -124,22 +118,18 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
const [activeStep, setActiveStep] = useState<Step>(steps[0]);
const [error, setError] = useState<string | null>(null);
const [jobTab, setJobTab] = useState<string>("select");
const [analysisState, setAnalysisState] = useState<AnalysisState | null>(
null
);
const [jobTab, setJobTab] = useState<string>('select');
const [analysisState, setAnalysisState] = useState<AnalysisState | null>(null);
const [canAdvance, setCanAdvance] = useState<boolean>(false);
const scrollRef = useRef<HTMLDivElement>(null);
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const canAccessStep = useCallback(
(step: Step) => {
if (!analysisState) {
return;
}
const missing = step.requiredState.find(
(f) => !(analysisState as any)[f]
);
const missing = step.requiredState.find(f => !(analysisState as any)[f]);
return missing;
},
[analysisState]
@ -157,19 +147,13 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
};
setAnalysisState(analysis);
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) {
setActiveStep(steps[i]);
return;
}
}
}, [
analysisState,
selectedCandidate,
selectedJob,
setActiveStep,
canAccessStep,
]);
}, [analysisState, selectedCandidate, selectedJob, setActiveStep, canAccessStep]);
useEffect(() => {
if (activeStep.index === steps.length - 1) {
@ -185,7 +169,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
if (scrollRef.current) {
scrollRef.current.scrollTo({
top: 0,
behavior: "smooth",
behavior: 'smooth',
});
}
}, [setCanAdvance, analysisState, activeStep]);
@ -201,7 +185,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
}
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;
}
setActiveStep((prevActiveStep) => steps[prevActiveStep.index - 1]);
setActiveStep(prevActiveStep => steps[prevActiveStep.index - 1]);
};
const moveToStep = (step: number) => {
@ -254,17 +238,17 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
// Render function for the job description step
const renderJobDescription = () => {
return (
<Box sx={{ mt: 3, width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 3 }}>
<Box sx={{ mt: 3, width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={jobTab} onChange={handleTabChange} centered>
<Tab value="select" icon={<WorkOutline />} label="Select Job" />
<Tab value="create" icon={<WorkAddIcon />} label="Create Job" />
</Tabs>
</Box>
{jobTab === "select" && <JobPicker onSelect={onJobSelect} />}
{jobTab === "create" && user && <JobCreator onSave={onJobSelect} />}
{jobTab === "create" && guest && (
{jobTab === 'select' && <JobPicker onSelect={onJobSelect} />}
{jobTab === 'create' && user && <JobCreator onSave={onJobSelect} />}
{jobTab === 'create' && guest && (
<LoginRestricted>
<JobCreator onSave={onJobSelect} />
</LoginRestricted>
@ -312,11 +296,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
if (!analysisState) {
return;
}
if (
!analysisState.job ||
!analysisState.candidate ||
!analysisState.analysis
) {
if (!analysisState.job || !analysisState.candidate || !analysisState.analysis) {
return <></>;
}
@ -334,28 +314,24 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%" /* Restrict to main-container's height */,
width: "100%",
display: 'flex',
flexDirection: 'column',
height: '100%' /* Restrict to main-container's height */,
width: '100%',
minHeight: 0 /* Prevent flex overflow */,
maxHeight: "min-content",
"& > *:not(.Scrollable)": {
maxHeight: 'min-content',
'& > *:not(.Scrollable)': {
flexShrink: 0 /* Prevent shrinking */,
},
position: "relative",
position: 'relative',
}}
>
<Paper elevation={4} sx={{ m: 0, borderRadius: 0, mb: 1, p: 0, gap: 1 }}>
<Stepper
activeStep={activeStep.index}
alternativeLabel
sx={{ mt: 2, mb: 2 }}
>
<Stepper activeStep={activeStep.index} alternativeLabel sx={{ mt: 2, mb: 2 }}>
{steps.map((step, index) => (
<Step>
<StepLabel
sx={{ cursor: "pointer" }}
sx={{ cursor: 'pointer' }}
onClick={() => {
moveToStep(index);
}}
@ -368,7 +344,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
activeStep.index >= step.index
? theme.palette.primary.main
: theme.palette.grey[300],
color: "white",
color: 'white',
}}
>
{step.icon}
@ -381,18 +357,16 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
</Step>
))}
</Stepper>
<Box
sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}
>
<Box sx={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row' }}>
{analysisState && analysisState.job && (
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}>
<Box sx={{ display: 'flex', flexDirection: 'row', width: '100%' }}>
{!isMobile && (
<Avatar
sx={{
ml: 1,
mt: 1,
bgcolor: theme.palette.primary.main,
color: "white",
color: 'white',
}}
>
<WorkIcon />
@ -401,21 +375,11 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
<JobInfo variant="minimal" job={analysisState.job} />
</Box>
)}
{isMobile && (
<Box
sx={{ display: "flex", borderBottom: "1px solid lightgrey" }}
/>
)}
{!isMobile && (
<Box sx={{ display: "flex", borderLeft: "1px solid lightgrey" }} />
)}
{isMobile && <Box sx={{ display: 'flex', borderBottom: '1px solid lightgrey' }} />}
{!isMobile && <Box sx={{ display: 'flex', borderLeft: '1px solid lightgrey' }} />}
{analysisState && analysisState.candidate && (
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}>
<CandidateInfo
variant="minimal"
candidate={analysisState.candidate}
sx={{}}
/>
<Box sx={{ display: 'flex', flexDirection: 'row', width: '100%' }}>
<CandidateInfo variant="minimal" candidate={analysisState.candidate} sx={{}} />
</Box>
)}
</Box>
@ -423,22 +387,22 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
<Scrollable
ref={scrollRef}
sx={{
position: "relative",
maxHeight: "100%",
width: "100%",
display: "flex",
position: 'relative',
maxHeight: '100%',
width: '100%',
display: 'flex',
flexGrow: 1,
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 === "select-candidate" && renderCandidateSelection()}
{activeStep.label === "job-analysis" && renderAnalysis()}
{activeStep.label === "generated-resume" && renderResume()}
{activeStep.label === 'job-selection' && renderJobDescription()}
{activeStep.label === 'select-candidate' && renderCandidateSelection()}
{activeStep.label === 'job-analysis' && renderAnalysis()}
{activeStep.label === 'generated-resume' && renderResume()}
</Scrollable>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Button
color="inherit"
disabled={activeStep.index === steps[0].index}
@ -447,7 +411,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
>
Back
</Button>
<Box sx={{ flex: "1 1 auto" }} />
<Box sx={{ flex: '1 1 auto' }} />
{activeStep.index === steps[steps.length - 1].index ? (
<Button
@ -460,12 +424,8 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
Start New Analysis
</Button>
) : (
<Button
disabled={!canAdvance}
onClick={handleNext}
variant="contained"
>
{activeStep.index === steps.length - 1 ? "Done" : "Next"}
<Button disabled={!canAdvance} onClick={handleNext} variant="contained">
{activeStep.index === steps.length - 1 ? 'Done' : 'Next'}
</Button>
)}
</Box>
@ -475,13 +435,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (
open={!!error}
autoHideDuration={6000}
onClose={() => setError(null)}
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={() => setError(null)}
severity="error"
sx={{ width: "100%" }}
>
<Alert onClose={() => setError(null)} severity="error" sx={{ width: '100%' }}>
{error}
</Alert>
</Snackbar>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,5 @@
import React from "react";
import {
Box,
Card,
CardContent,
Typography,
Button,
LinearProgress,
Stack,
} from "@mui/material";
import React from 'react';
import { Box, Card, CardContent, Typography, Button, LinearProgress, Stack } from '@mui/material';
import {
Add as AddIcon,
Visibility as VisibilityIcon,
@ -15,13 +7,13 @@ import {
ContactMail as ContactMailIcon,
Edit as EditIcon,
TipsAndUpdates as TipsIcon,
} from "@mui/icons-material";
import { useAuth } from "hooks/AuthContext";
import { LoginRequired } from "components/ui/LoginRequired";
import { BackstoryElementProps } from "components/BackstoryTab";
import { useNavigate } from "react-router-dom";
import { ComingSoon } from "components/ui/ComingSoon";
import { useAppState } from "hooks/GlobalContext";
} from '@mui/icons-material';
import { useAuth } from 'hooks/AuthContext';
import { LoginRequired } from 'components/ui/LoginRequired';
import { BackstoryElementProps } from 'components/BackstoryTab';
import { useNavigate } from 'react-router-dom';
import { ComingSoon } from 'components/ui/ComingSoon';
import { useAppState } from 'hooks/GlobalContext';
type CandidateDashboardProps = BackstoryElementProps;
@ -35,12 +27,12 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
return <LoginRequired asset="candidate dashboard" />;
}
if (user?.userType !== "candidate") {
if (user?.userType !== 'candidate') {
setSnack(
`The page you were on is only available for candidates (you are a ${user.userType}`,
"warning"
'warning'
);
navigate("/");
navigate('/');
return <></>;
}
@ -51,7 +43,7 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
<Box sx={{ flex: 1, p: 3 }}>
{/* Welcome Section */}
<Box sx={{ mb: 4 }}>
<Typography variant="h4" sx={{ mb: 2, fontWeight: "bold" }}>
<Typography variant="h4" sx={{ mb: 2, fontWeight: 'bold' }}>
Welcome back, {user.firstName}!
</Typography>
@ -65,9 +57,9 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
sx={{
height: 8,
borderRadius: 4,
backgroundColor: "#e0e0e0",
"& .MuiLinearProgress-bar": {
backgroundColor: "#4caf50",
backgroundColor: '#e0e0e0',
'& .MuiLinearProgress-bar': {
backgroundColor: '#4caf50',
},
}}
/>
@ -77,9 +69,9 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
variant="contained"
color="primary"
sx={{ mt: 1 }}
onClick={(e) => {
onClick={e => {
e.stopPropagation();
navigate("/candidate/profile");
navigate('/candidate/profile');
}}
>
Complete Your Profile
@ -87,21 +79,21 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
</Box>
{/* Cards Grid */}
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Top Row */}
<Box sx={{ display: "flex", gap: 3 }}>
<Box sx={{ display: 'flex', gap: 3 }}>
{/* Resume Builder Card */}
<Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: "bold" }}>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Resume Builder
</Typography>
<Typography variant="body2" sx={{ mb: 1, color: "#666" }}>
<Typography variant="body2" sx={{ mb: 1, color: '#666' }}>
3 custom resumes
</Typography>
<Typography variant="body2" sx={{ mb: 3, color: "#666" }}>
<Typography variant="body2" sx={{ mb: 3, color: '#666' }}>
Last created: May 15, 2025
</Typography>
@ -114,25 +106,23 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
{/* Recent Activity Card */}
<Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: "bold" }}>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Recent Activity
</Typography>
<Stack spacing={1} sx={{ mb: 3 }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<VisibilityIcon sx={{ fontSize: 16, color: "#666" }} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<VisibilityIcon sx={{ fontSize: 16, color: '#666' }} />
<Typography variant="body2">5 profile views</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<DownloadIcon sx={{ fontSize: 16, color: "#666" }} />
<Typography variant="body2">
2 resume downloads
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<DownloadIcon sx={{ fontSize: 16, color: '#666' }} />
<Typography variant="body2">2 resume downloads</Typography>
</Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<ContactMailIcon sx={{ fontSize: 16, color: "#666" }} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ContactMailIcon sx={{ fontSize: 16, color: '#666' }} />
<Typography variant="body2">1 direct contact</Typography>
</Box>
</Stack>
@ -145,31 +135,22 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
</Box>
{/* Bottom Row */}
<Box sx={{ display: "flex", gap: 3 }}>
<Box sx={{ display: 'flex', gap: 3 }}>
{/* Complete Your Backstory Card */}
<Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: "bold" }}>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Complete Your Backstory
</Typography>
<Stack spacing={1} sx={{ mb: 3 }}>
<Typography
variant="body2"
sx={{ display: "flex", alignItems: "center" }}
>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
Add projects
</Typography>
<Typography
variant="body2"
sx={{ display: "flex", alignItems: "center" }}
>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
Detail skills
</Typography>
<Typography
variant="body2"
sx={{ display: "flex", alignItems: "center" }}
>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
Work history
</Typography>
</Stack>
@ -183,21 +164,15 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
{/* Improvement Suggestions Card */}
<Card sx={{ flex: 1, minHeight: 200 }}>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: "bold" }}>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 'bold' }}>
Improvement Suggestions
</Typography>
<Stack spacing={1} sx={{ mb: 3 }}>
<Typography
variant="body2"
sx={{ display: "flex", alignItems: "center" }}
>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
Add certifications
</Typography>
<Typography
variant="body2"
sx={{ display: "flex", alignItems: "center" }}
>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
Enhance your project details
</Typography>
</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 {
Box,
Button,
@ -16,10 +16,10 @@ import {
CircularProgress,
Snackbar,
Alert,
} from "@mui/material";
import { styled } from "@mui/material/styles";
import { CloudUpload, PhotoCamera } from "@mui/icons-material";
import { useTheme } from "@mui/material/styles";
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { CloudUpload, PhotoCamera } from '@mui/icons-material';
import { useTheme } from '@mui/material/styles';
// import { Beta } from '../components/Beta';
// Interfaces
@ -34,21 +34,21 @@ interface ProfileFormData {
}
// Styled components
const VisuallyHiddenInput = styled("input")({
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
const VisuallyHiddenInput = styled('input')({
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: 1,
overflow: "hidden",
position: "absolute",
overflow: 'hidden',
position: 'absolute',
bottom: 0,
left: 0,
whiteSpace: "nowrap",
whiteSpace: 'nowrap',
width: 1,
});
const CreateProfilePage: React.FC = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
// State management
const [activeStep, setActiveStep] = useState<number>(0);
@ -58,29 +58,25 @@ const CreateProfilePage: React.FC = () => {
const [snackbar, setSnackbar] = useState<{
open: boolean;
message: string;
severity: "success" | "error";
severity: 'success' | 'error';
}>({
open: false,
message: "",
severity: "success",
message: '',
severity: 'success',
});
const [formData, setFormData] = useState<ProfileFormData>({
firstName: "",
lastName: "",
email: "",
phoneNumber: "",
jobTitle: "",
location: "",
bio: "",
firstName: '',
lastName: '',
email: '',
phoneNumber: '',
jobTitle: '',
location: '',
bio: '',
});
// Steps for the profile creation process
const steps = [
"Personal Information",
"Professional Details",
"Resume Upload",
];
const steps = ['Personal Information', 'Professional Details', 'Resume Upload'];
// Handle form input changes
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -95,7 +91,7 @@ const CreateProfilePage: React.FC = () => {
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onload = (event) => {
reader.onload = event => {
if (event.target?.result) {
setProfileImage(event.target.result.toString());
}
@ -111,7 +107,7 @@ const CreateProfilePage: React.FC = () => {
setSnackbar({
open: true,
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) {
handleSubmit();
} else {
setActiveStep((prevStep) => prevStep + 1);
setActiveStep(prevStep => prevStep + 1);
}
};
const handleBack = () => {
setActiveStep((prevStep) => prevStep - 1);
setActiveStep(prevStep => prevStep - 1);
};
// Form submission
@ -138,8 +134,8 @@ const CreateProfilePage: React.FC = () => {
setLoading(false);
setSnackbar({
open: true,
message: "Profile created successfully! Redirecting to dashboard...",
severity: "success",
message: 'Profile created successfully! Redirecting to dashboard...',
severity: 'success',
});
// Redirect would happen here in a real application
@ -152,12 +148,12 @@ const CreateProfilePage: React.FC = () => {
switch (activeStep) {
case 0:
return (
formData.firstName.trim() !== "" &&
formData.lastName.trim() !== "" &&
formData.email.trim() !== ""
formData.firstName.trim() !== '' &&
formData.lastName.trim() !== '' &&
formData.email.trim() !== ''
);
case 1:
return formData.jobTitle.trim() !== "";
return formData.jobTitle.trim() !== '';
case 2:
return resumeFile !== null;
default:
@ -171,16 +167,16 @@ const CreateProfilePage: React.FC = () => {
case 0:
return (
<Grid container spacing={3}>
<Grid size={{ xs: 12 }} sx={{ textAlign: "center", mb: 2 }}>
<Grid size={{ xs: 12 }} sx={{ textAlign: 'center', mb: 2 }}>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar
src={profileImage || ""}
src={profileImage || ''}
sx={{
width: 120,
height: 120,
@ -188,17 +184,9 @@ const CreateProfilePage: React.FC = () => {
border: `2px solid ${theme.palette.primary.main}`,
}}
/>
<IconButton
color="primary"
aria-label="upload picture"
component="label"
>
<IconButton color="primary" aria-label="upload picture" component="label">
<PhotoCamera />
<VisuallyHiddenInput
type="file"
accept="image/*"
onChange={handleImageUpload}
/>
<VisuallyHiddenInput type="file" accept="image/*" onChange={handleImageUpload} />
</IconButton>
<Typography variant="caption" color="textSecondary">
Add profile photo
@ -296,11 +284,10 @@ const CreateProfilePage: React.FC = () => {
<Grid container spacing={3}>
<Grid size={{ xs: 12 }}>
<Typography variant="body1" component="p">
Upload your resume to complete your profile. We'll analyze it to
better understand your skills and experience. (Supported
formats: .pdf, .docx, .md, and .txt)
Upload your resume to complete your profile. We'll analyze it to better understand
your skills and experience. (Supported formats: .pdf, .docx, .md, and .txt)
</Typography>
<Box sx={{ textAlign: "center", mt: 2 }}>
<Box sx={{ textAlign: 'center', mt: 2 }}>
<Button
component="label"
variant="contained"
@ -316,11 +303,7 @@ const CreateProfilePage: React.FC = () => {
</Button>
{resumeFile && (
<Typography
variant="body2"
color="textSecondary"
sx={{ mt: 1 }}
>
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
File uploaded: {resumeFile.name}
</Typography>
)}
@ -329,7 +312,7 @@ const CreateProfilePage: React.FC = () => {
</Grid>
);
default:
return "Unknown step";
return 'Unknown step';
}
};
@ -350,10 +333,10 @@ const CreateProfilePage: React.FC = () => {
<Stepper
activeStep={activeStep}
alternativeLabel={!isMobile}
orientation={isMobile ? "vertical" : "horizontal"}
orientation={isMobile ? 'vertical' : 'horizontal'}
sx={{ mt: 3, mb: 5 }}
>
{steps.map((label) => (
{steps.map(label => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
@ -362,23 +345,17 @@ const CreateProfilePage: React.FC = () => {
<Box sx={{ mt: 2, mb: 4 }}>{getStepContent(activeStep)}</Box>
<Box sx={{ display: "flex", justifyContent: "space-between", mt: 4 }}>
<Button
disabled={activeStep === 0}
onClick={handleBack}
variant="outlined"
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 4 }}>
<Button disabled={activeStep === 0} onClick={handleBack} variant="outlined">
Back
</Button>
<Button
variant="contained"
onClick={handleNext}
disabled={!isStepValid() || loading}
startIcon={
loading ? <CircularProgress size={20} color="inherit" /> : null
}
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : null}
>
{activeStep === steps.length - 1 ? "Create Profile" : "Next"}
{activeStep === steps.length - 1 ? 'Create Profile' : 'Next'}
</Button>
</Box>
</Paper>
@ -391,7 +368,7 @@ const CreateProfilePage: React.FC = () => {
<Alert
onClose={() => setSnackbar({ ...snackbar, open: false })}
severity={snackbar.severity}
sx={{ width: "100%" }}
sx={{ width: '100%' }}
>
{snackbar.message}
</Alert>

View File

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

View File

@ -1,21 +1,19 @@
import React, { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Box } from "@mui/material";
import React, { useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Box } from '@mui/material';
import { SetSnackType } from "../components/Snack";
import { LoadingComponent } from "../components/LoadingComponent";
import { User, Guest, Candidate } from "types/types";
import { useAuth } from "hooks/AuthContext";
import { useAppState, useSelectedCandidate } from "hooks/GlobalContext";
import { SetSnackType } from '../components/Snack';
import { LoadingComponent } from '../components/LoadingComponent';
import { User, Guest, Candidate } from 'types/types';
import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
interface CandidateRouteProps {
guest?: Guest | null;
user?: User | null;
}
const CandidateRoute: React.FC<CandidateRouteProps> = (
props: CandidateRouteProps
) => {
const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProps) => {
const { apiClient } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const { setSnack } = useAppState();
@ -30,22 +28,15 @@ const CandidateRoute: React.FC<CandidateRouteProps> = (
try {
const result: Candidate = await apiClient.getCandidate(reference);
setSelectedCandidate(result);
navigate("/chat");
navigate('/chat');
} catch {
setSnack(`Unable to obtain information for ${username}.`, "error");
navigate("/");
setSnack(`Unable to obtain information for ${username}.`, 'error');
navigate('/');
}
};
getCandidate(username);
}, [
setSelectedCandidate,
selectedCandidate,
username,
navigate,
setSnack,
apiClient,
]);
}, [setSelectedCandidate, selectedCandidate, username, navigate, setSnack, apiClient]);
if (selectedCandidate?.username !== username) {
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
*/
export function toSnakeCase<T extends Record<string, any>>(
obj: T
): Record<string, any> {
if (!obj || typeof obj !== "object") return obj;
export function toSnakeCase<T extends Record<string, any>>(obj: T): Record<string, any> {
if (!obj || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) {
return obj.map((item) => toSnakeCase(item));
return obj.map(item => toSnakeCase(item));
}
const result: Record<string, any> = {};
@ -29,13 +27,13 @@ export function toSnakeCase<T extends Record<string, any>>(
if (value === null || value === undefined) {
result[snakeCaseKey] = value;
} else if (Array.isArray(value)) {
result[snakeCaseKey] = value.map((item) =>
typeof item === "object" && item !== null ? toSnakeCase(item) : item
result[snakeCaseKey] = value.map(item =>
typeof item === 'object' && item !== null ? toSnakeCase(item) : item
);
} else if (value instanceof Date) {
// Convert Date to ISO string for Python datetime
result[snakeCaseKey] = value.toISOString();
} else if (typeof value === "object") {
} else if (typeof value === 'object') {
result[snakeCaseKey] = toSnakeCase(value);
} else {
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
*/
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)) {
return obj.map((item) => toCamelCase(item)) as T;
return obj.map(item => toCamelCase(item)) as T;
}
const result: Record<string, any> = {};
@ -63,13 +61,13 @@ export function toCamelCase<T>(obj: Record<string, any>): T {
if (value === null || value === undefined) {
result[camelCaseKey] = value;
} else if (Array.isArray(value)) {
result[camelCaseKey] = value.map((item) =>
typeof item === "object" && item !== null ? toCamelCase(item) : item
result[camelCaseKey] = value.map(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
result[camelCaseKey] = new Date(value);
} else if (typeof value === "object") {
} else if (typeof value === 'object') {
result[camelCaseKey] = toCamelCase(value);
} else {
result[camelCaseKey] = value;
@ -83,7 +81,7 @@ export function toCamelCase<T>(obj: Record<string, any>): T {
* Helper function to convert camelCase to snake_case
*/
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
*/
function isIsoDateString(value: string): boolean {
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(
value
);
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(value);
}
// ============================
@ -110,9 +106,7 @@ function isIsoDateString(value: string): boolean {
/**
* Format data for API requests (converts to format expected by Python backend)
*/
export function formatApiRequest<T extends Record<string, any>>(
data: T
): Record<string, any> {
export function formatApiRequest<T extends Record<string, any>>(data: T): Record<string, any> {
if (!data) return data;
// 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) {
formatted[key] = value.toISOString();
} else if (Array.isArray(value)) {
formatted[key] = value.map((item) => {
formatted[key] = value.map(item => {
if (item instanceof Date) {
return item.toISOString();
} else if (typeof item === "object" && item !== null) {
} else if (typeof item === 'object' && item !== null) {
return formatApiRequest(item);
}
return item;
});
} else if (typeof value === "object" && value !== null) {
} else if (typeof value === 'object' && value !== null) {
formatted[key] = formatApiRequest(value);
} else {
formatted[key] = value;
@ -145,12 +139,12 @@ export function formatApiRequest<T extends Record<string, any>>(
* Parse API responses and convert to TypeScript format
*/
export function parseApiResponse<T>(data: any): ApiResponse<T> {
if (!data || typeof data !== "object") {
if (!data || typeof data !== 'object') {
return {
success: false,
error: {
code: "INVALID_RESPONSE",
message: "Invalid response format",
code: 'INVALID_RESPONSE',
message: 'Invalid response format',
},
};
}
@ -204,12 +198,12 @@ export function toUrlParams(obj: Record<string, any>): URLSearchParams {
if (value !== null && value !== undefined) {
if (Array.isArray(value)) {
// Handle arrays by adding multiple params with same key
value.forEach((item) => {
value.forEach(item => {
params.append(key, String(item));
});
} else if (value instanceof Date) {
params.append(key, value.toISOString());
} else if (typeof value === "object") {
} else if (typeof value === 'object') {
// For nested objects, we could flatten or JSON stringify
params.append(key, JSON.stringify(value));
} else {
@ -228,17 +222,15 @@ export function toUrlParams(obj: Record<string, any>): URLSearchParams {
/**
* Check if response is a successful API response
*/
export function isSuccessResponse<T>(
response: any
): response is SuccessApiResponse<T> {
return response && typeof response === "object" && response.success === true;
export function isSuccessResponse<T>(response: any): response is SuccessApiResponse<T> {
return response && typeof response === 'object' && response.success === true;
}
/**
* Check if response is an error API response
*/
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)
? response.error?.message || "Unknown API error"
: "Invalid API response format";
? response.error?.message || 'Unknown API error'
: 'Invalid API response format';
throw new Error(errorMessage);
}
@ -298,7 +290,7 @@ export interface PaginatedRequest {
page?: number;
limit?: number;
sortBy?: string;
sortOrder?: "asc" | "desc";
sortOrder?: 'asc' | 'desc';
filters?: Record<string, any>;
}
@ -309,13 +301,11 @@ export interface PaginatedRequest {
/**
* Create a paginated request with defaults
*/
export function createPaginatedRequest(
params: Partial<PaginatedRequest> = {}
): PaginatedRequest {
export function createPaginatedRequest(params: Partial<PaginatedRequest> = {}): PaginatedRequest {
return {
page: 1,
limit: 20,
sortOrder: "desc",
sortOrder: 'desc',
...params,
};
}
@ -360,12 +350,12 @@ export async function handlePaginatedApiResponse<T>(
/**
* Log conversion for debugging
*/
export function debugConversion<T>(obj: T, label = "Object"): T {
if (process.env.NODE_ENV === "development") {
export function debugConversion<T>(obj: T, label = 'Object'): T {
if (process.env.NODE_ENV === 'development') {
console.group(`🔄 ${label} Conversion`);
console.log("Original:", obj);
if (typeof obj === "object" && obj !== null) {
console.log("Formatted for API:", formatApiRequest(obj as any));
console.log('Original:', obj);
if (typeof obj === 'object' && obj !== null) {
console.log('Formatted for API:', formatApiRequest(obj as any));
}
console.groupEnd();
}

View File

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

File diff suppressed because it is too large Load Diff