Fixing eslint issues

This commit is contained in:
James Ketr 2025-06-18 16:40:46 -07:00
parent 66b68270cd
commit 17381dded1
30 changed files with 765 additions and 900 deletions

View File

@ -28,8 +28,8 @@
"react/prop-types": "off", "react/prop-types": "off",
"@typescript-eslint/explicit-function-return-type": "warn", "@typescript-eslint/explicit-function-return-type": "warn",
"no-unused-vars": "off", "no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }], "prettier/prettier": "error",
"prettier/prettier": "error" "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }]
}, },
"settings": { "settings": {
"react": { "react": {

View File

@ -1,3 +1,4 @@
import React, { JSX } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
@ -10,7 +11,7 @@ interface BackstoryQueryInterface {
submitQuery?: ChatSubmitQueryInterface; submitQuery?: ChatSubmitQueryInterface;
} }
const BackstoryQuery = (props: BackstoryQueryInterface) => { const BackstoryQuery = (props: BackstoryQueryInterface): JSX.Element => {
const { question, submitQuery } = props; const { question, submitQuery } = props;
if (submitQuery === undefined) { if (submitQuery === undefined) {
@ -25,7 +26,7 @@ const BackstoryQuery = (props: BackstoryQueryInterface) => {
m: 1, m: 1,
}} }}
size="small" size="small"
onClick={(e: any) => { onClick={(): void => {
submitQuery(question); submitQuery(question);
}} }}
> >

View File

@ -1,8 +1,6 @@
import React, { ReactElement, JSXElementConstructor } from 'react'; import React, { ReactElement, JSX } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import { ChatSubmitQueryInterface } from './BackstoryQuery';
import { SetSnackType } from './Snack';
interface BackstoryElementProps { interface BackstoryElementProps {
// setSnack: SetSnackType, // setSnack: SetSnackType,
@ -24,12 +22,12 @@ interface BackstoryTabProps {
tabProps?: { tabProps?: {
label?: string; label?: string;
sx?: SxProps; sx?: SxProps;
icon?: string | ReactElement<unknown, string | JSXElementConstructor<any>> | undefined; icon?: string | ReactElement<unknown, string> | undefined;
iconPosition?: 'bottom' | 'top' | 'start' | 'end' | undefined; iconPosition?: 'bottom' | 'top' | 'start' | 'end' | undefined;
}; };
} }
function BackstoryPage(props: BackstoryTabProps) { function BackstoryPage(props: BackstoryTabProps): JSX.Element {
const { className, active, children } = props; const { className, active, children } = props;
return ( return (

View File

@ -72,21 +72,21 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
}); });
// Cleanup RAF to prevent memory leaks // Cleanup RAF to prevent memory leaks
return () => cancelAnimationFrame(raf); return (): void => cancelAnimationFrame(raf);
}, [editValue, placeholder]); }, [editValue, placeholder]);
// Expose getValue method via ref // Expose getValue method via ref
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getValue: () => editValue, getValue: (): string => editValue,
setValue: (value: string) => setEditValue(value), setValue: (value: string): void => setEditValue(value),
getAndResetValue: () => { getAndResetValue: (): string => {
const _ev = editValue; const _ev = editValue;
setEditValue(''); setEditValue('');
return _ev; return _ev;
}, },
})); }));
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {
if (!onEnter) { if (!onEnter) {
return; return;
} }
@ -122,7 +122,7 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
value={editValue} value={editValue}
disabled={disabled} disabled={disabled}
placeholder={placeholder} placeholder={placeholder}
onChange={e => { onChange={(e): void => {
setEditValue(e.target.value); setEditValue(e.target.value);
onChange && onChange(e.target.value); onChange && onChange(e.target.value);
}} }}
@ -152,7 +152,7 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
); );
} }
); );
BackstoryTextField.displayName = 'BackstoryTextField';
export type { BackstoryTextFieldRef }; export type { BackstoryTextFieldRef };
export { BackstoryTextField }; export { BackstoryTextField };

View File

