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-plotly.js": "^2.6.0",
"react-router-dom": "^7.6.0",
"react-scripts": "5.0.1",
"react-scripts": "^5.0.1",
"react-spinners": "^0.15.0",
"react-to-print": "^3.1.0",
"rehype-katex": "^7.0.1",
@ -50,7 +50,10 @@
"scripts": {
"start": "WDS_SOCKET_HOST=backstory-beta.ketrenos.com WDS_SOCKET_PORT=443 craco start",
"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": {
"extends": [
@ -73,6 +76,14 @@
"devDependencies": {
"@craco/craco": "^7.1.0",
"@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;
white-space: pre-wrap;
box-sizing: border-box;
border: 3px solid #E0E0E0;
border: 3px solid #e0e0e0;
}
button {
@ -72,8 +72,8 @@ button {
.Controls {
display: flex;
background-color: #F5F5F5;
border: 1px solid #E0E0E0;
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
overflow-y: auto;
padding: 10px;
flex-direction: column;
@ -93,8 +93,8 @@ button {
flex-direction: column;
min-width: 10rem;
flex-grow: 1;
background-color: #1A2536; /* Midnight Blue */
color: #D3CDBF; /* Warm Gray */
background-color: #1a2536; /* Midnight Blue */
color: #d3cdbf; /* Warm Gray */
border-radius: 0;
}
@ -115,12 +115,12 @@ button {
max-width: 1024px;
width: 100%;
margin: 0 auto;
background-color: #D3CDBF;
background-color: #d3cdbf;
}
.user-message.MuiCard-root {
background-color: #DCF8C6;
border: 1px solid #B2E0A7;
background-color: #dcf8c6;
border: 1px solid #b2e0a7;
color: #333333;
margin-bottom: 0.75rem;
margin-left: 1rem;
@ -140,8 +140,8 @@ button {
.Docs.MuiCard-root,
.assistant-message.MuiCard-root {
border: 1px solid #E0E0E0;
background-color: #FFFFFF;
border: 1px solid #e0e0e0;
background-color: #ffffff;
color: #333333;
margin-bottom: 0.75rem;
margin-right: 1rem;
@ -158,7 +158,6 @@ button {
font-size: 0.9rem;
}
.Docs.MuiCard-root {
display: flex;
flex-grow: 1;
@ -193,7 +192,7 @@ button {
}
.metadata {
border: 1px solid #E0E0E0;
border: 1px solid #e0e0e0;
font-size: 0.75rem;
padding: 0.125rem;
}
@ -239,7 +238,7 @@ button {
/* Reduce space around code blocks */
* .MuiTypography-root pre {
border: 1px solid #F5F5F5;
border: 1px solid #f5f5f5;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
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 { ThemeProvider } from '@mui/material/styles';
import { backstoryTheme } from './BackstoryTheme';
import { SeverityType } from 'components/Snack';
import { ConversationHandle } from 'components/Conversation';
import { CandidateRoute } from 'routes/CandidateRoute';
import { BackstoryLayout } from 'components/layout/BackstoryLayout';
@ -17,23 +16,21 @@ import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
const BackstoryApp = () => {
const BackstoryApp = (): JSX.Element => {
const navigate = useNavigate();
const location = useLocation();
const chatRef = useRef<ConversationHandle>(null);
const snackRef = useRef<any>(null);
const setSnack = useCallback((message: string, severity?: SeverityType) => {
snackRef.current?.setSnack(message, severity);
}, [snackRef]);
const submitQuery = (query: ChatQuery) => {
const submitQuery = (query: ChatQuery): void => {
console.log(`handleSubmitChatQuery:`, query, chatRef.current ? ' sending' : 'no handler');
chatRef.current?.submitQuery(query);
navigate('/chat');
};
const [page, setPage] = useState<string>("");
const [page, setPage] = useState<string>('');
useEffect(() => {
const currentRoute = location.pathname.split("/")[1] ? `/${location.pathname.split("/")[1]}` : "/";
const currentRoute = location.pathname.split('/')[1]
? `/${location.pathname.split('/')[1]}`
: '/';
setPage(currentRoute);
}, [location.pathname]);
@ -43,14 +40,9 @@ const BackstoryApp = () => {
<AuthProvider>
<AppStateProvider>
<Routes>
<Route path="/u/:username" element={<CandidateRoute {...{ setSnack }} />} />
<Route path="/u/:username" element={<CandidateRoute />} />
{/* Static/shared routes */}
<Route
path="/*"
element={
<BackstoryLayout {...{ setSnack, page, chatRef, snackRef, submitQuery }} />
}
/>
<Route path="/*" element={<BackstoryLayout {...{ page, chatRef, submitQuery }} />} />
</Routes>
</AppStateProvider>
</AuthProvider>
@ -58,6 +50,4 @@ const BackstoryApp = () => {
);
};
export {
BackstoryApp
};
export { BackstoryApp };

View File

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

View File

@ -7,47 +7,41 @@ import { SetSnackType } from './Snack';
interface BackstoryElementProps {
// setSnack: SetSnackType,
// submitQuery: ChatSubmitQueryInterface,
sx?: SxProps<Theme>,
sx?: SxProps<Theme>;
}
interface BackstoryPageProps extends BackstoryElementProps {
route?: string,
setRoute?: (route: string) => void,
};
route?: string;
setRoute?: (route: string) => void;
}
interface BackstoryTabProps {
label?: string,
path: string,
children?: ReactElement<BackstoryPageProps>,
active?: boolean,
className?: string,
label?: string;
path: string;
children?: ReactElement<BackstoryPageProps>;
active?: boolean;
className?: string;
tabProps?: {
label?: string,
sx?: SxProps,
icon?: string | ReactElement<unknown, string | JSXElementConstructor<any>> | undefined,
iconPosition?: "bottom" | "top" | "start" | "end" | undefined
}
};
label?: string;
sx?: SxProps;
icon?: string | ReactElement<unknown, string | JSXElementConstructor<any>> | undefined;
iconPosition?: 'bottom' | 'top' | 'start' | 'end' | undefined;
};
}
function BackstoryPage(props: BackstoryTabProps) {
const { className, active, children } = props;
return (
<Box
className={ className || "BackstoryTab"}
sx={{ "display": active ? "flex" : "none", p: 0, m: 0, borders: "none" }}
className={className || 'BackstoryTab'}
sx={{ display: active ? 'flex' : 'none', p: 0, m: 0, borders: 'none' }}
>
{children}
</Box>
);
}
export type {
BackstoryPageProps,
BackstoryTabProps,
BackstoryElementProps,
};
export type { BackstoryPageProps, BackstoryTabProps, BackstoryElementProps };
export {
BackstoryPage
}
export { 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 './BackstoryTextField.css';
@ -18,15 +25,9 @@ interface BackstoryTextFieldProps {
style?: CSSProperties;
}
const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryTextFieldProps>((props, ref) => {
const {
value = '',
disabled = false,
placeholder,
onEnter,
onChange,
style,
} = props;
const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryTextFieldProps>(
(props, ref) => {
const { value = '', disabled = false, placeholder, onEnter, onChange, style } = props;
const theme = useTheme();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const shadowRef = useRef<HTMLTextAreaElement>(null);
@ -34,7 +35,7 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
// Sync editValue with prop value if it changes externally
useEffect(() => {
setEditValue(value || "");
setEditValue(value || '');
}, [value]);
// Adjust textarea height based on content
@ -78,7 +79,11 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
useImperativeHandle(ref, () => ({
getValue: () => editValue,
setValue: (value: string) => setEditValue(value),
getAndResetValue: () => { const _ev = editValue; setEditValue(''); return _ev; }
getAndResetValue: () => {
const _ev = editValue;
setEditValue('');
return _ev;
},
}));
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
@ -117,7 +122,10 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
value={editValue}
disabled={disabled}
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}
style={fullStyle}
/>
@ -142,10 +150,9 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
/>
</>
);
});
}
);
export type {
BackstoryTextFieldRef
};
export type { BackstoryTextFieldRef };
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 IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button';
@ -6,25 +13,43 @@ import Box from '@mui/material/Box';
import SendIcon from '@mui/icons-material/Send';
import CancelIcon from '@mui/icons-material/Cancel';
import { SxProps, Theme } from '@mui/material';
import PropagateLoader from "react-spinners/PropagateLoader";
import PropagateLoader from 'react-spinners/PropagateLoader';
import { Message } from './Message';
import { DeleteConfirmation } from 'components/DeleteConfirmation';
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
import { BackstoryElementProps } from './BackstoryTab';
import { useAuth } from "hooks/AuthContext";
import { useAuth } from 'hooks/AuthContext';
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 './Conversation.css';
import { useAppState } from 'hooks/GlobalContext';
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';
@ -34,24 +59,25 @@ interface ConversationHandle {
}
interface ConversationProps extends BackstoryElementProps {
className?: string, // Override default className
type: ConversationMode, // Type of Conversation chat
placeholder?: string, // Prompt to display in TextField input
actionLabel?: string, // Label to put on the primary button
resetAction?: () => void, // Callback when Reset is pressed
resetLabel?: string, // Label to put on Reset button
defaultPrompts?: React.ReactElement[], // Set of Elements to display after the TextField
defaultQuery?: string, // Default text to populate the TextField input
preamble?: 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
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
messages?: ChatMessage[], //
sx?: SxProps<Theme>,
onResponse?: ((message: ChatMessage) => void) | undefined, // Event called when a query completes (provides messages)
};
className?: string; // Override default className
type: ConversationMode; // Type of Conversation chat
placeholder?: string; // Prompt to display in TextField input
actionLabel?: string; // Label to put on the primary button
resetAction?: () => void; // Callback when Reset is pressed
resetLabel?: string; // Label to put on Reset button
defaultPrompts?: React.ReactElement[]; // Set of Elements to display after the TextField
defaultQuery?: string; // Default text to populate the TextField input
preamble?: 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
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
messages?: ChatMessage[]; //
sx?: SxProps<Theme>;
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 {
actionLabel,
defaultPrompts,
@ -67,7 +93,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
sx,
type,
} = props;
const { apiClient } = useAuth()
const { apiClient } = useAuth();
const [processing, setProcessing] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0);
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);
} else {
//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.`);
}
if (filtered.length === 0) {
setFilteredConversation([
...(preamble || []),
...(messages || []),
]);
setFilteredConversation([...(preamble || []), ...(messages || [])]);
} else {
setFilteredConversation([
...(hidePreamble ? [] : (preamble || [])),
...(hidePreamble ? [] : preamble || []),
...(messages || []),
...filtered,
]);
};
}
}, [conversation, setFilteredConversation, messageFilter, preamble, messages, hidePreamble]);
useEffect(() => {
@ -122,17 +146,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
}
const createChatSession = async () => {
try {
const chatContext: ChatContext = { type: "general" };
const chatContext: ChatContext = { type: 'general' };
const response: ChatSession = await apiClient.createChatSession(chatContext);
setChatSession(response);
} catch (e) {
console.error(e);
setSnack("Unable to create chat session.", "error");
setSnack('Unable to create chat session.', 'error');
}
};
createChatSession();
}, [chatSession, setChatSession]);
const getChatMessages = useCallback(async () => {
@ -140,33 +163,38 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
return;
}
try {
const response: PaginatedResponse<ChatMessage> = await apiClient.getChatMessages(chatSession.id);
const response: PaginatedResponse<ChatMessage> = await apiClient.getChatMessages(
chatSession.id
);
const messages: ChatMessage[] = response.data;
setProcessingMessage(undefined);
setStreamingMessage(undefined);
if (messages.length === 0) {
console.log(`History returned with 0 entries`)
setConversation([])
console.log(`History returned with 0 entries`);
setConversation([]);
setNoInteractions(true);
} else {
console.log(`History returned with ${messages.length} entries:`, messages)
console.log(`History returned with ${messages.length} entries:`, messages);
setConversation(messages);
setNoInteractions(false);
}
} catch (error) {
console.error('Unable to obtain chat history', error);
setProcessingMessage({ ...defaultMessage, status: "error", content: `Unable to obtain history from server.` });
setProcessingMessage({
...defaultMessage,
status: 'error',
content: `Unable to obtain history from server.`,
});
setTimeout(() => {
setProcessingMessage(undefined);
setNoInteractions(true);
}, 3000);
setSnack("Unable to obtain chat history.", "error");
setSnack('Unable to obtain chat history.', 'error');
}
}, [chatSession]);
// Set the initial chat history to "loading" or the welcome message if loaded.
useEffect(() => {
if (!chatSession) {
@ -180,13 +208,12 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
setNoInteractions(true);
getChatMessages();
}, [chatSession]);
const handleEnter = (value: string) => {
const query: ChatQuery = {
prompt: value
}
prompt: value,
};
processQuery(query);
};
@ -194,10 +221,11 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
submitQuery: (query: ChatQuery) => {
processQuery(query);
},
fetchHistory: () => { getChatMessages(); }
fetchHistory: () => {
getChatMessages();
},
}));
// const reset = async () => {
// try {
// const response = await fetch(connectionBase + `/api/reset/${sessionId}/${type}`, {
@ -229,7 +257,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
// };
const cancelQuery = () => {
console.log("Stop query");
console.log('Stop query');
if (controllerRef.current) {
controllerRef.current.cancel();
}
@ -249,30 +277,28 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
...defaultMessage,
type: 'text',
content: query.prompt,
}
},
]);
setProcessing(true);
setProcessingMessage(
{ ...defaultMessage, content: 'Submitting request...' }
);
setProcessingMessage({
...defaultMessage,
content: 'Submitting request...',
});
const chatMessage: ChatMessageUser = {
role: "user",
role: 'user',
sessionId: chatSession.id,
content: query.prompt,
tunables: query.tunables,
status: "done",
type: "text",
timestamp: new Date()
status: 'done',
type: 'text',
timestamp: new Date(),
};
controllerRef.current = apiClient.sendMessageStream(chatMessage, {
onMessage: (msg: ChatMessage) => {
console.log("onMessage:", msg);
setConversation([
...conversationRef.current,
msg
]);
console.log('onMessage:', msg);
setConversation([...conversationRef.current, msg]);
setStreamingMessage(undefined);
setProcessingMessage(undefined);
setProcessing(false);
@ -281,32 +307,35 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
}
},
onError: (error: string | ChatMessageError) => {
console.log("onError:", error);
console.log('onError:', error);
// Type-guard to determine if this is a ChatMessageBase or a string
if (typeof error === "object" && error !== null && "content" in error) {
if (typeof error === 'object' && error !== null && 'content' in error) {
setProcessingMessage(error as ChatMessage);
setProcessing(false);
controllerRef.current = null;
} else {
setProcessingMessage({ ...defaultMessage, content: error as string });
setProcessingMessage({
...defaultMessage,
content: error as string,
});
}
},
onStreaming: (chunk: ChatMessageStreaming) => {
console.log("onStreaming:", chunk);
console.log('onStreaming:', chunk);
setStreamingMessage({ ...defaultMessage, ...chunk });
},
onStatus: (status: ChatMessageStatus) => {
console.log("onStatus:", status);
console.log('onStatus:', status);
},
onComplete: () => {
console.log("onComplete");
console.log('onComplete');
controllerRef.current = null;
}
},
});
};
if (!chatSession) {
return (<></>);
return <></>;
}
return (
// <Scrollable
@ -320,28 +349,47 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
// ...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 }}>
{
filteredConversation.map((message, index) =>
<Message key={index} {...{ chatSession, sendQuery: processQuery, message, }} />
)
}
{
processingMessage !== undefined &&
<Message {...{ chatSession, sendQuery: processQuery, message: processingMessage, }} />
}
{
streamingMessage !== undefined &&
<Message {...{ chatSession, sendQuery: processQuery, message: streamingMessage }} />
}
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
{filteredConversation.map((message, index) => (
<Message key={index} {...{ chatSession, sendQuery: processQuery, message }} />
))}
{processingMessage !== undefined && (
<Message
{...{
chatSession,
sendQuery: processQuery,
message: processingMessage,
}}
/>
)}
{streamingMessage !== undefined && (
<Message
{...{
chatSession,
sendQuery: processQuery,
message: streamingMessage,
}}
/>
)}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
m: 1,
}}>
}}
>
<PropagateLoader
size="10px"
loading={processing}
@ -352,16 +400,29 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
<Box
sx={{
pt: 1,
fontSize: "0.7rem",
color: "darkgrey"
fontSize: '0.7rem',
color: 'darkgrey',
}}
>Response will be stopped in: {countdown}s</Box>
>
Response will be stopped in: {countdown}s
</Box>
)}
</Box>
<Box className="Query" sx={{ display: "flex", flexDirection: "column", p: 1, flexGrow: 1 }}>
{placeholder &&
<Box sx={{ display: "flex", flexGrow: 1, p: 0, m: 0, flexDirection: "column" }}
ref={viewableElementRef}>
<Box
className="Query"
sx={{ display: 'flex', flexDirection: 'column', p: 1, flexGrow: 1 }}
>
{placeholder && (
<Box
sx={{
display: 'flex',
flexGrow: 1,
p: 0,
m: 0,
flexDirection: 'column',
}}
ref={viewableElementRef}
>
<BackstoryTextField
ref={backstoryTextRef}
disabled={processing}
@ -369,30 +430,53 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
placeholder={placeholder}
/>
</Box>
}
)}
<Box key="jobActions" sx={{ display: "flex", justifyContent: "center", flexDirection: "row" }}>
<Box
key="jobActions"
sx={{
display: 'flex',
justifyContent: 'center',
flexDirection: 'row',
}}
>
<DeleteConfirmation
label={resetLabel || "all data"}
label={resetLabel || 'all data'}
disabled={!chatSession || processingMessage !== undefined || noInteractions}
onDelete={() => { /*reset(); resetAction && resetAction(); */ }} />
<Tooltip title={actionLabel || "Send"}>
<span style={{ display: "flex", flexGrow: 1 }}>
onDelete={() => {
/*reset(); resetAction && resetAction(); */
}}
/>
<Tooltip title={actionLabel || 'Send'}>
<span style={{ display: 'flex', flexGrow: 1 }}>
<Button
sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained"
disabled={!chatSession || processingMessage !== undefined}
onClick={() => { processQuery({ prompt: (backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || "" }); }}>
{actionLabel}<SendIcon />
onClick={() => {
processQuery({
prompt:
(backstoryTextRef.current &&
backstoryTextRef.current.getAndResetValue()) ||
'',
});
}}
>
{actionLabel}
<SendIcon />
</Button>
</span>
</Tooltip>
<Tooltip title="Cancel">
<span style={{ display: "flex" }}> { /* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<span style={{ display: 'flex' }}>
{' '}
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton
aria-label="cancel"
onClick={() => { cancelQuery(); }}
sx={{ display: "flex", margin: 'auto 0px' }}
onClick={() => {
cancelQuery();
}}
sx={{ display: 'flex', margin: 'auto 0px' }}
size="large"
edge="start"
disabled={stopRef.current || !chatSession || processing === false}
@ -403,26 +487,22 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
</Tooltip>
</Box>
</Box>
{(noInteractions || !hideDefaultPrompts) && defaultPrompts !== undefined && defaultPrompts.length !== 0 &&
<Box sx={{ display: "flex", flexDirection: "column" }}>
{
defaultPrompts.map((element, index) => {
return (<Box key={index}>{element}</Box>);
})
}
{(noInteractions || !hideDefaultPrompts) &&
defaultPrompts !== undefined &&
defaultPrompts.length !== 0 && (
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{defaultPrompts.map((element, index) => {
return <Box key={index}>{element}</Box>;
})}
</Box>
)}
<Box sx={{ display: 'flex', flexGrow: 1 }}></Box>
</Box>
}
<Box sx={{ display: "flex", flexGrow: 1 }}></Box>
</Box >
</Box>
);
});
}
);
export type {
ConversationProps,
ConversationHandle,
};
export type { ConversationProps, ConversationHandle };
export {
Conversation
};
export { Conversation };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,6 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import {
Tabs,
Tab,
Box,
Button,
Paper,
Typography,
LinearProgress,
} from '@mui/material';
import { Job, Candidate, SkillAssessment } from "types/types";
import { Tabs, Tab, Box, Button, Paper, Typography, LinearProgress } from '@mui/material';
import { Job, Candidate, SkillAssessment } from 'types/types';
import { Scrollable } from './Scrollable';
import { useAuth } from 'hooks/AuthContext';
import * as Types from 'types/types';
@ -31,7 +23,12 @@ interface ResumeGeneratorProps {
}
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) => {
@ -49,7 +46,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
setTabValue(newValue);
}
};
useEffect(() => {
if (!job || !candidate || !skills || generated) {
@ -58,8 +55,8 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
setGenerated(true);
setStatusType("thinking");
setStatus("Starting resume generation...");
setStatusType('thinking');
setStatus('Starting resume generation...');
const generateResumeHandlers: StreamingOptions<Types.ChatMessageResume> = {
onMessage: (message: Types.ChatMessageResume) => {
@ -71,7 +68,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
onStreaming: (chunk: Types.ChatMessageStreaming) => {
if (status === '') {
setStatus('Generating resume...');
setStatusType("generating");
setStatusType('generating');
}
setResume(chunk.content);
},
@ -92,7 +89,11 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
};
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;
};
@ -125,18 +126,22 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
<Box
className="ResumeGenerator"
sx={{
display: "flex",
flexDirection: "column",
}}>
{user?.isAdmin && <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 1 }}>
display: 'flex',
flexDirection: 'column',
}}
>
{user?.isAdmin && (
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 1 }}>
<Tabs value={tabValue} onChange={handleTabChange} centered>
<Tab disabled={systemPrompt === ''} value="system" icon={<TuneIcon />} label="System" />
<Tab disabled={prompt === ''} value="prompt" icon={<InputIcon />} label="Prompt" />
<Tab disabled={resume === ''} value="resume" icon={<ArticleIcon />} label="Resume" />
</Tabs>
</Box>}
</Box>
)}
{status && <Box sx={{ mt: 0, mb: 1 }}>
{status && (
<Box sx={{ mt: 0, mb: 1 }}>
<StatusBox>
{statusType && <StatusIcon type={statusType} />}
<Typography variant="body2" sx={{ ml: 1 }}>
@ -144,23 +149,35 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
</Typography>
</StatusBox>
{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 === '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} /></>}
</Scrollable></Paper>
{tabValue === 'resume' && (
<>
<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
</Button>}
</Button>
)}
</Box>
)
};
export {
ResumeGenerator
);
};
export { ResumeGenerator };

View File

@ -14,14 +14,27 @@ interface ScrollableProps {
}
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
const defaultTextFieldRef = useRef<HTMLElement | null>(null);
const scrollRef = useAutoScrollToBottom(textFieldRef ?? defaultTextFieldRef, true, fallbackThreshold, contentUpdateTrigger);
const scrollRef = useAutoScrollToBottom(
textFieldRef ?? defaultTextFieldRef,
true,
fallbackThreshold,
contentUpdateTrigger
);
return (
<Box
className={`Scrollable ${className || ""}`}
className={`Scrollable ${className || ''}`}
sx={{
display: 'flex',
flexDirection: 'column',

View File

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

View File

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

View File

@ -28,7 +28,7 @@ import { useNavigate } from 'react-router-dom';
interface VectorVisualizerProps extends BackstoryPageProps {
inline?: boolean;
rag?: Types.ChromaDBGetResponse;
};
}
interface Metadata {
id: string;
@ -43,10 +43,10 @@ const emptyQuerySet: Types.ChromaDBGetResponse = {
metadatas: [],
embeddings: [],
distances: [],
name: "Empty",
name: 'Empty',
size: 0,
dimensions: 2,
query: ""
query: '',
};
interface PlotData {
@ -105,9 +105,7 @@ const config: Partial<Plotly.Config> = {
// | "hovercompare"
// | "hoverclosest"
// | "v1hovermode";
modeBarButtonsToRemove: [
'lasso2d', 'select2d',
]
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
};
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)
xanchor: 'left',
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[] => {
@ -160,26 +158,26 @@ const colorMap: Record<string, string> = {
projects: '#1A2536', // Midnight Blue — rich and deep
news: '#D3CDBF', // Warm Gray — soft and neutral
'performance-reviews': '#8FD0D0', // Light red
'jobs': '#F3aD8F', // Warm Gray — soft and neutral
jobs: '#F3aD8F', // Warm Gray — soft and neutral
};
const DEFAULT_SIZE = 6.;
const DEFAULT_UNFOCUS_SIZE = 2.;
const DEFAULT_SIZE = 6;
const DEFAULT_UNFOCUS_SIZE = 2;
type Node = {
id: string,
content: string, // Portion of content that was used for embedding
fullContent: string | undefined, // Portion of content plus/minus buffer
emoji: string,
docType: string,
source_file: string,
distance: number | undefined,
path: string,
chunkBegin: number,
lineBegin: number,
chunkEnd: number,
lineEnd: number,
sx: SxProps,
id: string;
content: string; // Portion of content that was used for embedding
fullContent: string | undefined; // Portion of content plus/minus buffer
emoji: string;
docType: string;
source_file: string;
distance: number | undefined;
path: string;
chunkBegin: number;
lineBegin: number;
chunkEnd: number;
lineEnd: number;
sx: SxProps;
};
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 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
* 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();
svgContainer.style.width = `${plotContainerRect.width}px`;
svgContainer.style.height = `${plotContainerRect.height}px`;
if (plotDimensions.width !== plotContainerRect.width || plotDimensions.height !== plotContainerRect.height) {
setPlotDimensions({ width: plotContainerRect.width, height: plotContainerRect.height });
if (
plotDimensions.width !== plotContainerRect.width ||
plotDimensions.height !== plotContainerRect.height
) {
setPlotDimensions({
width: plotContainerRect.width,
height: plotContainerRect.height,
});
}
}
});
}
};
resize();
});
@ -238,12 +243,12 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
setResult(result);
} catch (error) {
console.error('Error obtaining collection information:', error);
setSnack("Unable to obtain collection information.", "error");
};
setSnack('Unable to obtain collection information.', 'error');
}
};
fetchCollection();
}, [result, setSnack, view2D])
}, [result, setSnack, view2D]);
useEffect(() => {
if (!result || !result.embeddings) return;
@ -251,13 +256,13 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
const full: Types.ChromaDBGetResponse = {
...result,
ids: [...result.ids || []],
documents: [...result.documents || []],
ids: [...(result.ids || [])],
documents: [...(result.documents || [])],
embeddings: [...result.embeddings],
metadatas: [...result.metadatas || []],
metadatas: [...(result.metadatas || [])],
};
let is2D = full.embeddings.every((v: number[]) => v.length === 2);
let is3D = full.embeddings.every((v: number[]) => v.length === 3);
const is2D = full.embeddings.every((v: number[]) => v.length === 2);
const is3D = full.embeddings.every((v: number[]) => v.length === 3);
if ((view2D && !is2D) || (!view2D && !is3D)) {
return;
}
@ -267,7 +272,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
return;
}
let query: Types.ChromaDBGetResponse = {
const query: Types.ChromaDBGetResponse = {
ids: [],
documents: [],
embeddings: [],
@ -276,9 +281,9 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
query: '',
size: 0,
dimensions: 2,
name: ''
name: '',
};
let filtered: Types.ChromaDBGetResponse = {
const filtered: Types.ChromaDBGetResponse = {
ids: [],
documents: [],
embeddings: [],
@ -287,7 +292,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
query: '',
size: 0,
dimensions: 2,
name: ''
name: '',
};
/* 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];
if (foundIndex !== -1) {
/* 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.documents.push(full.documents[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) {
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);
}
if (!view2D && querySet.umapEmbedding3D && querySet.umapEmbedding3D.length) {
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);
}
const filtered_docTypes = filtered.metadatas.map(m => m.docType || 'unknown')
const query_docTypes = query.metadatas.map(m => m.docType || 'unknown')
const filtered_docTypes = filtered.metadatas.map(m => m.docType || 'unknown');
const query_docTypes = query.metadatas.map(m => m.docType || 'unknown');
const has_query = query.metadatas.length > 0;
const filtered_sizes = filtered.metadatas.map(m => 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_x = normalizeDimension(filtered.embeddings.map((v: number[]) => v[0]));
const filtered_y = normalizeDimension(filtered.embeddings.map((v: number[]) => v[1]));
const filtered_z = is3D ? normalizeDimension(filtered.embeddings.map((v: number[]) => v[2])) : undefined;
const 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_x = normalizeDimension(query.embeddings.map((v: number[]) => v[0]));
const query_y = normalizeDimension(query.embeddings.map((v: number[]) => v[1]));
const query_z = is3D ? normalizeDimension(query.embeddings.map((v: number[]) => v[2])) : undefined;
const query_z = is3D
? normalizeDimension(query.embeddings.map((v: number[]) => v[2]))
: undefined;
const data: any = [{
const data: any = [
{
name: 'All data',
x: filtered_x,
y: filtered_y,
@ -353,13 +379,14 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
size: filtered_sizes,
symbol: 'circle',
color: filtered_colors,
opacity: 1
opacity: 1,
},
text: filtered.ids,
customdata: filtered.metadatas,
type: is3D ? 'scatter3d' : 'scatter',
hovertemplate: '&nbsp;',
}, {
},
{
name: 'Query',
x: query_x,
y: query_y,
@ -368,13 +395,14 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
size: query_sizes,
symbol: 'circle',
color: query_colors,
opacity: 1
opacity: 1,
},
text: query.ids,
customdata: query.metadatas,
type: is3D ? 'scatter3d' : 'scatter',
hovertemplate: '%{text}',
}];
},
];
if (is3D) {
data[0].z = filtered_z;
@ -382,7 +410,6 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
}
setPlotData(data);
}, [result, querySet, view2D]);
const handleKeyPress = (event: any) => {
@ -400,20 +427,39 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
console.log(result);
setQuerySet(result);
} catch (error) {
const msg = `Error obtaining similar content to ${query}.`
setSnack(msg, "error");
};
const msg = `Error obtaining similar content to ${query}.`;
setSnack(msg, 'error');
}
};
if (!result) return (
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}>
if (!result)
return (
<Box
sx={{
display: 'flex',
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
}}
>
<div>Loading visualization...</div>
</Box>
);
if (!candidate) return (
<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>
if (!candidate)
return (
<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>
);
@ -422,14 +468,14 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
const result = await apiClient.getCandidateRAGContent(node.id);
const update: Node = {
...node,
fullContent: result.content
}
fullContent: result.content,
};
setNode(update);
} catch (error) {
const msg = `Error obtaining content for ${node.id}.`
const msg = `Error obtaining content for ${node.id}.`;
console.error(msg, error);
setSnack(msg, "error");
};
setSnack(msg, 'error');
}
};
const onNodeSelected = (metadata: any) => {
@ -440,21 +486,23 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
...metadata,
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],
sx: {
m: 0.5,
p: 2,
width: '3rem',
display: "flex",
alignContent: "center",
justifyContent: "center",
display: 'flex',
alignContent: 'center',
justifyContent: 'center',
flexGrow: 0,
flexWrap: "wrap",
flexWrap: 'wrap',
backgroundColor: colorMap[metadata.docType] || '#ff8080',
}
}
},
};
setNode(node);
return;
}
@ -463,7 +511,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
content: `Loading...`,
...metadata,
emoji: emojiMap[metadata.docType] || '❓',
}
};
setNode(node);
@ -471,95 +519,173 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
};
return (
<Box className="VectorVisualizer"
<Box
className="VectorVisualizer"
ref={boxRef}
sx={{
...sx
}}>
...sx,
}}
>
<Box sx={{ p: 0, m: 0, gap: 0 }}>
<Paper sx={{
p: 0.5, m: 0,
display: "flex",
<Paper
sx={{
p: 0.5,
m: 0,
display: 'flex',
flexGrow: 0,
height: isMobile ? "auto" : "auto", //"320px",
minHeight: isMobile ? "auto" : "auto", //"320px",
maxHeight: isMobile ? "auto" : "auto", //"320px",
position: "relative",
flexDirection: "column"
}}>
height: isMobile ? 'auto' : 'auto', //"320px",
minHeight: isMobile ? 'auto' : 'auto', //"320px",
maxHeight: isMobile ? 'auto' : 'auto', //"320px",
position: 'relative',
flexDirection: 'column',
}}
>
<FormControlLabel
sx={{
display: "flex",
position: "relative",
width: "fit-content",
display: 'flex',
position: 'relative',
width: 'fit-content',
ml: 1,
mb: '-2.5rem',
zIndex: 100,
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
ref={plotlyRef}
onClick={(event: any) => { onNodeSelected(event.points[0].customdata); }}
onClick={(event: any) => {
onNodeSelected(event.points[0].customdata);
}}
data={plotData}
useResizeHandler={true}
config={config}
style={{
display: "flex",
display: 'flex',
flexGrow: 1,
minHeight: '240px',
padding: 0,
margin: 0,
width: "100%",
height: "100%",
overflow: "hidden",
width: '100%',
height: '100%',
overflow: 'hidden',
}}
layout={{
...layout,
width: plotDimensions.width,
height: plotDimensions.height,
}}
layout={{...layout, width: plotDimensions.width, height: plotDimensions.height }}
/>
</Paper>
<Paper sx={{ 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" }}>
<Paper
sx={{
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 }}>
<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>
<TableCell>Type</TableCell>
<TableCell>{node.emoji} {node.docType}</TableCell>
<TableCell>
{node.emoji} {node.docType}
</TableCell>
</TableRow>
{node.source_file !== undefined && <TableRow>
{node.source_file !== undefined && (
<TableRow>
<TableCell>File</TableCell>
<TableCell>{node.source_file.replace(/^.*\//, '')}</TableCell>
</TableRow>}
{node.path !== undefined && <TableRow>
</TableRow>
)}
{node.path !== undefined && (
<TableRow>
<TableCell>Section</TableCell>
<TableCell>{node.path}</TableCell>
</TableRow>}
{node.distance !== undefined && <TableRow>
</TableRow>
)}
{node.distance !== undefined && (
<TableRow>
<TableCell>Distance</TableCell>
<TableCell>{node.distance}</TableCell>
</TableRow>}
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{node.content !== "" && node.content !== undefined &&
<Paper elevation={6} sx={{ 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 sx={{ display: "flex", p: 1, flexGrow: 1 }}>{node.content}</Box>
</Paper>
}
{node.content !== '' && node.content !== undefined && (
<Paper
elevation={6}
sx={{
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 sx={{ display: 'flex', p: 1, flexGrow: 1 }}>{node.content}</Box>
</Paper>
)}
</Box>
)}
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 2, flexBasis: 0, flexShrink: 1 }}>
{node === null &&
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 2,
flexBasis: 0,
flexShrink: 1,
}}
>
{node === null && (
<Paper sx={{ m: 0.5, p: 2, flexGrow: 1 }}>
Click a point in the scatter-graph to see information about that node.
</Paper>
}
{node !== null && node.fullContent &&
)}
{node !== null && node.fullContent && (
<Scrollable
autoscroll={false}
sx={{
@ -569,51 +695,104 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
p: 0.5,
pl: 1,
flexShrink: 1,
position: "relative",
maxWidth: "100%",
position: 'relative',
maxWidth: '100%',
}}
>
{
node.fullContent.split('\n').map((line, index) => {
{node.fullContent.split('\n').map((line, index) => {
index += 1 + node.chunkBegin;
const bgColor = (index > node.lineBegin && index <= node.lineEnd) ? '#f0f0f0' : 'auto';
return <Box key={index} sx={{ display: "flex", flexDirection: "row", borderBottom: '1px solid #d0d0d0', ':first-of-type': { borderTop: '1px solid #d0d0d0' }, backgroundColor: bgColor }}>
<Box sx={{ fontFamily: 'courier', fontSize: "0.8rem", minWidth: "2rem", pt: "0.1rem", align: "left", verticalAlign: "top" }}>{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>}
const bgColor =
index > node.lineBegin && index <= node.lineEnd ? '#f0f0f0' : 'auto';
return (
<Box
key={index}
sx={{
display: 'flex',
flexDirection: 'row',
borderBottom: '1px solid #d0d0d0',
':first-of-type': { borderTop: '1px solid #d0d0d0' },
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>
}
)}
</Box>
</Paper>
{!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 }}>
{!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,
}}
>
{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>
}
)}
{
!inline &&
<Box className="Query" sx={{ display: "flex", flexDirection: "row", p: 1 }}>
{!inline && (
<Box className="Query" sx={{ display: 'flex', flexDirection: 'row', p: 1 }}>
<TextField
variant="outlined"
fullWidth
type="text"
value={newQuery}
onChange={(e) => setNewQuery(e.target.value)}
onChange={e => setNewQuery(e.target.value)}
onKeyDown={handleKeyPress}
placeholder="Enter query to find related documents..."
id="QueryInput"
/>
<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>
</Box>
}
)}
</Box>
</Box>
);
@ -621,6 +800,4 @@ The scatter graph shows the query in N-dimensional space, mapped to ${view2D ? '
export type { VectorVisualizerProps };
export {
VectorVisualizer,
};
export { VectorVisualizer };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,18 @@
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 Box from '@mui/material/Box';
import { BackstoryElementProps } from 'components/BackstoryTab';
import { CandidateInfo } from 'components/ui/CandidateInfo';
import { Candidate, CandidateAI } from "types/types";
import { Candidate, CandidateAI } from 'types/types';
import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
import { Paper } from '@mui/material';
interface CandidatePickerProps extends BackstoryElementProps {
onSelect?: (candidate: Candidate) => void;
};
}
const CandidatePicker = (props: CandidatePickerProps) => {
const { onSelect, sx } = props;
@ -47,7 +47,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
});
setCandidates(candidates);
} catch (err) {
setSnack("" + err);
setSnack('' + err);
}
};
@ -55,33 +55,43 @@ const CandidatePicker = (props: CandidatePickerProps) => {
}, [candidates, setSnack]);
return (
<Box sx={{ display: "flex", flexDirection: "column", ...sx }}>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", justifyContent: "center" }}>
{candidates?.map((u, i) =>
<Paper key={`${u.username}`}
onClick={() => { onSelect ? onSelect(u) : setSelectedCandidate(u); }}
sx={{ cursor: "pointer" }}>
<CandidateInfo variant="small"
<Box sx={{ display: 'flex', flexDirection: 'column', ...sx }}>
<Box
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"
}
display: 'flex',
gap: 1,
flexWrap: 'wrap',
justifyContent: 'center',
}}
>
{candidates?.map((u, i) => (
<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}
/>
</Paper>
)}
))}
</Box>
</Box>
);
};
export {
CandidatePicker
};
export { CandidatePicker };

View File

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

View File

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

View File

@ -1,18 +1,18 @@
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 Box from '@mui/material/Box';
import { BackstoryElementProps } from 'components/BackstoryTab';
import { JobInfo } from 'components/ui/JobInfo';
import { Job } from "types/types";
import { Job } from 'types/types';
import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
import { Paper } from '@mui/material';
interface JobPickerProps extends BackstoryElementProps {
onSelect?: (job: Job) => void
};
onSelect?: (job: Job) => void;
}
const JobPicker = (props: JobPickerProps) => {
const { onSelect } = props;
@ -38,7 +38,7 @@ const JobPicker = (props: JobPickerProps) => {
});
setJobs(jobs);
} catch (err) {
setSnack("" + err);
setSnack('' + err);
}
};
@ -46,33 +46,44 @@ const JobPicker = (props: JobPickerProps) => {
}, [jobs, setSnack]);
return (
<Box sx={{display: "flex", flexDirection: "column", mb: 1}}>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", justifyContent: "center" }}>
{jobs?.map((j, i) =>
<Paper key={`${j.id}`}
onClick={() => { console.log('Selected job', j); onSelect && onSelect(j) }}
sx={{ cursor: "pointer" }}>
<JobInfo variant="small"
<Box sx={{ display: 'flex', flexDirection: 'column', mb: 1 }}>
<Box
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"
}
display: 'flex',
gap: 1,
flexWrap: 'wrap',
justifyContent: 'center',
}}
>
{jobs?.map((j, i) => (
<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}
/>
</Paper>
)}
))}
</Box>
</Box>
);
};
export {
JobPicker
};
export { JobPicker };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,8 +17,9 @@ const BackstoryAppAnalysisPage = () => {
Core Concept
</Typography>
<Typography variant="body1">
Backstory is a dual-purpose platform designed to bridge the gap between job candidates and
employers/recruiters with an AI-powered approach to professional profiles and resume generation.
Backstory is a dual-purpose platform designed to bridge the gap between job candidates
and employers/recruiters with an AI-powered approach to professional profiles and resume
generation.
</Typography>
<Typography variant="h3" component="h3" sx={{ mt: 3 }}>
@ -27,14 +28,15 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Job Candidates</strong> - Upload and manage comprehensive professional histories
and generate tailored resumes for specific positions
<strong>Job Candidates</strong> - Upload and manage comprehensive professional
histories and generate tailored resumes for specific positions
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Employers/Recruiters</strong> - Search for candidates, directly interact with AI
assistants about candidate experiences, and generate position-specific resumes
<strong>Employers/Recruiters</strong> - Search for candidates, directly interact
with AI assistants about candidate experiences, and generate position-specific
resumes
</Typography>
</li>
</Box>
@ -49,27 +51,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Complete Profile Management</strong> - Create detailed professional histories beyond typical resume constraints
<strong>Complete Profile Management</strong> - Create detailed professional
histories beyond typical resume constraints
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>AI-Assisted Q&A Setup</strong> - Configure an AI assistant to answer employer questions about your experience
<strong>AI-Assisted Q&A Setup</strong> - Configure an AI assistant to answer
employer questions about your experience
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Smart Resume Generator</strong> - Create tailored resumes for specific positions using AI
<strong>Smart Resume Generator</strong> - Create tailored resumes for specific
positions using AI
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Analytics Dashboard</strong> - Track profile views, resume downloads, and employer engagement
<strong>Analytics Dashboard</strong> - Track profile views, resume downloads, and
employer engagement
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Privacy Controls</strong> - Manage visibility and access to your professional information
<strong>Privacy Controls</strong> - Manage visibility and access to your
professional information
</Typography>
</li>
</Box>
@ -80,27 +87,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Advanced Candidate Search</strong> - Find candidates with specific skills, experience levels, and qualifications
<strong>Advanced Candidate Search</strong> - Find candidates with specific skills,
experience levels, and qualifications
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Interactive Q&A</strong> - Ask questions directly to candidate AI assistants to learn more about their experience
<strong>Interactive Q&A</strong> - Ask questions directly to candidate AI assistants
to learn more about their experience
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Resume Generation</strong> - Generate candidate resumes tailored to specific job requirements
<strong>Resume Generation</strong> - Generate candidate resumes tailored to specific
job requirements
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Talent Pool Management</strong> - Create and manage groups of candidates for different positions
<strong>Talent Pool Management</strong> - Create and manage groups of candidates for
different positions
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Job Posting Management</strong> - Create, manage, and track applications for job postings
<strong>Job Posting Management</strong> - Create, manage, and track applications for
job postings
</Typography>
</li>
</Box>
@ -118,17 +130,20 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Public Navigation</strong> - Home, Docs, Pricing, Login/Register accessible to all users
<strong>Public Navigation</strong> - Home, Docs, Pricing, Login/Register accessible
to all users
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Candidate Dashboard Navigation</strong> - Profile, Backstory, Resumes, Q&A Setup, Analytics, Settings
<strong>Candidate Dashboard Navigation</strong> - Profile, Backstory, Resumes, Q&A
Setup, Analytics, Settings
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Employer Dashboard Navigation</strong> - Dashboard, Search, Saved, Jobs, Company, Analytics, Settings
<strong>Employer Dashboard Navigation</strong> - Dashboard, Search, Saved, Jobs,
Company, Analytics, Settings
</Typography>
</li>
</Box>
@ -139,32 +154,38 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Dashboard Cards</strong> - Both user types have dashboards with card-based information displays
<strong>Dashboard Cards</strong> - Both user types have dashboards with card-based
information displays
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Tab-Based Content Organization</strong> - Many screens use horizontal tabs to organize related content
<strong>Tab-Based Content Organization</strong> - Many screens use horizontal tabs
to organize related content
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Form-Based Editors</strong> - Profile and content editors use structured forms with varied input types
<strong>Form-Based Editors</strong> - Profile and content editors use structured
forms with varied input types
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Three-Column Layouts</strong> - Many screens follow a left sidebar, main content, right sidebar pattern
<strong>Three-Column Layouts</strong> - Many screens follow a left sidebar, main
content, right sidebar pattern
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Preview/Edit Toggle</strong> - Resume and profile editing screens offer both editing and preview modes
<strong>Preview/Edit Toggle</strong> - Resume and profile editing screens offer both
editing and preview modes
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Filter-Based Search</strong> - Employer search uses multiple filter categories to refine candidate results
<strong>Filter-Based Search</strong> - Employer search uses multiple filter
categories to refine candidate results
</Typography>
</li>
</Box>
@ -173,8 +194,8 @@ const BackstoryAppAnalysisPage = () => {
Mobile Adaptations
</Typography>
<Typography variant="body1">
The mobile designs show a simplified navigation structure with bottom tabs and a hamburger menu,
maintaining the core functionality while adapting to smaller screens.
The mobile designs show a simplified navigation structure with bottom tabs and a
hamburger menu, maintaining the core functionality while adapting to smaller screens.
</Typography>
<Typography variant="h2" component="h2" sx={{ mt: 4 }}>
@ -187,22 +208,26 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ul" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>LLM Integration</strong> - Supports multiple AI models (Claude, GPT-4, self-hosted models)
<strong>LLM Integration</strong> - Supports multiple AI models (Claude, GPT-4,
self-hosted models)
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Candidate AI Assistant</strong> - Personalized AI chatbot that answers questions about candidate experience
<strong>Candidate AI Assistant</strong> - Personalized AI chatbot that answers
questions about candidate experience
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Resume Generation</strong> - AI-powered resume creation based on job requirements
<strong>Resume Generation</strong> - AI-powered resume creation based on job
requirements
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Skills Matching</strong> - Automated matching between candidate skills and job requirements
<strong>Skills Matching</strong> - Automated matching between candidate skills and
job requirements
</Typography>
</li>
</Box>
@ -218,17 +243,20 @@ const BackstoryAppAnalysisPage = () => {
</li>
<li>
<Typography variant="body1" component="div">
<strong>Data Import</strong> - LinkedIn profile import, resume parsing (PDF, DOCX), CSV/JSON import
<strong>Data Import</strong> - LinkedIn profile import, resume parsing (PDF, DOCX),
CSV/JSON import
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>ATS Compatibility</strong> - Integration with employer Applicant Tracking Systems
<strong>ATS Compatibility</strong> - Integration with employer Applicant Tracking
Systems
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Vector Databases</strong> - Semantic search capabilities for candidate matching
<strong>Vector Databases</strong> - Semantic search capabilities for candidate
matching
</Typography>
</li>
</Box>
@ -239,27 +267,32 @@ const BackstoryAppAnalysisPage = () => {
<Box component="ol" sx={{ pl: 4 }}>
<li>
<Typography variant="body1" component="div">
<strong>Beyond the Resume</strong> - Focuses on comprehensive professional stories rather than just resume highlights
<strong>Beyond the Resume</strong> - Focuses on comprehensive professional stories
rather than just resume highlights
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>AI-Mediated Communication</strong> - Uses AI to facilitate deeper understanding of candidate experiences
<strong>AI-Mediated Communication</strong> - Uses AI to facilitate deeper
understanding of candidate experiences
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Bidirectional Resume Generation</strong> - Both candidates and employers can generate tailored resumes
<strong>Bidirectional Resume Generation</strong> - Both candidates and employers can
generate tailored resumes
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Configurable AI Personalities</strong> - Candidates can customize how their AI assistant responds to questions
<strong>Configurable AI Personalities</strong> - Candidates can customize how their
AI assistant responds to questions
</Typography>
</li>
<li>
<Typography variant="body1" component="div">
<strong>Deep Analytics</strong> - Both candidates and employers receive insights about their engagement
<strong>Deep Analytics</strong> - Both candidates and employers receive insights
about their engagement
</Typography>
</li>
</Box>
@ -293,7 +326,9 @@ const BackstoryAppAnalysisPage = () => {
<Typography variant="body1">Role-based access for employer teams</Typography>
</li>
<li>
<Typography variant="body1">Data management options for compliance requirements</Typography>
<Typography variant="body1">
Data management options for compliance requirements
</Typography>
</li>
</Box>
</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="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}
</div>
<span className="text-xs">{color}</span>
@ -19,9 +20,11 @@ const BackstoryThemeVisualizerPage = () => {
<Box sx={{ backgroundColor: 'background.default', minHeight: '100%', py: 4 }}>
<Container maxWidth="lg">
<Paper sx={{ p: 4, boxShadow: 2 }}>
<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
</h1>
@ -30,8 +33,16 @@ const BackstoryThemeVisualizerPage = () => {
Primary Colors
</h2>
<div className="flex space-x-4">
{colorSwatch(backstoryTheme.palette.primary.main, 'Primary', backstoryTheme.palette.primary.contrastText)}
{colorSwatch(backstoryTheme.palette.secondary.main, 'Secondary', backstoryTheme.palette.secondary.contrastText)}
{colorSwatch(
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')}
</div>
</div>
@ -56,30 +67,40 @@ const BackstoryThemeVisualizerPage = () => {
</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 }}>
Typography Examples
</h2>
<div className="mb-4">
<h1 style={{
<h1
style={{
fontFamily: backstoryTheme.typography.fontFamily,
fontSize: backstoryTheme.typography.h1.fontSize,
fontWeight: backstoryTheme.typography.h1.fontWeight,
color: backstoryTheme.typography.h1.color,
}}>
}}
>
Heading 1 - Backstory Application
</h1>
</div>
<div className="mb-4">
<p style={{
<p
style={{
fontFamily: backstoryTheme.typography.fontFamily,
fontSize: backstoryTheme.typography.body1.fontSize,
color: backstoryTheme.typography.body1.color,
}}>
Body Text - This is how the regular text content will appear in the Backstory application.
The application uses Roboto as its primary font family, with carefully selected sizing and colors.
}}
>
Body Text - This is how the regular text content will appear in the Backstory
application. The application uses Roboto as its primary font family, with
carefully selected sizing and colors.
</p>
</div>
@ -98,14 +119,29 @@ const BackstoryThemeVisualizerPage = () => {
UI Component Examples
</h2>
<div className="p-4 mb-4 rounded-lg" 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 }}>
<div
className="p-4 mb-4 rounded-lg"
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
</span>
</div>
<div style={{
<div
style={{
padding: '8px 16px',
backgroundColor: backstoryTheme.palette.primary.main,
color: backstoryTheme.palette.primary.contrastText,
@ -113,11 +149,14 @@ const BackstoryThemeVisualizerPage = () => {
borderRadius: '4px',
cursor: 'pointer',
fontFamily: backstoryTheme.typography.fontFamily,
}}>
}}
>
Primary Button
</div>
<div className="mt-4" style={{
<div
className="mt-4"
style={{
padding: '8px 16px',
backgroundColor: backstoryTheme.palette.secondary.main,
color: backstoryTheme.palette.secondary.contrastText,
@ -125,11 +164,14 @@ const BackstoryThemeVisualizerPage = () => {
borderRadius: '4px',
cursor: 'pointer',
fontFamily: backstoryTheme.typography.fontFamily,
}}>
}}
>
Secondary Button
</div>
<div className="mt-4" style={{
<div
className="mt-4"
style={{
padding: '8px 16px',
backgroundColor: backstoryTheme.palette.action.active,
color: '#fff',
@ -137,7 +179,8 @@ const BackstoryThemeVisualizerPage = () => {
borderRadius: '4px',
cursor: 'pointer',
fontFamily: backstoryTheme.typography.fontFamily,
}}>
}}
>
Action Button
</div>
</div>
@ -150,53 +193,164 @@ const BackstoryThemeVisualizerPage = () => {
<table className="border-collapse">
<thead>
<tr>
<th className="border p-2 text-left"
style={{ backgroundColor: backstoryTheme.palette.background.default, color: backstoryTheme.palette.text.primary }}>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>
<th
className="border p-2 text-left"
style={{
backgroundColor: backstoryTheme.palette.background.default,
color: backstoryTheme.palette.text.primary,
}}
>
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>
</thead>
<tbody>
<tr>
<td className="border p-2" 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>
<td
className="border p-2"
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>
<td className="border p-2" 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>
<td
className="border p-2"
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>
<td className="border p-2" 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>
<td
className="border p-2"
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>
<td className="border p-2" 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>
<td
className="border p-2"
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>
<td className="border p-2" 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>
<td
className="border p-2"
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>
<td className="border p-2" 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>
<td
className="border p-2"
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>
</tbody>
</table>
</div>
</div>
</Paper></Container></Box>
</Paper>
</Container>
</Box>
);
};
export {
BackstoryThemeVisualizerPage
};
export { BackstoryThemeVisualizerPage };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import {
CardContent,
CardActionArea,
useTheme,
useMediaQuery
useMediaQuery,
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import PersonIcon from '@mui/icons-material/Person';
@ -61,48 +61,50 @@ const Sidebar: React.FC<{
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{
<Box
sx={{
p: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: 1,
borderColor: 'divider',
}}>
}}
>
<Typography variant="h6" component="h2" fontWeight="bold">
Documentation
</Typography>
{isMobile && onClose && (
<IconButton
onClick={onClose}
size="small"
aria-label="Close navigation"
>
<IconButton onClick={onClose} size="small" aria-label="Close navigation">
<CloseIcon />
</IconButton>
)}
</Box>
<Box sx={{
<Box
sx={{
flexGrow: 1,
overflow: 'auto',
p: 1
}}>
p: 1,
}}
>
<List>
{documents.map((doc, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => doc.route ? handleItemClick(doc.route) : navigate('/')}
onClick={() => (doc.route ? handleItemClick(doc.route) : navigate('/'))}
selected={currentPage === doc.route}
sx={{
borderRadius: 1,
mb: 0.5
mb: 0.5,
}}
>
<ListItemIcon sx={{
<ListItemIcon
sx={{
color: currentPage === doc.route ? 'primary.main' : 'text.secondary',
minWidth: 40
}}>
minWidth: 40,
}}
>
{getDocumentIcon(doc.title)}
</ListItemIcon>
<ListItemText
@ -110,7 +112,7 @@ const Sidebar: React.FC<{
slotProps={{
primary: {
fontWeight: currentPage === doc.route ? 'medium' : 'regular',
}
},
}}
/>
</ListItemButton>
@ -128,7 +130,7 @@ const getDocumentIcon = (title: string): React.ReactNode => {
throw Error(`${title} does not exist in documents`);
}
return item.icon || <ViewQuiltIcon />;
}
};
type DocType = {
title: string;
@ -137,26 +139,90 @@ type DocType = {
icon?: React.ReactNode;
};
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: "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 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: '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);
if (index === -1) {
return null
return null;
}
return documents[index];
};
@ -165,10 +231,10 @@ const documentFromRoute = (route: string) : DocType | null => {
const documentTitleFromRoute = (route: string): string => {
const doc = documentFromRoute(route);
if (doc === null) {
return 'Documentation'
return 'Documentation';
}
return doc.title;
}
};
const DocsPage = (props: BackstoryPageProps) => {
const { setSnack } = useAppState();
@ -200,10 +266,10 @@ const DocsPage = (props: BackstoryPageProps) => {
// Handle document navigation
const onDocumentExpand = (docName: string, open: boolean) => {
console.log("Document expanded:", { docName, open, location });
console.log('Document expanded:', { docName, open, location });
if (open) {
const parts = location.pathname.split('/');
if (docName === "backstory") {
if (docName === 'backstory') {
navigate('/');
return;
}
@ -230,8 +296,8 @@ const DocsPage = (props: BackstoryPageProps) => {
};
interface DocViewProps {
page: string
};
page: string;
}
const DocView = (props: DocViewProps) => {
const { page = 'about' } = props;
const title = documentTitleFromRoute(page);
@ -240,13 +306,22 @@ const DocsPage = (props: BackstoryPageProps) => {
return (
<Card>
<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}
{title}
</Box>
{page && <Document
filepath={`/docs/${page}.md`}
/>}
{page && <Document filepath={`/docs/${page}.md`} />}
</CardContent>
</Card>
);
@ -256,18 +331,22 @@ const DocsPage = (props: BackstoryPageProps) => {
function renderContent() {
switch (page) {
case 'ui-overview':
return (<BackstoryUIOverviewPage />);
return <BackstoryUIOverviewPage />;
case 'theme-visualizer':
return (<Paper sx={{ m: 0, p: 1 }}><BackstoryThemeVisualizerPage /></Paper>);
return (
<Paper sx={{ m: 0, p: 1 }}>
<BackstoryThemeVisualizerPage />
</Paper>
);
case 'app-analysis':
return (<BackstoryAppAnalysisPage />);
return <BackstoryAppAnalysisPage />;
case 'ui-mockup':
return (<MockupPage />);
return <MockupPage />;
case 'user-management':
return (<UserManagement />);
return <UserManagement />;
default:
if (documentFromRoute(page)) {
return <DocView page={page}/>
return <DocView page={page} />;
}
// Document grid for landing page
return (
@ -277,19 +356,41 @@ const DocsPage = (props: BackstoryPageProps) => {
Documentation
</Typography>
<Typography variant="body1" color="text.secondary">
Select a document from the sidebar to view detailed technical information about the application.
Select a document from the sidebar to view detailed technical information about the
application.
</Typography>
</Box>
<Grid container spacing={1}>
{documents.map((doc, index) => {
if (doc.route === null) return (<></>);
return (<Grid sx={{ minWidth: "164px" }} size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<Card sx={{ minHeight: "180px" }}>
<CardActionArea 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' }}>
if (doc.route === null) return <></>;
return (
<Grid sx={{ minWidth: '164px' }} size={{ xs: 12, sm: 6, md: 4 }} key={index}>
<Card sx={{ minHeight: '180px' }}>
<CardActionArea
onClick={() =>
doc.route ? onDocumentExpand(doc.route, true) : navigate('/')
}
>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
m: 0,
p: 1,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
gap: 1,
verticalAlign: 'top',
}}
>
{getDocumentIcon(doc.title)}
<Typography variant="h3" sx={{ m: "0 !important" }}>{doc.title}</Typography>
<Typography variant="h3" sx={{ m: '0 !important' }}>
{doc.title}
</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
{doc.description}
@ -298,7 +399,7 @@ const DocsPage = (props: BackstoryPageProps) => {
</CardActionArea>
</Card>
</Grid>
)
);
})}
</Grid>
</Paper>
@ -318,22 +419,17 @@ const DocsPage = (props: BackstoryPageProps) => {
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
display: { md: 'none' }
display: { md: 'none' },
}}
elevation={0}
color="default"
>
<Toolbar>
<IconButton
aria-label="open drawer"
edge="start"
onClick={toggleDrawer}
sx={{ mr: 2 }}
>
<IconButton aria-label="open drawer" edge="start" onClick={toggleDrawer} sx={{ mr: 2 }}>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div" sx={{ color: "white" }}>
{page ? documentTitleFromRoute(page) : "Documentation"}
<Typography variant="h6" noWrap component="div" sx={{ color: 'white' }}>
{page ? documentTitleFromRoute(page) : 'Documentation'}
</Typography>
</Toolbar>
</AppBar>
@ -344,7 +440,7 @@ const DocsPage = (props: BackstoryPageProps) => {
component="nav"
sx={{
width: { md: drawerWidth },
flexShrink: { md: 0 }
flexShrink: { md: 0 },
}}
>
{/* Mobile drawer (temporary) */}
@ -360,7 +456,7 @@ const DocsPage = (props: BackstoryPageProps) => {
display: { xs: 'block', md: 'none' },
'& .MuiDrawer-paper': {
boxSizing: 'border-box',
width: drawerWidth
width: drawerWidth,
},
}}
>
@ -381,16 +477,12 @@ const DocsPage = (props: BackstoryPageProps) => {
boxSizing: 'border-box',
width: drawerWidth,
position: 'relative',
height: '100%'
height: '100%',
},
}}
open
>
<Sidebar
currentPage={page}
onDocumentSelect={onDocumentExpand}
isMobile={false}
/>
<Sidebar currentPage={page} onDocumentSelect={onDocumentExpand} isMobile={false} />
</Drawer>
)}
</Box>
@ -404,7 +496,7 @@ const DocsPage = (props: BackstoryPageProps) => {
width: { md: `calc(100% - ${drawerWidth}px)` },
pt: isMobile ? { xs: 8, sm: 9 } : 3, // Add padding top on mobile to account for AppBar
height: '100%',
overflow: 'auto'
overflow: 'auto',
}}
>
{renderContent()}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,14 +11,21 @@ const LoadingPage = (props: BackstoryPageProps) => {
sessionId: '',
content: 'Please wait while connecting to Backstory...',
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} />
</Box>
);
};
export {
LoadingPage
};
export { LoadingPage };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,23 +1,22 @@
import React, { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Box } from "@mui/material";
import React, { useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Box } from '@mui/material';
import { SetSnackType } from '../components/Snack';
import { LoadingComponent } from "../components/LoadingComponent";
import { LoadingComponent } from '../components/LoadingComponent';
import { User, Guest, Candidate } from 'types/types';
import { useAuth } from "hooks/AuthContext";
import { useSelectedCandidate } from "hooks/GlobalContext";
import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
interface CandidateRouteProps {
guest?: Guest | null;
user?: User | null;
setSnack: SetSnackType,
};
}
const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProps) => {
const { apiClient } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const { setSnack } = props;
const { setSnack } = useAppState();
const { username } = useParams<{ username: string }>();
const navigate = useNavigate();
@ -25,30 +24,33 @@ const CandidateRoute: React.FC<CandidateRouteProps> = (props: CandidateRouteProp
if (selectedCandidate?.username === username || !username) {
return;
}
const getCandidate = async (reference: string) => {
const getCandidate = async (reference: string): Promise<void> => {
try {
const result: Candidate = await apiClient.getCandidate(reference);
setSelectedCandidate(result);
navigate('/chat');
} catch {
setSnack(`Unable to obtain information for ${username}.`, "error");
setSnack(`Unable to obtain information for ${username}.`, 'error');
navigate('/');
}
}
};
getCandidate(username);
}, [setSelectedCandidate, selectedCandidate, username, navigate, setSnack, apiClient]);
if (selectedCandidate?.username !== username) {
return (<Box>
return (
<Box>
<LoadingComponent
loadingText="Fetching candidate information..."
loaderType="linear"
withFade={true}
fadeDuration={1200} />
</Box>);
fadeDuration={1200}
/>
</Box>
);
} 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,
error: {
code: 'INVALID_RESPONSE',
message: 'Invalid response format'
}
message: 'Invalid response format',
},
};
}
@ -176,8 +176,8 @@ export function parsePaginatedResponse<T>(
...apiResponse,
data: {
...paginatedData,
data: paginatedData.data.map(itemParser)
}
data: paginatedData.data.map(itemParser),
},
};
}
@ -306,7 +306,7 @@ export function createPaginatedRequest(params: Partial<PaginatedRequest> = {}):
page: 1,
limit: 20,
sortOrder: 'desc',
...params
...params,
};
}
@ -350,7 +350,7 @@ export async function handlePaginatedApiResponse<T>(
/**
* 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') {
console.group(`🔄 ${label} Conversion`);
console.log('Original:', obj);
@ -375,7 +375,7 @@ const exports = {
createPaginatedRequest,
handleApiResponse,
handlePaginatedApiResponse,
debugConversion
}
debugConversion,
};
export default exports;

View File

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