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",
"@typescript-eslint/explicit-function-return-type": "warn",
"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": {
"react": {

View File

@ -1,3 +1,4 @@
import React, { JSX } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
@ -10,7 +11,7 @@ interface BackstoryQueryInterface {
submitQuery?: ChatSubmitQueryInterface;
}
const BackstoryQuery = (props: BackstoryQueryInterface) => {
const BackstoryQuery = (props: BackstoryQueryInterface): JSX.Element => {
const { question, submitQuery } = props;
if (submitQuery === undefined) {
@ -25,7 +26,7 @@ const BackstoryQuery = (props: BackstoryQueryInterface) => {
m: 1,
}}
size="small"
onClick={(e: any) => {
onClick={(): void => {
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 { SxProps, Theme } from '@mui/material';
import { ChatSubmitQueryInterface } from './BackstoryQuery';
import { SetSnackType } from './Snack';
interface BackstoryElementProps {
// setSnack: SetSnackType,
@ -24,12 +22,12 @@ interface BackstoryTabProps {
tabProps?: {
label?: string;
sx?: SxProps;
icon?: string | ReactElement<unknown, string | JSXElementConstructor<any>> | undefined;
icon?: string | ReactElement<unknown, string> | undefined;
iconPosition?: 'bottom' | 'top' | 'start' | 'end' | undefined;
};
}
function BackstoryPage(props: BackstoryTabProps) {
function BackstoryPage(props: BackstoryTabProps): JSX.Element {
const { className, active, children } = props;
return (

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { JSX, useState } from 'react';
import {
IconButton,
Dialog,
@ -45,11 +45,11 @@ interface DeleteConfirmationProps {
cancelButtonText?: string;
}
function capitalizeFirstLetter(str: string) {
function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
const DeleteConfirmation = (props: DeleteConfirmationProps) => {
const DeleteConfirmation = (props: DeleteConfirmationProps): JSX.Element => {
const {
// Legacy props
onDelete,
@ -79,13 +79,13 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
const isControlled = controlledOpen !== undefined;
const isOpen = isControlled ? controlledOpen : internalOpen;
const handleClickOpen = () => {
const handleClickOpen = (): void => {
if (!isControlled) {
setInternalOpen(true);
}
};
const handleClose = () => {
const handleClose = (): void => {
if (isControlled) {
controlledOnClose?.();
} else {
@ -93,7 +93,7 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
}
};
const handleConfirm = () => {
const handleConfirm = (): void => {
if (isControlled) {
onConfirm?.();
} else {
@ -122,7 +122,7 @@ const DeleteConfirmation = (props: DeleteConfirmationProps) => {
{/* This span is used to wrap the IconButton to ensure Tooltip works even when disabled */}
<IconButton
aria-label={action}
onClick={e => {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
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 { StyledMarkdown } from './StyledMarkdown';
@ -6,7 +6,7 @@ interface DocumentProps extends BackstoryElementProps {
filepath?: string;
}
const Document = (props: DocumentProps) => {
const Document = (props: DocumentProps): JSX.Element => {
const { filepath } = props;
const [document, setDocument] = useState<string>('');
@ -16,7 +16,7 @@ const Document = (props: DocumentProps) => {
if (!filepath) {
return;
}
const fetchDocument = async () => {
const fetchDocument = async (): Promise<void> => {
try {
const response = await fetch(filepath, {
method: 'GET',
@ -29,7 +29,7 @@ const Document = (props: DocumentProps) => {
}
const data = await response.text();
setDocument(data);
} catch (error: any) {
} catch (error) {
console.error('Error obtaining Docs content information:', error);
setDocument(`${filepath} not found.`);
}

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import React, { JSX } from 'react';
import { styled } from '@mui/material/styles';
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
@ -5,7 +6,8 @@ interface ExpandMoreProps extends IconButtonProps {
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;
return <IconButton {...other} />;
})(({ theme }) => ({
@ -15,13 +17,13 @@ const ExpandMore = styled((props: ExpandMoreProps) => {
}),
variants: [
{
props: ({ expand }) => !expand,
props: ({ expand }): boolean => !expand,
style: {
transform: 'rotate(0deg)',
},
},
{
props: ({ expand }) => !!expand,
props: ({ expand }): boolean => !!expand,
style: {
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 PropagateLoader from 'react-spinners/PropagateLoader';
import { Quote } from 'components/Quote';
import { BackstoryElementProps } from 'components/BackstoryTab';
import { Candidate, ChatSession } from 'types/types';
import { ChatSession } from 'types/types';
import { useAuth } from 'hooks/AuthContext';
import { useAppState } from 'hooks/GlobalContext';
@ -12,16 +12,13 @@ interface GenerateImageProps extends BackstoryElementProps {
chatSession: ChatSession;
}
const GenerateImage = (props: GenerateImageProps) => {
const GenerateImage = (props: GenerateImageProps): JSX.Element => {
const { user } = useAuth();
const { chatSession, prompt } = props;
const { setSnack } = useAppState();
const [processing, setProcessing] = useState<boolean>(false);
const [status, setStatus] = 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 [image, _setImage] = useState<string>('');
const controllerRef = useRef<string>(null);
// Effect to trigger profile generation when user data is ready
@ -35,8 +32,8 @@ const GenerateImage = (props: GenerateImageProps) => {
}
setStatus('Starting image generation...');
setProcessing(true);
const start = Date.now();
// const start = Date.now();
// controllerRef.current = streamQueryResponse({
// query: {
// prompt: prompt,

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import {
Box,
TextField,
@ -45,13 +45,16 @@ const LocationInput: React.FC<LocationInputProps> = ({
const [isRemote, setIsRemote] = useState<boolean>(value.remote || false);
// 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
const availableCities =
selectedCountry && selectedState
const availableCities = useMemo(() => {
return selectedCountry && selectedState
? City.getCitiesOfState(selectedCountry.isoCode, selectedState.isoCode)
: [];
}, [selectedCountry, selectedState]);
// Initialize state and city from value prop
useEffect(() => {
@ -104,24 +107,24 @@ const LocationInput: React.FC<LocationInputProps> = ({
showCity,
]);
const handleCountryChange = (event: any, newValue: ICountry | null) => {
const handleCountryChange = (_event: React.SyntheticEvent, newValue: ICountry | null): void => {
setSelectedCountry(newValue);
// Clear state and city when country changes
setSelectedState(null);
setSelectedCity(null);
};
const handleStateChange = (event: any, newValue: IState | null) => {
const handleStateChange = (_event: React.SyntheticEvent, newValue: IState | null): void => {
setSelectedState(newValue);
// Clear city when state changes
setSelectedCity(null);
};
const handleCityChange = (event: any, newValue: ICity | null) => {
const handleCityChange = (_event: React.SyntheticEvent, newValue: ICity | null): void => {
setSelectedCity(newValue);
};
const handleRemoteToggle = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleRemoteToggle = (event: React.ChangeEvent<HTMLInputElement>): void => {
setIsRemote(event.target.checked);
};
@ -139,9 +142,9 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedCountry}
onChange={handleCountryChange}
options={allCountries}
getOptionLabel={option => option.name}
getOptionLabel={(option): string => option.name}
disabled={disabled}
renderInput={params => (
renderInput={(params): React.ReactNode => (
<TextField
{...params}
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}>
<img
loading="lazy"
@ -180,9 +183,9 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedState}
onChange={handleStateChange}
options={availableStates}
getOptionLabel={option => option.name}
getOptionLabel={(option): string => option.name}
disabled={disabled || availableStates.length === 0}
renderInput={params => (
renderInput={(params): React.ReactNode => (
<TextField
{...params}
label="State/Region"
@ -203,9 +206,9 @@ const LocationInput: React.FC<LocationInputProps> = ({
value={selectedCity}
onChange={handleCityChange}
options={availableCities}
getOptionLabel={option => option.name}
getOptionLabel={(option): string => option.name}
disabled={disabled || availableCities.length === 0}
renderInput={params => (
renderInput={(params): React.ReactNode => (
<TextField
{...params}
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 };

View File

@ -32,7 +32,7 @@ const Mermaid: React.FC<MermaidProps> = (props: MermaidProps) => {
}, [containerRef, setVisible]);
useEffect(() => {
const renderMermaid = async () => {
const renderMermaid = async (): Promise<void> => {
if (containerRef.current && visible && chart) {
try {
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 Accordion from '@mui/material/Accordion';
import AccordionSummary from '@mui/material/AccordionSummary';
@ -12,11 +12,9 @@ import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Button from '@mui/material/Button';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Collapse from '@mui/material/Collapse';
import { ExpandMore } from './ExpandMore';
import JsonView from '@uiw/react-json-view';
import React from 'react';
import { Box } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { SxProps, Theme } from '@mui/material';
@ -28,9 +26,7 @@ import { ErrorOutline, InfoOutline, Memory, Psychology /* Stream, */ } from '@mu
import { StyledMarkdown } from './StyledMarkdown';
import { VectorVisualizer } from './VectorVisualizer';
import { SetSnackType } from './Snack';
import { CopyBubble } from './CopyBubble';
import { Scrollable } from './Scrollable';
import { BackstoryElementProps } from './BackstoryTab';
import {
ChatMessage,
@ -44,7 +40,10 @@ import {
ChatSenderType,
} 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 defaultStyle = {
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: {
...defaultStyle,
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}`);
}
return styles[type];
return styles[type] as Record<string, string>;
};
const getIcon = (
activityType: ApiActivityType | ChatSenderType | 'error'
): React.ReactNode | null => {
const icons: any = {
const icons: Record<string, React.ReactNode> = {
error: <ErrorOutline color="error" />,
generating: <LocationSearchingIcon />,
information: <InfoOutline color="info" />,
@ -207,7 +206,7 @@ interface MessageMetaProps {
messageProps: MessageProps;
}
const MessageMeta = (props: MessageMetaProps) => {
const MessageMeta = (props: MessageMetaProps): JSX.Element => {
const {
/* MessageData */
ragResults = [],
@ -217,11 +216,11 @@ const MessageMeta = (props: MessageMetaProps) => {
promptEvalCount = 0,
promptEvalDuration = 0,
} = props.metadata || {};
const message: any = props.messageProps.message;
const message: ChatMessage | ChatMessageError | ChatMessageStatus = props.messageProps.message;
let llm_submission = '<|system|>\n';
llm_submission += message.system_prompt + '\n\n';
llm_submission += message.context_prompt;
// let llm_submission = '<|system|>\n';
// llm_submission += message.system_prompt + '\n\n';
// llm_submission += message.context_prompt;
return (
<>
@ -280,72 +279,74 @@ const MessageMeta = (props: MessageMetaProps) => {
</TableContainer>
</>
)}
{tools && tools.tool_calls && tools.tool_calls.length !== 0 && (
{tools && tools.toolCalls && tools.toolCalls.length !== 0 && (
<Accordion sx={{ boxSizing: 'border-box' }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ fontSize: '0.8rem' }}>Tools queried</Box>
</AccordionSummary>
<AccordionDetails>
{tools.tool_calls.map((tool: any, index: number) => (
<Box
key={index}
sx={{
m: 0,
p: 1,
pt: 0,
display: 'flex',
flexDirection: 'column',
border: '1px solid #e0e0e0',
}}
>
{index !== 0 && <Divider />}
{tools.toolCalls.map(
(tool: Record<string, string>, index: number): React.ReactNode => (
<Box
key={index}
sx={{
fontSize: '0.75rem',
m: 0,
p: 1,
pt: 0,
display: 'flex',
flexDirection: 'column',
mt: 1,
mb: 1,
fontWeight: 'bold',
border: '1px solid #e0e0e0',
}}
>
{tool.name}
</Box>
{tool.content !== 'null' && (
<JsonView
displayDataTypes={false}
objectSortKeys={true}
collapsed={1}
value={JSON.parse(tool.content)}
style={{
fontSize: '0.8rem',
maxHeight: '20rem',
overflow: 'auto',
{index !== 0 && <Divider />}
<Box
sx={{
fontSize: '0.75rem',
display: 'flex',
flexDirection: 'column',
mt: 1,
mb: 1,
fontWeight: 'bold',
}}
>
<JsonView.String
render={({ children, ...reset }) => {
if (typeof children === 'string' && children.match('\n')) {
return (
<pre
{...reset}
style={{
display: 'flex',
border: 'none',
...reset.style,
}}
>
{children}
</pre>
);
}
{tool.name}
</Box>
{tool.content !== 'null' && (
<JsonView
displayDataTypes={false}
objectSortKeys={true}
collapsed={1}
value={JSON.parse(tool.content)}
style={{
fontSize: '0.8rem',
maxHeight: '20rem',
overflow: 'auto',
}}
/>
</JsonView>
)}
{tool.content === 'null' && 'No response from tool call'}
</Box>
))}
>
<JsonView.String
render={({ children, ...reset }): React.ReactNode => {
if (typeof children === 'string' && children.match('\n')) {
return (
<pre
{...reset}
style={{
display: 'flex',
border: 'none',
...reset.style,
}}
>
{children}
</pre>
);
}
}}
/>
</JsonView>
)}
{tool.content === 'null' && 'No response from tool call'}
</Box>
)
)}
</AccordionDetails>
</Accordion>
)}
@ -369,9 +370,9 @@ const MessageMeta = (props: MessageMetaProps) => {
<Box sx={{ fontSize: '0.8rem' }}>Full Response Details</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ pb: 1 }}>
{/* <Box sx={{ pb: 1 }}>
Copy LLM submission: <CopyBubble content={llm_submission} />
</Box>
</Box> */}
<JsonView
displayDataTypes={false}
objectSortKeys={true}
@ -380,7 +381,7 @@ const MessageMeta = (props: MessageMetaProps) => {
style={{ fontSize: '0.8rem', maxHeight: '20rem', overflow: 'auto' }}
>
<JsonView.String
render={({ children, ...reset }) => {
render={({ children, ...reset }): React.ReactNode => {
if (typeof children === 'string' && children.match('\n')) {
return (
<pre
@ -412,7 +413,7 @@ interface MessageContainerProps {
copyContent?: string;
}
const MessageContainer = (props: MessageContainerProps) => {
const MessageContainer = (props: MessageContainerProps): JSX.Element => {
const { type, sx, messageView, metadataView, copyContent } = props;
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 [metaExpanded, setMetaExpanded] = useState<boolean>(false);
const theme = useTheme();
@ -468,9 +469,9 @@ const Message = (props: MessageProps) => {
: 'error' in message
? 'error'
: (message as ChatMessage).role;
const style: any = getStyle(theme, type);
const style = getStyle(theme, type);
const handleMetaExpandClick = () => {
const handleMetaExpandClick = (): void => {
setMetaExpanded(!metaExpanded);
};
@ -563,7 +564,7 @@ const Message = (props: MessageProps) => {
expanded={isControlled ? expanded : undefined} // Omit expanded prop for uncontrolled
defaultExpanded={expanded} // Default to collapsed for uncontrolled Accordion
className={className}
onChange={(_event, newExpanded) => {
onChange={(_event, newExpanded): void => {
isControlled && onExpand && onExpand(newExpanded);
}}
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 { styled } from '@mui/material/styles';
@ -30,7 +30,7 @@ const QuoteContainer = styled(Paper, {
const QuoteText = styled(Typography, {
shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
})<QuoteContainerProps>(({ size = 'normal' }) => ({
fontSize: size === 'small' ? '0.9rem' : '1.2rem',
lineHeight: size === 'small' ? 1.4 : 1.6,
fontStyle: 'italic',
@ -44,7 +44,7 @@ const QuoteText = styled(Typography, {
const QuoteMark = styled(Typography, {
shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
})<QuoteContainerProps>(({ size = 'normal' }) => ({
fontSize: size === 'small' ? '2.5rem' : '4rem',
fontFamily: '"Georgia", "Times New Roman", serif',
fontWeight: 'bold',
@ -83,7 +83,7 @@ const AuthorText = styled(Typography, {
const AccentLine = styled(Box, {
shouldForwardProp: prop => prop !== 'size',
})<QuoteContainerProps>(({ theme, size = 'normal' }) => ({
})<QuoteContainerProps>(({ size = 'normal' }) => ({
width: size === 'small' ? '40px' : '60px',
height: size === 'small' ? '1px' : '2px',
background: 'linear-gradient(90deg, #D4A017 0%, #4A7A7D 100%)', // Golden Ochre to Dusty Teal
@ -98,12 +98,12 @@ interface QuoteProps {
sx?: SxProps;
}
const Quote = (props: QuoteProps) => {
const Quote = (props: QuoteProps): JSX.Element => {
const { quote, author, size = 'normal', sx } = props;
return (
<QuoteContainer size={size} elevation={0} sx={sx}>
<OpeningQuote size={size}>"</OpeningQuote>
<ClosingQuote size={size}>"</ClosingQuote>
<OpeningQuote size={size}>&quot;</OpeningQuote>
<ClosingQuote size={size}>&quot;</ClosingQuote>
<Box sx={{ position: 'relative', zIndex: 2 }}>
<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 { Job, Candidate, SkillAssessment } from 'types/types';
import { Scrollable } from './Scrollable';
import { useAuth } from 'hooks/AuthContext';
import * as Types from 'types/types';
import { StyledMarkdown } from './StyledMarkdown';
import { Message } from './Message';
import InputIcon from '@mui/icons-material/Input';
import TuneIcon from '@mui/icons-material/Tune';
import ArticleIcon from '@mui/icons-material/Article';
@ -13,7 +12,6 @@ import { StatusBox, StatusIcon } from './ui/StatusIcon';
import { CopyBubble } from './CopyBubble';
import { useAppState } from 'hooks/GlobalContext';
import { StreamingOptions } from 'services/api-client';
import { setDefaultResultOrder } from 'dns';
interface ResumeGeneratorProps {
job: Job;
@ -22,15 +20,6 @@ interface ResumeGeneratorProps {
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 { job, candidate, skills, onComplete } = props;
const { setSnack } = useAppState();
@ -44,7 +33,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
const [statusType, setStatusType] = useState<Types.ApiActivityType | 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);
};
@ -88,17 +77,32 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
},
};
const generateResume = async () => {
const request: any = await apiClient.generateResume(
const generateResume = async (): Promise<void> => {
const request = await apiClient.generateResume(
candidate.id || '',
job.id || '',
generateResumeHandlers
);
const result = await request.promise;
await request.promise;
};
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 () => {
if (!resume) {
@ -159,7 +163,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
{tabValue === 'resume' && (
<>
<CopyBubble
onClick={() => {
onClick={(): void => {
setSnack('Resume copied to clipboard!');
}}
sx={{ position: 'absolute', top: 0, right: 0 }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -16,17 +16,10 @@ from datetime import datetime, UTC
from prometheus_client import CollectorRegistry # type: ignore
import numpy as np # type: ignore
import json_extractor as json_extractor
from pydantic import BaseModel, Field # type: ignore
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 re
from pathlib import Path
from rag import start_file_watcher, ChromaDBFileWatcher
@ -56,21 +49,10 @@ from models import (
ChatMessageStatus,
ChatMessageStreaming,
LLMMessage,
ChatMessage,
ChatOptions,
ChatMessageUser,
Tunables,
ApiStatusType,
ChatMessageMetaData,
Candidate,
)
from logger import logger
import defines
from .registry import agent_registry
from models import ChromaDBGetResponse
class CandidateEntity(Candidate):
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"))
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):
model: AIModelType = AIModelType.QWEN2_5
@ -1050,7 +1062,7 @@ class ChatMessageMetaData(BaseModel):
prompt_eval_count: int = 0
prompt_eval_duration: int = 0
options: Optional[ChatOptions] = None
tools: Dict[str, Any] = Field(default_factory=dict)
tools: Optional[Tool] = None
timers: Dict[str, float] = Field(default_factory=dict)
model_config = ConfigDict(populate_by_name=True)