@ -30,6 +30,7 @@ import {
ChatMessageError, ChatMessageError,
ChatMessageStreaming, ChatMessageStreaming,
ChatMessageStatus, ChatMessageStatus,
ChatMessageMetaData,
} from 'types/types'; } from 'types/types';
import { PaginatedResponse } from 'types/conversion'; import { PaginatedResponse } from 'types/conversion';
@ -43,7 +44,7 @@ const defaultMessage: ChatMessage = {
timestamp: new Date(), timestamp: new Date(),
content: '', content: '',
role: 'assistant', role: 'assistant',
metadata: null as any, metadata: null as unknown as ChatMessageMetaData,
}; };
const loadingMessage: ChatMessage = { const loadingMessage: ChatMessage = {
@ -88,14 +89,11 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
onResponse, onResponse,
placeholder, placeholder,
preamble, preamble,
resetAction,
resetLabel, resetLabel,
sx, sx,
type,
} = props; } = props;
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [countdown, setCountdown] = useState<number>(0);
const [conversation, setConversation] = useState<ChatMessage[]>([]); const [conversation, setConversation] = useState<ChatMessage[]>([]);
const conversationRef = useRef<ChatMessage[]>([]); const conversationRef = useRef<ChatMessage[]>([]);
const [filteredConversation, setFilteredConversation] = useState<ChatMessage[]>([]); const [filteredConversation, setFilteredConversation] = useState<ChatMessage[]>([]);
@ -144,7 +142,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
if (chatSession) { if (chatSession) {
return; return;
} }
const createChatSession = async () => { const createChatSession = async (): Promise<void> => {
try { try {
const chatContext: ChatContext = { type: 'general' }; const chatContext: ChatContext = { type: 'general' };
const response: ChatSession = await apiClient.createChatSession(chatContext); const response: ChatSession = await apiClient.createChatSession(chatContext);
@ -156,7 +154,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
}; };
createChatSession(); createChatSession();
}, [chatSession, setChatSession]); }, [chatSession, setChatSession, apiClient, setSnack]);
const getChatMessages = useCallback(async () => { const getChatMessages = useCallback(async () => {
if (!chatSession || !chatSession.id) { if (!chatSession || !chatSession.id) {
@ -193,7 +191,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
}, 3000); }, 3000);
setSnack('Unable to obtain chat history.', 'error'); setSnack('Unable to obtain chat history.', 'error');
} }
}, [chatSession]); }, [chatSession, apiClient, setSnack]);
// Set the initial chat history to "loading" or the welcome message if loaded. // Set the initial chat history to "loading" or the welcome message if loaded.
useEffect(() => { useEffect(() => {
@ -208,9 +206,9 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
setNoInteractions(true); setNoInteractions(true);
getChatMessages(); getChatMessages();
}, [chatSession]); }, [chatSession, getChatMessages]);
const handleEnter = (value: string) => { const handleEnter = (value: string): void => {
const query: ChatQuery = { const query: ChatQuery = {
prompt: value, prompt: value,
}; };
@ -218,10 +216,10 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
}; };
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
submitQuery: (query: ChatQuery) => { submitQuery: (query: ChatQuery): void => {
processQuery(query); processQuery(query);
}, },
fetchHistory: () => { fetchHistory: (): void => {
getChatMessages(); getChatMessages();
}, },
})); }));
@ -256,7 +254,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
// } // }
// }; // };
const cancelQuery = () => { const cancelQuery = (): void => {
console.log('Stop query'); console.log('Stop query');
if (controllerRef.current) { if (controllerRef.current) {
controllerRef.current.cancel(); controllerRef.current.cancel();
@ -264,12 +262,10 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
controllerRef.current = null; controllerRef.current = null;
}; };
const processQuery = (query: ChatQuery) => { const processQuery = (query: ChatQuery): void => {
if (controllerRef.current || !chatSession || !chatSession.id) { if (controllerRef.current || !chatSession || !chatSession.id) {
return; return;
} }
const sessionId: string = chatSession.id;
setNoInteractions(false); setNoInteractions(false);
setConversation([ setConversation([
...conversationRef.current, ...conversationRef.current,
@ -396,17 +392,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
aria-label="Loading Spinner" aria-label="Loading Spinner"
data-testid="loader" data-testid="loader"
/> />
{processing === true && countdown > 0 && (
<Box
sx={{
pt: 1,
fontSize: '0.7rem',
color: 'darkgrey',
}}
>
Response will be stopped in: {countdown}s
</Box>
)}
</Box> </Box>
<Box <Box
className="Query" className="Query"
@ -443,7 +428,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
<DeleteConfirmation <DeleteConfirmation
label={resetLabel || 'all data'} label={resetLabel || 'all data'}
disabled={!chatSession || processingMessage !== undefined || noInteractions} disabled={!chatSession || processingMessage !== undefined || noInteractions}
onDelete={() => { onDelete={(): void => {
/*reset(); resetAction && resetAction(); */ /*reset(); resetAction && resetAction(); */
}} }}
/> />
@ -453,7 +438,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
sx={{ m: 1, gap: 1, flexGrow: 1 }} sx={{ m: 1, gap: 1, flexGrow: 1 }}
variant="contained" variant="contained"
disabled={!chatSession || processingMessage !== undefined} disabled={!chatSession || processingMessage !== undefined}
onClick={() => { onClick={(): void => {
processQuery({ processQuery({
prompt: prompt:
(backstoryTextRef.current && (backstoryTextRef.current &&
@ -473,7 +458,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} {/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton <IconButton
aria-label="cancel" aria-label="cancel"
onClick={() => { onClick={(): void => {
cancelQuery(); cancelQuery();
}} }}
sx={{ display: 'flex', margin: 'auto 0px' }} sx={{ display: 'flex', margin: 'auto 0px' }}
@ -502,7 +487,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>(
); );
} }
); );
Conversation.displayName = 'Conversation';
export type { ConversationProps, ConversationHandle }; export type { ConversationProps, ConversationHandle };
export { Conversation }; export { Conversation };

View File

@ -1,3 +1,4 @@
import React, { JSX } from 'react';
import { useState } from 'react'; import { useState } from 'react';
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import CheckIcon from '@mui/icons-material/Check'; import CheckIcon from '@mui/icons-material/Check';
@ -17,10 +18,10 @@ const CopyBubble = ({
tooltip = 'Copy to clipboard', tooltip = 'Copy to clipboard',
onClick, onClick,
...rest ...rest
}: CopyBubbleProps) => { }: CopyBubbleProps): JSX.Element => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const handleCopy = (e: any) => { const handleCopy = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
if (content === undefined) { if (content === undefined) {
return; return;
} }
@ -38,7 +39,7 @@ const CopyBubble = ({
return ( return (
<Tooltip title={tooltip} placement="top" arrow> <Tooltip title={tooltip} placement="top" arrow>
<IconButton <IconButton
onClick={e => { onClick={(e): void => {
handleCopy(e); handleCopy(e);
}} }}
sx={{ sx={{

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { JSX, useState } from 'react';
import { import {
IconButton, IconButton,
Dialog, Dialog,
@ -45,11 +45,11 @@ interface DeleteConfirmationProps {
cancelButtonText?: string; cancelButtonText?: string;
} }
function capitalizeFirstLetter(str: string) { function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
} }
const DeleteConfirmation = (props: DeleteConfirmationProps) => { const DeleteConfirmation = (props: DeleteConfirmationProps): JSX.Element => {
const { const {
// Legacy props // Legacy props
onDelete, onDelete,
@ -79,13 +79,13 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
const isControlled = controlledOpen !== undefined; const isControlled = controlledOpen !== undefined;
const isOpen = isControlled ? controlledOpen : internalOpen; const isOpen = isControlled ? controlledOpen : internalOpen;
const handleClickOpen = () => { const handleClickOpen = (): void => {
if (!isControlled) { if (!isControlled) {
setInternalOpen(true); setInternalOpen(true);
} }
}; };
const handleClose = () => { const handleClose = (): void => {
if (isControlled) { if (isControlled) {
controlledOnClose?.(); controlledOnClose?.();
} else { } else {
@ -93,7 +93,7 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
} }
}; };
const handleConfirm = () => { const handleConfirm = (): void => {
if (isControlled) { if (isControlled) {
onConfirm?.(); onConfirm?.();
} else { } else {
@ -122,7 +122,7 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */} {/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton <IconButton
aria-label={action} aria-label={action}
onClick={e => { onClick={(e): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
handleClickOpen(); handleClickOpen();

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, JSX } from 'react';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { StyledMarkdown } from './StyledMarkdown'; import { StyledMarkdown } from './StyledMarkdown';
@ -6,7 +6,7 @@ interface DocumentProps extends BackstoryElementProps {
filepath?: string; filepath?: string;
} }
const Document = (props: DocumentProps) => { const Document = (props: DocumentProps): JSX.Element => {
const { filepath } = props; const { filepath } = props;
const [document, setDocument] = useState<string>(''); const [document, setDocument] = useState<string>('');
@ -16,7 +16,7 @@ const Document = (props: DocumentProps) => {
if (!filepath) { if (!filepath) {
return; return;
} }
const fetchDocument = async () => { const fetchDocument = async (): Promise<void> => {
try { try {
const response = await fetch(filepath, { const response = await fetch(filepath, {
method: 'GET', method: 'GET',
@ -29,7 +29,7 @@ const Document = (props: DocumentProps) => {
} }
const data = await response.text(); const data = await response.text();
setDocument(data); setDocument(data);
} catch (error: any) { } catch (error) {
console.error('Error obtaining Docs content information:', error); console.error('Error obtaining Docs content information:', error);
setDocument(`${filepath} not found.`); setDocument(`${filepath} not found.`);
} }

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, JSX } from 'react';
import { import {
Box, Box,
Button, Button,
@ -44,7 +44,7 @@ const VisuallyHiddenInput = styled('input')({
width: 1, width: 1,
}); });
const DocumentManager = (props: BackstoryElementProps) => { const DocumentManager = (_props: BackstoryElementProps): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
@ -63,23 +63,23 @@ const DocumentManager = (props: BackstoryElementProps) => {
// Load documents on component mount // Load documents on component mount
useEffect(() => { useEffect(() => {
const loadDocuments = async (): Promise<void> => {
try {
const results = await apiClient.getCandidateDocuments();
setDocuments(results.documents);
} catch (error) {
console.error(error);
setSnack('Failed to load documents', 'error');
}
};
if (candidate) { if (candidate) {
loadDocuments(); loadDocuments();
} }
}, [candidate]); }, [candidate, apiClient, setSnack]);
const loadDocuments = async () => {
try {
const results = await apiClient.getCandidateDocuments();
setDocuments(results.documents);
} catch (error) {
console.error(error);
setSnack('Failed to load documents', 'error');
}
};
// Handle document upload // Handle document upload
const handleDocumentUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleDocumentUpload = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
if (e.target.files && e.target.files[0]) { if (e.target.files && e.target.files[0]) {
const file = e.target.files[0]; const file = e.target.files[0];
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
@ -132,7 +132,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
}; };
// Handle document deletion // Handle document deletion
const handleDeleteDocument = async (document: Types.Document) => { const handleDeleteDocument = async (document: Types.Document): Promise<void> => {
try { try {
// Call API to delete document // Call API to delete document
await apiClient.deleteCandidateDocument(document); await apiClient.deleteCandidateDocument(document);
@ -152,7 +152,10 @@ const DocumentManager = (props: BackstoryElementProps) => {
}; };
// Handle RAG flag toggle // Handle RAG flag toggle
const handleRAGToggle = async (document: Types.Document, includeInRag: boolean) => { const handleRAGToggle = async (
document: Types.Document,
includeInRag: boolean
): Promise<void> => {
try { try {
document.options = { includeInRag }; document.options = { includeInRag };
// Call API to update RAG flag // Call API to update RAG flag
@ -168,7 +171,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
}; };
// Handle document rename // Handle document rename
const handleRenameDocument = async (document: Types.Document, newName: string) => { const handleRenameDocument = async (document: Types.Document, newName: string): Promise<void> => {
if (!newName.trim()) { if (!newName.trim()) {
setSnack('Document name cannot be empty', 'error'); setSnack('Document name cannot be empty', 'error');
return; return;
@ -192,7 +195,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
}; };
// Handle document content viewing // Handle document content viewing
const handleViewDocument = async (document: Types.Document) => { const handleViewDocument = async (document: Types.Document): Promise<void> => {
try { try {
setSelectedDocument(document); setSelectedDocument(document);
setIsViewingContent(true); setIsViewingContent(true);
@ -207,7 +210,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
}; };
// Start rename process // Start rename process
const startRename = (document: Types.Document, currentName: string) => { const startRename = (document: Types.Document, currentName: string): void => {
setEditingDocument(document); setEditingDocument(document);
setEditingName(currentName); setEditingName(currentName);
setIsRenameDialogOpen(true); setIsRenameDialogOpen(true);
@ -331,7 +334,9 @@ const DocumentManager = (props: BackstoryElementProps) => {
control={ control={
<Switch <Switch
checked={doc.options?.includeInRag} checked={doc.options?.includeInRag}
onChange={e => handleRAGToggle(doc, e.target.checked)} onChange={(e): void => {
handleRAGToggle(doc, e.target.checked);
}}
size="small" size="small"
/> />
} }
@ -346,7 +351,9 @@ const DocumentManager = (props: BackstoryElementProps) => {
<IconButton <IconButton
edge="end" edge="end"
size="small" size="small"
onClick={() => handleViewDocument(doc)} onClick={(): void => {
handleViewDocument(doc);
}}
title="View content" title="View content"
> >
<Visibility /> <Visibility />
@ -354,7 +361,9 @@ const DocumentManager = (props: BackstoryElementProps) => {
<IconButton <IconButton
edge="end" edge="end"
size="small" size="small"
onClick={() => startRename(doc, doc.filename)} onClick={(): void => {
startRename(doc, doc.filename);
}}
title="Rename" title="Rename"
> >
<Edit /> <Edit />
@ -362,7 +371,9 @@ const DocumentManager = (props: BackstoryElementProps) => {
<IconButton <IconButton
edge="end" edge="end"
size="small" size="small"
onClick={() => handleDeleteDocument(doc)} onClick={(): void => {
handleDeleteDocument(doc);
}}
title="Delete" title="Delete"
color="error" color="error"
> >
@ -395,7 +406,7 @@ const DocumentManager = (props: BackstoryElementProps) => {
<Typography variant={isMobile ? 'subtitle2' : 'h6'}>Document Content</Typography> <Typography variant={isMobile ? 'subtitle2' : 'h6'}>Document Content</Typography>
<IconButton <IconButton
size="small" size="small"
onClick={() => { onClick={(): void => {
setIsViewingContent(false); setIsViewingContent(false);
setSelectedDocument(null); setSelectedDocument(null);
setDocumentContent(''); setDocumentContent('');
@ -433,7 +444,9 @@ const DocumentManager = (props: BackstoryElementProps) => {
{/* Rename Dialog */} {/* Rename Dialog */}
<Dialog <Dialog
open={isRenameDialogOpen} open={isRenameDialogOpen}
onClose={() => setIsRenameDialogOpen(false)} onClose={(): void => {
setIsRenameDialogOpen(false);
}}
maxWidth="sm" maxWidth="sm"
fullWidth fullWidth
> >
@ -446,8 +459,10 @@ const DocumentManager = (props: BackstoryElementProps) => {
fullWidth fullWidth
variant="outlined" variant="outlined"
value={editingName} value={editingName}
onChange={e => setEditingName(e.target.value)} onChange={(e): void => {
onKeyPress={e => { setEditingName(e.target.value);
}}
onKeyUp={(e): void => {
if (e.key === 'Enter' && editingDocument) { if (e.key === 'Enter' && editingDocument) {
handleRenameDocument(editingDocument, editingName); handleRenameDocument(editingDocument, editingName);
} }
@ -455,9 +470,17 @@ const DocumentManager = (props: BackstoryElementProps) => {
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setIsRenameDialogOpen(false)}>Cancel</Button>
<Button <Button
onClick={() => editingDocument && handleRenameDocument(editingDocument, editingName)} onClick={(): void => {
setIsRenameDialogOpen(false);
}}
>
Cancel
</Button>
<Button
onClick={(): void => {
editingDocument && handleRenameDocument(editingDocument, editingName);
}}
variant="contained" variant="contained"
disabled={!editingName.trim()} disabled={!editingName.trim()}
> >

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, JSX } from 'react';
import { import {
Box, Box,
Card, Card,
@ -8,8 +8,6 @@ import {
Button, Button,
Alert, Alert,
CircularProgress, CircularProgress,
Link,
Divider,
InputAdornment, InputAdornment,
Dialog, Dialog,
DialogTitle, DialogTitle,
@ -32,14 +30,14 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { BackstoryPageProps } from './BackstoryTab'; import { BackstoryPageProps } from './BackstoryTab';
import { Navigate, useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { MFAData } from 'types/types';
// Email Verification Component // Email Verification Component
const EmailVerificationPage = (props: BackstoryPageProps) => { const EmailVerificationPage = (_props: BackstoryPageProps): JSX.Element => {
const { verifyEmail, resendEmailVerification, getPendingVerificationEmail, isLoading, error } = const { verifyEmail, resendEmailVerification, getPendingVerificationEmail, isLoading, error } =
useAuth(); useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [verificationToken, setVerificationToken] = useState('');
const [status, setStatus] = useState<'pending' | 'success' | 'error'>('pending'); const [status, setStatus] = useState<'pending' | 'success' | 'error'>('pending');
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [userType, setUserType] = useState<string>(''); const [userType, setUserType] = useState<string>('');
@ -49,42 +47,41 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token'); const token = urlParams.get('token');
if (token) { const handleVerifyEmail = async (token: string): Promise<void> => {
setVerificationToken(token); if (!token) {
handleVerifyEmail(token); setStatus('error');
} setMessage('Invalid verification link');
}, []); return;
}
const handleVerifyEmail = async (token: string) => { try {
if (!token) { const result = await verifyEmail({ token });
setStatus('error');
setMessage('Invalid verification link');
return;
}
try { if (result) {
const result = await verifyEmail({ token }); setStatus('success');
setMessage(result.message);
setUserType(result.userType);
if (result) { // Redirect to login after 3 seconds
setStatus('success'); setTimeout(() => {
setMessage(result.message); navigate('/login');
setUserType(result.userType); }, 3000);
} else {
// Redirect to login after 3 seconds setStatus('error');
setTimeout(() => { setMessage('Email verification failed');
navigate('/login'); }
}, 3000); } catch (error) {
} else {
setStatus('error'); setStatus('error');
setMessage('Email verification failed'); setMessage('Email verification failed');
} }
} catch (error) { };
setStatus('error');
setMessage('Email verification failed');
}
};
const handleResendVerification = async () => { if (token) {
handleVerifyEmail(token);
}
}, [navigate, verifyEmail]);
const handleResendVerification = async (): Promise<void> => {
const email = getPendingVerificationEmail(); const email = getPendingVerificationEmail();
if (!email) { if (!email) {
setMessage('No pending verification email found.'); setMessage('No pending verification email found.');
@ -146,7 +143,7 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
Verification Failed Verification Failed
</Typography> </Typography>
<Typography color="text.secondary"> <Typography color="text.secondary">
We couldn't verify your email address. We couldn&apos;t verify your email address.
</Typography> </Typography>
</> </>
)} )}
@ -172,7 +169,13 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
<Typography variant="body2" color="text.secondary" mb={2}> <Typography variant="body2" color="text.secondary" mb={2}>
You will be redirected to the login page in a few seconds... You will be redirected to the login page in a few seconds...
</Typography> </Typography>
<Button variant="contained" onClick={() => navigate('/login')} fullWidth> <Button
variant="contained"
onClick={(): void => {
navigate('/login');
}}
fullWidth
>
Go to Login Go to Login
</Button> </Button>
</Box> </Box>
@ -190,7 +193,13 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
> >
Resend Verification Email Resend Verification Email
</Button> </Button>
<Button variant="contained" onClick={() => navigate('/login')} fullWidth> <Button
variant="contained"
onClick={(): void => {
navigate('/login');
}}
fullWidth
>
Back to Login Back to Login
</Button> </Button>
</Box> </Box>
@ -205,9 +214,9 @@ const EmailVerificationPage = (props: BackstoryPageProps) => {
interface MFAVerificationDialogProps { interface MFAVerificationDialogProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onVerificationSuccess: (authData: any) => void; onVerificationSuccess: () => void;
} }
const MFAVerificationDialog = (props: MFAVerificationDialogProps) => { const MFAVerificationDialog = (props: MFAVerificationDialogProps): JSX.Element => {
const { open, onClose, onVerificationSuccess } = props; const { open, onClose, onVerificationSuccess } = props;
const { verifyMFA, resendMFACode, clearMFA, mfaResponse, isLoading, error } = useAuth(); const { verifyMFA, resendMFACode, clearMFA, mfaResponse, isLoading, error } = useAuth();
const [code, setCode] = useState(''); const [code, setCode] = useState('');
@ -243,13 +252,13 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
return () => clearInterval(timer); return () => clearInterval(timer);
}, [open]); }, [open]);
const formatTime = (seconds: number) => { const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60); const mins = Math.floor(seconds / 60);
const secs = seconds % 60; const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`; return `${mins}:${secs.toString().padStart(2, '0')}`;
}; };
const handleVerifyMFA = async () => { const handleVerifyMFA = async (): Promise<void> => {
if (!code || code.length !== 6) { if (!code || code.length !== 6) {
setLocalError('Please enter a valid 6-digit code'); setLocalError('Please enter a valid 6-digit code');
return; return;
@ -271,7 +280,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
}); });
if (success) { if (success) {
onVerificationSuccess({ success: true }); onVerificationSuccess();
onClose(); onClose();
} }
} catch (error) { } catch (error) {
@ -279,7 +288,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
} }
}; };
const handleResendCode = async () => { const handleResendCode = async (): Promise<void> => {
if (!mfaResponse || !mfaResponse.mfaData) { if (!mfaResponse || !mfaResponse.mfaData) {
setLocalError('MFA data not available'); setLocalError('MFA data not available');
return; return;
@ -301,12 +310,12 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
} }
}; };
const handleClose = () => { const handleClose = (): void => {
clearMFA(); clearMFA();
onClose(); onClose();
}; };
if (!mfaResponse || !mfaResponse.mfaData) return null; if (!mfaResponse || !mfaResponse.mfaData) return <></>;
return ( return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
@ -319,12 +328,12 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
<DialogContent> <DialogContent>
<Alert severity="info" sx={{ mb: 3 }}> <Alert severity="info" sx={{ mb: 3 }}>
We've detected a login from a new device:{' '} We&apos;ve detected a login from a new device:{' '}
<strong>{mfaResponse.mfaData.deviceName}</strong> <strong>{mfaResponse.mfaData.deviceName}</strong>
</Alert> </Alert>
<Typography variant="body1" gutterBottom> <Typography variant="body1" gutterBottom>
We've sent a 6-digit verification code to: We&apos;ve sent a 6-digit verification code to:
</Typography> </Typography>
<Typography variant="h6" color="primary" gutterBottom> <Typography variant="h6" color="primary" gutterBottom>
{mfaResponse.mfaData.email} {mfaResponse.mfaData.email}
@ -334,7 +343,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
fullWidth fullWidth
label="Enter 6-digit code" label="Enter 6-digit code"
value={code} value={code}
onChange={e => { onChange={(e): void => {
const value = e.target.value.replace(/\D/g, '').slice(0, 6); const value = e.target.value.replace(/\D/g, '').slice(0, 6);
setCode(value); setCode(value);
setLocalError(''); setLocalError('');
@ -370,7 +379,9 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
control={ control={
<Checkbox <Checkbox
checked={rememberDevice} checked={rememberDevice}
onChange={e => setRememberDevice(e.target.checked)} onChange={(e): void => {
setRememberDevice(e.target.checked);
}}
/> />
} }
label="Remember this device for 90 days" label="Remember this device for 90 days"
@ -378,7 +389,7 @@ const MFAVerificationDialog = (props: MFAVerificationDialogProps) => {
<Alert severity="warning" sx={{ mt: 2 }}> <Alert severity="warning" sx={{ mt: 2 }}>
<Typography variant="body2"> <Typography variant="body2">
If you didn't attempt to log in, please change your password immediately. If you didn&apos;t attempt to log in, please change your password immediately.
</Typography> </Typography>
</Alert> </Alert>
</DialogContent> </DialogContent>
@ -410,18 +421,19 @@ const RegistrationSuccessDialog = ({
onClose: () => void; onClose: () => void;
email: string; email: string;
userType: string; userType: string;
}) => { }): JSX.Element => {
const { resendEmailVerification, isLoading } = useAuth(); const { resendEmailVerification, isLoading } = useAuth();
const [resendMessage, setResendMessage] = useState(''); const [resendMessage, setResendMessage] = useState('');
const handleResendVerification = async () => { const handleResendVerification = async (): Promise<void> => {
try { try {
const success = await resendEmailVerification(email); const success = await resendEmailVerification(email);
if (success) { if (success) {
setResendMessage('Verification email sent!'); setResendMessage('Verification email sent!');
} }
} catch (error: any) { } catch (error: unknown) {
setResendMessage(error?.message || 'Network error. Please try again.'); const tmp = error as { message?: string };
setResendMessage(tmp?.message || 'Network error. Please try again.');
} }
}; };
@ -435,7 +447,7 @@ const RegistrationSuccessDialog = ({
</Typography> </Typography>
<Typography variant="body1" color="text.secondary" paragraph> <Typography variant="body1" color="text.secondary" paragraph>
We've sent a verification link to: We&apos;e sent a verification link to:
</Typography> </Typography>
<Typography variant="h6" color="primary" gutterBottom> <Typography variant="h6" color="primary" gutterBottom>
@ -478,7 +490,7 @@ const RegistrationSuccessDialog = ({
}; };
// Enhanced Login Component with MFA Support // Enhanced Login Component with MFA Support
const LoginForm = () => { const LoginForm = (): JSX.Element => {
const { login, mfaResponse, isLoading, error, user } = useAuth(); const { login, mfaResponse, isLoading, error, user } = useAuth();
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@ -496,7 +508,7 @@ const LoginForm = () => {
setErrorMessage(data.error.message); setErrorMessage(data.error.message);
}, [error]); }, [error]);
const handleLogin = async (e: React.FormEvent) => { const handleLogin = async (e: React.FormEvent): Promise<void> => {
e.preventDefault(); e.preventDefault();
const success = await login({ const success = await login({
@ -512,11 +524,11 @@ const LoginForm = () => {
} }
}; };
const handleMFASuccess = (authData: any) => { const handleMFASuccess = (): void => {
handleLoginSuccess(); handleLoginSuccess();
}; };
const handleLoginSuccess = () => { const handleLoginSuccess = (): void => {
if (!user) { if (!user) {
navigate('/'); navigate('/');
} else { } else {
@ -533,7 +545,9 @@ const LoginForm = () => {
fullWidth fullWidth
label="Email or Username" label="Email or Username"
value={email} value={email}
onChange={e => setEmail(e.target.value)} onChange={(e): void => {
setEmail(e.target.value);
}}
autoComplete="email" autoComplete="email"
autoFocus autoFocus
/> />
@ -542,7 +556,9 @@ const LoginForm = () => {
label="Password" label="Password"
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={password} value={password}
onChange={e => setPassword(e.target.value)} onChange={(e): void => {
setPassword(e.target.value);
}}
autoComplete="current-password" autoComplete="current-password"
placeholder="Create a strong password" placeholder="Create a strong password"
required required
@ -551,8 +567,12 @@ const LoginForm = () => {
<InputAdornment position="end"> <InputAdornment position="end">
<IconButton <IconButton
aria-label="toggle password visibility" aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)} onClick={(): void => {
onMouseDown={e => e.preventDefault()} setShowPassword(!showPassword);
}}
onMouseDown={(e): void => {
e.preventDefault();
}}
edge="end" edge="end"
> >
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
@ -581,7 +601,9 @@ const LoginForm = () => {
{/* MFA Dialog */} {/* MFA Dialog */}
<MFAVerificationDialog <MFAVerificationDialog
open={mfaResponse?.mfaRequired || false} open={mfaResponse?.mfaRequired || false}
onClose={() => {}} // This will be handled by clearMFA in the dialog onClose={(): void => {
console.log();
}} // This will be handled by clearMFA in the dialog
onVerificationSuccess={handleMFASuccess} onVerificationSuccess={handleMFASuccess}
/> />
</Box> </Box>
@ -589,9 +611,9 @@ const LoginForm = () => {
}; };
// Device Management Component // Device Management Component
const TrustedDevicesManager = () => { const TrustedDevicesManager = (): JSX.Element => {
const [devices, setDevices] = useState<any[]>([]); const [devices, _setDevices] = useState<MFAData[]>([]);
const [loading, setLoading] = useState(true); const [_loading, setLoading] = useState(true);
// This would need API endpoints to manage trusted devices // This would need API endpoints to manage trusted devices
useEffect(() => { useEffect(() => {
@ -608,8 +630,8 @@ const TrustedDevicesManager = () => {
</Typography> </Typography>
<Typography variant="body2" color="text.secondary" paragraph> <Typography variant="body2" color="text.secondary" paragraph>
Manage devices that you've marked as trusted. You won't need to verify your identity when Manage devices that you&apos;ve marked as trusted. You won&apos;t need to verify your
signing in from these devices. identity when signing in from these devices.
</Typography> </Typography>
{devices.length === 0 ? ( {devices.length === 0 ? (
@ -624,18 +646,18 @@ const TrustedDevicesManager = () => {
<Card variant="outlined"> <Card variant="outlined">
<CardContent> <CardContent>
<Typography variant="subtitle1">{device.deviceName}</Typography> <Typography variant="subtitle1">{device.deviceName}</Typography>
<Typography variant="body2" color="text.secondary"> {/* <Typography variant="body2" color="text.secondary">
Added: {new Date(device.addedAt).toLocaleDateString()} Added: {new Date(device.addedAt).toLocaleDateString()}
</Typography> </Typography> */}
<Typography variant="body2" color="text.secondary"> {/* <Typography variant="body2" color="text.secondary">
Last used: {new Date(device.lastUsed).toLocaleDateString()} Last used: {new Date(device.lastUsed).toLocaleDateString()}
</Typography> </Typography> */}
<Button <Button
size="small" size="small"
color="error" color="error"
sx={{ mt: 1 }} sx={{ mt: 1 }}
onClick={() => { onClick={(): void => {
// Remove device console.log('Remove device');
}} }}
> >
Remove Remove

View File

@ -1,3 +1,4 @@
import React, { JSX } from 'react';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import IconButton, { IconButtonProps } from '@mui/material/IconButton'; import IconButton, { IconButtonProps } from '@mui/material/IconButton';
@ -5,7 +6,8 @@ interface ExpandMoreProps extends IconButtonProps {
expand: boolean; expand: boolean;
} }
const ExpandMore = styled((props: ExpandMoreProps) => { const ExpandMore = styled((props: ExpandMoreProps): JSX.Element => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { expand, ...other } = props; const { expand, ...other } = props;
return <IconButton {...other} />; return <IconButton {...other} />;
})(({ theme }) => ({ })(({ theme }) => ({
@ -15,13 +17,13 @@ const ExpandMore = styled((props: ExpandMoreProps) => {
}), }),
variants: [ variants: [
{ {
props: ({ expand }) => !expand, props: ({ expand }): boolean => !expand,
style: { style: {
transform: 'rotate(0deg)', transform: 'rotate(0deg)',
}, },
}, },
{ {
props: ({ expand }) => !!expand, props: ({ expand }): boolean => !!expand,
style: { style: {
transform: 'rotate(180deg)', transform: 'rotate(180deg)',
}, },

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef, JSX } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import PropagateLoader from 'react-spinners/PropagateLoader'; import PropagateLoader from 'react-spinners/PropagateLoader';
import { Quote } from 'components/Quote'; import { Quote } from 'components/Quote';
import { BackstoryElementProps } from 'components/BackstoryTab'; import { BackstoryElementProps } from 'components/BackstoryTab';
import { Candidate, ChatSession } from 'types/types'; import { ChatSession } from 'types/types';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
@ -12,16 +12,13 @@ interface GenerateImageProps extends BackstoryElementProps {
chatSession: ChatSession; chatSession: ChatSession;
} }
const GenerateImage = (props: GenerateImageProps) => { const GenerateImage = (props: GenerateImageProps): JSX.Element => {
const { user } = useAuth(); const { user } = useAuth();
const { chatSession, prompt } = props; const { chatSession, prompt } = props;
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [status, setStatus] = useState<string>(''); const [status, setStatus] = useState<string>('');
const [image, setImage] = useState<string>(''); const [image, _setImage] = useState<string>('');
const name = (user?.userType === 'candidate' ? (user as Candidate).username : user?.email) || '';
// Only keep refs that are truly necessary
const controllerRef = useRef<string>(null); const controllerRef = useRef<string>(null);
// Effect to trigger profile generation when user data is ready // Effect to trigger profile generation when user data is ready
@ -35,8 +32,8 @@ const GenerateImage = (props: GenerateImageProps) => {
} }
setStatus('Starting image generation...'); setStatus('Starting image generation...');
setProcessing(true); setProcessing(true);
const start = Date.now();
// const start = Date.now();
// controllerRef.current = streamQueryResponse({ // controllerRef.current = streamQueryResponse({
// query: { // query: {
// prompt: prompt, // prompt: prompt,

View File

@ -13,16 +13,9 @@ import {
CardHeader, CardHeader,
LinearProgress, LinearProgress,
Stack, Stack,
Paper,
} from '@mui/material'; } from '@mui/material';
import { import {
SyncAlt,
Favorite,
Settings,
Info,
Search,
AutoFixHigh, AutoFixHigh,
Image,
Psychology, Psychology,
Build, Build,
CloudUpload, CloudUpload,
@ -36,9 +29,8 @@ import { styled } from '@mui/material/styles';
import FileUploadIcon from '@mui/icons-material/FileUpload'; import FileUploadIcon from '@mui/icons-material/FileUpload';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { useAppState, useSelectedJob } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { LoginRequired } from 'components/ui/LoginRequired';
import * as Types from 'types/types'; import * as Types from 'types/types';
import { StyledMarkdown } from './StyledMarkdown'; import { StyledMarkdown } from './StyledMarkdown';
@ -60,7 +52,10 @@ const VisuallyHiddenInput = styled('input')({
const UploadBox = styled(Box)(({ theme }) => ({ const UploadBox = styled(Box)(({ theme }) => ({
border: `2px dashed ${theme.palette.primary.main}`, border: `2px dashed ${theme.palette.primary.main}`,
borderRadius: theme.shape.borderRadius * 2, borderRadius:
(typeof theme.shape.borderRadius === 'string'
? parseInt(theme.shape.borderRadius)
: theme.shape.borderRadius) * 2,
padding: theme.spacing(4), padding: theme.spacing(4),
textAlign: 'center', textAlign: 'center',
backgroundColor: theme.palette.action.hover, backgroundColor: theme.palette.action.hover,
@ -75,10 +70,9 @@ const UploadBox = styled(Box)(({ theme }) => ({
interface JobCreatorProps extends BackstoryElementProps { interface JobCreatorProps extends BackstoryElementProps {
onSave?: (job: Types.Job) => void; onSave?: (job: Types.Job) => void;
} }
const JobCreator = (props: JobCreatorProps) => { const JobCreator = (props: JobCreatorProps): JSX.Element => {
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const { onSave } = props; const { onSave } = props;
const { selectedJob, setSelectedJob } = useSelectedJob();
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
@ -96,12 +90,12 @@ const JobCreator = (props: JobCreatorProps) => {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const jobStatusHandlers = { const jobStatusHandlers = {
onStatus: (status: Types.ChatMessageStatus) => { onStatus: (status: Types.ChatMessageStatus): void => {
console.log('status:', status.content); console.log('status:', status.content);
setJobStatusType(status.activity); setJobStatusType(status.activity);
setJobStatus(status.content); setJobStatus(status.content);
}, },
onMessage: (jobMessage: Types.JobRequirementsMessage) => { onMessage: (jobMessage: Types.JobRequirementsMessage): void => {
const job: Types.Job = jobMessage.job; const job: Types.Job = jobMessage.job;
console.log('onMessage - job', job); console.log('onMessage - job', job);
setJob(job); setJob(job);
@ -113,19 +107,19 @@ const JobCreator = (props: JobCreatorProps) => {
setJobStatusType(null); setJobStatusType(null);
setJobStatus(''); setJobStatus('');
}, },
onError: (error: Types.ChatMessageError) => { onError: (error: Types.ChatMessageError): void => {
console.log('onError', error); console.log('onError', error);
setSnack(error.content, 'error'); setSnack(error.content, 'error');
setIsProcessing(false); setIsProcessing(false);
}, },
onComplete: () => { onComplete: (): void => {
setJobStatusType(null); setJobStatusType(null);
setJobStatus(''); setJobStatus('');
setIsProcessing(false); setIsProcessing(false);
}, },
}; };
const handleJobUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleJobUpload = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
if (e.target.files && e.target.files[0]) { if (e.target.files && e.target.files[0]) {
const file = e.target.files[0]; const file = e.target.files[0];
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
@ -171,7 +165,7 @@ const JobCreator = (props: JobCreatorProps) => {
} }
}; };
const handleUploadClick = () => { const handleUploadClick = (): void => {
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
@ -180,8 +174,8 @@ const JobCreator = (props: JobCreatorProps) => {
items: string[] | undefined, items: string[] | undefined,
icon: JSX.Element, icon: JSX.Element,
required = false required = false
) => { ): JSX.Element => {
if (!items || items.length === 0) return null; if (!items || items.length === 0) return <></>;
return ( return (
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
@ -201,8 +195,8 @@ const JobCreator = (props: JobCreatorProps) => {
); );
}; };
const renderJobRequirements = () => { const renderJobRequirements = (): JSX.Element => {
if (!jobRequirements) return null; if (!jobRequirements) return <></>;
return ( return (
<Card elevation={2} sx={{ mt: 3 }}> <Card elevation={2} sx={{ mt: 3 }}>
@ -264,7 +258,7 @@ const JobCreator = (props: JobCreatorProps) => {
); );
}; };
const handleSave = async () => { const handleSave = async (): Promise<void> => {
const newJob: Types.Job = { const newJob: Types.Job = {
ownerId: user?.id || '', ownerId: user?.id || '',
ownerType: 'candidate', ownerType: 'candidate',
@ -286,7 +280,7 @@ const JobCreator = (props: JobCreatorProps) => {
onSave && onSave(job); onSave && onSave(job);
}; };
const handleExtractRequirements = async () => { const handleExtractRequirements = async (): Promise<void> => {
try { try {
setIsProcessing(true); setIsProcessing(true);
const controller = apiClient.createJobFromDescription(jobDescription, jobStatusHandlers); const controller = apiClient.createJobFromDescription(jobDescription, jobStatusHandlers);
@ -304,7 +298,7 @@ const JobCreator = (props: JobCreatorProps) => {
setIsProcessing(false); setIsProcessing(false);
}; };
const renderJobCreation = () => { const renderJobCreation = (): JSX.Element => {
return ( return (
<Box <Box
sx={{ sx={{
@ -371,7 +365,9 @@ const JobCreator = (props: JobCreatorProps) => {
placeholder="Paste or type the job description here..." placeholder="Paste or type the job description here..."
variant="outlined" variant="outlined"
value={jobDescription} value={jobDescription}
onChange={e => setJobDescription(e.target.value)} onChange={(e): void => {
setJobDescription(e.target.value);
}}
disabled={isProcessing} disabled={isProcessing}
sx={{ mb: 2 }} sx={{ mb: 2 }}
/> />
@ -418,7 +414,9 @@ const JobCreator = (props: JobCreatorProps) => {
label="Job Title" label="Job Title"
variant="outlined" variant="outlined"
value={jobTitle} value={jobTitle}
onChange={e => setJobTitle(e.target.value)} onChange={(e): void => {
setJobTitle(e.target.value);
}}
required required
disabled={isProcessing} disabled={isProcessing}
InputProps={{ InputProps={{
@ -433,7 +431,9 @@ const JobCreator = (props: JobCreatorProps) => {
label="Company" label="Company"
variant="outlined" variant="outlined"
value={company} value={company}
onChange={e => setCompany(e.target.value)} onChange={(e): void => {
setCompany(e.target.value);
}}
required required
disabled={isProcessing} disabled={isProcessing}
InputProps={{ InputProps={{

View File

@ -1,15 +1,12 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback, JSX, useMemo } from 'react';
import { import {
Box, Box,
Typography, Typography,
Paper,
Accordion, Accordion,
AccordionSummary, AccordionSummary,
AccordionDetails, AccordionDetails,
CircularProgress, CircularProgress,
Grid,
Chip, Chip,
Divider,
Card, Card,
CardContent, CardContent,
useTheme, useTheme,
@ -22,28 +19,11 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
import PendingIcon from '@mui/icons-material/Pending'; import PendingIcon from '@mui/icons-material/Pending';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { import { Candidate, ChatMessage, SkillAssessment, SkillStatus } from 'types/types';
Candidate,
ChatMessage,
ChatMessageError,
ChatMessageStatus,
ChatMessageStreaming,
ChatMessageUser,
ChatSession,
EvidenceDetail,
JobRequirements,
SkillAssessment,
SkillStatus,
} from 'types/types';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import { BackstoryPageProps } from './BackstoryTab'; import { BackstoryPageProps } from './BackstoryTab';
import { Job } from 'types/types'; import { Job } from 'types/types';
import { StyledMarkdown } from './StyledMarkdown';
import { Scrollable } from './Scrollable';
import { useAppState } from 'hooks/GlobalContext';
import * as Types from 'types/types'; import * as Types from 'types/types';
import JsonView from '@uiw/react-json-view';
import { VectorVisualizer } from './VectorVisualizer';
import { JobInfo } from './ui/JobInfo'; import { JobInfo } from './ui/JobInfo';
interface JobAnalysisProps extends BackstoryPageProps { interface JobAnalysisProps extends BackstoryPageProps {
@ -53,16 +33,6 @@ interface JobAnalysisProps extends BackstoryPageProps {
onAnalysisComplete: (skills: SkillAssessment[]) => void; onAnalysisComplete: (skills: SkillAssessment[]) => void;
} }
const defaultMessage: ChatMessage = {
status: 'done',
type: 'text',
sessionId: '',
timestamp: new Date(),
content: '',
role: 'assistant',
metadata: null as any,
};
interface SkillMatch extends SkillAssessment { interface SkillMatch extends SkillAssessment {
domain: string; domain: string;
status: SkillStatus; status: SkillStatus;
@ -72,20 +42,17 @@ interface SkillMatch extends SkillAssessment {
const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) => { 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 { apiClient } = useAuth();
const { setSnack } = useAppState();
const theme = useTheme(); const theme = useTheme();
const [requirements, setRequirements] = useState<{ requirement: string; domain: string }[]>([]); const [requirements, setRequirements] = useState<{ requirement: string; domain: string }[]>([]);
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]); const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
const [creatingSession, setCreatingSession] = useState<boolean>(false);
const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false); const [loadingRequirements, setLoadingRequirements] = useState<boolean>(false);
const [expanded, setExpanded] = useState<string | false>(false); const [expanded, setExpanded] = useState<string | false>(false);
const [overallScore, setOverallScore] = useState<number>(0); const [overallScore, setOverallScore] = useState<number>(0);
const [requirementsSession, setRequirementsSession] = useState<ChatSession | null>(null); const [_statusMessage, setStatusMessage] = useState<ChatMessage | null>(null);
const [statusMessage, setStatusMessage] = useState<ChatMessage | null>(null);
const [startAnalysis, setStartAnalysis] = useState<boolean>(false); const [startAnalysis, setStartAnalysis] = useState<boolean>(false);
const [analyzing, setAnalyzing] = useState<boolean>(false); const [analyzing, setAnalyzing] = useState<boolean>(false);
const [matchStatus, setMatchStatus] = useState<string>(''); const [matchStatus, setMatchStatus] = useState<string>('');
const [matchStatusType, setMatchStatusType] = useState<Types.ApiActivityType | null>(null); const [_matchStatusType, setMatchStatusType] = useState<Types.ApiActivityType | null>(null);
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
@ -95,93 +62,98 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
setExpanded(isExpanded ? panel : false); setExpanded(isExpanded ? panel : false);
}; };
const initializeRequirements = (job: Job) => { const initializeRequirements = useCallback(
if (!job || !job.requirements) { (job: Job): void => {
return; if (!job || !job.requirements) {
} return;
const requirements: { requirement: string; domain: string }[] = []; }
if (job.requirements?.technicalSkills) { const requirements: { requirement: string; domain: string }[] = [];
job.requirements.technicalSkills.required?.forEach(req => if (job.requirements?.technicalSkills) {
requirements.push({ job.requirements.technicalSkills.required?.forEach(req =>
requirement: req, requirements.push({
domain: 'Technical Skills (required)', requirement: req,
}) domain: 'Technical Skills (required)',
); })
job.requirements.technicalSkills.preferred?.forEach(req => );
requirements.push({ job.requirements.technicalSkills.preferred?.forEach(req =>
requirement: req, requirements.push({
domain: 'Technical Skills (preferred)', requirement: req,
}) domain: 'Technical Skills (preferred)',
); })
} );
if (job.requirements?.experienceRequirements) { }
job.requirements.experienceRequirements.required?.forEach(req => if (job.requirements?.experienceRequirements) {
requirements.push({ requirement: req, domain: 'Experience (required)' }) job.requirements.experienceRequirements.required?.forEach(req =>
); requirements.push({ requirement: req, domain: 'Experience (required)' })
job.requirements.experienceRequirements.preferred?.forEach(req => );
requirements.push({ job.requirements.experienceRequirements.preferred?.forEach(req =>
requirement: req, requirements.push({
domain: 'Experience (preferred)', requirement: req,
}) domain: 'Experience (preferred)',
); })
} );
if (job.requirements?.softSkills) { }
job.requirements.softSkills.forEach(req => if (job.requirements?.softSkills) {
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 => if (job.requirements?.experience) {
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 => if (job.requirements?.education) {
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 => if (job.requirements?.certifications) {
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 => if (job.requirements?.preferredAttributes) {
requirements.push({ requirement: req, domain: 'Preferred Attributes' }) job.requirements.preferredAttributes.forEach(req =>
); requirements.push({ requirement: req, domain: 'Preferred Attributes' })
} );
}
const initialSkillMatches: SkillMatch[] = requirements.map(req => ({ const initialSkillMatches: SkillMatch[] = requirements.map(req => ({
skill: req.requirement, skill: req.requirement,
skillModified: req.requirement, skillModified: req.requirement,
candidateId: candidate.id || '', candidateId: candidate.id || '',
domain: req.domain, domain: req.domain,
status: 'waiting' as const, status: 'waiting' as const,
assessment: '', assessment: '',
description: '', description: '',
evidenceFound: false, evidenceFound: false,
evidenceStrength: 'none', evidenceStrength: 'none',
evidenceDetails: [], evidenceDetails: [],
matchScore: 0, matchScore: 0,
})); }));
setRequirements(requirements); setRequirements(requirements);
setSkillMatches(initialSkillMatches); setSkillMatches(initialSkillMatches);
setStatusMessage(null); setStatusMessage(null);
setLoadingRequirements(false); setLoadingRequirements(false);
setOverallScore(0); setOverallScore(0);
}; },
[candidate.id]
);
useEffect(() => { useEffect(() => {
initializeRequirements(job); initializeRequirements(job);
}, [job]); }, [job, initializeRequirements]);
const skillMatchHandlers = { const skillMatchHandlers = useMemo(() => {
onStatus: (status: Types.ChatMessageStatus) => { return {
setMatchStatusType(status.activity); onStatus: (status: Types.ChatMessageStatus): void => {
setMatchStatus(status.content.toLowerCase()); setMatchStatusType(status.activity);
}, setMatchStatus(status.content.toLowerCase());
}; },
};
}, [setMatchStatus, setMatchStatusType]);
// Fetch match data for each requirement // Fetch match data for each requirement
useEffect(() => { useEffect(() => {
@ -189,7 +161,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
return; return;
} }
const fetchMatchData = async (skills: SkillAssessment[]) => { const fetchMatchData = async (skills: SkillAssessment[]): Promise<void> => {
if (requirements.length === 0) return; if (requirements.length === 0) return;
// Process requirements one by one // Process requirements one by one
@ -201,7 +173,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
return updated; return updated;
}); });
const request: any = await apiClient.candidateMatchForRequirement( const request = await apiClient.candidateMatchForRequirement(
candidate.id || '', candidate.id || '',
requirements[i].requirement, requirements[i].requirement,
skillMatchHandlers skillMatchHandlers
@ -226,11 +198,11 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
break; break;
} }
if ( if (
skillMatch.evidenceStrength == 'NONE' && skillMatch.evidenceStrength == 'none' &&
skillMatch.citations && skillMatch.evidenceDetails &&
skillMatch.citations.length > 3 skillMatch.evidenceDetails.length > 3
) { ) {
matchScore = Math.min(skillMatch.citations.length * 8, 40); matchScore = Math.min(skillMatch.evidenceDetails.length * 8, 40);
} }
const match: SkillMatch = { const match: SkillMatch = {
...skillMatch, ...skillMatch,
@ -277,7 +249,17 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
setStartAnalysis(false); setStartAnalysis(false);
onAnalysisComplete && onAnalysisComplete(skills); onAnalysisComplete && onAnalysisComplete(skills);
}); });
}, [job, onAnalysisComplete, startAnalysis, analyzing, requirements, loadingRequirements]); }, [
job,
onAnalysisComplete,
startAnalysis,
analyzing,
requirements,
loadingRequirements,
apiClient,
candidate.id,
skillMatchHandlers,
]);
// Get color based on match score // Get color based on match score
const getMatchColor = (score: number): string => { const getMatchColor = (score: number): string => {
@ -288,7 +270,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
}; };
// Get icon based on status // Get icon based on status
const getStatusIcon = (status: string, score: number) => { const getStatusIcon = (status: string, score: number): JSX.Element => {
if (status === 'pending' || status === 'waiting') return <PendingIcon />; if (status === 'pending' || status === 'waiting') return <PendingIcon />;
if (status === 'error') return <ErrorIcon color="error" />; if (status === 'error') return <ErrorIcon color="error" />;
if (score >= 70) return <CheckCircleIcon color="success" />; if (score >= 70) return <CheckCircleIcon color="success" />;
@ -296,7 +278,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
return <ErrorIcon color="error" />; return <ErrorIcon color="error" />;
}; };
const beginAnalysis = () => { const beginAnalysis = (): void => {
initializeRequirements(job); initializeRequirements(job);
setStartAnalysis(true); setStartAnalysis(true);
}; };
@ -512,7 +494,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
<Box sx={{ width: '100%', p: 2 }}> <Box sx={{ width: '100%', p: 2 }}>
<LinearProgress /> <LinearProgress />
<Typography sx={{ mt: 2 }}> <Typography sx={{ mt: 2 }}>
Analyzing candidate's match for this requirement... {matchStatus} Analyzing candidate&apos;s match for this requirement... {matchStatus}
</Typography> </Typography>
</Box> </Box>
) : match.status === 'error' ? ( ) : match.status === 'error' ? (
@ -549,7 +531,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
component="div" component="div"
sx={{ mb: 1, fontStyle: 'italic' }} sx={{ mb: 1, fontStyle: 'italic' }}
> >
"{evidence.quote}" &quot;{evidence.quote}&quot;
</Typography> </Typography>
<Box <Box
sx={{ sx={{
@ -578,7 +560,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
)) ))
) : ( ) : (
<Typography color="text.secondary"> <Typography color="text.secondary">
No specific evidence found in candidate's profile. No specific evidence found in candidate&apos;s profile.
</Typography> </Typography>
)} )}
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { import {
Box, Box,
TextField, TextField,
@ -45,13 +45,16 @@ const LocationInput: React.FC<LocationInputProps> = ({
const [isRemote, setIsRemote] = useState<boolean>(value.remote || false); const [isRemote, setIsRemote] = useState<boolean>(value.remote || false);
// Get states for selected country // Get states for selected country
const availableStates = selectedCountry ? State.getStatesOfCountry(selectedCountry.isoCode) : []; const availableStates = useMemo(() => {
return selectedCountry ? State.getStatesOfCountry(selectedCountry.isoCode) : [];
}, [selectedCountry]);
// Get cities for selected state // Get cities for selected state
const availableCities = const availableCities = useMemo(() => {
selectedCountry && selectedState return selectedCountry && selectedState
? City.getCitiesOfState(selectedCountry.isoCode, selectedState.isoCode) ? City.getCitiesOfState(selectedCountry.isoCode, selectedState.isoCode)
: []; : [];
}, [selectedCountry, selectedState]);
// Initialize state and city from value prop // Initialize state and city from value prop
useEffect(() => { useEffect(() => {
@ -104,24 +107,24 @@ const LocationInput: React.FC<LocationInputProps> = ({
showCity, showCity,
]); ]);
const handleCountryChange = (event: any, newValue: ICountry | null) => { const handleCountryChange = (_event: React.SyntheticEvent, newValue: ICountry | null): void => {
setSelectedCountry(newValue); setSelectedCountry(newValue);
// Clear state and city when country changes // Clear state and city when country changes
setSelectedState(null); setSelectedState(null);
setSelectedCity(null); setSelectedCity(null);
}; };
const handleStateChange = (event: any, newValue: IState | null) => { const handleStateChange = (_event: React.SyntheticEvent, newValue: IState | null): void => {
setSelectedState(newValue); setSelectedState(newValue);
// Clear city when state changes // Clear city when state changes
setSelectedCity(null); setSelectedCity(null);
}; };
const handleCityChange = (event: any, newValue: ICity | null) => { const handleCityChange = (_event: React.SyntheticEvent, newValue: ICity | null): void => {
setSelectedCity(newValue); setSelectedCity(newValue);
}; };
const handleRemoteToggle = (event: React.ChangeEvent<HTMLInputElement>) => { const handleRemoteToggle = (event: React.ChangeEvent<HTMLInputElement>): void => {
setIsRemote(event.target.checked); setIsRemote(event.target.checked);
}; };
@ -139,9 +142,9 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedCountry} value={selectedCountry}
onChange={handleCountryChange} onChange={handleCountryChange}
options={allCountries} options={allCountries}
getOptionLabel={option => option.name} getOptionLabel={(option): string => option.name}
disabled={disabled} disabled={disabled}
renderInput={params => ( renderInput={(params): React.ReactNode => (
<TextField <TextField
{...params} {...params}
label="Country" label="Country"
@ -157,7 +160,7 @@ const LocationInput: React.FC<LocationInputProps> = ({
}} }}
/> />
)} )}
renderOption={(props, option) => ( renderOption={(props, option): React.ReactNode => (
<Box component="li" {...props} key={option.isoCode}> <Box component="li" {...props} key={option.isoCode}>
<img <img
loading="lazy" loading="lazy"
@ -180,9 +183,9 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedState} value={selectedState}
onChange={handleStateChange} onChange={handleStateChange}
options={availableStates} options={availableStates}
getOptionLabel={option => option.name} getOptionLabel={(option): string => option.name}
disabled={disabled || availableStates.length === 0} disabled={disabled || availableStates.length === 0}
renderInput={params => ( renderInput={(params): React.ReactNode => (
<TextField <TextField
{...params} {...params}
label="State/Region" label="State/Region"
@ -203,9 +206,9 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedCity} value={selectedCity}
onChange={handleCityChange} onChange={handleCityChange}
options={availableCities} options={availableCities}
getOptionLabel={option => option.name} getOptionLabel={(option): string => option.name}
disabled={disabled || availableCities.length === 0} disabled={disabled || availableCities.length === 0}
renderInput={params => ( renderInput={(params): React.ReactNode => (
<TextField <TextField
{...params} {...params}
label="City" label="City"
@ -275,94 +278,4 @@ const LocationInput: React.FC<LocationInputProps> = ({
); );
}; };
// Demo component to show usage with real data
const LocationInputDemo: React.FC = () => {
const [location, setLocation] = useState<Partial<Location>>({});
const [showAdvanced, setShowAdvanced] = useState(false);
const handleLocationChange = (newLocation: Partial<Location>) => {
setLocation(newLocation);
console.log('Location updated:', newLocation);
};
// Show some stats about the data
const totalCountries = Country.getAllCountries().length;
const usStates = State.getStatesOfCountry('US').length;
const canadaProvinces = State.getStatesOfCountry('CA').length;
return (
<Box sx={{ p: 3, maxWidth: 800, mx: 'auto' }}>
<Typography variant="h4" gutterBottom align="center" color="primary">
Location Input with Real Data
</Typography>
<Typography variant="body2" color="text.secondary" align="center" sx={{ mb: 3 }}>
Using country-state-city library with {totalCountries} countries,
{usStates} US states, {canadaProvinces} Canadian provinces, and thousands of cities
</Typography>
<Grid container spacing={4}>
<Grid size={{ xs: 12 }}>
<Typography variant="h5" gutterBottom>
Basic Location Input
</Typography>
<LocationInput value={location} onChange={handleLocationChange} required />
</Grid>
<Grid size={{ xs: 12 }}>
<FormControlLabel
control={
<Checkbox
checked={showAdvanced}
onChange={e => setShowAdvanced(e.target.checked)}
color="primary"
/>
}
label="Show city field"
/>
</Grid>
{showAdvanced && (
<Grid size={{ xs: 12 }}>
<Typography variant="h5" gutterBottom>
Advanced Location Input (with City)
</Typography>
<LocationInput
value={location}
onChange={handleLocationChange}
showCity
helperText="Include your city for more specific job matches"
/>
</Grid>
)}
<Grid size={{ xs: 12 }}>
<Typography variant="h6" gutterBottom>
Current Location Data:
</Typography>
<Box
component="pre"
sx={{
bgcolor: 'grey.100',
p: 2,
borderRadius: 1,
overflow: 'auto',
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.
</Typography>
</Grid>
</Grid>
</Box>
);
};
export { LocationInput }; export { LocationInput };

View File

@ -32,7 +32,7 @@ const Mermaid: React.FC<MermaidProps> = (props: MermaidProps) => {
}, [containerRef, setVisible]); }, [containerRef, setVisible]);
useEffect(() => { useEffect(() => {
const renderMermaid = async () => { const renderMermaid = async (): Promise<void> => {
if (containerRef.current && visible && chart) { if (containerRef.current && visible && chart) {
try { try {
await mermaid.initialize(mermaidConfig || defaultMermaidConfig); await mermaid.initialize(mermaidConfig || defaultMermaidConfig);

View File

@ -1,4 +1,4 @@
import { useState, useRef } from 'react'; import React, { JSX, useState } from 'react';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import Accordion from '@mui/material/Accordion'; import Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionSummary from '@mui/material/AccordionSummary';
@ -12,11 +12,9 @@ import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow'; import TableRow from '@mui/material/TableRow';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import CardContent from '@mui/material/CardContent'; import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Collapse from '@mui/material/Collapse'; import Collapse from '@mui/material/Collapse';
import { ExpandMore } from './ExpandMore'; import { ExpandMore } from './ExpandMore';
import JsonView from '@uiw/react-json-view'; import JsonView from '@uiw/react-json-view';
import React from 'react';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
@ -28,9 +26,7 @@ import { ErrorOutline, InfoOutline, Memory, Psychology /* Stream, */ } from '@mu
import { StyledMarkdown } from './StyledMarkdown'; import { StyledMarkdown } from './StyledMarkdown';
import { VectorVisualizer } from './VectorVisualizer'; import { VectorVisualizer } from './VectorVisualizer';
import { SetSnackType } from './Snack';
import { CopyBubble } from './CopyBubble'; import { CopyBubble } from './CopyBubble';
import { Scrollable } from './Scrollable';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { import {
ChatMessage, ChatMessage,
@ -44,7 +40,10 @@ import {
ChatSenderType, ChatSenderType,
} from 'types/types'; } from 'types/types';
const getStyle = (theme: Theme, type: ApiActivityType | ChatSenderType | 'error'): any => { const getStyle = (
theme: Theme,
type: ApiActivityType | ChatSenderType | 'error'
): Record<string, string> => {
const defaultRadius = '16px'; const defaultRadius = '16px';
const defaultStyle = { const defaultStyle = {
padding: theme.spacing(1, 2), padding: theme.spacing(1, 2),
@ -65,7 +64,7 @@ const getStyle = (theme: Theme, type: ApiActivityType | ChatSenderType | 'error'
}, },
}; };
const styles: any = { const styles: Record<string, Record<string, string | object | number | undefined> | string> = {
assistant: { assistant: {
...defaultStyle, ...defaultStyle,
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
@ -172,13 +171,13 @@ const getStyle = (theme: Theme, type: ApiActivityType | ChatSenderType | 'error'
console.log(`Style does not exist for: ${type}`); console.log(`Style does not exist for: ${type}`);
} }
return styles[type]; return styles[type] as Record<string, string>;
}; };
const getIcon = ( const getIcon = (
activityType: ApiActivityType | ChatSenderType | 'error' activityType: ApiActivityType | ChatSenderType | 'error'
): React.ReactNode | null => { ): React.ReactNode | null => {
const icons: any = { const icons: Record<string, React.ReactNode> = {
error: <ErrorOutline color="error" />, error: <ErrorOutline color="error" />,
generating: <LocationSearchingIcon />, generating: <LocationSearchingIcon />,
information: <InfoOutline color="info" />, information: <InfoOutline color="info" />,
@ -207,7 +206,7 @@ interface MessageMetaProps {
messageProps: MessageProps; messageProps: MessageProps;
} }
const MessageMeta = (props: MessageMetaProps) => { const MessageMeta = (props: MessageMetaProps): JSX.Element => {
const { const {
/* MessageData */ /* MessageData */
ragResults = [], ragResults = [],
@ -217,11 +216,11 @@ const MessageMeta = (props: MessageMetaProps) => {
promptEvalCount = 0, promptEvalCount = 0,
promptEvalDuration = 0, promptEvalDuration = 0,
} = props.metadata || {}; } = props.metadata || {};
const message: any = props.messageProps.message; const message: ChatMessage | ChatMessageError | ChatMessageStatus = props.messageProps.message;
let llm_submission = '<|system|>\n'; // let llm_submission = '<|system|>\n';
llm_submission += message.system_prompt + '\n\n'; // llm_submission += message.system_prompt + '\n\n';
llm_submission += message.context_prompt; // llm_submission += message.context_prompt;
return ( return (
<> <>
@ -280,72 +279,74 @@ const MessageMeta = (props: MessageMetaProps) => {
</TableContainer> </TableContainer>
</> </>
)} )}
{tools && tools.tool_calls && tools.tool_calls.length !== 0 && ( {tools && tools.toolCalls && tools.toolCalls.length !== 0 && (
<Accordion sx={{ boxSizing: 'border-box' }}> <Accordion sx={{ boxSizing: 'border-box' }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: '0.8rem' }}>Tools queried</Box> <Box sx={{ fontSize: '0.8rem' }}>Tools queried</Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
{tools.tool_calls.map((tool: any, index: number) => ( {tools.toolCalls.map(
<Box (tool: Record<string, string>, index: number): React.ReactNode => (
key={index}
sx={{
m: 0,
p: 1,
pt: 0,
display: 'flex',
flexDirection: 'column',
border: '1px solid #e0e0e0',
}}
>
{index !== 0 && <Divider />}
<Box <Box
key={index}
sx={{ sx={{
fontSize: '0.75rem', m: 0,
p: 1,
pt: 0,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
mt: 1, border: '1px solid #e0e0e0',
mb: 1,
fontWeight: 'bold',
}} }}
> >
{tool.name} {index !== 0 && <Divider />}
</Box> <Box
{tool.content !== 'null' && ( sx={{
<JsonView fontSize: '0.75rem',
displayDataTypes={false} display: 'flex',
objectSortKeys={true} flexDirection: 'column',
collapsed={1} mt: 1,
value={JSON.parse(tool.content)} mb: 1,
style={{ fontWeight: 'bold',
fontSize: '0.8rem',
maxHeight: '20rem',
overflow: 'auto',
}} }}
> >
<JsonView.String {tool.name}
render={({ children, ...reset }) => { </Box>
if (typeof children === 'string' && children.match('\n')) { {tool.content !== 'null' && (
return ( <JsonView
<pre displayDataTypes={false}
{...reset} objectSortKeys={true}
style={{ collapsed={1}
display: 'flex', value={JSON.parse(tool.content)}
border: 'none', style={{
...reset.style, fontSize: '0.8rem',
}} maxHeight: '20rem',
> overflow: 'auto',
{children}
</pre>
);
}
}} }}
/> >
</JsonView> <JsonView.String
)} render={({ children, ...reset }): React.ReactNode => {
{tool.content === 'null' && 'No response from tool call'} if (typeof children === 'string' && children.match('\n')) {
</Box> return (
))} <pre
{...reset}
style={{
display: 'flex',
border: 'none',
...reset.style,
}}
>
{children}
</pre>
);
}
}}
/>
</JsonView>
)}
{tool.content === 'null' && 'No response from tool call'}
</Box>
)
)}
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
)} )}
@ -369,9 +370,9 @@ const MessageMeta = (props: MessageMetaProps) => {
<Box sx={{ fontSize: '0.8rem' }}>Full Response Details</Box> <Box sx={{ fontSize: '0.8rem' }}>Full Response Details</Box>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Box sx={{ pb: 1 }}> {/* <Box sx={{ pb: 1 }}>
Copy LLM submission: <CopyBubble content={llm_submission} /> Copy LLM submission: <CopyBubble content={llm_submission} />
</Box> </Box> */}
<JsonView <JsonView
displayDataTypes={false} displayDataTypes={false}
objectSortKeys={true} objectSortKeys={true}
@ -380,7 +381,7 @@ const MessageMeta = (props: MessageMetaProps) => {
style={{ fontSize: '0.8rem', maxHeight: '20rem', overflow: 'auto' }} style={{ fontSize: '0.8rem', maxHeight: '20rem', overflow: 'auto' }}
> >
<JsonView.String <JsonView.String
render={({ children, ...reset }) => { render={({ children, ...reset }): React.ReactNode => {
if (typeof children === 'string' && children.match('\n')) { if (typeof children === 'string' && children.match('\n')) {
return ( return (
<pre <pre
@ -412,7 +413,7 @@ interface MessageContainerProps {
copyContent?: string; copyContent?: string;
} }
const MessageContainer = (props: MessageContainerProps) => { const MessageContainer = (props: MessageContainerProps): JSX.Element => {
const { type, sx, messageView, metadataView, copyContent } = props; const { type, sx, messageView, metadataView, copyContent } = props;
const icon = getIcon(type); const icon = getIcon(type);
@ -458,7 +459,7 @@ const MessageContainer = (props: MessageContainerProps) => {
); );
}; };
const Message = (props: MessageProps) => { const Message = (props: MessageProps): JSX.Element => {
const { message, title, sx, className, chatSession, onExpand, expanded, expandable } = props; const { message, title, sx, className, chatSession, onExpand, expanded, expandable } = props;
const [metaExpanded, setMetaExpanded] = useState<boolean>(false); const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
const theme = useTheme(); const theme = useTheme();
@ -468,9 +469,9 @@ const Message = (props: MessageProps) => {
: 'error' in message : 'error' in message
? 'error' ? 'error'
: (message as ChatMessage).role; : (message as ChatMessage).role;
const style: any = getStyle(theme, type); const style = getStyle(theme, type);
const handleMetaExpandClick = () => { const handleMetaExpandClick = (): void => {
setMetaExpanded(!metaExpanded); setMetaExpanded(!metaExpanded);
}; };
@ -563,7 +564,7 @@ const Message = (props: MessageProps) => {
expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled
defaultExpanded={expanded} // Default to collapsed for uncontrolled Accordion defaultExpanded={expanded} // Default to collapsed for uncontrolled Accordion
className={className} className={className}
onChange={(_event, newExpanded) => { onChange={(_event, newExpanded): void => {
isControlled && onExpand && onExpand(newExpanded); isControlled && onExpand && onExpand(newExpanded);
}} }}
sx={{ ...sx, ...style }} sx={{ ...sx, ...style }}

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { JSX } from 'react';
import { Box, Typography, Paper, SxProps } from '@mui/material'; import { Box, Typography, Paper, SxProps } from '@mui/material';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
@ -30,7 +30,7 @@ const QuoteContainer = styled(Paper, {
const QuoteText = styled(Typography, { const QuoteText = styled(Typography, {
shouldForwardProp: prop => prop !== 'size', shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({ })<QuoteContainerProps>(({ size = 'normal' }) => ({
fontSize: size === 'small' ? '0.9rem' : '1.2rem', fontSize: size === 'small' ? '0.9rem' : '1.2rem',
lineHeight: size === 'small' ? 1.4 : 1.6, lineHeight: size === 'small' ? 1.4 : 1.6,
fontStyle: 'italic', fontStyle: 'italic',
@ -44,7 +44,7 @@ const QuoteText = styled(Typography, {
const QuoteMark = styled(Typography, { const QuoteMark = styled(Typography, {
shouldForwardProp: prop => prop !== 'size', shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({ })<QuoteContainerProps>(({ size = 'normal' }) => ({
fontSize: size === 'small' ? '2.5rem' : '4rem', fontSize: size === 'small' ? '2.5rem' : '4rem',
fontFamily: '"Georgia", "Times New Roman", serif', fontFamily: '"Georgia", "Times New Roman", serif',
fontWeight: 'bold', fontWeight: 'bold',
@ -83,7 +83,7 @@ const AuthorText = styled(Typography, {
const AccentLine = styled(Box, { const AccentLine = styled(Box, {
shouldForwardProp: prop => prop !== 'size', shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({ })<QuoteContainerProps>(({ size = 'normal' }) => ({
width: size === 'small' ? '40px' : '60px', width: size === 'small' ? '40px' : '60px',
height: size === 'small' ? '1px' : '2px', height: size === 'small' ? '1px' : '2px',
background: 'linear-gradient(90deg, #D4A017 0%, #4A7A7D 100%)', // Golden Ochre to Dusty Teal background: 'linear-gradient(90deg, #D4A017 0%, #4A7A7D 100%)', // Golden Ochre to Dusty Teal
@ -98,12 +98,12 @@ interface QuoteProps {
sx?: SxProps; sx?: SxProps;
} }
const Quote = (props: QuoteProps) => { const Quote = (props: QuoteProps): JSX.Element => {
const { quote, author, size = 'normal', sx } = props; const { quote, author, size = 'normal', sx } = props;
return ( return (
<QuoteContainer size={size} elevation={0} sx={sx}> <QuoteContainer size={size} elevation={0} sx={sx}>
<OpeningQuote size={size}>"</OpeningQuote> <OpeningQuote size={size}>&quot;</OpeningQuote>
<ClosingQuote size={size}>"</ClosingQuote> <ClosingQuote size={size}>&quot;</ClosingQuote>
<Box sx={{ position: 'relative', zIndex: 2 }}> <Box sx={{ position: 'relative', zIndex: 2 }}>
<QuoteText size={size} variant="body1"> <QuoteText size={size} variant="body1">

View File

@ -1,11 +1,10 @@
import React, { useState, useCallback, useRef, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { Tabs, Tab, Box, Button, Paper, Typography, LinearProgress } from '@mui/material'; import { Tabs, Tab, Box, Button, Paper, Typography, LinearProgress } from '@mui/material';
import { Job, Candidate, SkillAssessment } from 'types/types'; import { Job, Candidate, SkillAssessment } from 'types/types';
import { Scrollable } from './Scrollable'; import { Scrollable } from './Scrollable';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import * as Types from 'types/types'; import * as Types from 'types/types';
import { StyledMarkdown } from './StyledMarkdown'; import { StyledMarkdown } from './StyledMarkdown';
import { Message } from './Message';
import InputIcon from '@mui/icons-material/Input'; import InputIcon from '@mui/icons-material/Input';
import TuneIcon from '@mui/icons-material/Tune'; import TuneIcon from '@mui/icons-material/Tune';
import ArticleIcon from '@mui/icons-material/Article'; import ArticleIcon from '@mui/icons-material/Article';
@ -13,7 +12,6 @@ import { StatusBox, StatusIcon } from './ui/StatusIcon';
import { CopyBubble } from './CopyBubble'; import { CopyBubble } from './CopyBubble';
import { useAppState } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
import { StreamingOptions } from 'services/api-client'; import { StreamingOptions } from 'services/api-client';
import { setDefaultResultOrder } from 'dns';
interface ResumeGeneratorProps { interface ResumeGeneratorProps {
job: Job; job: Job;
@ -22,15 +20,6 @@ interface ResumeGeneratorProps {
onComplete?: (resume: string) => void; onComplete?: (resume: string) => void;
} }
const defaultMessage: Types.ChatMessageStatus = {
status: 'done',
type: 'text',
sessionId: '',
timestamp: new Date(),
content: '',
activity: 'info',
};
const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => { const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorProps) => {
const { job, candidate, skills, onComplete } = props; const { job, candidate, skills, onComplete } = props;
const { setSnack } = useAppState(); const { setSnack } = useAppState();
@ -44,7 +33,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null); const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null);
const [error, setError] = useState<Types.ChatMessageError | null>(null); const [error, setError] = useState<Types.ChatMessageError | null>(null);
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { const handleTabChange = (event: React.SyntheticEvent, newValue: string): void => {
setTabValue(newValue); setTabValue(newValue);
}; };
@ -88,17 +77,32 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
}, },
}; };
const generateResume = async () => { const generateResume = async (): Promise<void> => {
const request: any = await apiClient.generateResume( const request = await apiClient.generateResume(
candidate.id || '', candidate.id || '',
job.id || '', job.id || '',
generateResumeHandlers generateResumeHandlers
); );
const result = await request.promise; await request.promise;
}; };
generateResume(); generateResume();
}, [job, candidate, apiClient, resume, skills, generated, setSystemPrompt, setPrompt, setResume]); }, [
job,
candidate,
apiClient,
resume,
skills,
generated,
status,
setSystemPrompt,
setPrompt,
setResume,
onComplete,
setStatus,
setStatusType,
setError,
]);
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
if (!resume) { if (!resume) {
@ -159,7 +163,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
{tabValue === 'resume' && ( {tabValue === 'resume' && (
<> <>
<CopyBubble <CopyBubble
onClick={() => { onClick={(): void => {
setSnack('Resume copied to clipboard!'); setSnack('Resume copied to clipboard!');
}} }}
sx={{ position: 'absolute', top: 0, right: 0 }} sx={{ position: 'absolute', top: 0, right: 0 }}

View File

@ -1,6 +1,7 @@
import React from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { SxProps, Theme } from '@mui/material'; import { SxProps, Theme } from '@mui/material';
import { RefObject, useRef, forwardRef, useImperativeHandle } from 'react'; import { RefObject, useRef, forwardRef } from 'react';
import { useAutoScrollToBottom } from '../hooks/useAutoScrollToBottom'; import { useAutoScrollToBottom } from '../hooks/useAutoScrollToBottom';
interface ScrollableProps { interface ScrollableProps {
@ -9,27 +10,17 @@ interface ScrollableProps {
autoscroll?: boolean; autoscroll?: boolean;
textFieldRef?: RefObject<HTMLElement | null>; // Reference to the element that triggers auto-scroll textFieldRef?: RefObject<HTMLElement | null>; // Reference to the element that triggers auto-scroll
fallbackThreshold?: number; fallbackThreshold?: number;
contentUpdateTrigger?: any;
className?: string; className?: string;
} }
const Scrollable = forwardRef((props: ScrollableProps, ref) => { const Scrollable = forwardRef((props: ScrollableProps, ref) => {
const { const { sx, className, children, autoscroll, textFieldRef, fallbackThreshold = 0.33 } = props;
sx,
className,
children,
autoscroll,
textFieldRef,
fallbackThreshold = 0.33,
contentUpdateTrigger,
} = props;
// Create a default ref if textFieldRef is not provided // Create a default ref if textFieldRef is not provided
const defaultTextFieldRef = useRef<HTMLElement | null>(null); const defaultTextFieldRef = useRef<HTMLElement | null>(null);
const scrollRef = useAutoScrollToBottom( const scrollRef = useAutoScrollToBottom(
textFieldRef ?? defaultTextFieldRef, textFieldRef ?? defaultTextFieldRef,
true, true,
fallbackThreshold, fallbackThreshold
contentUpdateTrigger
); );
return ( return (
@ -52,5 +43,5 @@ const Scrollable = forwardRef((props: ScrollableProps, ref) => {
</Box> </Box>
); );
}); });
Scrollable.displayName = 'Scrollable';
export { useAutoScrollToBottom, Scrollable }; export { useAutoScrollToBottom, Scrollable };

View File

@ -35,12 +35,15 @@ const Snack = forwardRef<SnackHandle, SnackProps>(({ className, sx }: SnackProps
); );
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
setSnack: (message: string, severity?: SeverityType) => { setSnack: (message: string, severity?: SeverityType): void => {
setSnack(message, severity); setSnack(message, severity);
}, },
})); }));
const handleSnackClose = (event: React.SyntheticEvent | Event, reason?: SnackbarCloseReason) => { const handleSnackClose = (
event: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason
): void => {
if (reason === 'clickaway') { if (reason === 'clickaway') {
return; return;
} }
@ -62,7 +65,7 @@ const Snack = forwardRef<SnackHandle, SnackProps>(({ className, sx }: SnackProps
</Snackbar> </Snackbar>
); );
}); });
Snack.displayName = 'Snack';
export type { SeverityType, SetSnackType }; export type { SeverityType, SetSnackType };
export { Snack }; export { Snack };

View File

@ -1,8 +1,8 @@
import React from 'react'; import React, { JSX } from 'react';
import { MuiMarkdown } from 'mui-markdown'; import { MuiMarkdown, Overrides } from 'mui-markdown';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { Link } from '@mui/material'; import { Link } from '@mui/material';
import { BackstoryQuery, BackstoryQueryInterface } from 'components/BackstoryQuery'; import { BackstoryQuery } from 'components/BackstoryQuery';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import JsonView from '@uiw/react-json-view'; import JsonView from '@uiw/react-json-view';
import { vscodeTheme } from '@uiw/react-json-view/vscode'; import { vscodeTheme } from '@uiw/react-json-view/vscode';
@ -13,7 +13,7 @@ import { GenerateImage } from 'components/GenerateImage';
import './StyledMarkdown.css'; import './StyledMarkdown.css';
import { BackstoryElementProps } from './BackstoryTab'; import { BackstoryElementProps } from './BackstoryTab';
import { CandidateQuestion, ChatQuery, ChatSession } from 'types/types'; import { ChatSession } from 'types/types';
import { ChatSubmitQueryInterface } from 'components/BackstoryQuery'; import { ChatSubmitQueryInterface } from 'components/BackstoryQuery';
interface StyledMarkdownProps extends BackstoryElementProps { interface StyledMarkdownProps extends BackstoryElementProps {
@ -28,21 +28,22 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
const { className, content, chatSession, submitQuery, sx, streaming } = props; const { className, content, chatSession, submitQuery, sx, streaming } = props;
const theme = useTheme(); const theme = useTheme();
const overrides: any = { const overrides: Overrides = {
/* eslint-disable @typescript-eslint/no-explicit-any */
p: { p: {
component: (element: any) => { component: (element: any): JSX.Element => {
return <div>{element.children}</div>; return <div>{element.children}</div>;
}, },
}, },
pre: { pre: {
component: (element: any) => { component: (element: any): JSX.Element => {
const { className } = element.children.props; const { className } = element.children.props;
const content = element.children?.props?.children || ''; const content = element.children?.props?.children || '';
if (className === 'lang-mermaid' && !streaming) { if (className === 'lang-mermaid' && !streaming) {
return <Mermaid className="Mermaid" chart={content} />; return <Mermaid className="Mermaid" chart={content} />;
} }
if (className === 'lang-markdown') { if (className === 'lang-markdown') {
return <MuiMarkdown children={content} />; return <MuiMarkdown>{content}</MuiMarkdown>;
} }
if (className === 'lang-json' && !streaming) { if (className === 'lang-json' && !streaming) {
try { try {
@ -68,7 +69,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
value={fixed} value={fixed}
> >
<JsonView.String <JsonView.String
render={({ children, ...reset }) => { render={({ children, ...reset }): JSX.Element => {
if (typeof children === 'string' && children.match('\n')) { if (typeof children === 'string' && children.match('\n')) {
return ( return (
<pre <pre
@ -83,6 +84,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
</pre> </pre>
); );
} }
return <></>;
}} }}
/> />
</JsonView> </JsonView>
@ -103,10 +105,11 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
); );
}, },
}, },
/* eslint-enable @typescript-eslint/no-explicit-any */
a: { a: {
component: Link, component: Link,
props: { props: {
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => { onClick: (event: React.MouseEvent<HTMLAnchorElement>): void => {
const href = event.currentTarget.getAttribute('href'); const href = event.currentTarget.getAttribute('href');
console.log('StyledMarkdown onClick:', href); console.log('StyledMarkdown onClick:', href);
if (href) { if (href) {
@ -127,14 +130,17 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
}, },
}, },
}, },
GenerateImage: {
component: (_props: { prompt: string }) => <></>,
},
BackstoryQuery: { BackstoryQuery: {
component: (props: { query: string }) => { component: (props: { query: string }) => {
const queryString = props.query.replace(/(\w+):/g, '"$1":'); const queryString = props.query.replace(/(\w+):/g, '"$1":');
try { try {
const query = JSON.parse(queryString); const query = JSON.parse(queryString);
const backstoryQuestion: CandidateQuestion = { // const backstoryQuestion: CandidateQuestion = {
question: queryString, // question: queryString,
}; // };
return submitQuery ? ( return submitQuery ? (
<BackstoryQuery submitQuery={submitQuery} question={query} /> <BackstoryQuery submitQuery={submitQuery} question={query} />
@ -151,13 +157,13 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
if (chatSession) { if (chatSession) {
overrides.GenerateImage = { overrides.GenerateImage = {
component: (props: { prompt: string }) => { component: (props: { prompt: string }): JSX.Element => {
const prompt = props.prompt.replace(/(\w+):/g, '"$1":'); const prompt = props.prompt.replace(/(\w+):/g, '"$1":');
try { try {
return <GenerateImage {...{ chatSession, prompt }} />; return <GenerateImage {...{ chatSession, prompt }} />;
} catch (e) { } catch (e) {
console.log('StyledMarkdown error:', prompt, e); console.log('StyledMarkdown error:', prompt, e);
return props.prompt; return <>{props.prompt}</>;
} }
}, },
}; };
@ -176,7 +182,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
...sx, ...sx,
}} }}
> >
<MuiMarkdown overrides={overrides} children={content} /> <MuiMarkdown overrides={overrides}>{content}</MuiMarkdown>
</Box> </Box>
); );
}; };

View File

@ -22,7 +22,7 @@ import './VectorVisualizer.css';
import { BackstoryPageProps } from './BackstoryTab'; import { BackstoryPageProps } from './BackstoryTab';
import { useAuth } from 'hooks/AuthContext'; import { useAuth } from 'hooks/AuthContext';
import * as Types from 'types/types'; import * as Types from 'types/types';
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext'; import { useAppState } from 'hooks/GlobalContext';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
interface VectorVisualizerProps extends BackstoryPageProps { interface VectorVisualizerProps extends BackstoryPageProps {
@ -30,12 +30,13 @@ interface VectorVisualizerProps extends BackstoryPageProps {
rag?: Types.ChromaDBGetResponse; rag?: Types.ChromaDBGetResponse;
} }
interface Metadata { // interface Metadata {
id: string; // id: string;
docType: string; // docType: string;
content: string; // content: string;
distance?: number; // distance?: number;
} // }
type Metadata = Record<string, string | number>;
const emptyQuerySet: Types.ChromaDBGetResponse = { const emptyQuerySet: Types.ChromaDBGetResponse = {
ids: [], ids: [],
@ -50,13 +51,21 @@ const emptyQuerySet: Types.ChromaDBGetResponse = {
}; };
interface PlotData { interface PlotData {
name: string;
mode: string;
type: string;
x: number[]; x: number[];
y: number[]; y: number[];
z?: number[]; z?: number[];
colors: string[];
text: string[]; text: string[];
sizes: number[]; marker: {
color: string[];
size: number[];
symbol: string;
opacity: number;
};
customdata: Metadata[]; customdata: Metadata[];
hovertemplate: string;
} }
const config: Partial<Plotly.Config> = { const config: Partial<Plotly.Config> = {
@ -184,7 +193,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
const { user, apiClient } = useAuth(); const { user, apiClient } = useAuth();
const { rag, inline, sx } = props; const { rag, inline, sx } = props;
const { setSnack } = useAppState(); const { setSnack } = useAppState();
const [plotData, setPlotData] = useState<PlotData | null>(null); const [plotData, setPlotData] = useState<PlotData[] | null>(null);
const [newQuery, setNewQuery] = useState<string>(''); const [newQuery, setNewQuery] = useState<string>('');
const [querySet, setQuerySet] = useState<Types.ChromaDBGetResponse>(rag || emptyQuerySet); const [querySet, setQuerySet] = useState<Types.ChromaDBGetResponse>(rag || emptyQuerySet);
const [result, setResult] = useState<Types.ChromaDBGetResponse | null>(null); const [result, setResult] = useState<Types.ChromaDBGetResponse | null>(null);
@ -206,7 +215,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
if (!boxRef.current) { if (!boxRef.current) {
return; return;
} }
const resize = () => { const resize = (): void => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const plotContainer = document.querySelector('.plot-container') as HTMLElement; const plotContainer = document.querySelector('.plot-container') as HTMLElement;
const svgContainer = document?.querySelector('.svg-container') as HTMLElement; const svgContainer = document?.querySelector('.svg-container') as HTMLElement;
@ -234,7 +243,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
if (result) { if (result) {
return; return;
} }
const fetchCollection = async () => { const fetchCollection = async (): Promise<void> => {
if (!candidate) { if (!candidate) {
return; return;
} }
@ -248,7 +257,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
}; };
fetchCollection(); fetchCollection();
}, [result, setSnack, view2D]); }, [result, setSnack, view2D, apiClient, candidate]);
useEffect(() => { useEffect(() => {
if (!result || !result.embeddings) return; if (!result || !result.embeddings) return;
@ -349,7 +358,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
const query_docTypes = query.metadatas.map(m => m.docType || 'unknown'); const query_docTypes = query.metadatas.map(m => m.docType || 'unknown');
const has_query = query.metadatas.length > 0; const has_query = query.metadatas.length > 0;
const filtered_sizes = filtered.metadatas.map(m => const filtered_sizes = filtered.metadatas.map(_m =>
has_query ? DEFAULT_UNFOCUS_SIZE : DEFAULT_SIZE has_query ? DEFAULT_UNFOCUS_SIZE : DEFAULT_SIZE
); );
const filtered_colors = filtered_docTypes.map(type => colorMap[type] || '#4d4d4d'); const filtered_colors = filtered_docTypes.map(type => colorMap[type] || '#4d4d4d');
@ -369,7 +378,7 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
? normalizeDimension(query.embeddings.map((v: number[]) => v[2])) ? normalizeDimension(query.embeddings.map((v: number[]) => v[2]))
: undefined; : undefined;
const data: any = [ const data: PlotData[] = [
{ {
name: 'All data', name: 'All data',
x: filtered_x, x: filtered_x,
@ -412,13 +421,13 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
setPlotData(data); setPlotData(data);
}, [result, querySet, view2D]); }, [result, querySet, view2D]);
const handleKeyPress = (event: any) => { const handleKeyPress = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
sendQuery(newQuery); sendQuery(newQuery);
} }
}; };
const sendQuery = async (query: string) => { const sendQuery = async (query: string): Promise<void> => {
if (!query.trim()) return; if (!query.trim()) return;
setNewQuery(''); setNewQuery('');
@ -458,12 +467,19 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
> >
<div> <div>
No candidate selected. Please{' '} No candidate selected. Please{' '}
<Button onClick={() => navigate('/find-a-candidate')}>select a candidate</Button> first. <Button
onClick={(): void => {
navigate('/find-a-candidate');
}}
>
select a candidate
</Button>{' '}
first.
</div> </div>
</Box> </Box>
); );
const fetchRAGMeta = async (node: Node) => { const fetchRAGMeta = async (node: Node): Promise<void> => {
try { try {
const result = await apiClient.getCandidateRAGContent(node.id); const result = await apiClient.getCandidateRAGContent(node.id);
const update: Node = { const update: Node = {
@ -478,8 +494,8 @@ const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualiz
} }
}; };
const onNodeSelected = (metadata: any) => { const onNodeSelected = (metadata: Metadata): void => {
let node: Node; let node: Partial<Node>;
console.log(metadata); console.log(metadata);
if (metadata.docType === 'query') { if (metadata.docType === 'query') {
node = { node = {
@ -503,7 +519,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
backgroundColor: colorMap[metadata.docType] || '#ff8080', backgroundColor: colorMap[metadata.docType] || '#ff8080',
}, },
}; };
setNode(node); setNode(node as Node);
return; return;
} }
@ -513,9 +529,9 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
emoji: emojiMap[metadata.docType] || '❓', emoji: emojiMap[metadata.docType] || '❓',
}; };
setNode(node); setNode(node as Node);
fetchRAGMeta(node); fetchRAGMeta(node as Node);
}; };
return ( return (
@ -552,7 +568,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
flexGrow: 0, flexGrow: 0,
}} }}
control={<Switch checked={!view2D} />} control={<Switch checked={!view2D} />}
onChange={() => { onChange={(): void => {
setView2D(!view2D); setView2D(!view2D);
setResult(null); setResult(null);
}} }}
@ -560,7 +576,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
/> />
<Plot <Plot
ref={plotlyRef} ref={plotlyRef}
onClick={(event: any) => { onClick={(event: { points: { customdata: Metadata }[] }): void => {
onNodeSelected(event.points[0].customdata); onNodeSelected(event.points[0].customdata);
}} }}
data={plotData} data={plotData}
@ -775,7 +791,9 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
fullWidth fullWidth
type="text" type="text"
value={newQuery} value={newQuery}
onChange={e => setNewQuery(e.target.value)} onChange={(e): void => {
setNewQuery(e.target.value);
}}
onKeyDown={handleKeyPress} onKeyDown={handleKeyPress}
placeholder="Enter query to find related documents..." placeholder="Enter query to find related documents..."
id="QueryInput" id="QueryInput"
@ -784,7 +802,7 @@ The scatter graph shows the query in N-dimensional space, mapped to ${
<Button <Button
sx={{ m: 1 }} sx={{ m: 1 }}
variant="contained" variant="contained"
onClick={() => { onClick={(): void => {
sendQuery(newQuery); sendQuery(newQuery);
}} }}
> >

View File

@ -1,5 +1,5 @@
// components/layout/BackstoryLayout.tsx // components/layout/BackstoryLayout.tsx
import React, { ReactElement, useEffect, useState } from 'react'; import React, { JSX, ReactElement, useEffect, useState } from 'react';
import { Outlet, useLocation, Routes, Route } from 'react-router-dom'; import { Outlet, useLocation, Routes, Route } from 'react-router-dom';
import { Box, Container, Paper } from '@mui/material'; import { Box, Container, Paper } from '@mui/material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -8,19 +8,17 @@ import { darken } from '@mui/material/styles';
import { Header } from 'components/layout/Header'; import { Header } from 'components/layout/Header';
import { Scrollable } from 'components/Scrollable'; import { Scrollable } from 'components/Scrollable';
import { Footer } from 'components/layout/Footer'; import { Footer } from 'components/layout/Footer';
import { Snack, SetSnackType } from 'components/Snack';
import { User } from 'types/types';
import { LoadingComponent } from 'components/LoadingComponent'; import { LoadingComponent } from 'components/LoadingComponent';
import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext'; import { useAuth } 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'; import { NavigationItem } from 'types/navigation';
import { ConversationHandle } from 'components/Conversation';
// Legacy type for backward compatibility // Legacy type for backward compatibility
export type NavigationLinkType = { export type NavigationLinkType = {
label: ReactElement<any> | string; label: JSX.Element | string;
path: string; path: string;
icon?: ReactElement<any>; icon?: JSX.Element | string;
}; };
interface BackstoryPageContainerProps { interface BackstoryPageContainerProps {
@ -28,7 +26,7 @@ interface BackstoryPageContainerProps {
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
} }
const BackstoryPageContainer = (props: BackstoryPageContainerProps) => { const BackstoryPageContainer = (props: BackstoryPageContainerProps): JSX.Element => {
const { children, sx } = props; const { children, sx } = props;
return ( return (
<Container <Container
@ -76,12 +74,11 @@ const BackstoryPageContainer = (props: BackstoryPageContainerProps) => {
interface BackstoryLayoutProps { interface BackstoryLayoutProps {
page: string; page: string;
chatRef: React.Ref<any>; chatRef: React.Ref<ConversationHandle>;
} }
const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => { const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => {
const { page, chatRef } = props; const { page, chatRef } = props;
const { setSnack } = useAppState();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { guest, user } = useAuth(); const { guest, user } = useAuth();
@ -93,7 +90,7 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
}, [user]); }, [user]);
// Generate dynamic routes from navigation config // Generate dynamic routes from navigation config
const generateRoutes = () => { const generateRoutes = (): React.ReactNode | null => {
if (!guest && !user) return null; if (!guest && !user) return null;
const userType = user?.userType || null; const userType = user?.userType || null;

View File

@ -7,7 +7,6 @@ import { ConversationHandle } from '../Conversation';
import { User } from 'types/types'; import { User } from 'types/types';
import { getAllRoutes } from 'config/navigationConfig'; import { getAllRoutes } from 'config/navigationConfig';
import { NavigationItem } from 'types/navigation'; import { NavigationItem } from 'types/navigation';
import { useAppState } from 'hooks/GlobalContext';
interface BackstoryDynamicRoutesProps extends BackstoryPageProps { interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
chatRef: Ref<ConversationHandle>; chatRef: Ref<ConversationHandle>;

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { JSX } from 'react';
import { import {
Paper, Paper,
Box, Box,
@ -13,11 +13,11 @@ import {
} from '@mui/material'; } from '@mui/material';
import { styled, useTheme } from '@mui/material/styles'; import { styled, useTheme } from '@mui/material/styles';
import { import {
Facebook, // Facebook,
Twitter, // Twitter,
LinkedIn, LinkedIn,
Instagram, // Instagram,
YouTube, // YouTube,
Email, Email,
LocationOn, LocationOn,
Copyright, Copyright,
@ -56,7 +56,7 @@ const ContactItem = styled(Box)(({ theme }) => ({
})); }));
// Footer component // Footer component
const Footer = () => { const Footer = (): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md')); const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
@ -125,9 +125,9 @@ const Footer = () => {
color: theme.palette.action.active, color: theme.palette.action.active,
}, },
}} }}
onClick={() => onClick={(): void => {
window.open('https://www.linkedin.com/in/james-ketrenos/', '_blank') window.open('https://www.linkedin.com/in/james-ketrenos/', '_blank');
} }}
> >
<LinkedIn /> <LinkedIn />
</IconButton> </IconButton>

View File

@ -108,7 +108,6 @@ const useAutoScrollToBottom = (
scrollToRef: RefObject<HTMLElement | null>, scrollToRef: RefObject<HTMLElement | null>,
smooth = true, smooth = true,
fallbackThreshold = 0.33, fallbackThreshold = 0.33,
contentUpdateTrigger?: any
): RefObject<HTMLDivElement | null> => { ): RefObject<HTMLDivElement | null> => {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const lastScrollTop = useRef(0); const lastScrollTop = useRef(0);
@ -243,7 +242,7 @@ const useAutoScrollToBottom = (
} }
if (scrollTimeout.current) clearTimeout(scrollTimeout.current); if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
}; };
}, [smooth, scrollToRef, fallbackThreshold, contentUpdateTrigger, checkAndScrollToBottom]); }, [smooth, scrollToRef, fallbackThreshold, checkAndScrollToBottom]);
// Observe container and TextField size, plus DOM changes // Observe container and TextField size, plus DOM changes
useResizeObserverAndMutationObserver(containerRef, scrollToRef, checkAndScrollToBottom); useResizeObserverAndMutationObserver(containerRef, scrollToRef, checkAndScrollToBottom);

View File

@ -1,137 +1,89 @@
// Generated TypeScript types from Pydantic models // Generated TypeScript types from Pydantic models
// Source: src/backend/models.py // Source: src/backend/models.py
// Generated on: 2025-06-13T18:33:37.696910 // Generated on: 2025-06-18T22:54:34.823060
// DO NOT EDIT MANUALLY - This file is auto-generated // DO NOT EDIT MANUALLY - This file is auto-generated
// ============================ // ============================
// Enums // Enums
// ============================ // ============================
export type AIModelType = 'qwen2.5' | 'flux-schnell'; export type AIModelType = "qwen2.5" | "flux-schnell";
export type ActivityType = export type ActivityType = "login" | "search" | "view_job" | "apply_job" | "message" | "update_profile" | "chat";
| 'login'
| 'search'
| 'view_job'
| 'apply_job'
| 'message'
| 'update_profile'
| 'chat';
export type ApiActivityType = export type ApiActivityType = "system" | "info" | "searching" | "thinking" | "generating" | "converting" | "generating_image" | "tooling" | "heartbeat";
| '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 = export type ApplicationStatus = "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn";
| 'applied'
| 'reviewing'
| 'interview'
| 'offer'
| 'rejected'
| 'accepted'
| 'withdrawn';
export type ChatContextType = 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";
| '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 = export type ProcessingStepType = "extract" | "transform" | "chunk" | "embed" | "filter" | "summarize";
| '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 = export type SocialPlatform = "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other";
| 'linkedin'
| 'twitter'
| 'github'
| 'dribbble'
| 'behance'
| 'website'
| 'other';
export type SortOrder = 'asc' | 'desc'; export type SortOrder = "asc" | "desc";
export type ThemePreference = 'light' | 'dark' | 'system'; export type ThemePreference = "light" | "dark" | "system";
export type UserGender = 'female' | 'male'; export type UserGender = "female" | "male";
export type UserStatus = 'active' | 'inactive' | 'pending' | 'banned'; export type UserStatus = "active" | "inactive" | "pending" | "banned";
export type UserType = 'candidate' | 'employer' | 'guest'; export type UserType = "candidate" | "employer" | "guest";
export type VectorStoreType = 'chroma'; export type VectorStoreType = "chroma";
// ============================ // ============================
// Interfaces // Interfaces
// ============================ // ============================
export interface AccessibilitySettings { export interface AccessibilitySettings {
fontSize: 'small' | 'medium' | 'large'; fontSize: "small" | "medium" | "large";
highContrast: boolean; highContrast: boolean;
reduceMotion: boolean; reduceMotion: boolean;
screenReader: boolean; screenReader: boolean;
colorBlindMode?: 'protanopia' | 'deuteranopia' | 'tritanopia' | 'none'; colorBlindMode?: "protanopia" | "deuteranopia" | "tritanopia" | "none";
} }
export interface Analytics { export interface Analytics {
id?: string; id?: string;
entityType: 'job' | 'candidate' | 'chat' | 'system' | 'employer'; entityType: "job" | "candidate" | "chat" | "system" | "employer";
entityId: string; entityId: string;
metricType: string; metricType: string;
value: number; value: number;
@ -144,8 +96,8 @@ export interface ApiMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
} }
@ -157,7 +109,7 @@ export interface ApiResponse {
} }
export interface ApplicationDecision { export interface ApplicationDecision {
status: 'accepted' | 'rejected'; status: "accepted" | "rejected";
reason?: string; reason?: string;
date: Date; date: Date;
by: string; by: string;
@ -193,14 +145,14 @@ export interface Authentication {
resetPasswordExpiry?: Date; resetPasswordExpiry?: Date;
lastPasswordChange: Date; lastPasswordChange: Date;
mfaEnabled: boolean; mfaEnabled: boolean;
mfaMethod?: 'app' | 'sms' | 'email'; mfaMethod?: "app" | "sms" | "email";
mfaSecret?: string; mfaSecret?: string;
loginAttempts: number; loginAttempts: number;
lockedUntil?: Date; lockedUntil?: Date;
} }
export interface BaseUser { export interface BaseUser {
userType: 'candidate' | 'employer' | 'guest'; userType: "candidate" | "employer" | "guest";
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
email: string; email: string;
@ -213,18 +165,18 @@ export interface BaseUser {
updatedAt?: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: 'active' | 'inactive' | 'pending' | 'banned'; status: "active" | "inactive" | "pending" | "banned";
isAdmin: boolean; isAdmin: boolean;
} }
export interface BaseUserWithType { export interface BaseUserWithType {
userType: 'candidate' | 'employer' | 'guest'; userType: "candidate" | "employer" | "guest";
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
} }
export interface Candidate { export interface Candidate {
userType: 'candidate' | 'employer' | 'guest'; userType: "candidate" | "employer" | "guest";
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
email: string; email: string;
@ -237,7 +189,7 @@ export interface Candidate {
updatedAt?: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: 'active' | 'inactive' | 'pending' | 'banned'; status: "active" | "inactive" | "pending" | "banned";
isAdmin: boolean; isAdmin: boolean;
username: string; username: string;
description?: string; description?: string;
@ -246,7 +198,7 @@ export interface Candidate {
experience?: Array<WorkExperience>; experience?: Array<WorkExperience>;
questions?: Array<CandidateQuestion>; questions?: Array<CandidateQuestion>;
education?: Array<Education>; education?: Array<Education>;
preferredJobTypes?: Array<'full-time' | 'part-time' | 'contract' | 'internship' | 'freelance'>; preferredJobTypes?: Array<"full-time" | "part-time" | "contract" | "internship" | "freelance">;
desiredSalary?: DesiredSalary; desiredSalary?: DesiredSalary;
availabilityDate?: Date; availabilityDate?: Date;
summary?: string; summary?: string;
@ -259,7 +211,7 @@ export interface Candidate {
} }
export interface CandidateAI { export interface CandidateAI {
userType: 'candidate' | 'employer' | 'guest'; userType: "candidate" | "employer" | "guest";
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
email: string; email: string;
@ -272,7 +224,7 @@ export interface CandidateAI {
updatedAt?: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: 'active' | 'inactive' | 'pending' | 'banned'; status: "active" | "inactive" | "pending" | "banned";
isAdmin: boolean; isAdmin: boolean;
username: string; username: string;
description?: string; description?: string;
@ -281,7 +233,7 @@ export interface CandidateAI {
experience?: Array<WorkExperience>; experience?: Array<WorkExperience>;
questions?: Array<CandidateQuestion>; questions?: Array<CandidateQuestion>;
education?: Array<Education>; education?: Array<Education>;
preferredJobTypes?: Array<'full-time' | 'part-time' | 'contract' | 'internship' | 'freelance'>; preferredJobTypes?: Array<"full-time" | "part-time" | "contract" | "internship" | "freelance">;
desiredSalary?: DesiredSalary; desiredSalary?: DesiredSalary;
availabilityDate?: Date; availabilityDate?: Date;
summary?: string; summary?: string;
@ -293,7 +245,7 @@ export interface CandidateAI {
isPublic: boolean; isPublic: boolean;
isAI: boolean; isAI: boolean;
age?: number; age?: number;
gender?: 'female' | 'male'; gender?: "female" | "male";
ethnicity?: string; ethnicity?: string;
} }
@ -332,21 +284,9 @@ export interface Certification {
} }
export interface ChatContext { export interface ChatContext {
type: type: "job_search" | "job_requirements" | "candidate_chat" | "interview_prep" | "resume_review" | "general" | "generate_persona" | "generate_profile" | "generate_resume" | "generate_image" | "rag_search" | "skill_match";
| 'job_search'
| 'job_requirements'
| 'candidate_chat'
| 'interview_prep'
| 'resume_review'
| 'general'
| 'generate_persona'
| 'generate_profile'
| 'generate_resume'
| 'generate_image'
| 'rag_search'
| 'skill_match';
relatedEntityId?: string; relatedEntityId?: string;
relatedEntityType?: 'job' | 'candidate' | 'employer'; relatedEntityType?: "job" | "candidate" | "employer";
additionalContext?: Record<string, any>; additionalContext?: Record<string, any>;
} }
@ -354,10 +294,10 @@ export interface ChatMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
role: 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error'; role: "user" | "assistant" | "system" | "information" | "warning" | "error";
content: string; content: string;
tunables?: Tunables; tunables?: Tunables;
metadata: ChatMessageMetaData; metadata: ChatMessageMetaData;
@ -367,14 +307,14 @@ export interface ChatMessageError {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
content: string; content: string;
} }
export interface ChatMessageMetaData { export interface ChatMessageMetaData {
model: 'qwen2.5' | 'flux-schnell'; model: "qwen2.5" | "flux-schnell";
temperature: number; temperature: number;
maxTokens: number; maxTokens: number;
topP: number; topP: number;
@ -388,7 +328,7 @@ export interface ChatMessageMetaData {
promptEvalCount: number; promptEvalCount: number;
promptEvalDuration: number; promptEvalDuration: number;
options?: ChatOptions; options?: ChatOptions;
tools?: Record<string, any>; tools?: Tool;
timers?: Record<string, number>; timers?: Record<string, number>;
} }
@ -396,8 +336,8 @@ export interface ChatMessageRagSearch {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
dimensions: number; dimensions: number;
content: Array<ChromaDBGetResponse>; content: Array<ChromaDBGetResponse>;
@ -407,10 +347,10 @@ export interface ChatMessageResume {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
role: 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error'; role: "user" | "assistant" | "system" | "information" | "warning" | "error";
content: string; content: string;
tunables?: Tunables; tunables?: Tunables;
metadata: ChatMessageMetaData; metadata: ChatMessageMetaData;
@ -423,10 +363,10 @@ export interface ChatMessageSkillAssessment {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
role: 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error'; role: "user" | "assistant" | "system" | "information" | "warning" | "error";
content: string; content: string;
tunables?: Tunables; tunables?: Tunables;
metadata: ChatMessageMetaData; metadata: ChatMessageMetaData;
@ -437,19 +377,10 @@ export interface ChatMessageStatus {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
activity: activity: "system" | "info" | "searching" | "thinking" | "generating" | "converting" | "generating_image" | "tooling" | "heartbeat";
| 'system'
| 'info'
| 'searching'
| 'thinking'
| 'generating'
| 'converting'
| 'generating_image'
| 'tooling'
| 'heartbeat';
content: any; content: any;
} }
@ -457,8 +388,8 @@ export interface ChatMessageStreaming {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
content: string; content: string;
} }
@ -467,10 +398,10 @@ export interface ChatMessageUser {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
role: 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error'; role: "user" | "assistant" | "system" | "information" | "warning" | "error";
content: string; content: string;
tunables?: Tunables; tunables?: Tunables;
} }
@ -490,6 +421,7 @@ export interface ChatQuery {
export interface ChatSession { export interface ChatSession {
id?: string; id?: string;
userId?: string; userId?: string;
guestId?: string;
createdAt?: Date; createdAt?: Date;
lastActivity?: Date; lastActivity?: Date;
title?: string; title?: string;
@ -543,12 +475,12 @@ export interface DataSourceConfiguration {
id?: string; id?: string;
ragConfigId: string; ragConfigId: string;
name: string; name: string;
sourceType: 'document' | 'website' | 'api' | 'database' | 'internal'; sourceType: "document" | "website" | "api" | "database" | "internal";
connectionDetails: Record<string, any>; connectionDetails: Record<string, any>;
processingPipeline: Array<ProcessingStep>; processingPipeline: Array<ProcessingStep>;
refreshSchedule?: string; refreshSchedule?: string;
lastRefreshed?: Date; lastRefreshed?: Date;
status: 'active' | 'pending' | 'error' | 'processing'; status: "active" | "pending" | "error" | "processing";
errorDetails?: string; errorDetails?: string;
metadata?: Record<string, any>; metadata?: Record<string, any>;
} }
@ -556,7 +488,7 @@ export interface DataSourceConfiguration {
export interface DesiredSalary { export interface DesiredSalary {
amount: number; amount: number;
currency: string; currency: string;
period: 'hour' | 'day' | 'month' | 'year'; period: "hour" | "day" | "month" | "year";
} }
export interface Document { export interface Document {
@ -564,7 +496,7 @@ export interface Document {
ownerId: string; ownerId: string;
filename: string; filename: string;
originalName: string; originalName: string;
type: 'pdf' | 'docx' | 'txt' | 'markdown' | 'image'; type: "pdf" | "docx" | "txt" | "markdown" | "image";
size: number; size: number;
uploadDate?: Date; uploadDate?: Date;
options?: DocumentOptions; options?: DocumentOptions;
@ -574,7 +506,7 @@ export interface Document {
export interface DocumentContentResponse { export interface DocumentContentResponse {
documentId: string; documentId: string;
filename: string; filename: string;
type: 'pdf' | 'docx' | 'txt' | 'markdown' | 'image'; type: "pdf" | "docx" | "txt" | "markdown" | "image";
content: string; content: string;
size: number; size: number;
} }
@ -588,8 +520,8 @@ export interface DocumentMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
document: Document; document: Document;
content?: string; content?: string;
@ -631,7 +563,7 @@ export interface EmailVerificationRequest {
} }
export interface Employer { export interface Employer {
userType: 'candidate' | 'employer' | 'guest'; userType: "candidate" | "employer" | "guest";
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
email: string; email: string;
@ -644,7 +576,7 @@ export interface Employer {
updatedAt?: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: 'active' | 'inactive' | 'pending' | 'banned'; status: "active" | "inactive" | "pending" | "banned";
isAdmin: boolean; isAdmin: boolean;
companyName: string; companyName: string;
industry: string; industry: string;
@ -684,7 +616,7 @@ export interface GPUInfo {
} }
export interface Guest { export interface Guest {
userType: 'candidate' | 'employer' | 'guest'; userType: "candidate" | "employer" | "guest";
id?: string; id?: string;
lastActivity?: Date; lastActivity?: Date;
email: string; email: string;
@ -697,7 +629,7 @@ export interface Guest {
updatedAt?: Date; updatedAt?: Date;
lastLogin?: Date; lastLogin?: Date;
profileImage?: string; profileImage?: string;
status: 'active' | 'inactive' | 'pending' | 'banned'; status: "active" | "inactive" | "pending" | "banned";
isAdmin: boolean; isAdmin: boolean;
sessionId: string; sessionId: string;
username: string; username: string;
@ -713,7 +645,7 @@ export interface GuestCleanupRequest {
} }
export interface GuestConversionRequest { export interface GuestConversionRequest {
accountType: 'candidate' | 'employer'; accountType: "candidate" | "employer";
email: string; email: string;
username: string; username: string;
password: string; password: string;
@ -732,7 +664,7 @@ export interface GuestSessionResponse {
refreshToken: string; refreshToken: string;
user: Guest; user: Guest;
expiresAt: number; expiresAt: number;
userType: 'guest'; userType: "guest";
isGuest: boolean; isGuest: boolean;
} }
@ -754,7 +686,7 @@ export interface InterviewFeedback {
overallScore: number; overallScore: number;
strengths: Array<string>; strengths: Array<string>;
weaknesses: Array<string>; weaknesses: Array<string>;
recommendation: 'strong_hire' | 'hire' | 'no_hire' | 'strong_no_hire'; recommendation: "strong_hire" | "hire" | "no_hire" | "strong_no_hire";
comments: string; comments: string;
createdAt?: Date; createdAt?: Date;
updatedAt?: Date; updatedAt?: Date;
@ -767,19 +699,19 @@ export interface InterviewSchedule {
applicationId: string; applicationId: string;
scheduledDate: Date; scheduledDate: Date;
endDate: Date; endDate: Date;
interviewType: 'phone' | 'video' | 'onsite' | 'technical' | 'behavioral'; interviewType: "phone" | "video" | "onsite" | "technical" | "behavioral";
interviewers: Array<string>; interviewers: Array<string>;
location?: string | Location; location?: string | Location;
notes?: string; notes?: string;
feedback?: InterviewFeedback; feedback?: InterviewFeedback;
status: 'scheduled' | 'completed' | 'cancelled' | 'rescheduled'; status: "scheduled" | "completed" | "cancelled" | "rescheduled";
meetingLink?: string; meetingLink?: string;
} }
export interface Job { export interface Job {
id?: string; id?: string;
ownerId: string; ownerId: string;
ownerType: 'candidate' | 'employer' | 'guest'; ownerType: "candidate" | "employer" | "guest";
owner?: BaseUser; owner?: BaseUser;
title?: string; title?: string;
summary?: string; summary?: string;
@ -795,7 +727,7 @@ export interface JobApplication {
id?: string; id?: string;
jobId: string; jobId: string;
candidateId: string; candidateId: string;
status: 'applied' | 'reviewing' | 'interview' | 'offer' | 'rejected' | 'accepted' | 'withdrawn'; status: "applied" | "reviewing" | "interview" | "offer" | "rejected" | "accepted" | "withdrawn";
appliedDate: Date; appliedDate: Date;
updatedDate: Date; updatedDate: Date;
resumeVersion: string; resumeVersion: string;
@ -810,7 +742,7 @@ export interface JobApplication {
export interface JobDetails { export interface JobDetails {
location: Location; location: Location;
salaryRange?: SalaryRange; salaryRange?: SalaryRange;
employmentType: 'full-time' | 'part-time' | 'contract' | 'internship' | 'freelance'; employmentType: "full-time" | "part-time" | "contract" | "internship" | "freelance";
datePosted?: Date; datePosted?: Date;
applicationDeadline?: Date; applicationDeadline?: Date;
isActive: boolean; isActive: boolean;
@ -846,8 +778,8 @@ export interface JobRequirementsMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
job: Job; job: Job;
} }
@ -861,7 +793,7 @@ export interface JobResponse {
export interface Language { export interface Language {
language: string; language: string;
proficiency: 'basic' | 'conversational' | 'fluent' | 'native'; proficiency: "basic" | "conversational" | "fluent" | "native";
} }
export interface Location { export interface Location {
@ -916,7 +848,7 @@ export interface MessageReaction {
} }
export interface NotificationPreference { export interface NotificationPreference {
type: 'email' | 'push' | 'in_app'; type: "email" | "push" | "in_app";
events: Array<string>; events: Array<string>;
isEnabled: boolean; isEnabled: boolean;
} }
@ -925,7 +857,7 @@ export interface PaginatedRequest {
page: number; page: number;
limit: number; limit: number;
sortBy?: string; sortBy?: string;
sortOrder?: 'asc' | 'desc'; sortOrder?: "asc" | "desc";
filters?: Record<string, any>; filters?: Record<string, any>;
} }
@ -947,7 +879,7 @@ export interface PointOfContact {
export interface ProcessingStep { export interface ProcessingStep {
id?: string; id?: string;
type: 'extract' | 'transform' | 'chunk' | 'embed' | 'filter' | 'summarize'; type: "extract" | "transform" | "chunk" | "embed" | "filter" | "summarize";
parameters: Record<string, any>; parameters: Record<string, any>;
order: number; order: number;
dependsOn?: Array<string>; dependsOn?: Array<string>;
@ -960,7 +892,7 @@ export interface RAGConfiguration {
description?: string; description?: string;
dataSourceConfigurations: Array<DataSourceConfiguration>; dataSourceConfigurations: Array<DataSourceConfiguration>;
embeddingModel: string; embeddingModel: string;
vectorStoreType: 'chroma'; vectorStoreType: "chroma";
retrievalParameters: RetrievalParameters; retrievalParameters: RetrievalParameters;
createdAt?: Date; createdAt?: Date;
updatedAt?: Date; updatedAt?: Date;
@ -968,6 +900,10 @@ export interface RAGConfiguration {
isActive: boolean; isActive: boolean;
} }
export interface RAGDocumentRequest {
id: string;
}
export interface RagContentMetadata { export interface RagContentMetadata {
sourceFile: string; sourceFile: string;
lineBegin: number; lineBegin: number;
@ -1050,17 +986,17 @@ export interface ResumeMessage {
id?: string; id?: string;
sessionId: string; sessionId: string;
senderId?: string; senderId?: string;
status: 'streaming' | 'status' | 'done' | 'error'; status: "streaming" | "status" | "done" | "error";
type: 'binary' | 'text' | 'json'; type: "binary" | "text" | "json";
timestamp?: Date; timestamp?: Date;
role: 'user' | 'assistant' | 'system' | 'information' | 'warning' | 'error'; role: "user" | "assistant" | "system" | "information" | "warning" | "error";
content: string; content: string;
tunables?: Tunables; tunables?: Tunables;
resume: Resume; resume: Resume;
} }
export interface RetrievalParameters { export interface RetrievalParameters {
searchType: 'similarity' | 'mmr' | 'hybrid' | 'keyword'; searchType: "similarity" | "mmr" | "hybrid" | "keyword";
topK: number; topK: number;
similarityThreshold?: number; similarityThreshold?: number;
rerankerModel?: string; rerankerModel?: string;
@ -1073,7 +1009,7 @@ export interface SalaryRange {
min: number; min: number;
max: number; max: number;
currency: string; currency: string;
period: 'hour' | 'day' | 'month' | 'year'; period: "hour" | "day" | "month" | "year";
isVisible: boolean; isVisible: boolean;
} }
@ -1083,14 +1019,14 @@ export interface SearchQuery {
page: number; page: number;
limit: number; limit: number;
sortBy?: string; sortBy?: string;
sortOrder?: 'asc' | 'desc'; sortOrder?: "asc" | "desc";
} }
export interface Skill { export interface Skill {
id?: string; id?: string;
name: string; name: string;
category: string; category: string;
level: 'beginner' | 'intermediate' | 'advanced' | 'expert'; level: "beginner" | "intermediate" | "advanced" | "expert";
yearsOfExperience?: number; yearsOfExperience?: number;
} }
@ -1099,7 +1035,7 @@ export interface SkillAssessment {
skill: string; skill: string;
skillModified?: string; skillModified?: string;
evidenceFound: boolean; evidenceFound: boolean;
evidenceStrength: 'strong' | 'moderate' | 'weak' | 'none'; evidenceStrength: "strong" | "moderate" | "weak" | "none";
assessment: string; assessment: string;
description: string; description: string;
evidenceDetails?: Array<EvidenceDetail>; evidenceDetails?: Array<EvidenceDetail>;
@ -1109,7 +1045,7 @@ export interface SkillAssessment {
} }
export interface SocialLink { export interface SocialLink {
platform: 'linkedin' | 'twitter' | 'github' | 'dribbble' | 'behance' | 'website' | 'other'; platform: "linkedin" | "twitter" | "github" | "dribbble" | "behance" | "website" | "other";
url: string; url: string;
} }
@ -1122,6 +1058,18 @@ export interface SystemInfo {
maxContextLength: number; maxContextLength: number;
} }
export interface Tool {
toolCalls: Array<Record<string, string>>;
messages?: Array<any>;
available?: Array<string>;
toolName: string;
}
export interface ToolCall {
name: string;
content: string;
}
export interface Tunables { export interface Tunables {
enableRAG: boolean; enableRAG: boolean;
enableTools: boolean; enableTools: boolean;
@ -1132,14 +1080,7 @@ export interface UserActivity {
id?: string; id?: string;
userId?: string; userId?: string;
guestId?: string; guestId?: string;
activityType: activityType: "login" | "search" | "view_job" | "apply_job" | "message" | "update_profile" | "chat";
| 'login'
| 'search'
| 'view_job'
| 'apply_job'
| 'message'
| 'update_profile'
| 'chat';
timestamp: Date; timestamp: Date;
metadata: Record<string, any>; metadata: Record<string, any>;
ipAddress?: string; ipAddress?: string;
@ -1149,13 +1090,13 @@ export interface UserActivity {
export interface UserPreference { export interface UserPreference {
userId: string; userId: string;
theme: 'light' | 'dark' | 'system'; theme: "light" | "dark" | "system";
notifications: Array<NotificationPreference>; notifications: Array<NotificationPreference>;
accessibility: AccessibilitySettings; accessibility: AccessibilitySettings;
dashboardLayout?: Record<string, any>; dashboardLayout?: Record<string, any>;
language: string; language: string;
timezone: string; timezone: string;
emailFrequency: 'immediate' | 'daily' | 'weekly' | 'never'; emailFrequency: "immediate" | "daily" | "weekly" | "never";
} }
export interface WorkExperience { export interface WorkExperience {
@ -1320,13 +1261,9 @@ export function convertCandidateFromApi(data: any): Candidate {
// Convert nested Education model // Convert nested Education model
education: data.education ? convertEducationFromApi(data.education) : undefined, education: data.education ? convertEducationFromApi(data.education) : undefined,
// Convert nested Certification model // Convert nested Certification model
certifications: data.certifications certifications: data.certifications ? convertCertificationFromApi(data.certifications) : undefined,
? convertCertificationFromApi(data.certifications)
: undefined,
// Convert nested JobApplication model // Convert nested JobApplication model
jobApplications: data.jobApplications jobApplications: data.jobApplications ? convertJobApplicationFromApi(data.jobApplications) : undefined,
? convertJobApplicationFromApi(data.jobApplications)
: undefined,
}; };
} }
/** /**
@ -1354,9 +1291,7 @@ export function convertCandidateAIFromApi(data: any): CandidateAI {
// Convert nested Education model // Convert nested Education model
education: data.education ? convertEducationFromApi(data.education) : undefined, education: data.education ? convertEducationFromApi(data.education) : undefined,
// Convert nested Certification model // Convert nested Certification model
certifications: data.certifications certifications: data.certifications ? convertCertificationFromApi(data.certifications) : undefined,
? convertCertificationFromApi(data.certifications)
: undefined,
}; };
} }
/** /**
@ -1687,9 +1622,7 @@ export function convertInterviewFeedbackFromApi(data: any): InterviewFeedback {
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined, updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert nested SkillAssessment model // Convert nested SkillAssessment model
skillAssessments: data.skillAssessments skillAssessments: data.skillAssessments ? convertSkillAssessmentFromApi(data.skillAssessments) : undefined,
? convertSkillAssessmentFromApi(data.skillAssessments)
: undefined,
}; };
} }
/** /**
@ -1745,9 +1678,7 @@ export function convertJobApplicationFromApi(data: any): JobApplication {
// Convert updatedDate from ISO string to Date // Convert updatedDate from ISO string to Date
updatedDate: new Date(data.updatedDate), updatedDate: new Date(data.updatedDate),
// Convert nested InterviewSchedule model // Convert nested InterviewSchedule model
interviewSchedules: data.interviewSchedules interviewSchedules: data.interviewSchedules ? convertInterviewScheduleFromApi(data.interviewSchedules) : undefined,
? convertInterviewScheduleFromApi(data.interviewSchedules)
: undefined,
// Convert nested ApplicationDecision model // Convert nested ApplicationDecision model
decision: data.decision ? convertApplicationDecisionFromApi(data.decision) : undefined, decision: data.decision ? convertApplicationDecisionFromApi(data.decision) : undefined,
}; };
@ -1839,9 +1770,7 @@ export function convertRAGConfigurationFromApi(data: any): RAGConfiguration {
// Convert updatedAt from ISO string to Date // Convert updatedAt from ISO string to Date
updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined, updatedAt: data.updatedAt ? new Date(data.updatedAt) : undefined,
// Convert nested DataSourceConfiguration model // Convert nested DataSourceConfiguration model
dataSourceConfigurations: data.dataSourceConfigurations.map((item: any) => dataSourceConfigurations: data.dataSourceConfigurations.map((item: any) => convertDataSourceConfigurationFromApi(item)),
convertDataSourceConfigurationFromApi(item)
),
}; };
} }
/** /**
@ -2090,4 +2019,4 @@ export function convertArrayFromApi<T>(data: any[], modelType: string): T[] {
export type User = Candidate | Employer; export type User = Candidate | Employer;
// Export all types // Export all types
export type {}; export type { };

View File

@ -16,17 +16,10 @@ from datetime import datetime, UTC
from prometheus_client import CollectorRegistry # type: ignore from prometheus_client import CollectorRegistry # type: ignore
import numpy as np # type: ignore import numpy as np # type: ignore
import json_extractor as json_extractor import json_extractor as json_extractor
from pydantic import BaseModel, Field # type: ignore
from uuid import uuid4 from uuid import uuid4
from typing import List, Optional, ClassVar, Any, Literal
from datetime import datetime, UTC
import numpy as np # type: ignore
from uuid import uuid4
from prometheus_client import CollectorRegistry # type: ignore
import os import os
import re
from pathlib import Path from pathlib import Path
from rag import start_file_watcher, ChromaDBFileWatcher from rag import start_file_watcher, ChromaDBFileWatcher
@ -56,21 +49,10 @@ from models import (
ChatMessageStatus, ChatMessageStatus,
ChatMessageStreaming, ChatMessageStreaming,
LLMMessage, LLMMessage,
ChatMessage,
ChatOptions, ChatOptions,
ChatMessageUser,
Tunables,
ApiStatusType,
ChatMessageMetaData,
Candidate,
) )
from logger import logger
import defines
from .registry import agent_registry from .registry import agent_registry
from models import ChromaDBGetResponse
class CandidateEntity(Candidate): class CandidateEntity(Candidate):
model_config = {"arbitrary_types_allowed": True} # Allow ChromaDBFileWatcher, etc model_config = {"arbitrary_types_allowed": True} # Allow ChromaDBFileWatcher, etc

View File

@ -1034,6 +1034,18 @@ class DocumentMessage(ApiMessage):
converted: bool = Field(False, alias=str("converted")) converted: bool = Field(False, alias=str("converted"))
model_config = ConfigDict(populate_by_name=True) model_config = ConfigDict(populate_by_name=True)
class ToolCall(BaseModel):
name: str
content: str
model_config = ConfigDict(populate_by_name=True)
class Tool(BaseModel):
# generate-types doesn't support nested-nested
tool_calls: List[dict[str, str]] = Field(default=[], alias=str("toolCalls"))
messages: List[LLMMessage] = Field(default_factory=list)
available: List[str] = Field(default_factory=list)
tool_name: str = Field(..., alias=str("toolName"))
model_config = ConfigDict(populate_by_name=True)
class ChatMessageMetaData(BaseModel): class ChatMessageMetaData(BaseModel):
model: AIModelType = AIModelType.QWEN2_5 model: AIModelType = AIModelType.QWEN2_5
@ -1050,7 +1062,7 @@ class ChatMessageMetaData(BaseModel):
prompt_eval_count: int = 0 prompt_eval_count: int = 0
prompt_eval_duration: int = 0 prompt_eval_duration: int = 0
options: Optional[ChatOptions] = None options: Optional[ChatOptions] = None
tools: Dict[str, Any] = Field(default_factory=dict) tools: Optional[Tool] = None
timers: Dict[str, float] = Field(default_factory=dict) timers: Dict[str, float] = Field(default_factory=dict)
model_config = ConfigDict(populate_by_name=True) model_config = ConfigDict(populate_by_name=True)