Compare commits

..

2 Commits

Author SHA1 Message Date
66b68270cd Prettier / eslint reformatting 2025-06-18 14:34:52 -07:00
f9307070a3 Reformatting tsx 2025-06-18 14:26:07 -07:00
84 changed files with 10737 additions and 8310 deletions

4
frontend/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
build
coverage

42
frontend/.eslintrc.json Normal file
View File

@ -0,0 +1,42 @@
{
"env": {
"browser": true,
"jest": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": [
"@typescript-eslint",
"react",
"react-hooks",
"prettier"
],
"rules": {
"react/prop-types": "off",
"@typescript-eslint/explicit-function-return-type": "warn",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }],
"prettier/prettier": "error"
},
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
"typescript": {}
}
}
}

10
frontend/.prettierrc Normal file
View File

@ -0,0 +1,10 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid"
}

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@
"react-phone-number-input": "^3.4.12", "react-phone-number-input": "^3.4.12",
"react-plotly.js": "^2.6.0", "react-plotly.js": "^2.6.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"react-scripts": "5.0.1", "react-scripts": "^5.0.1",
"react-spinners": "^0.15.0", "react-spinners": "^0.15.0",
"react-to-print": "^3.1.0", "react-to-print": "^3.1.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
@ -50,7 +50,10 @@
"scripts": { "scripts": {
"start": "WDS_SOCKET_HOST=backstory-beta.ketrenos.com WDS_SOCKET_PORT=443 craco start", "start": "WDS_SOCKET_HOST=backstory-beta.ketrenos.com WDS_SOCKET_PORT=443 craco start",
"build": "craco build", "build": "craco build",
"test": "craco test" "test": "craco test",
"lint": "eslint src/**/*.{ts,tsx} --no-color",
"lint:fix": "eslint src/**/*.{ts,tsx} --fix --no-color",
"format": "prettier --write src/**/*.{ts,tsx}"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -73,6 +76,14 @@
"devDependencies": { "devDependencies": {
"@craco/craco": "^7.1.0", "@craco/craco": "^7.1.0",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/plotly.js": "^2.35.5" "@types/plotly.js": "^2.35.5",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^2.8.8"
} }
} }

View File

@ -15,7 +15,7 @@ pre {
overflow: auto; overflow: auto;
white-space: pre-wrap; white-space: pre-wrap;
box-sizing: border-box; box-sizing: border-box;
border: 3px solid #E0E0E0; border: 3px solid #e0e0e0;
} }
button { button {
@ -72,8 +72,8 @@ button {
.Controls { .Controls {
display: flex; display: flex;
background-color: #F5F5F5; background-color: #f5f5f5;
border: 1px solid #E0E0E0; border: 1px solid #e0e0e0;
overflow-y: auto; overflow-y: auto;
padding: 10px; padding: 10px;
flex-direction: column; flex-direction: column;
@ -93,8 +93,8 @@ button {
flex-direction: column; flex-direction: column;
min-width: 10rem; min-width: 10rem;
flex-grow: 1; flex-grow: 1;
background-color: #1A2536; /* Midnight Blue */ background-color: #1a2536; /* Midnight Blue */
color: #D3CDBF; /* Warm Gray */ color: #d3cdbf; /* Warm Gray */
border-radius: 0; border-radius: 0;
} }
@ -115,12 +115,12 @@ button {
max-width: 1024px; max-width: 1024px;
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
background-color: #D3CDBF; background-color: #d3cdbf;
} }
.user-message.MuiCard-root { .user-message.MuiCard-root {
background-color: #DCF8C6; background-color: #dcf8c6;
border: 1px solid #B2E0A7; border: 1px solid #b2e0a7;
color: #333333; color: #333333;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
margin-left: 1rem; margin-left: 1rem;
@ -140,8 +140,8 @@ button {
.Docs.MuiCard-root, .Docs.MuiCard-root,
.assistant-message.MuiCard-root { .assistant-message.MuiCard-root {
border: 1px solid #E0E0E0; border: 1px solid #e0e0e0;
background-color: #FFFFFF; background-color: #ffffff;
color: #333333; color: #333333;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
margin-right: 1rem; margin-right: 1rem;
@ -158,7 +158,6 @@ button {
font-size: 0.9rem; font-size: 0.9rem;
} }
.Docs.MuiCard-root { .Docs.MuiCard-root {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
@ -193,7 +192,7 @@ button {
} }
.metadata { .metadata {
border: 1px solid #E0E0E0; border: 1px solid #e0e0e0;
font-size: 0.75rem; font-size: 0.75rem;
padding: 0.125rem; padding: 0.125rem;
} }
@ -239,7 +238,7 @@ button {
/* Reduce space around code blocks */ /* Reduce space around code blocks */
* .MuiTypography-root pre { * .MuiTypography-root pre {
border: 1px solid #F5F5F5; border: 1px solid #f5f5f5;
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
margin-top: 0; margin-top: 0;

View File

@ -1,9 +1,8 @@
import React, { useEffect, useState, useRef, useCallback } from 'react'; import React, { useEffect, useState, useRef, JSX } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { ThemeProvider } from '@mui/material/styles'; import { ThemeProvider } from '@mui/material/styles';
import { backstoryTheme } from './BackstoryTheme'; import { backstoryTheme } from './BackstoryTheme';
import { SeverityType } from 'components/Snack';
import { ConversationHandle } from 'components/Conversation'; import { ConversationHandle } from 'components/Conversation';
import { CandidateRoute } from 'routes/CandidateRoute'; import { CandidateRoute } from 'routes/CandidateRoute';
import { BackstoryLayout } from 'components/layout/BackstoryLayout'; import { BackstoryLayout } from 'components/layout/BackstoryLayout';
@ -17,23 +16,21 @@ import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css'; import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css'; import '@fontsource/roboto/700.css';
const BackstoryApp = () => { const BackstoryApp = (): JSX.Element => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const chatRef = useRef<ConversationHandle>(null); const chatRef = useRef<ConversationHandle>(null);
const snackRef = useRef<any>(null); const submitQuery = (query: ChatQuery): void => {
const setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity);
}, [snackRef]);
const submitQuery = (query: ChatQuery) => {
console.log(`handleSubmitChatQuery:`, query, chatRef.current ? ' sending' : 'no handler'); console.log(`handleSubmitChatQuery:`, query, chatRef.current ? ' sending' : 'no handler');
chatRef.current?.submitQuery(query); chatRef.current?.submitQuery(query);
navigate('/chat'); navigate('/chat');
}; };
const [page, setPage] = useState<string>(""); const [page, setPage] = useState<string>('');
useEffect(() => { useEffect(() => {
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/"; const currentRoute = location.pathname.split('/')[1]
? `/${location.pathname.split('/')[1]}`
: '/';
setPage(currentRoute); setPage(currentRoute);
}, [location.pathname]); }, [location.pathname]);
@ -43,14 +40,9 @@ const BackstoryApp = () => {
<AuthProvider> <AuthProvider>
<AppStateProvider> <AppStateProvider>
<Routes> <Routes>
<Route path="/u/:username" element={<CandidateRoute {...{ setSnack }} />} /> <Route path="/u/:username" element={<CandidateRoute />} />
{/* Static/shared routes */} {/* Static/shared routes */}
<Route <Route path="/*" element={<BackstoryLayout {...{ page, chatRef, submitQuery }} />} />
path="/*"
element={
<BackstoryLayout {...{ setSnack, page, chatRef, snackRef, submitQuery }} />
}
/>
</Routes> </Routes>
</AppStateProvider> </AppStateProvider>
</AuthProvider> </AuthProvider>
@ -58,6 +50,4 @@ const BackstoryApp = () => {
); );
}; };
export { export { BackstoryApp };
BackstoryApp
};

View File

@ -1,39 +1,39 @@
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { CandidateQuestion } from "types/types"; import { CandidateQuestion } from 'types/types';
type ChatSubmitQueryInterface = (query: CandidateQuestion) => void; type ChatSubmitQueryInterface = (query: CandidateQuestion) => void;
interface BackstoryQueryInterface { interface BackstoryQueryInterface {
question: CandidateQuestion, question: CandidateQuestion;
submitQuery?: ChatSubmitQueryInterface submitQuery?: ChatSubmitQueryInterface;
} }
const BackstoryQuery = (props : BackstoryQueryInterface) => { const BackstoryQuery = (props: BackstoryQueryInterface) => {
const { question, submitQuery } = props; const { question, submitQuery } = props;
if (submitQuery === undefined) { if (submitQuery === undefined) {
return (<Box>{question.question}</Box>); return <Box>{question.question}</Box>;
} }
return ( return (
<Button variant="outlined" sx={{ <Button
variant="outlined"
sx={{
color: theme => theme.palette.custom.highlight, // Golden Ochre (#D4A017) color: theme => theme.palette.custom.highlight, // Golden Ochre (#D4A017)
borderColor: theme => theme.palette.custom.highlight, borderColor: theme => theme.palette.custom.highlight,
m: 1 m: 1,
}} }}
size="small" onClick={(e: any) => { submitQuery(question); }}> size="small"
onClick={(e: any) => {
submitQuery(question);
}}
>
{question.question} {question.question}
</Button> </Button>
); );
}
export type {
BackstoryQueryInterface,
ChatSubmitQueryInterface,
}; };
export { export type { BackstoryQueryInterface, ChatSubmitQueryInterface };
BackstoryQuery,
};
export { BackstoryQuery };

View File

@ -7,47 +7,41 @@ import { SetSnackType } from './Snack';
interface BackstoryElementProps { interface BackstoryElementProps {
// setSnack: SetSnackType, // setSnack: SetSnackType,
// submitQuery: ChatSubmitQueryInterface, // submitQuery: ChatSubmitQueryInterface,
sx?: SxProps<Theme>, sx?: SxProps<Theme>;
} }
interface BackstoryPageProps extends BackstoryElementProps { interface BackstoryPageProps extends BackstoryElementProps {
route?: string, route?: string;
setRoute?: (route: string) => void, setRoute?: (route: string) => void;
}; }
interface BackstoryTabProps { interface BackstoryTabProps {
label?: string, label?: string;
path: string, path: string;
children?: ReactElement<BackstoryPageProps>, children?: ReactElement<BackstoryPageProps>;
active?: boolean, active?: boolean;
className?: string, className?: string;
tabProps?: { tabProps?: {
label?: string, label?: string;
sx?: SxProps, sx?: SxProps;
icon?: string | ReactElement<unknown, string | JSXElementConstructor<any>> | undefined, icon?: string | ReactElement<unknown, string | JSXElementConstructor<any>> | undefined;
iconPosition?: "bottom" | "top" | "start" | "end" | undefined iconPosition?: 'bottom' | 'top' | 'start' | 'end' | undefined;
} };
}; }
function BackstoryPage(props: BackstoryTabProps) { function BackstoryPage(props: BackstoryTabProps) {
const { className, active, children } = props; const { className, active, children } = props;
return ( return (
<Box <Box
className={ className || "BackstoryTab"} className={className || 'BackstoryTab'}
sx={{ "display": active ? "flex" : "none", p: 0, m: 0, borders: "none" }} sx={{ display: active ? 'flex' : 'none', p: 0, m: 0, borders: 'none' }}
> >
{children} {children}
</Box> </Box>
); );
} }
export type { export type { BackstoryPageProps, BackstoryTabProps, BackstoryElementProps };
BackstoryPageProps,
BackstoryTabProps,
BackstoryElementProps,
};
export { export { BackstoryPage };
BackstoryPage
}

View File

@ -1,4 +1,11 @@
import React, { useRef, useEffect, CSSProperties, KeyboardEvent, useState, useImperativeHandle } from 'react'; import React, {
useRef,
useEffect,
CSSProperties,
KeyboardEvent,
useState,
useImperativeHandle,
} from 'react';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import './BackstoryTextField.css'; import './BackstoryTextField.css';
@ -18,15 +25,9 @@ interface BackstoryTextFieldProps {
style?: CSSProperties; style?: CSSProperties;
} }
const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryTextFieldProps>((props, ref) => { const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryTextFieldProps>(
const { (props, ref) => {
value = '', const { value = '', disabled = false, placeholder, onEnter, onChange, style } = props;
disabled = false,
placeholder,
onEnter,
onChange,
style,
} = props;
const theme = useTheme(); const theme = useTheme();
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const shadowRef = useRef<HTMLTextAreaElement>(null); const shadowRef = useRef<HTMLTextAreaElement>(null);
@ -34,7 +35,7 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
// Sync editValue with prop value if it changes externally // Sync editValue with prop value if it changes externally
useEffect(() => { useEffect(() => {
setEditValue(value || ""); setEditValue(value || '');
}, [value]); }, [value]);
// Adjust textarea height based on content // Adjust textarea height based on content
@ -78,7 +79,11 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getValue: () => editValue, getValue: () => editValue,
setValue: (value: string) => setEditValue(value), setValue: (value: string) => setEditValue(value),
getAndResetValue: () => { const _ev = editValue; setEditValue(''); return _ev; } getAndResetValue: () => {
const _ev = editValue;
setEditValue('');
return _ev;
},
})); }));
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
@ -117,7 +122,10 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
value={editValue} value={editValue}
disabled={disabled} disabled={disabled}
placeholder={placeholder} placeholder={placeholder}
onChange={(e) => { setEditValue(e.target.value); onChange && onChange(e.target.value); }} onChange={e => {
setEditValue(e.target.value);
onChange && onChange(e.target.value);
}}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
style={fullStyle} style={fullStyle}
/> />
@ -142,10 +150,9 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
/> />
</> </>
); );
}); }
);
export type { export type { BackstoryTextFieldRef };
BackstoryTextFieldRef
};
export { BackstoryTextField }; export { BackstoryTextField };

View File

@ -1,4 +1,11 @@
import React, { useState, useImperativeHandle, forwardRef, useEffect, useRef, useCallback } from 'react'; import React, {
useState,
useImperativeHandle,
forwardRef,
useEffect,
useRef,
useCallback,
} from 'react';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@ -6,25 +13,43 @@ import Box from '@mui/material/Box';
import SendIcon from '@mui/icons-material/Send'; import SendIcon from '@mui/icons-material/Send';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import PropagateLoader from "react-spinners/PropagateLoader"; import PropagateLoader from 'react-spinners/PropagateLoader';
import { Message } from './Message'; import { Message } from './Message';
import { DeleteConfirmation } from 'components/DeleteConfirmation'; import { DeleteConfirmation } from 'components/DeleteConfirmation';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField'; import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { StreamingResponse } from 'services/api-client'; import { StreamingResponse } from 'services/api-client';
import { ChatMessage, ChatContext, ChatSession, ChatQuery, ChatMessageUser, ChatMessageError, ChatMessageStreaming, ChatMessageStatus } from 'types/types'; import {
ChatMessage,
ChatContext,
ChatSession,
ChatQuery,
ChatMessageUser,
ChatMessageError,
ChatMessageStreaming,
ChatMessageStatus,
} from 'types/types';
import { PaginatedResponse } from 'types/conversion'; import { PaginatedResponse } from 'types/conversion';
import './Conversation.css'; import './Conversation.css';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "assistant", metadata: null as any status: 'done',
type: 'text',
sessionId: '',
timestamp: new Date(),
content: '',
role: 'assistant',
metadata: null as any,
}; };
const loadingMessage: ChatMessage = { ...defaultMessage, content: "Establishing connection with server..." }; const loadingMessage: ChatMessage = {
...defaultMessage,
content: 'Establishing connection with server...',
};
type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check' | 'persona'; type ConversationMode = 'chat' | 'job_description' | 'resume' | 'fact_check' | 'persona';
@ -34,24 +59,25 @@ interface ConversationHandle {
} }
interface ConversationProps extends BackstoryElementProps { interface ConversationProps extends BackstoryElementProps {
className?: string, // Override default className className?: string; // Override default className
type: ConversationMode, // Type of Conversation chat type: ConversationMode; // Type of Conversation chat
placeholder?: string, // Prompt to display in TextField input placeholder?: string; // Prompt to display in TextField input
actionLabel?: string, // Label to put on the primary button actionLabel?: string; // Label to put on the primary button
resetAction?: () => void, // Callback when Reset is pressed resetAction?: () => void; // Callback when Reset is pressed
resetLabel?: string, // Label to put on Reset button resetLabel?: string; // Label to put on Reset button
defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField defaultPrompts?: React.ReactElement[]; // Set of Elements to display after the TextField
defaultQuery?: string, // Default text to populate the TextField input defaultQuery?: string; // Default text to populate the TextField input
preamble?: ChatMessage[], // Messages to display at start of Conversation until Action has been invoked preamble?: ChatMessage[]; // Messages to display at start of Conversation until Action has been invoked
hidePreamble?: boolean, // Whether to hide the preamble after an Action has been invoked hidePreamble?: boolean; // Whether to hide the preamble after an Action has been invoked
hideDefaultPrompts?: boolean, // Whether to hide the defaultPrompts after an Action has been invoked hideDefaultPrompts?: boolean; // Whether to hide the defaultPrompts after an Action has been invoked
messageFilter?: ((messages: ChatMessage[]) => ChatMessage[]) | undefined, // Filter callback to determine which Messages to display in Conversation messageFilter?: ((messages: ChatMessage[]) => ChatMessage[]) | undefined; // Filter callback to determine which Messages to display in Conversation
messages?: ChatMessage[], // messages?: ChatMessage[]; //
sx?: SxProps<Theme>, sx?: SxProps<Theme>;
onResponse?: ((message: ChatMessage) => void) | undefined, // Event called when a query completes (provides messages) onResponse?: ((message: ChatMessage) => void) | undefined; // Event called when a query completes (provides messages)
}; }
const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: ConversationProps, ref) => { const Conversation = forwardRef<ConversationHandle, ConversationProps>(
(props: ConversationProps, ref) => {
const { const {
actionLabel, actionLabel,
defaultPrompts, defaultPrompts,
@ -67,7 +93,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
sx, sx,
type, type,
} = props; } = props;
const { apiClient } = useAuth() const { apiClient } = useAuth();
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0); const [countdown, setCountdown] = useState<number>(0);
const [conversation, setConversation] = useState<ChatMessage[]>([]); const [conversation, setConversation] = useState<ChatMessage[]>([]);
@ -99,21 +125,19 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
// console.log('No message filter provided. Using all messages.', filtered); // console.log('No message filter provided. Using all messages.', filtered);
} else { } else {
//console.log('Filtering conversation...') //console.log('Filtering conversation...')
filtered = messageFilter(conversation); /* Do not copy conversation or useEffect will loop forever */ filtered =
messageFilter(conversation); /* Do not copy conversation or useEffect will loop forever */
//console.log(`${conversation.length - filtered.length} messages filtered out.`); //console.log(`${conversation.length - filtered.length} messages filtered out.`);
} }
if (filtered.length === 0) { if (filtered.length === 0) {
setFilteredConversation([ setFilteredConversation([...(preamble || []), ...(messages || [])]);
...(preamble || []),
...(messages || []),
]);
} else { } else {
setFilteredConversation([ setFilteredConversation([
...(hidePreamble ? [] : (preamble || [])), ...(hidePreamble ? [] : preamble || []),
...(messages || []), ...(messages || []),
...filtered, ...filtered,
]); ]);
}; }
}, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]); }, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]);
useEffect(() => { useEffect(() => {
@ -122,17 +146,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
} }
const createChatSession = async () => { const createChatSession = async () => {
try { try {
const chatContext: ChatContext = { type: "general" }; const chatContext: ChatContext = { type: 'general' };
const response: ChatSession = await apiClient.createChatSession(chatContext); const response: ChatSession = await apiClient.createChatSession(chatContext);
setChatSession(response); setChatSession(response);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setSnack("Unable to create chat session.", "error"); setSnack('Unable to create chat session.', 'error');
} }
}; };
createChatSession(); createChatSession();
}, [chatSession, setChatSession]); }, [chatSession, setChatSession]);
const getChatMessages = useCallback(async () => { const getChatMessages = useCallback(async () => {
@ -140,33 +163,38 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
return; return;
} }
try { try {
const response: PaginatedResponse<ChatMessage> = await apiClient.getChatMessages(chatSession.id); const response: PaginatedResponse<ChatMessage> = await apiClient.getChatMessages(
chatSession.id
);
const messages: ChatMessage[] = response.data; const messages: ChatMessage[] = response.data;
setProcessingMessage(undefined); setProcessingMessage(undefined);
setStreamingMessage(undefined); setStreamingMessage(undefined);
if (messages.length === 0) { if (messages.length === 0) {
console.log(`History returned with 0 entries`) console.log(`History returned with 0 entries`);
setConversation([]) setConversation([]);
setNoInteractions(true); setNoInteractions(true);
} else { } else {
console.log(`History returned with ${messages.length} entries:`, messages) console.log(`History returned with ${messages.length} entries:`, messages);
setConversation(messages); setConversation(messages);
setNoInteractions(false); setNoInteractions(false);
} }
} catch (error) { } catch (error) {
console.error('Unable to obtain chat history', error); console.error('Unable to obtain chat history', error);
setProcessingMessage({ ...defaultMessage, status: "error", content: `Unable to obtain history from server.` }); setProcessingMessage({
...defaultMessage,
status: 'error',
content: `Unable to obtain history from server.`,
});
setTimeout(() => { setTimeout(() => {
setProcessingMessage(undefined); setProcessingMessage(undefined);
setNoInteractions(true); setNoInteractions(true);
}, 3000); }, 3000);
setSnack("Unable to obtain chat history.", "error"); setSnack('Unable to obtain chat history.', 'error');
} }
}, [chatSession]); }, [chatSession]);
// Set the initial chat history to "loading" or the welcome message if loaded. // Set the initial chat history to "loading" or the welcome message if loaded.
useEffect(() => { useEffect(() => {
if (!chatSession) { if (!chatSession) {
@ -180,13 +208,12 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
setNoInteractions(true); setNoInteractions(true);
getChatMessages(); getChatMessages();
}, [chatSession]); }, [chatSession]);
const handleEnter = (value: string) => { const handleEnter = (value: string) => {
const query: ChatQuery = { const query: ChatQuery = {
prompt: value prompt: value,
} };
processQuery(query); processQuery(query);
}; };
@ -194,10 +221,11 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
submitQuery: (query: ChatQuery) => { submitQuery: (query: ChatQuery) => {
processQuery(query); processQuery(query);
}, },
fetchHistory: () => { getChatMessages(); } fetchHistory: () => {
getChatMessages();
},
})); }));
// const reset = async () => { // const reset = async () => {
// try { // try {
// const response = await fetch(connectionBase + `/api/reset/${sessionId}/${type}`, { // const response = await fetch(connectionBase + `/api/reset/${sessionId}/${type}`, {
@ -229,7 +257,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
// }; // };
const cancelQuery = () => { const cancelQuery = () => {
console.log("Stop query"); console.log('Stop query');
if (controllerRef.current) { if (controllerRef.current) {
controllerRef.current.cancel(); controllerRef.current.cancel();
} }
@ -249,30 +277,28 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
...defaultMessage, ...defaultMessage,
type: 'text', type: 'text',
content: query.prompt, content: query.prompt,
} },
]); ]);
setProcessing(true); setProcessing(true);
setProcessingMessage( setProcessingMessage({
{ ...defaultMessage, content: 'Submitting request...' } ...defaultMessage,
); content: 'Submitting request...',
});
const chatMessage: ChatMessageUser = { const chatMessage: ChatMessageUser = {
role: "user", role: 'user',
sessionId: chatSession.id, sessionId: chatSession.id,
content: query.prompt, content: query.prompt,
tunables: query.tunables, tunables: query.tunables,
status: "done", status: 'done',
type: "text", type: 'text',
timestamp: new Date() timestamp: new Date(),
}; };
controllerRef.current = apiClient.sendMessageStream(chatMessage, { controllerRef.current = apiClient.sendMessageStream(chatMessage, {
onMessage: (msg: ChatMessage) => { onMessage: (msg: ChatMessage) => {
console.log("onMessage:", msg); console.log('onMessage:', msg);
setConversation([ setConversation([...conversationRef.current, msg]);
...conversationRef.current,
msg
]);
setStreamingMessage(undefined); setStreamingMessage(undefined);
setProcessingMessage(undefined); setProcessingMessage(undefined);
setProcessing(false); setProcessing(false);
@ -281,32 +307,35 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
} }
}, },
onError: (error: string | ChatMessageError) => { onError: (error: string | ChatMessageError) => {
console.log("onError:", error); console.log('onError:', error);
// Type-guard to determine if this is a ChatMessageBase or a string // Type-guard to determine if this is a ChatMessageBase or a string
if (typeof error === "object" && error !== null && "content" in error) { if (typeof error === 'object' && error !== null && 'content' in error) {
setProcessingMessage(error as ChatMessage); setProcessingMessage(error as ChatMessage);
setProcessing(false); setProcessing(false);
controllerRef.current = null; controllerRef.current = null;
} else { } else {
setProcessingMessage({ ...defaultMessage, content: error as string }); setProcessingMessage({
...defaultMessage,
content: error as string,
});
} }
}, },
onStreaming: (chunk: ChatMessageStreaming) => { onStreaming: (chunk: ChatMessageStreaming) => {
console.log("onStreaming:", chunk); console.log('onStreaming:', chunk);
setStreamingMessage({ ...defaultMessage, ...chunk }); setStreamingMessage({ ...defaultMessage, ...chunk });
}, },
onStatus: (status: ChatMessageStatus) => { onStatus: (status: ChatMessageStatus) => {
console.log("onStatus:", status); console.log('onStatus:', status);
}, },
onComplete: () => { onComplete: () => {
console.log("onComplete"); console.log('onComplete');
controllerRef.current = null; controllerRef.current = null;
} },
}); });
}; };
if (!chatSession) { if (!chatSession) {
return (<></>); return <></>;
} }
return ( return (
// <Scrollable // <Scrollable
@ -320,28 +349,47 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
// ...sx // ...sx
// }} // }}
// > // >
<Box className="Conversation" sx={{ flexGrow: 1, minHeight: "max-content", height: "max-content", maxHeight: "max-content", overflow: "hidden" }}> <Box
className="Conversation"
sx={{
flexGrow: 1,
minHeight: 'max-content',
height: 'max-content',
maxHeight: 'max-content',
overflow: 'hidden',
}}
>
<Box sx={{ p: 1, mt: 0, ...sx }}> <Box sx={{ p: 1, mt: 0, ...sx }}>
{ {filteredConversation.map((message, index) => (
filteredConversation.map((message, index) => <Message key={index} {...{ chatSession, sendQuery: processQuery, message }} />
<Message key={index} {...{ chatSession, sendQuery: processQuery, message, }} /> ))}
) {processingMessage !== undefined && (
} <Message
{ {...{
processingMessage !== undefined && chatSession,
<Message {...{ chatSession, sendQuery: processQuery, message: processingMessage, }} /> sendQuery: processQuery,
} message: processingMessage,
{ }}
streamingMessage !== undefined && />
<Message {...{ chatSession, sendQuery: processQuery, message: streamingMessage }} /> )}
} {streamingMessage !== undefined && (
<Box sx={{ <Message
display: "flex", {...{
flexDirection: "column", chatSession,
alignItems: "center", sendQuery: processQuery,
justifyContent: "center", message: streamingMessage,
}}
/>
)}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
m: 1, m: 1,
}}> }}
>
<PropagateLoader <PropagateLoader
size="10px" size="10px"
loading={processing} loading={processing}
@ -352,16 +400,29 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
<Box <Box
sx={{ sx={{
pt: 1, pt: 1,
fontSize: "0.7rem", fontSize: '0.7rem',
color: "darkgrey" color: 'darkgrey',
}} }}
>Response will be stopped in: {countdown}s</Box> >
Response will be stopped in: {countdown}s
</Box>
)} )}
</Box> </Box>
<Box className="Query" sx={{ display: "flex", flexDirection: "column", p: 1, flexGrow: 1 }}> <Box
{placeholder && className="Query"
<Box sx={{ display: "flex", flexGrow: 1, p: 0, m: 0, flexDirection: "column" }} sx={{ display: 'flex', flexDirection: 'column', p: 1, flexGrow: 1 }}
ref={viewableElementRef}> >
{placeholder && (
<Box
sx={{
display: 'flex',
flexGrow: 1,
p: 0,
m: 0,
flexDirection: 'column',
}}
ref={viewableElementRef}
>
<BackstoryTextField <BackstoryTextField
ref={backstoryTextRef} ref={backstoryTextRef}
disabled={processing} disabled={processing}
@ -369,30 +430,53 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
placeholder={placeholder} placeholder={placeholder}
/> />
</Box> </Box>
} )}
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}> <Box
key="jobActions"
sx={{
display: 'flex',
justifyContent: 'center',
flexDirection: 'row',
}}
>
<DeleteConfirmation <DeleteConfirmation
label={resetLabel || "all data"} label={resetLabel || 'all data'}
disabled={!chatSession || processingMessage !== undefined || noInteractions} disabled={!chatSession || processingMessage !== undefined || noInteractions}
onDelete={() => { /*reset(); resetAction && resetAction(); */ }} /> onDelete={() => {
<Tooltip title={actionLabel || "Send"}> /*reset(); resetAction && resetAction(); */
<span style={{ display: "flex", flexGrow: 1 }}> }}
/>
<Tooltip title={actionLabel || 'Send'}>
<span style={{ display: 'flex', flexGrow: 1 }}>
<Button <Button
sx={{ m: 1, gap: 1, flexGrow: 1 }} sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained" variant="contained"
disabled={!chatSession || processingMessage !== undefined} disabled={!chatSession || processingMessage !== undefined}
onClick={() => { processQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}> onClick={() => {
{actionLabel}<SendIcon /> processQuery({
prompt:
(backstoryTextRef.current &&
backstoryTextRef.current.getAndResetValue()) ||
'',
});
}}
>
{actionLabel}
<SendIcon />
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
<Tooltip title="Cancel"> <Tooltip title="Cancel">
<span style={{ display: "flex" }}> { /* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} <span style={{ display: 'flex' }}>
{' '}
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton <IconButton
aria-label="cancel" aria-label="cancel"
onClick={() => { cancelQuery(); }} onClick={() => {
sx={{ display: "flex", margin: 'auto 0px' }} cancelQuery();
}}
sx={{ display: 'flex', margin: 'auto 0px' }}
size="large" size="large"
edge="start" edge="start"
disabled={stopRef.current || !chatSession || processing === false} disabled={stopRef.current || !chatSession || processing === false}
@ -403,26 +487,22 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
{(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length !== 0 && {(noInteractions || !hideDefaultPrompts) &&
<Box sx={{ display: "flex", flexDirection: "column" }}> defaultPrompts !== undefined &&
{ defaultPrompts.length !== 0 && (
defaultPrompts.map((element, index) => { <Box sx={{ display: 'flex', flexDirection: 'column' }}>
return (<Box key={index}>{element}</Box>); {defaultPrompts.map((element, index) => {
}) return <Box key={index}>{element}</Box>;
} })}
</Box>
)}
<Box sx={{ display: 'flex', flexGrow: 1 }}></Box>
</Box> </Box>
}
<Box sx={{ display: "flex", flexGrow: 1 }}></Box>
</Box >
</Box> </Box>
); );
}); }
);
export type { export type { ConversationProps, ConversationHandle };
ConversationProps,
ConversationHandle,
};
export { export { Conversation };
Conversation
};

View File

@ -6,7 +6,7 @@ import { Tooltip } from '@mui/material';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
interface CopyBubbleProps extends IconButtonProps { interface CopyBubbleProps extends IconButtonProps {
content: string | undefined, content: string | undefined;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
tooltip?: string; tooltip?: string;
} }
@ -14,10 +14,10 @@ interface CopyBubbleProps extends IconButtonProps {
const CopyBubble = ({ const CopyBubble = ({
content, content,
sx, sx,
tooltip = "Copy to clipboard", tooltip = 'Copy to clipboard',
onClick, onClick,
...rest ...rest
} : CopyBubbleProps) => { }: CopyBubbleProps) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const handleCopy = (e: any) => { const handleCopy = (e: any) => {
@ -35,10 +35,12 @@ const CopyBubble = ({
} }
}; };
return ( return (
<Tooltip title={tooltip} placement="top" arrow> <Tooltip title={tooltip} placement="top" arrow>
<IconButton <IconButton
onClick={(e) => { handleCopy(e) }} onClick={e => {
handleCopy(e);
}}
sx={{ sx={{
width: 24, width: 24,
height: 24, height: 24,
@ -48,15 +50,17 @@ return (
...sx, ...sx,
}} }}
size="small" size="small"
color={copied ? "success" : "default"} color={copied ? 'success' : 'default'}
{...rest} {...rest}
> >
{copied ? <CheckIcon sx={{ width: 16, height: 16 }} /> : <ContentCopyIcon sx={{ width: 16, height: 16 }} />} {copied ? (
<CheckIcon sx={{ width: 16, height: 16 }} />
) : (
<ContentCopyIcon sx={{ width: 16, height: 16 }} />
)}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
); );
} };
export { export { CopyBubble };
CopyBubble
}

View File

@ -19,8 +19,17 @@ interface DeleteConfirmationProps {
onDelete?: () => void; onDelete?: () => void;
disabled?: boolean; disabled?: boolean;
label?: string; label?: string;
action?: "delete" | "reset"; action?: 'delete' | 'reset';
color?: "inherit" | "default" | "primary" | "secondary" | "error" | "info" | "success" | "warning" | undefined; color?:
| 'inherit'
| 'default'
| 'primary'
| 'secondary'
| 'error'
| 'info'
| 'success'
| 'warning'
| undefined;
sx?: SxProps; sx?: SxProps;
// New props for controlled mode // New props for controlled mode
open?: boolean; open?: boolean;
@ -47,7 +56,7 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
disabled, disabled,
label, label,
color, color,
action = "delete", action = 'delete',
// New props // New props
open: controlledOpen, open: controlledOpen,
onClose: controlledOnClose, onClose: controlledOnClose,
@ -56,7 +65,7 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
message, message,
hideButton = false, hideButton = false,
confirmButtonText, confirmButtonText,
cancelButtonText = "Cancel", cancelButtonText = 'Cancel',
sx, sx,
icon = <ResetIcon />, icon = <ResetIcon />,
} = props; } = props;
@ -94,21 +103,32 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
}; };
// Determine dialog content based on mode // Determine dialog content based on mode
const dialogTitle = title || "Confirm Reset"; const dialogTitle = title || 'Confirm Reset';
const dialogMessage = message || `This action will permanently ${capitalizeFirstLetter(action)} ${label ? label.toLowerCase() : "all data"} without the ability to recover it. Are you sure you want to continue?`; const dialogMessage =
const confirmText = confirmButtonText || `${capitalizeFirstLetter(action)} ${label || "Everything"}`; message ||
`This action will permanently ${capitalizeFirstLetter(action)} ${
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'}`;
return ( return (
<> <>
{/* Only show button if not hidden (for controlled mode) */} {/* Only show button if not hidden (for controlled mode) */}
{!hideButton && ( {!hideButton && (
<Tooltip title={label ? `${capitalizeFirstLetter(action)} ${label}` : "Reset"}> <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 */} <span style={{ display: 'flex' }}>
{' '}
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton <IconButton
aria-label={action} aria-label={action}
onClick={(e) => { e.stopPropagation(); e.preventDefault(); handleClickOpen(); }} onClick={e => {
color={color || "inherit"} e.stopPropagation();
sx={{ display: "flex", margin: 'auto 0px', ...sx }} e.preventDefault();
handleClickOpen();
}}
color={color || 'inherit'}
sx={{ display: 'flex', margin: 'auto 0px', ...sx }}
size="large" size="large"
edge="start" edge="start"
disabled={disabled} disabled={disabled}
@ -125,13 +145,9 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
onClose={handleClose} onClose={handleClose}
aria-labelledby="responsive-dialog-title" aria-labelledby="responsive-dialog-title"
> >
<DialogTitle id="responsive-dialog-title"> <DialogTitle id="responsive-dialog-title">{dialogTitle}</DialogTitle>
{dialogTitle}
</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>{dialogMessage}</DialogContentText>
{dialogMessage}
</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button autoFocus onClick={handleClose}> <Button autoFocus onClick={handleClose}>
@ -144,8 +160,6 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
</Dialog> </Dialog>
</> </>
); );
}
export {
DeleteConfirmation
}; };
export { DeleteConfirmation };

View File

@ -9,7 +9,7 @@ interface DocumentProps extends BackstoryElementProps {
const Document = (props: DocumentProps) => { const Document = (props: DocumentProps) => {
const { filepath } = props; const { filepath } = props;
const [document, setDocument] = useState<string>(""); const [document, setDocument] = useState<string>('');
// Get the markdown // Get the markdown
useEffect(() => { useEffect(() => {
@ -32,17 +32,17 @@ const Document = (props: DocumentProps) => {
} catch (error: any) { } catch (error: any) {
console.error('Error obtaining Docs content information:', error); console.error('Error obtaining Docs content information:', error);
setDocument(`${filepath} not found.`); setDocument(`${filepath} not found.`);
}; }
}; };
fetchDocument(); fetchDocument();
}, [document, setDocument, filepath]) }, [document, setDocument, filepath]);
return (<> return (
<>
<StyledMarkdown content={document} /> <StyledMarkdown content={document} />
</>); </>
);
}; };
export { export { Document };
Document
};

View File

@ -24,16 +24,10 @@ import {
Paper, Paper,
} from '@mui/material'; } from '@mui/material';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { import { CloudUpload, Edit, Delete, Visibility, Close } from '@mui/icons-material';
CloudUpload,
Edit,
Delete,
Visibility,
Close,
} from '@mui/icons-material';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import * as Types from 'types/types'; import * as Types from 'types/types';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
@ -65,7 +59,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
// Check if user is a candidate // 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 // Load documents on component mount
useEffect(() => { useEffect(() => {
@ -89,19 +83,19 @@ const DocumentManager = (props: BackstoryElementProps) => {
if (e.target.files && e.target.files[0]) { if (e.target.files && e.target.files[0]) {
const file = e.target.files[0]; const file = e.target.files[0];
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
let docType : Types.DocumentType | null = null; let docType: Types.DocumentType | null = null;
switch (fileExtension.substring(1)) { switch (fileExtension.substring(1)) {
case "pdf": case 'pdf':
docType = "pdf"; docType = 'pdf';
break; break;
case "docx": case 'docx':
docType = "docx"; docType = 'docx';
break; break;
case "md": case 'md':
docType = "markdown"; docType = 'markdown';
break; break;
case "txt": case 'txt':
docType = "txt"; docType = 'txt';
break; break;
} }
@ -112,12 +106,16 @@ const DocumentManager = (props: BackstoryElementProps) => {
try { try {
// Upload file (replace with actual API call) // Upload file (replace with actual API call)
const controller = apiClient.uploadCandidateDocument(file, { includeInRag: true, isJobDocument: false }, { const controller = apiClient.uploadCandidateDocument(
onError: (error) => { file,
{ includeInRag: true, isJobDocument: false },
{
onError: error => {
console.error(error); console.error(error);
setSnack(error.content, 'error'); setSnack(error.content, 'error');
},
} }
}); );
const result = await controller.promise; const result = await controller.promise;
if (result && result.document) { if (result && result.document) {
setDocuments(prev => [...prev, result.document]); setDocuments(prev => [...prev, result.document]);
@ -161,11 +159,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
await apiClient.updateCandidateDocument(document); await apiClient.updateCandidateDocument(document);
setDocuments(prev => setDocuments(prev =>
prev.map(doc => prev.map(doc => (doc.id === document.id ? { ...doc, includeInRag } : doc))
doc.id === document.id
? { ...doc, includeInRag }
: doc
)
); );
setSnack(`Document ${includeInRag ? 'included in' : 'excluded from'} RAG`, 'success'); setSnack(`Document ${includeInRag ? 'included in' : 'excluded from'} RAG`, 'success');
} catch (error) { } catch (error) {
@ -182,15 +176,11 @@ const DocumentManager = (props: BackstoryElementProps) => {
try { try {
// Call API to rename document // Call API to rename document
document.filename = newName document.filename = newName;
await apiClient.updateCandidateDocument(document); await apiClient.updateCandidateDocument(document);
setDocuments(prev => setDocuments(prev =>
prev.map(doc => prev.map(doc => (doc.id === document.id ? { ...doc, filename: newName.trim() } : doc))
doc.id === document.id
? { ...doc, filename: newName.trim() }
: doc
)
); );
setSnack('Document renamed successfully', 'success'); setSnack('Document renamed successfully', 'success');
setIsRenameDialogOpen(false); setIsRenameDialogOpen(false);
@ -235,30 +225,43 @@ const DocumentManager = (props: BackstoryElementProps) => {
// Get file type color // Get file type color
const getFileTypeColor = (type: string): 'primary' | 'secondary' | 'success' | 'warning' => { const getFileTypeColor = (type: string): 'primary' | 'secondary' | 'success' | 'warning' => {
switch (type) { switch (type) {
case 'pdf': return 'primary'; case 'pdf':
case 'docx': return 'secondary'; return 'primary';
case 'txt': return 'success'; case 'docx':
case 'md': return 'warning'; return 'secondary';
default: return 'primary'; case 'txt':
return 'success';
case 'md':
return 'warning';
default:
return 'primary';
} }
}; };
if (!candidate) { 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 ( 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', mb: 2, width: "100%", verticalAlign: "center" }}> <Box
<Typography variant={isMobile ? "subtitle2" : "h6"}> sx={{
Documents display: 'flex',
</Typography> justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
width: '100%',
verticalAlign: 'center',
}}
>
<Typography variant={isMobile ? 'subtitle2' : 'h6'}>Documents</Typography>
<Button <Button
component="label" component="label"
variant="contained" variant="contained"
startIcon={<CloudUpload />} startIcon={<CloudUpload />}
size={isMobile ? "small" : "medium"}> size={isMobile ? 'small' : 'medium'}
>
Upload Document Upload Document
<VisuallyHiddenInput <VisuallyHiddenInput
type="file" type="file"
@ -272,11 +275,15 @@ const DocumentManager = (props: BackstoryElementProps) => {
<Card variant="outlined"> <Card variant="outlined">
<CardContent sx={{ p: { xs: 1.5, sm: 3 } }}> <CardContent sx={{ p: { xs: 1.5, sm: 3 } }}>
{documents.length === 0 ? ( {documents.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ <Typography
variant="body2"
color="text.secondary"
sx={{
fontSize: { xs: '0.8rem', sm: '0.875rem' }, fontSize: { xs: '0.8rem', sm: '0.875rem' },
textAlign: 'center', textAlign: 'center',
py: 3 py: 3,
}}> }}
>
No additional documents uploaded No additional documents uploaded
</Typography> </Typography>
) : ( ) : (
@ -287,11 +294,21 @@ const DocumentManager = (props: BackstoryElementProps) => {
<ListItem sx={{ px: 0 }}> <ListItem sx={{ px: 0 }}>
<ListItemText <ListItemText
primary={ primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}> <Box
<Typography variant="body1" sx={{ sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
flexWrap: 'wrap',
}}
>
<Typography
variant="body1"
sx={{
wordBreak: 'break-word', wordBreak: 'break-word',
fontSize: { xs: '0.9rem', sm: '1rem' } fontSize: { xs: '0.9rem', sm: '1rem' },
}}> }}
>
{doc.filename} {doc.filename}
</Typography> </Typography>
<Chip <Chip
@ -300,12 +317,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
color={getFileTypeColor(doc.type)} color={getFileTypeColor(doc.type)}
/> />
{doc.options?.includeInRag && ( {doc.options?.includeInRag && (
<Chip <Chip label="RAG" size="small" color="success" variant="outlined" />
label="RAG"
size="small"
color="success"
variant="outlined"
/>
)} )}
</Box> </Box>
} }
@ -319,15 +331,11 @@ const DocumentManager = (props: BackstoryElementProps) => {
control={ control={
<Switch <Switch
checked={doc.options?.includeInRag} checked={doc.options?.includeInRag}
onChange={(e) => handleRAGToggle(doc, e.target.checked)} onChange={e => handleRAGToggle(doc, e.target.checked)}
size="small" size="small"
/> />
} }
label={ label={<Typography variant="caption">Include in RAG</Typography>}
<Typography variant="caption">
Include in RAG
</Typography>
}
/> />
</Box> </Box>
</Box> </Box>
@ -376,10 +384,15 @@ const DocumentManager = (props: BackstoryElementProps) => {
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<Card variant="outlined"> <Card variant="outlined">
<CardContent sx={{ p: { xs: 1.5, sm: 3 } }}> <CardContent sx={{ p: { xs: 1.5, sm: 3 } }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box
<Typography variant={isMobile ? "subtitle2" : "h6"}> sx={{
Document Content display: 'flex',
</Typography> justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
}}
>
<Typography variant={isMobile ? 'subtitle2' : 'h6'}>Document Content</Typography>
<IconButton <IconButton
size="small" size="small"
onClick={() => { onClick={() => {
@ -397,16 +410,18 @@ const DocumentManager = (props: BackstoryElementProps) => {
p: 2, p: 2,
maxHeight: 400, maxHeight: 400,
overflow: 'auto', overflow: 'auto',
backgroundColor: 'grey.50' backgroundColor: 'grey.50',
}} }}
> >
<pre style={{ <pre
style={{
margin: 0, margin: 0,
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: isMobile ? '0.75rem' : '0.875rem', fontSize: isMobile ? '0.75rem' : '0.875rem',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
wordBreak: 'break-word' wordBreak: 'break-word',
}}> }}
>
{documentContent || 'Loading content...'} {documentContent || 'Loading content...'}
</pre> </pre>
</Paper> </Paper>
@ -431,8 +446,8 @@ const DocumentManager = (props: BackstoryElementProps) => {
fullWidth fullWidth
variant="outlined" variant="outlined"
value={editingName} value={editingName}
onChange={(e) => setEditingName(e.target.value)} onChange={e => setEditingName(e.target.value)}
onKeyPress={(e) => { onKeyPress={e => {
if (e.key === 'Enter' && editingDocument) { if (e.key === 'Enter' && editingDocument) {
handleRenameDocument(editingDocument, editingName); handleRenameDocument(editingDocument, editingName);
} }
@ -440,9 +455,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setIsRenameDialogOpen(false)}> <Button onClick={() => setIsRenameDialogOpen(false)}>Cancel</Button>
Cancel
</Button>
<Button <Button
onClick={() => editingDocument && handleRenameDocument(editingDocument, editingName)} onClick={() => editingDocument && handleRenameDocument(editingDocument, editingName)}
variant="contained" variant="contained"

View File

@ -18,7 +18,7 @@ import {
Checkbox, Checkbox,
FormControlLabel, FormControlLabel,
Grid, Grid,
IconButton IconButton,
} from '@mui/material'; } from '@mui/material';
import { import {
Email as EmailIcon, Email as EmailIcon,
@ -28,7 +28,7 @@ import {
Refresh as RefreshIcon, Refresh as RefreshIcon,
DevicesOther as DevicesIcon, DevicesOther as DevicesIcon,
VisibilityOff, VisibilityOff,
Visibility Visibility,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { BackstoryPageProps } from './BackstoryTab'; import { BackstoryPageProps } from './BackstoryTab';
@ -36,7 +36,8 @@ import { Navigate, useNavigate } from 'react-router-dom';
// Email Verification Component // Email Verification Component
const EmailVerificationPage = (props: BackstoryPageProps) => { const EmailVerificationPage = (props: BackstoryPageProps) => {
const { verifyEmail, resendEmailVerification, getPendingVerificationEmail, isLoading, error } = useAuth(); const { verifyEmail, resendEmailVerification, getPendingVerificationEmail, isLoading, error } =
useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [verificationToken, setVerificationToken] = useState(''); const [verificationToken, setVerificationToken] = useState('');
const [status, setStatus] = useState<'pending' | 'success' | 'error'>('pending'); const [status, setStatus] = useState<'pending' | 'success' | 'error'>('pending');
@ -108,7 +109,7 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
bgcolor: 'grey.50', bgcolor: 'grey.50',
p: 2 p: 2,
}} }}
> >
<Card sx={{ maxWidth: 500, width: '100%' }}> <Card sx={{ maxWidth: 500, width: '100%' }}>
@ -171,11 +172,7 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
<Typography variant="body2" color="text.secondary" mb={2}> <Typography variant="body2" color="text.secondary" mb={2}>
You will be redirected to the login page in a few seconds... You will be redirected to the login page in a few seconds...
</Typography> </Typography>
<Button <Button variant="contained" onClick={() => navigate('/login')} fullWidth>
variant="contained"
onClick={() => navigate('/login')}
fullWidth
>
Go to Login Go to Login
</Button> </Button>
</Box> </Box>
@ -193,11 +190,7 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
> >
Resend Verification Email Resend Verification Email
</Button> </Button>
<Button <Button variant="contained" onClick={() => navigate('/login')} fullWidth>
variant="contained"
onClick={() => navigate('/login')}
fullWidth
>
Back to Login Back to Login
</Button> </Button>
</Box> </Box>
@ -206,7 +199,7 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
</Card> </Card>
</Box> </Box>
); );
} };
// MFA Verification Component // MFA Verification Component
interface MFAVerificationDialogProps { interface MFAVerificationDialogProps {
@ -215,11 +208,7 @@ interface MFAVerificationDialogProps {
onVerificationSuccess: (authData: any) => void; onVerificationSuccess: (authData: any) => void;
} }
const MFAVerificationDialog = (props: MFAVerificationDialogProps) => { const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
const { const { open, onClose, onVerificationSuccess } = props;
open,
onClose,
onVerificationSuccess
} = props;
const { verifyMFA, resendMFACode, clearMFA, mfaResponse, isLoading, error } = useAuth(); const { verifyMFA, resendMFACode, clearMFA, mfaResponse, isLoading, error } = useAuth();
const [code, setCode] = useState(''); const [code, setCode] = useState('');
const [rememberDevice, setRememberDevice] = useState(false); const [rememberDevice, setRememberDevice] = useState(false);
@ -235,14 +224,13 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
const jsonStr = error.replace(/^[^{]*/, ''); const jsonStr = error.replace(/^[^{]*/, '');
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
setErrorMessage(data.error.message); setErrorMessage(data.error.message);
}, [error]); }, [error]);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
const timer = setInterval(() => { const timer = setInterval(() => {
setTimeLeft((prev) => { setTimeLeft(prev => {
if (prev <= 1) { if (prev <= 1) {
clearInterval(timer); clearInterval(timer);
setLocalError('MFA code has expired. Please try logging in again.'); setLocalError('MFA code has expired. Please try logging in again.');
@ -298,7 +286,11 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
} }
try { try {
const success = await resendMFACode(mfaResponse.mfaData.email, mfaResponse.mfaData.deviceId, mfaResponse.mfaData.deviceName); const success = await resendMFACode(
mfaResponse.mfaData.email,
mfaResponse.mfaData.deviceId,
mfaResponse.mfaData.deviceName
);
if (success) { if (success) {
setTimeLeft(600); // Reset timer setTimeLeft(600); // Reset timer
setLocalError(''); setLocalError('');
@ -321,15 +313,14 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
<DialogTitle> <DialogTitle>
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<SecurityIcon color="primary" /> <SecurityIcon color="primary" />
<Typography variant="h6"> <Typography variant="h6">Verify Your Identity</Typography>
Verify Your Identity
</Typography>
</Box> </Box>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<Alert severity="info" sx={{ mb: 3 }}> <Alert severity="info" sx={{ mb: 3 }}>
We've detected a login from a new device: <strong>{mfaResponse.mfaData.deviceName}</strong> We've detected a login from a new device:{' '}
<strong>{mfaResponse.mfaData.deviceName}</strong>
</Alert> </Alert>
<Typography variant="body1" gutterBottom> <Typography variant="body1" gutterBottom>
@ -343,7 +334,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
fullWidth fullWidth
label="Enter 6-digit code" label="Enter 6-digit code"
value={code} value={code}
onChange={(e) => { onChange={e => {
const value = e.target.value.replace(/\D/g, '').slice(0, 6); const value = e.target.value.replace(/\D/g, '').slice(0, 6);
setCode(value); setCode(value);
setLocalError(''); setLocalError('');
@ -354,8 +345,8 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
style: { style: {
fontSize: 24, fontSize: 24,
textAlign: 'center', textAlign: 'center',
letterSpacing: 8 letterSpacing: 8,
} },
}} }}
sx={{ mt: 2, mb: 2 }} sx={{ mt: 2, mb: 2 }}
error={!!(localError || errorMessage)} error={!!(localError || errorMessage)}
@ -379,7 +370,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
control={ control={
<Checkbox <Checkbox
checked={rememberDevice} checked={rememberDevice}
onChange={(e) => setRememberDevice(e.target.checked)} onChange={e => setRememberDevice(e.target.checked)}
/> />
} }
label="Remember this device for 90 days" label="Remember this device for 90 days"
@ -406,14 +397,14 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );
} };
// Enhanced Registration Success Component // Enhanced Registration Success Component
const RegistrationSuccessDialog = ({ const RegistrationSuccessDialog = ({
open, open,
onClose, onClose,
email, email,
userType userType,
}: { }: {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@ -464,10 +455,7 @@ const RegistrationSuccessDialog = ({
</Alert> </Alert>
{resendMessage && ( {resendMessage && (
<Alert <Alert severity={resendMessage.includes('sent') ? 'success' : 'error'} sx={{ mb: 2 }}>
severity={resendMessage.includes('sent') ? 'success' : 'error'}
sx={{ mb: 2 }}
>
{resendMessage} {resendMessage}
</Alert> </Alert>
)} )}
@ -487,7 +475,7 @@ const RegistrationSuccessDialog = ({
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );
} };
// Enhanced Login Component with MFA Support // Enhanced Login Component with MFA Support
const LoginForm = () => { const LoginForm = () => {
@ -506,7 +494,6 @@ const LoginForm = () => {
const jsonStr = error.replace(/^[^{]*/, ''); const jsonStr = error.replace(/^[^{]*/, '');
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
setErrorMessage(data.error.message); setErrorMessage(data.error.message);
}, [error]); }, [error]);
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent) => {
@ -514,7 +501,7 @@ const LoginForm = () => {
const success = await login({ const success = await login({
login: email, login: email,
password password,
}); });
console.log(`login success: ${success}`); console.log(`login success: ${success}`);
@ -546,7 +533,7 @@ const LoginForm = () => {
fullWidth fullWidth
label="Email or Username" label="Email or Username"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={e => setEmail(e.target.value)}
autoComplete="email" autoComplete="email"
autoFocus autoFocus
/> />
@ -555,7 +542,7 @@ const LoginForm = () => {
label="Password" label="Password"
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
autoComplete="current-password" autoComplete="current-password"
placeholder="Create a strong password" placeholder="Create a strong password"
required required
@ -565,7 +552,7 @@ const LoginForm = () => {
<IconButton <IconButton
aria-label="toggle password visibility" aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
onMouseDown={(e) => e.preventDefault()} onMouseDown={e => e.preventDefault()}
edge="end" edge="end"
> >
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
@ -594,12 +581,12 @@ const LoginForm = () => {
{/* MFA Dialog */} {/* MFA Dialog */}
<MFAVerificationDialog <MFAVerificationDialog
open={mfaResponse?.mfaRequired || false} open={mfaResponse?.mfaRequired || false}
onClose={() => { }} // This will be handled by clearMFA in the dialog onClose={() => {}} // This will be handled by clearMFA in the dialog
onVerificationSuccess={handleMFASuccess} onVerificationSuccess={handleMFASuccess}
/> />
</Box> </Box>
); );
} };
// Device Management Component // Device Management Component
const TrustedDevicesManager = () => { const TrustedDevicesManager = () => {
@ -621,14 +608,14 @@ const TrustedDevicesManager = () => {
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" paragraph> <Typography variant="body2" color="text.secondary" paragraph>
Manage devices that you've marked as trusted. You won't need to verify Manage devices that you've marked as trusted. You won't need to verify your identity when
your identity when signing in from these devices. signing in from these devices.
</Typography> </Typography>
{devices.length === 0 ? ( {devices.length === 0 ? (
<Alert severity="info"> <Alert severity="info">
No trusted devices yet. When you log in from a new device and choose No trusted devices yet. When you log in from a new device and choose to remember it, it
to remember it, it will appear here. will appear here.
</Alert> </Alert>
) : ( ) : (
<Grid container spacing={2}> <Grid container spacing={2}>
@ -636,9 +623,7 @@ const TrustedDevicesManager = () => {
<Grid key={index} size={{ xs: 12, md: 6 }}> <Grid key={index} size={{ xs: 12, md: 6 }}>
<Card variant="outlined"> <Card variant="outlined">
<CardContent> <CardContent>
<Typography variant="subtitle1"> <Typography variant="subtitle1">{device.deviceName}</Typography>
{device.deviceName}
</Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Added: {new Date(device.addedAt).toLocaleDateString()} Added: {new Date(device.addedAt).toLocaleDateString()}
</Typography> </Typography>
@ -664,6 +649,12 @@ const TrustedDevicesManager = () => {
</CardContent> </CardContent>
</Card> </Card>
); );
} };
export { EmailVerificationPage, MFAVerificationDialog, TrustedDevicesManager, RegistrationSuccessDialog, LoginForm }; export {
EmailVerificationPage,
MFAVerificationDialog,
TrustedDevicesManager,
RegistrationSuccessDialog,
LoginForm,
};

View File

@ -29,6 +29,4 @@ const ExpandMore = styled((props: ExpandMoreProps) => {
], ],
})); }));
export { export { ExpandMore };
ExpandMore
};

View File

@ -10,7 +10,7 @@ import { useAppState } from 'hooks/GlobalContext';
interface GenerateImageProps extends BackstoryElementProps { interface GenerateImageProps extends BackstoryElementProps {
prompt: string; prompt: string;
chatSession: ChatSession; chatSession: ChatSession;
}; }
const GenerateImage = (props: GenerateImageProps) => { const GenerateImage = (props: GenerateImageProps) => {
const { user } = useAuth(); const { user } = useAuth();
@ -27,7 +27,7 @@ const GenerateImage = (props: GenerateImageProps) => {
// Effect to trigger profile generation when user data is ready // Effect to trigger profile generation when user data is ready
useEffect(() => { useEffect(() => {
if (controllerRef.current) { if (controllerRef.current) {
console.log("Controller already active, skipping profile generation"); console.log('Controller already active, skipping profile generation');
return; return;
} }
if (!prompt) { if (!prompt) {
@ -89,35 +89,44 @@ const GenerateImage = (props: GenerateImageProps) => {
} }
return ( return (
<Box className="GenerateImage" sx={{ <Box
display: "flex", className="GenerateImage"
flexDirection: "column", sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
gap: 1, gap: 1,
maxWidth: { xs: '100%', md: '700px', lg: '1024px' }, maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
minHeight: "max-content", minHeight: 'max-content',
}}> }}
>
{image !== '' && <img alt={prompt} src={`${image}/${chatSession.id}`} />} {image !== '' && <img alt={prompt} src={`${image}/${chatSession.id}`} />}
{ prompt && {prompt && (
<Quote size={processing ? "normal" : "small"} quote={prompt} sx={{ "& *": { color: "#2E2E2E !important" }}}/> <Quote
} size={processing ? 'normal' : 'small'}
{processing && quote={prompt}
<Box sx={{ sx={{ '& *': { color: '#2E2E2E !important' } }}
display: "flex", />
flexDirection: "column", )}
alignItems: "center", {processing && (
justifyContent: "center", <Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
m: 0, m: 0,
gap: 1, gap: 1,
minHeight: "min-content", minHeight: 'min-content',
mb: 2 mb: 2,
}}> }}
{ status && >
<Box sx={{ display: "flex", flexDirection: "column"}}> {status && (
<Box sx={{ fontSize: "0.5rem"}}>Generation status</Box> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box sx={{ fontWeight: "bold"}}>{status}</Box> <Box sx={{ fontSize: '0.5rem' }}>Generation status</Box>
<Box sx={{ fontWeight: 'bold' }}>{status}</Box>
</Box> </Box>
} )}
<PropagateLoader <PropagateLoader
size="10px" size="10px"
loading={processing} loading={processing}
@ -126,10 +135,9 @@ const GenerateImage = (props: GenerateImageProps) => {
data-testid="loader" data-testid="loader"
/> />
</Box> </Box>
} )}
</Box>); </Box>
);
}; };
export { export { GenerateImage };
GenerateImage
};

View File

@ -30,7 +30,7 @@ import {
Business, Business,
Work, Work,
CheckCircle, CheckCircle,
Star Star,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import FileUploadIcon from '@mui/icons-material/FileUpload'; import FileUploadIcon from '@mui/icons-material/FileUpload';
@ -102,7 +102,7 @@ const JobCreator = (props: JobCreatorProps) => {
setJobStatus(status.content); setJobStatus(status.content);
}, },
onMessage: (jobMessage: Types.JobRequirementsMessage) => { onMessage: (jobMessage: Types.JobRequirementsMessage) => {
const job: Types.Job = jobMessage.job const job: Types.Job = jobMessage.job;
console.log('onMessage - job', job); console.log('onMessage - job', job);
setJob(job); setJob(job);
setCompany(job.company || ''); setCompany(job.company || '');
@ -115,14 +115,14 @@ const JobCreator = (props: JobCreatorProps) => {
}, },
onError: (error: Types.ChatMessageError) => { onError: (error: Types.ChatMessageError) => {
console.log('onError', error); console.log('onError', error);
setSnack(error.content, "error"); setSnack(error.content, 'error');
setIsProcessing(false); setIsProcessing(false);
}, },
onComplete: () => { onComplete: () => {
setJobStatusType(null); setJobStatusType(null);
setJobStatus(''); setJobStatus('');
setIsProcessing(false); setIsProcessing(false);
} },
}; };
const handleJobUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleJobUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
@ -131,17 +131,17 @@ const JobCreator = (props: JobCreatorProps) => {
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
let docType: Types.DocumentType | null = null; let docType: Types.DocumentType | null = null;
switch (fileExtension.substring(1)) { switch (fileExtension.substring(1)) {
case "pdf": case 'pdf':
docType = "pdf"; docType = 'pdf';
break; break;
case "docx": case 'docx':
docType = "docx"; docType = 'docx';
break; break;
case "md": case 'md':
docType = "markdown"; docType = 'markdown';
break; break;
case "txt": case 'txt':
docType = "txt"; docType = 'txt';
break; break;
} }
@ -175,7 +175,12 @@ const JobCreator = (props: JobCreatorProps) => {
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
const renderRequirementSection = (title: string, items: string[] | undefined, icon: JSX.Element, required = false) => { const renderRequirementSection = (
title: string,
items: string[] | undefined,
icon: JSX.Element,
required = false
) => {
if (!items || items.length === 0) return null; if (!items || items.length === 0) return null;
return ( return (
@ -189,13 +194,7 @@ const JobCreator = (props: JobCreatorProps) => {
</Box> </Box>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap> <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{items.map((item, index) => ( {items.map((item, index) => (
<Chip <Chip key={index} label={item} variant="outlined" size="small" sx={{ mb: 1 }} />
key={index}
label={item}
variant="outlined"
size="small"
sx={{ mb: 1 }}
/>
))} ))}
</Stack> </Stack>
</Box> </Box>
@ -214,49 +213,49 @@ const JobCreator = (props: JobCreatorProps) => {
/> />
<CardContent sx={{ pt: 0 }}> <CardContent sx={{ pt: 0 }}>
{renderRequirementSection( {renderRequirementSection(
"Technical Skills (Required)", 'Technical Skills (Required)',
jobRequirements.technicalSkills.required, jobRequirements.technicalSkills.required,
<Build color="primary" />, <Build color="primary" />,
true true
)} )}
{renderRequirementSection( {renderRequirementSection(
"Technical Skills (Preferred)", 'Technical Skills (Preferred)',
jobRequirements.technicalSkills.preferred, jobRequirements.technicalSkills.preferred,
<Build color="action" /> <Build color="action" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience Requirements (Required)", 'Experience Requirements (Required)',
jobRequirements.experienceRequirements.required, jobRequirements.experienceRequirements.required,
<Work color="primary" />, <Work color="primary" />,
true true
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience Requirements (Preferred)", 'Experience Requirements (Preferred)',
jobRequirements.experienceRequirements.preferred, jobRequirements.experienceRequirements.preferred,
<Work color="action" /> <Work color="action" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Soft Skills", 'Soft Skills',
jobRequirements.softSkills, jobRequirements.softSkills,
<Psychology color="secondary" /> <Psychology color="secondary" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience", 'Experience',
jobRequirements.experience, jobRequirements.experience,
<Star color="warning" /> <Star color="warning" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Education", 'Education',
jobRequirements.education, jobRequirements.education,
<Description color="info" /> <Description color="info" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Certifications", 'Certifications',
jobRequirements.certifications, jobRequirements.certifications,
<CheckCircle color="success" /> <CheckCircle color="success" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Preferred Attributes", 'Preferred Attributes',
jobRequirements.preferredAttributes, jobRequirements.preferredAttributes,
<Star color="secondary" /> <Star color="secondary" />
)} )}
@ -307,10 +306,12 @@ const JobCreator = (props: JobCreatorProps) => {
const renderJobCreation = () => { const renderJobCreation = () => {
return ( return (
<Box sx={{ <Box
width: "100%", sx={{
p: 1 width: '100%',
}}> p: 1,
}}
>
{/* Upload Section */} {/* Upload Section */}
<Card elevation={3} sx={{ mb: 4 }}> <Card elevation={3} sx={{ mb: 4 }}>
<CardHeader <CardHeader
@ -321,7 +322,11 @@ const JobCreator = (props: JobCreatorProps) => {
<CardContent> <CardContent>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center' }}> <Typography
variant="h6"
gutterBottom
sx={{ display: 'flex', alignItems: 'center' }}
>
<CloudUpload sx={{ mr: 1 }} /> <CloudUpload sx={{ mr: 1 }} />
Upload Job Description Upload Job Description
</Typography> </Typography>
@ -351,7 +356,11 @@ const JobCreator = (props: JobCreatorProps) => {
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center' }}> <Typography
variant="h6"
gutterBottom
sx={{ display: 'flex', alignItems: 'center' }}
>
<Description sx={{ mr: 1 }} /> <Description sx={{ mr: 1 }} />
Or Enter Manually Or Enter Manually
</Typography> </Typography>
@ -362,7 +371,7 @@ const JobCreator = (props: JobCreatorProps) => {
placeholder="Paste or type the job description here..." placeholder="Paste or type the job description here..."
variant="outlined" variant="outlined"
value={jobDescription} value={jobDescription}
onChange={(e) => setJobDescription(e.target.value)} onChange={e => setJobDescription(e.target.value)}
disabled={isProcessing} disabled={isProcessing}
sx={{ mb: 2 }} sx={{ mb: 2 }}
/> />
@ -409,11 +418,11 @@ const JobCreator = (props: JobCreatorProps) => {
label="Job Title" label="Job Title"
variant="outlined" variant="outlined"
value={jobTitle} value={jobTitle}
onChange={(e) => setJobTitle(e.target.value)} onChange={e => setJobTitle(e.target.value)}
required required
disabled={isProcessing} disabled={isProcessing}
InputProps={{ InputProps={{
startAdornment: <Work sx={{ mr: 1, color: 'text.secondary' }} /> startAdornment: <Work sx={{ mr: 1, color: 'text.secondary' }} />,
}} }}
/> />
</Grid> </Grid>
@ -424,11 +433,11 @@ const JobCreator = (props: JobCreatorProps) => {
label="Company" label="Company"
variant="outlined" variant="outlined"
value={company} value={company}
onChange={(e) => setCompany(e.target.value)} onChange={e => setCompany(e.target.value)}
required required
disabled={isProcessing} disabled={isProcessing}
InputProps={{ InputProps={{
startAdornment: <Business sx={{ mr: 1, color: 'text.secondary' }} /> startAdornment: <Business sx={{ mr: 1, color: 'text.secondary' }} />,
}} }}
/> />
</Grid> </Grid>
@ -451,60 +460,83 @@ const JobCreator = (props: JobCreatorProps) => {
</Card> </Card>
{/* Job Summary */} {/* Job Summary */}
{summary !== '' && {summary !== '' && (
<Card elevation={2} sx={{ mt: 3 }}> <Card elevation={2} sx={{ mt: 3 }}>
<CardHeader <CardHeader
title="Job Summary" title="Job Summary"
avatar={<CheckCircle color="success" />} avatar={<CheckCircle color="success" />}
sx={{ pb: 1 }} sx={{ pb: 1 }}
/> />
<CardContent sx={{ pt: 0 }}> <CardContent sx={{ pt: 0 }}>{summary}</CardContent>
{summary}
</CardContent>
</Card> </Card>
} )}
{/* Requirements Display */} {/* Requirements Display */}
{renderJobRequirements()} {renderJobRequirements()}
</Box> </Box>
); );
}; };
return ( return (
<Box className="JobManagement" <Box
className="JobManagement"
sx={{ sx={{
background: "white", background: 'white',
p: 0, p: 0,
width: "100%", width: '100%',
display: "flex", flexDirection: "column" display: 'flex',
}}> flexDirection: 'column',
}}
>
{job === null && renderJobCreation()} {job === null && renderJobCreation()}
{job && {job && (
<Box sx={{ <Box
display: "flex", flexDirection: "column", sx={{
height: "100%", /* Restrict to main-container's height */ display: 'flex',
width: "100%", flexDirection: 'column',
minHeight: 0,/* Prevent flex overflow */ height: '100%' /* Restrict to main-container's height */,
maxHeight: "min-content", width: '100%',
position: "relative", minHeight: 0 /* Prevent flex overflow */,
}}> maxHeight: 'min-content',
<Box sx={{ position: 'relative',
display: "flex", }}
flexDirection: "row", >
<Box
sx={{
display: 'flex',
flexDirection: 'row',
flexGrow: 1, flexGrow: 1,
gap: 1, gap: 1,
height: "100%", /* Restrict to main-container's height */ height: '100%' /* Restrict to main-container's height */,
width: "100%", width: '100%',
minHeight: 0,/* Prevent flex overflow */ minHeight: 0 /* Prevent flex overflow */,
maxHeight: "min-content", maxHeight: 'min-content',
"& > *:not(.Scrollable)": { '& > *:not(.Scrollable)': {
flexShrink: 0, /* Prevent shrinking */ flexShrink: 0 /* Prevent shrinking */,
}, },
position: "relative", position: 'relative',
}}> }}
<Scrollable sx={{ display: "flex", flexGrow: 1, position: "relative", maxHeight: "30rem" }}><JobInfo job={job} /></Scrollable> >
<Scrollable sx={{ display: "flex", flexGrow: 1, position: "relative", maxHeight: "30rem" }}><StyledMarkdown content={job.description} /></Scrollable> <Scrollable
sx={{
display: 'flex',
flexGrow: 1,
position: 'relative',
maxHeight: '30rem',
}}
>
<JobInfo job={job} />
</Scrollable>
<Scrollable
sx={{
display: 'flex',
flexGrow: 1,
position: 'relative',
maxHeight: '30rem',
}}
>
<StyledMarkdown content={job.description} />
</Scrollable>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-end' }}> <Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-end' }}>
<Button <Button
@ -518,9 +550,8 @@ const JobCreator = (props: JobCreatorProps) => {
Save Job Save Job
</Button> </Button>
</Box> </Box>
</Box> </Box>
} )}
</Box> </Box>
); );
}; };

View File

@ -15,14 +15,26 @@ import {
useTheme, useTheme,
LinearProgress, LinearProgress,
useMediaQuery, useMediaQuery,
Button Button,
} from '@mui/material'; } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
import PendingIcon from '@mui/icons-material/Pending'; import PendingIcon from '@mui/icons-material/Pending';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Candidate, ChatMessage, ChatMessageError, ChatMessageStatus, ChatMessageStreaming, ChatMessageUser, ChatSession, EvidenceDetail, JobRequirements, SkillAssessment, SkillStatus } from 'types/types'; import {
Candidate,
ChatMessage,
ChatMessageError,
ChatMessageStatus,
ChatMessageStreaming,
ChatMessageUser,
ChatSession,
EvidenceDetail,
JobRequirements,
SkillAssessment,
SkillStatus,
} from 'types/types';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { BackstoryPageProps } from './BackstoryTab'; import { BackstoryPageProps } from './BackstoryTab';
import { Job } from 'types/types'; import { Job } from 'types/types';
@ -37,12 +49,18 @@ import { JobInfo } from './ui/JobInfo';
interface JobAnalysisProps extends BackstoryPageProps { interface JobAnalysisProps extends BackstoryPageProps {
job: Job; job: Job;
candidate: Candidate; candidate: Candidate;
variant?: "small" | "normal"; variant?: 'small' | 'normal';
onAnalysisComplete: (skills: SkillAssessment[]) => void; onAnalysisComplete: (skills: SkillAssessment[]) => void;
} }
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "assistant", metadata: null as any status: 'done',
type: 'text',
sessionId: '',
timestamp: new Date(),
content: '',
role: 'assistant',
metadata: null as any,
}; };
interface SkillMatch extends SkillAssessment { interface SkillMatch extends SkillAssessment {
@ -52,16 +70,11 @@ interface SkillMatch extends SkillAssessment {
} }
const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) => { const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) => {
const { const { job, candidate, onAnalysisComplete, variant = 'normal' } = props;
job,
candidate,
onAnalysisComplete,
variant = "normal",
} = props
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const theme = useTheme(); const theme = useTheme();
const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]); const [requirements, setRequirements] = useState<{ requirement: string; domain: string }[]>([]);
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]); const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
const [creatingSession, setCreatingSession] = useState<boolean>(false); const [creatingSession, setCreatingSession] = useState<boolean>(false);
const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false); const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false);
@ -77,7 +90,8 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
// Handle accordion expansion // Handle accordion expansion
const handleAccordionChange = (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => { const handleAccordionChange =
(panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
setExpanded(isExpanded ? panel : false); setExpanded(isExpanded ? panel : false);
}; };
@ -85,41 +99,68 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
if (!job || !job.requirements) { if (!job || !job.requirements) {
return; return;
} }
const requirements: { requirement: string, domain: string }[] = []; const requirements: { requirement: string; domain: string }[] = [];
if (job.requirements?.technicalSkills) { if (job.requirements?.technicalSkills) {
job.requirements.technicalSkills.required?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (required)' })); job.requirements.technicalSkills.required?.forEach(req =>
job.requirements.technicalSkills.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Technical Skills (preferred)' })); requirements.push({
requirement: req,
domain: 'Technical Skills (required)',
})
);
job.requirements.technicalSkills.preferred?.forEach(req =>
requirements.push({
requirement: req,
domain: 'Technical Skills (preferred)',
})
);
} }
if (job.requirements?.experienceRequirements) { if (job.requirements?.experienceRequirements) {
job.requirements.experienceRequirements.required?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (required)' })); job.requirements.experienceRequirements.required?.forEach(req =>
job.requirements.experienceRequirements.preferred?.forEach(req => requirements.push({ requirement: req, domain: 'Experience (preferred)' })); requirements.push({ requirement: req, domain: 'Experience (required)' })
);
job.requirements.experienceRequirements.preferred?.forEach(req =>
requirements.push({
requirement: req,
domain: 'Experience (preferred)',
})
);
} }
if (job.requirements?.softSkills) { 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) { 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) { 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) { 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) { 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, skill: req.requirement,
skillModified: req.requirement, skillModified: req.requirement,
candidateId: candidate.id || "", candidateId: candidate.id || '',
domain: req.domain, domain: req.domain,
status: 'waiting' as const, status: 'waiting' as const,
assessment: "", assessment: '',
description: "", description: '',
evidenceFound: false, evidenceFound: false,
evidenceStrength: "none", evidenceStrength: 'none',
evidenceDetails: [], evidenceDetails: [],
matchScore: 0, matchScore: 0,
})); }));
@ -129,7 +170,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
setStatusMessage(null); setStatusMessage(null);
setLoadingRequirements(false); setLoadingRequirements(false);
setOverallScore(0); setOverallScore(0);
} };
useEffect(() => { useEffect(() => {
initializeRequirements(job); initializeRequirements(job);
@ -160,19 +201,35 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
return updated; return updated;
}); });
const request: any = await apiClient.candidateMatchForRequirement(candidate.id || '', requirements[i].requirement, skillMatchHandlers); const request: any = await apiClient.candidateMatchForRequirement(
candidate.id || '',
requirements[i].requirement,
skillMatchHandlers
);
const result = await request.promise; const result = await request.promise;
const skillMatch = result.skillAssessment; const skillMatch = result.skillAssessment;
skills.push(skillMatch); skills.push(skillMatch);
setMatchStatus(''); setMatchStatus('');
let matchScore: number = 0; let matchScore = 0;
switch (skillMatch.evidenceStrength.toUpperCase()) { switch (skillMatch.evidenceStrength.toUpperCase()) {
case "STRONG": matchScore = 100; break; case 'STRONG':
case "MODERATE": matchScore = 75; break; matchScore = 100;
case "WEAK": matchScore = 50; break; break;
case "NONE": matchScore = 0; break; case 'MODERATE':
matchScore = 75;
break;
case 'WEAK':
matchScore = 50;
break;
case 'NONE':
matchScore = 0;
break;
} }
if (skillMatch.evidenceStrength == "NONE" && skillMatch.citations && skillMatch.citations.length > 3) { if (
skillMatch.evidenceStrength == 'NONE' &&
skillMatch.citations &&
skillMatch.citations.length > 3
) {
matchScore = Math.min(skillMatch.citations.length * 8, 40); matchScore = Math.min(skillMatch.citations.length * 8, 40);
} }
const match: SkillMatch = { const match: SkillMatch = {
@ -191,7 +248,9 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
setSkillMatches(current => { setSkillMatches(current => {
const completedMatches = current.filter(match => match.status === 'complete'); const completedMatches = current.filter(match => match.status === 'complete');
if (completedMatches.length > 0) { if (completedMatches.length > 0) {
const newOverallScore = completedMatches.reduce((sum, match) => sum + match.matchScore, 0) / completedMatches.length; const newOverallScore =
completedMatches.reduce((sum, match) => sum + match.matchScore, 0) /
completedMatches.length;
setOverallScore(newOverallScore); setOverallScore(newOverallScore);
} }
return current; return current;
@ -203,7 +262,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
updated[i] = { updated[i] = {
...updated[i], ...updated[i],
status: 'error', status: 'error',
assessment: 'Failed to analyze this requirement.' assessment: 'Failed to analyze this requirement.',
}; };
return updated; return updated;
}); });
@ -243,22 +302,39 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
}; };
return ( return (
<Box sx={{ display: "flex", flexDirection: "column", m: 0, p: 0 }}> <Box sx={{ display: 'flex', flexDirection: 'column', m: 0, p: 0 }}>
{variant !== "small" && {variant !== 'small' && <JobInfo job={job} variant="normal" />}
<JobInfo job={job} variant="normal" />
}
<Box sx={{ display: 'flex', flexDirection: "row", alignItems: 'center', mb: isMobile ? 1 : 2, gap: 1, justifyContent: "space-between" }}> <Box
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row", flexGrow: 1, gap: 1 }}> sx={{
{overallScore !== 0 && <> display: 'flex',
flexDirection: 'row',
alignItems: 'center',
mb: isMobile ? 1 : 2,
gap: 1,
justifyContent: 'space-between',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
flexGrow: 1,
gap: 1,
}}
>
{overallScore !== 0 && (
<>
<Typography variant="h5" component="h2" sx={{ mr: 2 }}> <Typography variant="h5" component="h2" sx={{ mr: 2 }}>
Overall Match: Overall Match:
</Typography> </Typography>
<Box sx={{ <Box
sx={{
position: 'relative', position: 'relative',
display: 'inline-flex', display: 'inline-flex',
mr: 2 mr: 2,
}}> }}
>
<CircularProgress <CircularProgress
variant="determinate" variant="determinate"
value={overallScore} value={overallScore}
@ -287,20 +363,30 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
</Box> </Box>
<Chip <Chip
label={ label={
overallScore >= 80 ? "Excellent Match" : overallScore >= 80
overallScore >= 60 ? "Good Match" : ? 'Excellent Match'
overallScore >= 40 ? "Partial Match" : "Low Match" : overallScore >= 60
? 'Good Match'
: overallScore >= 40
? 'Partial Match'
: 'Low Match'
} }
sx={{ sx={{
bgcolor: getMatchColor(overallScore), bgcolor: getMatchColor(overallScore),
color: 'white', color: 'white',
fontWeight: 'bold' fontWeight: 'bold',
}} }}
/> />
</>} </>
)}
</Box> </Box>
<Button sx={{ marginLeft: "auto" }} disabled={analyzing || startAnalysis} onClick={beginAnalysis} variant="contained"> <Button
{analyzing ? "Assessment in Progress" : "Start Skill Assessment"} sx={{ marginLeft: 'auto' }}
disabled={analyzing || startAnalysis}
onClick={beginAnalysis}
variant="contained"
>
{analyzing ? 'Assessment in Progress' : 'Start Skill Assessment'}
</Button> </Button>
</Box> </Box>
@ -325,9 +411,10 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
sx={{ sx={{
mb: 2, mb: 2,
border: '1px solid', border: '1px solid',
borderColor: match.status === 'complete' borderColor:
match.status === 'complete'
? getMatchColor(match.matchScore) ? getMatchColor(match.matchScore)
: theme.palette.divider : theme.palette.divider,
}} }}
> >
<AccordionSummary <AccordionSummary
@ -335,21 +422,39 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
aria-controls={`panel${index}bh-content`} aria-controls={`panel${index}bh-content`}
id={`panel${index}bh-header`} id={`panel${index}bh-header`}
sx={{ sx={{
bgcolor: match.status === 'complete' bgcolor:
match.status === 'complete'
? `${getMatchColor(match.matchScore)}22` // Add transparency ? `${getMatchColor(match.matchScore)}22` // Add transparency
: 'inherit' : 'inherit',
}} }}
> >
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '100%', width: '100%',
justifyContent: 'space-between' justifyContent: 'space-between',
}}> }}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
{getStatusIcon(match.status, match.matchScore)} {getStatusIcon(match.status, match.matchScore)}
<Box sx={{ display: "flex", flexDirection: "column", gap: 0, p: 0, m: 0 }}> <Box
<Typography sx={{ ml: 1, mb: 0, fontWeight: 'medium', marginBottom: "0px !important" }}> sx={{
display: 'flex',
flexDirection: 'column',
gap: 0,
p: 0,
m: 0,
}}
>
<Typography
sx={{
ml: 1,
mb: 0,
fontWeight: 'medium',
marginBottom: '0px !important',
}}
>
{match.skill} {match.skill}
</Typography> </Typography>
<Typography variant="caption" sx={{ ml: 1, fontWeight: 'light' }}> <Typography variant="caption" sx={{ ml: 1, fontWeight: 'light' }}>
@ -365,26 +470,38 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
sx={{ sx={{
bgcolor: getMatchColor(match.matchScore), bgcolor: getMatchColor(match.matchScore),
color: 'white', color: 'white',
minWidth: 90 minWidth: 90,
}} }}
/> />
) : match.status === 'waiting' ? ( ) : match.status === 'waiting' ? (
<Chip <Chip
label="Waiting..." label="Waiting..."
size="small" size="small"
sx={{ bgcolor: "rgb(189, 173, 85)", color: 'white', minWidth: 90 }} sx={{
bgcolor: 'rgb(189, 173, 85)',
color: 'white',
minWidth: 90,
}}
/> />
) : match.status === 'pending' ? ( ) : match.status === 'pending' ? (
<Chip <Chip
label="Analyzing..." label="Analyzing..."
size="small" size="small"
sx={{ bgcolor: theme.palette.grey[400], color: 'white', minWidth: 90 }} sx={{
bgcolor: theme.palette.grey[400],
color: 'white',
minWidth: 90,
}}
/> />
) : ( ) : (
<Chip <Chip
label="Error" label="Error"
size="small" size="small"
sx={{ bgcolor: theme.palette.error.main, color: 'white', minWidth: 90 }} sx={{
bgcolor: theme.palette.error.main,
color: 'white',
minWidth: 90,
}}
/> />
)} )}
</Box> </Box>
@ -400,7 +517,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
</Box> </Box>
) : match.status === 'error' ? ( ) : match.status === 'error' ? (
<Typography color="error"> <Typography color="error">
{match.assessment || "An error occurred while analyzing this requirement."} {match.assessment || 'An error occurred while analyzing this requirement.'}
</Typography> </Typography>
) : ( ) : (
<Box> <Box>
@ -427,10 +544,21 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
}} }}
> >
<CardContent> <CardContent>
<Typography variant="body1" component="div" sx={{ mb: 1, fontStyle: 'italic' }}> <Typography
variant="body1"
component="div"
sx={{ mb: 1, fontStyle: 'italic' }}
>
"{evidence.quote}" "{evidence.quote}"
</Typography> </Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexDirection: "column" }}> <Box
sx={{
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} Relevance: {evidence.context}
</Typography> </Typography>
@ -456,9 +584,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Skill description Skill description
</Typography> </Typography>
<Typography paragraph> <Typography paragraph>{match.description}</Typography>
{match.description}
</Typography>
{/* { match.ragResults && match.ragResults.length !== 0 && <> {/* { match.ragResults && match.ragResults.length !== 0 && <>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
RAG Information RAG Information
@ -466,7 +592,6 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
<VectorVisualizer inline rag={match.ragResults[0]} /> <VectorVisualizer inline rag={match.ragResults[0]} />
</> </>
} */} } */}
</Box> </Box>
)} )}
</AccordionDetails> </AccordionDetails>

View File

@ -65,4 +65,4 @@ const LoadingComponent: React.FC<LoadingComponentProps> = ({
); );
}; };
export { LoadingComponent}; export { LoadingComponent };

View File

@ -7,7 +7,7 @@ import {
Grid, Grid,
Chip, Chip,
FormControlLabel, FormControlLabel,
Checkbox Checkbox,
} from '@mui/material'; } from '@mui/material';
import { LocationOn, Public, Home } from '@mui/icons-material'; import { LocationOn, Public, Home } from '@mui/icons-material';
import { Country, State, City } from 'country-state-city'; import { Country, State, City } from 'country-state-city';
@ -32,7 +32,7 @@ const LocationInput: React.FC<LocationInputProps> = ({
helperText, helperText,
required = false, required = false,
disabled = false, disabled = false,
showCity = false showCity = false,
}) => { }) => {
// Get all countries from the library // Get all countries from the library
const allCountries = Country.getAllCountries(); const allCountries = Country.getAllCountries();
@ -48,7 +48,8 @@ const LocationInput: React.FC<LocationInputProps> = ({
const availableStates = selectedCountry ? State.getStatesOfCountry(selectedCountry.isoCode) : []; const availableStates = selectedCountry ? State.getStatesOfCountry(selectedCountry.isoCode) : [];
// Get cities for selected state // Get cities for selected state
const availableCities = selectedCountry && selectedState const availableCities =
selectedCountry && selectedState
? City.getCitiesOfState(selectedCountry.isoCode, selectedState.isoCode) ? City.getCitiesOfState(selectedCountry.isoCode, selectedState.isoCode)
: []; : [];
@ -88,10 +89,20 @@ const LocationInput: React.FC<LocationInputProps> = ({
} }
// Only call onChange if there's actual data or if clearing // Only call onChange if there's actual data or if clearing
if (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); onChange(newLocation);
} }
}, [selectedCountry, selectedState, selectedCity, isRemote, onChange, value.country, value.state, value.city, showCity]); }, [
selectedCountry,
selectedState,
selectedCity,
isRemote,
onChange,
value.country,
value.state,
value.city,
showCity,
]);
const handleCountryChange = (event: any, newValue: ICountry | null) => { const handleCountryChange = (event: any, newValue: ICountry | null) => {
setSelectedCountry(newValue); setSelectedCountry(newValue);
@ -128,19 +139,21 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedCountry} value={selectedCountry}
onChange={handleCountryChange} onChange={handleCountryChange}
options={allCountries} options={allCountries}
getOptionLabel={(option) => option.name} getOptionLabel={option => option.name}
disabled={disabled} disabled={disabled}
renderInput={(params) => ( renderInput={params => (
<TextField <TextField
{...params} {...params}
label="Country" label="Country"
variant="outlined" variant="outlined"
required={required} required={required}
error={error && required && !selectedCountry} error={error && required && !selectedCountry}
helperText={error && required && !selectedCountry ? 'Country is required' : helperText} helperText={
error && required && !selectedCountry ? 'Country is required' : helperText
}
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
startAdornment: <Public sx={{ mr: 1, color: 'text.secondary' }} /> startAdornment: <Public sx={{ mr: 1, color: 'text.secondary' }} />,
}} }}
/> />
)} )}
@ -167,14 +180,16 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedState} value={selectedState}
onChange={handleStateChange} onChange={handleStateChange}
options={availableStates} options={availableStates}
getOptionLabel={(option) => option.name} getOptionLabel={option => option.name}
disabled={disabled || availableStates.length === 0} disabled={disabled || availableStates.length === 0}
renderInput={(params) => ( renderInput={params => (
<TextField <TextField
{...params} {...params}
label="State/Region" label="State/Region"
variant="outlined" variant="outlined"
placeholder={availableStates.length > 0 ? "Select state/region" : "No states available"} placeholder={
availableStates.length > 0 ? 'Select state/region' : 'No states available'
}
/> />
)} )}
/> />
@ -188,17 +203,17 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedCity} value={selectedCity}
onChange={handleCityChange} onChange={handleCityChange}
options={availableCities} options={availableCities}
getOptionLabel={(option) => option.name} getOptionLabel={option => option.name}
disabled={disabled || availableCities.length === 0} disabled={disabled || availableCities.length === 0}
renderInput={(params) => ( renderInput={params => (
<TextField <TextField
{...params} {...params}
label="City" label="City"
variant="outlined" variant="outlined"
placeholder={availableCities.length > 0 ? "Select city" : "No cities available"} placeholder={availableCities.length > 0 ? 'Select city' : 'No cities available'}
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
startAdornment: <Home sx={{ mr: 1, color: 'text.secondary' }} /> startAdornment: <Home sx={{ mr: 1, color: 'text.secondary' }} />,
}} }}
/> />
)} )}
@ -251,14 +266,7 @@ const LocationInput: React.FC<LocationInputProps> = ({
size="small" size="small"
/> />
)} )}
{isRemote && ( {isRemote && <Chip label="Remote" variant="filled" color="success" size="small" />}
<Chip
label="Remote"
variant="filled"
color="success"
size="small"
/>
)}
</Box> </Box>
</Grid> </Grid>
)} )}
@ -298,11 +306,7 @@ const LocationInputDemo: React.FC = () => {
<Typography variant="h5" gutterBottom> <Typography variant="h5" gutterBottom>
Basic Location Input Basic Location Input
</Typography> </Typography>
<LocationInput <LocationInput value={location} onChange={handleLocationChange} required />
value={location}
onChange={handleLocationChange}
required
/>
</Grid> </Grid>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
@ -310,7 +314,7 @@ const LocationInputDemo: React.FC = () => {
control={ control={
<Checkbox <Checkbox
checked={showAdvanced} checked={showAdvanced}
onChange={(e) => setShowAdvanced(e.target.checked)} onChange={e => setShowAdvanced(e.target.checked)}
color="primary" color="primary"
/> />
} }
@ -336,21 +340,24 @@ const LocationInputDemo: React.FC = () => {
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Current Location Data: Current Location Data:
</Typography> </Typography>
<Box component="pre" sx={{ <Box
component="pre"
sx={{
bgcolor: 'grey.100', bgcolor: 'grey.100',
p: 2, p: 2,
borderRadius: 1, borderRadius: 1,
overflow: 'auto', overflow: 'auto',
fontSize: '0.875rem' fontSize: '0.875rem',
}}> }}
>
{JSON.stringify(location, null, 2)} {JSON.stringify(location, null, 2)}
</Box> </Box>
</Grid> </Grid>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
💡 This component uses the country-state-city library which is regularly updated 💡 This component uses the country-state-city library which is regularly updated and
and includes ISO codes, flags, and comprehensive location data. includes ISO codes, flags, and comprehensive location data.
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -4,7 +4,7 @@ import { SxProps } from '@mui/material/styles';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { useResizeObserverAndMutationObserver } from '../hooks/useAutoScrollToBottom'; import { useResizeObserverAndMutationObserver } from '../hooks/useAutoScrollToBottom';
const defaultMermaidConfig : MermaidConfig = { const defaultMermaidConfig: MermaidConfig = {
startOnLoad: true, startOnLoad: true,
securityLevel: 'loose', securityLevel: 'loose',
fontFamily: 'Fira Code', fontFamily: 'Fira Code',
@ -19,7 +19,7 @@ interface MermaidProps {
const Mermaid: React.FC<MermaidProps> = (props: MermaidProps) => { const Mermaid: React.FC<MermaidProps> = (props: MermaidProps) => {
const { chart, sx, className, mermaidConfig } = props; const { chart, sx, className, mermaidConfig } = props;
const [ visible, setVisible] = useState<boolean>(false); const [visible, setVisible] = useState<boolean>(false);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const checkVisible = useCallback(() => { const checkVisible = useCallback(() => {
@ -38,25 +38,29 @@ const Mermaid: React.FC<MermaidProps> = (props: MermaidProps) => {
await mermaid.initialize(mermaidConfig || defaultMermaidConfig); await mermaid.initialize(mermaidConfig || defaultMermaidConfig);
await mermaid.run({ nodes: [containerRef.current] }); await mermaid.run({ nodes: [containerRef.current] });
} catch (e) { } catch (e) {
console.error("Mermaid render error:", e, containerRef.current); console.error('Mermaid render error:', e, containerRef.current);
}
} }
} }
};
renderMermaid(); renderMermaid();
}, [containerRef, mermaidConfig, visible, chart]); }, [containerRef, mermaidConfig, visible, chart]);
// Observe container and TextField size, plus DOM changes // Observe container and TextField size, plus DOM changes
useResizeObserverAndMutationObserver(containerRef, null, checkVisible); useResizeObserverAndMutationObserver(containerRef, null, checkVisible);
return <Box className={className || "Mermaid"} ref={containerRef} sx={{ return (
display: "flex", <Box
className={className || 'Mermaid'}
ref={containerRef}
sx={{
display: 'flex',
flexGrow: 1, flexGrow: 1,
...sx ...sx,
}}> }}
>
{chart} {chart}
</Box>; </Box>
);
}; };
export { export { Mermaid };
Mermaid
};

View File

@ -23,7 +23,7 @@ import { SxProps, Theme } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import LocationSearchingIcon from '@mui/icons-material/LocationSearching'; import LocationSearchingIcon from '@mui/icons-material/LocationSearching';
import { 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';
@ -32,9 +32,19 @@ import { SetSnackType } from './Snack';
import { CopyBubble } from './CopyBubble'; import { CopyBubble } from './CopyBubble';
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { ChatMessage, ChatSession, ChatMessageMetaData, ChromaDBGetResponse, ApiActivityType, ChatMessageUser, ChatMessageError, ChatMessageStatus, ChatSenderType } from 'types/types'; import {
ChatMessage,
ChatSession,
ChatMessageMetaData,
ChromaDBGetResponse,
ApiActivityType,
ChatMessageUser,
ChatMessageError,
ChatMessageStatus,
ChatSenderType,
} from 'types/types';
const getStyle = (theme: Theme, type: ApiActivityType | ChatSenderType | "error"): any => { const getStyle = (theme: Theme, type: ApiActivityType | ChatSenderType | 'error'): any => {
const defaultRadius = '16px'; const defaultRadius = '16px';
const defaultStyle = { const defaultStyle = {
padding: theme.spacing(1, 2), padding: theme.spacing(1, 2),
@ -163,9 +173,11 @@ const getStyle = (theme: Theme, type: ApiActivityType | ChatSenderType | "error"
} }
return styles[type]; return styles[type];
} };
const getIcon = (activityType: ApiActivityType | ChatSenderType | "error"): React.ReactNode | null => { const getIcon = (
activityType: ApiActivityType | ChatSenderType | 'error'
): React.ReactNode | null => {
const icons: any = { const icons: any = {
error: <ErrorOutline color="error" />, error: <ErrorOutline color="error" />,
generating: <LocationSearchingIcon />, generating: <LocationSearchingIcon />,
@ -177,23 +189,23 @@ const getIcon = (activityType: ApiActivityType | ChatSenderType | "error"): Reac
tooling: <LocationSearchingIcon />, tooling: <LocationSearchingIcon />,
}; };
return icons[activityType] || null; return icons[activityType] || null;
} };
interface MessageProps extends BackstoryElementProps { interface MessageProps extends BackstoryElementProps {
message: ChatMessageUser | ChatMessage | ChatMessageError | ChatMessageStatus, message: ChatMessageUser | ChatMessage | ChatMessageError | ChatMessageStatus;
title?: string, title?: string;
chatSession?: ChatSession, chatSession?: ChatSession;
className?: string, className?: string;
sx?: SxProps<Theme>, sx?: SxProps<Theme>;
expandable?: boolean, expandable?: boolean;
expanded?: boolean, expanded?: boolean;
onExpand?: (open: boolean) => void, onExpand?: (open: boolean) => void;
}; }
interface MessageMetaProps { interface MessageMetaProps {
metadata: ChatMessageMetaData, metadata: ChatMessageMetaData;
messageProps: MessageProps messageProps: MessageProps;
}; }
const MessageMeta = (props: MessageMetaProps) => { const MessageMeta = (props: MessageMetaProps) => {
const { const {
@ -207,162 +219,255 @@ const MessageMeta = (props: MessageMetaProps) => {
} = props.metadata || {}; } = props.metadata || {};
const message: any = props.messageProps.message; const message: any = props.messageProps.message;
let llm_submission: string = "<|system|>\n" let llm_submission = '<|system|>\n';
llm_submission += message.system_prompt + "\n\n" llm_submission += message.system_prompt + '\n\n';
llm_submission += message.context_prompt llm_submission += message.context_prompt;
return (<> return (
{ <>
promptEvalDuration !== 0 && evalDuration !== 0 && <> {promptEvalDuration !== 0 && evalDuration !== 0 && (
<>
<TableContainer component={Card} className="PromptStats" sx={{ mb: 1 }}> <TableContainer component={Card} className="PromptStats" sx={{ mb: 1 }}>
<Table aria-label="prompt stats" size="small"> <Table aria-label="prompt stats" size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell></TableCell> <TableCell></TableCell>
<TableCell align="right" >Tokens</TableCell> <TableCell align="right">Tokens</TableCell>
<TableCell align="right">Time (s)</TableCell> <TableCell align="right">Time (s)</TableCell>
<TableCell align="right">TPS</TableCell> <TableCell align="right">TPS</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <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> <TableCell component="th" scope="row">
Prompt
</TableCell>
<TableCell align="right">{promptEvalCount}</TableCell> <TableCell align="right">{promptEvalCount}</TableCell>
<TableCell align="right">{Math.round(promptEvalDuration / 10 ** 7) / 100}</TableCell> <TableCell align="right">
<TableCell align="right">{Math.round(promptEvalCount * 10 ** 9 / promptEvalDuration)}</TableCell> {Math.round(promptEvalDuration / 10 ** 7) / 100}
</TableCell>
<TableCell align="right">
{Math.round((promptEvalCount * 10 ** 9) / promptEvalDuration)}
</TableCell>
</TableRow> </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 component="th" scope="row">
Response
</TableCell>
<TableCell align="right">{evalCount}</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> <TableCell align="right">
{Math.round((evalCount * 10 ** 9) / evalDuration)}
</TableCell>
</TableRow> </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 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}</TableCell> <TableCell align="right">
<TableCell align="right">{Math.round((promptEvalCount + evalCount) * 10 ** 9 / (promptEvalDuration + evalDuration))}</TableCell> {Math.round((promptEvalDuration + evalDuration) / 10 ** 7) / 100}
</TableCell>
<TableCell align="right">
{Math.round(
((promptEvalCount + evalCount) * 10 ** 9) /
(promptEvalDuration + evalDuration)
)}
</TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
</> </>
} )}
{ {tools && tools.tool_calls && tools.tool_calls.length !== 0 && (
tools && tools.tool_calls && tools.tool_calls.length !== 0 && <Accordion sx={{ boxSizing: 'border-box' }}>
<Accordion sx={{ boxSizing: "border-box" }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: '0.8rem' }}>Tools queried</Box>
Tools queried
</Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
{ {tools.tool_calls.map((tool: any, index: number) => (
tools.tool_calls.map((tool: any, index: number) => <Box
<Box key={index} sx={{ m: 0, p: 1, pt: 0, display: "flex", flexDirection: "column", border: "1px solid #e0e0e0" }}> key={index}
sx={{
m: 0,
p: 1,
pt: 0,
display: 'flex',
flexDirection: 'column',
border: '1px solid #e0e0e0',
}}
>
{index !== 0 && <Divider />} {index !== 0 && <Divider />}
<Box sx={{ fontSize: "0.75rem", display: "flex", flexDirection: "column", mt: 1, mb: 1, fontWeight: "bold" }}> <Box
sx={{
fontSize: '0.75rem',
display: 'flex',
flexDirection: 'column',
mt: 1,
mb: 1,
fontWeight: 'bold',
}}
>
{tool.name} {tool.name}
</Box> </Box>
{tool.content !== "null" && {tool.content !== 'null' && (
<JsonView <JsonView
displayDataTypes={false} displayDataTypes={false}
objectSortKeys={true} objectSortKeys={true}
collapsed={1} value={JSON.parse(tool.content)} style={{ fontSize: "0.8rem", maxHeight: "20rem", overflow: "auto" }}> collapsed={1}
value={JSON.parse(tool.content)}
style={{
fontSize: '0.8rem',
maxHeight: '20rem',
overflow: 'auto',
}}
>
<JsonView.String <JsonView.String
render={({ children, ...reset }) => { render={({ children, ...reset }) => {
if (typeof (children) === "string" && children.match("\n")) { if (typeof children === 'string' && children.match('\n')) {
return <pre {...reset} style={{ display: "flex", border: "none", ...reset.style }}>{children}</pre> return (
<pre
{...reset}
style={{
display: 'flex',
border: 'none',
...reset.style,
}}
>
{children}
</pre>
);
} }
}} }}
/> />
</JsonView> </JsonView>
} )}
{tool.content === "null" && "No response from tool call"} {tool.content === 'null' && 'No response from tool call'}
</Box>) </Box>
} ))}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
} )}
{ {ragResults.map((collection: ChromaDBGetResponse) => (
ragResults.map((collection: ChromaDBGetResponse) => (
<Accordion key={collection.name}> <Accordion key={collection.name}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: '0.8rem' }}>
Top {collection.ids?.length} RAG matches from {collection.size} entries using an embedding vector of {collection.queryEmbedding?.length} dimensions Top {collection.ids?.length} RAG matches from {collection.size} entries using an
embedding vector of {collection.queryEmbedding?.length} dimensions
</Box> </Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<VectorVisualizer inline <VectorVisualizer inline {...props.messageProps} {...props.metadata} rag={collection} />
{...props.messageProps} {...props.metadata}
rag={collection} />
{/* { ...rag, query: message.prompt }} /> */} {/* { ...rag, query: message.prompt }} /> */}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
)) ))}
}
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: '0.8rem' }}>Full Response Details</Box>
Full Response Details
</Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Box sx={{ pb: 1 }}>Copy LLM submission: <CopyBubble content={llm_submission} /></Box> <Box sx={{ pb: 1 }}>
<JsonView displayDataTypes={false} objectSortKeys={true} collapsed={1} value={message} style={{ fontSize: "0.8rem", maxHeight: "20rem", overflow: "auto" }}> Copy LLM submission: <CopyBubble content={llm_submission} />
</Box>
<JsonView
displayDataTypes={false}
objectSortKeys={true}
collapsed={1}
value={message}
style={{ fontSize: '0.8rem', maxHeight: '20rem', overflow: 'auto' }}
>
<JsonView.String <JsonView.String
render={({ children, ...reset }) => { render={({ children, ...reset }) => {
if (typeof (children) === "string" && children.match("\n")) { if (typeof children === 'string' && children.match('\n')) {
return <pre {...reset} style={{ display: "inline", border: "none", ...reset.style }}>{children.trim()}</pre> return (
<pre
{...reset}
style={{
display: 'inline',
border: 'none',
...reset.style,
}}
>
{children.trim()}
</pre>
);
} }
}} }}
/> />
</JsonView> </JsonView>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
</>); </>
);
}; };
interface MessageContainerProps { interface MessageContainerProps {
type: ApiActivityType | ChatSenderType | "error", type: ApiActivityType | ChatSenderType | 'error';
metadataView?: React.ReactNode | null, metadataView?: React.ReactNode | null;
messageView?: React.ReactNode | null, messageView?: React.ReactNode | null;
sx?: SxProps<Theme>, sx?: SxProps<Theme>;
copyContent?: string, copyContent?: string;
}; }
const MessageContainer = (props: MessageContainerProps) => { const MessageContainer = (props: MessageContainerProps) => {
const { type, sx, messageView, metadataView, copyContent } = props; const { type, sx, messageView, metadataView, copyContent } = props;
const icon = getIcon(type); const icon = getIcon(type);
return <Box return (
<Box
className={`Message Message-${type}`} className={`Message Message-${type}`}
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
m: 0, m: 0,
mt: 1, mt: 1,
marginBottom: "0px !important", // Remove whitespace from expanded Accordion marginBottom: '0px !important', // Remove whitespace from expanded Accordion
gap: 1, gap: 1,
...sx, ...sx,
}}> }}
<Box sx={{ display: "flex", flexDirection: 'row', alignItems: 'center', gap: 1 }}> >
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 1,
}}
>
{icon !== null && icon} {icon !== null && icon}
{messageView} {messageView}
</Box> </Box>
<Box flex={{ display: "flex", position: "relative", flexDirection: "row", justifyContent: "flex-end", alignItems: "center" }}> <Box
{copyContent && <CopyBubble content={copyContent} sx={{ position: "absolute", top: "11px", left: 0 }} />} flex={{
display: 'flex',
position: 'relative',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
{copyContent && (
<CopyBubble content={copyContent} sx={{ position: 'absolute', top: '11px', left: 0 }} />
)}
{metadataView} {metadataView}
</Box> </Box>
</Box>; </Box>
);
}; };
const Message = (props: MessageProps) => { 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 [metaExpanded, setMetaExpanded] = useState<boolean>(false);
const theme = useTheme(); const theme = useTheme();
const type: ApiActivityType | ChatSenderType | "error" = ('activity' in message) ? message.activity : ('error' in message) ? 'error' : (message as ChatMessage).role; const type: ApiActivityType | ChatSenderType | 'error' =
'activity' in message
? message.activity
: 'error' in message
? 'error'
: (message as ChatMessage).role;
const style: any = getStyle(theme, type); const style: any = getStyle(theme, type);
const handleMetaExpandClick = () => { const handleMetaExpandClick = () => {
@ -370,32 +475,48 @@ const Message = (props: MessageProps) => {
}; };
let content; let content;
if (typeof (message.content) === "string") { if (typeof message.content === 'string') {
content = message.content.trim(); content = message.content.trim();
} else { } else {
console.error(`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 (<></>) return <></>;
} }
if (!content) { if (!content) {
return (<></>) return <></>;
}; }
const messageView = ( const messageView = (
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={content} /> <StyledMarkdown
chatSession={chatSession}
streaming={message.status === 'streaming'}
content={content}
/>
); );
let metadataView = (<></>); let metadataView = <></>;
let metadata: ChatMessageMetaData | null = ('metadata' in message) ? (message.metadata as ChatMessageMetaData || null) : null; let metadata: ChatMessageMetaData | null =
'metadata' in message ? (message.metadata as ChatMessageMetaData) || null : null;
if ('role' in message && message.role === 'user') { if ('role' in message && message.role === 'user') {
metadata = null; metadata = null;
} }
if (metadata) { if (metadata) {
metadataView = ( metadataView = (
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}> <Box sx={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexDirection: "row" }}> <Box
<Box sx={{ display: "flex", flexGrow: 1 }} /> sx={{
<Button variant="text" onClick={handleMetaExpandClick} sx={{ flexShrink: 1, color: "darkgrey", p: 0 }}> display: 'flex',
alignItems: 'center',
gap: 1,
flexDirection: 'row',
}}
>
<Box sx={{ display: 'flex', flexGrow: 1 }} />
<Button
variant="text"
onClick={handleMetaExpandClick}
sx={{ flexShrink: 1, color: 'darkgrey', p: 0 }}
>
LLM information for this query LLM information for this query
</Button> </Button>
<ExpandMore <ExpandMore
@ -403,7 +524,8 @@ const Message = (props: MessageProps) => {
expand={metaExpanded} expand={metaExpanded}
onClick={handleMetaExpandClick} onClick={handleMetaExpandClick}
aria-expanded={true /*message.expanded*/} aria-expanded={true /*message.expanded*/}
aria-label="show more"> aria-label="show more"
>
<ExpandMoreIcon /> <ExpandMoreIcon />
</ExpandMore> </ExpandMore>
</Box> </Box>
@ -412,16 +534,26 @@ const Message = (props: MessageProps) => {
<MessageMeta messageProps={props} metadata={metadata} /> <MessageMeta messageProps={props} metadata={metadata} />
</CardContent> </CardContent>
</Collapse> </Collapse>
</Box>); </Box>
);
} }
const copyContent = (type === 'assistant') ? message.content : undefined; const copyContent = type === 'assistant' ? message.content : undefined;
if (!expandable) { if (!expandable) {
/* When not expandable, the styles are applied directly to MessageContainer */ /* When not expandable, the styles are applied directly to MessageContainer */
return (<> return (
{messageView && <MessageContainer copyContent={copyContent} type={type} {...{ messageView, metadataView }} sx={{ ...style, ...sx }} />} <>
</>); {messageView && (
<MessageContainer
copyContent={copyContent}
type={type}
{...{ messageView, metadataView }}
sx={{ ...style, ...sx }}
/>
)}
</>
);
} }
// Determine if Accordion is controlled // Determine if Accordion is controlled
@ -431,8 +563,11 @@ const Message = (props: MessageProps) => {
expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled
defaultExpanded={expanded} // Default to collapsed for uncontrolled Accordion defaultExpanded={expanded} // Default to collapsed for uncontrolled Accordion
className={className} className={className}
onChange={(_event, newExpanded) => { isControlled && onExpand && onExpand(newExpanded) }} onChange={(_event, newExpanded) => {
sx={{ ...sx, ...style }}> isControlled && onExpand && onExpand(newExpanded);
}}
sx={{ ...sx, ...style }}
>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMoreIcon />} expandIcon={<ExpandMoreIcon />}
slotProps={{ slotProps={{
@ -440,27 +575,27 @@ const Message = (props: MessageProps) => {
sx: { sx: {
display: 'flex', display: 'flex',
justifyItems: 'center', justifyItems: 'center',
m: 0, p: 0, m: 0,
p: 0,
fontWeight: 'bold', fontWeight: 'bold',
fontSize: '1.1rem', fontSize: '1.1rem',
}, },
}, },
}}> }}
>
{title || ''} {title || ''}
</AccordionSummary> </AccordionSummary>
<AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}> <AccordionDetails sx={{ mt: 0, mb: 0, p: 0, pl: 2, pr: 2 }}>
<MessageContainer copyContent={copyContent} type={type} {...{ messageView, metadataView }} /> <MessageContainer
copyContent={copyContent}
type={type}
{...{ messageView, metadataView }}
/>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
); );
}
export type {
MessageProps,
}; };
export { export type { MessageProps };
Message,
MessageMeta,
};
export { Message, MessageMeta };

View File

@ -136,7 +136,7 @@ const Pulse: React.FC<PulseProps> = ({ timestamp, sx }) => {
`} `}
</style> </style>
<Box sx={{...containerStyle, ...sx}}> <Box sx={{ ...containerStyle, ...sx }}>
{/* Base circle */} {/* Base circle */}
<div style={coreStyle} /> <div style={coreStyle} />
@ -144,35 +144,21 @@ const Pulse: React.FC<PulseProps> = ({ timestamp, sx }) => {
{isAnimating && ( {isAnimating && (
<> <>
{/* Primary pulse ring */} {/* Primary pulse ring */}
<div <div key={`pulse-1-${animationKey}`} style={pulseRing1Style} />
key={`pulse-1-${animationKey}`}
style={pulseRing1Style}
/>
{/* Secondary pulse ring with delay */} {/* Secondary pulse ring with delay */}
<div <div key={`pulse-2-${animationKey}`} style={pulseRing2Style} />
key={`pulse-2-${animationKey}`}
style={pulseRing2Style}
/>
{/* Ripple effect */} {/* Ripple effect */}
<div <div key={`ripple-${animationKey}`} style={rippleStyle} />
key={`ripple-${animationKey}`}
style={rippleStyle}
/>
{/* Outer ripple */} {/* Outer ripple */}
<div <div key={`ripple-outer-${animationKey}`} style={outerRippleStyle} />
key={`ripple-outer-${animationKey}`}
style={outerRippleStyle}
/>
</> </>
)} )}
</Box> </Box>
</> </>
); );
}; };
export { Pulse } ; export { Pulse };

View File

@ -7,7 +7,7 @@ interface QuoteContainerProps {
} }
const QuoteContainer = styled(Paper, { const QuoteContainer = styled(Paper, {
shouldForwardProp: (prop) => prop !== 'size', shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({ })<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
position: 'relative', position: 'relative',
padding: size === 'small' ? theme.spacing(1) : theme.spacing(4), padding: size === 'small' ? theme.spacing(1) : theme.spacing(4),
@ -29,7 +29,7 @@ const QuoteContainer = styled(Paper, {
})); }));
const QuoteText = styled(Typography, { const QuoteText = styled(Typography, {
shouldForwardProp: (prop) => prop !== 'size', shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({ })<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
fontSize: size === 'small' ? '0.9rem' : '1.2rem', fontSize: size === 'small' ? '0.9rem' : '1.2rem',
lineHeight: size === 'small' ? 1.4 : 1.6, lineHeight: size === 'small' ? 1.4 : 1.6,
@ -43,7 +43,7 @@ const QuoteText = styled(Typography, {
})); }));
const QuoteMark = styled(Typography, { const QuoteMark = styled(Typography, {
shouldForwardProp: (prop) => prop !== 'size', shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({ })<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
fontSize: size === 'small' ? '2.5rem' : '4rem', fontSize: size === 'small' ? '2.5rem' : '4rem',
fontFamily: '"Georgia", "Times New Roman", serif', fontFamily: '"Georgia", "Times New Roman", serif',
@ -67,7 +67,7 @@ const ClosingQuote = styled(QuoteMark)(({ size = 'normal' }: QuoteContainerProps
})); }));
const AuthorText = styled(Typography, { const AuthorText = styled(Typography, {
shouldForwardProp: (prop) => prop !== 'size', shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({ })<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
marginTop: size === 'small' ? theme.spacing(1) : theme.spacing(2), marginTop: size === 'small' ? theme.spacing(1) : theme.spacing(2),
textAlign: 'right', textAlign: 'right',
@ -82,7 +82,7 @@ const AuthorText = styled(Typography, {
})); }));
const AccentLine = styled(Box, { const AccentLine = styled(Box, {
shouldForwardProp: (prop) => prop !== 'size', shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({ })<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
width: size === 'small' ? '40px' : '60px', width: size === 'small' ? '40px' : '60px',
height: size === 'small' ? '1px' : '2px', height: size === 'small' ? '1px' : '2px',

View File

@ -1,14 +1,6 @@
import React, { useState, useCallback, useRef, useEffect } from 'react'; import React, { useState, useCallback, useRef, useEffect } from 'react';
import { import { Tabs, Tab, Box, Button, Paper, Typography, LinearProgress } from '@mui/material';
Tabs, import { Job, Candidate, SkillAssessment } from 'types/types';
Tab,
Box,
Button,
Paper,
Typography,
LinearProgress,
} from '@mui/material';
import { Job, Candidate, SkillAssessment } from "types/types";
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import * as Types from 'types/types'; import * as Types from 'types/types';
@ -31,7 +23,12 @@ interface ResumeGeneratorProps {
} }
const defaultMessage: Types.ChatMessageStatus = { const defaultMessage: Types.ChatMessageStatus = {
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", activity: 'info' status: 'done',
type: 'text',
sessionId: '',
timestamp: new Date(),
content: '',
activity: 'info',
}; };
const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => { const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => {
@ -49,7 +46,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
setTabValue(newValue); setTabValue(newValue);
} };
useEffect(() => { useEffect(() => {
if (!job || !candidate || !skills || generated) { if (!job || !candidate || !skills || generated) {
@ -58,8 +55,8 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
setGenerated(true); setGenerated(true);
setStatusType("thinking"); setStatusType('thinking');
setStatus("Starting resume generation..."); setStatus('Starting resume generation...');
const generateResumeHandlers: StreamingOptions<Types.ChatMessageResume> = { const generateResumeHandlers: StreamingOptions<Types.ChatMessageResume> = {
onMessage: (message: Types.ChatMessageResume) => { onMessage: (message: Types.ChatMessageResume) => {
@ -71,7 +68,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
onStreaming: (chunk: Types.ChatMessageStreaming) => { onStreaming: (chunk: Types.ChatMessageStreaming) => {
if (status === '') { if (status === '') {
setStatus('Generating resume...'); setStatus('Generating resume...');
setStatusType("generating"); setStatusType('generating');
} }
setResume(chunk.content); setResume(chunk.content);
}, },
@ -92,7 +89,11 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
}; };
const generateResume = async () => { const generateResume = async () => {
const request: any = await apiClient.generateResume(candidate.id || '', job.id || '', generateResumeHandlers); const request: any = await apiClient.generateResume(
candidate.id || '',
job.id || '',
generateResumeHandlers
);
const result = await request.promise; const result = await request.promise;
}; };
@ -125,18 +126,22 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
<Box <Box
className="ResumeGenerator" className="ResumeGenerator"
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
}}> }}
{user?.isAdmin && <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 1 }}> >
{user?.isAdmin && (
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 1 }}>
<Tabs value={tabValue} onChange={handleTabChange} centered> <Tabs value={tabValue} onChange={handleTabChange} centered>
<Tab disabled={systemPrompt === ''} value="system" icon={<TuneIcon />} label="System" /> <Tab disabled={systemPrompt === ''} value="system" icon={<TuneIcon />} label="System" />
<Tab disabled={prompt === ''} value="prompt" icon={<InputIcon />} label="Prompt" /> <Tab disabled={prompt === ''} value="prompt" icon={<InputIcon />} label="Prompt" />
<Tab disabled={resume === ''} value="resume" icon={<ArticleIcon />} label="Resume" /> <Tab disabled={resume === ''} value="resume" icon={<ArticleIcon />} label="Resume" />
</Tabs> </Tabs>
</Box>} </Box>
)}
{status && <Box sx={{ mt: 0, mb: 1 }}> {status && (
<Box sx={{ mt: 0, mb: 1 }}>
<StatusBox> <StatusBox>
{statusType && <StatusIcon type={statusType} />} {statusType && <StatusIcon type={statusType} />}
<Typography variant="body2" sx={{ ml: 1 }}> <Typography variant="body2" sx={{ ml: 1 }}>
@ -144,23 +149,35 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
</Typography> </Typography>
</StatusBox> </StatusBox>
{status && !error && <LinearProgress sx={{ mt: 1 }} />} {status && !error && <LinearProgress sx={{ mt: 1 }} />}
</Box>} </Box>
)}
<Paper elevation={3} sx={{ p: 3, m: 1, mt: 0 }}><Scrollable autoscroll sx={{ display: "flex", flexGrow: 1, position: "relative" }}> <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 === 'system' && <pre>{systemPrompt}</pre>}
{tabValue === 'prompt' && <pre>{prompt}</pre>} {tabValue === 'prompt' && <pre>{prompt}</pre>}
{tabValue === 'resume' && <><CopyBubble onClick={() => { setSnack('Resume copied to clipboard!'); }} sx={{ position: "absolute", top: 0, right: 0 }} content={resume} /><StyledMarkdown content={resume} /></>} {tabValue === 'resume' && (
</Scrollable></Paper> <>
<CopyBubble
onClick={() => {
setSnack('Resume copied to clipboard!');
}}
sx={{ position: 'absolute', top: 0, right: 0 }}
content={resume}
/>
<StyledMarkdown content={resume} />
</>
)}
</Scrollable>
</Paper>
{resume && !status && !error && <Button onClick={handleSave} variant="contained" color="primary" sx={{ mt: 2 }}> {resume && !status && !error && (
<Button onClick={handleSave} variant="contained" color="primary" sx={{ mt: 2 }}>
Save Resume Save Resume
</Button>} </Button>
)}
</Box> </Box>
) );
};
export {
ResumeGenerator
}; };
export { ResumeGenerator };

View File

@ -14,14 +14,27 @@ interface ScrollableProps {
} }
const Scrollable = forwardRef((props: ScrollableProps, ref) => { const Scrollable = forwardRef((props: ScrollableProps, ref) => {
const { sx, className, children, autoscroll, textFieldRef, fallbackThreshold = 0.33, contentUpdateTrigger } = props; const {
sx,
className,
children,
autoscroll,
textFieldRef,
fallbackThreshold = 0.33,
contentUpdateTrigger,
} = props;
// Create a default ref if textFieldRef is not provided // Create a default ref if textFieldRef is not provided
const defaultTextFieldRef = useRef<HTMLElement | null>(null); const defaultTextFieldRef = useRef<HTMLElement | null>(null);
const scrollRef = useAutoScrollToBottom(textFieldRef ?? defaultTextFieldRef, true, fallbackThreshold, contentUpdateTrigger); const scrollRef = useAutoScrollToBottom(
textFieldRef ?? defaultTextFieldRef,
true,
fallbackThreshold,
contentUpdateTrigger
);
return ( return (
<Box <Box
className={`Scrollable ${className || ""}`} className={`Scrollable ${className || ''}`}
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',

View File

@ -10,40 +10,37 @@ type SetSnackType = (message: string, severity?: SeverityType) => void;
interface SnackHandle { interface SnackHandle {
setSnack: SetSnackType; setSnack: SetSnackType;
}; }
interface SnackProps { interface SnackProps {
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
className?: string; className?: string;
}; }
const Snack = forwardRef<SnackHandle, SnackProps>(({ const Snack = forwardRef<SnackHandle, SnackProps>(({ className, sx }: SnackProps, ref) => {
className,
sx
}: SnackProps, ref) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [message, setMessage] = useState(""); const [message, setMessage] = useState('');
const [severity, setSeverity] = useState<SeverityType>("success"); const [severity, setSeverity] = useState<SeverityType>('success');
// Set the snack pop-up and open it // Set the snack pop-up and open it
const setSnack: SetSnackType = useCallback<SetSnackType>((message: string, severity: SeverityType = "success") => { const setSnack: SetSnackType = useCallback<SetSnackType>(
(message: string, severity: SeverityType = 'success') => {
setTimeout(() => { setTimeout(() => {
setMessage(message); setMessage(message);
setSeverity(severity); setSeverity(severity);
setOpen(true); setOpen(true);
}); });
}, [setMessage, setSeverity, setOpen]); },
[setMessage, setSeverity, setOpen]
);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
setSnack: (message: string, severity?: SeverityType) => { setSnack: (message: string, severity?: SeverityType) => {
setSnack(message, severity); setSnack(message, severity);
} },
})); }));
const handleSnackClose = ( const handleSnackClose = (event: React.SyntheticEvent | Event, reason?: SnackbarCloseReason) => {
event: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason,
) => {
if (reason === 'clickaway') { if (reason === 'clickaway') {
return; return;
} }
@ -53,28 +50,19 @@ const Snack = forwardRef<SnackHandle, SnackProps>(({
return ( return (
<Snackbar <Snackbar
className={className || "Snack"} className={className || 'Snack'}
sx={{ ...sx }} sx={{ ...sx }}
open={open} open={open}
autoHideDuration={(severity === "success" || severity === "info") ? 1500 : 6000} autoHideDuration={severity === 'success' || severity === 'info' ? 1500 : 6000}
onClose={handleSnackClose}>
<Alert
onClose={handleSnackClose} onClose={handleSnackClose}
severity={severity}
variant="filled"
sx={{ width: '100%' }}
> >
<Alert onClose={handleSnackClose} severity={severity} variant="filled" sx={{ width: '100%' }}>
{message} {message}
</Alert> </Alert>
</Snackbar> </Snackbar>
) );
}); });
export type { export type { SeverityType, SetSnackType };
SeverityType,
SetSnackType
};
export { export { Snack };
Snack
};

View File

@ -17,66 +17,90 @@ import { CandidateQuestion, ChatQuery, ChatSession } from 'types/types';
import { ChatSubmitQueryInterface } from 'components/BackstoryQuery'; import { ChatSubmitQueryInterface } from 'components/BackstoryQuery';
interface StyledMarkdownProps extends BackstoryElementProps { interface StyledMarkdownProps extends BackstoryElementProps {
className?: string, className?: string;
content: string, content: string;
streaming?: boolean, streaming?: boolean;
chatSession?: ChatSession, chatSession?: ChatSession;
submitQuery?: ChatSubmitQueryInterface 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 { className, content, chatSession, submitQuery, sx, streaming } = props;
const theme = useTheme(); const theme = useTheme();
const overrides: any = { const overrides: any = {
p: { component: (element: any) =>{ p: {
return <div>{element.children}</div> component: (element: any) => {
}}, return <div>{element.children}</div>;
},
},
pre: { pre: {
component: (element: any) => { component: (element: any) => {
const { className } = element.children.props; const { className } = element.children.props;
const content = element.children?.props?.children || ""; const content = element.children?.props?.children || '';
if (className === "lang-mermaid" && !streaming) { if (className === 'lang-mermaid' && !streaming) {
return <Mermaid className="Mermaid" chart={content} />; return <Mermaid className="Mermaid" chart={content} />;
} }
if (className === "lang-markdown") { if (className === 'lang-markdown') {
return <MuiMarkdown children={content} />; return <MuiMarkdown children={content} />;
} }
if (className === "lang-json" && !streaming) { if (className === 'lang-json' && !streaming) {
try { try {
let fixed = JSON.parse(jsonrepair(content)); const fixed = JSON.parse(jsonrepair(content));
return <Scrollable className="JsonViewScrollable"> return (
<Scrollable className="JsonViewScrollable">
<JsonView <JsonView
className="JsonView" className="JsonView"
style={{ style={{
...vscodeTheme, ...vscodeTheme,
fontSize: "0.8rem", fontSize: '0.8rem',
maxHeight: "10rem", maxHeight: '10rem',
padding: "14px 0", padding: '14px 0',
overflow: "hidden", overflow: 'hidden',
width: "100%", width: '100%',
minHeight: "max-content", minHeight: 'max-content',
backgroundColor: "transparent", backgroundColor: 'transparent',
}} }}
displayDataTypes={false} displayDataTypes={false}
objectSortKeys={false} objectSortKeys={false}
collapsed={1} collapsed={1}
shortenTextAfterLength={100} shortenTextAfterLength={100}
value={fixed}> value={fixed}
>
<JsonView.String <JsonView.String
render={({ children, ...reset }) => { render={({ children, ...reset }) => {
if (typeof (children) === "string" && children.match("\n")) { if (typeof children === 'string' && children.match('\n')) {
return <pre {...reset} style={{ display: "flex", border: "none", ...reset.style }}>{children}</pre> return (
<pre
{...reset}
style={{
display: 'flex',
border: 'none',
...reset.style,
}}
>
{children}
</pre>
);
} }
}} }}
/> />
</JsonView> </JsonView>
</Scrollable> </Scrollable>
);
} catch (e) { } catch (e) {
return <pre><code className="JsonRaw">{content}</code></pre> return (
}; <pre>
<code className="JsonRaw">{content}</code>
</pre>
);
} }
return <pre><code className={className || ''}>{element.children}</code></pre>; }
return (
<pre>
<code className={className || ''}>{element.children}</code>
</pre>
);
}, },
}, },
a: { a: {
@ -84,7 +108,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
props: { props: {
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => { onClick: (event: React.MouseEvent<HTMLAnchorElement>) => {
const href = event.currentTarget.getAttribute('href'); const href = event.currentTarget.getAttribute('href');
console.log("StyledMarkdown onClick:", href); console.log('StyledMarkdown onClick:', href);
if (href) { if (href) {
if (href.match(/^\//)) { if (href.match(/^\//)) {
event.preventDefault(); event.preventDefault();
@ -93,15 +117,15 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
} }
}, },
sx: { sx: {
wordBreak: "break-all", wordBreak: 'break-all',
color: theme.palette.secondary.main, color: theme.palette.secondary.main,
textDecoration: 'none', textDecoration: 'none',
'&:hover': { '&:hover': {
color: theme.palette.custom.highlight, color: theme.palette.custom.highlight,
textDecoration: 'underline', textDecoration: 'underline',
} },
} },
} },
}, },
BackstoryQuery: { BackstoryQuery: {
component: (props: { query: string }) => { component: (props: { query: string }) => {
@ -109,16 +133,20 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
try { try {
const query = JSON.parse(queryString); const query = JSON.parse(queryString);
const backstoryQuestion: CandidateQuestion = { const backstoryQuestion: CandidateQuestion = {
question: queryString question: queryString,
} };
return submitQuery ? <BackstoryQuery submitQuery={submitQuery} question={query} /> : query.question; return submitQuery ? (
<BackstoryQuery submitQuery={submitQuery} question={query} />
) : (
query.question
);
} catch (e) { } catch (e) {
console.log("StyledMarkdown error:", queryString, e); console.log('StyledMarkdown error:', queryString, e);
return props.query; return props.query;
} }
}, },
} },
}; };
if (chatSession) { if (chatSession) {
@ -126,31 +154,31 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
component: (props: { prompt: string }) => { component: (props: { prompt: string }) => {
const prompt = props.prompt.replace(/(\w+):/g, '"$1":'); const prompt = props.prompt.replace(/(\w+):/g, '"$1":');
try { try {
return <GenerateImage {...{ chatSession, prompt }} /> return <GenerateImage {...{ chatSession, prompt }} />;
} catch (e) { } catch (e) {
console.log("StyledMarkdown error:", prompt, e); console.log('StyledMarkdown error:', prompt, e);
return props.prompt; return props.prompt;
} }
} },
} };
} }
return <Box return (
className={`MuiMarkdown ${className || ""}`} <Box
className={`MuiMarkdown ${className || ''}`}
sx={{ sx={{
display: "flex", display: 'flex',
m: 0, m: 0,
p: 0, p: 0,
boxSizing: "border-box", boxSizing: 'border-box',
flexGrow: 1, flexGrow: 1,
height: "auto", height: 'auto',
...sx ...sx,
}}> }}
<MuiMarkdown >
overrides={overrides} <MuiMarkdown overrides={overrides} children={content} />
children={content} </Box>
/> );
</Box>;
}; };
export { StyledMarkdown }; export { StyledMarkdown };

View File

@ -28,7 +28,7 @@ import { useNavigate } from 'react-router-dom';
interface VectorVisualizerProps extends BackstoryPageProps { interface VectorVisualizerProps extends BackstoryPageProps {
inline?: boolean; inline?: boolean;
rag?: Types.ChromaDBGetResponse; rag?: Types.ChromaDBGetResponse;
}; }
interface Metadata { interface Metadata {
id: string; id: string;
@ -43,10 +43,10 @@ const emptyQuerySet: Types.ChromaDBGetResponse = {
metadatas: [], metadatas: [],
embeddings: [], embeddings: [],
distances: [], distances: [],
name: "Empty", name: 'Empty',
size: 0, size: 0,
dimensions: 2, dimensions: 2,
query: "" query: '',
}; };
interface PlotData { interface PlotData {
@ -105,9 +105,7 @@ const config: Partial<Plotly.Config> = {
// | "hovercompare" // | "hovercompare"
// | "hoverclosest" // | "hoverclosest"
// | "v1hovermode"; // | "v1hovermode";
modeBarButtonsToRemove: [ modeBarButtonsToRemove: ['lasso2d', 'select2d'],
'lasso2d', 'select2d',
]
}; };
const layout: Partial<Plotly.Layout> = { const layout: Partial<Plotly.Layout> = {
@ -132,9 +130,9 @@ const layout: Partial<Plotly.Layout> = {
y: 0, // Vertical position (0 to 1, 0 is bottom, 1 is top) y: 0, // Vertical position (0 to 1, 0 is bottom, 1 is top)
xanchor: 'left', xanchor: 'left',
yanchor: 'top', yanchor: 'top',
orientation: 'h' // 'v' for horizontal legend orientation: 'h', // 'v' for horizontal legend
}, },
showlegend: true // Show the legend showlegend: true, // Show the legend
}; };
const normalizeDimension = (arr: number[]): number[] => { const normalizeDimension = (arr: number[]): number[] => {
@ -160,26 +158,26 @@ const colorMap: Record<string, string> = {
projects: '#1A2536', // Midnight Blue — rich and deep projects: '#1A2536', // Midnight Blue — rich and deep
news: '#D3CDBF', // Warm Gray — soft and neutral news: '#D3CDBF', // Warm Gray — soft and neutral
'performance-reviews': '#8FD0D0', // Light red 'performance-reviews': '#8FD0D0', // Light red
'jobs': '#F3aD8F', // Warm Gray — soft and neutral jobs: '#F3aD8F', // Warm Gray — soft and neutral
}; };
const DEFAULT_SIZE = 6.; const DEFAULT_SIZE = 6;
const DEFAULT_UNFOCUS_SIZE = 2.; const DEFAULT_UNFOCUS_SIZE = 2;
type Node = { type Node = {
id: string, id: string;
content: string, // Portion of content that was used for embedding content: string; // Portion of content that was used for embedding
fullContent: string | undefined, // Portion of content plus/minus buffer fullContent: string | undefined; // Portion of content plus/minus buffer
emoji: string, emoji: string;
docType: string, docType: string;
source_file: string, source_file: string;
distance: number | undefined, distance: number | undefined;
path: string, path: string;
chunkBegin: number, chunkBegin: number;
lineBegin: number, lineBegin: number;
chunkEnd: number, chunkEnd: number;
lineEnd: number, lineEnd: number;
sx: SxProps, sx: SxProps;
}; };
const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => { const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
@ -199,7 +197,8 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
const [plotDimensions, setPlotDimensions] = useState({ width: 0, height: 0 }); const [plotDimensions, setPlotDimensions] = useState({ width: 0, height: 0 });
const navigate = useNavigate(); const navigate = useNavigate();
const candidate: Types.Candidate | null = user?.userType === 'candidate' ? user as Types.Candidate : null; const candidate: Types.Candidate | null =
user?.userType === 'candidate' ? (user as Types.Candidate) : null;
/* Force resize of Plotly as it tends to not be the correct size if it is initially rendered /* Force resize of Plotly as it tends to not be the correct size if it is initially rendered
* off screen (eg., the VectorVisualizer is not on the tab the app loads to) */ * off screen (eg., the VectorVisualizer is not on the tab the app loads to) */
@ -215,12 +214,18 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
const plotContainerRect = plotContainer.getBoundingClientRect(); const plotContainerRect = plotContainer.getBoundingClientRect();
svgContainer.style.width = `${plotContainerRect.width}px`; svgContainer.style.width = `${plotContainerRect.width}px`;
svgContainer.style.height = `${plotContainerRect.height}px`; svgContainer.style.height = `${plotContainerRect.height}px`;
if (plotDimensions.width !== plotContainerRect.width || plotDimensions.height !== plotContainerRect.height) { if (
setPlotDimensions({ width: plotContainerRect.width, height: plotContainerRect.height }); plotDimensions.width !== plotContainerRect.width ||
plotDimensions.height !== plotContainerRect.height
) {
setPlotDimensions({
width: plotContainerRect.width,
height: plotContainerRect.height,
});
} }
} }
}); });
} };
resize(); resize();
}); });
@ -238,12 +243,12 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
setResult(result); setResult(result);
} catch (error) { } catch (error) {
console.error('Error obtaining collection information:', error); console.error('Error obtaining collection information:', error);
setSnack("Unable to obtain collection information.", "error"); setSnack('Unable to obtain collection information.', 'error');
}; }
}; };
fetchCollection(); fetchCollection();
}, [result, setSnack, view2D]) }, [result, setSnack, view2D]);
useEffect(() => { useEffect(() => {
if (!result || !result.embeddings) return; if (!result || !result.embeddings) return;
@ -251,13 +256,13 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
const full: Types.ChromaDBGetResponse = { const full: Types.ChromaDBGetResponse = {
...result, ...result,
ids: [...result.ids || []], ids: [...(result.ids || [])],
documents: [...result.documents || []], documents: [...(result.documents || [])],
embeddings: [...result.embeddings], embeddings: [...result.embeddings],
metadatas: [...result.metadatas || []], metadatas: [...(result.metadatas || [])],
}; };
let is2D = full.embeddings.every((v: number[]) => v.length === 2); const is2D = full.embeddings.every((v: number[]) => v.length === 2);
let is3D = full.embeddings.every((v: number[]) => v.length === 3); const is3D = full.embeddings.every((v: number[]) => v.length === 3);
if ((view2D && !is2D) || (!view2D && !is3D)) { if ((view2D && !is2D) || (!view2D && !is3D)) {
return; return;
} }
@ -267,7 +272,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
return; return;
} }
let query: Types.ChromaDBGetResponse = { const query: Types.ChromaDBGetResponse = {
ids: [], ids: [],
documents: [], documents: [],
embeddings: [], embeddings: [],
@ -276,9 +281,9 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
query: '', query: '',
size: 0, size: 0,
dimensions: 2, dimensions: 2,
name: '' name: '',
}; };
let filtered: Types.ChromaDBGetResponse = { const filtered: Types.ChromaDBGetResponse = {
ids: [], ids: [],
documents: [], documents: [],
embeddings: [], embeddings: [],
@ -287,7 +292,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
query: '', query: '',
size: 0, size: 0,
dimensions: 2, dimensions: 2,
name: '' name: '',
}; };
/* Loop through all items and divide into two groups: /* Loop through all items and divide into two groups:
@ -301,7 +306,9 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
full.metadatas[index].content = full.documents[index]; full.metadatas[index].content = full.documents[index];
if (foundIndex !== -1) { if (foundIndex !== -1) {
/* The query set will contain the distance to the query */ /* The query set will contain the distance to the query */
full.metadatas[index].distance = querySet.distances ? querySet.distances[foundIndex] : undefined; full.metadatas[index].distance = querySet.distances
? querySet.distances[foundIndex]
: undefined;
query.ids.push(id); query.ids.push(id);
query.documents.push(full.documents[index]); query.documents.push(full.documents[index]);
query.embeddings.push(full.embeddings[index]); query.embeddings.push(full.embeddings[index]);
@ -318,33 +325,52 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
if (view2D && querySet.umapEmbedding2D && querySet.umapEmbedding2D.length) { if (view2D && querySet.umapEmbedding2D && querySet.umapEmbedding2D.length) {
query.ids.unshift('query'); query.ids.unshift('query');
query.metadatas.unshift({ id: 'query', docType: 'query', content: querySet.query || '', distance: 0 }); query.metadatas.unshift({
id: 'query',
docType: 'query',
content: querySet.query || '',
distance: 0,
});
query.embeddings.unshift(querySet.umapEmbedding2D); query.embeddings.unshift(querySet.umapEmbedding2D);
} }
if (!view2D && querySet.umapEmbedding3D && querySet.umapEmbedding3D.length) { if (!view2D && querySet.umapEmbedding3D && querySet.umapEmbedding3D.length) {
query.ids.unshift('query'); query.ids.unshift('query');
query.metadatas.unshift({ id: 'query', docType: 'query', content: querySet.query || '', distance: 0 }); query.metadatas.unshift({
id: 'query',
docType: 'query',
content: querySet.query || '',
distance: 0,
});
query.embeddings.unshift(querySet.umapEmbedding3D); query.embeddings.unshift(querySet.umapEmbedding3D);
} }
const filtered_docTypes = filtered.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 query_docTypes = query.metadatas.map(m => m.docType || 'unknown');
const has_query = query.metadatas.length > 0; const has_query = query.metadatas.length > 0;
const filtered_sizes = filtered.metadatas.map(m => has_query ? DEFAULT_UNFOCUS_SIZE : DEFAULT_SIZE); 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_colors = filtered_docTypes.map(type => colorMap[type] || '#4d4d4d');
const filtered_x = normalizeDimension(filtered.embeddings.map((v: number[]) => v[0])); const filtered_x = normalizeDimension(filtered.embeddings.map((v: number[]) => v[0]));
const filtered_y = normalizeDimension(filtered.embeddings.map((v: number[]) => v[1])); 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 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_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_colors = query_docTypes.map(type => colorMap[type] || '#4d4d4d');
const query_x = normalizeDimension(query.embeddings.map((v: number[]) => v[0])); const query_x = normalizeDimension(query.embeddings.map((v: number[]) => v[0]));
const query_y = normalizeDimension(query.embeddings.map((v: number[]) => v[1])); 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 query_z = is3D
? normalizeDimension(query.embeddings.map((v: number[]) => v[2]))
: undefined;
const data: any = [{ const data: any = [
{
name: 'All data', name: 'All data',
x: filtered_x, x: filtered_x,
y: filtered_y, y: filtered_y,
@ -353,13 +379,14 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
size: filtered_sizes, size: filtered_sizes,
symbol: 'circle', symbol: 'circle',
color: filtered_colors, color: filtered_colors,
opacity: 1 opacity: 1,
}, },
text: filtered.ids, text: filtered.ids,
customdata: filtered.metadatas, customdata: filtered.metadatas,
type: is3D ? 'scatter3d' : 'scatter', type: is3D ? 'scatter3d' : 'scatter',
hovertemplate: '&nbsp;', hovertemplate: '&nbsp;',
}, { },
{
name: 'Query', name: 'Query',
x: query_x, x: query_x,
y: query_y, y: query_y,
@ -368,13 +395,14 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
size: query_sizes, size: query_sizes,
symbol: 'circle', symbol: 'circle',
color: query_colors, color: query_colors,
opacity: 1 opacity: 1,
}, },
text: query.ids, text: query.ids,
customdata: query.metadatas, customdata: query.metadatas,
type: is3D ? 'scatter3d' : 'scatter', type: is3D ? 'scatter3d' : 'scatter',
hovertemplate: '%{text}', hovertemplate: '%{text}',
}]; },
];
if (is3D) { if (is3D) {
data[0].z = filtered_z; data[0].z = filtered_z;
@ -382,7 +410,6 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
} }
setPlotData(data); setPlotData(data);
}, [result, querySet, view2D]); }, [result, querySet, view2D]);
const handleKeyPress = (event: any) => { const handleKeyPress = (event: any) => {
@ -400,20 +427,39 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
console.log(result); console.log(result);
setQuerySet(result); setQuerySet(result);
} catch (error) { } catch (error) {
const msg = `Error obtaining similar content to ${query}.` const msg = `Error obtaining similar content to ${query}.`;
setSnack(msg, "error"); setSnack(msg, 'error');
}; }
}; };
if (!result) return ( if (!result)
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}> return (
<Box
sx={{
display: 'flex',
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
}}
>
<div>Loading visualization...</div> <div>Loading visualization...</div>
</Box> </Box>
); );
if (!candidate) return ( if (!candidate)
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}> return (
<div>No candidate selected. Please <Button onClick={() => navigate('/find-a-candidate')}>select a candidate</Button> first.</div> <Box
sx={{
display: 'flex',
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
}}
>
<div>
No candidate selected. Please{' '}
<Button onClick={() => navigate('/find-a-candidate')}>select a candidate</Button> first.
</div>
</Box> </Box>
); );
@ -422,14 +468,14 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
const result = await apiClient.getCandidateRAGContent(node.id); const result = await apiClient.getCandidateRAGContent(node.id);
const update: Node = { const update: Node = {
...node, ...node,
fullContent: result.content fullContent: result.content,
} };
setNode(update); setNode(update);
} catch (error) { } catch (error) {
const msg = `Error obtaining content for ${node.id}.` const msg = `Error obtaining content for ${node.id}.`;
console.error(msg, error); console.error(msg, error);
setSnack(msg, "error"); setSnack(msg, 'error');
}; }
}; };
const onNodeSelected = (metadata: any) => { const onNodeSelected = (metadata: any) => {
@ -440,21 +486,23 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
...metadata, ...metadata,
content: `Similarity results for the query **${querySet.query || ''}** content: `Similarity results for the query **${querySet.query || ''}**
The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '2' : '3'}-dimensional space. Larger dots represent relative similarity in N-dimensional space. The scatter graph shows the query in N-dimensional space, mapped to ${
view2D ? '2' : '3'
}-dimensional space. Larger dots represent relative similarity in N-dimensional space.
`, `,
emoji: emojiMap[metadata.docType], emoji: emojiMap[metadata.docType],
sx: { sx: {
m: 0.5, m: 0.5,
p: 2, p: 2,
width: '3rem', width: '3rem',
display: "flex", display: 'flex',
alignContent: "center", alignContent: 'center',
justifyContent: "center", justifyContent: 'center',
flexGrow: 0, flexGrow: 0,
flexWrap: "wrap", flexWrap: 'wrap',
backgroundColor: colorMap[metadata.docType] || '#ff8080', backgroundColor: colorMap[metadata.docType] || '#ff8080',
} },
} };
setNode(node); setNode(node);
return; return;
} }
@ -463,7 +511,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
content: `Loading...`, content: `Loading...`,
...metadata, ...metadata,
emoji: emojiMap[metadata.docType] || '❓', emoji: emojiMap[metadata.docType] || '❓',
} };
setNode(node); setNode(node);
@ -471,95 +519,173 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
}; };
return ( return (
<Box className="VectorVisualizer" <Box
className="VectorVisualizer"
ref={boxRef} ref={boxRef}
sx={{ sx={{
...sx ...sx,
}}> }}
>
<Box sx={{ p: 0, m: 0, gap: 0 }}> <Box sx={{ p: 0, m: 0, gap: 0 }}>
<Paper sx={{ <Paper
p: 0.5, m: 0, sx={{
display: "flex", p: 0.5,
m: 0,
display: 'flex',
flexGrow: 0, flexGrow: 0,
height: isMobile ? "auto" : "auto", //"320px", height: isMobile ? 'auto' : 'auto', //"320px",
minHeight: isMobile ? "auto" : "auto", //"320px", minHeight: isMobile ? 'auto' : 'auto', //"320px",
maxHeight: isMobile ? "auto" : "auto", //"320px", maxHeight: isMobile ? 'auto' : 'auto', //"320px",
position: "relative", position: 'relative',
flexDirection: "column" flexDirection: 'column',
}}> }}
>
<FormControlLabel <FormControlLabel
sx={{ sx={{
display: "flex", display: 'flex',
position: "relative", position: 'relative',
width: "fit-content", width: 'fit-content',
ml: 1, ml: 1,
mb: '-2.5rem', mb: '-2.5rem',
zIndex: 100, zIndex: 100,
flexBasis: 0, flexBasis: 0,
flexGrow: 0 flexGrow: 0,
}} }}
control={<Switch checked={!view2D} />} onChange={() => { setView2D(!view2D); setResult(null); }} label="3D" /> control={<Switch checked={!view2D} />}
onChange={() => {
setView2D(!view2D);
setResult(null);
}}
label="3D"
/>
<Plot <Plot
ref={plotlyRef} ref={plotlyRef}
onClick={(event: any) => { onNodeSelected(event.points[0].customdata); }} onClick={(event: any) => {
onNodeSelected(event.points[0].customdata);
}}
data={plotData} data={plotData}
useResizeHandler={true} useResizeHandler={true}
config={config} config={config}
style={{ style={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
minHeight: '240px', minHeight: '240px',
padding: 0, padding: 0,
margin: 0, margin: 0,
width: "100%", width: '100%',
height: "100%", height: '100%',
overflow: "hidden", overflow: 'hidden',
}}
layout={{
...layout,
width: plotDimensions.width,
height: plotDimensions.height,
}} }}
layout={{...layout, width: plotDimensions.width, height: plotDimensions.height }}
/> />
</Paper> </Paper>
<Paper sx={{ display: "flex", flexDirection: isMobile ? "column" : "row", mt: 0.5, p: 0.5, flexGrow: 1, minHeight: "fit-content" }}> <Paper
{node !== null && sx={{
<Box sx={{ display: "flex", fontSize: "0.75rem", flexDirection: "column", flexGrow: 1, maxWidth: "100%", flexBasis: 1, maxHeight: "min-content" }}> display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
mt: 0.5,
p: 0.5,
flexGrow: 1,
minHeight: 'fit-content',
}}
>
{node !== null && (
<Box
sx={{
display: 'flex',
fontSize: '0.75rem',
flexDirection: 'column',
flexGrow: 1,
maxWidth: '100%',
flexBasis: 1,
maxHeight: 'min-content',
}}
>
<TableContainer component={Paper} sx={{ mb: isMobile ? 1 : 0, mr: isMobile ? 0 : 1 }}> <TableContainer component={Paper} sx={{ mb: isMobile ? 1 : 0, mr: isMobile ? 0 : 1 }}>
<Table size="small" sx={{ tableLayout: 'fixed' }}> <Table size="small" sx={{ tableLayout: 'fixed' }}>
<TableBody sx={{ '& td': { verticalAlign: "top", fontSize: "0.75rem", }, '& td:first-of-type': { whiteSpace: "nowrap", width: "1rem" } }}> <TableBody
sx={{
'& td': { verticalAlign: 'top', fontSize: '0.75rem' },
'& td:first-of-type': {
whiteSpace: 'nowrap',
width: '1rem',
},
}}
>
<TableRow> <TableRow>
<TableCell>Type</TableCell> <TableCell>Type</TableCell>
<TableCell>{node.emoji} {node.docType}</TableCell> <TableCell>
{node.emoji} {node.docType}
</TableCell>
</TableRow> </TableRow>
{node.source_file !== undefined && <TableRow> {node.source_file !== undefined && (
<TableRow>
<TableCell>File</TableCell> <TableCell>File</TableCell>
<TableCell>{node.source_file.replace(/^.*\//, '')}</TableCell> <TableCell>{node.source_file.replace(/^.*\//, '')}</TableCell>
</TableRow>} </TableRow>
{node.path !== undefined && <TableRow> )}
{node.path !== undefined && (
<TableRow>
<TableCell>Section</TableCell> <TableCell>Section</TableCell>
<TableCell>{node.path}</TableCell> <TableCell>{node.path}</TableCell>
</TableRow>} </TableRow>
{node.distance !== undefined && <TableRow> )}
{node.distance !== undefined && (
<TableRow>
<TableCell>Distance</TableCell> <TableCell>Distance</TableCell>
<TableCell>{node.distance}</TableCell> <TableCell>{node.distance}</TableCell>
</TableRow>} </TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </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", mt: 1 }}> <Paper
<Box sx={{ display: "flex", background: "#404040", p: 1, color: "white" }}>Vector Embedded Content</Box> elevation={6}
<Box sx={{ display: "flex", p: 1, flexGrow: 1 }}>{node.content}</Box> sx={{
</Paper> display: 'flex',
} flexDirection: 'column',
border: '1px solid #808080',
minHeight: 'fit-content',
mt: 1,
}}
>
<Box
sx={{
display: 'flex',
background: '#404040',
p: 1,
color: 'white',
}}
>
Vector Embedded Content
</Box> </Box>
} <Box sx={{ display: 'flex', p: 1, flexGrow: 1 }}>{node.content}</Box>
</Paper>
)}
</Box>
)}
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 2, flexBasis: 0, flexShrink: 1 }}> <Box
{node === null && sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 2,
flexBasis: 0,
flexShrink: 1,
}}
>
{node === null && (
<Paper sx={{ m: 0.5, p: 2, flexGrow: 1 }}> <Paper sx={{ m: 0.5, p: 2, flexGrow: 1 }}>
Click a point in the scatter-graph to see information about that node. Click a point in the scatter-graph to see information about that node.
</Paper> </Paper>
} )}
{node !== null && node.fullContent && {node !== null && node.fullContent && (
<Scrollable <Scrollable
autoscroll={false} autoscroll={false}
sx={{ sx={{
@ -569,51 +695,104 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
p: 0.5, p: 0.5,
pl: 1, pl: 1,
flexShrink: 1, flexShrink: 1,
position: "relative", position: 'relative',
maxWidth: "100%", maxWidth: '100%',
}} }}
> >
{ {node.fullContent.split('\n').map((line, index) => {
node.fullContent.split('\n').map((line, index) => {
index += 1 + node.chunkBegin; index += 1 + node.chunkBegin;
const bgColor = (index > node.lineBegin && index <= node.lineEnd) ? '#f0f0f0' : 'auto'; const bgColor =
return <Box key={index} sx={{ display: "flex", flexDirection: "row", borderBottom: '1px solid #d0d0d0', ':first-of-type': { borderTop: '1px solid #d0d0d0' }, backgroundColor: bgColor }}> index > node.lineBegin && index <= node.lineEnd ? '#f0f0f0' : 'auto';
<Box sx={{ fontFamily: 'courier', fontSize: "0.8rem", minWidth: "2rem", pt: "0.1rem", align: "left", verticalAlign: "top" }}>{index}</Box> return (
<pre style={{ margin: 0, padding: 0, border: "none", minHeight: "1rem", overflow: "hidden" }} >{line || " "}</pre> <Box
</Box>; key={index}
}) sx={{
} display: 'flex',
{!node.lineBegin && <pre style={{ margin: 0, padding: 0, border: "none" }}>{node.content}</pre>} 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',
}}
>
{index}
</Box>
<pre
style={{
margin: 0,
padding: 0,
border: 'none',
minHeight: '1rem',
overflow: 'hidden',
}}
>
{line || ' '}
</pre>
</Box>
);
})}
{!node.lineBegin && (
<pre style={{ margin: 0, padding: 0, border: 'none' }}>{node.content}</pre>
)}
</Scrollable> </Scrollable>
} )}
</Box> </Box>
</Paper> </Paper>
{!inline && querySet.query !== undefined && querySet.query !== '' && {!inline && querySet.query !== undefined && querySet.query !== '' && (
<Paper sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', flexGrow: 0, minHeight: '2.5rem', maxHeight: '2.5rem', height: '2.5rem', alignItems: 'center', mt: 1, pb: 0 }}> <Paper
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
flexGrow: 0,
minHeight: '2.5rem',
maxHeight: '2.5rem',
height: '2.5rem',
alignItems: 'center',
mt: 1,
pb: 0,
}}
>
{querySet.query !== undefined && querySet.query !== '' && `Query: ${querySet.query}`} {querySet.query !== undefined && querySet.query !== '' && `Query: ${querySet.query}`}
{querySet.ids.length === 0 && "Enter query below to perform a similarity search."} {querySet.ids.length === 0 && 'Enter query below to perform a similarity search.'}
</Paper> </Paper>
} )}
{ {!inline && (
!inline && <Box className="Query" sx={{ display: 'flex', flexDirection: 'row', p: 1 }}>
<Box className="Query" sx={{ display: "flex", flexDirection: "row", p: 1 }}>
<TextField <TextField
variant="outlined" variant="outlined"
fullWidth fullWidth
type="text" type="text"
value={newQuery} value={newQuery}
onChange={(e) => setNewQuery(e.target.value)} onChange={e => setNewQuery(e.target.value)}
onKeyDown={handleKeyPress} onKeyDown={handleKeyPress}
placeholder="Enter query to find related documents..." placeholder="Enter query to find related documents..."
id="QueryInput" id="QueryInput"
/> />
<Tooltip title="Send"> <Tooltip title="Send">
<Button sx={{ m: 1 }} variant="contained" onClick={() => { sendQuery(newQuery); }}><SendIcon /></Button> <Button
sx={{ m: 1 }}
variant="contained"
onClick={() => {
sendQuery(newQuery);
}}
>
<SendIcon />
</Button>
</Tooltip> </Tooltip>
</Box> </Box>
} )}
</Box> </Box>
</Box> </Box>
); );
@ -621,6 +800,4 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
export type { VectorVisualizerProps }; export type { VectorVisualizerProps };
export { export { VectorVisualizer };
VectorVisualizer,
};

View File

@ -1,8 +1,8 @@
// components/layout/BackstoryLayout.tsx // components/layout/BackstoryLayout.tsx
import React, { ReactElement, useEffect, useState } from 'react'; import React, { ReactElement, useEffect, useState } from 'react';
import { Outlet, useLocation, Routes, Route } from "react-router-dom"; import { Outlet, useLocation, Routes, Route } from 'react-router-dom';
import { Box, Container, Paper } from '@mui/material'; import { Box, Container, Paper } from '@mui/material';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import { darken } from '@mui/material/styles'; import { darken } from '@mui/material/styles';
import { Header } from 'components/layout/Header'; import { Header } from 'components/layout/Header';
@ -10,13 +10,10 @@ import { Scrollable } from 'components/Scrollable';
import { Footer } from 'components/layout/Footer'; import { Footer } from 'components/layout/Footer';
import { Snack, SetSnackType } from 'components/Snack'; import { Snack, SetSnackType } from 'components/Snack';
import { User } from 'types/types'; import { User } from 'types/types';
import { LoadingComponent } from "components/LoadingComponent"; import { LoadingComponent } from 'components/LoadingComponent';
import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext'; import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext';
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext'; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import { import { getMainNavigationItems, getAllRoutes } from 'config/navigationConfig';
getMainNavigationItems,
getAllRoutes,
} from 'config/navigationConfig';
import { NavigationItem } from 'types/navigation'; import { NavigationItem } from 'types/navigation';
// Legacy type for backward compatibility // Legacy type for backward compatibility
@ -37,35 +34,39 @@ const BackstoryPageContainer = (props: BackstoryPageContainerProps) => {
<Container <Container
className="BackstoryPageContainer" className="BackstoryPageContainer"
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
flexGrow: 1, flexGrow: 1,
p: "0 !important", p: '0 !important',
m: "0 auto !important", m: '0 auto !important',
maxWidth: '1024px', maxWidth: '1024px',
height: "100%", height: '100%',
minHeight: 0, minHeight: 0,
...sx ...sx,
}}> }}
<Box sx={{ >
display: "flex", <Box
sx={{
display: 'flex',
p: { xs: 0, sm: 0.5 }, p: { xs: 0, sm: 0.5 },
flexGrow: 1, flexGrow: 1,
minHeight: "min-content", minHeight: 'min-content',
}}> }}
>
<Paper <Paper
elevation={2} elevation={2}
sx={{ sx={{
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
m: 0, m: 0,
p: 0.5, p: 0.5,
minHeight: "min-content", minHeight: 'min-content',
backgroundColor: 'background.paper', backgroundColor: 'background.paper',
borderRadius: 0.5, borderRadius: 0.5,
maxWidth: '100%', maxWidth: '100%',
flexDirection: "column", flexDirection: 'column',
}}> }}
>
{children} {children}
</Paper> </Paper>
</Box> </Box>
@ -79,7 +80,7 @@ interface BackstoryLayoutProps {
} }
const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => { const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => {
const { page, chatRef, } = props; const { page, chatRef } = props;
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -99,7 +100,8 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
const isAdmin = user?.isAdmin ? true : false; const isAdmin = user?.isAdmin ? true : false;
const routes = getAllRoutes(userType, isAdmin); const routes = getAllRoutes(userType, isAdmin);
return routes.map((route, index) => { return routes
.map((route, index) => {
if (!route.path || !route.component) return null; if (!route.path || !route.component) return null;
// Clone the component and pass necessary props if it's a page component // Clone the component and pass necessary props if it's a page component
@ -109,51 +111,48 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
}); });
return ( return (
<Route <Route key={`${route.id}-${index}`} path={route.path} element={componentWithProps} />
key={`${route.id}-${index}`}
path={route.path}
element={componentWithProps}
/>
); );
}).filter(Boolean); })
.filter(Boolean);
}; };
return ( return (
<Box sx={{ <Box
height: "100%", sx={{
maxHeight: "100%", height: '100%',
minHeight: "100%", maxHeight: '100%',
flexDirection: "column" minHeight: '100%',
}}> flexDirection: 'column',
<Header }}
currentPath={page} >
navigate={navigate} <Header currentPath={page} navigate={navigate} navigationItems={navigationItems} />
navigationItems={navigationItems} <Box
/> sx={{
<Box sx={{ display: 'flex',
display: "flex", width: '100%',
width: "100%", maxHeight: '100%',
maxHeight: "100%", minHeight: '100%',
minHeight: "100%",
flex: 1, flex: 1,
m: 0, m: 0,
p: 0, p: 0,
flexDirection: "column", flexDirection: 'column',
backgroundColor: "#D3CDBF", /* Warm Gray */ backgroundColor: '#D3CDBF' /* Warm Gray */,
}}> }}
>
<Scrollable <Scrollable
className="BackstoryPageScrollable" className="BackstoryPageScrollable"
sx={{ sx={{
m: 0, m: 0,
p: 0, p: 0,
mt: "72px", /* Needs to be kept in sync with the height of Header */ mt: '72px' /* Needs to be kept in sync with the height of Header */,
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
backgroundColor: (theme) => darken(theme.palette.background.default, 0.4), backgroundColor: theme => darken(theme.palette.background.default, 0.4),
height: "100%", height: '100%',
maxHeight: "100%", maxHeight: '100%',
minHeight: "100%", minHeight: '100%',
minWidth: "min-content", minWidth: 'min-content',
}} }}
> >
<BackstoryPageContainer> <BackstoryPageContainer>
@ -170,12 +169,10 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
{(guest || user) && ( {(guest || user) && (
<> <>
<Outlet /> <Outlet />
<Routes> <Routes>{generateRoutes()}</Routes>
{generateRoutes()}
</Routes>
</> </>
)} )}
{location.pathname === "/" && <Footer />} {location.pathname === '/' && <Footer />}
</BackstoryPageContainer> </BackstoryPageContainer>
</Scrollable> </Scrollable>
</Box> </Box>
@ -183,6 +180,4 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
); );
}; };
export { export { BackstoryLayout };
BackstoryLayout
};

View File

@ -1,13 +1,13 @@
// components/layout/BackstoryRoutes.tsx // components/layout/BackstoryRoutes.tsx
import React, { Ref, ReactNode } from "react"; import React, { Ref, ReactNode } from 'react';
import { Route } from "react-router-dom"; import { Route } from 'react-router-dom';
import { BackstoryPageProps } from '../BackstoryTab'; import { BackstoryPageProps } from '../BackstoryTab';
import { ConversationHandle } from '../Conversation'; import { ConversationHandle } from '../Conversation';
import { User } from 'types/types'; import { User } from 'types/types';
import { getAllRoutes } from 'config/navigationConfig'; import { getAllRoutes } from 'config/navigationConfig';
import { NavigationItem } from 'types/navigation'; import { NavigationItem } from 'types/navigation';
import { useAppState } from "hooks/GlobalContext"; import { useAppState } from 'hooks/GlobalContext';
interface BackstoryDynamicRoutesProps extends BackstoryPageProps { interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
chatRef: Ref<ConversationHandle>; chatRef: Ref<ConversationHandle>;
@ -22,7 +22,8 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNod
// Get all routes from navigation config // Get all routes from navigation config
const routes = getAllRoutes(userType, isAdmin); const routes = getAllRoutes(userType, isAdmin);
return routes.map((route: NavigationItem, index: number) => { return routes
.map((route: NavigationItem, index: number) => {
if (!route.path || !route.component) return null; if (!route.path || !route.component) return null;
// Clone the component and pass necessary props // Clone the component and pass necessary props
@ -33,14 +34,9 @@ const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNod
...(route.component.props || {}), ...(route.component.props || {}),
}); });
return ( return <Route key={`${route.id}-${index}`} path={route.path} element={componentWithProps} />;
<Route })
key={`${route.id}-${index}`} .filter(Boolean);
path={route.path}
element={componentWithProps}
/>
);
}).filter(Boolean);
}; };
export { getBackstoryDynamicRoutes }; export { getBackstoryDynamicRoutes };

View File

@ -62,7 +62,7 @@ const Footer = () => {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
return ( return (
<FooterContainer elevation={0} > <FooterContainer elevation={0}>
<Container maxWidth="lg"> <Container maxWidth="lg">
<Grid container spacing={4} justifyContent="space-between"> <Grid container spacing={4} justifyContent="space-between">
{/* About Company */} {/* About Company */}
@ -79,8 +79,9 @@ const Footer = () => {
> >
BACKSTORY BACKSTORY
</Typography> </Typography>
<Typography variant="body2" sx={{ mb: 2, color: "white" }}> <Typography variant="body2" sx={{ mb: 2, color: 'white' }}>
Helping candidates share their professional journey and connect with the right employers through compelling backstories. Helping candidates share their professional journey and connect with the right
employers through compelling backstories.
</Typography> </Typography>
<Stack direction="row"> <Stack direction="row">
{/* <IconButton {/* <IconButton
@ -122,9 +123,11 @@ const Footer = () => {
'&:hover': { '&:hover': {
backgroundColor: 'rgba(211, 205, 191, 0.1)', backgroundColor: 'rgba(211, 205, 191, 0.1)',
color: theme.palette.action.active, color: theme.palette.action.active,
} },
}} }}
onClick={() => window.open('https://www.linkedin.com/in/james-ketrenos/', '_blank')} onClick={() =>
window.open('https://www.linkedin.com/in/james-ketrenos/', '_blank')
}
> >
<LinkedIn /> <LinkedIn />
</IconButton> </IconButton>
@ -163,38 +166,37 @@ const Footer = () => {
</Grid> </Grid>
{/* Quick Links */} {/* Quick Links */}
{false && <> {false && (
<>
<Grid size={{ xs: 12, sm: 6, md: 2 }}> <Grid size={{ xs: 12, sm: 6, md: 2 }}>
<FooterHeading variant="subtitle1"> <FooterHeading variant="subtitle1">For Candidates</FooterHeading>
For Candidates
</FooterHeading>
<FooterLink href="/create-profile">Create Profile</FooterLink> <FooterLink href="/create-profile">Create Profile</FooterLink>
<FooterLink href="/backstory-editor">Backstory Editor</FooterLink> <FooterLink href="/backstory-editor">Backstory Editor</FooterLink>
<FooterLink href="/resume-builder">Resume Builder</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> <FooterLink href="/interview-tips">Interview Tips</FooterLink>
</Grid> </Grid>
</>} </>
)}
{/* Quick Links */} {/* Quick Links */}
{false && <> {false && (
<>
<Grid size={{ xs: 12, sm: 6, md: 2 }}> <Grid size={{ xs: 12, sm: 6, md: 2 }}>
<FooterHeading variant="subtitle1"> <FooterHeading variant="subtitle1">For Employers</FooterHeading>
For Employers
</FooterHeading>
<FooterLink href="/post-job">Post a Job</FooterLink> <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="/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> <FooterLink href="/pricing-plans">Pricing Plans</FooterLink>
</Grid> </Grid>
</>} </>
)}
{/* Contact */} {/* Contact */}
{false && <> {false && (
<>
<Grid size={{ xs: 12, sm: 6, md: 2 }}> <Grid size={{ xs: 12, sm: 6, md: 2 }}>
<FooterHeading variant="subtitle1"> <FooterHeading variant="subtitle1">Company</FooterHeading>
Company
</FooterHeading>
<FooterLink href="/about-us">About Us</FooterLink> <FooterLink href="/about-us">About Us</FooterLink>
<FooterLink href="/our-team">Our Team</FooterLink> <FooterLink href="/our-team">Our Team</FooterLink>
<FooterLink href="/blog">Blog</FooterLink> <FooterLink href="/blog">Blog</FooterLink>
@ -202,7 +204,8 @@ const Footer = () => {
<FooterLink href="/careers">Careers</FooterLink> <FooterLink href="/careers">Careers</FooterLink>
<FooterLink href="/contact-us">Contact Us</FooterLink> <FooterLink href="/contact-us">Contact Us</FooterLink>
</Grid> </Grid>
</>} </>
)}
{/* Newsletter */} {/* Newsletter */}
<Grid size={{ xs: 12, sm: 6, md: 3 }}> <Grid size={{ xs: 12, sm: 6, md: 3 }}>
<ContactItem> <ContactItem>
@ -215,7 +218,7 @@ const Footer = () => {
</ContactItem> */} </ContactItem> */}
<ContactItem> <ContactItem>
<LocationOn sx={{ mr: 1, fontSize: 20 }} /> <LocationOn sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="body2" sx={{ color: "white" }}> <Typography variant="body2" sx={{ color: 'white' }}>
Beaverton, OR 97003 Beaverton, OR 97003
</Typography> </Typography>
</ContactItem> </ContactItem>
@ -228,25 +231,35 @@ const Footer = () => {
<Grid container spacing={2} alignItems="center"> <Grid container spacing={2} alignItems="center">
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Copyright sx={{ fontSize: 16, mr: 1, color: "white" }} /> <Copyright sx={{ fontSize: 16, mr: 1, color: 'white' }} />
<Typography variant="body2" sx={{ color: "white" }}> <Typography variant="body2" sx={{ color: 'white' }}>
{currentYear} James P. Ketrenos. All rights reserved. {currentYear} James P. Ketrenos. All rights reserved.
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
{false && <> {false && (
<>
<Stack <Stack
direction={isMobile ? 'column' : 'row'} direction={isMobile ? 'column' : 'row'}
spacing={isMobile ? 1 : 3} spacing={isMobile ? 1 : 3}
sx={{ textAlign: { xs: 'left', md: 'right' } }} sx={{ textAlign: { xs: 'left', md: 'right' } }}
> >
<FooterLink href="/terms" sx={{ mb: 0 }}>Terms of Service</FooterLink> <FooterLink href="/terms" sx={{ mb: 0 }}>
<FooterLink href="/privacy" sx={{ mb: 0 }}>Privacy Policy</FooterLink> Terms of Service
<FooterLink href="/accessibility" sx={{ mb: 0 }}>Accessibility</FooterLink> </FooterLink>
<FooterLink href="/sitemap" sx={{ mb: 0 }}>Sitemap</FooterLink> <FooterLink href="/privacy" sx={{ mb: 0 }}>
Privacy Policy
</FooterLink>
<FooterLink href="/accessibility" sx={{ mb: 0 }}>
Accessibility
</FooterLink>
<FooterLink href="/sitemap" sx={{ mb: 0 }}>
Sitemap
</FooterLink>
</Stack> </Stack>
</>} </>
)}
</Grid> </Grid>
</Grid> </Grid>
</Container> </Container>
@ -254,6 +267,4 @@ const Footer = () => {
); );
}; };
export { export { Footer };
Footer
};

View File

@ -53,7 +53,7 @@ import { useAppState } from 'hooks/GlobalContext';
// Styled components // Styled components
const StyledAppBar = styled(AppBar, { const StyledAppBar = styled(AppBar, {
shouldForwardProp: (prop) => prop !== 'transparent', shouldForwardProp: prop => prop !== 'transparent',
})<{ transparent?: boolean }>(({ theme, transparent }) => ({ })<{ transparent?: boolean }>(({ theme, transparent }) => ({
backgroundColor: transparent ? 'transparent' : theme.palette.primary.main, backgroundColor: transparent ? 'transparent' : theme.palette.primary.main,
boxShadow: transparent ? 'none' : '', boxShadow: transparent ? 'none' : '',
@ -126,24 +126,22 @@ interface HeaderProps {
const Header: React.FC<HeaderProps> = (props: HeaderProps) => { const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const { const { transparent = false, className, navigate, navigationItems, sessionId } = props;
transparent = false,
className,
navigate,
navigationItems,
sessionId,
} = props;
const theme = useTheme(); const theme = useTheme();
const location = useLocation(); const location = useLocation();
const name = (user?.firstName || user?.email || ''); const name = user?.firstName || user?.email || '';
// State for desktop dropdown menus // State for desktop dropdown menus
const [dropdownAnchors, setDropdownAnchors] = useState<{ [key: string]: HTMLElement | null }>({}); const [dropdownAnchors, setDropdownAnchors] = useState<{
[key: string]: HTMLElement | null;
}>({});
// State for mobile drawer // State for mobile drawer
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const [mobileExpanded, setMobileExpanded] = useState<{ [key: string]: boolean }>({}); const [mobileExpanded, setMobileExpanded] = useState<{
[key: string]: boolean;
}>({});
// State for user menu // State for user menu
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null); const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
@ -172,7 +170,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
label: item.label as string, label: item.label as string,
icon: item.icon || null, icon: item.icon || null,
action: () => item.path && navigate(item.path.replace(/:.*$/, '')), action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
group: 'profile' group: 'profile',
}); });
} }
}); });
@ -183,8 +181,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
id: 'divider', id: 'divider',
label: '', label: '',
icon: null, icon: null,
action: () => { }, action: () => {},
group: 'divider' group: 'divider',
}); });
} }
@ -196,7 +194,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
label: item.label as string, label: item.label as string,
icon: item.icon || null, icon: item.icon || null,
action: () => item.path && navigate(item.path.replace(/:.*$/, '')), action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
group: 'account' group: 'account',
}); });
} }
}); });
@ -207,8 +205,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
id: 'divider', id: 'divider',
label: '', label: '',
icon: null, icon: null,
action: () => { }, action: () => {},
group: 'divider' group: 'divider',
}); });
} }
@ -220,7 +218,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
label: item.label as string, label: item.label as string,
icon: item.icon || null, icon: item.icon || null,
action: () => item.path && navigate(item.path.replace(/:.*$/, '')), action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
group: 'admin' group: 'admin',
}); });
} }
}); });
@ -231,8 +229,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
id: 'divider', id: 'divider',
label: '', label: '',
icon: null, icon: null,
action: () => { }, action: () => {},
group: 'divider' group: 'divider',
}); });
} }
@ -247,7 +245,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
logout(); logout();
navigate('/'); navigate('/');
}, },
group: 'system' group: 'system',
}); });
} else if (!item.divider) { } else if (!item.divider) {
items.push({ items.push({
@ -255,7 +253,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
label: item.label as string, label: item.label as string,
icon: item.icon || null, icon: item.icon || null,
action: () => item.path && navigate(item.path.replace(/:.*$/, '')), action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
group: 'system' group: 'system',
}); });
} }
}); });
@ -268,7 +266,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
label: item.label as string, label: item.label as string,
icon: item.icon || null, icon: item.icon || null,
action: () => item.path && navigate(item.path.replace(/:.*$/, '')), action: () => item.path && navigate(item.path.replace(/:.*$/, '')),
group: 'other' group: 'other',
}); });
} }
}); });
@ -319,7 +317,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
setUserMenuAnchor(null); setUserMenuAnchor(null);
}; };
const handleUserMenuAction = (item: { id: string; label: string; icon: React.ReactElement | null; action: () => void; group?: string }) => { const handleUserMenuAction = (item: {
id: string;
label: string;
icon: React.ReactElement | null;
action: () => void;
group?: string;
}) => {
if (item.group !== 'divider') { if (item.group !== 'divider') {
item.action(); item.action();
handleUserMenuClose(); handleUserMenuClose();
@ -337,18 +341,28 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
// Render desktop navigation with dropdowns // Render desktop navigation with dropdowns
const renderDesktopNavigation = () => { const renderDesktopNavigation = () => {
return ( return (
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%', justifyContent: 'space-between' }}> <Box
sx={{
display: 'flex',
alignItems: 'center',
width: '100%',
justifyContent: 'space-between',
}}
>
{navigationItems.map((item, index) => { {navigationItems.map((item, index) => {
const hasChildren = item.children && item.children.length > 0; const hasChildren = item.children && item.children.length > 0;
const isActive = isCurrentPath(item) || hasActiveChild(item); const isActive = isCurrentPath(item) || hasActiveChild(item);
if (hasChildren) { if (hasChildren) {
return ( return (
<Box key={item.id} sx={{ <Box
mr: (index === 0 || index === navigationItems.length - 1) ? "auto" : "unset", key={item.id}
}}> sx={{
mr: index === 0 || index === navigationItems.length - 1 ? 'auto' : 'unset',
}}
>
<DropdownButton <DropdownButton
onClick={(e) => handleDropdownOpen(e, item.id)} onClick={e => handleDropdownOpen(e, item.id)}
endIcon={<KeyboardArrowDown />} endIcon={<KeyboardArrowDown />}
sx={{ sx={{
backgroundColor: isActive ? 'action.selected' : 'transparent', backgroundColor: isActive ? 'action.selected' : 'transparent',
@ -372,7 +386,12 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
onClick={() => child.path && handleNavigate(child.path)} onClick={() => child.path && handleNavigate(child.path)}
selected={isCurrentPath(child)} selected={isCurrentPath(child)}
disabled={!child.path} disabled={!child.path}
sx={{ display: 'flex', alignItems: 'center', "& *": { m: 0, p: 0 }, m: 0 }} sx={{
display: 'flex',
alignItems: 'center',
'& *': { m: 0, p: 0 },
m: 0,
}}
> >
{child.icon && <ListItemIcon>{child.icon}</ListItemIcon>} {child.icon && <ListItemIcon>{child.icon}</ListItemIcon>}
<ListItemText>{child.label}</ListItemText> <ListItemText>{child.label}</ListItemText>
@ -389,7 +408,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
sx={{ sx={{
backgroundColor: isActive ? 'action.selected' : 'transparent', backgroundColor: isActive ? 'action.selected' : 'transparent',
color: isActive ? 'secondary.main' : 'primary.contrastText', color: isActive ? 'secondary.main' : 'primary.contrastText',
mr: (index === 0 || index === navigationItems.length - 1) ? "auto" : "unset", 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>}
@ -404,7 +423,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
// Render mobile accordion navigation // Render mobile accordion navigation
const renderMobileNavigation = () => { const renderMobileNavigation = () => {
const renderNavigationItem = (item: NavigationItem, depth: number = 0) => { const renderNavigationItem = (item: NavigationItem, depth = 0) => {
const hasChildren = item.children && item.children.length > 0; const hasChildren = item.children && item.children.length > 0;
const isActive = isCurrentPath(item) || hasActiveChild(item); const isActive = isCurrentPath(item) || hasActiveChild(item);
const isExpanded = mobileExpanded[item.id]; const isExpanded = mobileExpanded[item.id];
@ -432,24 +451,18 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
}, },
}} }}
> >
{item.icon && ( {item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
<ListItemIcon sx={{ minWidth: 36 }}>
{item.icon}
</ListItemIcon>
)}
<ListItemText <ListItemText
primary={item.label} primary={item.label}
sx={{ sx={{
'& .MuiTypography-root': { '& .MuiTypography-root': {
fontSize: depth > 0 ? '0.875rem' : '1rem', fontSize: depth > 0 ? '0.875rem' : '1rem',
fontWeight: depth === 0 ? 500 : 400, fontWeight: depth === 0 ? 500 : 400,
} },
}} }}
/> />
{hasChildren && ( {hasChildren && (
<IconButton size="small"> <IconButton size="small">{isExpanded ? <ExpandLess /> : <ExpandMore />}</IconButton>
{isExpanded ? <ExpandLess /> : <ExpandMore />}
</IconButton>
)} )}
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
@ -466,7 +479,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
return ( return (
<List sx={{ pt: 0 }}> <List sx={{ pt: 0 }}>
{navigationItems.map((item) => renderNavigationItem(item))} {navigationItems.map(item => renderNavigationItem(item))}
<Divider sx={{ my: 1 }} /> <Divider sx={{ my: 1 }} />
{!user && ( {!user && (
<ListItem disablePadding> <ListItem disablePadding>
@ -484,7 +497,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
return ( return (
<UserMenuContainer> <UserMenuContainer>
<List dense> <List dense>
{userMenuItems.map((item, index) => ( {userMenuItems.map((item, index) =>
item.group === 'divider' ? ( item.group === 'divider' ? (
<Divider key={`divider-${index}`} /> <Divider key={`divider-${index}`} />
) : ( ) : (
@ -495,7 +508,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
) )
))} )}
</List> </List>
</UserMenuContainer> </UserMenuContainer>
); );
@ -508,7 +521,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<Button <Button
color="info" color="info"
variant="contained" variant="contained"
onClick={() => navigate("/login")} onClick={() => navigate('/login')}
sx={{ sx={{
display: { xs: 'none', sm: 'block' }, display: { xs: 'none', sm: 'block' },
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
@ -527,16 +540,16 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
aria-haspopup="true" aria-haspopup="true"
aria-expanded={userMenuOpen ? 'true' : undefined} aria-expanded={userMenuOpen ? 'true' : undefined}
> >
<Avatar sx={{ <Avatar
sx={{
width: 32, width: 32,
height: 32, height: 32,
bgcolor: theme.palette.secondary.main, bgcolor: theme.palette.secondary.main,
}}> }}
>
{name.charAt(0).toUpperCase()} {name.charAt(0).toUpperCase()}
</Avatar> </Avatar>
<Box sx={{ display: { xs: 'none', sm: 'block' } }}> <Box sx={{ display: { xs: 'none', sm: 'block' } }}>{name}</Box>
{name}
</Box>
<ExpandMore fontSize="small" /> <ExpandMore fontSize="small" />
</UserButton> </UserButton>
@ -566,14 +579,12 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
position="fixed" position="fixed"
transparent={transparent} transparent={transparent}
className={className} className={className}
sx={{ overflow: "hidden" }} sx={{ overflow: 'hidden' }}
> >
<Container maxWidth="xl"> <Container maxWidth="xl">
<Toolbar disableGutters> <Toolbar disableGutters>
{/* Navigation Links - Desktop */} {/* Navigation Links - Desktop */}
<NavLinksContainer> <NavLinksContainer>{renderDesktopNavigation()}</NavLinksContainer>
{renderDesktopNavigation()}
</NavLinksContainer>
{/* User Actions Section */} {/* User Actions Section */}
<UserActionsContainer> <UserActionsContainer>
@ -608,7 +619,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`} content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
onClick={() => { onClick={() => {
navigate(`${window.location.pathname}?id=${sessionId}`); navigate(`${window.location.pathname}?id=${sessionId}`);
setSnack("Link copied!"); setSnack('Link copied!');
}} }}
size="large" size="large"
/> />
@ -630,8 +641,10 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
</Toolbar> </Toolbar>
</Container> </Container>
<Beta <Beta
sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} sx={{ left: '-90px', '& .mobile': { right: '-72px' } }}
onClick={() => { navigate('/docs/beta'); }} onClick={() => {
navigate('/docs/beta');
}}
/> />
</StyledAppBar> </StyledAppBar>
); );

View File

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

View File

@ -1,39 +1,39 @@
import React from 'react'; import React from 'react';
import { import { Typography, Avatar } from '@mui/material';
Typography,
Avatar,
} from '@mui/material';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import 'components/layout/Header.css'; import 'components/layout/Header.css';
const BackstoryLogo = () => { const BackstoryLogo = () => {
const theme = useTheme(); const theme = useTheme();
return <Typography return (
<Typography
variant="h6" variant="h6"
className="BackstoryLogo" className="BackstoryLogo"
noWrap noWrap
sx={{ sx={{
cursor: "pointer", cursor: 'pointer',
fontWeight: 700, fontWeight: 700,
letterSpacing: '.2rem', letterSpacing: '.2rem',
color: theme.palette.primary.contrastText, color: theme.palette.primary.contrastText,
textDecoration: 'none', textDecoration: 'none',
display: "inline-flex", display: 'inline-flex',
flexDirection: "row", flexDirection: 'row',
alignItems: "center", alignItems: 'center',
verticalAlign: "center", verticalAlign: 'center',
gap: 1, gap: 1,
textTransform: "uppercase", textTransform: 'uppercase',
}} }}
> >
<Avatar sx={{ width: 24, height: 24 }} <Avatar
sx={{ width: 24, height: 24 }}
variant="rounded" variant="rounded"
alt="Backstory logo" alt="Backstory logo"
src="/logo192.png" /> src="/logo192.png"
/>
Backstory Backstory
</Typography> </Typography>
);
}; };
export { BackstoryLogo }; export { BackstoryLogo };

View File

@ -9,9 +9,9 @@ type BetaProps = {
adaptive?: boolean; adaptive?: boolean;
onClick?: (event?: React.MouseEvent<HTMLElement>) => void; onClick?: (event?: React.MouseEvent<HTMLElement>) => void;
sx?: SxProps; sx?: SxProps;
} };
const Beta: React.FC<BetaProps> = (props : BetaProps) => { const Beta: React.FC<BetaProps> = (props: BetaProps) => {
const { onClick, adaptive = true, sx = {} } = props; const { onClick, adaptive = true, sx = {} } = props;
const betaRef = useRef<HTMLElement | null>(null); const betaRef = useRef<HTMLElement | null>(null);
const theme = useTheme(); const theme = useTheme();
@ -38,8 +38,14 @@ const Beta: React.FC<BetaProps> = (props : BetaProps) => {
}; };
return ( return (
<Box sx={sx} className={`beta-clipper ${adaptive && isMobile && "mobile"}`} onClick={(e) => { onClick && onClick(e); }}> <Box
<Box ref={betaRef} className={`beta-label ${adaptive && isMobile && "mobile"}`}> sx={sx}
className={`beta-clipper ${adaptive && isMobile && 'mobile'}`}
onClick={e => {
onClick && onClick(e);
}}
>
<Box ref={betaRef} className={`beta-label ${adaptive && isMobile && 'mobile'}`}>
<Box key={animationKey} className="particles"></Box> <Box key={animationKey} className="particles"></Box>
<Box>BETA</Box> <Box>BETA</Box>
</Box> </Box>
@ -47,6 +53,4 @@ const Beta: React.FC<BetaProps> = (props : BetaProps) => {
); );
}; };
export { export { Beta };
Beta
};

View File

@ -1,15 +1,10 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { Box, Link, Typography, Avatar, Grid, SxProps, Tooltip, IconButton } from '@mui/material'; import { Box, Link, Typography, Avatar, Grid, SxProps, Tooltip, IconButton } from '@mui/material';
import { import { Card, CardContent, Divider, useTheme } from '@mui/material';
Card,
CardContent,
Divider,
useTheme,
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import { useMediaQuery } from '@mui/material'; import { useMediaQuery } from '@mui/material';
import { Candidate, CandidateAI } from 'types/types'; import { Candidate, CandidateAI } from 'types/types';
import { CopyBubble } from "components/CopyBubble"; import { CopyBubble } from 'components/CopyBubble';
import { rest } from 'lodash'; import { rest } from 'lodash';
import { AIBanner } from 'components/ui/AIBanner'; import { AIBanner } from 'components/ui/AIBanner';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
@ -20,21 +15,16 @@ interface CandidateInfoProps {
sx?: SxProps; sx?: SxProps;
action?: string; action?: string;
elevation?: number; elevation?: number;
variant?: "minimal" | "small" | "normal" | undefined; variant?: 'minimal' | 'small' | 'normal' | undefined;
}; }
const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => { const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps) => {
const { candidate } = props; const { candidate } = props;
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const { const { sx, action = '', elevation = 1, variant = 'normal' } = props;
sx,
action = '',
elevation = 1,
variant = "normal"
} = props;
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')) || variant === "minimal"; const isMobile = useMediaQuery(theme.breakpoints.down('md')) || variant === 'minimal';
const ai: CandidateAI | null = ('isAI' in candidate) ? candidate as CandidateAI : null; const ai: CandidateAI | null = 'isAI' in candidate ? (candidate as CandidateAI) : null;
const isAdmin = user?.isAdmin; const isAdmin = user?.isAdmin;
// State for description expansion // State for description expansion
@ -55,7 +45,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
if (candidateId) { if (candidateId) {
await apiClient.deleteCandidate(candidateId); await apiClient.deleteCandidate(candidateId);
} }
} };
if (!candidate) { if (!candidate) {
return <Box>No user loaded.</Box>; return <Box>No user loaded.</Box>;
@ -64,27 +54,27 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
flexGrow: 1, flexGrow: 1,
p: isMobile ? 1 : 2, p: isMobile ? 1 : 2,
height: '100%', height: '100%',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'stretch', alignItems: 'stretch',
position: "relative", position: 'relative',
overflow: "hidden", overflow: 'hidden',
...sx ...sx,
}} }}
{...rest} {...rest}
> >
{ai && <AIBanner variant={variant} />} {ai && <AIBanner variant={variant} />}
<Box sx={{ display: "flex", flexDirection: "row" }}> <Box sx={{ display: 'flex', flexDirection: 'row' }}>
<Avatar <Avatar
src={candidate.profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''} src={candidate.profileImage ? `/api/1.0/candidates/profile/${candidate.username}` : ''}
alt={`${candidate.fullName}'s profile`} alt={`${candidate.fullName}'s profile`}
sx={{ sx={{
alignSelf: "flex-start", alignSelf: 'flex-start',
width: isMobile ? 40 : 80, width: isMobile ? 40 : 80,
height: isMobile ? 40 : 80, height: isMobile ? 40 : 80,
border: '2px solid #e0e0e0', border: '2px solid #e0e0e0',
@ -97,34 +87,42 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'flex-start',
mb: 1 mb: 1,
}}> }}
>
<Box> <Box>
<Box sx={{ <Box
display: "flex", sx={{
flexDirection: isMobile ? "column" : "row", display: 'flex',
alignItems: "left", flexDirection: isMobile ? 'column' : 'row',
gap: 1, "& > .MuiTypography-root": { m: 0 } alignItems: 'left',
}}> gap: 1,
{ '& > .MuiTypography-root': { m: 0 },
action !== '' && }}
<Typography variant="body1">{action}</Typography> >
} {action !== '' && <Typography variant="body1">{action}</Typography>}
{action === '' && {action === '' && (
<Typography variant="h5" component="h1" <Typography
variant="h5"
component="h1"
sx={{ sx={{
fontWeight: 'bold', fontWeight: 'bold',
whiteSpace: 'nowrap' whiteSpace: 'nowrap',
}}> }}
>
{candidate.fullName} {candidate.fullName}
</Typography> </Typography>
} )}
</Box> </Box>
<Box sx={{ fontSize: "0.75rem", alignItems: "center" }} > <Box sx={{ fontSize: '0.75rem', alignItems: 'center' }}>
<Link href={`/u/${candidate.username}`}>{`/u/${candidate.username}`}</Link> <Link href={`/u/${candidate.username}`}>{`/u/${candidate.username}`}</Link>
<CopyBubble <CopyBubble
onClick={(event: any) => { event.stopPropagation() }} onClick={(event: any) => {
tooltip="Copy link" content={`${window.location.origin}/u/{candidate.username}`} /> event.stopPropagation();
}}
tooltip="Copy link"
content={`${window.location.origin}/u/{candidate.username}`}
/>
</Box> </Box>
</Box> </Box>
</Box> </Box>
@ -132,8 +130,8 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
</Box> </Box>
<Box> <Box>
{(!isMobile && variant === "normal") && ( {!isMobile && variant === 'normal' && (
<Box sx={{ minHeight: "5rem" }}> <Box sx={{ minHeight: '5rem' }}>
<Typography <Typography
ref={descriptionRef} ref={descriptionRef}
variant="body1" variant="body1"
@ -145,7 +143,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
lineHeight: 1.5, lineHeight: 1.5,
fontSize: "0.8rem !important", fontSize: '0.8rem !important',
}} }}
> >
{candidate.description} {candidate.description}
@ -154,7 +152,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
<Link <Link
component="button" component="button"
variant="body2" variant="body2"
onClick={(e) => { onClick={e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsDescriptionExpanded(!isDescriptionExpanded); setIsDescriptionExpanded(!isDescriptionExpanded);
@ -169,46 +167,59 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
display: 'block', display: 'block',
'&:hover': { '&:hover': {
textDecoration: 'underline', textDecoration: 'underline',
} },
}} }}
> >
[{isDescriptionExpanded ? "less" : "more"}] [{isDescriptionExpanded ? 'less' : 'more'}]
</Link> </Link>
)} )}
</Box> </Box>
)} )}
{(variant !== "small" && variant !== "minimal") && <> {variant !== 'small' && variant !== 'minimal' && (
<>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
{candidate.location && {candidate.location && (
<Typography variant="body2" sx={{ mb: 1 }}> <Typography variant="body2" sx={{ mb: 1 }}>
<strong>Location:</strong> {candidate.location.city}, {candidate.location.state || candidate.location.country} <strong>Location:</strong> {candidate.location.city},{' '}
{candidate.location.state || candidate.location.country}
</Typography> </Typography>
} )}
{candidate.email && {candidate.email && (
<Typography variant="body2" sx={{ mb: 1 }}> <Typography variant="body2" sx={{ mb: 1 }}>
<strong>Email:</strong> {candidate.email} <strong>Email:</strong> {candidate.email}
</Typography> </Typography>
} )}
{candidate.phone && <Typography variant="body2"> {candidate.phone && (
<Typography variant="body2">
<strong>Phone:</strong> {candidate.phone} <strong>Phone:</strong> {candidate.phone}
</Typography> </Typography>
} )}
</>} </>
)}
</Box> </Box>
{isAdmin && ai && {isAdmin && ai && (
<Box sx={{ display: "flex", flexDirection: "row", justifyContent: "flex-start" }}> <Box
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-start',
}}
>
<Tooltip title="Delete Job"> <Tooltip title="Delete Job">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { e.stopPropagation(); deleteCandidate(candidate.id); }} onClick={e => {
e.stopPropagation();
deleteCandidate(candidate.id);
}}
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Box> </Box>
} )}
</Box> </Box>
); );
}; };

View File

@ -1,18 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { BackstoryElementProps } from 'components/BackstoryTab'; import { BackstoryElementProps } from 'components/BackstoryTab';
import { CandidateInfo } from 'components/ui/CandidateInfo'; import { CandidateInfo } from 'components/ui/CandidateInfo';
import { Candidate, CandidateAI } from "types/types"; import { Candidate, CandidateAI } from 'types/types';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext'; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import { Paper } from '@mui/material'; import { Paper } from '@mui/material';
interface CandidatePickerProps extends BackstoryElementProps { interface CandidatePickerProps extends BackstoryElementProps {
onSelect?: (candidate: Candidate) => void; onSelect?: (candidate: Candidate) => void;
}; }
const CandidatePicker = (props: CandidatePickerProps) => { const CandidatePicker = (props: CandidatePickerProps) => {
const { onSelect, sx } = props; const { onSelect, sx } = props;
@ -47,7 +47,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
}); });
setCandidates(candidates); setCandidates(candidates);
} catch (err) { } catch (err) {
setSnack("" + err); setSnack('' + err);
} }
}; };
@ -55,33 +55,43 @@ const CandidatePicker = (props: CandidatePickerProps) => {
}, [candidates, setSnack]); }, [candidates, setSnack]);
return ( return (
<Box sx={{ display: "flex", flexDirection: "column", ...sx }}> <Box sx={{ display: 'flex', flexDirection: 'column', ...sx }}>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", justifyContent: "center" }}> <Box
{candidates?.map((u, i) =>
<Paper key={`${u.username}`}
onClick={() => { onSelect ? onSelect(u) : setSelectedCandidate(u); }}
sx={{ cursor: "pointer" }}>
<CandidateInfo variant="small"
sx={{ sx={{
maxWidth: "100%", display: 'flex',
minWidth: "320px", gap: 1,
width: "320px", flexWrap: 'wrap',
"cursor": "pointer", justifyContent: 'center',
backgroundColor: (selectedCandidate?.id === u.id) ? "#f0f0f0" : "inherit", }}
border: "2px solid transparent", >
"&:hover": { {candidates?.map((u, i) => (
border: "2px solid orange" <Paper
} key={`${u.username}`}
onClick={() => {
onSelect ? onSelect(u) : setSelectedCandidate(u);
}}
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',
},
}} }}
candidate={u} candidate={u}
/> />
</Paper> </Paper>
)} ))}
</Box> </Box>
</Box> </Box>
); );
}; };
export { export { CandidatePicker };
CandidatePicker
};

View File

@ -7,9 +7,9 @@ import './ComingSoon.css';
type ComingSoonProps = { type ComingSoonProps = {
children?: React.ReactNode; children?: React.ReactNode;
} };
const ComingSoon: React.FC<ComingSoonProps> = (props : ComingSoonProps) => { const ComingSoon: React.FC<ComingSoonProps> = (props: ComingSoonProps) => {
const { children } = props; const { children } = props;
const theme = useTheme(); const theme = useTheme();
return ( return (
@ -20,6 +20,4 @@ const ComingSoon: React.FC<ComingSoonProps> = (props : ComingSoonProps) => {
); );
}; };
export { export { ComingSoon };
ComingSoon
};

View File

@ -1,15 +1,26 @@
import React, { JSX, useActionState, useEffect, useRef, useState } from 'react'; import React, { JSX, useActionState, useEffect, useRef, useState } from 'react';
import { Box, Link, Typography, Avatar, Grid, SxProps, CardActions, Chip, Stack, CardHeader, Button, styled, LinearProgress, IconButton, Tooltip } from '@mui/material';
import { import {
Card, Box,
CardContent, Link,
Divider, Typography,
useTheme, Avatar,
Grid,
SxProps,
CardActions,
Chip,
Stack,
CardHeader,
Button,
styled,
LinearProgress,
IconButton,
Tooltip,
} from '@mui/material'; } from '@mui/material';
import { Card, CardContent, Divider, useTheme } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import { useMediaQuery } from '@mui/material'; import { useMediaQuery } from '@mui/material';
import { Job } from 'types/types'; import { Job } from 'types/types';
import { CopyBubble } from "components/CopyBubble"; import { CopyBubble } from 'components/CopyBubble';
import { rest } from 'lodash'; import { rest } from 'lodash';
import { AIBanner } from 'components/ui/AIBanner'; import { AIBanner } from 'components/ui/AIBanner';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
@ -19,7 +30,7 @@ import ModelTrainingIcon from '@mui/icons-material/ModelTraining';
import { StatusIcon, StatusBox } from 'components/ui/StatusIcon'; import { StatusIcon, StatusBox } from 'components/ui/StatusIcon';
import RestoreIcon from '@mui/icons-material/Restore'; import RestoreIcon from '@mui/icons-material/Restore';
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from '@mui/icons-material/Save';
import * as Types from "types/types"; import * as Types from 'types/types';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
import { StyledMarkdown } from 'components/StyledMarkdown'; import { StyledMarkdown } from 'components/StyledMarkdown';
@ -28,25 +39,22 @@ interface JobInfoProps {
sx?: SxProps; sx?: SxProps;
action?: string; action?: string;
elevation?: number; elevation?: number;
variant?: "minimal" | "small" | "normal" | "all" | null variant?: 'minimal' | 'small' | 'normal' | 'all' | null;
}; }
const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => { const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const { job } = props; const { job } = props;
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const { const { sx, action = '', elevation = 1, variant = 'normal' } = props;
sx,
action = '',
elevation = 1,
variant = "normal"
} = props;
const theme = useTheme(); 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 isAdmin = user?.isAdmin;
const [adminStatus, setAdminStatus] = useState<string | null>(null); 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 */ const [activeJob, setActiveJob] = useState<Types.Job>({
...job,
}); /* Copy of job */
// State for description expansion // State for description expansion
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
const [shouldShowMoreButton, setShouldShowMoreButton] = useState(false); const [shouldShowMoreButton, setShouldShowMoreButton] = useState(false);
@ -72,11 +80,11 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
if (jobId) { if (jobId) {
await apiClient.deleteJob(jobId); await apiClient.deleteJob(jobId);
} }
} };
const handleReset = async () => { const handleReset = async () => {
setActiveJob({ ...job }); setActiveJob({ ...job });
} };
if (!job) { if (!job) {
return <Box>No job provided.</Box>; return <Box>No job provided.</Box>;
@ -88,12 +96,12 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
requirements: activeJob.requirements, requirements: activeJob.requirements,
}); });
job.updatedAt = newJob.updatedAt; job.updatedAt = newJob.updatedAt;
setActiveJob(newJob) setActiveJob(newJob);
setSnack('Job updated.'); setSnack('Job updated.');
} };
const handleRefresh = () => { const handleRefresh = () => {
setAdminStatus("Re-extracting Job information..."); setAdminStatus('Re-extracting Job information...');
const jobStatusHandlers = { const jobStatusHandlers = {
onStatus: (status: Types.ChatMessageStatus) => { onStatus: (status: Types.ChatMessageStatus) => {
console.log('status:', status.content); console.log('status:', status.content);
@ -101,7 +109,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
setAdminStatus(status.content); setAdminStatus(status.content);
}, },
onMessage: async (jobMessage: Types.JobRequirementsMessage) => { onMessage: async (jobMessage: Types.JobRequirementsMessage) => {
const newJob: Types.Job = jobMessage.job const newJob: Types.Job = jobMessage.job;
console.log('onMessage - job', newJob); console.log('onMessage - job', newJob);
newJob.id = job.id; newJob.id = job.id;
newJob.createdAt = job.createdAt; newJob.createdAt = job.createdAt;
@ -116,22 +124,37 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
onComplete: () => { onComplete: () => {
setAdminStatusType(null); setAdminStatusType(null);
setAdminStatus(null); setAdminStatus(null);
} },
}; };
apiClient.createJobFromDescription(activeJob.description, jobStatusHandlers); apiClient.createJobFromDescription(activeJob.description, jobStatusHandlers);
}; };
const renderRequirementSection = (title: string, items: string[] | undefined, icon: JSX.Element, required = false) => { const renderRequirementSection = (
title: string,
items: string[] | undefined,
icon: JSX.Element,
required = false
) => {
if (!items || items.length === 0) return null; if (!items || items.length === 0) return null;
return ( return (
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}>
{icon} {icon}
<Typography variant="subtitle1" sx={{ ml: 1, fontWeight: 600, fontSize: '0.85rem !important'}}> <Typography
variant="subtitle1"
sx={{ ml: 1, fontWeight: 600, fontSize: '0.85rem !important' }}
>
{title} {title}
</Typography> </Typography>
{required && <Chip label="Required" size="small" color="error" sx={{ ml: 1, fontSize: '0.75rem !important' }} />} {required && (
<Chip
label="Required"
size="small"
color="error"
sx={{ ml: 1, fontSize: '0.75rem !important' }}
/>
)}
</Box> </Box>
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap> <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
{items.map((item, index) => ( {items.map((item, index) => (
@ -152,7 +175,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
if (!activeJob.requirements) return null; if (!activeJob.requirements) return null;
return ( 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 <CardHeader
title="Job Requirements Analysis" title="Job Requirements Analysis"
avatar={<CheckCircle color="success" />} avatar={<CheckCircle color="success" />}
@ -160,49 +183,49 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
/> />
<CardContent sx={{ p: 0 }}> <CardContent sx={{ p: 0 }}>
{renderRequirementSection( {renderRequirementSection(
"Technical Skills (Required)", 'Technical Skills (Required)',
activeJob.requirements.technicalSkills.required, activeJob.requirements.technicalSkills.required,
<Build color="primary" />, <Build color="primary" />,
true true
)} )}
{renderRequirementSection( {renderRequirementSection(
"Technical Skills (Preferred)", 'Technical Skills (Preferred)',
activeJob.requirements.technicalSkills.preferred, activeJob.requirements.technicalSkills.preferred,
<Build color="action" /> <Build color="action" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience Requirements (Required)", 'Experience Requirements (Required)',
activeJob.requirements.experienceRequirements.required, activeJob.requirements.experienceRequirements.required,
<Work color="primary" />, <Work color="primary" />,
true true
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience Requirements (Preferred)", 'Experience Requirements (Preferred)',
activeJob.requirements.experienceRequirements.preferred, activeJob.requirements.experienceRequirements.preferred,
<Work color="action" /> <Work color="action" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Soft Skills", 'Soft Skills',
activeJob.requirements.softSkills, activeJob.requirements.softSkills,
<Psychology color="secondary" /> <Psychology color="secondary" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Experience", 'Experience',
activeJob.requirements.experience, activeJob.requirements.experience,
<Star color="warning" /> <Star color="warning" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Education", 'Education',
activeJob.requirements.education, activeJob.requirements.education,
<Description color="info" /> <Description color="info" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Certifications", 'Certifications',
activeJob.requirements.certifications, activeJob.requirements.certifications,
<CheckCircle color="success" /> <CheckCircle color="success" />
)} )}
{renderRequirementSection( {renderRequirementSection(
"Preferred Attributes", 'Preferred Attributes',
activeJob.requirements.preferredAttributes, activeJob.requirements.preferredAttributes,
<Star color="secondary" /> <Star color="secondary" />
)} )}
@ -214,44 +237,77 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
borderColor: 'transparent', borderColor: 'transparent',
borderWidth: 2, borderWidth: 2,
borderStyle: 'solid', borderStyle: 'solid',
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
flexDirection: "column", flexDirection: 'column',
minWidth: 0, minWidth: 0,
opacity: deleted ? 0.5 : 1.0, opacity: deleted ? 0.5 : 1.0,
backgroundColor: deleted ? theme.palette.action.disabledBackground : theme.palette.background.paper, backgroundColor: deleted
pointerEvents: deleted ? "none" : "auto", ? theme.palette.action.disabledBackground
: theme.palette.background.paper,
pointerEvents: deleted ? 'none' : 'auto',
...sx, ...sx,
}} }}
{...rest} {...rest}
> >
<Box sx={{ display: "flex", flexGrow: 1, p: 1, pb: 0, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}> <Box
<Box sx={{ sx={{
display: "flex", flexDirection: (isMobile || variant === "small") ? "column" : "row", display: 'flex',
"& > div > div > :first-of-type": { fontWeight: "bold", whiteSpace: "nowrap" }, flexGrow: 1,
"& > div > div > :last-of-type": { mb: 0.75, mr: 1 } p: 1,
}}> pb: 0,
<Box sx={{ display: "flex", flexDirection: isMobile ? "row" : "column", flexGrow: 1, gap: 1 }}> height: '100%',
{activeJob.company && flexDirection: 'column',
<Box sx={{ fontSize: "0.8rem" }}> alignItems: 'stretch',
position: 'relative',
}}
>
<Box
sx={{
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 },
}}
>
<Box
sx={{
display: 'flex',
flexDirection: isMobile ? 'row' : 'column',
flexGrow: 1,
gap: 1,
}}
>
{activeJob.company && (
<Box sx={{ fontSize: '0.8rem' }}>
<Box>Company</Box> <Box>Company</Box>
<Box sx={{ whiteSpace: "nowrap" }}>{activeJob.company}</Box> <Box sx={{ whiteSpace: 'nowrap' }}>{activeJob.company}</Box>
</Box> </Box>
} )}
{activeJob.title && {activeJob.title && (
<Box sx={{ fontSize: "0.8rem" }}> <Box sx={{ fontSize: '0.8rem' }}>
<Box>Title</Box> <Box>Title</Box>
<Box>{activeJob.title}</Box> <Box>{activeJob.title}</Box>
</Box> </Box>
} )}
</Box> </Box>
<Box sx={{ display: "flex", flexDirection: "column", width: (variant !== "small" && variant !== "minimal") ? "75%" : "100%" }}> <Box
{!isMobile && activeJob.summary && <Box sx={{ fontSize: "0.8rem" }}> sx={{
display: 'flex',
flexDirection: 'column',
width: variant !== 'small' && variant !== 'minimal' ? '75%' : '100%',
}}
>
{!isMobile && activeJob.summary && (
<Box sx={{ fontSize: '0.8rem' }}>
<Box>Summary</Box> <Box>Summary</Box>
<Box sx={{ minHeight: variant === "small" ? "5rem" : "inherit" }}> <Box sx={{ minHeight: variant === 'small' ? '5rem' : 'inherit' }}>
<Typography <Typography
ref={descriptionRef} ref={descriptionRef}
variant="body1" variant="body1"
@ -263,7 +319,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
lineHeight: 1.5, lineHeight: 1.5,
fontSize: "0.8rem !important", fontSize: '0.8rem !important',
}} }}
> >
{activeJob.summary} {activeJob.summary}
@ -272,7 +328,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Link <Link
component="button" component="button"
variant="body2" variant="body2"
onClick={(e) => { onClick={e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsDescriptionExpanded(!isDescriptionExpanded); setIsDescriptionExpanded(!isDescriptionExpanded);
@ -287,55 +343,90 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
display: 'block', display: 'block',
'&:hover': { '&:hover': {
textDecoration: 'underline', textDecoration: 'underline',
} },
}} }}
> >
[{isDescriptionExpanded ? "less" : "more"}] [{isDescriptionExpanded ? 'less' : 'more'}]
</Link> </Link>
)} )}
</Box> </Box>
</Box>} </Box>
)}
</Box> </Box>
</Box> </Box>
{(variant !== "small" && variant !== "minimal") && <> {variant !== 'small' && variant !== 'minimal' && (
{activeJob.details && <>
{activeJob.details && (
<Typography variant="body2" sx={{ mb: 1 }}> <Typography variant="body2" sx={{ mb: 1 }}>
<strong>Location:</strong> {activeJob.details.location.city}, {activeJob.details.location.state || activeJob.details.location.country} <strong>Location:</strong> {activeJob.details.location.city},{' '}
{activeJob.details.location.state || activeJob.details.location.country}
</Typography> </Typography>
} )}
{activeJob.owner && <Typography variant="body2"> {activeJob.owner && (
<Typography variant="body2">
<strong>Submitted by:</strong> {activeJob.owner.fullName} <strong>Submitted by:</strong> {activeJob.owner.fullName}
</Typography>} </Typography>
{activeJob.createdAt && )}
<Typography variant="caption">Created: {activeJob.createdAt.toISOString()}</Typography> {activeJob.createdAt && (
} <Typography variant="caption">
{activeJob.updatedAt && Created: {activeJob.createdAt.toISOString()}
<Typography variant="caption">Updated: {activeJob.updatedAt.toISOString()}</Typography> </Typography>
} )}
{activeJob.updatedAt && (
<Typography variant="caption">
Updated: {activeJob.updatedAt.toISOString()}
</Typography>
)}
<Typography variant="caption">Job ID: {job.id}</Typography> <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') && <Box><Divider />{renderJobRequirements()}</Box>} {variant !== 'small' && variant !== 'minimal' && (
<Box>
<Divider />
{renderJobRequirements()}
</Box>
)}
{isAdmin && {isAdmin && (
<Box sx={{ display: "flex", flexDirection: "column", p: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', p: 1 }}>
<Box sx={{ display: "flex", flexDirection: "row", pl: 1, pr: 1, gap: 1, alignContent: "center", height: "32px" }}> <Box
{(job.updatedAt && job.updatedAt.toISOString()) !== (activeJob.updatedAt && activeJob.updatedAt.toISOString()) && sx={{
display: 'flex',
flexDirection: 'row',
pl: 1,
pr: 1,
gap: 1,
alignContent: 'center',
height: '32px',
}}
>
{(job.updatedAt && job.updatedAt.toISOString()) !==
(activeJob.updatedAt && activeJob.updatedAt.toISOString()) && (
<Tooltip title="Save Job"> <Tooltip title="Save Job">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { e.stopPropagation(); handleSave(); }} onClick={e => {
e.stopPropagation();
handleSave();
}}
> >
<SaveIcon /> <SaveIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
} )}
<Tooltip title="Delete Job"> <Tooltip title="Delete Job">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { e.stopPropagation(); deleteJob(job.id); setDeleted(true) }} onClick={e => {
e.stopPropagation();
deleteJob(job.id);
setDeleted(true);
}}
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
@ -343,7 +434,10 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Tooltip title="Reset Job"> <Tooltip title="Reset Job">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { e.stopPropagation(); handleReset(); }} onClick={e => {
e.stopPropagation();
handleReset();
}}
> >
<RestoreIcon /> <RestoreIcon />
</IconButton> </IconButton>
@ -351,13 +445,16 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Tooltip title="Reprocess Job"> <Tooltip title="Reprocess Job">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { e.stopPropagation(); handleRefresh(); }} onClick={e => {
e.stopPropagation();
handleRefresh();
}}
> >
<ModelTrainingIcon /> <ModelTrainingIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Box> </Box>
{adminStatus && {adminStatus && (
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 3 }}>
<StatusBox> <StatusBox>
{adminStatusType && <StatusIcon type={adminStatusType} />} {adminStatusType && <StatusIcon type={adminStatusType} />}
@ -367,11 +464,11 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
</StatusBox> </StatusBox>
{adminStatus && <LinearProgress sx={{ mt: 1 }} />} {adminStatus && <LinearProgress sx={{ mt: 1 }} />}
</Box> </Box>
} )}
</Box>
)}
</Box>
</Box> </Box>
}
</Box >
</Box >
); );
}; };

View File

@ -1,18 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useNavigate } from "react-router-dom"; import { useNavigate } from 'react-router-dom';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { BackstoryElementProps } from 'components/BackstoryTab'; import { BackstoryElementProps } from 'components/BackstoryTab';
import { JobInfo } from 'components/ui/JobInfo'; import { JobInfo } from 'components/ui/JobInfo';
import { Job } from "types/types"; import { Job } from 'types/types';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedJob } from 'hooks/GlobalContext'; import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
import { Paper } from '@mui/material'; import { Paper } from '@mui/material';
interface JobPickerProps extends BackstoryElementProps { interface JobPickerProps extends BackstoryElementProps {
onSelect?: (job: Job) => void onSelect?: (job: Job) => void;
}; }
const JobPicker = (props: JobPickerProps) => { const JobPicker = (props: JobPickerProps) => {
const { onSelect } = props; const { onSelect } = props;
@ -38,7 +38,7 @@ const JobPicker = (props: JobPickerProps) => {
}); });
setJobs(jobs); setJobs(jobs);
} catch (err) { } catch (err) {
setSnack("" + err); setSnack('' + err);
} }
}; };
@ -46,33 +46,44 @@ const JobPicker = (props: JobPickerProps) => {
}, [jobs, setSnack]); }, [jobs, setSnack]);
return ( return (
<Box sx={{display: "flex", flexDirection: "column", mb: 1}}> <Box sx={{ display: 'flex', flexDirection: 'column', mb: 1 }}>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", justifyContent: "center" }}> <Box
{jobs?.map((j, i) =>
<Paper key={`${j.id}`}
onClick={() => { console.log('Selected job', j); onSelect && onSelect(j) }}
sx={{ cursor: "pointer" }}>
<JobInfo variant="small"
sx={{ sx={{
maxWidth: "100%", display: 'flex',
minWidth: "320px", gap: 1,
width: "320px", flexWrap: 'wrap',
"cursor": "pointer", justifyContent: 'center',
backgroundColor: (selectedJob?.id === j.id) ? "#f0f0f0" : "inherit", }}
border: "2px solid transparent", >
"&:hover": { {jobs?.map((j, i) => (
border: "2px solid orange" <Paper
} key={`${j.id}`}
onClick={() => {
console.log('Selected job', j);
onSelect && onSelect(j);
}}
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',
},
}} }}
job={j} job={j}
/> />
</Paper> </Paper>
)} ))}
</Box> </Box>
</Box> </Box>
); );
}; };
export { export { JobPicker };
JobPicker
};

View File

@ -20,7 +20,7 @@ import {
Toolbar, Toolbar,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
Slide Slide,
} from '@mui/material'; } from '@mui/material';
import { import {
KeyboardArrowUp as ArrowUpIcon, KeyboardArrowUp as ArrowUpIcon,
@ -29,11 +29,11 @@ import {
Work as WorkIcon, Work as WorkIcon,
Schedule as ScheduleIcon, Schedule as ScheduleIcon,
Close as CloseIcon, Close as CloseIcon,
ArrowBack as ArrowBackIcon ArrowBack as ArrowBackIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { TransitionProps } from '@mui/material/transitions'; import { TransitionProps } from '@mui/material/transitions';
import { JobInfo } from 'components/ui/JobInfo'; import { JobInfo } from 'components/ui/JobInfo';
import { Job } from "types/types"; import { Job } from 'types/types';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedJob } from 'hooks/GlobalContext'; import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
import { Navigate, useNavigate, useParams } from 'react-router-dom'; import { Navigate, useNavigate, useParams } from 'react-router-dom';
@ -49,7 +49,7 @@ const Transition = React.forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
children: React.ReactElement; children: React.ReactElement;
}, },
ref: React.Ref<unknown>, ref: React.Ref<unknown>
) { ) {
return <Slide direction="up" ref={ref} {...props} />; return <Slide direction="up" ref={ref} {...props} />;
}); });
@ -95,7 +95,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
onSelect?.(firstJob); onSelect?.(firstJob);
} }
} catch (err) { } catch (err) {
setSnack("Failed to load jobs: " + err); setSnack('Failed to load jobs: ' + err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -164,13 +164,17 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
...(isMobile ? {} : { year: 'numeric' }), ...(isMobile ? {} : { year: 'numeric' }),
...(isSmall ? {} : { hour: '2-digit', minute: '2-digit' }) ...(isSmall ? {} : { hour: '2-digit', minute: '2-digit' }),
}).format(date); }).format(date);
}; };
const getSortIcon = (field: SortField) => { const getSortIcon = (field: SortField) => {
if (sortField !== field) return null; if (sortField !== field) return null;
return sortOrder === 'asc' ? <ArrowUpIcon fontSize="small" /> : <ArrowDownIcon fontSize="small" />; return sortOrder === 'asc' ? (
<ArrowUpIcon fontSize="small" />
) : (
<ArrowDownIcon fontSize="small" />
);
}; };
const JobList = () => ( const JobList = () => (
@ -181,17 +185,19 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
flexDirection: 'column', flexDirection: 'column',
width: '100%', width: '100%',
boxShadow: 'none', boxShadow: 'none',
backgroundColor: 'transparent' backgroundColor: 'transparent',
}} }}
> >
<Box sx={{ <Box
sx={{
p: isMobile ? 0.5 : 1, p: isMobile ? 0.5 : 1,
borderBottom: 1, borderBottom: 1,
borderColor: 'divider', borderColor: 'divider',
backgroundColor: isMobile ? 'background.paper' : 'inherit' backgroundColor: isMobile ? 'background.paper' : 'inherit',
}}> }}
>
<Typography <Typography
variant={isSmall ? "subtitle2" : isMobile ? "subtitle1" : "h6"} variant={isSmall ? 'subtitle2' : isMobile ? 'subtitle1' : 'h6'}
gutterBottom gutterBottom
sx={{ mb: isMobile ? 0.5 : 1, fontWeight: 600 }} sx={{ mb: isMobile ? 0.5 : 1, fontWeight: 600 }}
> >
@ -203,7 +209,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
<Select <Select
value={`${sortField}-${sortOrder}`} value={`${sortField}-${sortOrder}`}
label="Sort by" label="Sort by"
onChange={(e) => { onChange={e => {
const [field, order] = e.target.value.split('-') as [SortField, SortOrder]; const [field, order] = e.target.value.split('-') as [SortField, SortOrder];
setSortField(field); setSortField(field);
setSortOrder(order); setSortOrder(order);
@ -221,13 +227,15 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
</FormControl> </FormControl>
</Box> </Box>
<TableContainer sx={{ <TableContainer
sx={{
flex: 1, flex: 1,
overflow: 'auto', overflow: 'auto',
'& .MuiTable-root': { '& .MuiTable-root': {
tableLayout: isMobile ? 'fixed' : 'auto' tableLayout: isMobile ? 'fixed' : 'auto',
} },
}}> }}
>
<Table stickyHeader size="small"> <Table stickyHeader size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
@ -238,12 +246,12 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? '25%' : 'auto', width: isMobile ? '25%' : 'auto',
backgroundColor: 'background.paper' backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort('company')} onClick={() => handleSort('company')}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<BusinessIcon fontSize={isMobile ? "small" : "medium"} /> <BusinessIcon fontSize={isMobile ? 'small' : 'medium'} />
<Typography variant="caption" fontWeight="bold" noWrap> <Typography variant="caption" fontWeight="bold" noWrap>
{isSmall ? 'Co.' : isMobile ? 'Company' : 'Company'} {isSmall ? 'Co.' : isMobile ? 'Company' : 'Company'}
</Typography> </Typography>
@ -257,13 +265,15 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? '45%' : 'auto', width: isMobile ? '45%' : 'auto',
backgroundColor: 'background.paper' backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort('title')} onClick={() => handleSort('title')}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<WorkIcon fontSize={isMobile ? "small" : "medium"} /> <WorkIcon fontSize={isMobile ? 'small' : 'medium'} />
<Typography variant="caption" fontWeight="bold" noWrap>Title</Typography> <Typography variant="caption" fontWeight="bold" noWrap>
Title
</Typography>
{getSortIcon('title')} {getSortIcon('title')}
</Box> </Box>
</TableCell> </TableCell>
@ -274,23 +284,27 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
userSelect: 'none', userSelect: 'none',
py: 0.5, py: 0.5,
px: 1, px: 1,
backgroundColor: 'background.paper' backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort('updatedAt')} onClick={() => handleSort('updatedAt')}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<ScheduleIcon fontSize="medium" /> <ScheduleIcon fontSize="medium" />
<Typography variant="caption" fontWeight="bold">Updated</Typography> <Typography variant="caption" fontWeight="bold">
Updated
</Typography>
{getSortIcon('updatedAt')} {getSortIcon('updatedAt')}
</Box> </Box>
</TableCell> </TableCell>
)} )}
<TableCell sx={{ <TableCell
sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? '30%' : 'auto', width: isMobile ? '30%' : 'auto',
backgroundColor: 'background.paper' backgroundColor: 'background.paper',
}}> }}
>
<Typography variant="caption" fontWeight="bold" noWrap> <Typography variant="caption" fontWeight="bold" noWrap>
{isMobile ? 'Status' : 'Status'} {isMobile ? 'Status' : 'Status'}
</Typography> </Typography>
@ -298,7 +312,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{sortedJobs.map((job) => ( {sortedJobs.map(job => (
<TableRow <TableRow
key={job.id} key={job.id}
hover hover
@ -312,16 +326,18 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
}, },
'&:hover': { '&:hover': {
backgroundColor: 'action.hover', backgroundColor: 'action.hover',
} },
}} }}
> >
<TableCell sx={{ <TableCell
sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: 'hidden' overflow: 'hidden',
}}> }}
>
<Typography <Typography
variant={isMobile ? "caption" : "body2"} variant={isMobile ? 'caption' : 'body2'}
fontWeight="medium" fontWeight="medium"
noWrap noWrap
sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }} sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }}
@ -335,17 +351,20 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
noWrap 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> </Typography>
)} )}
</TableCell> </TableCell>
<TableCell sx={{ <TableCell
sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: 'hidden' overflow: 'hidden',
}}> }}
>
<Typography <Typography
variant={isMobile ? "caption" : "body2"} variant={isMobile ? 'caption' : 'body2'}
fontWeight="medium" fontWeight="medium"
noWrap noWrap
sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }} sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }}
@ -361,7 +380,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
mt: 0.25, mt: 0.25,
fontSize: '0.6rem', fontSize: '0.6rem',
height: 16, height: 16,
'& .MuiChip-label': { px: 0.5 } '& .MuiChip-label': { px: 0.5 },
}} }}
/> />
)} )}
@ -382,14 +401,16 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
)} )}
</TableCell> </TableCell>
)} )}
<TableCell sx={{ <TableCell
sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: 'hidden' overflow: 'hidden',
}}> }}
>
<Chip <Chip
label={job.details?.isActive ? "Active" : "Inactive"} label={job.details?.isActive ? 'Active' : 'Inactive'}
color={job.details?.isActive ? "success" : "default"} color={job.details?.isActive ? 'success' : 'default'}
size="small" size="small"
variant="outlined" variant="outlined"
sx={{ sx={{
@ -397,8 +418,8 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
height: isMobile ? 20 : 22, height: isMobile ? 20 : 22,
'& .MuiChip-label': { '& .MuiChip-label': {
px: isMobile ? 0.5 : 0.75, px: isMobile ? 0.5 : 0.75,
py: 0 py: 0,
} },
}} }}
/> />
</TableCell> </TableCell>
@ -411,12 +432,14 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
); );
const JobDetails = ({ inDialog = false }: { inDialog?: boolean }) => ( const JobDetails = ({ inDialog = false }: { inDialog?: boolean }) => (
<Box sx={{ <Box
sx={{
flex: 1, flex: 1,
overflow: 'auto', overflow: 'auto',
p: inDialog ? 1.5 : 0.75, p: inDialog ? 1.5 : 0.75,
height: inDialog ? '100%' : 'auto' height: inDialog ? '100%' : 'auto',
}}> }}
>
{selectedJob ? ( {selectedJob ? (
<JobInfo <JobInfo
job={selectedJob} job={selectedJob}
@ -426,34 +449,36 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
boxShadow: 'none', boxShadow: 'none',
backgroundColor: 'transparent', backgroundColor: 'transparent',
'& .MuiTypography-h6': { '& .MuiTypography-h6': {
fontSize: inDialog ? '1.25rem' : '1.1rem' fontSize: inDialog ? '1.25rem' : '1.1rem',
} },
}} }}
/> />
) : ( ) : (
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
height: '100%', height: '100%',
color: 'text.secondary', color: 'text.secondary',
textAlign: 'center', textAlign: 'center',
p: 2 p: 2,
}}> }}
<Typography variant="body2"> >
Select a job to view details <Typography variant="body2">Select a job to view details</Typography>
</Typography>
</Box> </Box>
)} )}
</Box> </Box>
); );
return ( return (
<Box sx={{ <Box
sx={{
height: '100%', height: '100%',
p: 0.5, p: 0.5,
backgroundColor: 'background.default' backgroundColor: 'background.default',
}}> }}
>
<JobList /> <JobList />
<Dialog <Dialog
@ -475,12 +500,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
<Box sx={{ ml: 1, flex: 1, minWidth: 0 }}> <Box sx={{ ml: 1, flex: 1, minWidth: 0 }}>
<Typography <Typography variant="h6" component="div" noWrap sx={{ fontSize: '1rem' }}>
variant="h6"
component="div"
noWrap
sx={{ fontSize: '1rem' }}
>
{selectedJob?.title} {selectedJob?.title}
</Typography> </Typography>
<Typography <Typography

View File

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

View File

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

View File

@ -25,7 +25,7 @@ import {
DialogContent, DialogContent,
DialogActions, DialogActions,
Tabs, Tabs,
Tab Tab,
} from '@mui/material'; } from '@mui/material';
import PrintIcon from '@mui/icons-material/Print'; import PrintIcon from '@mui/icons-material/Print';
import { import {
@ -38,12 +38,12 @@ import {
Person as PersonIcon, Person as PersonIcon,
Schedule as ScheduleIcon, Schedule as ScheduleIcon,
Visibility as VisibilityIcon, Visibility as VisibilityIcon,
VisibilityOff as VisibilityOffIcon VisibilityOff as VisibilityOffIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import PreviewIcon from '@mui/icons-material/Preview'; import PreviewIcon from '@mui/icons-material/Preview';
import EditDocumentIcon from '@mui/icons-material/EditDocument'; import EditDocumentIcon from '@mui/icons-material/EditDocument';
import { useReactToPrint } from "react-to-print"; import { useReactToPrint } from 'react-to-print';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
@ -57,21 +57,16 @@ interface ResumeInfoProps {
sx?: SxProps; sx?: SxProps;
action?: string; action?: string;
elevation?: number; elevation?: number;
variant?: "minimal" | "small" | "normal" | "all" | null; variant?: 'minimal' | 'small' | 'normal' | 'all' | null;
} }
const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => { const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const { resume } = props; const { resume } = props;
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const { const { sx, action = '', elevation = 1, variant = 'normal' } = props;
sx,
action = '',
elevation = 1,
variant = "normal"
} = props;
const theme = useTheme(); 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 isAdmin = user?.isAdmin;
const [activeResume, setActiveResume] = useState<Resume>({ ...resume }); const [activeResume, setActiveResume] = useState<Resume>({ ...resume });
const [isContentExpanded, setIsContentExpanded] = useState(false); const [isContentExpanded, setIsContentExpanded] = useState(false);
@ -82,9 +77,12 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
const [editContent, setEditContent] = useState<string>(''); const [editContent, setEditContent] = useState<string>('');
const [saving, setSaving] = useState<boolean>(false); const [saving, setSaving] = useState<boolean>(false);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const [tabValue, setTabValue] = useState("markdown"); const [tabValue, setTabValue] = useState('markdown');
const printContentRef = useRef<HTMLDivElement>(null); const printContentRef = useRef<HTMLDivElement>(null);
const reactToPrintFn = useReactToPrint({ contentRef: printContentRef, pageStyle: '@page { margin: 10px; }' }); const reactToPrintFn = useReactToPrint({
contentRef: printContentRef,
pageStyle: '@page { margin: 10px; }',
});
useEffect(() => { useEffect(() => {
if (resume && resume.id !== activeResume?.id) { if (resume && resume.id !== activeResume?.id) {
@ -120,7 +118,11 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
setSaving(true); setSaving(true);
try { 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() }; const updatedResume = {
...activeResume,
resume: editContent,
updatedAt: new Date(),
};
setActiveResume(updatedResume); setActiveResume(updatedResume);
setSnack('Resume updated successfully.'); setSnack('Resume updated successfully.');
} catch (error) { } catch (error) {
@ -146,12 +148,12 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
day: 'numeric', day: 'numeric',
year: 'numeric', year: 'numeric',
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit',
}).format(date); }).format(date);
}; };
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
if (newValue === "print") { if (newValue === 'print') {
reactToPrintFn(); reactToPrintFn();
return; return;
} }
@ -161,28 +163,42 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
borderColor: 'transparent', borderColor: 'transparent',
borderWidth: 2, borderWidth: 2,
borderStyle: 'solid', borderStyle: 'solid',
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
flexDirection: "column", flexDirection: 'column',
minWidth: 0, minWidth: 0,
opacity: deleted ? 0.5 : 1.0, opacity: deleted ? 0.5 : 1.0,
backgroundColor: deleted ? theme.palette.action.disabledBackground : theme.palette.background.paper, backgroundColor: deleted
pointerEvents: deleted ? "none" : "auto", ? theme.palette.action.disabledBackground
: theme.palette.background.paper,
pointerEvents: deleted ? 'none' : 'auto',
...sx, ...sx,
}} }}
> >
<Box sx={{ display: "flex", flexGrow: 1, p: 1, pb: 0, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}> <Box
sx={{
display: 'flex',
flexGrow: 1,
p: 1,
pb: 0,
height: '100%',
flexDirection: 'column',
alignItems: 'stretch',
position: 'relative',
}}
>
{/* Header Information */} {/* Header Information */}
<Box sx={{ <Box
display: "flex", sx={{
flexDirection: isMobile ? "column" : "row", display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
gap: 2, gap: 2,
mb: 2 mb: 2,
}}> }}
>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Stack spacing={1}> <Stack spacing={1}>
@ -200,7 +216,14 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{activeResume.job && ( {activeResume.job && (
<> <>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}> <Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mt: 1,
}}
>
<WorkIcon color="primary" fontSize="small" /> <WorkIcon color="primary" fontSize="small" />
<Typography variant="subtitle2" fontWeight="bold"> <Typography variant="subtitle2" fontWeight="bold">
Job Job
@ -240,7 +263,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{/* Resume Content */} {/* Resume Content */}
{activeResume.resume && ( {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 <CardHeader
title="Resume Content" title="Resume Content"
avatar={<DescriptionIcon color="success" />} avatar={<DescriptionIcon color="success" />}
@ -263,12 +286,18 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
component="div" component="div"
sx={{ sx={{
display: '-webkit-box', display: '-webkit-box',
WebkitLineClamp: isContentExpanded ? 'unset' : (variant === "small" ? 5 : variant === "minimal" ? 3 : 10), WebkitLineClamp: isContentExpanded
? 'unset'
: variant === 'small'
? 5
: variant === 'minimal'
? 3
: 10,
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
lineHeight: 1.6, lineHeight: 1.6,
fontSize: "0.875rem !important", fontSize: '0.875rem !important',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
fontFamily: 'monospace', fontFamily: 'monospace',
backgroundColor: theme.palette.action.hover, backgroundColor: theme.palette.action.hover,
@ -280,7 +309,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{activeResume.resume} {activeResume.resume}
</Typography> </Typography>
{shouldShowMoreButton && variant !== "all" && ( {shouldShowMoreButton && variant !== 'all' && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}> <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
<Button <Button
variant="text" variant="text"
@ -289,7 +318,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
startIcon={isContentExpanded ? <VisibilityOffIcon /> : <VisibilityIcon />} startIcon={isContentExpanded ? <VisibilityOffIcon /> : <VisibilityIcon />}
sx={{ fontSize: '0.75rem' }} sx={{ fontSize: '0.75rem' }}
> >
{isContentExpanded ? "Show Less" : "Show More"} {isContentExpanded ? 'Show Less' : 'Show More'}
</Button> </Button>
</Box> </Box>
)} )}
@ -303,17 +332,29 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<StyledMarkdown content={activeResume.resume} /> <StyledMarkdown content={activeResume.resume} />
</Box> </Box>
)} )}
</Box> </Box>
{/* Admin Controls */} {/* Admin Controls */}
{isAdmin && ( {isAdmin && (
<Box sx={{ display: "flex", flexDirection: "column", p: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', p: 1 }}>
<Box sx={{ display: "flex", flexDirection: "row", pl: 1, pr: 1, gap: 1, alignContent: "center", height: "32px" }}> <Box
sx={{
display: 'flex',
flexDirection: 'row',
pl: 1,
pr: 1,
gap: 1,
alignContent: 'center',
height: '32px',
}}
>
<Tooltip title="Edit Resume"> <Tooltip title="Edit Resume">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { e.stopPropagation(); handleEditOpen(); }} onClick={e => {
e.stopPropagation();
handleEditOpen();
}}
> >
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
@ -322,7 +363,10 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<Tooltip title="Delete Resume"> <Tooltip title="Delete Resume">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { e.stopPropagation(); deleteResume(activeResume.id); }} onClick={e => {
e.stopPropagation();
deleteResume(activeResume.id);
}}
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
@ -331,7 +375,10 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<Tooltip title="Reset Resume"> <Tooltip title="Reset Resume">
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { e.stopPropagation(); handleReset(); }} onClick={e => {
e.stopPropagation();
handleReset();
}}
> >
<RestoreIcon /> <RestoreIcon />
</IconButton> </IconButton>
@ -352,7 +399,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{/* Print Dialog */} {/* Print Dialog */}
<Dialog <Dialog
open={printDialogOpen} open={printDialogOpen}
onClose={() => { }}//setPrintDialogOpen(false)} onClose={() => {}} //setPrintDialogOpen(false)}
maxWidth="lg" maxWidth="lg"
fullWidth fullWidth
fullScreen={true} fullScreen={true}
@ -361,14 +408,15 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
content={activeResume.resume} content={activeResume.resume}
sx={{ sx={{
p: 2, p: 2,
position: "relative", position: 'relative',
maxHeight: "100%", maxHeight: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
flex: 1, /* Take remaining space in some-container */ flex: 1 /* Take remaining space in some-container */,
overflowY: "auto", /* Scroll if content overflows */ overflowY: 'auto' /* Scroll if content overflows */,
}} /> }}
/>
</Dialog> </Dialog>
{/* Edit Dialog */} {/* Edit Dialog */}
@ -383,90 +431,104 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<DialogTitle> <DialogTitle>
Edit Resume Content Edit Resume Content
<Typography variant="caption" display="block" color="text.secondary"> <Typography variant="caption" display="block" color="text.secondary">
Resume for {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>
<Typography variant="caption" display="block" color="text.secondary"> <Typography variant="caption" display="block" color="text.secondary">
Resume ID: # {activeResume.id} Resume ID: # {activeResume.id}
</Typography> </Typography>
<Typography variant="caption" display="block" color="text.secondary"> <Typography variant="caption" display="block" color="text.secondary">
Last saved: {activeResume.updatedAt ? new Date(activeResume.updatedAt).toLocaleString() : 'N/A'} Last saved:{' '}
{activeResume.updatedAt ? new Date(activeResume.updatedAt).toLocaleString() : 'N/A'}
</Typography> </Typography>
</DialogTitle> </DialogTitle>
<DialogContent sx={{ position: "relative", display: "flex", flexDirection: "column", height: "100%" }}> <DialogContent
sx={{
position: 'relative',
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<Tabs value={tabValue} onChange={handleTabChange} centered> <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="preview" icon={<PreviewIcon />} label="Preview" />
<Tab value="job" icon={<WorkIcon />} label="Job" /> <Tab value="job" icon={<WorkIcon />} label="Job" />
<Tab value="print" icon={<PrintIcon />} label="Print" /> <Tab value="print" icon={<PrintIcon />} label="Print" />
</Tabs> </Tabs>
<Box ref={printContentRef} sx={{ <Box
display: "flex", flexDirection: "column", ref={printContentRef}
height: "100%", /* Restrict to main-container's height */ sx={{
width: "100%", display: 'flex',
minHeight: 0,/* Prevent flex overflow */ flexDirection: 'column',
height: '100%' /* Restrict to main-container's height */,
width: '100%',
minHeight: 0 /* Prevent flex overflow */,
//maxHeight: "min-content", //maxHeight: "min-content",
"& > *:not(.Scrollable)": { '& > *:not(.Scrollable)': {
flexShrink: 0, /* Prevent shrinking */ flexShrink: 0 /* Prevent shrinking */,
}, },
position: "relative", position: 'relative',
}}> }}
>
{tabValue === "markdown" && {tabValue === 'markdown' && (
<BackstoryTextField <BackstoryTextField
value={editContent} value={editContent}
onChange={(value) => setEditContent(value)} onChange={value => setEditContent(value)}
style={{ style={{
position: "relative", position: 'relative',
// maxHeight: "100%", // maxHeight: "100%",
height: "100%", height: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
minHeight: "100%", minHeight: '100%',
flexGrow: 1, flexGrow: 1,
flex: 1, /* Take remaining space in some-container */ flex: 1 /* Take remaining space in some-container */,
overflowY: "auto", /* Scroll if content overflows */ overflowY: 'auto' /* Scroll if content overflows */,
}} }}
placeholder="Enter resume content..." placeholder="Enter resume content..."
/> />
} )}
{tabValue === "preview" && <> {tabValue === 'preview' && (
<>
<StyledMarkdown <StyledMarkdown
sx={{ sx={{
p: 2, p: 2,
position: "relative", position: 'relative',
maxHeight: "100%", maxHeight: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
flex: 1, /* Take remaining space in some-container */ flex: 1 /* Take remaining space in some-container */,
overflowY: "auto", /* Scroll if content overflows */ overflowY: 'auto' /* Scroll if content overflows */,
}} }}
content={editContent}
content={editContent} /> />
<Box sx={{ pb: 2 }}></Box></> <Box sx={{ pb: 2 }}></Box>
} </>
{tabValue === "job" && activeResume.job && <JobInfo )}
{tabValue === 'job' && activeResume.job && (
<JobInfo
variant="all" variant="all"
job={activeResume.job} job={activeResume.job}
sx={{ sx={{
p: 2, p: 2,
position: "relative", position: 'relative',
maxHeight: "100%", maxHeight: '100%',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexGrow: 1, flexGrow: 1,
flex: 1, /* Take remaining space in some-container */ flex: 1 /* Take remaining space in some-container */,
overflowY: "auto", /* Scroll if content overflows */ overflowY: 'auto' /* Scroll if content overflows */,
}} }}
/>} />
)}
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setEditDialogOpen(false)}> <Button onClick={() => setEditDialogOpen(false)}>Cancel</Button>
Cancel
</Button>
<Button <Button
onClick={handleSave} onClick={handleSave}
variant="contained" variant="contained"

View File

@ -22,7 +22,7 @@ import {
useTheme, useTheme,
Slide, Slide,
TextField, TextField,
InputAdornment InputAdornment,
} from '@mui/material'; } from '@mui/material';
import { import {
KeyboardArrowUp as ArrowUpIcon, KeyboardArrowUp as ArrowUpIcon,
@ -34,7 +34,7 @@ import {
Close as CloseIcon, Close as CloseIcon,
ArrowBack as ArrowBackIcon, ArrowBack as ArrowBackIcon,
Search as SearchIcon, Search as SearchIcon,
Clear as ClearIcon Clear as ClearIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { TransitionProps } from '@mui/material/transitions'; import { TransitionProps } from '@mui/material/transitions';
import { ResumeInfo } from 'components/ui/ResumeInfo'; import { ResumeInfo } from 'components/ui/ResumeInfo';
@ -56,7 +56,7 @@ const Transition = React.forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
children: React.ReactElement; children: React.ReactElement;
}, },
ref: React.Ref<unknown>, ref: React.Ref<unknown>
) { ) {
return <Slide direction="up" ref={ref} {...props} />; return <Slide direction="up" ref={ref} {...props} />;
}); });
@ -113,8 +113,8 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
onSelect?.(firstResume); onSelect?.(firstResume);
} }
} catch (err) { } catch (err) {
console.error("Failed to load resumes:", err); console.error('Failed to load resumes:', err);
setSnack("Failed to load resumes: " + err, 'error'); setSnack('Failed to load resumes: ' + err, 'error');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -128,7 +128,8 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
if (!searchQuery.trim()) { if (!searchQuery.trim()) {
setFilteredResumes(resumes); setFilteredResumes(resumes);
} else { } else {
const filtered = resumes.filter(resume => const filtered = resumes.filter(
resume =>
resume.candidate?.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) || resume.candidate?.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
resume.job?.title?.toLowerCase().includes(searchQuery.toLowerCase()) || resume.job?.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
resume.job?.company?.toLowerCase().includes(searchQuery.toLowerCase()) || resume.job?.company?.toLowerCase().includes(searchQuery.toLowerCase()) ||
@ -206,13 +207,17 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
...(isMobile ? {} : { year: 'numeric' }), ...(isMobile ? {} : { year: 'numeric' }),
...(isSmall ? {} : { hour: '2-digit', minute: '2-digit' }) ...(isSmall ? {} : { hour: '2-digit', minute: '2-digit' }),
}).format(date); }).format(date);
}; };
const getSortIcon = (field: SortField) => { const getSortIcon = (field: SortField) => {
if (sortField !== field) return null; if (sortField !== field) return null;
return sortOrder === 'asc' ? <ArrowUpIcon fontSize="small" /> : <ArrowDownIcon fontSize="small" />; return sortOrder === 'asc' ? (
<ArrowUpIcon fontSize="small" />
) : (
<ArrowDownIcon fontSize="small" />
);
}; };
const getDisplayTitle = () => { const getDisplayTitle = () => {
@ -227,33 +232,44 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
...(isMobile ? { ...(isMobile
? {
width: '100%', width: '100%',
boxShadow: 'none', boxShadow: 'none',
backgroundColor: 'transparent' backgroundColor: 'transparent',
} : { width: '50%' }) }
: { width: '50%' }),
}} }}
> >
<Box sx={{ <Box
sx={{
p: isMobile ? 0.5 : 1, p: isMobile ? 0.5 : 1,
borderBottom: 1, borderBottom: 1,
borderColor: 'divider', borderColor: 'divider',
backgroundColor: isMobile ? 'background.paper' : 'inherit' backgroundColor: isMobile ? 'background.paper' : 'inherit',
}}> }}
>
<Typography <Typography
variant={isSmall ? "subtitle2" : isMobile ? "subtitle1" : "h6"} variant={isSmall ? 'subtitle2' : isMobile ? 'subtitle1' : 'h6'}
gutterBottom gutterBottom
sx={{ mb: isMobile ? 0.5 : 1, fontWeight: 600 }} sx={{ mb: isMobile ? 0.5 : 1, fontWeight: 600 }}
> >
{getDisplayTitle()} ({sortedResumes.length}) {getDisplayTitle()} ({sortedResumes.length})
</Typography> </Typography>
<Box sx={{ display: 'flex', gap: 1, flexDirection: isSmall ? 'column' : 'row', alignItems: isSmall ? 'stretch' : 'center' }}> <Box
sx={{
display: 'flex',
gap: 1,
flexDirection: isSmall ? 'column' : 'row',
alignItems: isSmall ? 'stretch' : 'center',
}}
>
<TextField <TextField
size="small" size="small"
placeholder="Search resumes..." placeholder="Search resumes..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={e => setSearchQuery(e.target.value)}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
@ -266,7 +282,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
<ClearIcon fontSize="small" /> <ClearIcon fontSize="small" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
) ),
}} }}
sx={{ flexGrow: 1, minWidth: isSmall ? '100%' : 200 }} sx={{ flexGrow: 1, minWidth: isSmall ? '100%' : 200 }}
/> />
@ -276,7 +292,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
<Select <Select
value={`${sortField}-${sortOrder}`} value={`${sortField}-${sortOrder}`}
label="Sort by" label="Sort by"
onChange={(e) => { onChange={e => {
const [field, order] = e.target.value.split('-') as [SortField, SortOrder]; const [field, order] = e.target.value.split('-') as [SortField, SortOrder];
setSortField(field); setSortField(field);
setSortOrder(order); setSortOrder(order);
@ -295,13 +311,15 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
</Box> </Box>
</Box> </Box>
<TableContainer sx={{ <TableContainer
sx={{
flex: 1, flex: 1,
overflow: 'auto', overflow: 'auto',
'& .MuiTable-root': { '& .MuiTable-root': {
tableLayout: isMobile ? 'fixed' : 'auto' tableLayout: isMobile ? 'fixed' : 'auto',
} },
}}> }}
>
<Table stickyHeader size="small"> <Table stickyHeader size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
@ -312,12 +330,12 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? '35%' : 'auto', width: isMobile ? '35%' : 'auto',
backgroundColor: 'background.paper' backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort('candidateId')} onClick={() => handleSort('candidateId')}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<PersonIcon fontSize={isMobile ? "small" : "medium"} /> <PersonIcon fontSize={isMobile ? 'small' : 'medium'} />
<Typography variant="caption" fontWeight="bold" noWrap> <Typography variant="caption" fontWeight="bold" noWrap>
{isSmall ? 'Candidate' : 'Candidate'} {isSmall ? 'Candidate' : 'Candidate'}
</Typography> </Typography>
@ -331,13 +349,15 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? '35%' : 'auto', width: isMobile ? '35%' : 'auto',
backgroundColor: 'background.paper' backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort('jobId')} onClick={() => handleSort('jobId')}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<WorkIcon fontSize={isMobile ? "small" : "medium"} /> <WorkIcon fontSize={isMobile ? 'small' : 'medium'} />
<Typography variant="caption" fontWeight="bold" noWrap>Job</Typography> <Typography variant="caption" fontWeight="bold" noWrap>
Job
</Typography>
{getSortIcon('jobId')} {getSortIcon('jobId')}
</Box> </Box>
</TableCell> </TableCell>
@ -348,23 +368,27 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
userSelect: 'none', userSelect: 'none',
py: 0.5, py: 0.5,
px: 1, px: 1,
backgroundColor: 'background.paper' backgroundColor: 'background.paper',
}} }}
onClick={() => handleSort('updatedAt')} onClick={() => handleSort('updatedAt')}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<ScheduleIcon fontSize="medium" /> <ScheduleIcon fontSize="medium" />
<Typography variant="caption" fontWeight="bold">Updated</Typography> <Typography variant="caption" fontWeight="bold">
Updated
</Typography>
{getSortIcon('updatedAt')} {getSortIcon('updatedAt')}
</Box> </Box>
</TableCell> </TableCell>
)} )}
<TableCell sx={{ <TableCell
sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
width: isMobile ? '30%' : 'auto', width: isMobile ? '30%' : 'auto',
backgroundColor: 'background.paper' backgroundColor: 'background.paper',
}}> }}
>
<Typography variant="caption" fontWeight="bold" noWrap> <Typography variant="caption" fontWeight="bold" noWrap>
ID ID
</Typography> </Typography>
@ -372,7 +396,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{sortedResumes.map((resume) => ( {sortedResumes.map(resume => (
<TableRow <TableRow
key={resume.id} key={resume.id}
hover hover
@ -386,16 +410,18 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
}, },
'&:hover': { '&:hover': {
backgroundColor: 'action.hover', backgroundColor: 'action.hover',
} },
}} }}
> >
<TableCell sx={{ <TableCell
sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: 'hidden' overflow: 'hidden',
}}> }}
>
<Typography <Typography
variant={isMobile ? "caption" : "body2"} variant={isMobile ? 'caption' : 'body2'}
fontWeight="medium" fontWeight="medium"
noWrap noWrap
sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }} sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }}
@ -413,13 +439,15 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
</Typography> </Typography>
)} )}
</TableCell> </TableCell>
<TableCell sx={{ <TableCell
sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: 'hidden' overflow: 'hidden',
}}> }}
>
<Typography <Typography
variant={isMobile ? "caption" : "body2"} variant={isMobile ? 'caption' : 'body2'}
fontWeight="medium" fontWeight="medium"
noWrap noWrap
sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }} sx={{ fontSize: isMobile ? '0.75rem' : '0.875rem' }}
@ -453,11 +481,13 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
)} )}
</TableCell> </TableCell>
)} )}
<TableCell sx={{ <TableCell
sx={{
py: isMobile ? 0.25 : 0.5, py: isMobile ? 0.25 : 0.5,
px: isMobile ? 0.5 : 1, px: isMobile ? 0.5 : 1,
overflow: 'hidden' overflow: 'hidden',
}}> }}
>
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
@ -476,12 +506,14 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
); );
const ResumeDetails = ({ inDialog = false }: { inDialog?: boolean }) => ( const ResumeDetails = ({ inDialog = false }: { inDialog?: boolean }) => (
<Box sx={{ <Box
sx={{
flex: 1, flex: 1,
overflow: 'auto', overflow: 'auto',
p: inDialog ? 1.5 : 0.75, p: inDialog ? 1.5 : 0.75,
height: inDialog ? '100%' : 'auto' height: inDialog ? '100%' : 'auto',
}}> }}
>
{selectedResume ? ( {selectedResume ? (
<ResumeInfo <ResumeInfo
resume={selectedResume} resume={selectedResume}
@ -491,23 +523,23 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
boxShadow: 'none', boxShadow: 'none',
backgroundColor: 'transparent', backgroundColor: 'transparent',
'& .MuiTypography-h6': { '& .MuiTypography-h6': {
fontSize: inDialog ? '1.25rem' : '1.1rem' fontSize: inDialog ? '1.25rem' : '1.1rem',
} },
}} }}
/> />
) : ( ) : (
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
height: '100%', height: '100%',
color: 'text.secondary', color: 'text.secondary',
textAlign: 'center', textAlign: 'center',
p: 2 p: 2,
}}> }}
<Typography variant="body2"> >
Select a resume to view details <Typography variant="body2">Select a resume to view details</Typography>
</Typography>
</Box> </Box>
)} )}
</Box> </Box>
@ -515,11 +547,13 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
if (isMobile) { if (isMobile) {
return ( return (
<Box sx={{ <Box
sx={{
height: '100%', height: '100%',
p: 0.5, p: 0.5,
backgroundColor: 'background.default' backgroundColor: 'background.default',
}}> }}
>
<ResumeList /> <ResumeList />
<Dialog <Dialog
@ -541,12 +575,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
<ArrowBackIcon /> <ArrowBackIcon />
</IconButton> </IconButton>
<Box sx={{ ml: 1, flex: 1, minWidth: 0 }}> <Box sx={{ ml: 1, flex: 1, minWidth: 0 }}>
<Typography <Typography variant="h6" component="div" noWrap sx={{ fontSize: '1rem' }}>
variant="h6"
component="div"
noWrap
sx={{ fontSize: '1rem' }}
>
Resume Details Resume Details
</Typography> </Typography>
<Typography <Typography
@ -567,27 +596,33 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
} }
return ( return (
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
height: '100%', height: '100%',
gap: 0.75, gap: 0.75,
p: 0.75, p: 0.75,
backgroundColor: 'background.default' backgroundColor: 'background.default',
}}> }}
>
<ResumeList /> <ResumeList />
<Paper sx={{ <Paper
sx={{
width: '50%', width: '50%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
elevation: 1 elevation: 1,
}}> }}
<Box sx={{ >
<Box
sx={{
p: 0.75, p: 0.75,
borderBottom: 1, borderBottom: 1,
borderColor: 'divider', borderColor: 'divider',
backgroundColor: 'background.paper' backgroundColor: 'background.paper',
}}> }}
>
<Typography variant="h6" sx={{ fontSize: '1.1rem', fontWeight: 600 }}> <Typography variant="h6" sx={{ fontSize: '1.1rem', fontWeight: 600 }}>
Resume Details Resume Details
</Typography> </Typography>

View File

@ -30,7 +30,7 @@ const StatusBox = styled(Box)(({ theme }) => ({
})); }));
const StatusIcon = (props: StatusIconProps) => { const StatusIcon = (props: StatusIconProps) => {
const {type} = props; const { type } = props;
switch (type) { switch (type) {
case 'converting': case 'converting':

View File

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

View File

@ -17,8 +17,9 @@ const BackstoryAppAnalysisPage = () => {
Core Concept Core Concept
</Typography> </Typography>
<Typography variant="body1"> <Typography variant="body1">
Backstory is a dual-purpose platform designed to bridge the gap between job candidates and Backstory is a dual-purpose platform designed to bridge the gap between job candidates
employers/recruiters with an AI-powered approach to professional profiles and resume generation. and employers/recruiters with an AI-powered approach to professional profiles and resume
generation.
</Typography> </Typography>
<Typography variant="h3" component="h3" sx={{ mt: 3 }}> <Typography variant="h3" component="h3" sx={{ mt: 3 }}>
@ -27,14 +28,15 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}> <Box component="ol" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Job Candidates</strong> - Upload and manage comprehensive professional histories <strong>Job Candidates</strong> - Upload and manage comprehensive professional
and generate tailored resumes for specific positions histories and generate tailored resumes for specific positions
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Employers/Recruiters</strong> - Search for candidates, directly interact with AI <strong>Employers/Recruiters</strong> - Search for candidates, directly interact
assistants about candidate experiences, and generate position-specific resumes with AI assistants about candidate experiences, and generate position-specific
resumes
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -49,27 +51,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}> <Box component="ul" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Complete Profile Management</strong> - Create detailed professional histories beyond typical resume constraints <strong>Complete Profile Management</strong> - Create detailed professional
histories beyond typical resume constraints
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>AI-Assisted Q&A Setup</strong> - Configure an AI 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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
</Box> </Box>
@ -80,27 +87,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}> <Box component="ul" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Advanced Candidate Search</strong> - Find candidates with specific skills, experience levels, and qualifications <strong>Advanced Candidate Search</strong> - Find candidates with specific skills,
experience levels, and qualifications
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Interactive Q&A</strong> - Ask questions directly to 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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
</Box> </Box>
@ -118,17 +130,20 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}> <Box component="ul" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Public Navigation</strong> - Home, Docs, Pricing, Login/Register accessible to all users <strong>Public Navigation</strong> - Home, Docs, Pricing, Login/Register accessible
to all users
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Candidate Dashboard Navigation</strong> - Profile, Backstory, Resumes, Q&A Setup, Analytics, Settings <strong>Candidate Dashboard Navigation</strong> - Profile, Backstory, Resumes, Q&A
Setup, Analytics, Settings
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Employer Dashboard Navigation</strong> - Dashboard, Search, Saved, Jobs, Company, Analytics, Settings <strong>Employer Dashboard Navigation</strong> - Dashboard, Search, Saved, Jobs,
Company, Analytics, Settings
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -139,32 +154,38 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}> <Box component="ol" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Dashboard Cards</strong> - Both user types have dashboards with card-based information displays <strong>Dashboard Cards</strong> - Both user types have dashboards with card-based
information displays
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Tab-Based Content Organization</strong> - Many screens use horizontal tabs to organize related content <strong>Tab-Based Content Organization</strong> - Many screens use horizontal tabs
to organize related content
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Form-Based Editors</strong> - Profile and content 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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
</Box> </Box>
@ -173,8 +194,8 @@ const BackstoryAppAnalysisPage = () => {
Mobile Adaptations Mobile Adaptations
</Typography> </Typography>
<Typography variant="body1"> <Typography variant="body1">
The mobile designs show a simplified navigation structure with bottom tabs and a hamburger menu, The mobile designs show a simplified navigation structure with bottom tabs and a
maintaining the core functionality while adapting to smaller screens. hamburger menu, maintaining the core functionality while adapting to smaller screens.
</Typography> </Typography>
<Typography variant="h2" component="h2" sx={{ mt: 4 }}> <Typography variant="h2" component="h2" sx={{ mt: 4 }}>
@ -187,22 +208,26 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}> <Box component="ul" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>LLM Integration</strong> - Supports multiple AI models (Claude, GPT-4, self-hosted models) <strong>LLM Integration</strong> - Supports multiple AI models (Claude, GPT-4,
self-hosted models)
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Candidate AI Assistant</strong> - Personalized AI chatbot that answers questions about candidate experience <strong>Candidate AI Assistant</strong> - Personalized AI chatbot that answers
questions about candidate experience
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Resume Generation</strong> - AI-powered resume creation based on job requirements <strong>Resume Generation</strong> - AI-powered resume creation based on job
requirements
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
</Box> </Box>
@ -218,17 +243,20 @@ const BackstoryAppAnalysisPage = () => {
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <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> </Typography>
</li> </li>
</Box> </Box>
@ -239,27 +267,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}> <Box component="ol" sx={{ pl: 4 }}>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Beyond the Resume</strong> - Focuses on comprehensive professional stories rather than just resume highlights <strong>Beyond the Resume</strong> - Focuses on comprehensive professional stories
rather than just resume highlights
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>AI-Mediated Communication</strong> - Uses AI to facilitate deeper understanding of candidate experiences <strong>AI-Mediated Communication</strong> - Uses AI to facilitate deeper
understanding of candidate experiences
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Bidirectional Resume Generation</strong> - Both candidates and employers can generate tailored resumes <strong>Bidirectional Resume Generation</strong> - Both candidates and employers can
generate tailored resumes
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Configurable AI Personalities</strong> - Candidates can customize how their AI assistant responds to questions <strong>Configurable AI Personalities</strong> - Candidates can customize how their
AI assistant responds to questions
</Typography> </Typography>
</li> </li>
<li> <li>
<Typography variant="body1" component="div"> <Typography variant="body1" component="div">
<strong>Deep Analytics</strong> - Both candidates and employers receive insights about their engagement <strong>Deep Analytics</strong> - Both candidates and employers receive insights
about their engagement
</Typography> </Typography>
</li> </li>
</Box> </Box>
@ -293,7 +326,9 @@ const BackstoryAppAnalysisPage = () => {
<Typography variant="body1">Role-based access for employer teams</Typography> <Typography variant="body1">Role-based access for employer teams</Typography>
</li> </li>
<li> <li>
<Typography variant="body1">Data management options for compliance requirements</Typography> <Typography variant="body1">
Data management options for compliance requirements
</Typography>
</li> </li>
</Box> </Box>
</Paper> </Paper>
@ -302,7 +337,4 @@ const BackstoryAppAnalysisPage = () => {
); );
}; };
export { BackstoryAppAnalysisPage };
export {
BackstoryAppAnalysisPage
}

View File

@ -8,7 +8,8 @@ const BackstoryThemeVisualizerPage = () => {
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div <div
className="w-20 h-20 rounded-lg shadow-md flex items-center justify-center mb-2" className="w-20 h-20 rounded-lg shadow-md flex items-center justify-center mb-2"
style={{ backgroundColor: color, color: textColor }}> style={{ backgroundColor: color, color: textColor }}
>
{name} {name}
</div> </div>
<span className="text-xs">{color}</span> <span className="text-xs">{color}</span>
@ -19,9 +20,11 @@ const BackstoryThemeVisualizerPage = () => {
<Box sx={{ backgroundColor: 'background.default', minHeight: '100%', py: 4 }}> <Box sx={{ backgroundColor: 'background.default', minHeight: '100%', py: 4 }}>
<Container maxWidth="lg"> <Container maxWidth="lg">
<Paper sx={{ p: 4, boxShadow: 2 }}> <Paper sx={{ p: 4, boxShadow: 2 }}>
<div className="p-8"> <div className="p-8">
<h1 className="text-2xl font-bold mb-6" style={{ color: backstoryTheme.palette.text.primary }}> <h1
className="text-2xl font-bold mb-6"
style={{ color: backstoryTheme.palette.text.primary }}
>
Backstory Theme Visualization Backstory Theme Visualization
</h1> </h1>
@ -30,8 +33,16 @@ const BackstoryThemeVisualizerPage = () => {
Primary Colors Primary Colors
</h2> </h2>
<div className="flex space-x-4"> <div className="flex space-x-4">
{colorSwatch(backstoryTheme.palette.primary.main, 'Primary', backstoryTheme.palette.primary.contrastText)} {colorSwatch(
{colorSwatch(backstoryTheme.palette.secondary.main, 'Secondary', backstoryTheme.palette.secondary.contrastText)} backstoryTheme.palette.primary.main,
'Primary',
backstoryTheme.palette.primary.contrastText
)}
{colorSwatch(
backstoryTheme.palette.secondary.main,
'Secondary',
backstoryTheme.palette.secondary.contrastText
)}
{colorSwatch(backstoryTheme.palette.custom.highlight, 'Highlight', '#fff')} {colorSwatch(backstoryTheme.palette.custom.highlight, 'Highlight', '#fff')}
</div> </div>
</div> </div>
@ -56,30 +67,40 @@ const BackstoryThemeVisualizerPage = () => {
</div> </div>
</div> </div>
<div className="mb-8 border p-6 rounded-lg" style={{ backgroundColor: backstoryTheme.palette.background.paper }}> <div
className="mb-8 border p-6 rounded-lg"
style={{
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 Typography Examples
</h2> </h2>
<div className="mb-4"> <div className="mb-4">
<h1 style={{ <h1
style={{
fontFamily: backstoryTheme.typography.fontFamily, fontFamily: backstoryTheme.typography.fontFamily,
fontSize: backstoryTheme.typography.h1.fontSize, fontSize: backstoryTheme.typography.h1.fontSize,
fontWeight: backstoryTheme.typography.h1.fontWeight, fontWeight: backstoryTheme.typography.h1.fontWeight,
color: backstoryTheme.typography.h1.color, color: backstoryTheme.typography.h1.color,
}}> }}
>
Heading 1 - Backstory Application Heading 1 - Backstory Application
</h1> </h1>
</div> </div>
<div className="mb-4"> <div className="mb-4">
<p style={{ <p
style={{
fontFamily: backstoryTheme.typography.fontFamily, fontFamily: backstoryTheme.typography.fontFamily,
fontSize: backstoryTheme.typography.body1.fontSize, fontSize: backstoryTheme.typography.body1.fontSize,
color: backstoryTheme.typography.body1.color, 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> </p>
</div> </div>
@ -98,14 +119,29 @@ const BackstoryThemeVisualizerPage = () => {
UI Component Examples UI Component Examples
</h2> </h2>
<div className="p-4 mb-4 rounded-lg" style={{ backgroundColor: backstoryTheme.palette.background.paper }}> <div
<div className="p-2 mb-4 rounded" style={{ backgroundColor: backstoryTheme.palette.primary.main }}> className="p-4 mb-4 rounded-lg"
<span style={{ color: backstoryTheme.palette.primary.contrastText }}> style={{
backgroundColor: backstoryTheme.palette.background.paper,
}}
>
<div
className="p-2 mb-4 rounded"
style={{
backgroundColor: backstoryTheme.palette.primary.main,
}}
>
<span
style={{
color: backstoryTheme.palette.primary.contrastText,
}}
>
AppBar Background AppBar Background
</span> </span>
</div> </div>
<div style={{ <div
style={{
padding: '8px 16px', padding: '8px 16px',
backgroundColor: backstoryTheme.palette.primary.main, backgroundColor: backstoryTheme.palette.primary.main,
color: backstoryTheme.palette.primary.contrastText, color: backstoryTheme.palette.primary.contrastText,
@ -113,11 +149,14 @@ const BackstoryThemeVisualizerPage = () => {
borderRadius: '4px', borderRadius: '4px',
cursor: 'pointer', cursor: 'pointer',
fontFamily: backstoryTheme.typography.fontFamily, fontFamily: backstoryTheme.typography.fontFamily,
}}> }}
>
Primary Button Primary Button
</div> </div>
<div className="mt-4" style={{ <div
className="mt-4"
style={{
padding: '8px 16px', padding: '8px 16px',
backgroundColor: backstoryTheme.palette.secondary.main, backgroundColor: backstoryTheme.palette.secondary.main,
color: backstoryTheme.palette.secondary.contrastText, color: backstoryTheme.palette.secondary.contrastText,
@ -125,11 +164,14 @@ const BackstoryThemeVisualizerPage = () => {
borderRadius: '4px', borderRadius: '4px',
cursor: 'pointer', cursor: 'pointer',
fontFamily: backstoryTheme.typography.fontFamily, fontFamily: backstoryTheme.typography.fontFamily,
}}> }}
>
Secondary Button Secondary Button
</div> </div>
<div className="mt-4" style={{ <div
className="mt-4"
style={{
padding: '8px 16px', padding: '8px 16px',
backgroundColor: backstoryTheme.palette.action.active, backgroundColor: backstoryTheme.palette.action.active,
color: '#fff', color: '#fff',
@ -137,7 +179,8 @@ const BackstoryThemeVisualizerPage = () => {
borderRadius: '4px', borderRadius: '4px',
cursor: 'pointer', cursor: 'pointer',
fontFamily: backstoryTheme.typography.fontFamily, fontFamily: backstoryTheme.typography.fontFamily,
}}> }}
>
Action Button Action Button
</div> </div>
</div> </div>
@ -150,53 +193,164 @@ const BackstoryThemeVisualizerPage = () => {
<table className="border-collapse"> <table className="border-collapse">
<thead> <thead>
<tr> <tr>
<th className="border p-2 text-left" <th
style={{ backgroundColor: backstoryTheme.palette.background.default, color: backstoryTheme.palette.text.primary }}>Color Name</th> className="border p-2 text-left"
<th className="border p-2 text-left" style={{
style={{ backgroundColor: backstoryTheme.palette.background.default, color: backstoryTheme.palette.text.primary }}>Hex Value</th> backgroundColor: backstoryTheme.palette.background.default,
<th className="border p-2 text-left" color: backstoryTheme.palette.text.primary,
style={{ backgroundColor: backstoryTheme.palette.background.default, color: backstoryTheme.palette.text.primary }}>Description</th> }}
>
Color Name
</th>
<th
className="border p-2 text-left"
style={{
backgroundColor: backstoryTheme.palette.background.default,
color: backstoryTheme.palette.text.primary,
}}
>
Hex Value
</th>
<th
className="border p-2 text-left"
style={{
backgroundColor: backstoryTheme.palette.background.default,
color: backstoryTheme.palette.text.primary,
}}
>
Description
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Primary Main</td> <td
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>{backstoryTheme.palette.primary.main}</td> className="border p-2"
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Midnight Blue - Used for main headers and primary UI elements</td> style={{ color: backstoryTheme.palette.text.primary }}
>
Primary Main
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
{backstoryTheme.palette.primary.main}
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
Midnight Blue - Used for main headers and primary UI elements
</td>
</tr> </tr>
<tr> <tr>
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Primary Contrast</td> <td
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>{backstoryTheme.palette.primary.contrastText}</td> className="border p-2"
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Warm Gray - Text that appears on primary color backgrounds</td> style={{ color: backstoryTheme.palette.text.primary }}
>
Primary Contrast
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
{backstoryTheme.palette.primary.contrastText}
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
Warm Gray - Text that appears on primary color backgrounds
</td>
</tr> </tr>
<tr> <tr>
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Secondary Main</td> <td
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>{backstoryTheme.palette.secondary.main}</td> className="border p-2"
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Dusty Teal - Used for secondary actions and accents</td> style={{ color: backstoryTheme.palette.text.primary }}
>
Secondary Main
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
{backstoryTheme.palette.secondary.main}
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
Dusty Teal - Used for secondary actions and accents
</td>
</tr> </tr>
<tr> <tr>
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Highlight</td> <td
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>{backstoryTheme.palette.custom.highlight}</td> className="border p-2"
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Golden Ochre - Used for highlights, accents, and important actions</td> style={{ color: backstoryTheme.palette.text.primary }}
>
Highlight
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
{backstoryTheme.palette.custom.highlight}
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
Golden Ochre - Used for highlights, accents, and important actions
</td>
</tr> </tr>
<tr> <tr>
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Background Default</td> <td
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>{backstoryTheme.palette.background.default}</td> className="border p-2"
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Warm Gray - Main background color for the application</td> style={{ color: backstoryTheme.palette.text.primary }}
>
Background Default
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
{backstoryTheme.palette.background.default}
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
Warm Gray - Main background color for the application
</td>
</tr> </tr>
<tr> <tr>
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Text Primary</td> <td
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>{backstoryTheme.palette.text.primary}</td> className="border p-2"
<td className="border p-2" style={{ color: backstoryTheme.palette.text.primary }}>Charcoal Black - Primary text color throughout the app</td> style={{ color: backstoryTheme.palette.text.primary }}
>
Text Primary
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
{backstoryTheme.palette.text.primary}
</td>
<td
className="border p-2"
style={{ color: backstoryTheme.palette.text.primary }}
>
Charcoal Black - Primary text color throughout the app
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</Paper></Container></Box> </Paper>
</Container>
</Box>
); );
}; };
export { export { BackstoryThemeVisualizerPage };
BackstoryThemeVisualizerPage
};

View File

@ -7,12 +7,24 @@ const BackstoryUIOverviewPage: React.FC = () => {
return ( return (
<ThemeProvider theme={backstoryTheme}> <ThemeProvider theme={backstoryTheme}>
<CssBaseline /> <CssBaseline />
<Box sx={{ bgcolor: 'background.default', overflow: "hidden", py: 4 }}> <Box sx={{ bgcolor: 'background.default', overflow: 'hidden', py: 4 }}>
<Container maxWidth="lg"> <Container maxWidth="lg">
<Paper sx={{ p: 4, borderRadius: 2, boxShadow: 2 }}> <Paper sx={{ p: 4, borderRadius: 2, boxShadow: 2 }}>
{/* Header */} {/* Header */}
<Box sx={{ p: 3, bgcolor: 'background.paper', borderRadius: 2, mb: 4, boxShadow: 1 }}> <Box
<Typography variant="h4" component="h1" sx={{ fontWeight: 'bold', color: 'primary.main', mb: 1 }}> sx={{
p: 3,
bgcolor: 'background.paper',
borderRadius: 2,
mb: 4,
boxShadow: 1,
}}
>
<Typography
variant="h4"
component="h1"
sx={{ fontWeight: 'bold', color: 'primary.main', mb: 1 }}
>
Backstory UI Architecture Backstory UI Architecture
</Typography> </Typography>
<Typography variant="body1" color="text.secondary"> <Typography variant="body1" color="text.secondary">
@ -22,16 +34,21 @@ const BackstoryUIOverviewPage: React.FC = () => {
{/* User Types */} {/* User Types */}
<Grid container spacing={3} sx={{ mb: 4 }}> <Grid container spacing={3} sx={{ mb: 4 }}>
<Grid size={{xs: 12, md: 6}}> <Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ <Box
sx={{
p: 3, p: 3,
bgcolor: 'rgba(74, 122, 125, 0.1)', bgcolor: 'rgba(74, 122, 125, 0.1)',
borderRadius: 2, borderRadius: 2,
border: '1px solid', border: '1px solid',
borderColor: 'rgba(74, 122, 125, 0.3)', borderColor: 'rgba(74, 122, 125, 0.3)',
height: '100%' height: '100%',
}}> }}
<Typography variant="h6" sx={{ color: 'secondary.main', mb: 2, fontWeight: 'bold' }}> >
<Typography
variant="h6"
sx={{ color: 'secondary.main', mb: 2, fontWeight: 'bold' }}
>
Candidate Experience Candidate Experience
</Typography> </Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
@ -39,10 +56,17 @@ const BackstoryUIOverviewPage: React.FC = () => {
'Create comprehensive professional profiles', 'Create comprehensive professional profiles',
'Configure AI assistant for employer Q&A', 'Configure AI assistant for employer Q&A',
'Generate tailored resumes for specific jobs', 'Generate tailored resumes for specific jobs',
'Track profile engagement metrics' 'Track profile engagement metrics',
].map((item, index) => ( ].map((item, index) => (
<Box 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' }} /> <Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: 'secondary.main',
}}
/>
<Typography variant="body2">{item}</Typography> <Typography variant="body2">{item}</Typography>
</Box> </Box>
))} ))}
@ -50,16 +74,21 @@ const BackstoryUIOverviewPage: React.FC = () => {
</Box> </Box>
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6}}> <Grid size={{ xs: 12, md: 6 }}>
<Box sx={{ <Box
sx={{
p: 3, p: 3,
bgcolor: 'rgba(26, 37, 54, 0.1)', bgcolor: 'rgba(26, 37, 54, 0.1)',
borderRadius: 2, borderRadius: 2,
border: '1px solid', border: '1px solid',
borderColor: 'rgba(26, 37, 54, 0.3)', borderColor: 'rgba(26, 37, 54, 0.3)',
height: '100%' height: '100%',
}}> }}
<Typography variant="h6" sx={{ color: 'primary.main', mb: 2, fontWeight: 'bold' }}> >
<Typography
variant="h6"
sx={{ color: 'primary.main', mb: 2, fontWeight: 'bold' }}
>
Employer Experience Employer Experience
</Typography> </Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
@ -67,10 +96,17 @@ const BackstoryUIOverviewPage: React.FC = () => {
'Search for candidates with specific skills', 'Search for candidates with specific skills',
'Interact with candidate AI assistants', 'Interact with candidate AI assistants',
'Generate position-specific candidate resumes', 'Generate position-specific candidate resumes',
'Manage talent pools and job listings' 'Manage talent pools and job listings',
].map((item, index) => ( ].map((item, index) => (
<Box 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' }} /> <Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: 'primary.main',
}}
/>
<Typography variant="body2">{item}</Typography> <Typography variant="body2">{item}</Typography>
</Box> </Box>
))} ))}
@ -80,21 +116,49 @@ const BackstoryUIOverviewPage: React.FC = () => {
</Grid> </Grid>
{/* UI Components */} {/* UI Components */}
<Box sx={{ p: 3, bgcolor: 'background.paper', borderRadius: 2, mb: 4, boxShadow: 1 }}> <Box
sx={{
p: 3,
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 Key UI Components
</Typography> </Typography>
<Grid container spacing={2}> <Grid container spacing={2}>
{[ {[
{ title: 'Dashboards', description: 'Role-specific dashboards with card-based metrics and action items' }, {
{ title: 'Profile Editors', description: 'Comprehensive forms for managing professional information' }, title: 'Dashboards',
{ title: 'Resume Builder', description: 'AI-powered tools for creating tailored resumes' }, description:
{ title: 'Q&A Interface', description: 'Chat-like interface for employer-candidate AI interaction' }, 'Role-specific dashboards with card-based metrics and action items',
{ title: 'Search & Filters', description: 'Advanced search with multiple filter categories' }, },
{ title: 'Analytics Dashboards', description: 'Visual metrics for tracking engagement and performance' } {
title: 'Profile Editors',
description: 'Comprehensive forms for managing professional information',
},
{
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: 'Search & Filters',
description: 'Advanced search with multiple filter categories',
},
{
title: 'Analytics Dashboards',
description: 'Visual metrics for tracking engagement and performance',
},
].map((component, index) => ( ].map((component, index) => (
<Grid size={{xs: 12, sm: 6, md: 4}} key={index}> <Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<Box sx={{ <Box
sx={{
p: 2, p: 2,
border: '1px solid', border: '1px solid',
borderColor: 'divider', borderColor: 'divider',
@ -105,10 +169,18 @@ const BackstoryUIOverviewPage: React.FC = () => {
bgcolor: 'rgba(212, 160, 23, 0.05)', bgcolor: 'rgba(212, 160, 23, 0.05)',
borderColor: 'action.active', borderColor: 'action.active',
transform: 'translateY(-2px)', transform: 'translateY(-2px)',
boxShadow: 1 boxShadow: 1,
} },
}}> }}
<Typography variant="h6" sx={{ color: 'secondary.main', mb: 1, fontWeight: 'medium' }}> >
<Typography
variant="h6"
sx={{
color: 'secondary.main',
mb: 1,
fontWeight: 'medium',
}}
>
{component.title} {component.title}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
@ -125,43 +197,66 @@ const BackstoryUIOverviewPage: React.FC = () => {
{[ {[
{ {
title: 'Candidate Navigation', title: 'Candidate Navigation',
items: ['Dashboard', 'Profile', 'Backstory', 'Resumes', 'Q&A Setup', 'Analytics', 'Settings'], items: [
'Dashboard',
'Profile',
'Backstory',
'Resumes',
'Q&A Setup',
'Analytics',
'Settings',
],
color: 'secondary.main', color: 'secondary.main',
borderColor: 'secondary.main' borderColor: 'secondary.main',
}, },
{ {
title: 'Employer Navigation', title: 'Employer Navigation',
items: ['Dashboard', 'Search', 'Saved', 'Jobs', 'Company', 'Analytics', 'Settings'], items: [
'Dashboard',
'Search',
'Saved',
'Jobs',
'Company',
'Analytics',
'Settings',
],
color: 'primary.main', color: 'primary.main',
borderColor: 'primary.main' borderColor: 'primary.main',
}, },
{ {
title: 'Public Navigation', title: 'Public Navigation',
items: ['Home', 'Docs', 'Pricing', 'Login', 'Register'], items: ['Home', 'Docs', 'Pricing', 'Login', 'Register'],
color: 'custom.highlight', color: 'custom.highlight',
borderColor: 'custom.highlight' borderColor: 'custom.highlight',
} },
].map((nav, index) => ( ].map((nav, index) => (
<Grid size={{xs:12, md:4}} key={index}> <Grid size={{ xs: 12, md: 4 }} key={index}>
<Box sx={{ <Box
sx={{
p: 3, p: 3,
bgcolor: 'background.paper', bgcolor: 'background.paper',
borderRadius: 2, borderRadius: 2,
boxShadow: 1, boxShadow: 1,
height: '100%' height: '100%',
}}> }}
<Typography variant="h6" sx={{ color: 'text.primary', mb: 2, fontWeight: 'bold' }}> >
<Typography
variant="h6"
sx={{ color: 'text.primary', mb: 2, fontWeight: 'bold' }}
>
{nav.title} {nav.title}
</Typography> </Typography>
<Box sx={{ <Box
sx={{
borderLeft: 3, borderLeft: 3,
borderColor: nav.borderColor, borderColor: nav.borderColor,
pl: 2, pl: 2,
py: 1, py: 1,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: 1.5 gap: 1.5,
}}> }}
>
{nav.items.map((item, idx) => ( {nav.items.map((item, idx) => (
<Typography key={idx} sx={{ color: nav.color, fontWeight: 'medium' }}> <Typography key={idx} sx={{ color: nav.color, fontWeight: 'medium' }}>
{item} {item}
@ -174,13 +269,22 @@ const BackstoryUIOverviewPage: React.FC = () => {
</Grid> </Grid>
{/* Connection Points */} {/* Connection Points */}
<Box sx={{ p: 3, bgcolor: 'background.paper', borderRadius: 2, mb: 4, boxShadow: 1 }}> <Box
sx={{
p: 3,
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 System Connection Points
</Typography> </Typography>
<Box sx={{ position: 'relative', py: 2 }}> <Box sx={{ position: 'relative', py: 2 }}>
{/* Connection line */} {/* Connection line */}
<Box sx={{ <Box
sx={{
position: 'absolute', position: 'absolute',
left: '50%', left: '50%',
top: 0, top: 0,
@ -188,15 +292,16 @@ const BackstoryUIOverviewPage: React.FC = () => {
width: 1, width: 1,
borderColor: 'divider', borderColor: 'divider',
zIndex: 0, zIndex: 0,
borderLeft: "1px solid", borderLeft: '1px solid',
overflow: "hidden", overflow: 'hidden',
}} /> }}
/>
{/* Connection points */} {/* Connection points */}
{[ {[
{ left: 'Candidate Profile', right: 'Employer Search' }, { left: 'Candidate Profile', right: 'Employer Search' },
{ left: 'Q&A Setup', right: 'Q&A Interface' }, { left: 'Q&A Setup', right: 'Q&A Interface' },
{ left: 'Resume Generator', right: 'Job Posts' } { left: 'Resume Generator', right: 'Job Posts' },
].map((connection, index) => ( ].map((connection, index) => (
<Box <Box
key={index} key={index}
@ -206,16 +311,18 @@ const BackstoryUIOverviewPage: React.FC = () => {
mb: index < 2 ? 5 : 0, mb: index < 2 ? 5 : 0,
position: 'relative', position: 'relative',
zIndex: 1, zIndex: 1,
}} }}
> >
<Box sx={{ <Box
sx={{
flex: 1, flex: 1,
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
pr: 3 pr: 3,
}}> }}
<Box sx={{ >
<Box
sx={{
display: 'inline-block', display: 'inline-block',
bgcolor: 'rgba(74, 122, 125, 0.1)', bgcolor: 'rgba(74, 122, 125, 0.1)',
p: 2, p: 2,
@ -223,26 +330,32 @@ const BackstoryUIOverviewPage: React.FC = () => {
color: 'secondary.main', color: 'secondary.main',
fontWeight: 'medium', fontWeight: 'medium',
border: '1px solid', border: '1px solid',
borderColor: 'rgba(74, 122, 125, 0.3)' borderColor: 'rgba(74, 122, 125, 0.3)',
}}> }}
>
{connection.left} {connection.left}
</Box> </Box>
</Box> </Box>
<Box sx={{ <Box
sx={{
width: 16, width: 16,
height: 16, height: 16,
borderRadius: '50%', borderRadius: '50%',
bgcolor: 'custom.highlight', bgcolor: 'custom.highlight',
zIndex: 2, zIndex: 2,
boxShadow: 2, boxShadow: 2,
}} /> }}
/>
<Box sx={{ <Box
sx={{
flex: 1, flex: 1,
pl: 3, pl: 3,
}}> }}
<Box sx={{ >
<Box
sx={{
display: 'inline-block', display: 'inline-block',
bgcolor: 'rgba(26, 37, 54, 0.1)', bgcolor: 'rgba(26, 37, 54, 0.1)',
p: 2, p: 2,
@ -251,7 +364,8 @@ const BackstoryUIOverviewPage: React.FC = () => {
fontWeight: 'medium', fontWeight: 'medium',
border: '1px solid', border: '1px solid',
borderColor: 'rgba(26, 37, 54, 0.3)', borderColor: 'rgba(26, 37, 54, 0.3)',
}}> }}
>
{connection.right} {connection.right}
</Box> </Box>
</Box> </Box>
@ -261,89 +375,144 @@ const BackstoryUIOverviewPage: React.FC = () => {
</Box> </Box>
{/* Mobile Adaptation */} {/* Mobile Adaptation */}
<Box sx={{ p: 3, bgcolor: 'background.paper', borderRadius: 2, boxShadow: 1 }}> <Box
sx={{
p: 3,
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 Mobile Adaptation
</Typography> </Typography>
<Box sx={{ display: 'flex', justifyContent: 'center' }}> <Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Box sx={{ <Box
sx={{
width: 200, width: 200,
height: 400, height: 400,
border: '4px solid', border: '4px solid',
borderColor: 'text.primary', borderColor: 'text.primary',
borderRadius: 5, borderRadius: 5,
p: 1, p: 1,
bgcolor: 'background.default' bgcolor: 'background.default',
}}> }}
<Box sx={{ >
<Box
sx={{
height: '100%', height: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
border: '1px solid', border: '1px solid',
borderColor: 'divider', borderColor: 'divider',
borderRadius: 4, borderRadius: 4,
overflow: 'hidden' overflow: 'hidden',
}}> }}
>
{/* Mobile header */} {/* Mobile header */}
<Box sx={{ <Box
sx={{
bgcolor: 'primary.main', bgcolor: 'primary.main',
color: 'primary.contrastText', color: 'primary.contrastText',
p: 1, p: 1,
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center' alignItems: 'center',
}}> }}
<Typography sx={{ fontWeight: 'bold', fontSize: '0.875rem' }}>BACKSTORY</Typography> >
<Typography sx={{ fontWeight: 'bold', fontSize: '0.875rem' }}>
BACKSTORY
</Typography>
<Box></Box> <Box></Box>
</Box> </Box>
{/* Mobile content */} {/* Mobile content */}
<Box sx={{ <Box
sx={{
flex: 1, flex: 1,
p: 1.5, p: 1.5,
overflow: 'auto', overflow: 'auto',
fontSize: '0.75rem' fontSize: '0.75rem',
}}> }}
<Typography sx={{ mb: 1, fontWeight: 'medium' }}>Welcome back, [Name]!</Typography> >
<Typography sx={{ fontSize: '0.675rem', mb: 2 }}>Profile: 75% complete</Typography> <Typography sx={{ mb: 1, fontWeight: 'medium' }}>
Welcome back, [Name]!
</Typography>
<Typography sx={{ fontSize: '0.675rem', mb: 2 }}>
Profile: 75% complete
</Typography>
<Box sx={{ <Box
sx={{
border: '1px solid', border: '1px solid',
borderColor: 'divider', borderColor: 'divider',
borderRadius: 1, borderRadius: 1,
p: 1.5, p: 1.5,
mb: 2, mb: 2,
bgcolor: 'background.paper' bgcolor: 'background.paper',
}}> }}
<Typography sx={{ fontWeight: 'bold', fontSize: '0.75rem', mb: 0.5 }}>Resume Builder</Typography> >
<Typography
sx={{
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>
<Box sx={{ <Box
sx={{
border: '1px solid', border: '1px solid',
borderColor: 'divider', borderColor: 'divider',
borderRadius: 1, borderRadius: 1,
p: 1.5, p: 1.5,
bgcolor: 'background.paper' bgcolor: 'background.paper',
}}> }}
<Typography sx={{ fontWeight: 'bold', fontSize: '0.75rem', mb: 0.5 }}>Recent Activity</Typography> >
<Typography
sx={{
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' }}> 5 profile views</Typography>
<Typography sx={{ fontSize: '0.675rem' }}> 2 downloads</Typography> <Typography sx={{ fontSize: '0.675rem' }}> 2 downloads</Typography>
</Box> </Box>
</Box> </Box>
{/* Mobile footer */} {/* Mobile footer */}
<Box sx={{ <Box
sx={{
bgcolor: 'background.default', bgcolor: 'background.default',
p: 1, p: 1,
display: 'flex', display: 'flex',
justifyContent: 'space-around', justifyContent: 'space-around',
borderTop: '1px solid', borderTop: '1px solid',
borderColor: 'divider' borderColor: 'divider',
}}> }}
<Typography sx={{ fontWeight: 'bold', fontSize: '0.75rem', color: 'secondary.main' }}>Home</Typography> >
<Typography sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>Profile</Typography> <Typography
<Typography sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>More</Typography> sx={{
fontWeight: 'bold',
fontSize: '0.75rem',
color: 'secondary.main',
}}
>
Home
</Typography>
<Typography sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
Profile
</Typography>
<Typography sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
More
</Typography>
</Box> </Box>
</Box> </Box>
</Box> </Box>
@ -356,6 +525,4 @@ const BackstoryUIOverviewPage: React.FC = () => {
); );
}; };
export { export { BackstoryUIOverviewPage };
BackstoryUIOverviewPage
};

View File

@ -1,15 +1,36 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
AppBar, Box, Button, Chip, Drawer, AppBar,
IconButton, List, ListItem, ListItemButton, ListItemIcon, Box,
ListItemText, Paper, Tab, Tabs, TextField, Typography, Button,
useMediaQuery, useTheme Chip,
Drawer,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Paper,
Tab,
Tabs,
TextField,
Typography,
useMediaQuery,
useTheme,
} from '@mui/material'; } from '@mui/material';
import { import {
Menu as MenuIcon, Search as SearchIcon, Description as FileTextIcon, Menu as MenuIcon,
Person as UserIcon, Settings as SettingsIcon, Add as PlusIcon, Search as SearchIcon,
Edit as EditIcon, Visibility as EyeIcon, Save as SaveIcon, Description as FileTextIcon,
Delete as TrashIcon, AccessTime as ClockIcon Person as UserIcon,
Settings as SettingsIcon,
Add as PlusIcon,
Edit as EditIcon,
Visibility as EyeIcon,
Save as SaveIcon,
Delete as TrashIcon,
AccessTime as ClockIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
interface Resume { interface Resume {
@ -22,37 +43,74 @@ interface Resume {
const MockupPage = () => { const MockupPage = () => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [activeTab, setActiveTab] = useState<string>("resume"); const [activeTab, setActiveTab] = useState<string>('resume');
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState<boolean>(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState<boolean>(false);
const [selectedResume, setSelectedResume] = useState<number | null>(null); const [selectedResume, setSelectedResume] = useState<number | null>(null);
// Mock data // Mock data
const savedResumes: Resume[] = [ const savedResumes: Resume[] = [
{ id: 1, name: "Software Engineer - Tech Co", date: "May 15, 2025", isRecent: true }, {
{ id: 2, name: "Product Manager - StartupX", date: "May 10, 2025", isRecent: false }, id: 1,
{ id: 3, name: "Data Scientist - AI Corp", date: "May 5, 2025", isRecent: false }, name: 'Software Engineer - Tech Co',
date: 'May 15, 2025',
isRecent: true,
},
{
id: 2,
name: 'Product Manager - StartupX',
date: 'May 10, 2025',
isRecent: false,
},
{
id: 3,
name: 'Data Scientist - AI Corp',
date: 'May 5, 2025',
isRecent: false,
},
]; ];
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', bgcolor: 'background.default' }}> <Box
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%',
bgcolor: 'background.default',
}}
>
{/* Header */} {/* Header */}
<AppBar position="static" color="default" elevation={1} sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}> <AppBar
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', px: 2, py: 1 }}> position="static"
color="default"
elevation={1}
sx={{ zIndex: theme => theme.zIndex.drawer + 1 }}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
px: 2,
py: 1,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6" component="h1" fontWeight="bold" color="text.primary">Backstory</Typography> <Typography variant="h6" component="h1" fontWeight="bold" color="text.primary">
Backstory
</Typography>
{isMobile && ( {isMobile && (
<IconButton edge="start" color="inherit" onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}> <IconButton
edge="start"
color="inherit"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
)} )}
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{!isMobile && ( {!isMobile && (
<Button <Button startIcon={<PlusIcon />} color="primary" size="small">
startIcon={<PlusIcon />}
color="primary"
size="small"
>
New Resume New Resume
</Button> </Button>
)} )}
@ -71,12 +129,25 @@ const MockupPage = () => {
sx={{ sx={{
width: 240, width: 240,
flexShrink: 0, flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: 240, boxSizing: 'border-box', position: 'relative' }, [`& .MuiDrawer-paper`]: {
width: 240,
boxSizing: 'border-box',
position: 'relative',
},
}}
>
<Box
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
height: '100%',
}} }}
> >
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography variant="overline" color="text.secondary" gutterBottom>Main</Typography> <Typography variant="overline" color="text.secondary" gutterBottom>
Main
</Typography>
<List disablePadding> <List disablePadding>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton sx={{ borderRadius: 1 }}> <ListItemButton sx={{ borderRadius: 1 }}>
@ -88,7 +159,11 @@ const MockupPage = () => {
</ListItem> </ListItem>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton <ListItemButton
sx={{ borderRadius: 1, bgcolor: 'primary.lighter', color: 'primary.main' }} sx={{
borderRadius: 1,
bgcolor: 'primary.lighter',
color: 'primary.main',
}}
> >
<ListItemIcon sx={{ minWidth: 36, color: 'primary.main' }}> <ListItemIcon sx={{ minWidth: 36, color: 'primary.main' }}>
<FileTextIcon fontSize="small" /> <FileTextIcon fontSize="small" />
@ -100,7 +175,9 @@ const MockupPage = () => {
</Box> </Box>
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography variant="overline" color="text.secondary" gutterBottom>My Content</Typography> <Typography variant="overline" color="text.secondary" gutterBottom>
My Content
</Typography>
<List disablePadding> <List disablePadding>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton sx={{ borderRadius: 1 }}> <ListItemButton sx={{ borderRadius: 1 }}>
@ -128,14 +205,20 @@ const MockupPage = () => {
<Box sx={{ mt: 'auto' }}> <Box sx={{ mt: 'auto' }}>
<Paper variant="outlined" sx={{ p: 2, bgcolor: 'background.default' }}> <Paper variant="outlined" sx={{ p: 2, bgcolor: 'background.default' }}>
<Typography variant="subtitle2" color="text.primary" gutterBottom>Recent Activity</Typography> <Typography variant="subtitle2" color="text.primary" gutterBottom>
Recent Activity
</Typography>
<List dense disablePadding> <List dense disablePadding>
{savedResumes.filter(r => r.isRecent).map(resume => ( {savedResumes
.filter(r => r.isRecent)
.map(resume => (
<ListItem key={resume.id} disablePadding sx={{ mb: 0.5 }}> <ListItem key={resume.id} disablePadding sx={{ mb: 0.5 }}>
<ListItemIcon sx={{ minWidth: 24 }}> <ListItemIcon sx={{ minWidth: 24 }}>
<ClockIcon fontSize="small" /> <ClockIcon fontSize="small" />
</ListItemIcon> </ListItemIcon>
<Typography variant="body2" noWrap>{resume.name}</Typography> <Typography variant="body2" noWrap>
{resume.name}
</Typography>
</ListItem> </ListItem>
))} ))}
</List> </List>
@ -152,12 +235,21 @@ const MockupPage = () => {
onClose={() => setIsMobileMenuOpen(false)} onClose={() => setIsMobileMenuOpen(false)}
sx={{ sx={{
display: { xs: 'block', md: 'none' }, display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': { width: 240 } '& .MuiDrawer-paper': { width: 240 },
}}
>
<Box
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
height: '100%',
}} }}
> >
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography variant="overline" color="text.secondary" gutterBottom>Main</Typography> <Typography variant="overline" color="text.secondary" gutterBottom>
Main
</Typography>
<List disablePadding> <List disablePadding>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton onClick={() => setIsMobileMenuOpen(false)}> <ListItemButton onClick={() => setIsMobileMenuOpen(false)}>
@ -182,7 +274,9 @@ const MockupPage = () => {
</Box> </Box>
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
<Typography variant="overline" color="text.secondary" gutterBottom>My Content</Typography> <Typography variant="overline" color="text.secondary" gutterBottom>
My Content
</Typography>
<List disablePadding> <List disablePadding>
<ListItem disablePadding> <ListItem disablePadding>
<ListItemButton onClick={() => setIsMobileMenuOpen(false)}> <ListItemButton onClick={() => setIsMobileMenuOpen(false)}>
@ -214,18 +308,21 @@ const MockupPage = () => {
<Box sx={{ flex: 1, overflow: 'auto', p: 3 }}> <Box sx={{ flex: 1, overflow: 'auto', p: 3 }}>
{/* Resume Builder content */} {/* Resume Builder content */}
<Box sx={{ mb: 4 }}> <Box sx={{ mb: 4 }}>
<Typography variant="h5" component="h2" fontWeight="bold" gutterBottom>Resume Builder</Typography> <Typography variant="h5" component="h2" fontWeight="bold" gutterBottom>
<Typography variant="body2" color="text.secondary">Generate and customize resumes based on job descriptions</Typography> Resume Builder
</Typography>
<Typography variant="body2" color="text.secondary">
Generate and customize resumes based on job descriptions
</Typography>
</Box> </Box>
{/* Tabs */} {/* Tabs */}
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}> <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs <Tabs
value={activeTab} value={activeTab}
onChange={(_, newValue) => setActiveTab(newValue)} onChange={(_, newValue) => setActiveTab(newValue)}
aria-label="Resume builder tabs" aria-label="Resume builder tabs"
variant={isMobile ? "scrollable" : "standard"} variant={isMobile ? 'scrollable' : 'standard'}
scrollButtons={isMobile ? "auto" : undefined} scrollButtons={isMobile ? 'auto' : undefined}
> >
<Tab label="Job Description" value="job" /> <Tab label="Job Description" value="job" />
<Tab label="Resume" value="resume" /> <Tab label="Resume" value="resume" />
@ -233,11 +330,12 @@ const MockupPage = () => {
<Tab label="Saved Resumes" value="saved" /> <Tab label="Saved Resumes" value="saved" />
</Tabs> </Tabs>
</Box> </Box>
{/* Tab content */} {/* Tab content */}
{activeTab === 'job' && ( {activeTab === 'job' && (
<Paper variant="outlined" sx={{ p: 3 }}> <Paper variant="outlined" sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>Job Description</Typography> <Typography variant="h6" gutterBottom>
Job Description
</Typography>
<TextField <TextField
fullWidth fullWidth
multiline multiline
@ -252,24 +350,22 @@ const MockupPage = () => {
</Box> </Box>
</Paper> </Paper>
)} )}
{activeTab === 'resume' && ( {activeTab === 'resume' && (
<Paper variant="outlined" sx={{ p: 3 }}> <Paper variant="outlined" sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}> <Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 3,
}}
>
<Typography variant="h6">Resume Editor</Typography> <Typography variant="h6">Resume Editor</Typography>
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<Button <Button variant="outlined" size="small" startIcon={<SaveIcon />}>
variant="outlined"
size="small"
startIcon={<SaveIcon />}
>
Save Save
</Button> </Button>
<Button <Button variant="outlined" size="small" startIcon={<EyeIcon />}>
variant="outlined"
size="small"
startIcon={<EyeIcon />}
>
Preview Preview
</Button> </Button>
</Box> </Box>
@ -279,21 +375,46 @@ const MockupPage = () => {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Contact information */} {/* Contact information */}
<Paper variant="outlined" sx={{ p: 2 }}> <Paper variant="outlined" sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box
<Typography variant="subtitle1" fontWeight="medium">Contact Information</Typography> sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
}}
>
<Typography variant="subtitle1" fontWeight="medium">
Contact Information
</Typography>
<IconButton size="small" color="default"> <IconButton size="small" color="default">
<EditIcon fontSize="small" /> <EditIcon fontSize="small" />
</IconButton> </IconButton>
</Box> </Box>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 2 }}> <Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
gap: 2,
}}
>
<Box> <Box>
<Typography variant="caption" color="text.secondary" display="block" gutterBottom> <Typography
variant="caption"
color="text.secondary"
display="block"
gutterBottom
>
Full Name Full Name
</Typography> </Typography>
<TextField size="small" fullWidth defaultValue="John Doe" /> <TextField size="small" fullWidth defaultValue="John Doe" />
</Box> </Box>
<Box> <Box>
<Typography variant="caption" color="text.secondary" display="block" gutterBottom> <Typography
variant="caption"
color="text.secondary"
display="block"
gutterBottom
>
Email Email
</Typography> </Typography>
<TextField size="small" fullWidth defaultValue="john@example.com" /> <TextField size="small" fullWidth defaultValue="john@example.com" />
@ -303,8 +424,17 @@ const MockupPage = () => {
{/* Professional Summary */} {/* Professional Summary */}
<Paper variant="outlined" sx={{ p: 2 }}> <Paper variant="outlined" sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box
<Typography variant="subtitle1" fontWeight="medium">Professional Summary</Typography> sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
}}
>
<Typography variant="subtitle1" fontWeight="medium">
Professional Summary
</Typography>
<IconButton size="small" color="default"> <IconButton size="small" color="default">
<EditIcon fontSize="small" /> <EditIcon fontSize="small" />
</IconButton> </IconButton>
@ -320,13 +450,18 @@ const MockupPage = () => {
{/* Work Experience */} {/* Work Experience */}
<Paper variant="outlined" sx={{ p: 2 }}> <Paper variant="outlined" sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box
<Typography variant="subtitle1" fontWeight="medium">Work Experience</Typography> sx={{
<Button display: 'flex',
startIcon={<PlusIcon />} justifyContent: 'space-between',
color="primary" alignItems: 'center',
size="small" mb: 2,
}}
> >
<Typography variant="subtitle1" fontWeight="medium">
Work Experience
</Typography>
<Button startIcon={<PlusIcon />} color="primary" size="small">
Add Position Add Position
</Button> </Button>
</Box> </Box>
@ -334,7 +469,9 @@ const MockupPage = () => {
{/* Job entry */} {/* Job entry */}
<Paper variant="outlined" sx={{ p: 2, mb: 2, bgcolor: 'background.default' }}> <Paper variant="outlined" sx={{ p: 2, mb: 2, bgcolor: 'background.default' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="subtitle2" fontWeight="medium">Senior Developer</Typography> <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"> <IconButton size="small">
<EditIcon fontSize="small" /> <EditIcon fontSize="small" />
@ -344,10 +481,16 @@ const MockupPage = () => {
</IconButton> </IconButton>
</Box> </Box>
</Box> </Box>
<Typography variant="body2" color="text.secondary">Tech Company Inc. 2020-Present</Typography> <Typography variant="body2" color="text.secondary">
Tech Company Inc. 2020-Present
</Typography>
<Box component="ul" sx={{ pl: 2, mt: 1 }}> <Box component="ul" sx={{ pl: 2, mt: 1 }}>
<Typography component="li" variant="body2">Led development of company's flagship product</Typography> <Typography component="li" variant="body2">
<Typography component="li" variant="body2">Improved performance by 40% through code optimization</Typography> Led development of company's flagship product
</Typography>
<Typography component="li" variant="body2">
Improved performance by 40% through code optimization
</Typography>
</Box> </Box>
</Paper> </Paper>
</Paper> </Paper>
@ -360,7 +503,7 @@ const MockupPage = () => {
borderStyle: 'dashed', borderStyle: 'dashed',
p: 1.5, p: 1.5,
color: 'text.secondary', color: 'text.secondary',
'&:hover': { bgcolor: 'background.default' } '&:hover': { bgcolor: 'background.default' },
}} }}
startIcon={<PlusIcon />} startIcon={<PlusIcon />}
> >
@ -368,16 +511,19 @@ const MockupPage = () => {
</Button> </Button>
</Box> </Box>
</Paper> </Paper>
)} {activeTab === 'saved' && ( )}{' '}
{activeTab === 'saved' && (
<Paper variant="outlined" sx={{ p: 3 }}> <Paper variant="outlined" sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}> <Box
<Typography variant="h6">Saved Resumes</Typography> sx={{
<Button display: 'flex',
variant="contained" justifyContent: 'space-between',
color="primary" alignItems: 'center',
size="small" mb: 3,
startIcon={<PlusIcon />} }}
> >
<Typography variant="h6">Saved Resumes</Typography>
<Button variant="contained" color="primary" size="small" startIcon={<PlusIcon />}>
New Resume New Resume
</Button> </Button>
</Box> </Box>
@ -394,15 +540,20 @@ const MockupPage = () => {
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
cursor: 'pointer', cursor: 'pointer',
bgcolor: selectedResume === resume.id ? 'primary.lighter' : 'background.paper', bgcolor:
selectedResume === resume.id ? 'primary.lighter' : 'background.paper',
borderColor: selectedResume === resume.id ? 'primary.light' : 'divider', borderColor: selectedResume === resume.id ? 'primary.light' : 'divider',
'&:hover': { bgcolor: selectedResume === resume.id ? 'primary.lighter' : 'action.hover' } '&:hover': {
bgcolor: selectedResume === resume.id ? 'primary.lighter' : 'action.hover',
},
}} }}
onClick={() => setSelectedResume(resume.id)} onClick={() => setSelectedResume(resume.id)}
> >
<Box> <Box>
<Typography variant="subtitle2">{resume.name}</Typography> <Typography variant="subtitle2">{resume.name}</Typography>
<Typography variant="caption" color="text.secondary">Last edited: {resume.date}</Typography> <Typography variant="caption" color="text.secondary">
Last edited: {resume.date}
</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<IconButton size="small"> <IconButton size="small">
@ -417,10 +568,11 @@ const MockupPage = () => {
</Box> </Box>
</Paper> </Paper>
)} )}
{activeTab === 'fact' && ( {activeTab === 'fact' && (
<Paper variant="outlined" sx={{ p: 3 }}> <Paper variant="outlined" sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>Fact Check</Typography> <Typography variant="h6" gutterBottom>
Fact Check
</Typography>
<Typography variant="body2" color="text.secondary" paragraph> <Typography variant="body2" color="text.secondary" paragraph>
This tab shows how your resume content compares to your employment history data. This tab shows how your resume content compares to your employment history data.
</Typography> </Typography>
@ -440,7 +592,8 @@ const MockupPage = () => {
Skills Verification Skills Verification
</Typography> </Typography>
<Typography variant="body2"> <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> </Typography>
</Paper> </Paper>
</Box> </Box>
@ -461,7 +614,7 @@ const MockupPage = () => {
justifyContent: 'space-around', justifyContent: 'space-around',
borderTop: 1, borderTop: 1,
borderColor: 'divider', borderColor: 'divider',
zIndex: 1100 zIndex: 1100,
}} }}
elevation={3} elevation={3}
> >
@ -472,7 +625,7 @@ const MockupPage = () => {
alignItems: 'center', alignItems: 'center',
py: 1, py: 1,
px: 2, px: 2,
color: 'text.secondary' color: 'text.secondary',
}} }}
component="button" component="button"
> >
@ -486,7 +639,7 @@ const MockupPage = () => {
alignItems: 'center', alignItems: 'center',
py: 1, py: 1,
px: 2, px: 2,
color: 'primary.main' color: 'primary.main',
}} }}
component="button" component="button"
> >
@ -500,7 +653,7 @@ const MockupPage = () => {
alignItems: 'center', alignItems: 'center',
py: 1, py: 1,
px: 2, px: 2,
color: 'text.secondary' color: 'text.secondary',
}} }}
component="button" component="button"
> >
@ -511,8 +664,6 @@ const MockupPage = () => {
)} )}
</Box> </Box>
); );
}
export {
MockupPage
}; };
export { MockupPage };

View File

@ -22,7 +22,7 @@ import {
Select, Select,
FormControl, FormControl,
InputLabel, InputLabel,
Grid Grid,
} from '@mui/material'; } from '@mui/material';
import { Person, Business, AssignmentInd } from '@mui/icons-material'; import { Person, Business, AssignmentInd } from '@mui/icons-material';
@ -68,9 +68,9 @@ const mockUsers: User[] = [
lastName: 'Doe', lastName: 'Doe',
skills: [ skills: [
{ id: 's1', name: 'React', level: 'advanced' }, { id: 's1', name: 'React', level: 'advanced' },
{ id: 's2', name: 'TypeScript', level: 'intermediate' } { id: 's2', name: 'TypeScript', level: 'intermediate' },
], ],
location: { city: 'Austin', country: 'USA' } location: { city: 'Austin', country: 'USA' },
}, },
{ {
id: '2', id: '2',
@ -83,9 +83,9 @@ const mockUsers: User[] = [
lastName: 'Smith', lastName: 'Smith',
skills: [ skills: [
{ id: 's3', name: 'Python', level: 'expert' }, { id: 's3', name: 'Python', level: 'expert' },
{ id: 's4', name: 'Data Science', level: 'advanced' } { id: 's4', name: 'Data Science', level: 'advanced' },
], ],
location: { city: 'Seattle', country: 'USA', remote: true } location: { city: 'Seattle', country: 'USA', remote: true },
}, },
{ {
id: '3', id: '3',
@ -97,7 +97,7 @@ const mockUsers: User[] = [
companyName: 'Acme Tech', companyName: 'Acme Tech',
industry: 'Software', industry: 'Software',
companySize: '50-200', companySize: '50-200',
location: { city: 'San Francisco', country: 'USA' } location: { city: 'San Francisco', country: 'USA' },
}, },
{ {
id: '4', id: '4',
@ -109,8 +109,8 @@ const mockUsers: User[] = [
companyName: 'Globex Corporation', companyName: 'Globex Corporation',
industry: 'Manufacturing', industry: 'Manufacturing',
companySize: '1000+', companySize: '1000+',
location: { city: 'Chicago', country: 'USA' } location: { city: 'Chicago', country: 'USA' },
} },
]; ];
// Component for User Management // Component for User Management
@ -200,10 +200,16 @@ const UserManagement: React.FC = () => {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{filteredUsers.map((user) => ( {filteredUsers.map(user => (
<TableRow key={user.id} sx={{ "& > td": { whiteSpace: "nowrap"}}}> <TableRow key={user.id} sx={{ '& > td': { whiteSpace: 'nowrap' } }}>
<TableCell> <TableCell>
<Box sx={{ display: 'flex', alignItems: 'flex-start', flexDirection: "column" }}> <Box
sx={{
display: 'flex',
alignItems: 'flex-start',
flexDirection: 'column',
}}
>
<Typography>{getUserDisplayName(user)}</Typography> <Typography>{getUserDisplayName(user)}</Typography>
</Box> </Box>
</TableCell> </TableCell>
@ -264,7 +270,7 @@ const UserManagement: React.FC = () => {
<DialogContent dividers> <DialogContent dividers>
{selectedUser.type === 'candidate' ? ( {selectedUser.type === 'candidate' ? (
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{xs: 12, md: 6}}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="subtitle1">Personal Information</Typography> <Typography variant="subtitle1">Personal Information</Typography>
<TextField <TextField
label="First Name" label="First Name"
@ -288,10 +294,10 @@ const UserManagement: React.FC = () => {
InputProps={{ readOnly: true }} InputProps={{ readOnly: true }}
/> />
</Grid> </Grid>
<Grid size={{xs: 12, md: 6}}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="subtitle1">Skills</Typography> <Typography variant="subtitle1">Skills</Typography>
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
{selectedUser.skills.map((skill) => ( {selectedUser.skills.map(skill => (
<Chip <Chip
key={skill.id} key={skill.id}
label={`${skill.name} (${skill.level})`} label={`${skill.name} (${skill.level})`}
@ -303,7 +309,7 @@ const UserManagement: React.FC = () => {
</Grid> </Grid>
) : ( ) : (
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{xs: 12, md: 6}}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="subtitle1">Company Information</Typography> <Typography variant="subtitle1">Company Information</Typography>
<TextField <TextField
label="Company Name" label="Company Name"
@ -327,7 +333,7 @@ const UserManagement: React.FC = () => {
InputProps={{ readOnly: true }} InputProps={{ readOnly: true }}
/> />
</Grid> </Grid>
<Grid size={{xs: 12, md: 6}}> <Grid size={{ xs: 12, md: 6 }}>
<Typography variant="subtitle1">Contact Information</Typography> <Typography variant="subtitle1">Contact Information</Typography>
<TextField <TextField
label="Email" label="Email"
@ -358,9 +364,7 @@ const UserManagement: React.FC = () => {
<Dialog open={aiConfigOpen} onClose={handleCloseAiConfig} maxWidth="md" fullWidth> <Dialog open={aiConfigOpen} onClose={handleCloseAiConfig} maxWidth="md" fullWidth>
{selectedUser && ( {selectedUser && (
<> <>
<DialogTitle> <DialogTitle>AI Configuration for {getUserDisplayName(selectedUser)}</DialogTitle>
AI Configuration for {getUserDisplayName(selectedUser)}
</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Typography variant="subtitle1" gutterBottom> <Typography variant="subtitle1" gutterBottom>
RAG Database Configuration RAG Database Configuration
@ -381,11 +385,7 @@ const UserManagement: React.FC = () => {
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel id="vector-store-label">Vector Store</InputLabel> <InputLabel id="vector-store-label">Vector Store</InputLabel>
<Select <Select labelId="vector-store-label" label="Vector Store" defaultValue="pinecone">
labelId="vector-store-label"
label="Vector Store"
defaultValue="pinecone"
>
<MenuItem value="pinecone">Pinecone</MenuItem> <MenuItem value="pinecone">Pinecone</MenuItem>
<MenuItem value="qdrant">Qdrant</MenuItem> <MenuItem value="qdrant">Qdrant</MenuItem>
<MenuItem value="faiss">FAISS</MenuItem> <MenuItem value="faiss">FAISS</MenuItem>
@ -397,21 +397,17 @@ const UserManagement: React.FC = () => {
</Typography> </Typography>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{xs: 12, md: 6}}> <Grid size={{ xs: 12, md: 6 }}>
<FormControl fullWidth margin="normal"> <FormControl fullWidth margin="normal">
<InputLabel id="model-label">AI Model</InputLabel> <InputLabel id="model-label">AI Model</InputLabel>
<Select <Select labelId="model-label" label="AI Model" defaultValue="gpt-4">
labelId="model-label"
label="AI Model"
defaultValue="gpt-4"
>
<MenuItem value="gpt-4">GPT-4</MenuItem> <MenuItem value="gpt-4">GPT-4</MenuItem>
<MenuItem value="claude-3">Claude 3</MenuItem> <MenuItem value="claude-3">Claude 3</MenuItem>
<MenuItem value="custom">Custom</MenuItem> <MenuItem value="custom">Custom</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Grid> </Grid>
<Grid size={{xs: 12, md: 6}}> <Grid size={{ xs: 12, md: 6 }}>
<TextField <TextField
label="Temperature" label="Temperature"
type="number" type="number"
@ -421,7 +417,7 @@ const UserManagement: React.FC = () => {
InputProps={{ inputProps: { min: 0, max: 1, step: 0.1 } }} InputProps={{ inputProps: { min: 0, max: 1, step: 0.1 } }}
/> />
</Grid> </Grid>
<Grid size={{xs: 12, md: 6}}> <Grid size={{ xs: 12, md: 6 }}>
<TextField <TextField
label="Max Tokens" label="Max Tokens"
type="number" type="number"
@ -430,7 +426,7 @@ const UserManagement: React.FC = () => {
margin="normal" margin="normal"
/> />
</Grid> </Grid>
<Grid size={{xs: 12, md: 6}}> <Grid size={{ xs: 12, md: 6 }}>
<TextField <TextField
label="Top P" label="Top P"
type="number" type="number"
@ -448,7 +444,11 @@ const UserManagement: React.FC = () => {
rows={4} rows={4}
fullWidth fullWidth
margin="normal" margin="normal"
defaultValue={`You are an AI assistant helping ${selectedUser.type === 'candidate' ? 'job candidates find relevant positions' : 'employers find qualified candidates'}. Be professional, helpful, and concise in your responses.`} defaultValue={`You are an AI assistant helping ${
selectedUser.type === 'candidate'
? 'job candidates find relevant positions'
: 'employers find qualified candidates'
}. Be professional, helpful, and concise in your responses.`}
/> />
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2 }}> <Typography variant="subtitle1" gutterBottom sx={{ mt: 2 }}>
@ -496,7 +496,9 @@ const UserManagement: React.FC = () => {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleCloseAiConfig}>Cancel</Button> <Button onClick={handleCloseAiConfig}>Cancel</Button>
<Button variant="contained" color="primary">Save Configuration</Button> <Button variant="contained" color="primary">
Save Configuration
</Button>
</DialogActions> </DialogActions>
</> </>
)} )}

View File

@ -2,7 +2,12 @@
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react'; import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
import * as Types from '../types/types'; import * as Types from '../types/types';
import { ApiClient, CreateCandidateRequest, CreateEmployerRequest, GuestConversionRequest } from 'services/api-client'; import {
ApiClient,
CreateCandidateRequest,
CreateEmployerRequest,
GuestConversionRequest,
} from 'services/api-client';
import { formatApiRequest, toCamelCase } from 'types/conversion'; import { formatApiRequest, toCamelCase } from 'types/conversion';
// ============================ // ============================
@ -48,7 +53,7 @@ const TOKEN_STORAGE = {
TOKEN_EXPIRY: 'tokenExpiry', TOKEN_EXPIRY: 'tokenExpiry',
USER_TYPE: 'userType', USER_TYPE: 'userType',
IS_GUEST: 'isGuest', IS_GUEST: 'isGuest',
PENDING_VERIFICATION_EMAIL: 'pendingVerificationEmail' PENDING_VERIFICATION_EMAIL: 'pendingVerificationEmail',
} as const; } as const;
// ============================ // ============================
@ -83,7 +88,7 @@ function isTokenExpired(token: string): boolean {
const bufferTime = 5 * 60 * 1000; // 5 minutes const bufferTime = 5 * 60 * 1000; // 5 minutes
const currentTime = Date.now(); const currentTime = Date.now();
return currentTime >= (expiryTime - bufferTime); return currentTime >= expiryTime - bufferTime;
} }
// ============================ // ============================
@ -132,7 +137,7 @@ function updateStoredUserData(user: Types.User): void {
} }
} }
function storeAuthData(authResponse: Types.AuthResponse, isGuest: boolean = false): void { function storeAuthData(authResponse: Types.AuthResponse, isGuest = false): void {
localStorage.setItem(TOKEN_STORAGE.ACCESS_TOKEN, authResponse.accessToken); localStorage.setItem(TOKEN_STORAGE.ACCESS_TOKEN, authResponse.accessToken);
localStorage.setItem(TOKEN_STORAGE.REFRESH_TOKEN, authResponse.refreshToken); localStorage.setItem(TOKEN_STORAGE.REFRESH_TOKEN, authResponse.refreshToken);
localStorage.setItem(TOKEN_STORAGE.USER_DATA, prepareUserDataForStorage(authResponse.user)); localStorage.setItem(TOKEN_STORAGE.USER_DATA, prepareUserDataForStorage(authResponse.user));
@ -177,7 +182,7 @@ function getStoredAuthData(): {
userData, userData,
expiresAt, expiresAt,
userType, userType,
isGuest: isGuestStr === 'true' isGuest: isGuestStr === 'true',
}; };
} }
@ -202,7 +207,8 @@ function useAuthenticationLogic() {
const guestCreationAttempted = useRef(false); const guestCreationAttempted = useRef(false);
// Token refresh function // Token refresh function
const refreshAccessToken = useCallback(async (refreshToken: string): Promise<Types.AuthResponse | null> => { const refreshAccessToken = useCallback(
async (refreshToken: string): Promise<Types.AuthResponse | null> => {
try { try {
const response = await apiClient.refreshToken(refreshToken); const response = await apiClient.refreshToken(refreshToken);
return response; return response;
@ -210,7 +216,9 @@ function useAuthenticationLogic() {
console.error('Token refresh failed:', error); console.error('Token refresh failed:', error);
return null; return null;
} }
}, [apiClient]); },
[apiClient]
);
// Create guest session // Create guest session
const createGuestSession = useCallback(async (): Promise<boolean> => { const createGuestSession = useCallback(async (): Promise<boolean> => {
@ -286,7 +294,7 @@ function useAuthenticationLogic() {
try { try {
// Make a quick API call to verify guest still exists // Make a quick API call to verify guest still exists
const response = await fetch(`${apiClient.getBaseUrl()}/users/${stored.userData.id}`, { const response = await fetch(`${apiClient.getBaseUrl()}/users/${stored.userData.id}`, {
headers: { 'Authorization': `Bearer ${stored.accessToken}` } headers: { Authorization: `Bearer ${stored.accessToken}` },
}); });
if (!response.ok) { if (!response.ok) {
@ -316,13 +324,13 @@ function useAuthenticationLogic() {
setAuthState({ setAuthState({
user: isGuest ? null : refreshResult.user, user: isGuest ? null : refreshResult.user,
guest: isGuest ? refreshResult.user as Types.Guest : null, guest: isGuest ? (refreshResult.user as Types.Guest) : null,
isAuthenticated: true, isAuthenticated: true,
isGuest, isGuest,
isLoading: false, isLoading: false,
isInitializing: false, isInitializing: false,
error: null, error: null,
mfaResponse: null mfaResponse: null,
}); });
console.log('✅ Token refreshed successfully'); console.log('✅ Token refreshed successfully');
@ -339,13 +347,13 @@ function useAuthenticationLogic() {
setAuthState({ setAuthState({
user: isGuest ? null : stored.userData, user: isGuest ? null : stored.userData,
guest: isGuest ? stored.userData as Types.Guest : null, guest: isGuest ? (stored.userData as Types.Guest) : null,
isAuthenticated: true, isAuthenticated: true,
isGuest, isGuest,
isLoading: false, isLoading: false,
isInitializing: false, isInitializing: false,
error: null, error: null,
mfaResponse: null mfaResponse: null,
}); });
console.log('✅ Restored authentication from stored tokens'); console.log('✅ Restored authentication from stored tokens');
@ -378,7 +386,7 @@ function useAuthenticationLogic() {
const expiryTime = stored.expiresAt * 1000; const expiryTime = stored.expiresAt * 1000;
const currentTime = Date.now(); const currentTime = Date.now();
const timeUntilExpiry = expiryTime - currentTime - (5 * 60 * 1000); // 5 minute buffer const timeUntilExpiry = expiryTime - currentTime - 5 * 60 * 1000; // 5 minute buffer
if (timeUntilExpiry <= 0) { if (timeUntilExpiry <= 0) {
initializeAuth(); initializeAuth();
@ -394,8 +402,14 @@ function useAuthenticationLogic() {
}, [authState.isAuthenticated, initializeAuth]); }, [authState.isAuthenticated, initializeAuth]);
// Enhanced login with MFA support // Enhanced login with MFA support
const login = useCallback(async (loginData: LoginRequest): Promise<boolean> => { const login = useCallback(
setAuthState(prev => ({ ...prev, isLoading: true, error: null, mfaResponse: null })); async (loginData: LoginRequest): Promise<boolean> => {
setAuthState(prev => ({
...prev,
isLoading: true,
error: null,
mfaResponse: null,
}));
try { try {
const result = await apiClient.login({ const result = await apiClient.login({
@ -432,7 +446,8 @@ function useAuthenticationLogic() {
return true; return true;
} }
} catch (error: any) { } catch (error: any) {
const errorMessage = error instanceof Error ? error.message : 'Network error occurred. Please try again.'; const errorMessage =
error instanceof Error ? error.message : 'Network error occurred. Please try again.';
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
isLoading: false, isLoading: false,
@ -441,10 +456,13 @@ function useAuthenticationLogic() {
})); }));
return false; return false;
} }
}, [apiClient]); },
[apiClient]
);
// Convert guest to permanent user // Convert guest to permanent user
const convertGuestToUser = useCallback(async (registrationData: GuestConversionRequest): Promise<boolean> => { const convertGuestToUser = useCallback(
async (registrationData: GuestConversionRequest): Promise<boolean> => {
if (!authState.isGuest || !authState.guest) { if (!authState.isGuest || !authState.guest) {
throw new Error('Not currently a guest user'); throw new Error('Not currently a guest user');
} }
@ -471,7 +489,8 @@ function useAuthenticationLogic() {
console.log('✅ Guest successfully converted to permanent user'); console.log('✅ Guest successfully converted to permanent user');
return true; return true;
} catch (error: any) { } catch (error: any) {
const errorMessage = error instanceof Error ? error.message : 'Failed to convert guest account'; const errorMessage =
error instanceof Error ? error.message : 'Failed to convert guest account';
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
isLoading: false, isLoading: false,
@ -479,10 +498,13 @@ function useAuthenticationLogic() {
})); }));
return false; return false;
} }
}, [apiClient, authState.isGuest, authState.guest]); },
[apiClient, authState.isGuest, authState.guest]
);
// MFA verification // MFA verification
const verifyMFA = useCallback(async (mfaData: Types.MFAVerifyRequest): Promise<boolean> => { const verifyMFA = useCallback(
async (mfaData: Types.MFAVerifyRequest): Promise<boolean> => {
setAuthState(prev => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
@ -514,11 +536,13 @@ function useAuthenticationLogic() {
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage error: errorMessage,
})); }));
return false; return false;
} }
}, [apiClient]); },
[apiClient]
);
// Logout - returns to guest session // Logout - returns to guest session
const logout = useCallback(async () => { const logout = useCallback(async () => {
@ -550,18 +574,22 @@ function useAuthenticationLogic() {
}, [apiClient, authState.isAuthenticated, authState.isGuest, createGuestSession]); }, [apiClient, authState.isAuthenticated, authState.isGuest, createGuestSession]);
// Update user data // Update user data
const updateUserData = useCallback((updatedUser: Types.User) => { const updateUserData = useCallback(
(updatedUser: Types.User) => {
updateStoredUserData(updatedUser); updateStoredUserData(updatedUser);
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
user: authState.isGuest ? null : updatedUser, user: authState.isGuest ? null : updatedUser,
guest: authState.isGuest ? updatedUser as Types.Guest : prev.guest guest: authState.isGuest ? (updatedUser as Types.Guest) : prev.guest,
})); }));
console.log('✅ User data updated'); console.log('✅ User data updated');
}, [authState.isGuest]); },
[authState.isGuest]
);
// Email verification functions (unchanged) // Email verification functions (unchanged)
const verifyEmail = useCallback(async (verificationData: EmailVerificationRequest) => { const verifyEmail = useCallback(
async (verificationData: EmailVerificationRequest) => {
setAuthState(prev => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
@ -569,21 +597,24 @@ function useAuthenticationLogic() {
setAuthState(prev => ({ ...prev, isLoading: false })); setAuthState(prev => ({ ...prev, isLoading: false }));
return { return {
message: result.message || 'Email verified successfully', message: result.message || 'Email verified successfully',
userType: result.userType || 'user' userType: result.userType || 'user',
}; };
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Email verification failed'; const errorMessage = error instanceof Error ? error.message : 'Email verification failed';
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage error: errorMessage,
})); }));
return null; return null;
} }
}, [apiClient]); },
[apiClient]
);
// Other existing methods remain the same... // Other existing methods remain the same...
const resendEmailVerification = useCallback(async (email: string): Promise<boolean> => { const resendEmailVerification = useCallback(
async (email: string): Promise<boolean> => {
setAuthState(prev => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
@ -591,15 +622,18 @@ function useAuthenticationLogic() {
setAuthState(prev => ({ ...prev, isLoading: false })); setAuthState(prev => ({ ...prev, isLoading: false }));
return true; return true;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to resend verification email'; const errorMessage =
error instanceof Error ? error.message : 'Failed to resend verification email';
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage error: errorMessage,
})); }));
return false; return false;
} }
}, [apiClient]); },
[apiClient]
);
const setPendingVerificationEmail = useCallback((email: string) => { const setPendingVerificationEmail = useCallback((email: string) => {
localStorage.setItem(TOKEN_STORAGE.PENDING_VERIFICATION_EMAIL, email); localStorage.setItem(TOKEN_STORAGE.PENDING_VERIFICATION_EMAIL, email);
@ -609,7 +643,8 @@ function useAuthenticationLogic() {
return localStorage.getItem(TOKEN_STORAGE.PENDING_VERIFICATION_EMAIL); return localStorage.getItem(TOKEN_STORAGE.PENDING_VERIFICATION_EMAIL);
}, []); }, []);
const createEmployerAccount = useCallback(async (employerData: CreateEmployerRequest): Promise<boolean> => { const createEmployerAccount = useCallback(
async (employerData: CreateEmployerRequest): Promise<boolean> => {
setAuthState(prev => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
@ -625,13 +660,16 @@ function useAuthenticationLogic() {
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage error: errorMessage,
})); }));
return false; return false;
} }
}, [apiClient, setPendingVerificationEmail]); },
[apiClient, setPendingVerificationEmail]
);
const requestPasswordReset = useCallback(async (email: string): Promise<boolean> => { const requestPasswordReset = useCallback(
async (email: string): Promise<boolean> => {
setAuthState(prev => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
@ -639,15 +677,18 @@ function useAuthenticationLogic() {
setAuthState(prev => ({ ...prev, isLoading: false })); setAuthState(prev => ({ ...prev, isLoading: false }));
return true; return true;
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Password reset request failed'; const errorMessage =
error instanceof Error ? error.message : 'Password reset request failed';
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage error: errorMessage,
})); }));
return false; return false;
} }
}, [apiClient]); },
[apiClient]
);
const refreshAuth = useCallback(async (): Promise<boolean> => { const refreshAuth = useCallback(async (): Promise<boolean> => {
const stored = getStoredAuthData(); const stored = getStoredAuthData();
@ -667,11 +708,11 @@ function useAuthenticationLogic() {
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
user: isGuest ? null : refreshResult.user, user: isGuest ? null : refreshResult.user,
guest: isGuest ? refreshResult.user as Types.Guest : null, guest: isGuest ? (refreshResult.user as Types.Guest) : null,
isAuthenticated: true, isAuthenticated: true,
isGuest, isGuest,
isLoading: false, isLoading: false,
error: null error: null,
})); }));
return true; return true;
@ -682,7 +723,8 @@ function useAuthenticationLogic() {
}, [refreshAccessToken, logout]); }, [refreshAccessToken, logout]);
// Resend MFA code // Resend MFA code
const resendMFACode = useCallback(async (email: string, deviceId: string, deviceName: string): Promise<boolean> => { const resendMFACode = useCallback(
async (email: string, deviceId: string, deviceName: string): Promise<boolean> => {
setAuthState(prev => ({ ...prev, isLoading: true, error: null })); setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try { try {
@ -700,18 +742,20 @@ function useAuthenticationLogic() {
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
isLoading: false, isLoading: false,
error: errorMessage error: errorMessage,
})); }));
return false; return false;
} }
}, [apiClient]); },
[apiClient]
);
// Clear MFA state // Clear MFA state
const clearMFA = useCallback(() => { const clearMFA = useCallback(() => {
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
mfaResponse: null, mfaResponse: null,
error: null error: null,
})); }));
}, []); }, []);
@ -732,7 +776,7 @@ function useAuthenticationLogic() {
refreshAuth, refreshAuth,
updateUserData, updateUserData,
convertGuestToUser, convertGuestToUser,
createGuestSession createGuestSession,
}; };
} }
@ -745,11 +789,7 @@ const AuthContext = createContext<ReturnType<typeof useAuthenticationLogic> | nu
function AuthProvider({ children }: { children: React.ReactNode }) { function AuthProvider({ children }: { children: React.ReactNode }) {
const auth = useAuthenticationLogic(); const auth = useAuthenticationLogic();
return ( return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
<AuthContext.Provider value={auth}>
{children}
</AuthContext.Provider>
);
} }
function useAuth() { function useAuth() {
@ -775,7 +815,7 @@ function ProtectedRoute({
children, children,
fallback = <div>Please log in to access this page.</div>, fallback = <div>Please log in to access this page.</div>,
requiredUserType, requiredUserType,
allowGuests = false allowGuests = false,
}: ProtectedRouteProps) { }: ProtectedRouteProps) {
const { isAuthenticated, isInitializing, user, isGuest } = useAuth(); const { isAuthenticated, isInitializing, user, isGuest } = useAuth();
@ -808,14 +848,9 @@ export type {
EmailVerificationRequest, EmailVerificationRequest,
ResendVerificationRequest, ResendVerificationRequest,
PasswordResetRequest, PasswordResetRequest,
GuestConversionRequest GuestConversionRequest,
} };
export type { CreateCandidateRequest, CreateEmployerRequest } from '../services/api-client'; export type { CreateCandidateRequest, CreateEmployerRequest } from '../services/api-client';
export { export { useAuthenticationLogic, AuthProvider, useAuth, ProtectedRoute };
useAuthenticationLogic,
AuthProvider,
useAuth,
ProtectedRoute
}

View File

@ -17,7 +17,7 @@ const STORAGE_KEYS = {
ROUTE_STATE: 'routeState', ROUTE_STATE: 'routeState',
ACTIVE_TAB: 'activeTab', ACTIVE_TAB: 'activeTab',
APPLIED_FILTERS: 'appliedFilters', APPLIED_FILTERS: 'appliedFilters',
SIDEBAR_COLLAPSED: 'sidebarCollapsed' SIDEBAR_COLLAPSED: 'sidebarCollapsed',
} as const; } as const;
// ============================ // ============================
@ -118,7 +118,7 @@ function getInitialRouteState(): RouteState {
lastRoute: getStoredId(STORAGE_KEYS.LAST_ROUTE), lastRoute: getStoredId(STORAGE_KEYS.LAST_ROUTE),
activeTab: getStoredId(STORAGE_KEYS.ACTIVE_TAB), activeTab: getStoredId(STORAGE_KEYS.ACTIVE_TAB),
appliedFilters: getStoredObject(STORAGE_KEYS.APPLIED_FILTERS, {}), appliedFilters: getStoredObject(STORAGE_KEYS.APPLIED_FILTERS, {}),
sidebarCollapsed: getStoredObject(STORAGE_KEYS.SIDEBAR_COLLAPSED, false) sidebarCollapsed: getStoredObject(STORAGE_KEYS.SIDEBAR_COLLAPSED, false),
}; };
} }
@ -227,7 +227,6 @@ export function useAppStateLogic(): AppStateContextType {
// Wait for all restoration attempts to complete // Wait for all restoration attempts to complete
await Promise.all(promises); await Promise.all(promises);
} catch (error) { } catch (error) {
console.error('Error during app state initialization:', error); console.error('Error during app state initialization:', error);
} finally { } finally {
@ -352,7 +351,7 @@ export function useAppStateLogic(): AppStateContextType {
lastRoute: null, lastRoute: null,
activeTab: null, activeTab: null,
appliedFilters: {}, appliedFilters: {},
sidebarCollapsed: false sidebarCollapsed: false,
}; };
setRouteStateState(clearedState); setRouteStateState(clearedState);
@ -388,7 +387,7 @@ export function useAppStateLogic(): AppStateContextType {
setActiveTab, setActiveTab,
setFilters, setFilters,
setSidebarCollapsed, setSidebarCollapsed,
clearRouteState clearRouteState,
}; };
} }
@ -403,9 +402,12 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) {
const snackRef = useRef<any>(null); const snackRef = useRef<any>(null);
// Global UI components // Global UI components
appState.setSnack = useCallback((message: string, severity?: SeverityType) => { appState.setSnack = useCallback(
(message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity); snackRef.current?.setSnack(message, severity);
}, [snackRef]); },
[snackRef]
);
return ( return (
<AppStateContext.Provider value={appState}> <AppStateContext.Provider value={appState}>
@ -454,7 +456,7 @@ export function useRouteState() {
setFilters, setFilters,
setSidebarCollapsed, setSidebarCollapsed,
restoreLastRoute, restoreLastRoute,
clearRouteState clearRouteState,
} = useAppState(); } = useAppState();
return { return {
@ -463,7 +465,7 @@ export function useRouteState() {
setFilters, setFilters,
setSidebarCollapsed, setSidebarCollapsed,
restoreLastRoute, restoreLastRoute,
clearRouteState clearRouteState,
}; };
} }

View File

@ -1,13 +1,13 @@
import { useEffect, useRef, RefObject, useCallback } from 'react'; import { useEffect, useRef, RefObject, useCallback } from 'react';
const debug: boolean = false; const debug = false;
type ResizeCallback = () => void; type ResizeCallback = () => void;
// Define the debounce function with cancel capability // Define the debounce function with cancel capability
function debounce<T extends (...args: any[]) => void>(func: T, wait: number) { function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
let timeout: NodeJS.Timeout | null = null; let timeout: NodeJS.Timeout | null = null;
let lastCall: number = 0; let lastCall = 0;
const debounced = function (...args: Parameters<T>) { const debounced = function (...args: Parameters<T>) {
const now = Date.now(); const now = Date.now();
@ -68,8 +68,12 @@ const useResizeObserverAndMutationObserver = (
requestAnimationFrame(() => callbackRef.current()); requestAnimationFrame(() => callbackRef.current());
}, 500); }, 500);
const resizeObserver = new ResizeObserver((e: any) => { debouncedCallback("resize"); }); const resizeObserver = new ResizeObserver((e: any) => {
const mutationObserver = new MutationObserver((e: any) => { debouncedCallback("mutation"); }); debouncedCallback('resize');
});
const mutationObserver = new MutationObserver((e: any) => {
debouncedCallback('mutation');
});
// Observe container size // Observe container size
resizeObserver.observe(container); resizeObserver.observe(container);
@ -102,8 +106,8 @@ const useResizeObserverAndMutationObserver = (
*/ */
const useAutoScrollToBottom = ( const useAutoScrollToBottom = (
scrollToRef: RefObject<HTMLElement | null>, scrollToRef: RefObject<HTMLElement | null>,
smooth: boolean = true, smooth = true,
fallbackThreshold: number = 0.33, fallbackThreshold = 0.33,
contentUpdateTrigger?: any contentUpdateTrigger?: any
): RefObject<HTMLDivElement | null> => { ): RefObject<HTMLDivElement | null> => {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
@ -111,7 +115,8 @@ const useAutoScrollToBottom = (
const scrollTimeout = useRef<NodeJS.Timeout | null>(null); const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
const isUserScrollingUpRef = useRef(false); const isUserScrollingUpRef = useRef(false);
const checkAndScrollToBottom = useCallback((isPasteEvent: boolean = false) => { const checkAndScrollToBottom = useCallback(
(isPasteEvent = false) => {
const container = containerRef.current; const container = containerRef.current;
if (!container) return; if (!container) return;
@ -119,7 +124,7 @@ const useAutoScrollToBottom = (
const scrollTo = scrollToRef.current; const scrollTo = scrollToRef.current;
if (isPasteEvent && !scrollTo) { if (isPasteEvent && !scrollTo) {
console.error("Paste Event triggered without scrollTo"); console.error('Paste Event triggered without scrollTo');
} }
if (scrollTo) { if (scrollTo) {
@ -137,7 +142,8 @@ const useAutoScrollToBottom = (
shouldScroll = isPasteEvent || (isTextFieldVisible && !isUserScrollingUpRef.current); shouldScroll = isPasteEvent || (isTextFieldVisible && !isUserScrollingUpRef.current);
if (shouldScroll) { if (shouldScroll) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
debug && console.debug('Scrolling to container bottom:', { debug &&
console.debug('Scrolling to container bottom:', {
scrollHeight: container.scrollHeight, scrollHeight: container.scrollHeight,
scrollToHeight: scrollToRect.height, scrollToHeight: scrollToRect.height,
containerHeight: container.clientHeight, containerHeight: container.clientHeight,
@ -161,7 +167,10 @@ const useAutoScrollToBottom = (
if (shouldScroll) { if (shouldScroll) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
debug && console.debug('Scrolling to container bottom (fallback):', { scrollHeight }); debug &&
console.debug('Scrolling to container bottom (fallback):', {
scrollHeight,
});
container.scrollTo({ container.scrollTo({
top: container.scrollHeight, top: container.scrollHeight,
behavior: smooth ? 'smooth' : 'auto', behavior: smooth ? 'smooth' : 'auto',
@ -169,7 +178,9 @@ const useAutoScrollToBottom = (
}); });
} }
} }
}, [fallbackThreshold, smooth, scrollToRef]); },
[fallbackThreshold, smooth, scrollToRef]
);
useEffect(() => { useEffect(() => {
const container = containerRef.current; const container = containerRef.current;
@ -180,32 +191,36 @@ const useAutoScrollToBottom = (
const currentScrollTop = container.scrollTop; const currentScrollTop = container.scrollTop;
/* If the user is scrolling up *or* they used the scroll wheel and didn't scroll, /* If the user is scrolling up *or* they used the scroll wheel and didn't scroll,
* they may be zooming in a region; pause scrolling */ * they may be zooming in a region; pause scrolling */
isUserScrollingUpRef.current = (currentScrollTop <= lastScrollTop.current) || pause ? true : false; 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; lastScrollTop.current = currentScrollTop;
if (scrollTimeout.current) clearTimeout(scrollTimeout.current); if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
scrollTimeout.current = setTimeout(() => { scrollTimeout.current = setTimeout(
() => {
isUserScrollingUpRef.current = false; isUserScrollingUpRef.current = false;
debug && console.debug(`Scrolling up: ${isUserScrollingUpRef.current}`); debug && console.debug(`Scrolling up: ${isUserScrollingUpRef.current}`);
}, pause ? pause : 500); },
pause ? pause : 500
);
}; };
const pauseScroll = (ev: Event) => { const pauseScroll = (ev: Event) => {
debug && console.log("Pausing for mouse movement"); debug && console.log('Pausing for mouse movement');
handleScroll(ev, 500); handleScroll(ev, 500);
} };
const pauseClick = (ev: Event) => { const pauseClick = (ev: Event) => {
debug && console.log("Pausing for mouse click"); debug && console.log('Pausing for mouse click');
handleScroll(ev, 1000); handleScroll(ev, 1000);
} };
const handlePaste = () => { const handlePaste = () => {
console.log("handlePaste"); console.log('handlePaste');
// Delay scroll check to ensure DOM updates // Delay scroll check to ensure DOM updates
setTimeout(() => { setTimeout(() => {
console.log("scrolling for handlePaste"); console.log('scrolling for handlePaste');
requestAnimationFrame(() => checkAndScrollToBottom(true)); requestAnimationFrame(() => checkAndScrollToBottom(true));
}, 100); }, 100);
}; };
@ -236,7 +251,4 @@ const useAutoScrollToBottom = (
return containerRef; return containerRef;
}; };
export { export { useResizeObserverAndMutationObserver, useAutoScrollToBottom };
useResizeObserverAndMutationObserver,
useAutoScrollToBottom
}

View File

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

View File

@ -8,7 +8,7 @@ import {
Grid, Grid,
Button, Button,
alpha, alpha,
GlobalStyles GlobalStyles,
} from '@mui/material'; } from '@mui/material';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import ConstructionIcon from '@mui/icons-material/Construction'; import ConstructionIcon from '@mui/icons-material/Construction';
@ -26,10 +26,10 @@ interface BetaPageProps {
const BetaPage: React.FC<BetaPageProps> = ({ const BetaPage: React.FC<BetaPageProps> = ({
children, children,
title = "Coming Soon", title = 'Coming Soon',
subtitle = "This page is currently in development", subtitle = 'This page is currently in development',
returnPath = "/", returnPath = '/',
returnLabel = "Return to Backstory", returnLabel = 'Return to Backstory',
onReturn, onReturn,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
@ -38,11 +38,18 @@ const BetaPage: React.FC<BetaPageProps> = ({
const location = useLocation(); const location = useLocation();
if (!children) { if (!children) {
children = (<Box sx={{ width: "100%", display: "flex", justifyContent: "center" }}><Typography>The page you requested (<b>{location.pathname.replace(/^\//, '')}</b>) is not yet ready.</Typography></Box>); children = (
<Box sx={{ width: '100%', display: 'flex', justifyContent: 'center' }}>
<Typography>
The page you requested (<b>{location.pathname.replace(/^\//, '')}</b>) is not yet ready.
</Typography>
</Box>
);
} }
// Enhanced sparkle effect for background elements // Enhanced sparkle effect for background elements
const [sparkles, setSparkles] = useState<Array<{ const [sparkles, setSparkles] = useState<
Array<{
id: number; id: number;
x: number; x: number;
y: number; y: number;
@ -50,7 +57,8 @@ const BetaPage: React.FC<BetaPageProps> = ({
opacity: number; opacity: number;
duration: number; duration: number;
delay: number; delay: number;
}>>([]); }>
>([]);
useEffect(() => { useEffect(() => {
// Generate sparkle elements with random properties // Generate sparkle elements with random properties
@ -86,7 +94,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
<Box <Box
sx={{ sx={{
minHeight: '100%', minHeight: '100%',
width: "100%", width: '100%',
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
bgcolor: theme.palette.background.default, bgcolor: theme.palette.background.default,
@ -95,8 +103,18 @@ const BetaPage: React.FC<BetaPageProps> = ({
}} }}
> >
{/* Animated background elements */} {/* Animated background elements */}
<Box sx={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 0, overflow: 'hidden' }}> <Box
{sparkles.map((sparkle) => ( sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 0,
overflow: 'hidden',
}}
>
{sparkles.map(sparkle => (
<Box <Box
key={sparkle.id} key={sparkle.id}
sx={{ sx={{
@ -107,7 +125,10 @@ const BetaPage: React.FC<BetaPageProps> = ({
height: sparkle.size, height: sparkle.size,
borderRadius: '50%', borderRadius: '50%',
bgcolor: alpha(theme.palette.primary.main, sparkle.opacity), bgcolor: alpha(theme.palette.primary.main, sparkle.opacity),
boxShadow: `0 0 ${sparkle.size * 2}px ${alpha(theme.palette.primary.main, sparkle.opacity)}`, boxShadow: `0 0 ${sparkle.size * 2}px ${alpha(
theme.palette.primary.main,
sparkle.opacity
)}`,
animation: `float ${sparkle.duration}s ease-in-out ${sparkle.delay}s infinite alternate`, animation: `float ${sparkle.duration}s ease-in-out ${sparkle.delay}s infinite alternate`,
}} }}
/> />
@ -116,7 +137,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
<Container maxWidth="lg" sx={{ position: 'relative', zIndex: 2 }}> <Container maxWidth="lg" sx={{ position: 'relative', zIndex: 2 }}>
<Grid container spacing={4} direction="column" alignItems="center"> <Grid container spacing={4} direction="column" alignItems="center">
<Grid size={{xs: 12}} sx={{ textAlign: 'center', mb: 2 }}> <Grid size={{ xs: 12 }} sx={{ textAlign: 'center', mb: 2 }}>
<Typography <Typography
variant="h2" variant="h2"
component="h1" component="h1"
@ -131,17 +152,12 @@ const BetaPage: React.FC<BetaPageProps> = ({
{title} {title}
</Typography> </Typography>
<Typography <Typography variant="h5" component="h2" color="textSecondary" sx={{ mb: 6 }}>
variant="h5"
component="h2"
color="textSecondary"
sx={{ mb: 6 }}
>
{subtitle} {subtitle}
</Typography> </Typography>
</Grid> </Grid>
<Grid size={{xs: 12, md: 10, lg: 8}} sx={{ mb: 4 }}> <Grid size={{ xs: 12, md: 10, lg: 8 }} sx={{ mb: 4 }}>
<Paper <Paper
elevation={8} elevation={8}
sx={{ sx={{
@ -182,7 +198,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
sx={{ sx={{
fontSize: 80, fontSize: 80,
mb: 2, mb: 2,
animation: 'rocketWobble 3s ease-in-out infinite' animation: 'rocketWobble 3s ease-in-out infinite',
}} }}
/> />
<Typography> <Typography>
@ -193,7 +209,21 @@ const BetaPage: React.FC<BetaPageProps> = ({
</Typography> </Typography>
</Box> </Box>
)} )}
<Beta adaptive={false} sx={{ opacity: 0.5, left: "-72px", "& > div": { paddingRight: "30px", background: "gold", color: "#808080" } }} onClick={() => { navigate('/docs/beta'); }} /> <Beta
adaptive={false}
sx={{
opacity: 0.5,
left: '-72px',
'& > div': {
paddingRight: '30px',
background: 'gold',
color: '#808080',
},
}}
onClick={() => {
navigate('/docs/beta');
}}
/>
</Box> </Box>
{/* Return button */} {/* Return button */}
@ -210,7 +240,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
boxShadow: `0 4px 14px ${alpha(theme.palette.primary.main, 0.4)}`, boxShadow: `0 4px 14px ${alpha(theme.palette.primary.main, 0.4)}`,
'&:hover': { '&:hover': {
boxShadow: `0 6px 20px ${alpha(theme.palette.primary.main, 0.6)}`, boxShadow: `0 6px 20px ${alpha(theme.palette.primary.main, 0.6)}`,
} },
}} }}
> >
{returnLabel} {returnLabel}
@ -250,7 +280,10 @@ const BetaPage: React.FC<BetaPageProps> = ({
textShadow: `0 0 10px ${alpha(theme.palette.primary.main, 0.3)}`, textShadow: `0 0 10px ${alpha(theme.palette.primary.main, 0.3)}`,
}, },
'100%': { '100%': {
textShadow: `0 0 25px ${alpha(theme.palette.primary.main, 0.7)}, 0 0 40px ${alpha(theme.palette.primary.main, 0.4)}`, textShadow: `0 0 25px ${alpha(theme.palette.primary.main, 0.7)}, 0 0 40px ${alpha(
theme.palette.primary.main,
0.4
)}`,
}, },
}, },
'@keyframes rocketWobble': { '@keyframes rocketWobble': {
@ -270,6 +303,4 @@ const BetaPage: React.FC<BetaPageProps> = ({
); );
}; };
export { export { BetaPage };
BetaPage
}

View File

@ -1,18 +1,15 @@
import React, { forwardRef, useState, useEffect, useRef } from 'react'; import React, { forwardRef, useState, useEffect, useRef } from 'react';
import { import { Box, Paper, Button, Divider, useTheme, useMediaQuery, Tooltip } from '@mui/material';
Box, import { Send as SendIcon } from '@mui/icons-material';
Paper,
Button,
Divider,
useTheme,
useMediaQuery,
Tooltip,
} from '@mui/material';
import {
Send as SendIcon
} from '@mui/icons-material';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { ChatMessage, ChatSession, ChatMessageUser, ChatMessageError, ChatMessageStreaming, ChatMessageStatus } from 'types/types'; import {
ChatMessage,
ChatSession,
ChatMessageUser,
ChatMessageError,
ChatMessageStreaming,
ChatMessageStatus,
} from 'types/types';
import { ConversationHandle } from 'components/Conversation'; import { ConversationHandle } from 'components/Conversation';
import { BackstoryPageProps } from 'components/BackstoryTab'; import { BackstoryPageProps } from 'components/BackstoryTab';
import { Message } from 'components/Message'; import { Message } from 'components/Message';
@ -27,15 +24,24 @@ import { CandidatePicker } from 'components/ui/CandidatePicker';
import { Scrollable } from 'components/Scrollable'; import { Scrollable } from 'components/Scrollable';
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "user", metadata: null as any status: 'done',
type: 'text',
sessionId: '',
timestamp: new Date(),
content: '',
role: 'user',
metadata: null as any,
}; };
const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => { const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
(props: BackstoryPageProps, ref) => {
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate() const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const theme = useTheme(); const theme = useTheme();
const [processingMessage, setProcessingMessage] = useState<ChatMessageStatus | ChatMessageError | null>(null); 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 backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
@ -87,14 +93,19 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
const chatMessage: ChatMessageUser = { const chatMessage: ChatMessageUser = {
sessionId: chatSession.id, sessionId: chatSession.id,
role: "user", role: 'user',
content: messageContent, content: messageContent,
status: "done", status: 'done',
type: "text", type: 'text',
timestamp: new Date() timestamp: new Date(),
}; };
setProcessingMessage({ ...defaultMessage, status: 'status', activity: "info", content: `Establishing connection with ${selectedCandidate.firstName}'s chat session.` }); setProcessingMessage({
...defaultMessage,
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); const filtered = prev.filter((m: any) => m.id !== chatMessage.id);
@ -112,30 +123,38 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
setProcessingMessage(null); setProcessingMessage(null);
}, },
onError: (error: string | ChatMessageError) => { onError: (error: string | ChatMessageError) => {
console.log("onError:", error); console.log('onError:', error);
let message: string; let message: string;
// Type-guard to determine if this is a ChatMessageBase or a string // Type-guard to determine if this is a ChatMessageBase or a string
if (typeof error === "object" && error !== null && "content" in error) { if (typeof error === 'object' && error !== null && 'content' in error) {
setProcessingMessage(error); setProcessingMessage(error);
message = error.content as string; message = error.content as string;
} else { } else {
setProcessingMessage({ ...defaultMessage, status: "error", content: error }) setProcessingMessage({
...defaultMessage,
status: 'error',
content: error,
});
} }
setStreaming(false); setStreaming(false);
}, },
onStreaming: (chunk: ChatMessageStreaming) => { onStreaming: (chunk: ChatMessageStreaming) => {
// console.log("onStreaming:", chunk); // console.log("onStreaming:", chunk);
setStreamingMessage({ ...chunk, role: 'assistant', metadata: null as any }); setStreamingMessage({
...chunk,
role: 'assistant',
metadata: null as any,
});
}, },
onStatus: (status: ChatMessageStatus) => { onStatus: (status: ChatMessageStatus) => {
setProcessingMessage(status); setProcessingMessage(status);
}, },
onComplete: () => { onComplete: () => {
console.log("onComplete"); console.log('onComplete');
setStreamingMessage(null); setStreamingMessage(null);
setProcessingMessage(null); setProcessingMessage(null);
setStreaming(false); setStreaming(false);
} },
}); });
} catch (error) { } catch (error) {
console.error('Failed to send message:', error); console.error('Failed to send message:', error);
@ -153,7 +172,12 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
if (!selectedCandidate) return; if (!selectedCandidate) return;
try { try {
setLoading(true); setLoading(true);
apiClient.getOrCreateChatSession(selectedCandidate, `Backstory chat with ${selectedCandidate.fullName}`, 'candidate_chat') apiClient
.getOrCreateChatSession(
selectedCandidate,
`Backstory chat with ${selectedCandidate.fullName}`,
'candidate_chat'
)
.then(session => { .then(session => {
setChatSession(session); setChatSession(session);
setLoading(false); setLoading(false);
@ -178,27 +202,30 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
const welcomeMessage: ChatMessage = { const welcomeMessage: ChatMessage = {
sessionId: chatSession?.id || '', sessionId: chatSession?.id || '',
role: "information", role: 'information',
type: "text", type: 'text',
status: "done", status: 'done',
timestamp: new Date(), timestamp: new Date(),
content: `Welcome to the Backstory Chat about ${selectedCandidate.fullName}. Ask any questions you have about ${selectedCandidate.firstName}.`, content: `Welcome to the Backstory Chat about ${selectedCandidate.fullName}. Ask any questions you have about ${selectedCandidate.firstName}.`,
metadata: null as any metadata: null as any,
}; };
return ( return (
<Box ref={ref} <Box
ref={ref}
sx={{ sx={{
display: "flex", flexDirection: "column", display: 'flex',
height: "100%", /* Restrict to main-container's height */ flexDirection: 'column',
width: "100%", height: '100%' /* Restrict to main-container's height */,
minHeight: 0,/* Prevent flex overflow */ width: '100%',
maxHeight: "min-content", minHeight: 0 /* Prevent flex overflow */,
"& > *:not(.Scrollable)": { maxHeight: 'min-content',
flexShrink: 0, /* Prevent shrinking */ '& > *:not(.Scrollable)': {
flexShrink: 0 /* Prevent shrinking */,
}, },
position: "relative", position: 'relative',
}}> }}
>
<Paper elevation={2} sx={{ m: 1, p: 1 }}> <Paper elevation={2} sx={{ m: 1, p: 1 }}>
<CandidateInfo <CandidateInfo
key={selectedCandidate.username} key={selectedCandidate.username}
@ -206,27 +233,42 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
elevation={4} elevation={4}
candidate={selectedCandidate} candidate={selectedCandidate}
variant="small" variant="small"
sx={{ flexShrink: 1, width: "100%", maxHeight: 0, minHeight: "min-content" }} // Prevent header from shrinking sx={{
flexShrink: 1,
width: '100%',
maxHeight: 0,
minHeight: 'min-content',
}} // Prevent header from shrinking
/> />
<Button sx={{ maxWidth: "max-content" }} onClick={() => { setSelectedCandidate(null); }} variant="contained">Change Candidates</Button> <Button
sx={{ maxWidth: 'max-content' }}
onClick={() => {
setSelectedCandidate(null);
}}
variant="contained"
>
Change Candidates
</Button>
</Paper> </Paper>
{/* Chat Interface */} {/* Chat Interface */}
{/* Scrollable Messages Area */} {/* Scrollable Messages Area */}
{chatSession && {chatSession && (
<Scrollable <Scrollable
sx={{ sx={{
position: "relative", position: 'relative',
maxHeight: "100%", maxHeight: '100%',
width: "100%", width: '100%',
display: "flex", flexGrow: 1, display: 'flex',
flex: 1, /* Take remaining space in some-container */ flexGrow: 1,
overflowY: "auto", /* Scroll if content overflows */ flex: 1 /* Take remaining space in some-container */,
overflowY: 'auto' /* Scroll if content overflows */,
pt: 2, pt: 2,
pl: 1, pl: 1,
pr: 1, pr: 1,
pb: 2, pb: 2,
}}> }}
{messages.length === 0 && <Message {...{ chatSession, message: welcomeMessage, }} />} >
{messages.length === 0 && <Message {...{ chatSession, message: welcomeMessage }} />}
{messages.map((message: ChatMessage) => ( {messages.map((message: ChatMessage) => (
<Message key={message.id} {...{ chatSession, message }} /> <Message key={message.id} {...{ chatSession, message }} />
))} ))}
@ -237,13 +279,15 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
<Message {...{ chatSession, message: streamingMessage }} /> <Message {...{ chatSession, message: streamingMessage }} />
)} )}
{streaming && ( {streaming && (
<Box sx={{ <Box
display: "flex", sx={{
flexDirection: "column", display: 'flex',
alignItems: "center", flexDirection: 'column',
justifyContent: "center", alignItems: 'center',
justifyContent: 'center',
m: 1, m: 1,
}}> }}
>
<PropagateLoader <PropagateLoader
size="10px" size="10px"
loading={streaming} loading={streaming}
@ -254,14 +298,17 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
)} )}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</Scrollable> </Scrollable>
} )}
{selectedCandidate.questions?.length !== 0 && selectedCandidate.questions?.map(q => <BackstoryQuery question={q} />)} {selectedCandidate.questions?.length !== 0 &&
selectedCandidate.questions?.map(q => <BackstoryQuery question={q} />)}
{/* Fixed Message Input */} {/* Fixed Message Input */}
<Box sx={{ display: 'flex', flexShrink: 1, gap: 1 }}> <Box sx={{ display: 'flex', flexShrink: 1, gap: 1 }}>
<DeleteConfirmation <DeleteConfirmation
onDelete={() => { chatSession && onDelete(chatSession); }} onDelete={() => {
chatSession && onDelete(chatSession);
}}
disabled={!chatSession} disabled={!chatSession}
sx={{ minWidth: 'auto', px: 2, maxHeight: "min-content" }} sx={{ minWidth: 'auto', px: 2, maxHeight: 'min-content' }}
action="reset" action="reset"
label="chat session" label="chat session"
title="Reset Chat Session" title="Reset Chat Session"
@ -274,21 +321,30 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
disabled={streaming || loading} disabled={streaming || loading}
/> />
<Tooltip title="Send"> <Tooltip title="Send">
<span style={{ minWidth: 'auto', maxHeight: "min-content", alignSelf: "center" }} <span
style={{
minWidth: 'auto',
maxHeight: 'min-content',
alignSelf: 'center',
}}
> >
<Button <Button
variant="contained" variant="contained"
onClick={() => { sendMessage((backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ""); }} onClick={() => {
sendMessage(
(backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ''
);
}}
disabled={streaming || loading} disabled={streaming || loading}
> >
<SendIcon /> <SendIcon />
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
); );
}); }
);
export { CandidateChatPage }; export { CandidateChatPage };

View File

@ -18,7 +18,7 @@ import {
CardContent, CardContent,
CardActionArea, CardActionArea,
useTheme, useTheme,
useMediaQuery useMediaQuery,
} from '@mui/material'; } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import PersonIcon from '@mui/icons-material/Person'; import PersonIcon from '@mui/icons-material/Person';
@ -61,48 +61,50 @@ const Sidebar: React.FC<{
return ( return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ <Box
sx={{
p: 2, p: 2,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
borderBottom: 1, borderBottom: 1,
borderColor: 'divider', borderColor: 'divider',
}}> }}
>
<Typography variant="h6" component="h2" fontWeight="bold"> <Typography variant="h6" component="h2" fontWeight="bold">
Documentation Documentation
</Typography> </Typography>
{isMobile && onClose && ( {isMobile && onClose && (
<IconButton <IconButton onClick={onClose} size="small" aria-label="Close navigation">
onClick={onClose}
size="small"
aria-label="Close navigation"
>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
)} )}
</Box> </Box>
<Box sx={{ <Box
sx={{
flexGrow: 1, flexGrow: 1,
overflow: 'auto', overflow: 'auto',
p: 1 p: 1,
}}> }}
>
<List> <List>
{documents.map((doc, index) => ( {documents.map((doc, index) => (
<ListItem key={index} disablePadding> <ListItem key={index} disablePadding>
<ListItemButton <ListItemButton
onClick={() => doc.route ? handleItemClick(doc.route) : navigate('/')} onClick={() => (doc.route ? handleItemClick(doc.route) : navigate('/'))}
selected={currentPage === doc.route} selected={currentPage === doc.route}
sx={{ sx={{
borderRadius: 1, borderRadius: 1,
mb: 0.5 mb: 0.5,
}} }}
> >
<ListItemIcon sx={{ <ListItemIcon
sx={{
color: currentPage === doc.route ? 'primary.main' : 'text.secondary', color: currentPage === doc.route ? 'primary.main' : 'text.secondary',
minWidth: 40 minWidth: 40,
}}> }}
>
{getDocumentIcon(doc.title)} {getDocumentIcon(doc.title)}
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
@ -110,7 +112,7 @@ const Sidebar: React.FC<{
slotProps={{ slotProps={{
primary: { primary: {
fontWeight: currentPage === doc.route ? 'medium' : 'regular', fontWeight: currentPage === doc.route ? 'medium' : 'regular',
} },
}} }}
/> />
</ListItemButton> </ListItemButton>
@ -128,7 +130,7 @@ const getDocumentIcon = (title: string): React.ReactNode => {
throw Error(`${title} does not exist in documents`); throw Error(`${title} does not exist in documents`);
} }
return item.icon || <ViewQuiltIcon />; return item.icon || <ViewQuiltIcon />;
} };
type DocType = { type DocType = {
title: string; title: string;
@ -137,26 +139,90 @@ type DocType = {
icon?: React.ReactNode; icon?: React.ReactNode;
}; };
const documents : DocType[] = [ const documents: DocType[] = [
{ title: "Backstory", route: null, description: "Backstory", icon: <ArrowBackIcon /> }, {
{ title: "About", route: "about", description: "General information about the application and its purpose", icon: <DescriptionIcon /> }, title: 'Backstory',
{ title: "BETA", route: "beta", description: "Details about the current beta version and upcoming features", icon: <CodeIcon /> }, route: null,
{ title: "Resume Generation Architecture", route: "resume-generation", description: "Technical overview of how resumes are processed and generated", icon: <LayersIcon /> }, description: 'Backstory',
{ title: "Application Architecture", route: "about-app", description: "System design and technical stack information", icon: <LayersIcon /> }, icon: <ArrowBackIcon />,
{ 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", icon: <DashboardIcon /> }, {
{ title: "UI Mockup", route: "ui-mockup", description: "Visual previews of interfaces and layout concepts", icon: <DashboardIcon /> }, title: 'About',
{ title: "Theme Visualizer", route: "theme-visualizer", description: "Explore and customize application themes and visual styles", icon: <PaletteIcon /> }, route: 'about',
{ title: "App Analysis", route: "app-analysis", description: "Statistics and performance metrics of the application", icon: <AnalyticsIcon /> }, description: 'General information about the application and its purpose',
{ title: 'Text Mockups', route: "backstory-ui-mockups", description: "Early text mockups of many of the interaction points." }, icon: <DescriptionIcon />,
{ 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.", icon: <CodeIcon /> }, {
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',
icon: <LayersIcon />,
},
{
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',
icon: <LayersIcon />,
},
{
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',
icon: <DashboardIcon />,
},
{
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',
icon: <AnalyticsIcon />,
},
{
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.',
icon: <PersonIcon />,
},
{
title: 'Type Safety',
route: 'type-safety',
description: 'Overview of front/back-end type synchronization.',
icon: <CodeIcon />,
},
]; ];
const documentFromRoute = (route: string) : DocType | null => { const documentFromRoute = (route: string): DocType | null => {
const index = documents.findIndex(v => v.route === route); const index = documents.findIndex(v => v.route === route);
if (index === -1) { if (index === -1) {
return null return null;
} }
return documents[index]; return documents[index];
}; };
@ -165,10 +231,10 @@ const documentFromRoute = (route: string) : DocType | null => {
const documentTitleFromRoute = (route: string): string => { const documentTitleFromRoute = (route: string): string => {
const doc = documentFromRoute(route); const doc = documentFromRoute(route);
if (doc === null) { if (doc === null) {
return 'Documentation' return 'Documentation';
} }
return doc.title; return doc.title;
} };
const DocsPage = (props: BackstoryPageProps) => { const DocsPage = (props: BackstoryPageProps) => {
const { setSnack } = useAppState(); const { setSnack } = useAppState();
@ -200,10 +266,10 @@ const DocsPage = (props: BackstoryPageProps) => {
// Handle document navigation // Handle document navigation
const onDocumentExpand = (docName: string, open: boolean) => { const onDocumentExpand = (docName: string, open: boolean) => {
console.log("Document expanded:", { docName, open, location }); console.log('Document expanded:', { docName, open, location });
if (open) { if (open) {
const parts = location.pathname.split('/'); const parts = location.pathname.split('/');
if (docName === "backstory") { if (docName === 'backstory') {
navigate('/'); navigate('/');
return; return;
} }
@ -230,8 +296,8 @@ const DocsPage = (props: BackstoryPageProps) => {
}; };
interface DocViewProps { interface DocViewProps {
page: string page: string;
}; }
const DocView = (props: DocViewProps) => { const DocView = (props: DocViewProps) => {
const { page = 'about' } = props; const { page = 'about' } = props;
const title = documentTitleFromRoute(page); const title = documentTitleFromRoute(page);
@ -240,13 +306,22 @@ const DocsPage = (props: BackstoryPageProps) => {
return ( return (
<Card> <Card>
<CardContent> <CardContent>
<Box sx={{ color: 'inherit', fontSize: "1.75rem", fontWeight: "bold", display: "flex", flexDirection: "row", gap: 1, alignItems: "center", mr: 1.5 }}> <Box
sx={{
color: 'inherit',
fontSize: '1.75rem',
fontWeight: 'bold',
display: 'flex',
flexDirection: 'row',
gap: 1,
alignItems: 'center',
mr: 1.5,
}}
>
{icon} {icon}
{title} {title}
</Box> </Box>
{page && <Document {page && <Document filepath={`/docs/${page}.md`} />}
filepath={`/docs/${page}.md`}
/>}
</CardContent> </CardContent>
</Card> </Card>
); );
@ -256,18 +331,22 @@ const DocsPage = (props: BackstoryPageProps) => {
function renderContent() { function renderContent() {
switch (page) { switch (page) {
case 'ui-overview': case 'ui-overview':
return (<BackstoryUIOverviewPage />); return <BackstoryUIOverviewPage />;
case 'theme-visualizer': case 'theme-visualizer':
return (<Paper sx={{ m: 0, p: 1 }}><BackstoryThemeVisualizerPage /></Paper>); return (
<Paper sx={{ m: 0, p: 1 }}>
<BackstoryThemeVisualizerPage />
</Paper>
);
case 'app-analysis': case 'app-analysis':
return (<BackstoryAppAnalysisPage />); return <BackstoryAppAnalysisPage />;
case 'ui-mockup': case 'ui-mockup':
return (<MockupPage />); return <MockupPage />;
case 'user-management': case 'user-management':
return (<UserManagement />); return <UserManagement />;
default: default:
if (documentFromRoute(page)) { if (documentFromRoute(page)) {
return <DocView page={page}/> return <DocView page={page} />;
} }
// Document grid for landing page // Document grid for landing page
return ( return (
@ -277,19 +356,41 @@ const DocsPage = (props: BackstoryPageProps) => {
Documentation Documentation
</Typography> </Typography>
<Typography variant="body1" color="text.secondary"> <Typography variant="body1" color="text.secondary">
Select a document from the sidebar to view detailed technical information about the application. Select a document from the sidebar to view detailed technical information about the
application.
</Typography> </Typography>
</Box> </Box>
<Grid container spacing={1}> <Grid container spacing={1}>
{documents.map((doc, index) => { {documents.map((doc, index) => {
if (doc.route === null) return (<></>); if (doc.route === null) return <></>;
return (<Grid sx={{ minWidth: "164px" }} size={{ xs: 12, sm: 6, md: 4 }} key={index}> return (
<Card sx={{ minHeight: "180px" }}> <Grid sx={{ minWidth: '164px' }} size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<CardActionArea onClick={() => doc.route ? onDocumentExpand(doc.route, true) : navigate('/')}> <Card sx={{ minHeight: '180px' }}>
<CardContent sx={{ display: "flex", flexDirection: "column", m: 0, p: 1 }}> <CardActionArea
<Box sx={{ display: 'flex', flexDirection: "row", gap: 1, verticalAlign: 'top' }}> onClick={() =>
doc.route ? onDocumentExpand(doc.route, true) : navigate('/')
}
>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
m: 0,
p: 1,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
gap: 1,
verticalAlign: 'top',
}}
>
{getDocumentIcon(doc.title)} {getDocumentIcon(doc.title)}
<Typography variant="h3" sx={{ m: "0 !important" }}>{doc.title}</Typography> <Typography variant="h3" sx={{ m: '0 !important' }}>
{doc.title}
</Typography>
</Box> </Box>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{doc.description} {doc.description}
@ -298,7 +399,7 @@ const DocsPage = (props: BackstoryPageProps) => {
</CardActionArea> </CardActionArea>
</Card> </Card>
</Grid> </Grid>
) );
})} })}
</Grid> </Grid>
</Paper> </Paper>
@ -318,22 +419,17 @@ const DocsPage = (props: BackstoryPageProps) => {
sx={{ sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` }, width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` }, ml: { sm: `${drawerWidth}px` },
display: { md: 'none' } display: { md: 'none' },
}} }}
elevation={0} elevation={0}
color="default" color="default"
> >
<Toolbar> <Toolbar>
<IconButton <IconButton aria-label="open drawer" edge="start" onClick={toggleDrawer} sx={{ mr: 2 }}>
aria-label="open drawer"
edge="start"
onClick={toggleDrawer}
sx={{ mr: 2 }}
>
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography variant="h6" noWrap component="div" sx={{ color: "white" }}> <Typography variant="h6" noWrap component="div" sx={{ color: 'white' }}>
{page ? documentTitleFromRoute(page) : "Documentation"} {page ? documentTitleFromRoute(page) : 'Documentation'}
</Typography> </Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
@ -344,7 +440,7 @@ const DocsPage = (props: BackstoryPageProps) => {
component="nav" component="nav"
sx={{ sx={{
width: { md: drawerWidth }, width: { md: drawerWidth },
flexShrink: { md: 0 } flexShrink: { md: 0 },
}} }}
> >
{/* Mobile drawer (temporary) */} {/* Mobile drawer (temporary) */}
@ -360,7 +456,7 @@ const DocsPage = (props: BackstoryPageProps) => {
display: { xs: 'block', md: 'none' }, display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': { '& .MuiDrawer-paper': {
boxSizing: 'border-box', boxSizing: 'border-box',
width: drawerWidth width: drawerWidth,
}, },
}} }}
> >
@ -381,16 +477,12 @@ const DocsPage = (props: BackstoryPageProps) => {
boxSizing: 'border-box', boxSizing: 'border-box',
width: drawerWidth, width: drawerWidth,
position: 'relative', position: 'relative',
height: '100%' height: '100%',
}, },
}} }}
open open
> >
<Sidebar <Sidebar currentPage={page} onDocumentSelect={onDocumentExpand} isMobile={false} />
currentPage={page}
onDocumentSelect={onDocumentExpand}
isMobile={false}
/>
</Drawer> </Drawer>
)} )}
</Box> </Box>
@ -404,7 +496,7 @@ const DocsPage = (props: BackstoryPageProps) => {
width: { md: `calc(100% - ${drawerWidth}px)` }, width: { md: `calc(100% - ${drawerWidth}px)` },
pt: isMobile ? { xs: 8, sm: 9 } : 3, // Add padding top on mobile to account for AppBar pt: isMobile ? { xs: 8, sm: 9 } : 3, // Add padding top on mobile to account for AppBar
height: '100%', height: '100%',
overflow: 'auto' overflow: 'auto',
}} }}
> >
{renderContent()} {renderContent()}

View File

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

View File

@ -17,13 +17,26 @@ import { StyledMarkdown } from 'components/StyledMarkdown';
import { Scrollable } from '../components/Scrollable'; import { Scrollable } from '../components/Scrollable';
import { Pulse } from 'components/Pulse'; import { Pulse } from 'components/Pulse';
import { StreamingResponse } from 'services/api-client'; import { StreamingResponse } from 'services/api-client';
import { ChatMessage, ChatMessageUser, ChatSession, CandidateAI, ChatMessageStatus, ChatMessageError } from 'types/types'; import {
ChatMessage,
ChatMessageUser,
ChatSession,
CandidateAI,
ChatMessageStatus,
ChatMessageError,
} from 'types/types';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { Message } from 'components/Message'; import { Message } from 'components/Message';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
const defaultMessage: ChatMessage = { const defaultMessage: ChatMessage = {
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "user", metadata: null as any status: 'done',
type: 'text',
sessionId: '',
timestamp: new Date(),
content: '',
role: 'user',
metadata: null as any,
}; };
const GenerateCandidate = (props: BackstoryElementProps) => { const GenerateCandidate = (props: BackstoryElementProps) => {
@ -52,7 +65,12 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
try { try {
setLoading(true); setLoading(true);
apiClient.getOrCreateChatSession(generatedUser, `Profile image generator for ${generatedUser.fullName}`, 'generate_image') apiClient
.getOrCreateChatSession(
generatedUser,
`Profile image generator for ${generatedUser.fullName}`,
'generate_image'
)
.then(session => { .then(session => {
setChatSession(session); setChatSession(session);
setLoading(false); setLoading(false);
@ -72,23 +90,27 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
} }
}, []); }, []);
const onEnter = useCallback((value: string) => { const onEnter = useCallback(
(value: string) => {
if (processing) { if (processing) {
return; return;
} }
const generatePersona = async (prompt: string) => { const generatePersona = async (prompt: string) => {
const userMessage: ChatMessageUser = { const userMessage: ChatMessageUser = {
type: "text", type: 'text',
role: "user", role: 'user',
content: prompt, content: prompt,
sessionId: "", sessionId: '',
status: "done", status: 'done',
timestamp: new Date() timestamp: new Date(),
}; };
setPrompt(prompt || ''); setPrompt(prompt || '');
setProcessing(true); setProcessing(true);
setProcessingMessage({ ...defaultMessage, content: "Generating persona..." }); setProcessingMessage({
...defaultMessage,
content: 'Generating persona...',
});
try { try {
const result = await apiClient.createCandidateAI(userMessage); const result = await apiClient.createCandidateAI(userMessage);
console.log(result.message, result); console.log(result.message, result);
@ -102,15 +124,17 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
setResume(null); setResume(null);
setProcessing(false); setProcessing(false);
setProcessingMessage(null); setProcessingMessage(null);
setSnack("Unable to generate AI persona", "error"); setSnack('Unable to generate AI persona', 'error');
} }
}; };
generatePersona(value); generatePersona(value);
}, [processing, apiClient, setSnack]); },
[processing, apiClient, setSnack]
);
const handleSendClick = useCallback(() => { const handleSendClick = useCallback(() => {
const value = (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ""; const value = (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || '';
onEnter(value); onEnter(value);
}, [onEnter]); }, [onEnter]);
@ -121,26 +145,33 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
} }
const username = generatedUser.username; const username = generatedUser.username;
if (!shouldGenerateProfile || username === "[blank]" || generatedUser?.firstName === "[blank]") { if (
!shouldGenerateProfile ||
username === '[blank]' ||
generatedUser?.firstName === '[blank]'
) {
return; return;
} }
if (controllerRef.current) { if (controllerRef.current) {
console.log("Controller already active, skipping profile generation"); console.log('Controller already active, skipping profile generation');
return; return;
} }
setProcessingMessage({ ...defaultMessage, content: 'Starting image generation...' }); setProcessingMessage({
...defaultMessage,
content: 'Starting image generation...',
});
setProcessing(true); setProcessing(true);
setCanGenImage(false); setCanGenImage(false);
const chatMessage: ChatMessageUser = { const chatMessage: ChatMessageUser = {
sessionId: chatSession.id || '', sessionId: chatSession.id || '',
role: "user", role: 'user',
status: "done", status: 'done',
type: "text", type: 'text',
timestamp: new Date(), timestamp: new Date(),
content: prompt content: prompt,
}; };
controllerRef.current = apiClient.sendMessageStream(chatMessage, { controllerRef.current = apiClient.sendMessageStream(chatMessage, {
@ -148,27 +179,36 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
console.log(`onMessage: ${msg.type} ${msg.content}`, msg); console.log(`onMessage: ${msg.type} ${msg.content}`, msg);
controllerRef.current = null; controllerRef.current = null;
try { try {
await apiClient.updateCandidate(generatedUser.id || '', { 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 ' : ''} deleted: ${message}}`); console.log(
`Profile generated for ${username} and chat session was ${
!success ? 'not ' : ''
} deleted: ${message}}`
);
setGeneratedUser({ setGeneratedUser({
...generatedUser, ...generatedUser,
profileImage: "profile.png" profileImage: 'profile.png',
} as CandidateAI); } as CandidateAI);
setCanGenImage(true); setCanGenImage(true);
setShouldGenerateProfile(false); setShouldGenerateProfile(false);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setSnack(`Unable to update ${username} to indicate they have a profile picture.`, "error"); setSnack(
`Unable to update ${username} to indicate they have a profile picture.`,
'error'
);
} }
}, },
onError: (error: string | ChatMessageError) => { onError: (error: string | ChatMessageError) => {
console.log("onError:", error); console.log('onError:', error);
// Type-guard to determine if this is a ChatMessageBase or a string // Type-guard to determine if this is a ChatMessageBase or a string
if (typeof error === "object" && error !== null && "content" in error) { if (typeof error === 'object' && error !== null && 'content' in error) {
setSnack(error.content || "Unknown error generating profile image", "error"); setSnack(error.content || 'Unknown error generating profile image', 'error');
} else { } else {
setSnack(error as string, "error"); setSnack(error as string, 'error');
} }
setProcessingMessage(null); setProcessingMessage(null);
setProcessing(false); setProcessing(false);
@ -184,7 +224,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
setShouldGenerateProfile(false); setShouldGenerateProfile(false);
}, },
onStatus: (status: ChatMessageStatus) => { onStatus: (status: ChatMessageStatus) => {
if (status.activity === "heartbeat" && status.content) { if (status.activity === 'heartbeat' && status.content) {
setTimestamp(status.timestamp?.toISOString() || ''); setTimestamp(status.timestamp?.toISOString() || '');
} else if (status.content) { } else if (status.content) {
setProcessingMessage({ ...defaultMessage, content: status.content }); setProcessingMessage({ ...defaultMessage, content: status.content });
@ -195,33 +235,35 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
}, [chatSession, shouldGenerateProfile, generatedUser, prompt, setSnack, apiClient]); }, [chatSession, shouldGenerateProfile, generatedUser, prompt, setSnack, apiClient]);
if (!user?.isAdmin) { 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 ( return (
<Box className="GenerateCandidate" sx={{ <Box
display: "flex", className="GenerateCandidate"
flexDirection: "column", sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
gap: 1, gap: 1,
maxWidth: { xs: '100%', md: '700px', lg: '1024px' }, maxWidth: { xs: '100%', md: '700px', lg: '1024px' },
}}> }}
{generatedUser && <CandidateInfo >
candidate={generatedUser} {generatedUser && <CandidateInfo candidate={generatedUser} sx={{ flexShrink: 1 }} />}
sx={{flexShrink: 1}}/> {prompt && <Quote quote={prompt} />}
} {processing && (
{ prompt && <Box
<Quote quote={prompt}/> sx={{
} display: 'flex',
{processing && flexDirection: 'column',
<Box sx={{ alignItems: 'center',
display: "flex", justifyContent: 'center',
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
m: 2, m: 2,
}}> }}
{processingMessage && chatSession && <Message message={processingMessage} {...{ chatSession }} />} >
{processingMessage && chatSession && (
<Message message={processingMessage} {...{ chatSession }} />
)}
<PropagateLoader <PropagateLoader
size="10px" size="10px"
loading={processing} loading={processing}
@ -229,16 +271,29 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
data-testid="loader" data-testid="loader"
/> />
</Box> </Box>
} )}
<Box sx={{display: "flex", flexDirection: "column"}}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box sx={{ <Box
display: "flex", sx={{
flexDirection: "row", display: 'flex',
position: "relative" flexDirection: 'row',
}}> position: 'relative',
<Box sx={{ display: "flex", position: "relative", width: "min-content", height: "min-content" }}> }}
>
<Box
sx={{
display: 'flex',
position: 'relative',
width: 'min-content',
height: 'min-content',
}}
>
<Avatar <Avatar
src={generatedUser?.profileImage ? `/api/1.0/candidates/profile/${generatedUser.username}` : ''} src={
generatedUser?.profileImage
? `/api/1.0/candidates/profile/${generatedUser.username}`
: ''
}
alt={`${generatedUser?.fullName}'s profile`} alt={`${generatedUser?.fullName}'s profile`}
sx={{ sx={{
width: 80, width: 80,
@ -246,31 +301,50 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
border: '2px solid #e0e0e0', border: '2px solid #e0e0e0',
}} }}
/> />
{processing && <Pulse sx={{ position: "relative", left: "-80px", top: "0px", mr: "-80px" }} timestamp={timestamp} />} {processing && (
<Pulse
sx={{
position: 'relative',
left: '-80px',
top: '0px',
mr: '-80px',
}}
timestamp={timestamp}
/>
)}
</Box> </Box>
<Tooltip title={`${generatedUser?.profileImage ? 'Re-' : ''}Generate Picture`}> <Tooltip title={`${generatedUser?.profileImage ? 'Re-' : ''}Generate Picture`}>
<span style={{ display: "flex", flexGrow: 1 }}> <span style={{ display: 'flex', flexGrow: 1 }}>
<Button <Button
sx={{ m: 1, gap: 1, justifySelf: "flex-start", alignSelf: "center", flexGrow: 0, maxHeight: "min-content" }} sx={{
m: 1,
gap: 1,
justifySelf: 'flex-start',
alignSelf: 'center',
flexGrow: 0,
maxHeight: 'min-content',
}}
variant="contained" variant="contained"
disabled={ disabled={processing || !canGenImage}
processing || !canGenImage onClick={() => {
} setShouldGenerateProfile(true);
onClick={() => { setShouldGenerateProfile(true); }}> }}
{generatedUser?.profileImage ? 'Re-' : ''}Generate Picture<SendIcon /> >
{generatedUser?.profileImage ? 'Re-' : ''}Generate Picture
<SendIcon />
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
{resume && {resume && (
<Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}> <Paper sx={{ pt: 1, pb: 1, pl: 2, pr: 2 }}>
<Scrollable sx={{flexGrow: 1}}> <Scrollable sx={{ flexGrow: 1 }}>
<StyledMarkdown content={resume} /> <StyledMarkdown content={resume} />
</Scrollable> </Scrollable>
</Paper> </Paper>
} )}
<BackstoryTextField <BackstoryTextField
style={{ flexGrow: 0, flexShrink: 1 }} style={{ flexGrow: 0, flexShrink: 1 }}
ref={backstoryTextRef} ref={backstoryTextRef}
@ -278,24 +352,28 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
onEnter={onEnter} onEnter={onEnter}
placeholder='Specify any characteristics you would like the persona to have. For example, "This person likes yo-yos."' placeholder='Specify any characteristics you would like the persona to have. For example, "This person likes yo-yos."'
/> />
<Box sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}> <Box sx={{ display: 'flex', justifyContent: 'center', flexDirection: 'row' }}>
<Tooltip title={"Send"}> <Tooltip title={'Send'}>
<span style={{ display: "flex", flexGrow: 1 }}> <span style={{ display: 'flex', flexGrow: 1 }}>
<Button <Button
sx={{ m: 1, gap: 1, flexGrow: 1 }} sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained" variant="contained"
disabled={processing} disabled={processing}
onClick={handleSendClick}> onClick={handleSendClick}
Generate New Persona<SendIcon /> >
Generate New Persona
<SendIcon />
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
<Tooltip title="Cancel"> <Tooltip title="Cancel">
<span style={{ display: "flex" }}> { /* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} <span style={{ display: 'flex' }}>
{' '}
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton <IconButton
aria-label="cancel" aria-label="cancel"
onClick={cancelQuery} onClick={cancelQuery}
sx={{ display: "flex", margin: 'auto 0px' }} sx={{ display: 'flex', margin: 'auto 0px' }}
size="large" size="large"
edge="start" edge="start"
disabled={controllerRef.current === null || processing === false} disabled={controllerRef.current === null || processing === false}
@ -305,10 +383,9 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
</span> </span>
</Tooltip> </Tooltip>
</Box> </Box>
<Box sx={{display: "flex", flexGrow: 1}}/> <Box sx={{ display: 'flex', flexGrow: 1 }} />
</Box>); </Box>
);
}; };
export { export { GenerateCandidate };
GenerateCandidate
};

View File

@ -65,11 +65,12 @@ const HeroButton = (props: HeroButtonProps) => {
opacity: 0.9, opacity: 0.9,
}, },
})); }));
return <HeroStyledButton onClick={onClick ? onClick : handleClick} {...rest}> return (
<HeroStyledButton onClick={onClick ? onClick : handleClick} {...rest}>
{children} {children}
</HeroStyledButton> </HeroStyledButton>
} );
};
interface ActionButtonProps extends ButtonProps { interface ActionButtonProps extends ButtonProps {
children?: string; children?: string;
@ -84,10 +85,12 @@ const ActionButton = (props: ActionButtonProps) => {
navigate(path); navigate(path);
}; };
return <Button onClick={onClick ? onClick : handleClick} {...rest}> return (
<Button onClick={onClick ? onClick : handleClick} {...rest}>
{children} {children}
</Button> </Button>
} );
};
const FeatureIcon = styled(Box)(({ theme }) => ({ const FeatureIcon = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.action.active, backgroundColor: theme.palette.action.active,
@ -106,7 +109,7 @@ const FeatureIcon = styled(Box)(({ theme }) => ({
const FeatureCard = ({ const FeatureCard = ({
icon, icon,
title, title,
description description,
}: { }: {
icon: React.ReactNode; icon: React.ReactNode;
title: string; title: string;
@ -140,24 +143,27 @@ const HomePage = () => {
if (isGuest) { if (isGuest) {
// Show guest-specific UI // Show guest-specific UI
console.log('Guest session:', guest?.sessionId || "No guest"); console.log('Guest session:', guest?.sessionId || 'No guest');
} else { } else {
// Show authenticated user UI // Show authenticated user UI
console.log('Authenticated user:', user?.email || "No user"); console.log('Authenticated user:', user?.email || 'No user');
} }
return (<Box sx={{display: "flex", flexDirection: "column"}}> return (
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{/* Hero Section */} {/* Hero Section */}
<HeroSection> <HeroSection>
<Container> <Container>
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', md: 'row' }, flexDirection: { xs: 'column', md: 'row' },
gap: 4, gap: 4,
alignItems: 'center', alignItems: 'center',
flexGrow: 1, flexGrow: 1,
maxWidth: "1024px" maxWidth: '1024px',
}}> }}
>
<Box sx={{ flex: 1, flexGrow: 1 }}> <Box sx={{ flex: 1, flexGrow: 1 }}>
<Typography <Typography
variant="h2" variant="h2"
@ -166,19 +172,17 @@ const HomePage = () => {
fontWeight: 700, fontWeight: 700,
fontSize: { xs: '2rem', md: '3rem' }, fontSize: { xs: '2rem', md: '3rem' },
mb: 2, mb: 2,
color: "white" color: 'white',
}} }}
> >
Your complete professional story, beyond a single page Your complete professional story, beyond a single page
</Typography> </Typography>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}> <Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}>
Let potential employers discover the depth of your experience through interactive Q&A and tailored resumes Let potential employers discover the depth of your experience through interactive
Q&A and tailored resumes
</Typography> </Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<HeroButton <HeroButton variant="contained" size="large">
variant="contained"
size="large"
>
Get Started as Candidate Get Started as Candidate
</HeroButton> </HeroButton>
<HeroButton <HeroButton
@ -187,14 +191,19 @@ const HomePage = () => {
sx={{ sx={{
backgroundColor: 'transparent', backgroundColor: 'transparent',
border: '2px solid', border: '2px solid',
borderColor: 'action.active' borderColor: 'action.active',
}} }}
> >
Recruit Talent Recruit Talent
</HeroButton> </HeroButton>
</Stack> </Stack>
</Box> </Box>
<Box sx={{ justifyContent: "center", display: { xs: 'none', md: 'block' } }}> <Box
sx={{
justifyContent: 'center',
display: { xs: 'none', md: 'block' },
}}
>
<Box <Box
component="img" component="img"
src={professionalConversationPng} src={professionalConversationPng}
@ -224,20 +233,28 @@ const HomePage = () => {
How Backstory Works How Backstory Works
</Typography> </Typography>
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 4 }}> <Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
gap: 4,
}}
>
<Box sx={{ flex: 1 }}> <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 For Job Seekers
</Typography> </Typography>
<Box sx={{ my: 3 }}> <Box sx={{ my: 3 }}>
<Typography variant="body1" paragraph> <Typography variant="body1" paragraph>
Backstory helps you tell your complete professional story, 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> </Typography>
</Box> </Box>
<Stack spacing={3}> <Stack spacing={3}>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box sx={{ <Box
sx={{
backgroundColor: 'primary.main', backgroundColor: 'primary.main',
color: 'primary.contrastText', color: 'primary.contrastText',
borderRadius: '50%', borderRadius: '50%',
@ -249,8 +266,9 @@ const HomePage = () => {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
mr: 2, mr: 2,
fontWeight: 'bold' fontWeight: 'bold',
}}> }}
>
1 1
</Box> </Box>
<Typography variant="body1"> <Typography variant="body1">
@ -259,7 +277,8 @@ const HomePage = () => {
</Box> </Box>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box sx={{ <Box
sx={{
backgroundColor: 'primary.main', backgroundColor: 'primary.main',
color: 'primary.contrastText', color: 'primary.contrastText',
borderRadius: '50%', borderRadius: '50%',
@ -271,8 +290,9 @@ const HomePage = () => {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
mr: 2, mr: 2,
fontWeight: 'bold' fontWeight: 'bold',
}}> }}
>
2 2
</Box> </Box>
<Typography variant="body1"> <Typography variant="body1">
@ -281,7 +301,8 @@ const HomePage = () => {
</Box> </Box>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box sx={{ <Box
sx={{
backgroundColor: 'primary.main', backgroundColor: 'primary.main',
color: 'primary.contrastText', color: 'primary.contrastText',
borderRadius: '50%', borderRadius: '50%',
@ -293,8 +314,9 @@ const HomePage = () => {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
mr: 2, mr: 2,
fontWeight: 'bold' fontWeight: 'bold',
}}> }}
>
3 3
</Box> </Box>
<Typography variant="body1"> <Typography variant="body1">
@ -320,13 +342,15 @@ const HomePage = () => {
</Typography> </Typography>
<Box sx={{ my: 3 }}> <Box sx={{ my: 3 }}>
<Typography variant="body1" paragraph> <Typography variant="body1" paragraph>
Discover candidates with the perfect skills and experience for 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> </Typography>
</Box> </Box>
<Stack spacing={3}> <Stack spacing={3}>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box sx={{ <Box
sx={{
backgroundColor: 'secondary.main', backgroundColor: 'secondary.main',
color: 'secondary.contrastText', color: 'secondary.contrastText',
borderRadius: '50%', borderRadius: '50%',
@ -338,8 +362,9 @@ const HomePage = () => {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
mr: 2, mr: 2,
fontWeight: 'bold' fontWeight: 'bold',
}}> }}
>
1 1
</Box> </Box>
<Typography variant="body1"> <Typography variant="body1">
@ -348,7 +373,8 @@ const HomePage = () => {
</Box> </Box>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box sx={{ <Box
sx={{
backgroundColor: 'secondary.main', backgroundColor: 'secondary.main',
color: 'secondary.contrastText', color: 'secondary.contrastText',
borderRadius: '50%', borderRadius: '50%',
@ -360,8 +386,9 @@ const HomePage = () => {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
mr: 2, mr: 2,
fontWeight: 'bold' fontWeight: 'bold',
}}> }}
>
2 2
</Box> </Box>
<Typography variant="body1"> <Typography variant="body1">
@ -370,7 +397,8 @@ const HomePage = () => {
</Box> </Box>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box sx={{ <Box
sx={{
backgroundColor: 'secondary.main', backgroundColor: 'secondary.main',
color: 'secondary.contrastText', color: 'secondary.contrastText',
borderRadius: '50%', borderRadius: '50%',
@ -382,8 +410,9 @@ const HomePage = () => {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
mr: 2, mr: 2,
fontWeight: 'bold' fontWeight: 'bold',
}}> }}
>
3 3
</Box> </Box>
<Typography variant="body1"> <Typography variant="body1">
@ -419,7 +448,16 @@ const HomePage = () => {
</Typography> </Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
<Box sx={{ flex: '1 1 250px', minWidth: { xs: '100%', sm: 'calc(50% - 16px)', md: 'calc(25% - 16px)' } }}> <Box
sx={{
flex: '1 1 250px',
minWidth: {
xs: '100%',
sm: 'calc(50% - 16px)',
md: 'calc(25% - 16px)',
},
}}
>
<FeatureCard <FeatureCard
icon={ icon={
<FeatureIcon> <FeatureIcon>
@ -430,7 +468,16 @@ const HomePage = () => {
description="Find the perfect candidates based on skills, experience, and fit for your specific requirements." description="Find the perfect candidates based on skills, experience, and fit for your specific requirements."
/> />
</Box> </Box>
<Box sx={{ flex: '1 1 250px', minWidth: { xs: '100%', sm: 'calc(50% - 16px)', md: 'calc(25% - 16px)' } }}> <Box
sx={{
flex: '1 1 250px',
minWidth: {
xs: '100%',
sm: 'calc(50% - 16px)',
md: 'calc(25% - 16px)',
},
}}
>
<FeatureCard <FeatureCard
icon={ icon={
<FeatureIcon> <FeatureIcon>
@ -441,7 +488,16 @@ const HomePage = () => {
description="Share your full professional journey beyond the limitations of a traditional resume." description="Share your full professional journey beyond the limitations of a traditional resume."
/> />
</Box> </Box>
<Box sx={{ flex: '1 1 250px', minWidth: { xs: '100%', sm: 'calc(50% - 16px)', md: 'calc(25% - 16px)' } }}> <Box
sx={{
flex: '1 1 250px',
minWidth: {
xs: '100%',
sm: 'calc(50% - 16px)',
md: 'calc(25% - 16px)',
},
}}
>
<FeatureCard <FeatureCard
icon={ icon={
<FeatureIcon> <FeatureIcon>
@ -452,7 +508,16 @@ const HomePage = () => {
description="Ask detailed questions about a candidate's experience and get immediate answers." description="Ask detailed questions about a candidate's experience and get immediate answers."
/> />
</Box> </Box>
<Box sx={{ flex: '1 1 250px', minWidth: { xs: '100%', sm: 'calc(50% - 16px)', md: 'calc(25% - 16px)' } }}> <Box
sx={{
flex: '1 1 250px',
minWidth: {
xs: '100%',
sm: 'calc(50% - 16px)',
md: 'calc(25% - 16px)',
},
}}
>
<FeatureCard <FeatureCard
icon={ icon={
<FeatureIcon> <FeatureIcon>
@ -468,7 +533,7 @@ const HomePage = () => {
</Box> </Box>
{/* Testimonials Section */} {/* Testimonials Section */}
{testimonials && {testimonials && (
<Container sx={{ py: 8 }}> <Container sx={{ py: 8 }}>
<Typography <Typography
variant="h3" variant="h3"
@ -479,48 +544,41 @@ const HomePage = () => {
> >
Success Stories Success Stories
</Typography> </Typography>
<Typography <Typography variant="body1" align="center" sx={{ mb: 6, maxWidth: 800, mx: 'auto' }}>
variant="body1"
align="center"
sx={{ mb: 6, maxWidth: 800, mx: 'auto' }}
>
See how Backstory has transformed the hiring process for both candidates and employers. See how Backstory has transformed the hiring process for both candidates and employers.
</Typography> </Typography>
<Testimonials /> <Testimonials />
</Container> </Container>
} )}
{/* CTA Section */} {/* CTA Section */}
<Box sx={{ <Box
sx={{
backgroundColor: 'primary.main', backgroundColor: 'primary.main',
color: 'primary.contrastText', color: 'primary.contrastText',
py: 8 py: 8,
}}> }}
>
<Container> <Container>
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
textAlign: 'center', textAlign: 'center',
maxWidth: 800, maxWidth: 800,
mx: 'auto' mx: 'auto',
}}> }}
<Typography variant="h3" component="h2" gutterBottom sx={{ color: "white" }}> >
<Typography variant="h3" component="h2" gutterBottom sx={{ color: 'white' }}>
Ready to transform your hiring process? Ready to transform your hiring process?
</Typography> </Typography>
<Typography variant="h6" sx={{ mb: 4 }}> <Typography variant="h6" sx={{ mb: 4 }}>
Join Backstory today and discover a better way to connect talent with opportunity. Join Backstory today and discover a better way to connect talent with opportunity.
</Typography> </Typography>
<Stack <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} justifyContent="center">
direction={{ xs: 'column', sm: 'row' }} <HeroButton variant="contained" size="large">
spacing={2}
justifyContent="center"
>
<HeroButton
variant="contained"
size="large"
>
Sign Up as Candidate Sign Up as Candidate
</HeroButton> </HeroButton>
<HeroButton <HeroButton
@ -529,7 +587,7 @@ const HomePage = () => {
sx={{ sx={{
backgroundColor: 'transparent', backgroundColor: 'transparent',
border: '2px solid', border: '2px solid',
borderColor: 'action.active' borderColor: 'action.active',
}} }}
> >
Sign Up as Employer Sign Up as Employer
@ -542,7 +600,4 @@ const HomePage = () => {
); );
}; };
export { export { HomePage };
HomePage
};

View File

@ -96,7 +96,7 @@ const steps = [
'Select a Candidate', 'Select a Candidate',
'Start Assessment', 'Start Assessment',
'Review Results', 'Review Results',
'Generate Resume' 'Generate Resume',
]; ];
interface StepContentProps { interface StepContentProps {
@ -122,7 +122,7 @@ const StepContent: React.FC<StepContentProps> = ({
imageAlt, imageAlt,
note, note,
success, success,
reversed = false reversed = false,
}) => { }) => {
const textContent = ( const textContent = (
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
@ -132,7 +132,14 @@ const StepContent: React.FC<StepContentProps> = ({
<Typography variant="h3" component="h2" sx={{ color: 'primary.main', mb: 1 }}> <Typography variant="h3" component="h2" sx={{ color: 'primary.main', mb: 1 }}>
{title} {title}
</Typography> </Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', justifyContent: { xs: 'center', md: 'flex-start' } }}> <Box
sx={{
display: 'flex',
gap: 1,
alignItems: 'center',
justifyContent: { xs: 'center', md: 'flex-start' },
}}
>
{icon} {icon}
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
{subtitle} {subtitle}
@ -146,14 +153,29 @@ const StepContent: React.FC<StepContentProps> = ({
</Typography> </Typography>
))} ))}
{note && ( {note && (
<Paper sx={{ p: 2, backgroundColor: 'action.hover', border: '1px solid', borderColor: 'action.active', mt: 2 }}> <Paper
sx={{
p: 2,
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} <strong>Note:</strong> {note}
</Typography> </Typography>
</Paper> </Paper>
)} )}
{success && ( {success && (
<Paper sx={{ p: 2, backgroundColor: 'secondary.main', color: 'secondary.contrastText', mt: 2 }}> <Paper
sx={{
p: 2,
backgroundColor: 'secondary.main',
color: 'secondary.contrastText',
mt: 2,
}}
>
<Typography variant="body1" sx={{ fontWeight: 'bold' }}> <Typography variant="body1" sx={{ fontWeight: 'bold' }}>
🎉 {success} 🎉 {success}
</Typography> </Typography>
@ -210,10 +232,12 @@ const HeroButton = (props: HeroButtonProps) => {
opacity: 0.9, opacity: 0.9,
}, },
})); }));
return <HeroStyledButton onClick={onClick ? onClick : handleClick} {...rest}> return (
<HeroStyledButton onClick={onClick ? onClick : handleClick} {...rest}>
{children} {children}
</HeroStyledButton> </HeroStyledButton>
} );
};
const HowItWorks: React.FC = () => { const HowItWorks: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -225,19 +249,21 @@ const HowItWorks: React.FC = () => {
}; };
return ( return (
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
{/* Hero Section */} {/* Hero Section */}
{/* Hero Section */} {/* Hero Section */}
<HeroSection> <HeroSection>
<Container> <Container>
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', md: 'row' }, flexDirection: { xs: 'column', md: 'row' },
gap: 4, gap: 4,
alignItems: 'center', alignItems: 'center',
flexGrow: 1, flexGrow: 1,
maxWidth: "1024px" maxWidth: '1024px',
}}> }}
>
<Box sx={{ flex: 1, flexGrow: 1 }}> <Box sx={{ flex: 1, flexGrow: 1 }}>
<Typography <Typography
variant="h2" variant="h2"
@ -246,20 +272,17 @@ const HowItWorks: React.FC = () => {
fontWeight: 700, fontWeight: 700,
fontSize: { xs: '2rem', md: '3rem' }, fontSize: { xs: '2rem', md: '3rem' },
mb: 2, mb: 2,
color: "white" color: 'white',
}} }}
> >
Your complete professional story, beyond a single page Your complete professional story, beyond a single page
</Typography> </Typography>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}> <Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}>
Let potential employers discover the depth of your experience through interactive Q&A and tailored resumes Let potential employers discover the depth of your experience through interactive
Q&A and tailored resumes
</Typography> </Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<HeroButton <HeroButton variant="contained" size="large" path="/login/register">
variant="contained"
size="large"
path="/login/register"
>
Get Started as Candidate Get Started as Candidate
</HeroButton> </HeroButton>
{/* <HeroButton {/* <HeroButton
@ -275,7 +298,12 @@ const HowItWorks: React.FC = () => {
</HeroButton> */} </HeroButton> */}
</Stack> </Stack>
</Box> </Box>
<Box sx={{ justifyContent: "center", display: { xs: 'none', md: 'block' } }}> <Box
sx={{
justifyContent: 'center',
display: { xs: 'none', md: 'block' },
}}
>
<Box <Box
component="img" component="img"
src={professionalConversationPng} src={professionalConversationPng}
@ -292,10 +320,26 @@ const HowItWorks: React.FC = () => {
</Box> </Box>
</Container> </Container>
</HeroSection> </HeroSection>
<HeroSection sx={{ display: "flex", position: "relative", overflow: "hidden", border: "2px solid orange" }}> <HeroSection
<Beta adaptive={false} sx={{ left: "-90px" }} /> sx={{
<Container sx={{ display: "flex", position: "relative" }}> display: 'flex',
<Box sx={{ display: "flex", flexDirection: "column", textAlign: 'center', maxWidth: 800, mx: 'auto', position: "relative" }}> position: 'relative',
overflow: 'hidden',
border: '2px solid orange',
}}
>
<Beta adaptive={false} sx={{ left: '-90px' }} />
<Container sx={{ display: 'flex', position: 'relative' }}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
textAlign: 'center',
maxWidth: 800,
mx: 'auto',
position: 'relative',
}}
>
<Typography <Typography
variant="h2" variant="h2"
component="h1" component="h1"
@ -303,7 +347,7 @@ const HowItWorks: React.FC = () => {
fontWeight: 700, fontWeight: 700,
fontSize: { xs: '2rem', md: '2.5rem' }, fontSize: { xs: '2rem', md: '2.5rem' },
mb: 2, mb: 2,
color: "white" color: 'white',
}} }}
> >
Welcome to the Backstory Beta! Welcome to the Backstory Beta!
@ -319,7 +363,7 @@ const HowItWorks: React.FC = () => {
<Container sx={{ py: 4 }}> <Container sx={{ py: 4 }}>
<Box sx={{ display: { xs: 'none', md: 'block' } }}> <Box sx={{ display: { xs: 'none', md: 'block' } }}>
<Stepper alternativeLabel sx={{ mb: 4 }}> <Stepper alternativeLabel sx={{ mb: 4 }}>
{steps.map((label) => ( {steps.map(label => (
<Step key={label}> <Step key={label}>
<StepLabel>{label}</StepLabel> <StepLabel>{label}</StepLabel>
</Step> </Step>
@ -337,7 +381,7 @@ const HowItWorks: React.FC = () => {
subtitle="Navigate to the main feature" subtitle="Navigate to the main feature"
icon={<AssessmentIcon sx={{ color: 'action.active' }} />} icon={<AssessmentIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"Select 'Job Analysis' from the menu. This takes you to the interactive Job Analysis page, where you will get to evaluate a candidate for a selected job." "Select 'Job Analysis' from the menu. This takes you to the interactive Job Analysis page, where you will get to evaluate a candidate for a selected job.",
]} ]}
imageSrc={selectJobAnalysisPng} imageSrc={selectJobAnalysisPng}
imageAlt="Select Job Analysis from menu" imageAlt="Select Job Analysis from menu"
@ -354,7 +398,7 @@ const HowItWorks: React.FC = () => {
subtitle="Pick from existing job postings" subtitle="Pick from existing job postings"
icon={<WorkIcon sx={{ color: 'action.active' }} />} icon={<WorkIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"Once on the Job Analysis Page, explore a little bit and then select one of the jobs. The requirements and information provided on Backstory are extracted from job postings that users have pasted as a job description or uploaded from a PDF." 'Once on the Job Analysis Page, explore a little bit and then select one of the jobs. The requirements and information provided on Backstory are extracted from job postings that users have pasted as a job description or uploaded from a PDF.',
]} ]}
imageSrc={selectAJobPng} imageSrc={selectAJobPng}
imageAlt="Select a job from the available options" imageAlt="Select a job from the available options"
@ -373,7 +417,7 @@ const HowItWorks: React.FC = () => {
subtitle="Choose from available profiles" subtitle="Choose from available profiles"
icon={<PersonIcon sx={{ color: 'action.active' }} />} icon={<PersonIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"Now that you have a Job selected, you need to select a candidate. In addition to myself (James), there are several candidates which AI has generated. Each has a unique skillset and can be used to test out the system." 'Now that you have a Job selected, you need to select a candidate. In addition to myself (James), there are several candidates which AI has generated. Each has a unique skillset and can be used to test out the system.',
]} ]}
imageSrc={selectACandidatePng} imageSrc={selectACandidatePng}
imageAlt="Select a candidate from the available profiles" imageAlt="Select a candidate from the available profiles"
@ -391,9 +435,9 @@ const HowItWorks: React.FC = () => {
subtitle="Begin the AI analysis" subtitle="Begin the AI analysis"
icon={<PlayArrowIcon sx={{ color: 'action.active' }} />} icon={<PlayArrowIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"After selecting a candidate, you are ready to have Backstory perform the Job Analysis. During this phase, Backstory will take each of requirements extracted from the Job and match it against information about the selected candidate.", 'After selecting a candidate, you are ready to have Backstory perform the Job Analysis. During this phase, Backstory will take each of requirements extracted from the Job and match it against information about the selected candidate.',
"This could be as little as a simple resume, or as complete as a full work history. Backstory performs similarity searches to identify key elements from the candidate that pertain to a given skill and provides a graded response.", 'This could be as little as a simple resume, or as complete as a full work history. Backstory performs similarity searches to identify key elements from the candidate that pertain to a given skill and provides a graded response.',
"To see that in action, click the \"Start Skill Assessment\" button." 'To see that in action, click the "Start Skill Assessment" button.',
]} ]}
imageSrc={selectStartAnalysisPng} imageSrc={selectStartAnalysisPng}
imageAlt="Start the skill assessment process" imageAlt="Start the skill assessment process"
@ -411,8 +455,8 @@ const HowItWorks: React.FC = () => {
subtitle="Watch the magic happen" subtitle="Watch the magic happen"
icon={<AutoAwesomeIcon sx={{ color: 'action.active' }} />} icon={<AutoAwesomeIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"Once you begin that action, the Start Skill Assessment button will grey out and the page will begin updating as it collates information about the candidate. As Backstory performs its magic, you can monitor the progress and explore the different identified skills to see how or why a candidate does or does not have that skill.", 'Once you begin that action, the Start Skill Assessment button will grey out and the page will begin updating as it collates information about the candidate. As Backstory performs its magic, you can monitor the progress and explore the different identified skills to see how or why a candidate does or does not have that skill.',
"Once it is done, you can see the final Overall Match. This is a weighted score based on amount of evidence a skill had, whether the skill was required or preferred, and other metrics." 'Once it is done, you can see the final Overall Match. This is a weighted score based on amount of evidence a skill had, whether the skill was required or preferred, and other metrics.',
]} ]}
imageSrc={waitPng} imageSrc={waitPng}
imageAlt="Wait for the analysis to complete and review results" imageAlt="Wait for the analysis to complete and review results"
@ -429,8 +473,8 @@ const HowItWorks: React.FC = () => {
subtitle="Create your tailored resume" subtitle="Create your tailored resume"
icon={<DescriptionIcon sx={{ color: 'action.active' }} />} icon={<DescriptionIcon sx={{ color: 'action.active' }} />}
description={[ description={[
"The final step is creating the custom resume for the Candidate tailored to the particular Job. On the bottom right you can click \"Next\" to have Backstory generate the custom resume.", 'The final step is creating the custom resume for the Candidate tailored to the particular Job. On the bottom right you can click "Next" to have Backstory generate the custom resume.',
"Note that the resume focuses on identifying key areas from the Candidate's work history that align with skills which were extracted from the original job posting." "Note that the resume focuses on identifying key areas from the Candidate's work history that align with skills which were extracted from the original job posting.",
]} ]}
imageSrc={finalResumePng} imageSrc={finalResumePng}
imageAlt="Generated custom resume tailored to the job" imageAlt="Generated custom resume tailored to the job"
@ -441,21 +485,25 @@ const HowItWorks: React.FC = () => {
</StepSection> </StepSection>
{/* CTA Section */} {/* CTA Section */}
<Box sx={{ <Box
sx={{
backgroundColor: 'primary.main', backgroundColor: 'primary.main',
color: 'primary.contrastText', color: 'primary.contrastText',
py: 6 py: 6,
}}> }}
>
<Container> <Container>
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
textAlign: 'center', textAlign: 'center',
maxWidth: 600, maxWidth: 600,
mx: 'auto' mx: 'auto',
}}> }}
<Typography variant="h3" component="h2" gutterBottom sx={{ color: "white" }}> >
<Typography variant="h3" component="h2" gutterBottom sx={{ color: 'white' }}>
Ready to try Backstory? Ready to try Backstory?
</Typography> </Typography>
<Typography variant="h6" sx={{ mb: 4 }}> <Typography variant="h6" sx={{ mb: 4 }}>

View File

@ -16,15 +16,12 @@ import {
useMediaQuery, useMediaQuery,
Divider, Divider,
} from '@mui/material'; } from '@mui/material';
import { import { Add, WorkOutline } from '@mui/icons-material';
Add,
WorkOutline,
} from '@mui/icons-material';
import PersonIcon from '@mui/icons-material/Person'; import PersonIcon from '@mui/icons-material/Person';
import WorkIcon from '@mui/icons-material/Work'; import WorkIcon from '@mui/icons-material/Work';
import AssessmentIcon from '@mui/icons-material/Assessment'; import AssessmentIcon from '@mui/icons-material/Assessment';
import { JobMatchAnalysis } from 'components/JobMatchAnalysis'; import { JobMatchAnalysis } from 'components/JobMatchAnalysis';
import { Candidate, Job, SkillAssessment } from "types/types"; import { Candidate, Job, SkillAssessment } from 'types/types';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { BackstoryPageProps } from 'components/BackstoryTab'; import { BackstoryPageProps } from 'components/BackstoryTab';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
@ -43,10 +40,12 @@ import { JobInfo } from 'components/ui/JobInfo';
function WorkAddIcon() { function WorkAddIcon() {
return ( return (
<Box position="relative" display="inline-flex" <Box
position="relative"
display="inline-flex"
sx={{ sx={{
lineHeight: "30px", lineHeight: '30px',
mb: "6px", mb: '6px',
}} }}
> >
<WorkOutline sx={{ fontSize: 24 }} /> <WorkOutline sx={{ fontSize: 24 }} />
@ -71,7 +70,7 @@ interface AnalysisState {
candidate: Candidate | null; candidate: Candidate | null;
analysis: SkillAssessment[] | null; analysis: SkillAssessment[] | null;
resume: string | null; resume: string | null;
}; }
interface Step { interface Step {
index: number; index: number;
@ -79,7 +78,7 @@ interface Step {
requiredState: string[]; requiredState: string[];
title: string; title: string;
icon: React.ReactNode; icon: React.ReactNode;
}; }
const initialState: AnalysisState = { const initialState: AnalysisState = {
job: null, job: null,
@ -92,13 +91,23 @@ const initialState: AnalysisState = {
const steps: Step[] = [ const steps: Step[] = [
{ requiredState: [], title: 'Job Selection', icon: <WorkIcon /> }, { requiredState: [], title: 'Job Selection', icon: <WorkIcon /> },
{ requiredState: ['job'], title: 'Select Candidate', icon: <PersonIcon /> }, { requiredState: ['job'], title: 'Select Candidate', icon: <PersonIcon /> },
{ requiredState: ['job', 'candidate'], title: 'Job Analysis', icon: <WorkIcon /> }, {
{ requiredState: ['job', 'candidate', 'analysis'], title: 'Generated Resume', icon: <AssessmentIcon /> } requiredState: ['job', 'candidate'],
].map((item, index) => { return { ...item, index, label: item.title.toLowerCase().replace(/ /g, '-') } }); title: 'Job Analysis',
icon: <WorkIcon />,
},
{
requiredState: ['job', 'candidate', 'analysis'],
title: 'Generated Resume',
icon: <AssessmentIcon />,
},
].map((item, index) => {
return { ...item, index, label: item.title.toLowerCase().replace(/ /g, '-') };
});
const capitalize = (str: string) => { const capitalize = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
} };
// Main component // Main component
const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => { const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
@ -115,23 +124,30 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const canAccessStep = useCallback((step: Step) => { const canAccessStep = useCallback(
(step: Step) => {
if (!analysisState) { if (!analysisState) {
return; return;
} }
const missing = step.requiredState.find(f => !(analysisState as any)[f]) const missing = step.requiredState.find(f => !(analysisState as any)[f]);
return missing; return missing;
}, [analysisState]); },
[analysisState]
);
useEffect(() => { useEffect(() => {
if (analysisState !== null) { if (analysisState !== null) {
return; return;
} }
const analysis = { ...initialState, candidate: selectedCandidate, job: selectedJob } const analysis = {
...initialState,
candidate: selectedCandidate,
job: selectedJob,
};
setAnalysisState(analysis); setAnalysisState(analysis);
for (let i = steps.length - 1; i >= 0; i--) { for (let i = steps.length - 1; i >= 0; i--) {
const missing = steps[i].requiredState.find(f => !(analysis as any)[f]) const missing = steps[i].requiredState.find(f => !(analysis as any)[f]);
if (!missing) { if (!missing) {
setActiveStep(steps[i]); setActiveStep(steps[i]);
return; return;
@ -153,7 +169,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollTo({ scrollRef.current.scrollTo({
top: 0, top: 0,
behavior: "smooth", behavior: 'smooth',
}); });
} }
}, [setCanAdvance, analysisState, activeStep]); }, [setCanAdvance, analysisState, activeStep]);
@ -169,7 +185,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
} }
if (activeStep.index < steps.length - 1) { if (activeStep.index < steps.length - 1) {
setActiveStep((prevActiveStep) => steps[prevActiveStep.index + 1]); setActiveStep(prevActiveStep => steps[prevActiveStep.index + 1]);
} }
}; };
@ -178,7 +194,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
return; return;
} }
setActiveStep((prevActiveStep) => steps[prevActiveStep.index - 1]); setActiveStep(prevActiveStep => steps[prevActiveStep.index - 1]);
}; };
const moveToStep = (step: number) => { const moveToStep = (step: number) => {
@ -188,7 +204,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
return; return;
} }
setActiveStep(steps[step]); setActiveStep(steps[step]);
} };
const onCandidateSelect = (candidate: Candidate) => { const onCandidateSelect = (candidate: Candidate) => {
if (!analysisState) { if (!analysisState) {
@ -198,7 +214,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
setAnalysisState({ ...analysisState }); setAnalysisState({ ...analysisState });
setSelectedCandidate(candidate); setSelectedCandidate(candidate);
handleNext(); handleNext();
} };
const onJobSelect = (job: Job) => { const onJobSelect = (job: Job) => {
if (!analysisState) { if (!analysisState) {
@ -208,7 +224,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
setAnalysisState({ ...analysisState }); setAnalysisState({ ...analysisState });
setSelectedJob(job); setSelectedJob(job);
handleNext(); handleNext();
} };
// Render function for the candidate selection step // Render function for the candidate selection step
const renderCandidateSelection = () => ( const renderCandidateSelection = () => (
@ -221,28 +237,25 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
// Render function for the job description step // Render function for the job description step
const renderJobDescription = () => { const renderJobDescription = () => {
return (<Box sx={{ mt: 3, width: "100%" }}> return (
<Box sx={{ mt: 3, width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}> <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={jobTab} onChange={handleTabChange} centered> <Tabs value={jobTab} onChange={handleTabChange} centered>
<Tab value='select' icon={<WorkOutline />} label="Select Job" /> <Tab value="select" icon={<WorkOutline />} label="Select Job" />
<Tab value='create' icon={<WorkAddIcon />} label="Create Job" /> <Tab value="create" icon={<WorkAddIcon />} label="Create Job" />
</Tabs> </Tabs>
</Box> </Box>
{jobTab === 'select' && {jobTab === 'select' && <JobPicker onSelect={onJobSelect} />}
<JobPicker onSelect={onJobSelect} /> {jobTab === 'create' && user && <JobCreator onSave={onJobSelect} />}
} {jobTab === 'create' && guest && (
{jobTab === 'create' && user && <LoginRestricted>
<JobCreator <JobCreator onSave={onJobSelect} />
onSave={onJobSelect} </LoginRestricted>
/>} )}
{jobTab === 'create' && guest &&
<LoginRestricted><JobCreator
onSave={onJobSelect}
/></LoginRestricted>}
</Box> </Box>
); );
} };
const onAnalysisComplete = (skills: SkillAssessment[]) => { const onAnalysisComplete = (skills: SkillAssessment[]) => {
if (!analysisState) { if (!analysisState) {
@ -258,16 +271,25 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
return; return;
} }
if (!analysisState.job || !analysisState.candidate) { if (!analysisState.job || !analysisState.candidate) {
return <Box>{JSON.stringify({ job: analysisState.job, candidate: analysisState.candidate })}</Box> return (
<Box>
{JSON.stringify({
job: analysisState.job,
candidate: analysisState.candidate,
})}
</Box>
);
} }
return (<Box sx={{ mt: 3 }}> return (
<Box sx={{ mt: 3 }}>
<JobMatchAnalysis <JobMatchAnalysis
variant="small" variant="small"
job={analysisState.job} job={analysisState.job}
candidate={analysisState.candidate} candidate={analysisState.candidate}
onAnalysisComplete={onAnalysisComplete} onAnalysisComplete={onAnalysisComplete}
/> />
</Box>); </Box>
);
}; };
const renderResume = () => { const renderResume = () => {
@ -278,43 +300,56 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
return <></>; return <></>;
} }
return (<Box sx={{ mt: 3 }}> return (
<Box sx={{ mt: 3 }}>
<ResumeGenerator <ResumeGenerator
job={analysisState.job} job={analysisState.job}
candidate={analysisState.candidate} candidate={analysisState.candidate}
skills={analysisState.analysis} skills={analysisState.analysis}
/> />
</Box>); </Box>
);
}; };
return ( return (
<Box sx={{ <Box
display: "flex", flexDirection: "column", sx={{
height: "100%", /* Restrict to main-container's height */ display: 'flex',
width: "100%", flexDirection: 'column',
minHeight: 0,/* Prevent flex overflow */ height: '100%' /* Restrict to main-container's height */,
maxHeight: "min-content", width: '100%',
"& > *:not(.Scrollable)": { minHeight: 0 /* Prevent flex overflow */,
flexShrink: 0, /* Prevent shrinking */ 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 }}> <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) => ( {steps.map((step, index) => (
<Step> <Step>
<StepLabel sx={{ cursor: "pointer" }} onClick={() => { moveToStep(index); }} <StepLabel
sx={{ cursor: 'pointer' }}
onClick={() => {
moveToStep(index);
}}
slots={{ slots={{
stepIcon: () => ( stepIcon: () => (
<Avatar key={step.index} <Avatar
key={step.index}
sx={{ sx={{
bgcolor: activeStep.index >= step.index ? theme.palette.primary.main : theme.palette.grey[300], bgcolor:
color: 'white' activeStep.index >= step.index
? theme.palette.primary.main
: theme.palette.grey[300],
color: 'white',
}} }}
> >
{step.icon} {step.icon}
</Avatar> </Avatar>
) ),
}} }}
> >
{step.title} {step.title}
@ -322,42 +357,45 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
</Step> </Step>
))} ))}
</Stepper> </Stepper>
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}> <Box sx={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row' }}>
{analysisState && analysisState.job && {analysisState && analysisState.job && (
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}> <Box sx={{ display: 'flex', flexDirection: 'row', width: '100%' }}>
{!isMobile && {!isMobile && (
<Avatar <Avatar
sx={{ sx={{
ml: 1, mt: 1, ml: 1,
mt: 1,
bgcolor: theme.palette.primary.main, bgcolor: theme.palette.primary.main,
color: 'white' color: 'white',
}} }}
> >
<WorkIcon /> <WorkIcon />
</Avatar> </Avatar>
} )}
<JobInfo variant="minimal" job={analysisState.job} /> <JobInfo variant="minimal" job={analysisState.job} />
</Box> </Box>
} )}
{isMobile && <Box sx={{ display: "flex", borderBottom: "1px solid lightgrey" }} />} {isMobile && <Box sx={{ display: 'flex', borderBottom: '1px solid lightgrey' }} />}
{!isMobile && <Box sx={{ display: "flex", borderLeft: "1px solid lightgrey" }} />} {!isMobile && <Box sx={{ display: 'flex', borderLeft: '1px solid lightgrey' }} />}
{analysisState && analysisState.candidate && {analysisState && analysisState.candidate && (
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}> <Box sx={{ display: 'flex', flexDirection: 'row', width: '100%' }}>
<CandidateInfo variant="minimal" candidate={analysisState.candidate} sx={{}} /> <CandidateInfo variant="minimal" candidate={analysisState.candidate} sx={{}} />
</Box> </Box>
} )}
</Box> </Box>
</Paper> </Paper>
<Scrollable <Scrollable
ref={scrollRef} ref={scrollRef}
sx={{ sx={{
position: "relative", position: 'relative',
maxHeight: "100%", maxHeight: '100%',
width: "100%", width: '100%',
display: "flex", flexGrow: 1, display: 'flex',
flex: 1, /* Take remaining space in some-container */ flexGrow: 1,
overflowY: "auto", /* Scroll if content overflows */ flex: 1 /* Take remaining space in some-container */,
}}> overflowY: 'auto' /* Scroll if content overflows */,
}}
>
{activeStep.label === 'job-selection' && renderJobDescription()} {activeStep.label === 'job-selection' && renderJobDescription()}
{activeStep.label === 'select-candidate' && renderCandidateSelection()} {activeStep.label === 'select-candidate' && renderCandidateSelection()}
{activeStep.label === 'job-analysis' && renderAnalysis()} {activeStep.label === 'job-analysis' && renderAnalysis()}
@ -376,7 +414,13 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
<Box sx={{ flex: '1 1 auto' }} /> <Box sx={{ flex: '1 1 auto' }} />
{activeStep.index === steps[steps.length - 1].index ? ( {activeStep.index === steps[steps.length - 1].index ? (
<Button disabled={!canAdvance} onClick={() => { moveToStep(0) }} variant="outlined"> <Button
disabled={!canAdvance}
onClick={() => {
moveToStep(0);
}}
variant="outlined"
>
Start New Analysis Start New Analysis
</Button> </Button>
) : ( ) : (
@ -397,7 +441,8 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
{error} {error}
</Alert> </Alert>
</Snackbar> </Snackbar>
</Box>); </Box>
);
}; };
export { JobAnalysisPage }; export { JobAnalysisPage };

View File

@ -11,14 +11,21 @@ const LoadingPage = (props: BackstoryPageProps) => {
sessionId: '', sessionId: '',
content: 'Please wait while connecting to Backstory...', content: 'Please wait while connecting to Backstory...',
timestamp: new Date(), timestamp: new Date(),
metadata: null as any metadata: null as any,
} };
return <Box sx={{display: "flex", flexGrow: 1, maxWidth: "1024px", margin: "0 auto"}}> return (
<Box
sx={{
display: 'flex',
flexGrow: 1,
maxWidth: '1024px',
margin: '0 auto',
}}
>
<Message message={preamble} {...props} /> <Message message={preamble} {...props} />
</Box> </Box>
);
}; };
export { export { LoadingPage };
LoadingPage
};

View File

@ -12,10 +12,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from '@mui/material'; } from '@mui/material';
import { import { Person, PersonAdd } from '@mui/icons-material';
Person,
PersonAdd,
} from '@mui/icons-material';
import 'react-phone-number-input/style.css'; import 'react-phone-number-input/style.css';
import './LoginPage.css'; import './LoginPage.css';
@ -24,8 +21,8 @@ import { BackstoryLogo } from 'components/ui/BackstoryLogo';
import { BackstoryPageProps } from 'components/BackstoryTab'; import { BackstoryPageProps } from 'components/BackstoryTab';
import { LoginForm } from "components/EmailVerificationComponents"; import { LoginForm } from 'components/EmailVerificationComponents';
import { CandidateRegistrationForm } from "pages/candidate/RegistrationForms"; import { CandidateRegistrationForm } from 'pages/candidate/RegistrationForms';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
import * as Types from 'types/types'; import * as Types from 'types/types';
@ -37,11 +34,12 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
const { guest, user, login, isLoading, error } = useAuth(); const { guest, user, login, isLoading, error } = useAuth();
const name = (user?.userType === 'candidate') ? (user as Types.Candidate).username : user?.email || ''; const name =
user?.userType === 'candidate' ? (user as Types.Candidate).username : user?.email || '';
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const showGuest: boolean = false; const showGuest = false;
const { tab } = useParams(); const { tab } = useParams();
useEffect(() => { useEffect(() => {
@ -53,7 +51,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
const jsonStr = error.replace(/^[^{]*/, ''); const jsonStr = error.replace(/^[^{]*/, '');
const data = JSON.parse(jsonStr); const data = JSON.parse(jsonStr);
setErrorMessage(data.error.message); setErrorMessage(data.error.message);
setSnack(data.error.message, "error"); setSnack(data.error.message, 'error');
setTimeout(() => { setTimeout(() => {
setErrorMessage(null); setErrorMessage(null);
setLoading(false); setLoading(false);
@ -75,7 +73,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
// If user is logged in, navigate to the profile page // If user is logged in, navigate to the profile page
if (user) { if (user) {
navigate('/candidate/profile'); navigate('/candidate/profile');
return (<></>); return <></>;
} }
return ( return (
@ -117,13 +115,9 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
</Alert> </Alert>
)} )}
{tabValue === "login" && ( {tabValue === 'login' && <LoginForm />}
<LoginForm />
)}
{tabValue === "register" && ( {tabValue === 'register' && <CandidateRegistrationForm />}
<CandidateRegistrationForm />
)}
</Paper> </Paper>
); );
}; };

View File

@ -13,13 +13,15 @@ import { Candidate } from 'types/types';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
import * as Types from 'types/types'; import * as Types from 'types/types';
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => { const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
(props: BackstoryPageProps, ref) => {
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const { user } = useAuth(); const { user } = useAuth();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [questions, setQuestions] = useState<React.ReactElement[]>([]); const [questions, setQuestions] = useState<React.ReactElement[]>([]);
const candidate: Candidate | null = user?.userType === 'candidate' ? user as Types.Candidate : null; const candidate: Candidate | null =
user?.userType === 'candidate' ? (user as Types.Candidate) : null;
// console.log("ChatPage candidate =>", candidate); // console.log("ChatPage candidate =>", candidate);
useEffect(() => { useEffect(() => {
@ -28,20 +30,21 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
} }
setQuestions([ setQuestions([
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}> <Box sx={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row' }}>
{candidate.questions?.map((q, i: number) => {candidate.questions?.map((q, i: number) => (
<BackstoryQuery key={i} question={q} /> <BackstoryQuery key={i} question={q} />
)} ))}
</Box>, </Box>,
<Box sx={{ p: 1 }}> <Box sx={{ p: 1 }}>
<MuiMarkdown> <MuiMarkdown>
{`As with all LLM interactions, the results may not be 100% accurate. Please contact **${candidate.fullName}** if you have any questions.`} {`As with all LLM interactions, the results may not be 100% accurate. Please contact **${candidate.fullName}** if you have any questions.`}
</MuiMarkdown> </MuiMarkdown>
</Box>]); </Box>,
]);
}, [candidate, isMobile]); }, [candidate, isMobile]);
if (!candidate) { if (!candidate) {
return (<></>); return <></>;
} }
return ( return (
<Box> <Box>
@ -50,14 +53,15 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
ref={ref} ref={ref}
{...{ {...{
multiline: true, multiline: true,
type: "chat", type: 'chat',
placeholder: `What would you like to know about ${candidate?.firstName}?`, placeholder: `What would you like to know about ${candidate?.firstName}?`,
resetLabel: "chat", resetLabel: 'chat',
defaultPrompts: questions, defaultPrompts: questions,
}} /> }}
</Box>); />
}); </Box>
);
}
);
export { export { ChatPage };
ChatPage
};

View File

@ -8,7 +8,7 @@ import './VectorVisualizerPage.css';
interface VectorVisualizerProps extends BackstoryPageProps { interface VectorVisualizerProps extends BackstoryPageProps {
inline?: boolean; inline?: boolean;
rag?: any; rag?: any;
}; }
const VectorVisualizerPage: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => { const VectorVisualizerPage: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
return <VectorVisualizer inline={false} {...props} />; return <VectorVisualizer inline={false} {...props} />;
@ -16,6 +16,4 @@ const VectorVisualizerPage: React.FC<VectorVisualizerProps> = (props: VectorVisu
export type { VectorVisualizerProps }; export type { VectorVisualizerProps };
export { export { VectorVisualizerPage };
VectorVisualizerPage
};

View File

@ -1,13 +1,5 @@
import React from 'react'; import React from 'react';
import { import { Box, Card, CardContent, Typography, Button, LinearProgress, Stack } from '@mui/material';
Box,
Card,
CardContent,
Typography,
Button,
LinearProgress,
Stack
} from '@mui/material';
import { import {
Add as AddIcon, Add as AddIcon,
Visibility as VisibilityIcon, Visibility as VisibilityIcon,
@ -23,8 +15,7 @@ import { useNavigate } from 'react-router-dom';
import { ComingSoon } from 'components/ui/ComingSoon'; import { ComingSoon } from 'components/ui/ComingSoon';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
interface CandidateDashboardProps extends BackstoryElementProps { type CandidateDashboardProps = BackstoryElementProps;
};
const CandidateDashboard = (props: CandidateDashboardProps) => { const CandidateDashboard = (props: CandidateDashboardProps) => {
const { setSnack } = useAppState(); const { setSnack } = useAppState();
@ -33,16 +24,20 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
const profileCompletion = 75; const profileCompletion = 75;
if (!user) { if (!user) {
return <LoginRequired asset="candidate dashboard"/>; 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'); setSnack(
`The page you were on is only available for candidates (you are a ${user.userType}`,
'warning'
);
navigate('/'); navigate('/');
return (<></>); return <></>;
} }
return (<> return (
<>
{/* Main Content */} {/* Main Content */}
<ComingSoon> <ComingSoon>
<Box sx={{ flex: 1, p: 3 }}> <Box sx={{ flex: 1, p: 3 }}>
@ -74,7 +69,10 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
variant="contained" variant="contained"
color="primary" color="primary"
sx={{ mt: 1 }} sx={{ mt: 1 }}
onClick={(e) => { e.stopPropagation(); navigate('/candidate/profile'); }} onClick={e => {
e.stopPropagation();
navigate('/candidate/profile');
}}
> >
Complete Your Profile Complete Your Profile
</Button> </Button>
@ -99,11 +97,7 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
Last created: May 15, 2025 Last created: May 15, 2025
</Typography> </Typography>
<Button <Button variant="outlined" startIcon={<AddIcon />} fullWidth>
variant="outlined"
startIcon={<AddIcon />}
fullWidth
>
Create New Create New
</Button> </Button>
</CardContent> </CardContent>
@ -133,10 +127,7 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
</Box> </Box>
</Stack> </Stack>
<Button <Button variant="outlined" fullWidth>
variant="outlined"
fullWidth
>
View All Activity View All Activity
</Button> </Button>
</CardContent> </CardContent>
@ -164,11 +155,7 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
</Typography> </Typography>
</Stack> </Stack>
<Button <Button variant="outlined" startIcon={<EditIcon />} fullWidth>
variant="outlined"
startIcon={<EditIcon />}
fullWidth
>
Edit Backstory Edit Backstory
</Button> </Button>
</CardContent> </CardContent>
@ -190,11 +177,7 @@ const CandidateDashboard = (props: CandidateDashboardProps) => {
</Typography> </Typography>
</Stack> </Stack>
<Button <Button variant="outlined" startIcon={<TipsIcon />} fullWidth>
variant="outlined"
startIcon={<TipsIcon />}
fullWidth
>
View All Tips View All Tips
</Button> </Button>
</CardContent> </CardContent>

View File

@ -30,7 +30,7 @@ import {
Switch, Switch,
FormControlLabel, FormControlLabel,
ToggleButton, ToggleButton,
Checkbox Checkbox,
} from '@mui/material'; } from '@mui/material';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { import {
@ -49,7 +49,7 @@ import {
AccountCircle, AccountCircle,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import * as Types from 'types/types'; import * as Types from 'types/types';
import { ComingSoon } from 'components/ui/ComingSoon'; import { ComingSoon } from 'components/ui/ComingSoon';
import { BackstoryPageProps } from 'components/BackstoryTab'; import { BackstoryPageProps } from 'components/BackstoryTab';
@ -86,11 +86,13 @@ function TabPanel(props: TabPanelProps) {
{...other} {...other}
> >
{value === index && ( {value === index && (
<Box sx={{ <Box
sx={{
p: { xs: 1, sm: 3 }, p: { xs: 1, sm: 3 },
maxWidth: '100%', maxWidth: '100%',
overflow: 'hidden' overflow: 'hidden',
}}> }}
>
{children} {children}
</Box> </Box>
)} )}
@ -106,7 +108,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
const [isPublic, setIsPublic] = React.useState(true); const [isPublic, setIsPublic] = React.useState(true);
// Check if user is a candidate // 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;
// State management // State management
const [tabValue, setTabValue] = useState(0); const [tabValue, setTabValue] = useState(0);
@ -115,11 +117,11 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
const [snackbar, setSnackbar] = useState<{ const [snackbar, setSnackbar] = useState<{
open: boolean; open: boolean;
message: string; message: string;
severity: "success" | "error" | "info" | "warning"; severity: 'success' | 'error' | 'info' | 'warning';
}>({ }>({
open: false, open: false,
message: '', message: '',
severity: 'success' severity: 'success',
}); });
// Form data state // Form data state
@ -135,7 +137,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
name: '', name: '',
category: '', category: '',
level: 'beginner', level: 'beginner',
yearsOfExperience: 0 yearsOfExperience: 0,
}); });
const [newExperience, setNewExperience] = useState<Partial<Types.WorkExperience>>({ const [newExperience, setNewExperience] = useState<Partial<Types.WorkExperience>>({
companyName: '', companyName: '',
@ -144,7 +146,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
isCurrent: false, isCurrent: false,
description: '', description: '',
skills: [], skills: [],
location: { city: '', country: '' } location: { city: '', country: '' },
}); });
useEffect(() => { useEffect(() => {
@ -162,9 +164,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
if (!candidate) { if (!candidate) {
return ( return (
<Container maxWidth="md" sx={{ mt: 4 }}> <Container maxWidth="md" sx={{ mt: 4 }}>
<Alert severity="error"> <Alert severity="error">Access denied. This page is only available for candidates.</Alert>
Access denied. This page is only available for candidates.
</Alert>
</Container> </Container>
); );
} }
@ -200,7 +200,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
setSnackbar({ setSnackbar({
open: true, open: true,
message: 'Failed to upload profile image. Please try again.', message: 'Failed to upload profile image. Please try again.',
severity: 'error' severity: 'error',
}); });
} }
} catch (error) { } catch (error) {
@ -208,7 +208,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
setSnackbar({ setSnackbar({
open: true, open: true,
message: 'Failed to upload profile image. Please try again.', message: 'Failed to upload profile image. Please try again.',
severity: 'error' severity: 'error',
}); });
} }
}; };
@ -217,7 +217,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
const toggleEditMode = (section: string) => { const toggleEditMode = (section: string) => {
setEditMode({ setEditMode({
...editMode, ...editMode,
[section]: !editMode[section] [section]: !editMode[section],
}); });
}; };
@ -235,7 +235,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
setSnackbar({ setSnackbar({
open: true, open: true,
message: 'Failed to update profile. Please try again.', message: 'Failed to update profile. Please try again.',
severity: 'error' severity: 'error',
}); });
} finally { } finally {
setLoading(false); setLoading(false);
@ -253,7 +253,12 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
if (newSkill.name && newSkill.category) { if (newSkill.name && newSkill.category) {
const updatedSkills = [...(formData.skills || []), newSkill as Types.Skill]; const updatedSkills = [...(formData.skills || []), newSkill as Types.Skill];
setFormData({ ...formData, skills: updatedSkills }); setFormData({ ...formData, skills: updatedSkills });
setNewSkill({ name: '', category: '', level: 'beginner', yearsOfExperience: 0 }); setNewSkill({
name: '',
category: '',
level: 'beginner',
yearsOfExperience: 0,
});
setSkillDialog(false); setSkillDialog(false);
} }
}; };
@ -267,7 +272,10 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
// Add new work experience // Add new work experience
const handleAddExperience = () => { const handleAddExperience = () => {
if (newExperience.companyName && newExperience.position) { if (newExperience.companyName && newExperience.position) {
const updatedExperience = [...(formData.experience || []), newExperience as Types.WorkExperience]; const updatedExperience = [
...(formData.experience || []),
newExperience as Types.WorkExperience,
];
setFormData({ ...formData, experience: updatedExperience }); setFormData({ ...formData, experience: updatedExperience });
setNewExperience({ setNewExperience({
companyName: '', companyName: '',
@ -276,7 +284,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
isCurrent: false, isCurrent: false,
description: '', description: '',
skills: [], skills: [],
location: { city: '', country: '' } location: { city: '', country: '' },
}); });
setExperienceDialog(false); setExperienceDialog(false);
} }
@ -290,16 +298,34 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
// Basic Information Tab // Basic Information Tab
const renderBasicInfo = () => ( const renderBasicInfo = () => (
<Box sx={{ display: "flex", flexDirection: "column", "& .entry": { flexDirection: "column", fontSize: "0.9rem", display: "flex", mt: 1 }, "& .title": { display: "flex", fontWeight: "bold" } }}> <Box
sx={{
display: 'flex',
flexDirection: 'column',
'& .entry': {
flexDirection: 'column',
fontSize: '0.9rem',
display: 'flex',
mt: 1,
},
'& .title': { display: 'flex', fontWeight: 'bold' },
}}
>
<Box sx={{ textAlign: 'center', mb: { xs: 1, sm: 2 } }}> <Box sx={{ textAlign: 'center', mb: { xs: 1, sm: 2 } }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar <Avatar
src={profileImage} src={profileImage}
sx={{ sx={{
width: { xs: 80, sm: 120 }, width: { xs: 80, sm: 120 },
height: { xs: 80, sm: 120 }, height: { xs: 80, sm: 120 },
mb: { xs: 1, sm: 2 }, mb: { xs: 1, sm: 2 },
border: `2px solid ${theme.palette.primary.main}` border: `2px solid ${theme.palette.primary.main}`,
}} }}
> >
{!profileImage && <AccountCircle sx={{ fontSize: { xs: 50, sm: 80 } }} />} {!profileImage && <AccountCircle sx={{ fontSize: { xs: 50, sm: 80 } }} />}
@ -310,16 +336,19 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
color="primary" color="primary"
aria-label="upload picture" aria-label="upload picture"
component="label" component="label"
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
> >
<PhotoCamera /> <PhotoCamera />
<VisuallyHiddenInput <VisuallyHiddenInput type="file" accept="image/*" onChange={handleImageUpload} />
type="file"
accept="image/*"
onChange={handleImageUpload}
/>
</IconButton> </IconButton>
<Typography variant="caption" color="textSecondary" sx={{ textAlign: 'center', fontSize: { xs: '0.7rem', sm: '0.75rem' } }}> <Typography
variant="caption"
color="textSecondary"
sx={{
textAlign: 'center',
fontSize: { xs: '0.7rem', sm: '0.75rem' },
}}
>
Update profile photo Update profile photo
</Typography> </Typography>
</> </>
@ -327,38 +356,57 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
</Box> </Box>
</Box> </Box>
<Box className="entry" sx={{ display: 'flex', justifyContent: 'center', alignContent: 'center', gap: 1, "& span": { mb: 0 } }}> <Box
className="entry"
sx={{
display: 'flex',
justifyContent: 'center',
alignContent: 'center',
gap: 1,
'& span': { mb: 0 },
}}
>
{editMode.basic ? ( {editMode.basic ? (
<FormControlLabel sx={{ display: 'flex' }} control={ <FormControlLabel
sx={{ display: 'flex' }}
control={
<Switch <Switch
checked={formData.isPublic} checked={formData.isPublic}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => handleInputChange('isPublic', event.target.checked)} />} onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
handleInputChange('isPublic', event.target.checked)
}
/>
}
label={ label={
formData.isPublic ? formData.isPublic
'Your account will appear in candidate lists and searches on Backstory.' : ? 'Your account will appear in candidate lists and searches on Backstory.'
`Your account will not be listed as a candidate. You can still share your profile by users navigating to '/u/${candidate.username}'.` : `Your account will not be listed as a candidate. You can still share your profile by users navigating to '/u/${candidate.username}'.`
} /> }
) : (<>{ />
candidate.isPublic ? ) : (
'Your account will appear in candidate lists and searches on Backstory.' : <>
`Your account will not be listed as a candidate. You can still share your profile by users navigating to '/u/${candidate.username}'.` {candidate.isPublic
}</>)} ? 'Your account will appear in candidate lists and searches on Backstory.'
: `Your account will not be listed as a candidate. You can still share your profile by users navigating to '/u/${candidate.username}'.`}
</>
)}
</Box> </Box>
<Box className="entry"> <Box className="entry">
{editMode.basic ? ( {editMode.basic ? (
<TextField <TextField
fullWidth fullWidth
label="First Name" label="First Name"
value={formData.firstName || ''} value={formData.firstName || ''}
onChange={(e) => handleInputChange('firstName', e.target.value)} onChange={e => handleInputChange('firstName', e.target.value)}
variant="outlined" variant="outlined"
/> />
) : (<> ) : (
<>
<Box className="title">First Name</Box> <Box className="title">First Name</Box>
<Box className="value">{candidate.firstName}</Box> <Box className="value">{candidate.firstName}</Box>
</>)} </>
)}
</Box> </Box>
<Box className="entry"> <Box className="entry">
@ -367,28 +415,33 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
fullWidth fullWidth
label="Last Name" label="Last Name"
value={formData.lastName || ''} value={formData.lastName || ''}
onChange={(e) => handleInputChange('lastName', e.target.value)} onChange={e => handleInputChange('lastName', e.target.value)}
variant="outlined" variant="outlined"
/> />
) : (<> ) : (
<>
<Box className="title">Last Name</Box> <Box className="title">Last Name</Box>
<Box className="value">{candidate.lastName}</Box> <Box className="value">{candidate.lastName}</Box>
</>)} </>
)}
</Box> </Box>
<Box className="entry"> <Box className="entry">
{(false && editMode.basic) ? ( {false && editMode.basic ? (
<TextField <TextField
fullWidth fullWidth
label="Email" label="Email"
type="email" type="email"
value={formData.email || ''} value={formData.email || ''}
onChange={(e) => handleInputChange('email', e.target.value)} onChange={e => handleInputChange('email', e.target.value)}
variant="outlined" variant="outlined"
/> />
) : (<> ) : (
<Box className="title"><Email sx={{ mr: 1, verticalAlign: 'middle' }} /> <>
Email</Box> <Box className="title">
<Email sx={{ mr: 1, verticalAlign: 'middle' }} />
Email
</Box>
<Box className="value">{candidate.email}</Box> <Box className="value">{candidate.email}</Box>
</> </>
)} )}
@ -400,12 +453,15 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
fullWidth fullWidth
label="Phone" label="Phone"
value={formData.phone || ''} value={formData.phone || ''}
onChange={(e) => handleInputChange('phone', e.target.value)} onChange={e => handleInputChange('phone', e.target.value)}
variant="outlined" variant="outlined"
/> />
) : (<> ) : (
<Box className="title"><Phone sx={{ mr: 1, verticalAlign: 'middle' }} /> <>
Phone</Box> <Box className="title">
<Phone sx={{ mr: 1, verticalAlign: 'middle' }} />
Phone
</Box>
<Box className="value">{candidate.phone || 'Not provided'}</Box> <Box className="value">{candidate.phone || 'Not provided'}</Box>
</> </>
)} )}
@ -419,13 +475,15 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
rows={3} rows={3}
label="Professional Summary" label="Professional Summary"
value={formData.description || ''} value={formData.description || ''}
onChange={(e) => handleInputChange('description', e.target.value)} onChange={e => handleInputChange('description', e.target.value)}
variant="outlined" variant="outlined"
/> />
) : (<> ) : (
<>
<Box className="title">Professional Summary</Box> <Box className="title">Professional Summary</Box>
<Box className="value">{candidate.description || 'No summary provided'}</Box> <Box className="value">{candidate.description || 'No summary provided'}</Box>
</>)} </>
)}
</Box> </Box>
<Box className="entry"> <Box className="entry">
@ -434,29 +492,38 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
fullWidth fullWidth
label="Location" label="Location"
value={formData.location?.city || ''} value={formData.location?.city || ''}
onChange={(e) => handleInputChange('location', { onChange={e =>
handleInputChange('location', {
...formData.location, ...formData.location,
city: e.target.value city: e.target.value,
})} })
}
variant="outlined" variant="outlined"
placeholder="City, State, Country" placeholder="City, State, Country"
/> />
) : (<><Box className="title"> ) : (
<>
<Box className="title">
<LocationOn sx={{ mr: 1, verticalAlign: 'middle' }} /> <LocationOn sx={{ mr: 1, verticalAlign: 'middle' }} />
Location</Box> Location
<Box className="value">{candidate.location?.city || 'Not specified'} {candidate.location?.country || ''}</Box> </Box>
<Box className="value">
{candidate.location?.city || 'Not specified'} {candidate.location?.country || ''}
</Box>
</> </>
)} )}
</Box> </Box>
<Box className="entry"> <Box className="entry">
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', sm: 'row' }, flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'flex-end', justifyContent: 'flex-end',
gap: 2, gap: 2,
mt: { xs: 2, sm: 0 } mt: { xs: 2, sm: 0 },
}}> }}
>
{editMode.basic ? ( {editMode.basic ? (
<> <>
<Button <Button
@ -489,27 +556,29 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
)} )}
</Box> </Box>
</Box> </Box>
</Box > </Box>
); );
// Skills Tab // Skills Tab
const renderSkills = () => ( const renderSkills = () => (
<Box> <Box>
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', sm: 'row' }, flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: { xs: 'stretch', sm: 'center' }, alignItems: { xs: 'stretch', sm: 'center' },
mb: { xs: 2, sm: 3 }, mb: { xs: 2, sm: 3 },
gap: { xs: 1, sm: 0 } gap: { xs: 1, sm: 0 },
}}> }}
<Typography variant={isMobile ? "subtitle1" : "h6"}>Skills & Expertise</Typography> >
<Typography variant={isMobile ? 'subtitle1' : 'h6'}>Skills & Expertise</Typography>
<Button <Button
variant="outlined" variant="outlined"
startIcon={<Add />} startIcon={<Add />}
onClick={() => setSkillDialog(true)} onClick={() => setSkillDialog(true)}
fullWidth={isMobile} fullWidth={isMobile}
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
> >
Add Skill Add Skill
</Button> </Button>
@ -520,18 +589,32 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}> <Grid size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<Card variant="outlined" sx={{ height: '100%' }}> <Card variant="outlined" sx={{ height: '100%' }}>
<CardContent sx={{ p: { xs: 1.5, sm: 3 } }}> <CardContent sx={{ p: { xs: 1.5, sm: 3 } }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> <Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
}}
>
<Box sx={{ flex: 1, minWidth: 0 }}> <Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant={isMobile ? "subtitle2" : "h6"} component="div" sx={{ <Typography
variant={isMobile ? 'subtitle2' : 'h6'}
component="div"
sx={{
fontSize: { xs: '0.9rem', sm: '1.25rem' }, fontSize: { xs: '0.9rem', sm: '1.25rem' },
wordBreak: 'break-word' wordBreak: 'break-word',
}}> }}
>
{skill.name} {skill.name}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ <Typography
variant="body2"
color="text.secondary"
sx={{
wordBreak: 'break-word', wordBreak: 'break-word',
fontSize: { xs: '0.75rem', sm: '0.875rem' } fontSize: { xs: '0.75rem', sm: '0.875rem' },
}}> }}
>
{skill.category} {skill.category}
</Typography> </Typography>
<Chip <Chip
@ -542,11 +625,15 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
sx={{ sx={{
mt: 1, mt: 1,
fontSize: { xs: '0.65rem', sm: '0.75rem' }, fontSize: { xs: '0.65rem', sm: '0.75rem' },
height: { xs: 20, sm: 24 } height: { xs: 20, sm: 24 },
}} }}
/> />
{skill.yearsOfExperience && ( {skill.yearsOfExperience && (
<Typography variant="caption" display="block" sx={{ fontSize: { xs: '0.65rem', sm: '0.75rem' } }}> <Typography
variant="caption"
display="block"
sx={{ fontSize: { xs: '0.65rem', sm: '0.75rem' } }}
>
{skill.yearsOfExperience} years experience {skill.yearsOfExperience} years experience
</Typography> </Typography>
)} )}
@ -577,21 +664,23 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
// Experience Tab // Experience Tab
const renderExperience = () => ( const renderExperience = () => (
<Box> <Box>
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', sm: 'row' }, flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: { xs: 'stretch', sm: 'center' }, alignItems: { xs: 'stretch', sm: 'center' },
mb: { xs: 2, sm: 3 }, mb: { xs: 2, sm: 3 },
gap: { xs: 1, sm: 0 } gap: { xs: 1, sm: 0 },
}}> }}
<Typography variant={isMobile ? "subtitle1" : "h6"}>Work Experience</Typography> >
<Typography variant={isMobile ? 'subtitle1' : 'h6'}>Work Experience</Typography>
<Button <Button
variant="outlined" variant="outlined"
startIcon={<Add />} startIcon={<Add />}
onClick={() => setExperienceDialog(true)} onClick={() => setExperienceDialog(true)}
fullWidth={isMobile} fullWidth={isMobile}
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
> >
Add Experience Add Experience
</Button> </Button>
@ -600,34 +689,52 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
{(formData.experience || []).map((exp, index) => ( {(formData.experience || []).map((exp, index) => (
<Card key={index} sx={{ mb: { xs: 1.5, sm: 2 }, overflow: 'hidden' }}> <Card key={index} sx={{ mb: { xs: 1.5, sm: 2 }, overflow: 'hidden' }}>
<CardContent sx={{ p: { xs: 1.5, sm: 3 } }}> <CardContent sx={{ p: { xs: 1.5, sm: 3 } }}>
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', sm: 'row' }, flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'flex-start',
gap: { xs: 1, sm: 0 } gap: { xs: 1, sm: 0 },
}}> }}
>
<Box sx={{ flex: 1, minWidth: 0 }}> <Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant={isMobile ? "subtitle1" : "h6"} component="div" sx={{ <Typography
variant={isMobile ? 'subtitle1' : 'h6'}
component="div"
sx={{
fontSize: { xs: '1rem', sm: '1.25rem' }, fontSize: { xs: '1rem', sm: '1.25rem' },
wordBreak: 'break-word' wordBreak: 'break-word',
}}> }}
>
{exp.position} {exp.position}
</Typography> </Typography>
<Typography variant="subtitle1" color="primary" sx={{ <Typography
variant="subtitle1"
color="primary"
sx={{
wordBreak: 'break-word', wordBreak: 'break-word',
fontSize: { xs: '0.9rem', sm: '1rem' } fontSize: { xs: '0.9rem', sm: '1rem' },
}}> }}
>
{exp.companyName} {exp.companyName}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.8rem', sm: '0.875rem' } }}> <Typography
{exp.startDate?.toLocaleDateString()} - {exp.isCurrent ? 'Present' : exp.endDate?.toLocaleDateString()} variant="body2"
color="text.secondary"
sx={{ fontSize: { xs: '0.8rem', sm: '0.875rem' } }}
>
{exp.startDate?.toLocaleDateString()} -{' '}
{exp.isCurrent ? 'Present' : exp.endDate?.toLocaleDateString()}
</Typography> </Typography>
<Typography variant="body2" sx={{ <Typography
variant="body2"
sx={{
mt: 1, mt: 1,
wordBreak: 'break-word', wordBreak: 'break-word',
fontSize: { xs: '0.8rem', sm: '0.875rem' } fontSize: { xs: '0.8rem', sm: '0.875rem' },
}}> }}
>
{exp.description} {exp.description}
</Typography> </Typography>
{exp.skills && exp.skills.length > 0 && ( {exp.skills && exp.skills.length > 0 && (
@ -641,7 +748,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
mr: 0.5, mr: 0.5,
mb: 0.5, mb: 0.5,
fontSize: { xs: '0.65rem', sm: '0.75rem' }, fontSize: { xs: '0.65rem', sm: '0.75rem' },
height: { xs: 20, sm: 24 } height: { xs: 20, sm: 24 },
}} }}
/> />
))} ))}
@ -654,7 +761,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
size="small" size="small"
sx={{ sx={{
alignSelf: { xs: 'flex-end', sm: 'flex-start' }, alignSelf: { xs: 'flex-end', sm: 'flex-start' },
ml: { sm: 1 } ml: { sm: 1 },
}} }}
> >
<Delete sx={{ fontSize: { xs: 16, sm: 20 } }} /> <Delete sx={{ fontSize: { xs: 16, sm: 20 } }} />
@ -665,45 +772,54 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
))} ))}
{(!formData.experience || formData.experience.length === 0) && ( {(!formData.experience || formData.experience.length === 0) && (
<Typography variant="body2" color="text.secondary" sx={{ <Typography
variant="body2"
color="text.secondary"
sx={{
textAlign: 'center', textAlign: 'center',
py: { xs: 2, sm: 4 }, py: { xs: 2, sm: 4 },
fontSize: { xs: '0.8rem', sm: '0.875rem' } fontSize: { xs: '0.8rem', sm: '0.875rem' },
}}> }}
>
No work experience added yet. Click "Add Experience" to get started. No work experience added yet. Click "Add Experience" to get started.
</Typography> </Typography>
)} )}
</Box> </Box>
); );
const renderEducation = () => ( const renderEducation = () => (
<Box> <Box>
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', sm: 'row' }, flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: { xs: 'stretch', sm: 'center' }, alignItems: { xs: 'stretch', sm: 'center' },
mb: { xs: 2, sm: 3 }, mb: { xs: 2, sm: 3 },
gap: { xs: 1, sm: 0 } gap: { xs: 1, sm: 0 },
}}> }}
<Typography variant={isMobile ? "subtitle1" : "h6"}>Education</Typography> >
<Typography variant={isMobile ? 'subtitle1' : 'h6'}>Education</Typography>
<Button <Button
variant="outlined" variant="outlined"
startIcon={<Add />} startIcon={<Add />}
fullWidth={isMobile} fullWidth={isMobile}
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
> >
Add Education Add Education
</Button> </Button>
</Box> </Box>
{(!formData.experience || formData.experience.length === 0) && ( {(!formData.experience || formData.experience.length === 0) && (
<Typography variant="body2" color="text.secondary" sx={{ <Typography
variant="body2"
color="text.secondary"
sx={{
textAlign: 'center', textAlign: 'center',
py: { xs: 2, sm: 4 }, py: { xs: 2, sm: 4 },
fontSize: { xs: '0.8rem', sm: '0.875rem' } fontSize: { xs: '0.8rem', sm: '0.875rem' },
}}> }}
>
No work experience added yet. Click "Add Experience" to get started. No work experience added yet. Click "Add Experience" to get started.
</Typography> </Typography>
)} )}
@ -711,15 +827,21 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
); );
return ( return (
<Container maxWidth="lg" sx={{ <Container
maxWidth="lg"
sx={{
mt: { xs: 1, sm: 4 }, mt: { xs: 1, sm: 4 },
mb: { xs: 1, sm: 4 }, mb: { xs: 1, sm: 4 },
px: { xs: 0.5, sm: 3 } px: { xs: 0.5, sm: 3 },
}}> }}
<Paper elevation={3} sx={{ >
<Paper
elevation={3}
sx={{
overflow: 'hidden', overflow: 'hidden',
mx: { xs: 0, sm: 0 } mx: { xs: 0, sm: 0 },
}}> }}
>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs <Tabs
value={tabValue} value={tabValue}
@ -729,34 +851,34 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
allowScrollButtonsMobile allowScrollButtonsMobile
sx={{ sx={{
'& .MuiTabs-flexContainer': { '& .MuiTabs-flexContainer': {
justifyContent: isMobile ? 'flex-start' : 'center' justifyContent: isMobile ? 'flex-start' : 'center',
}, },
'& .MuiTab-root': { '& .MuiTab-root': {
fontSize: { xs: '0.75rem', sm: '0.875rem' }, fontSize: { xs: '0.75rem', sm: '0.875rem' },
minWidth: { xs: 60, sm: 120 }, minWidth: { xs: 60, sm: 120 },
padding: { xs: '6px 8px', sm: '12px 16px' } padding: { xs: '6px 8px', sm: '12px 16px' },
} },
}} }}
> >
<Tab <Tab
label={isMobile ? "Info" : "Basic Info"} label={isMobile ? 'Info' : 'Basic Info'}
icon={<AccountCircle sx={{ fontSize: { xs: 18, sm: 24 } }} />} icon={<AccountCircle sx={{ fontSize: { xs: 18, sm: 24 } }} />}
iconPosition={isMobile ? "top" : "start"} iconPosition={isMobile ? 'top' : 'start'}
/> />
<Tab <Tab
label="Skills" label="Skills"
icon={<EmojiEvents sx={{ fontSize: { xs: 18, sm: 24 } }} />} icon={<EmojiEvents sx={{ fontSize: { xs: 18, sm: 24 } }} />}
iconPosition={isMobile ? "top" : "start"} iconPosition={isMobile ? 'top' : 'start'}
/> />
<Tab <Tab
label={isMobile ? "Work" : "Experience"} label={isMobile ? 'Work' : 'Experience'}
icon={<Work sx={{ fontSize: { xs: 18, sm: 24 } }} />} icon={<Work sx={{ fontSize: { xs: 18, sm: 24 } }} />}
iconPosition={isMobile ? "top" : "start"} iconPosition={isMobile ? 'top' : 'start'}
/> />
<Tab <Tab
label={isMobile ? "Edu" : "Education"} label={isMobile ? 'Edu' : 'Education'}
icon={<School sx={{ fontSize: { xs: 18, sm: 24 } }} />} icon={<School sx={{ fontSize: { xs: 18, sm: 24 } }} />}
iconPosition={isMobile ? "top" : "start"} iconPosition={isMobile ? 'top' : 'start'}
/> />
</Tabs> </Tabs>
</Box> </Box>
@ -791,16 +913,16 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
margin: 0, margin: 0,
width: '100%', width: '100%',
height: '100%', height: '100%',
maxHeight: '100%' maxHeight: '100%',
}) }),
} },
}} }}
> >
<DialogTitle sx={{ pb: { xs: 1, sm: 2 } }}>Add New Skill</DialogTitle> <DialogTitle sx={{ pb: { xs: 1, sm: 2 } }}>Add New Skill</DialogTitle>
<DialogContent <DialogContent
sx={{ sx={{
overflow: 'auto', overflow: 'auto',
pt: { xs: 1, sm: 2 } pt: { xs: 1, sm: 2 },
}} }}
> >
<Grid container spacing={{ xs: 1.5, sm: 2 }} sx={{ mt: 0.5, maxWidth: '100%' }}> <Grid container spacing={{ xs: 1.5, sm: 2 }} sx={{ mt: 0.5, maxWidth: '100%' }}>
@ -809,8 +931,8 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
fullWidth fullWidth
label="Skill Name" label="Skill Name"
value={newSkill.name || ''} value={newSkill.name || ''}
onChange={(e) => setNewSkill({ ...newSkill, name: e.target.value })} onChange={e => setNewSkill({ ...newSkill, name: e.target.value })}
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
@ -818,17 +940,22 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
fullWidth fullWidth
label="Category" label="Category"
value={newSkill.category || ''} value={newSkill.category || ''}
onChange={(e) => setNewSkill({ ...newSkill, category: e.target.value })} onChange={e => setNewSkill({ ...newSkill, category: e.target.value })}
placeholder="e.g., Programming, Design, Marketing" placeholder="e.g., Programming, Design, Marketing"
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, sm: 6 }}> <Grid size={{ xs: 12, sm: 6 }}>
<FormControl fullWidth size={isMobile ? "small" : "medium"}> <FormControl fullWidth size={isMobile ? 'small' : 'medium'}>
<InputLabel>Proficiency Level</InputLabel> <InputLabel>Proficiency Level</InputLabel>
<Select <Select
value={newSkill.level || 'beginner'} value={newSkill.level || 'beginner'}
onChange={(e) => setNewSkill({ ...newSkill, level: e.target.value as Types.SkillLevel })} onChange={e =>
setNewSkill({
...newSkill,
level: e.target.value as Types.SkillLevel,
})
}
label="Proficiency Level" label="Proficiency Level"
> >
<MenuItem value="beginner">Beginner</MenuItem> <MenuItem value="beginner">Beginner</MenuItem>
@ -844,21 +971,28 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
type="number" type="number"
label="Years of Experience" label="Years of Experience"
value={newSkill.yearsOfExperience || 0} value={newSkill.yearsOfExperience || 0}
onChange={(e) => setNewSkill({ ...newSkill, yearsOfExperience: parseInt(e.target.value) || 0 })} onChange={e =>
size={isMobile ? "small" : "medium"} setNewSkill({
...newSkill,
yearsOfExperience: parseInt(e.target.value) || 0,
})
}
size={isMobile ? 'small' : 'medium'}
/> />
</Grid> </Grid>
</Grid> </Grid>
</DialogContent> </DialogContent>
<DialogActions sx={{ <DialogActions
sx={{
p: { xs: 1.5, sm: 3 }, p: { xs: 1.5, sm: 3 },
flexDirection: { xs: 'column', sm: 'row' }, flexDirection: { xs: 'column', sm: 'row' },
gap: { xs: 1, sm: 0 } gap: { xs: 1, sm: 0 },
}}> }}
>
<Button <Button
onClick={() => setSkillDialog(false)} onClick={() => setSkillDialog(false)}
fullWidth={isMobile} fullWidth={isMobile}
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
> >
Cancel Cancel
</Button> </Button>
@ -866,7 +1000,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
onClick={handleAddSkill} onClick={handleAddSkill}
variant="contained" variant="contained"
fullWidth={isMobile} fullWidth={isMobile}
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
> >
Add Skill Add Skill
</Button> </Button>
@ -886,16 +1020,16 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
margin: 0, margin: 0,
width: '100%', width: '100%',
height: '100%', height: '100%',
maxHeight: '100%' maxHeight: '100%',
}) }),
} },
}} }}
> >
<DialogTitle sx={{ pb: { xs: 1, sm: 2 } }}>Add Work Experience</DialogTitle> <DialogTitle sx={{ pb: { xs: 1, sm: 2 } }}>Add Work Experience</DialogTitle>
<DialogContent <DialogContent
sx={{ sx={{
overflow: 'auto', overflow: 'auto',
pt: { xs: 1, sm: 2 } pt: { xs: 1, sm: 2 },
}} }}
> >
<Grid container spacing={{ xs: 1.5, sm: 2 }} sx={{ mt: 0.5, maxWidth: '100%' }}> <Grid container spacing={{ xs: 1.5, sm: 2 }} sx={{ mt: 0.5, maxWidth: '100%' }}>
@ -904,8 +1038,13 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
fullWidth fullWidth
label="Company Name" label="Company Name"
value={newExperience.companyName || ''} value={newExperience.companyName || ''}
onChange={(e) => setNewExperience({ ...newExperience, companyName: e.target.value })} onChange={e =>
size={isMobile ? "small" : "medium"} setNewExperience({
...newExperience,
companyName: e.target.value,
})
}
size={isMobile ? 'small' : 'medium'}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, sm: 6 }}> <Grid size={{ xs: 12, sm: 6 }}>
@ -913,8 +1052,13 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
fullWidth fullWidth
label="Position/Title" label="Position/Title"
value={newExperience.position || ''} value={newExperience.position || ''}
onChange={(e) => setNewExperience({ ...newExperience, position: e.target.value })} onChange={e =>
size={isMobile ? "small" : "medium"} setNewExperience({
...newExperience,
position: e.target.value,
})
}
size={isMobile ? 'small' : 'medium'}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, sm: 6 }}> <Grid size={{ xs: 12, sm: 6 }}>
@ -923,9 +1067,14 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
type="date" type="date"
label="Start Date" label="Start Date"
value={newExperience.startDate?.toISOString().split('T')[0] || ''} value={newExperience.startDate?.toISOString().split('T')[0] || ''}
onChange={(e) => setNewExperience({ ...newExperience, startDate: new Date(e.target.value) })} onChange={e =>
setNewExperience({
...newExperience,
startDate: new Date(e.target.value),
})
}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, sm: 6 }}> <Grid size={{ xs: 12, sm: 6 }}>
@ -933,15 +1082,20 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
control={ control={
<Switch <Switch
checked={newExperience.isCurrent || false} checked={newExperience.isCurrent || false}
onChange={(e) => setNewExperience({ ...newExperience, isCurrent: e.target.checked })} onChange={e =>
size={isMobile ? "small" : "medium"} setNewExperience({
...newExperience,
isCurrent: e.target.checked,
})
}
size={isMobile ? 'small' : 'medium'}
/> />
} }
label="Currently working here" label="Currently working here"
sx={{ sx={{
'& .MuiFormControlLabel-label': { '& .MuiFormControlLabel-label': {
fontSize: { xs: '0.875rem', sm: '1rem' } fontSize: { xs: '0.875rem', sm: '1rem' },
} },
}} }}
/> />
</Grid> </Grid>
@ -952,22 +1106,29 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
rows={isMobile ? 3 : 4} rows={isMobile ? 3 : 4}
label="Job Description" label="Job Description"
value={newExperience.description || ''} value={newExperience.description || ''}
onChange={(e) => setNewExperience({ ...newExperience, description: e.target.value })} onChange={e =>
setNewExperience({
...newExperience,
description: e.target.value,
})
}
placeholder="Describe your responsibilities and achievements..." placeholder="Describe your responsibilities and achievements..."
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
/> />
</Grid> </Grid>
</Grid> </Grid>
</DialogContent> </DialogContent>
<DialogActions sx={{ <DialogActions
sx={{
p: { xs: 1.5, sm: 3 }, p: { xs: 1.5, sm: 3 },
flexDirection: { xs: 'column', sm: 'row' }, flexDirection: { xs: 'column', sm: 'row' },
gap: { xs: 1, sm: 0 } gap: { xs: 1, sm: 0 },
}}> }}
>
<Button <Button
onClick={() => setExperienceDialog(false)} onClick={() => setExperienceDialog(false)}
fullWidth={isMobile} fullWidth={isMobile}
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
> >
Cancel Cancel
</Button> </Button>
@ -975,7 +1136,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
onClick={handleAddExperience} onClick={handleAddExperience}
variant="contained" variant="contained"
fullWidth={isMobile} fullWidth={isMobile}
size={isMobile ? "small" : "medium"} size={isMobile ? 'small' : 'medium'}
> >
Add Experience Add Experience
</Button> </Button>

View File

@ -15,7 +15,7 @@ import {
useMediaQuery, useMediaQuery,
CircularProgress, CircularProgress,
Snackbar, Snackbar,
Alert Alert,
} from '@mui/material'; } from '@mui/material';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { CloudUpload, PhotoCamera } from '@mui/icons-material'; import { CloudUpload, PhotoCamera } from '@mui/icons-material';
@ -55,10 +55,14 @@ const CreateProfilePage: React.FC = () => {
const [profileImage, setProfileImage] = useState<string | null>(null); const [profileImage, setProfileImage] = useState<string | null>(null);
const [resumeFile, setResumeFile] = useState<File | null>(null); const [resumeFile, setResumeFile] = useState<File | null>(null);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [snackbar, setSnackbar] = useState<{open: boolean, message: string, severity: "success" | "error"}>({ const [snackbar, setSnackbar] = useState<{
open: boolean;
message: string;
severity: 'success' | 'error';
}>({
open: false, open: false,
message: '', message: '',
severity: 'success' severity: 'success',
}); });
const [formData, setFormData] = useState<ProfileFormData>({ const [formData, setFormData] = useState<ProfileFormData>({
@ -87,7 +91,7 @@ const CreateProfilePage: React.FC = () => {
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) { if (e.target.files && e.target.files[0]) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (event) => { reader.onload = event => {
if (event.target?.result) { if (event.target?.result) {
setProfileImage(event.target.result.toString()); setProfileImage(event.target.result.toString());
} }
@ -103,7 +107,7 @@ const CreateProfilePage: React.FC = () => {
setSnackbar({ setSnackbar({
open: true, open: true,
message: `Resume uploaded: ${e.target.files[0].name}`, message: `Resume uploaded: ${e.target.files[0].name}`,
severity: 'success' severity: 'success',
}); });
} }
}; };
@ -113,12 +117,12 @@ const CreateProfilePage: React.FC = () => {
if (activeStep === steps.length - 1) { if (activeStep === steps.length - 1) {
handleSubmit(); handleSubmit();
} else { } else {
setActiveStep((prevStep) => prevStep + 1); setActiveStep(prevStep => prevStep + 1);
} }
}; };
const handleBack = () => { const handleBack = () => {
setActiveStep((prevStep) => prevStep - 1); setActiveStep(prevStep => prevStep - 1);
}; };
// Form submission // Form submission
@ -131,7 +135,7 @@ const CreateProfilePage: React.FC = () => {
setSnackbar({ setSnackbar({
open: true, open: true,
message: 'Profile created successfully! Redirecting to dashboard...', message: 'Profile created successfully! Redirecting to dashboard...',
severity: 'success' severity: 'success',
}); });
// Redirect would happen here in a real application // Redirect would happen here in a real application
@ -143,9 +147,11 @@ const CreateProfilePage: React.FC = () => {
const isStepValid = () => { const isStepValid = () => {
switch (activeStep) { switch (activeStep) {
case 0: case 0:
return formData.firstName.trim() !== '' && return (
formData.firstName.trim() !== '' &&
formData.lastName.trim() !== '' && formData.lastName.trim() !== '' &&
formData.email.trim() !== ''; formData.email.trim() !== ''
);
case 1: case 1:
return formData.jobTitle.trim() !== ''; return formData.jobTitle.trim() !== '';
case 2: case 2:
@ -161,35 +167,33 @@ const CreateProfilePage: React.FC = () => {
case 0: case 0:
return ( return (
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid size={{xs: 12}} sx={{ textAlign: 'center', mb: 2 }}> <Grid size={{ xs: 12 }} sx={{ textAlign: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar <Avatar
src={profileImage || ''} src={profileImage || ''}
sx={{ sx={{
width: 120, width: 120,
height: 120, height: 120,
mb: 2, mb: 2,
border: `2px solid ${theme.palette.primary.main}` border: `2px solid ${theme.palette.primary.main}`,
}} }}
/> />
<IconButton <IconButton color="primary" aria-label="upload picture" component="label">
color="primary"
aria-label="upload picture"
component="label"
>
<PhotoCamera /> <PhotoCamera />
<VisuallyHiddenInput <VisuallyHiddenInput type="file" accept="image/*" onChange={handleImageUpload} />
type="file"
accept="image/*"
onChange={handleImageUpload}
/>
</IconButton> </IconButton>
<Typography variant="caption" color="textSecondary"> <Typography variant="caption" color="textSecondary">
Add profile photo Add profile photo
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
<Grid size={{xs: 12, sm: 6}}> <Grid size={{ xs: 12, sm: 6 }}>
<TextField <TextField
required required
fullWidth fullWidth
@ -200,7 +204,7 @@ const CreateProfilePage: React.FC = () => {
variant="outlined" variant="outlined"
/> />
</Grid> </Grid>
<Grid size={{xs: 12, sm: 6}}> <Grid size={{ xs: 12, sm: 6 }}>
<TextField <TextField
required required
fullWidth fullWidth
@ -211,7 +215,7 @@ const CreateProfilePage: React.FC = () => {
variant="outlined" variant="outlined"
/> />
</Grid> </Grid>
<Grid size={{xs: 12}}> <Grid size={{ xs: 12 }}>
<TextField <TextField
required required
fullWidth fullWidth
@ -223,7 +227,7 @@ const CreateProfilePage: React.FC = () => {
variant="outlined" variant="outlined"
/> />
</Grid> </Grid>
<Grid size={{xs:12}}> <Grid size={{ xs: 12 }}>
<TextField <TextField
fullWidth fullWidth
label="Phone Number" label="Phone Number"
@ -238,7 +242,7 @@ const CreateProfilePage: React.FC = () => {
case 1: case 1:
return ( return (
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid size={{xs:12}}> <Grid size={{ xs: 12 }}>
<TextField <TextField
required required
fullWidth fullWidth
@ -249,7 +253,7 @@ const CreateProfilePage: React.FC = () => {
variant="outlined" variant="outlined"
/> />
</Grid> </Grid>
<Grid size={{xs: 12}}> <Grid size={{ xs: 12 }}>
<TextField <TextField
fullWidth fullWidth
label="Location" label="Location"
@ -260,7 +264,7 @@ const CreateProfilePage: React.FC = () => {
variant="outlined" variant="outlined"
/> />
</Grid> </Grid>
<Grid size={{xs:12}}> <Grid size={{ xs: 12 }}>
<TextField <TextField
fullWidth fullWidth
multiline multiline
@ -278,10 +282,10 @@ const CreateProfilePage: React.FC = () => {
case 2: case 2:
return ( return (
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid size={{xs: 12}}> <Grid size={{ xs: 12 }}>
<Typography variant="body1" component="p"> <Typography variant="body1" component="p">
Upload your resume to complete your profile. We'll analyze it to better understand your skills and experience. Upload your resume to complete your profile. We'll analyze it to better understand
(Supported formats: .pdf, .docx, .md, and .txt) your skills and experience. (Supported formats: .pdf, .docx, .md, and .txt)
</Typography> </Typography>
<Box sx={{ textAlign: 'center', mt: 2 }}> <Box sx={{ textAlign: 'center', mt: 2 }}>
<Button <Button
@ -332,23 +336,17 @@ const CreateProfilePage: React.FC = () => {
orientation={isMobile ? 'vertical' : 'horizontal'} orientation={isMobile ? 'vertical' : 'horizontal'}
sx={{ mt: 3, mb: 5 }} sx={{ mt: 3, mb: 5 }}
> >
{steps.map((label) => ( {steps.map(label => (
<Step key={label}> <Step key={label}>
<StepLabel>{label}</StepLabel> <StepLabel>{label}</StepLabel>
</Step> </Step>
))} ))}
</Stepper> </Stepper>
<Box sx={{ mt: 2, mb: 4 }}> <Box sx={{ mt: 2, mb: 4 }}>{getStepContent(activeStep)}</Box>
{getStepContent(activeStep)}
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 4 }}>
<Button <Button disabled={activeStep === 0} onClick={handleBack} variant="outlined">
disabled={activeStep === 0}
onClick={handleBack}
variant="outlined"
>
Back Back
</Button> </Button>
<Button <Button

View File

@ -21,7 +21,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
IconButton, IconButton,
InputAdornment InputAdornment,
} from '@mui/material'; } from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material'; import { Visibility, VisibilityOff } from '@mui/icons-material';
import { ApiClient } from 'services/api-client'; import { ApiClient } from 'services/api-client';
@ -40,7 +40,7 @@ const CandidateRegistrationForm = () => {
confirmPassword: '', confirmPassword: '',
firstName: '', firstName: '',
lastName: '', lastName: '',
phone: '' phone: '',
}); });
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
@ -150,7 +150,7 @@ const CandidateRegistrationForm = () => {
password: formData.password, password: formData.password,
firstName: formData.firstName, firstName: formData.firstName,
lastName: formData.lastName, lastName: formData.lastName,
phone: formData.phone || undefined phone: formData.phone || undefined,
}); });
// Set pending verification // Set pending verification
@ -158,7 +158,6 @@ const CandidateRegistrationForm = () => {
setRegistrationResult(result); setRegistrationResult(result);
setShowSuccess(true); setShowSuccess(true);
} catch (error: any) { } catch (error: any) {
if (error.message.includes('already exists')) { if (error.message.includes('already exists')) {
if (error.message.includes('email')) { if (error.message.includes('email')) {
@ -167,7 +166,9 @@ const CandidateRegistrationForm = () => {
setErrors({ username: 'This username is already taken' }); setErrors({ username: 'This username is already taken' });
} }
} else { } else {
setErrors({ general: error.message || 'Registration failed. Please try again.' }); setErrors({
general: error.message || 'Registration failed. Please try again.',
});
} }
} finally { } finally {
setLoading(false); setLoading(false);
@ -180,7 +181,7 @@ const CandidateRegistrationForm = () => {
/[a-z]/.test(password), /[a-z]/.test(password),
/[A-Z]/.test(password), /[A-Z]/.test(password),
/\d/.test(password), /\d/.test(password),
/[!@#$%^&*(),.?":{}|<>]/.test(password) /[!@#$%^&*(),.?":{}|<>]/.test(password),
]; ];
const strength = validations.filter(Boolean).length; const strength = validations.filter(Boolean).length;
@ -209,7 +210,7 @@ const CandidateRegistrationForm = () => {
label="Email Address" label="Email Address"
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)} onChange={e => handleInputChange('email', e.target.value)}
placeholder="your.email@example.com" placeholder="your.email@example.com"
error={!!errors.email} error={!!errors.email}
helperText={errors.email} helperText={errors.email}
@ -220,7 +221,7 @@ const CandidateRegistrationForm = () => {
fullWidth fullWidth
label="Username" label="Username"
value={formData.username} value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value.toLowerCase())} onChange={e => handleInputChange('username', e.target.value.toLowerCase())}
placeholder="johndoe123" placeholder="johndoe123"
error={!!errors.username} error={!!errors.username}
helperText={errors.username} helperText={errors.username}
@ -232,7 +233,7 @@ const CandidateRegistrationForm = () => {
fullWidth fullWidth
label="First Name" label="First Name"
value={formData.firstName} value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)} onChange={e => handleInputChange('firstName', e.target.value)}
placeholder="John" placeholder="John"
error={!!errors.firstName} error={!!errors.firstName}
helperText={errors.firstName} helperText={errors.firstName}
@ -242,7 +243,7 @@ const CandidateRegistrationForm = () => {
fullWidth fullWidth
label="Last Name" label="Last Name"
value={formData.lastName} value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)} onChange={e => handleInputChange('lastName', e.target.value)}
placeholder="Doe" placeholder="Doe"
error={!!errors.lastName} error={!!errors.lastName}
helperText={errors.lastName} helperText={errors.lastName}
@ -255,10 +256,10 @@ const CandidateRegistrationForm = () => {
label="Phone Number" label="Phone Number"
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)} onChange={e => handleInputChange('phone', e.target.value)}
placeholder="+1 (555) 123-4567" placeholder="+1 (555) 123-4567"
error={!!errors.phone} error={!!errors.phone}
helperText={errors.phone || "Optional"} helperText={errors.phone || 'Optional'}
/> />
<Box> <Box>
@ -267,7 +268,7 @@ const CandidateRegistrationForm = () => {
label="Password" label="Password"
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={formData.password} value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)} onChange={e => handleInputChange('password', e.target.value)}
placeholder="Create a strong password" placeholder="Create a strong password"
error={!!errors.password} error={!!errors.password}
helperText={errors.password} helperText={errors.password}
@ -278,7 +279,7 @@ const CandidateRegistrationForm = () => {
<IconButton <IconButton
aria-label="toggle password visibility" aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
onMouseDown={(e) => e.preventDefault()} onMouseDown={e => e.preventDefault()}
edge="end" edge="end"
> >
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
@ -295,7 +296,11 @@ const CandidateRegistrationForm = () => {
color={passwordStrength.color as any} color={passwordStrength.color as any}
sx={{ height: 6, borderRadius: 3 }} sx={{ height: 6, borderRadius: 3 }}
/> />
<Typography variant="caption" color={`${passwordStrength.color}.main`} sx={{ mt: 0.5, display: 'block', textTransform: 'capitalize' }}> <Typography
variant="caption"
color={`${passwordStrength.color}.main`}
sx={{ mt: 0.5, display: 'block', textTransform: 'capitalize' }}
>
Password strength: {passwordStrength.level} Password strength: {passwordStrength.level}
</Typography> </Typography>
</Box> </Box>
@ -307,7 +312,7 @@ const CandidateRegistrationForm = () => {
label="Confirm Password" label="Confirm Password"
type={showConfirmPassword ? 'text' : 'password'} type={showConfirmPassword ? 'text' : 'password'}
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={(e) => handleInputChange('confirmPassword', e.target.value)} onChange={e => handleInputChange('confirmPassword', e.target.value)}
placeholder="Confirm your password" placeholder="Confirm your password"
error={!!errors.confirmPassword} error={!!errors.confirmPassword}
helperText={errors.confirmPassword} helperText={errors.confirmPassword}
@ -318,7 +323,7 @@ const CandidateRegistrationForm = () => {
<IconButton <IconButton
aria-label="toggle confirm password visibility" aria-label="toggle confirm password visibility"
onClick={() => setShowConfirmPassword(!showConfirmPassword)} onClick={() => setShowConfirmPassword(!showConfirmPassword)}
onMouseDown={(e) => e.preventDefault()} onMouseDown={e => e.preventDefault()}
edge="end" edge="end"
> >
{showConfirmPassword ? <VisibilityOff /> : <Visibility />} {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
@ -328,11 +333,7 @@ const CandidateRegistrationForm = () => {
}} }}
/> />
{errors.general && ( {errors.general && <Alert severity="error">{errors.general}</Alert>}
<Alert severity="error">
{errors.general}
</Alert>
)}
<Button <Button
fullWidth fullWidth
@ -357,7 +358,10 @@ const CandidateRegistrationForm = () => {
Already have an account?{' '} Already have an account?{' '}
<Link <Link
component="button" component="button"
onClick={(e) => { e.preventDefault(); navigate('/login'); }} onClick={e => {
e.preventDefault();
navigate('/login');
}}
sx={{ fontWeight: 600 }} sx={{ fontWeight: 600 }}
> >
Sign in here Sign in here
@ -389,7 +393,7 @@ const EmployerRegistrationForm = () => {
companySize: '', companySize: '',
companyDescription: '', companyDescription: '',
websiteUrl: '', websiteUrl: '',
phone: '' phone: '',
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -404,13 +408,26 @@ const EmployerRegistrationForm = () => {
const apiClient = new ApiClient(); const apiClient = new ApiClient();
const industryOptions = [ const industryOptions = [
'Technology', 'Healthcare', 'Finance', 'Education', 'Manufacturing', 'Technology',
'Retail', 'Consulting', 'Media', 'Non-profit', 'Government', 'Other' 'Healthcare',
'Finance',
'Education',
'Manufacturing',
'Retail',
'Consulting',
'Media',
'Non-profit',
'Government',
'Other',
]; ];
const companySizeOptions = [ const companySizeOptions = [
'1-10 employees', '11-50 employees', '51-200 employees', '1-10 employees',
'201-500 employees', '501-1000 employees', '1000+ employees' '11-50 employees',
'51-200 employees',
'201-500 employees',
'501-1000 employees',
'1000+ employees',
]; ];
const validateForm = () => { const validateForm = () => {
@ -522,7 +539,7 @@ const EmployerRegistrationForm = () => {
companySize: formData.companySize, companySize: formData.companySize,
companyDescription: formData.companyDescription, companyDescription: formData.companyDescription,
websiteUrl: formData.websiteUrl || undefined, websiteUrl: formData.websiteUrl || undefined,
phone: formData.phone || undefined phone: formData.phone || undefined,
}); });
// Set pending verification // Set pending verification
@ -530,7 +547,6 @@ const EmployerRegistrationForm = () => {
setRegistrationResult(result); setRegistrationResult(result);
setShowSuccess(true); setShowSuccess(true);
} catch (error: any) { } catch (error: any) {
if (error.message.includes('already exists')) { if (error.message.includes('already exists')) {
if (error.message.includes('email')) { if (error.message.includes('email')) {
@ -539,7 +555,9 @@ const EmployerRegistrationForm = () => {
setErrors({ username: 'This username is already taken' }); setErrors({ username: 'This username is already taken' });
} }
} else { } else {
setErrors({ general: error.message || 'Registration failed. Please try again.' }); setErrors({
general: error.message || 'Registration failed. Please try again.',
});
} }
} finally { } finally {
setLoading(false); setLoading(false);
@ -572,7 +590,7 @@ const EmployerRegistrationForm = () => {
label="Email Address" label="Email Address"
type="email" type="email"
value={formData.email} value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)} onChange={e => handleInputChange('email', e.target.value)}
placeholder="company@example.com" placeholder="company@example.com"
error={!!errors.email} error={!!errors.email}
helperText={errors.email} helperText={errors.email}
@ -582,7 +600,7 @@ const EmployerRegistrationForm = () => {
fullWidth fullWidth
label="Username" label="Username"
value={formData.username} value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value.toLowerCase())} onChange={e => handleInputChange('username', e.target.value.toLowerCase())}
placeholder="company123" placeholder="company123"
error={!!errors.username} error={!!errors.username}
helperText={errors.username} helperText={errors.username}
@ -596,7 +614,7 @@ const EmployerRegistrationForm = () => {
label="Password" label="Password"
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={formData.password} value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)} onChange={e => handleInputChange('password', e.target.value)}
placeholder="Create a strong password" placeholder="Create a strong password"
error={!!errors.password} error={!!errors.password}
helperText={errors.password} helperText={errors.password}
@ -607,7 +625,7 @@ const EmployerRegistrationForm = () => {
<IconButton <IconButton
aria-label="toggle password visibility" aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
onMouseDown={(e) => e.preventDefault()} onMouseDown={e => e.preventDefault()}
edge="end" edge="end"
> >
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
@ -621,7 +639,7 @@ const EmployerRegistrationForm = () => {
label="Confirm Password" label="Confirm Password"
type={showConfirmPassword ? 'text' : 'password'} type={showConfirmPassword ? 'text' : 'password'}
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={(e) => handleInputChange('confirmPassword', e.target.value)} onChange={e => handleInputChange('confirmPassword', e.target.value)}
placeholder="Confirm your password" placeholder="Confirm your password"
error={!!errors.confirmPassword} error={!!errors.confirmPassword}
helperText={errors.confirmPassword} helperText={errors.confirmPassword}
@ -632,7 +650,7 @@ const EmployerRegistrationForm = () => {
<IconButton <IconButton
aria-label="toggle confirm password visibility" aria-label="toggle confirm password visibility"
onClick={() => setShowConfirmPassword(!showConfirmPassword)} onClick={() => setShowConfirmPassword(!showConfirmPassword)}
onMouseDown={(e) => e.preventDefault()} onMouseDown={e => e.preventDefault()}
edge="end" edge="end"
> >
{showConfirmPassword ? <VisibilityOff /> : <Visibility />} {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
@ -656,7 +674,7 @@ const EmployerRegistrationForm = () => {
fullWidth fullWidth
label="Company Name" label="Company Name"
value={formData.companyName} value={formData.companyName}
onChange={(e) => handleInputChange('companyName', e.target.value)} onChange={e => handleInputChange('companyName', e.target.value)}
placeholder="Your Company Inc." placeholder="Your Company Inc."
error={!!errors.companyName} error={!!errors.companyName}
helperText={errors.companyName} helperText={errors.companyName}
@ -668,11 +686,13 @@ const EmployerRegistrationForm = () => {
<InputLabel>Industry</InputLabel> <InputLabel>Industry</InputLabel>
<Select <Select
value={formData.industry} value={formData.industry}
onChange={(e) => handleInputChange('industry', e.target.value)} onChange={e => handleInputChange('industry', e.target.value)}
label="Industry" label="Industry"
> >
{industryOptions.map(industry => ( {industryOptions.map(industry => (
<MenuItem key={industry} value={industry}>{industry}</MenuItem> <MenuItem key={industry} value={industry}>
{industry}
</MenuItem>
))} ))}
</Select> </Select>
{errors.industry && <FormHelperText>{errors.industry}</FormHelperText>} {errors.industry && <FormHelperText>{errors.industry}</FormHelperText>}
@ -682,11 +702,13 @@ const EmployerRegistrationForm = () => {
<InputLabel>Company Size</InputLabel> <InputLabel>Company Size</InputLabel>
<Select <Select
value={formData.companySize} value={formData.companySize}
onChange={(e) => handleInputChange('companySize', e.target.value)} onChange={e => handleInputChange('companySize', e.target.value)}
label="Company Size" label="Company Size"
> >
{companySizeOptions.map(size => ( {companySizeOptions.map(size => (
<MenuItem key={size} value={size}>{size}</MenuItem> <MenuItem key={size} value={size}>
{size}
</MenuItem>
))} ))}
</Select> </Select>
{errors.companySize && <FormHelperText>{errors.companySize}</FormHelperText>} {errors.companySize && <FormHelperText>{errors.companySize}</FormHelperText>}
@ -700,10 +722,13 @@ const EmployerRegistrationForm = () => {
multiline multiline
rows={4} rows={4}
value={formData.companyDescription} 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..." placeholder="Tell us about your company, what you do, your mission, and what makes you unique..."
error={!!errors.companyDescription} error={!!errors.companyDescription}
helperText={errors.companyDescription || `${formData.companyDescription.length}/50 characters minimum`} helperText={
errors.companyDescription ||
`${formData.companyDescription.length}/50 characters minimum`
}
required required
/> />
</Box> </Box>
@ -714,30 +739,26 @@ const EmployerRegistrationForm = () => {
label="Website URL" label="Website URL"
type="url" type="url"
value={formData.websiteUrl} value={formData.websiteUrl}
onChange={(e) => handleInputChange('websiteUrl', e.target.value)} onChange={e => handleInputChange('websiteUrl', e.target.value)}
placeholder="https://www.yourcompany.com" placeholder="https://www.yourcompany.com"
error={!!errors.websiteUrl} error={!!errors.websiteUrl}
helperText={errors.websiteUrl || "Optional"} helperText={errors.websiteUrl || 'Optional'}
/> />
<TextField <TextField
fullWidth fullWidth
label="Phone Number" label="Phone Number"
type="tel" type="tel"
value={formData.phone} value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)} onChange={e => handleInputChange('phone', e.target.value)}
placeholder="+1 (555) 123-4567" placeholder="+1 (555) 123-4567"
error={!!errors.phone} error={!!errors.phone}
helperText={errors.phone || "Optional"} helperText={errors.phone || 'Optional'}
/> />
</Stack> </Stack>
</Stack> </Stack>
</Box> </Box>
{errors.general && ( {errors.general && <Alert severity="error">{errors.general}</Alert>}
<Alert severity="error">
{errors.general}
</Alert>
)}
<Button <Button
fullWidth fullWidth
@ -805,13 +826,15 @@ export function RegistrationTypeSelector() {
'&:hover': { '&:hover': {
transform: 'translateY(-4px)', transform: 'translateY(-4px)',
boxShadow: 6, boxShadow: 6,
borderColor: 'primary.main' borderColor: 'primary.main',
} },
}} }}
onClick={() => window.location.href = '/register/candidate'} onClick={() => (window.location.href = '/register/candidate')}
> >
<CardContent sx={{ textAlign: 'center', py: 4 }}> <CardContent sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h1" sx={{ mb: 2 }}>👤</Typography> <Typography variant="h1" sx={{ mb: 2 }}>
👤
</Typography>
<Typography variant="h5" component="h3" sx={{ mb: 1.5 }}> <Typography variant="h5" component="h3" sx={{ mb: 1.5 }}>
I'm looking for work I'm looking for work
</Typography> </Typography>
@ -836,13 +859,15 @@ export function RegistrationTypeSelector() {
'&:hover': { '&:hover': {
transform: 'translateY(-4px)', transform: 'translateY(-4px)',
boxShadow: 6, boxShadow: 6,
borderColor: 'primary.main' borderColor: 'primary.main',
} },
}} }}
onClick={() => window.location.href = '/register/employer'} onClick={() => (window.location.href = '/register/employer')}
> >
<CardContent sx={{ textAlign: 'center', py: 4 }}> <CardContent sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h1" sx={{ mb: 2 }}>🏢</Typography> <Typography variant="h1" sx={{ mb: 2 }}>
🏢
</Typography>
<Typography variant="h5" component="h3" sx={{ mb: 1.5 }}> <Typography variant="h5" component="h3" sx={{ mb: 1.5 }}>
I'm hiring I'm hiring
</Typography> </Typography>

View File

@ -34,7 +34,9 @@ import * as Types from 'types/types';
// returns?: any // returns?: any
// }; // };
const SystemInfoComponent: React.FC<{ systemInfo: Types.SystemInfo | undefined }> = ({ systemInfo }) => { const SystemInfoComponent: React.FC<{
systemInfo: Types.SystemInfo | undefined;
}> = ({ systemInfo }) => {
const [systemElements, setSystemElements] = useState<ReactElement[]>([]); const [systemElements, setSystemElements] = useState<ReactElement[]>([]);
const convertToSymbols = (text: string) => { const convertToSymbols = (text: string) => {
@ -53,8 +55,15 @@ const SystemInfoComponent: React.FC<{ systemInfo: Types.SystemInfo | undefined }
if (Array.isArray(v)) { if (Array.isArray(v)) {
return v.map((card, index) => ( return v.map((card, index) => (
<div key={index} className="SystemInfoItem"> <div key={index} className="SystemInfoItem">
<div>{convertToSymbols(k)} {index}</div> <div>
<div>{convertToSymbols(card.name)} {card.discrete ? `w/ ${Math.round(card.memory / (1024 * 1024 * 1024))}GB RAM` : "(integrated)"}</div> {convertToSymbols(k)} {index}
</div>
<div>
{convertToSymbols(card.name)}{' '}
{card.discrete
? `w/ ${Math.round(card.memory / (1024 * 1024 * 1024))}GB RAM`
: '(integrated)'}
</div>
</div> </div>
)); ));
} }
@ -173,12 +182,11 @@ const Settings = (props: BackstoryPageProps) => {
setSystemInfo(response); setSystemInfo(response);
} catch (error) { } catch (error) {
console.error('Error obtaining system information:', error); console.error('Error obtaining system information:', error);
setSnack("Unable to obtain system information.", "error"); setSnack('Unable to obtain system information.', 'error');
};
} }
};
fetchSystemInfo(); fetchSystemInfo();
}, [systemInfo, setSystemInfo, setSnack, apiClient]); }, [systemInfo, setSystemInfo, setSnack, apiClient]);
// useEffect(() => { // useEffect(() => {
@ -284,7 +292,8 @@ const Settings = (props: BackstoryPageProps) => {
// } // }
// }; // };
return (<div className="Controls"> return (
<div className="Controls">
{/* <Typography component="span" sx={{ mb: 1 }}> {/* <Typography component="span" sx={{ mb: 1 }}>
You can change the information available to the LLM by adjusting the following settings: You can change the information available to the LLM by adjusting the following settings:
</Typography> </Typography>
@ -386,9 +395,7 @@ const Settings = (props: BackstoryPageProps) => {
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography component="span">System Information</Typography> <Typography component="span">System Information</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>The server is running on the following hardware:</AccordionDetails>
The server is running on the following hardware:
</AccordionDetails>
<AccordionActions> <AccordionActions>
<SystemInfoComponent systemInfo={systemInfo} /> <SystemInfoComponent systemInfo={systemInfo} />
</AccordionActions> </AccordionActions>
@ -396,9 +403,8 @@ const Settings = (props: BackstoryPageProps) => {
{/* <Button startIcon={<ResetIcon />} onClick={() => { reset(["history"], "History cleared."); }}>Delete Backstory History</Button> {/* <Button startIcon={<ResetIcon />} onClick={() => { reset(["history"], "History cleared."); }}>Delete Backstory History</Button>
<Button onClick={() => { reset(["rags", "tools", "system_prompt"], "Default settings restored.") }}>Reset system prompt, tunables, and RAG to defaults</Button> */} <Button onClick={() => { reset(["rags", "tools", "system_prompt"], "Default settings restored.") }}>Reset system prompt, tunables, and RAG to defaults</Button> */}
</div>); </div>
} );
export {
Settings
}; };
export { Settings };

View File

@ -1,23 +1,22 @@
import React, { useEffect } from "react"; import React, { useEffect } from 'react';
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from 'react-router-dom';
import { Box } from "@mui/material"; import { Box } from '@mui/material';
import { SetSnackType } from '../components/Snack'; import { SetSnackType } from '../components/Snack';
import { LoadingComponent } from "../components/LoadingComponent"; import { LoadingComponent } from '../components/LoadingComponent';
import { User, Guest, Candidate } from 'types/types'; import { User, Guest, Candidate } from 'types/types';
import { useAuth } from "hooks/AuthContext"; import { useAuth } from 'hooks/AuthContext';
import { useSelectedCandidate } from "hooks/GlobalContext"; import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
interface CandidateRouteProps { interface CandidateRouteProps {
guest?: Guest | null; guest?: Guest | null;
user?: User | null; user?: User | null;
setSnack: SetSnackType, }
};
const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProps) => { const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProps) => {
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate(); const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const { setSnack } = props; const { setSnack } = useAppState();
const { username } = useParams<{ username: string }>(); const { username } = useParams<{ username: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
@ -25,30 +24,33 @@ const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProp
if (selectedCandidate?.username === username || !username) { if (selectedCandidate?.username === username || !username) {
return; return;
} }
const getCandidate = async (reference: string) => { const getCandidate = async (reference: string): Promise<void> => {
try { try {
const result: Candidate = await apiClient.getCandidate(reference); const result: Candidate = await apiClient.getCandidate(reference);
setSelectedCandidate(result); setSelectedCandidate(result);
navigate('/chat'); navigate('/chat');
} catch { } catch {
setSnack(`Unable to obtain information for ${username}.`, "error"); setSnack(`Unable to obtain information for ${username}.`, 'error');
navigate('/'); navigate('/');
} }
} };
getCandidate(username); getCandidate(username);
}, [setSelectedCandidate, selectedCandidate, username, navigate, setSnack, apiClient]); }, [setSelectedCandidate, selectedCandidate, username, navigate, setSnack, apiClient]);
if (selectedCandidate?.username !== username) { if (selectedCandidate?.username !== username) {
return (<Box> return (
<Box>
<LoadingComponent <LoadingComponent
loadingText="Fetching candidate information..." loadingText="Fetching candidate information..."
loaderType="linear" loaderType="linear"
withFade={true} withFade={true}
fadeDuration={1200} /> fadeDuration={1200}
</Box>); />
</Box>
);
} else { } else {
return (<></>); return <></>;
} }
}; };

File diff suppressed because it is too large Load Diff

View File

@ -144,8 +144,8 @@ export function parseApiResponse<T>(data: any): ApiResponse<T> {
success: false, success: false,
error: { error: {
code: 'INVALID_RESPONSE', code: 'INVALID_RESPONSE',
message: 'Invalid response format' message: 'Invalid response format',
} },
}; };
} }
@ -176,8 +176,8 @@ export function parsePaginatedResponse<T>(
...apiResponse, ...apiResponse,
data: { data: {
...paginatedData, ...paginatedData,
data: paginatedData.data.map(itemParser) data: paginatedData.data.map(itemParser),
} },
}; };
} }
@ -306,7 +306,7 @@ export function createPaginatedRequest(params: Partial<PaginatedRequest> = {}):
page: 1, page: 1,
limit: 20, limit: 20,
sortOrder: 'desc', sortOrder: 'desc',
...params ...params,
}; };
} }
@ -350,7 +350,7 @@ export async function handlePaginatedApiResponse<T>(
/** /**
* Log conversion for debugging * Log conversion for debugging
*/ */
export function debugConversion<T>(obj: T, label: string = 'Object'): T { export function debugConversion<T>(obj: T, label = 'Object'): T {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.group(`🔄 ${label} Conversion`); console.group(`🔄 ${label} Conversion`);
console.log('Original:', obj); console.log('Original:', obj);
@ -375,7 +375,7 @@ const exports = {
createPaginatedRequest, createPaginatedRequest,
handleApiResponse, handleApiResponse,
handlePaginatedApiResponse, handlePaginatedApiResponse,
debugConversion debugConversion,
} };
export default exports; export default exports;

View File

@ -7,83 +7,131 @@
// Enums // Enums
// ============================ // ============================
export type AIModelType = "qwen2.5" | "flux-schnell"; export type AIModelType = 'qwen2.5' | 'flux-schnell';
export type ActivityType = "login" | "search" | "view_job" | "apply_job" | "message" | "update_profile" | "chat"; export type ActivityType =
| 'login'
| 'search'
| 'view_job'
| 'apply_job'
| 'message'
| 'update_profile'
| 'chat';
export type ApiActivityType = "system" | "info" | "searching" | "thinking" | "generating" | "converting" | "generating_image" | "tooling" | "heartbeat"; export type ApiActivityType =
| 'system'
| 'info'
| 'searching'
| 'thinking'
| 'generating'
| 'converting'
| 'generating_image'
| 'tooling'
| 'heartbeat';
export type ApiMessageType = "binary" | "text" | "json"; export type ApiMessageType = 'binary' | 'text' | 'json';
export type ApiStatusType = "streaming" | "status" | "done" | "error"; export type ApiStatusType = 'streaming' | 'status' | 'done' | 'error';
export type ApplicationStatus = "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn"; export type ApplicationStatus =
| 'applied'
| 'reviewing'
| 'interview'
| 'offer'
| 'rejected'
| 'accepted'
| 'withdrawn';
export type ChatContextType = "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_resume" | "generate_image" | "rag_search" | "skill_match"; export type ChatContextType =
| 'job_search'
| 'job_requirements'
| 'candidate_chat'
| 'interview_prep'
| 'resume_review'
| 'general'
| 'generate_persona'
| 'generate_profile'
| 'generate_resume'
| 'generate_image'
| 'rag_search'
| 'skill_match';
export type ChatSenderType = "user" | "assistant" | "system" | "information" | "warning" | "error"; export type ChatSenderType = 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error';
export type ColorBlindMode = "protanopia" | "deuteranopia" | "tritanopia" | "none"; export type ColorBlindMode = 'protanopia' | 'deuteranopia' | 'tritanopia' | 'none';
export type DataSourceType = "document" | "website" | "api" | "database" | "internal"; export type DataSourceType = 'document' | 'website' | 'api' | 'database' | 'internal';
export type DocumentType = "pdf" | "docx" | "txt" | "markdown" | "image"; export type DocumentType = 'pdf' | 'docx' | 'txt' | 'markdown' | 'image';
export type EmploymentType = "full-time" | "part-time" | "contract" | "internship" | "freelance"; export type EmploymentType = 'full-time' | 'part-time' | 'contract' | 'internship' | 'freelance';
export type FontSize = "small" | "medium" | "large"; export type FontSize = 'small' | 'medium' | 'large';
export type InterviewRecommendation = "strong_hire" | "hire" | "no_hire" | "strong_no_hire"; export type InterviewRecommendation = 'strong_hire' | 'hire' | 'no_hire' | 'strong_no_hire';
export type InterviewType = "phone" | "video" | "onsite" | "technical" | "behavioral"; export type InterviewType = 'phone' | 'video' | 'onsite' | 'technical' | 'behavioral';
export type LanguageProficiency = "basic" | "conversational" | "fluent" | "native"; export type LanguageProficiency = 'basic' | 'conversational' | 'fluent' | 'native';
export type MFAMethod = "app" | "sms" | "email"; export type MFAMethod = 'app' | 'sms' | 'email';
export type NotificationType = "email" | "push" | "in_app"; export type NotificationType = 'email' | 'push' | 'in_app';
export type ProcessingStepType = "extract" | "transform" | "chunk" | "embed" | "filter" | "summarize"; export type ProcessingStepType =
| 'extract'
| 'transform'
| 'chunk'
| 'embed'
| 'filter'
| 'summarize';
export type SalaryPeriod = "hour" | "day" | "month" | "year"; export type SalaryPeriod = 'hour' | 'day' | 'month' | 'year';
export type SearchType = "similarity" | "mmr" | "hybrid" | "keyword"; export type SearchType = 'similarity' | 'mmr' | 'hybrid' | 'keyword';
export type SkillLevel = "beginner" | "intermediate" | "advanced" | "expert"; export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'expert';
export type SkillStatus = "pending" | "complete" | "waiting" | "error"; export type SkillStatus = 'pending' | 'complete' | 'waiting' | 'error';
export type SkillStrength = "strong" | "moderate" | "weak" | "none"; export type SkillStrength = 'strong' | 'moderate' | 'weak' | 'none';
export type SocialPlatform = "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other"; export type SocialPlatform =
| 'linkedin'
| 'twitter'
| 'github'
| 'dribbble'
| 'behance'
| 'website'
| 'other';
export type SortOrder = "asc" | "desc"; export type SortOrder = 'asc' | 'desc';
export type ThemePreference = "light" | "dark" | "system"; export type ThemePreference = 'light' | 'dark' | 'system';
export type UserGender = "female" | "male"; export type UserGender = 'female' | 'male';
export type UserStatus = "active" | "inactive" | "pending" | "banned"; export type UserStatus = 'active' | 'inactive' | 'pending' | 'banned';
export type UserType = "candidate" | "employer" | "guest"; export type UserType = 'candidate' | 'employer' | 'guest';
export type VectorStoreType = "chroma"; export type VectorStoreType = 'chroma';
// ============================ // ============================
// Interfaces // Interfaces
// ============================ // ============================
export interface AccessibilitySettings { export interface AccessibilitySettings {
fontSize: "small" | "medium" | "large"; fontSize: 'small' | 'medium' | 'large';
highContrast: boolean; highContrast: boolean;
reduceMotion: boolean; reduceMotion: boolean;
screenReader: boolean; screenReader: boolean;
colorBlindMode?: "protanopia" | "deuteranopia" | "tritanopia" | "none"; colorBlindMode?: 'protanopia' | 'deuteranopia' | 'tritanopia' | 'none';
} }
export interface Analytics { export interface Analytics {
id?: string; id?: string;
entityType: "job" | "candidate" | "chat" | "system" | "employer"; entityType: 'job' | 'candidate' | 'chat' | 'system' | 'employer';
entityId: string; entityId: string;
metricType: string; metricType: string;
value: number; value: number;
@ -96,8 +144,8 @@ export interface ApiMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
} }
@ -109,7 +157,7 @@ export interface ApiResponse {
} }
export interface ApplicationDecision { export interface ApplicationDecision {
status: "accepted" | "rejected"; status: 'accepted' | 'rejected';
reason?: string; reason?: string;
date: Date; date: Date;
by: string; by: string;
@ -145,14 +193,14 @@ export interface Authentication {
resetPasswordExpiry?: Date; resetPasswordExpiry?: Date;
lastPasswordChange: Date; lastPasswordChange: Date;
mfaEnabled: boolean; mfaEnabled: boolean;
mfaMethod?: "app" | "sms" | "email"; mfaMethod?: 'app' | 'sms' | 'email';
mfaSecret?: string; mfaSecret?: string;
loginAttempts: number; loginAttempts: number;
lockedUntil?: Date; lockedUntil?: Date;
} }
export interface BaseUser { export interface BaseUser {
userType: "candidate" | "employer" | "guest"; userType: 'candidate' | 'employer' | 'guest';
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
email: string; email: string;
@ -165,18 +213,18 @@ export interface BaseUser {
updatedAt?: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: 'active' | 'inactive' | 'pending' | 'banned';
isAdmin: boolean; isAdmin: boolean;
} }
export interface BaseUserWithType { export interface BaseUserWithType {
userType: "candidate" | "employer" | "guest"; userType: 'candidate' | 'employer' | 'guest';
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
} }
export interface Candidate { export interface Candidate {
userType: "candidate" | "employer" | "guest"; userType: 'candidate' | 'employer' | 'guest';
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
email: string; email: string;
@ -189,7 +237,7 @@ export interface Candidate {
updatedAt?: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: 'active' | 'inactive' | 'pending' | 'banned';
isAdmin: boolean; isAdmin: boolean;
username: string; username: string;
description?: string; description?: string;
@ -198,7 +246,7 @@ export interface Candidate {
experience?: Array<WorkExperience>; experience?: Array<WorkExperience>;
questions?: Array<CandidateQuestion>; questions?: Array<CandidateQuestion>;
education?: Array<Education>; education?: Array<Education>;
preferredJobTypes?: Array<"full-time" | "part-time" | "contract" | "internship" | "freelance">; preferredJobTypes?: Array<'full-time' | 'part-time' | 'contract' | 'internship' | 'freelance'>;
desiredSalary?: DesiredSalary; desiredSalary?: DesiredSalary;
availabilityDate?: Date; availabilityDate?: Date;
summary?: string; summary?: string;
@ -211,7 +259,7 @@ export interface Candidate {
} }
export interface CandidateAI { export interface CandidateAI {
userType: "candidate" | "employer" | "guest"; userType: 'candidate' | 'employer' | 'guest';
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
email: string; email: string;
@ -224,7 +272,7 @@ export interface CandidateAI {
updatedAt?: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: 'active' | 'inactive' | 'pending' | 'banned';
isAdmin: boolean; isAdmin: boolean;
username: string; username: string;
description?: string; description?: string;
@ -233,7 +281,7 @@ export interface CandidateAI {
experience?: Array<WorkExperience>; experience?: Array<WorkExperience>;
questions?: Array<CandidateQuestion>; questions?: Array<CandidateQuestion>;
education?: Array<Education>; education?: Array<Education>;
preferredJobTypes?: Array<"full-time" | "part-time" | "contract" | "internship" | "freelance">; preferredJobTypes?: Array<'full-time' | 'part-time' | 'contract' | 'internship' | 'freelance'>;
desiredSalary?: DesiredSalary; desiredSalary?: DesiredSalary;
availabilityDate?: Date; availabilityDate?: Date;
summary?: string; summary?: string;
@ -245,7 +293,7 @@ export interface CandidateAI {
isPublic: boolean; isPublic: boolean;
isAI: boolean; isAI: boolean;
age?: number; age?: number;
gender?: "female" | "male"; gender?: 'female' | 'male';
ethnicity?: string; ethnicity?: string;
} }
@ -284,9 +332,21 @@ export interface Certification {
} }
export interface ChatContext { export interface ChatContext {
type: "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_resume" | "generate_image" | "rag_search" | "skill_match"; type:
| 'job_search'
| 'job_requirements'
| 'candidate_chat'
| 'interview_prep'
| 'resume_review'
| 'general'
| 'generate_persona'
| 'generate_profile'
| 'generate_resume'
| 'generate_image'
| 'rag_search'
| 'skill_match';
relatedEntityId?: string; relatedEntityId?: string;
relatedEntityType?: "job" | "candidate" | "employer"; relatedEntityType?: 'job' | 'candidate' | 'employer';
additionalContext?: Record<string, any>; additionalContext?: Record<string, any>;
} }
@ -294,10 +354,10 @@ export interface ChatMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
role: "user" | "assistant" | "system" | "information" | "warning" | "error"; role: 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error';
content: string; content: string;
tunables?: Tunables; tunables?: Tunables;
metadata: ChatMessageMetaData; metadata: ChatMessageMetaData;
@ -307,14 +367,14 @@ export interface ChatMessageError {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
content: string; content: string;
} }
export interface ChatMessageMetaData { export interface ChatMessageMetaData {
model: "qwen2.5" | "flux-schnell"; model: 'qwen2.5' | 'flux-schnell';
temperature: number; temperature: number;
maxTokens: number; maxTokens: number;
topP: number; topP: number;
@ -336,8 +396,8 @@ export interface ChatMessageRagSearch {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
dimensions: number; dimensions: number;
content: Array<ChromaDBGetResponse>; content: Array<ChromaDBGetResponse>;
@ -347,10 +407,10 @@ export interface ChatMessageResume {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
role: "user" | "assistant" | "system" | "information" | "warning" | "error"; role: 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error';
content: string; content: string;
tunables?: Tunables; tunables?: Tunables;
metadata: ChatMessageMetaData; metadata: ChatMessageMetaData;
@ -363,10 +423,10 @@ export interface ChatMessageSkillAssessment {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
role: "user" | "assistant" | "system" | "information" | "warning" | "error"; role: 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error';
content: string; content: string;
tunables?: Tunables; tunables?: Tunables;
metadata: ChatMessageMetaData; metadata: ChatMessageMetaData;
@ -377,10 +437,19 @@ export interface ChatMessageStatus {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
activity: "system" | "info" | "searching" | "thinking" | "generating" | "converting" | "generating_image" | "tooling" | "heartbeat"; activity:
| 'system'
| 'info'
| 'searching'
| 'thinking'
| 'generating'
| 'converting'
| 'generating_image'
| 'tooling'
| 'heartbeat';
content: any; content: any;
} }
@ -388,8 +457,8 @@ export interface ChatMessageStreaming {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
content: string; content: string;
} }
@ -398,10 +467,10 @@ export interface ChatMessageUser {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
role: "user" | "assistant" | "system" | "information" | "warning" | "error"; role: 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error';
content: string; content: string;
tunables?: Tunables; tunables?: Tunables;
} }
@ -474,12 +543,12 @@ export interface DataSourceConfiguration {
id?: string; id?: string;
ragConfigId: string; ragConfigId: string;
name: string; name: string;
sourceType: "document" | "website" | "api" | "database" | "internal"; sourceType: 'document' | 'website' | 'api' | 'database' | 'internal';
connectionDetails: Record<string, any>; connectionDetails: Record<string, any>;
processingPipeline: Array<ProcessingStep>; processingPipeline: Array<ProcessingStep>;
refreshSchedule?: string; refreshSchedule?: string;
lastRefreshed?: Date; lastRefreshed?: Date;
status: "active" | "pending" | "error" | "processing"; status: 'active' | 'pending' | 'error' | 'processing';
errorDetails?: string; errorDetails?: string;
metadata?: Record<string, any>; metadata?: Record<string, any>;
} }
@ -487,7 +556,7 @@ export interface DataSourceConfiguration {
export interface DesiredSalary { export interface DesiredSalary {
amount: number; amount: number;
currency: string; currency: string;
period: "hour" | "day" | "month" | "year"; period: 'hour' | 'day' | 'month' | 'year';
} }
export interface Document { export interface Document {
@ -495,7 +564,7 @@ export interface Document {
ownerId: string; ownerId: string;
filename: string; filename: string;
originalName: string; originalName: string;
type: "pdf" | "docx" | "txt" | "markdown" | "image"; type: 'pdf' | 'docx' | 'txt' | 'markdown' | 'image';
size: number; size: number;
uploadDate?: Date; uploadDate?: Date;
options?: DocumentOptions; options?: DocumentOptions;
@ -505,7 +574,7 @@ export interface Document {
export interface DocumentContentResponse { export interface DocumentContentResponse {
documentId: string; documentId: string;
filename: string; filename: string;
type: "pdf" | "docx" | "txt" | "markdown" | "image"; type: 'pdf' | 'docx' | 'txt' | 'markdown' | 'image';
content: string; content: string;
size: number; size: number;
} }
@ -519,8 +588,8 @@ export interface DocumentMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
document: Document; document: Document;
content?: string; content?: string;
@ -562,7 +631,7 @@ export interface EmailVerificationRequest {
} }
export interface Employer { export interface Employer {
userType: "candidate" | "employer" | "guest"; userType: 'candidate' | 'employer' | 'guest';
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
email: string; email: string;
@ -575,7 +644,7 @@ export interface Employer {
updatedAt?: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: 'active' | 'inactive' | 'pending' | 'banned';
isAdmin: boolean; isAdmin: boolean;
companyName: string; companyName: string;
industry: string; industry: string;
@ -615,7 +684,7 @@ export interface GPUInfo {
} }
export interface Guest { export interface Guest {
userType: "candidate" | "employer" | "guest"; userType: 'candidate' | 'employer' | 'guest';
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
email: string; email: string;
@ -628,7 +697,7 @@ export interface Guest {
updatedAt?: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: "active" | "inactive" | "pending" | "banned"; status: 'active' | 'inactive' | 'pending' | 'banned';
isAdmin: boolean; isAdmin: boolean;
sessionId: string; sessionId: string;
username: string; username: string;
@ -644,7 +713,7 @@ export interface GuestCleanupRequest {
} }
export interface GuestConversionRequest { export interface GuestConversionRequest {
accountType: "candidate" | "employer"; accountType: 'candidate' | 'employer';
email: string; email: string;
username: string; username: string;
password: string; password: string;
@ -663,7 +732,7 @@ export interface GuestSessionResponse {
refreshToken: string; refreshToken: string;
user: Guest; user: Guest;
expiresAt: number; expiresAt: number;
userType: "guest"; userType: 'guest';
isGuest: boolean; isGuest: boolean;
} }
@ -685,7 +754,7 @@ export interface InterviewFeedback {
overallScore: number; overallScore: number;
strengths: Array<string>; strengths: Array<string>;
weaknesses: Array<string>; weaknesses: Array<string>;
recommendation: "strong_hire" | "hire" | "no_hire" | "strong_no_hire"; recommendation: 'strong_hire' | 'hire' | 'no_hire' | 'strong_no_hire';
comments: string; comments: string;
createdAt?: Date; createdAt?: Date;
updatedAt?: Date; updatedAt?: Date;
@ -698,19 +767,19 @@ export interface InterviewSchedule {
applicationId: string; applicationId: string;
scheduledDate: Date; scheduledDate: Date;
endDate: Date; endDate: Date;
interviewType: "phone" | "video" | "onsite" | "technical" | "behavioral"; interviewType: 'phone' | 'video' | 'onsite' | 'technical' | 'behavioral';
interviewers: Array<string>; interviewers: Array<string>;
location?: string | Location; location?: string | Location;
notes?: string; notes?: string;
feedback?: InterviewFeedback; feedback?: InterviewFeedback;
status: "scheduled" | "completed" | "cancelled" | "rescheduled"; status: 'scheduled' | 'completed' | 'cancelled' | 'rescheduled';
meetingLink?: string; meetingLink?: string;
} }
export interface Job { export interface Job {
id?: string; id?: string;
ownerId: string; ownerId: string;
ownerType: "candidate" | "employer" | "guest"; ownerType: 'candidate' | 'employer' | 'guest';
owner?: BaseUser; owner?: BaseUser;
title?: string; title?: string;
summary?: string; summary?: string;
@ -726,7 +795,7 @@ export interface JobApplication {
id?: string; id?: string;
jobId: string; jobId: string;
candidateId: string; candidateId: string;
status: "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn"; status: 'applied' | 'reviewing' | 'interview' | 'offer' | 'rejected' | 'accepted' | 'withdrawn';
appliedDate: Date; appliedDate: Date;
updatedDate: Date; updatedDate: Date;
resumeVersion: string; resumeVersion: string;
@ -741,7 +810,7 @@ export interface JobApplication {
export interface JobDetails { export interface JobDetails {
location: Location; location: Location;
salaryRange?: SalaryRange; salaryRange?: SalaryRange;
employmentType: "full-time" | "part-time" | "contract" | "internship" | "freelance"; employmentType: 'full-time' | 'part-time' | 'contract' | 'internship' | 'freelance';
datePosted?: Date; datePosted?: Date;
applicationDeadline?: Date; applicationDeadline?: Date;
isActive: boolean; isActive: boolean;
@ -777,8 +846,8 @@ export interface JobRequirementsMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
job: Job; job: Job;
} }
@ -792,7 +861,7 @@ export interface JobResponse {
export interface Language { export interface Language {
language: string; language: string;
proficiency: "basic" | "conversational" | "fluent" | "native"; proficiency: 'basic' | 'conversational' | 'fluent' | 'native';
} }
export interface Location { export interface Location {
@ -847,7 +916,7 @@ export interface MessageReaction {
} }
export interface NotificationPreference { export interface NotificationPreference {
type: "email" | "push" | "in_app"; type: 'email' | 'push' | 'in_app';
events: Array<string>; events: Array<string>;
isEnabled: boolean; isEnabled: boolean;
} }
@ -856,7 +925,7 @@ export interface PaginatedRequest {
page: number; page: number;
limit: number; limit: number;
sortBy?: string; sortBy?: string;
sortOrder?: "asc" | "desc"; sortOrder?: 'asc' | 'desc';
filters?: Record<string, any>; filters?: Record<string, any>;
} }
@ -878,7 +947,7 @@ export interface PointOfContact {
export interface ProcessingStep { export interface ProcessingStep {
id?: string; id?: string;
type: "extract" | "transform" | "chunk" | "embed" | "filter" | "summarize"; type: 'extract' | 'transform' | 'chunk' | 'embed' | 'filter' | 'summarize';
parameters: Record<string, any>; parameters: Record<string, any>;
order: number; order: number;
dependsOn?: Array<string>; dependsOn?: Array<string>;
@ -891,7 +960,7 @@ export interface RAGConfiguration {
description?: string; description?: string;
dataSourceConfigurations: Array<DataSourceConfiguration>; dataSourceConfigurations: Array<DataSourceConfiguration>;
embeddingModel: string; embeddingModel: string;
vectorStoreType: "chroma"; vectorStoreType: 'chroma';
retrievalParameters: RetrievalParameters; retrievalParameters: RetrievalParameters;
createdAt?: Date; createdAt?: Date;
updatedAt?: Date; updatedAt?: Date;
@ -981,17 +1050,17 @@ export interface ResumeMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: "streaming" | "status" | "done" | "error"; status: 'streaming' | 'status' | 'done' | 'error';
type: "binary" | "text" | "json"; type: 'binary' | 'text' | 'json';
timestamp?: Date; timestamp?: Date;
role: "user" | "assistant" | "system" | "information" | "warning" | "error"; role: 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error';
content: string; content: string;
tunables?: Tunables; tunables?: Tunables;
resume: Resume; resume: Resume;
} }
export interface RetrievalParameters { export interface RetrievalParameters {
searchType: "similarity" | "mmr" | "hybrid" | "keyword"; searchType: 'similarity' | 'mmr' | 'hybrid' | 'keyword';
topK: number; topK: number;
similarityThreshold?: number; similarityThreshold?: number;
rerankerModel?: string; rerankerModel?: string;
@ -1004,7 +1073,7 @@ export interface SalaryRange {
min: number; min: number;
max: number; max: number;
currency: string; currency: string;
period: "hour" | "day" | "month" | "year"; period: 'hour' | 'day' | 'month' | 'year';
isVisible: boolean; isVisible: boolean;
} }
@ -1014,14 +1083,14 @@ export interface SearchQuery {
page: number; page: number;
limit: number; limit: number;
sortBy?: string; sortBy?: string;
sortOrder?: "asc" | "desc"; sortOrder?: 'asc' | 'desc';
} }
export interface Skill { export interface Skill {
id?: string; id?: string;
name: string; name: string;
category: string; category: string;
level: "beginner" | "intermediate" | "advanced" | "expert"; level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
yearsOfExperience?: number; yearsOfExperience?: number;
} }
@ -1030,7 +1099,7 @@ export interface SkillAssessment {
skill: string; skill: string;
skillModified?: string; skillModified?: string;
evidenceFound: boolean; evidenceFound: boolean;
evidenceStrength: "strong" | "moderate" | "weak" | "none"; evidenceStrength: 'strong' | 'moderate' | 'weak' | 'none';
assessment: string; assessment: string;
description: string; description: string;
evidenceDetails?: Array<EvidenceDetail>; evidenceDetails?: Array<EvidenceDetail>;
@ -1040,7 +1109,7 @@ export interface SkillAssessment {
} }
export interface SocialLink { export interface SocialLink {
platform: "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other"; platform: 'linkedin' | 'twitter' | 'github' | 'dribbble' | 'behance' | 'website' | 'other';
url: string; url: string;
} }
@ -1063,7 +1132,14 @@ export interface UserActivity {
id?: string; id?: string;
userId?: string; userId?: string;
guestId?: string; guestId?: string;
activityType: "login" | "search" | "view_job" | "apply_job" | "message" | "update_profile" | "chat"; activityType:
| 'login'
| 'search'
| 'view_job'
| 'apply_job'
| 'message'
| 'update_profile'
| 'chat';
timestamp: Date; timestamp: Date;
metadata: Record<string, any>; metadata: Record<string, any>;
ipAddress?: string; ipAddress?: string;
@ -1073,13 +1149,13 @@ export interface UserActivity {
export interface UserPreference { export interface UserPreference {
userId: string; userId: string;
theme: "light" | "dark" | "system"; theme: 'light' | 'dark' | 'system';
notifications: Array<NotificationPreference>; notifications: Array<NotificationPreference>;
accessibility: AccessibilitySettings; accessibility: AccessibilitySettings;
dashboardLayout?: Record<string, any>; dashboardLayout?: Record<string, any>;
language: string; language: string;
timezone: string; timezone: string;
emailFrequency: "immediate" | "daily" | "weekly" | "never"; emailFrequency: 'immediate' | 'daily' | 'weekly' | 'never';
} }
export interface WorkExperience { export interface WorkExperience {
@ -1244,9 +1320,13 @@ export function convertCandidateFromApi(data: any): Candidate {
// Convert nested Education model // Convert nested Education model
education: data.education ? convertEducationFromApi(data.education) : undefined, education: data.education ? convertEducationFromApi(data.education) : undefined,
// Convert nested Certification model // Convert nested Certification model
certifications: data.certifications ? convertCertificationFromApi(data.certifications) : undefined, certifications: data.certifications
? convertCertificationFromApi(data.certifications)
: undefined,
// Convert nested JobApplication model // Convert nested JobApplication model
jobApplications: data.jobApplications ? convertJobApplicationFromApi(data.jobApplications) : undefined, jobApplications: data.jobApplications
? convertJobApplicationFromApi(data.jobApplications)
: undefined,
}; };
} }
/** /**
@ -1274,7 +1354,9 @@ export function convertCandidateAIFromApi(data: any): CandidateAI {
// Convert nested Education model // Convert nested Education model
education: data.education ? convertEducationFromApi(data.education) : undefined, education: data.education ? convertEducationFromApi(data.education) : undefined,
// Convert nested Certification model // Convert nested Certification model
certifications: data.certifications ? convertCertificationFromApi(data.certifications) : undefined, certifications: data.certifications
? convertCertificationFromApi(data.certifications)
: undefined,
}; };
} }
/** /**
@ -1605,7 +1687,9 @@ export function convertInterviewFeedbackFromApi(data: any): InterviewFeedback {
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined, updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert nested SkillAssessment model // Convert nested SkillAssessment model
skillAssessments: data.skillAssessments ? convertSkillAssessmentFromApi(data.skillAssessments) : undefined, skillAssessments: data.skillAssessments
? convertSkillAssessmentFromApi(data.skillAssessments)
: undefined,
}; };
} }
/** /**
@ -1661,7 +1745,9 @@ export function convertJobApplicationFromApi(data: any): JobApplication {
// Convert updatedDate from ISO string to Date // Convert updatedDate from ISO string to Date
updatedDate: new Date(data.updatedDate), updatedDate: new Date(data.updatedDate),
// Convert nested InterviewSchedule model // Convert nested InterviewSchedule model
interviewSchedules: data.interviewSchedules ? convertInterviewScheduleFromApi(data.interviewSchedules) : undefined, interviewSchedules: data.interviewSchedules
? convertInterviewScheduleFromApi(data.interviewSchedules)
: undefined,
// Convert nested ApplicationDecision model // Convert nested ApplicationDecision model
decision: data.decision ? convertApplicationDecisionFromApi(data.decision) : undefined, decision: data.decision ? convertApplicationDecisionFromApi(data.decision) : undefined,
}; };
@ -1753,7 +1839,9 @@ export function convertRAGConfigurationFromApi(data: any): RAGConfiguration {
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined, updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert nested DataSourceConfiguration model // Convert nested DataSourceConfiguration model
dataSourceConfigurations: data.dataSourceConfigurations.map((item: any) => convertDataSourceConfigurationFromApi(item)), dataSourceConfigurations: data.dataSourceConfigurations.map((item: any) =>
convertDataSourceConfigurationFromApi(item)
),
}; };
} }
/** /**
@ -2002,4 +2090,4 @@ export function convertArrayFromApi<T>(data: any[], modelType: string): T[] {
export type User = Candidate | Employer; export type User = Candidate | Employer;
// Export all types // Export all types
export type { }; export type {};