import React, { useState, useEffect, JSX } from 'react'; import { Box, Button, Grid, useMediaQuery, Typography, Card, CardContent, List, ListItem, ListItemText, ListItemSecondaryAction, IconButton, Switch, FormControlLabel, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Chip, Divider, Paper, } from '@mui/material'; import { styled } from '@mui/material/styles'; import { CloudUpload, Edit, Delete, Visibility, Close } from '@mui/icons-material'; import { useTheme } from '@mui/material/styles'; import { useAuth } from 'hooks/AuthContext'; import * as Types from 'types/types'; import { BackstoryElementProps } from './BackstoryTab'; import { useAppState } from 'hooks/GlobalContext'; const VisuallyHiddenInput = styled('input')({ clip: 'rect(0 0 0 0)', clipPath: 'inset(50%)', height: 1, overflow: 'hidden', position: 'absolute', bottom: 0, left: 0, whiteSpace: 'nowrap', width: 1, }); const DocumentManager = (_props: BackstoryElementProps): JSX.Element => { const theme = useTheme(); const { setSnack } = useAppState(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const { user, apiClient } = useAuth(); const [documents, setDocuments] = useState([]); const [selectedDocument, setSelectedDocument] = useState(null); const [documentContent, setDocumentContent] = useState(''); const [isViewingContent, setIsViewingContent] = useState(false); const [editingDocument, setEditingDocument] = useState(null); const [editingName, setEditingName] = useState(''); const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); // Check if user is a candidate const candidate = user?.userType === 'candidate' ? (user as Types.Candidate) : null; // Load documents on component mount useEffect(() => { const loadDocuments = async (): Promise => { try { const results = await apiClient.getCandidateDocuments(); setDocuments(results.documents); } catch (error) { console.error(error); setSnack('Failed to load documents', 'error'); } }; if (candidate) { loadDocuments(); } }, [candidate, apiClient, setSnack]); // Handle document upload const handleDocumentUpload = async (e: React.ChangeEvent): Promise => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); let docType: Types.DocumentType | null = null; switch (fileExtension.substring(1)) { case 'pdf': docType = 'pdf'; break; case 'docx': docType = 'docx'; break; case 'md': docType = 'markdown'; break; case 'txt': docType = 'txt'; break; } if (!docType) { setSnack('Invalid file type. Please upload .txt, .md, .docx, or .pdf files only.', 'error'); return; } try { // Upload file (replace with actual API call) const controller = apiClient.uploadCandidateDocument( file, { includeInRag: true, isJobDocument: false }, { onError: error => { console.error(error); setSnack(error.content, 'error'); }, } ); const result = await controller.promise; if (result && result.document) { setDocuments(prev => [...prev, result.document]); setSnack(`Document uploaded: ${file.name}`, 'success'); } // Reset file input e.target.value = ''; } catch (error) { console.error(error); setSnack('Failed to upload document', 'error'); } } }; // Handle document deletion const handleDeleteDocument = async (document: Types.Document): Promise => { try { // Call API to delete document await apiClient.deleteCandidateDocument(document); setDocuments(prev => prev.filter(doc => doc.id !== document.id)); setSnack('Document deleted successfully', 'success'); // Close content view if this document was being viewed if (selectedDocument?.id === document.id) { setIsViewingContent(false); setSelectedDocument(null); setDocumentContent(''); } } catch (error) { setSnack('Failed to delete document', 'error'); } }; // Handle RAG flag toggle const handleRAGToggle = async ( document: Types.Document, includeInRag: boolean ): Promise => { try { document.options = { includeInRag }; // Call API to update RAG flag await apiClient.updateCandidateDocument(document); setDocuments(prev => prev.map(doc => (doc.id === document.id ? { ...doc, includeInRag } : doc)) ); setSnack(`Document ${includeInRag ? 'included in' : 'excluded from'} RAG`, 'success'); } catch (error) { setSnack('Failed to update RAG setting', 'error'); } }; // Handle document rename const handleRenameDocument = async (document: Types.Document, newName: string): Promise => { if (!newName.trim()) { setSnack('Document name cannot be empty', 'error'); return; } try { // Call API to rename document document.filename = newName; await apiClient.updateCandidateDocument(document); setDocuments(prev => prev.map(doc => (doc.id === document.id ? { ...doc, filename: newName.trim() } : doc)) ); setSnack('Document renamed successfully', 'success'); setIsRenameDialogOpen(false); setEditingDocument(null); setEditingName(''); } catch (error) { setSnack('Failed to rename document', 'error'); } }; // Handle document content viewing const handleViewDocument = async (document: Types.Document): Promise => { try { setSelectedDocument(document); setIsViewingContent(true); // Call API to get document content const result = await apiClient.getCandidateDocumentText(document); setDocumentContent(result.content); } catch (error) { setSnack('Failed to load document content', 'error'); setIsViewingContent(false); } }; // Start rename process const startRename = (document: Types.Document, currentName: string): void => { setEditingDocument(document); setEditingName(currentName); setIsRenameDialogOpen(true); }; // Format file size const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; // Get file type color const getFileTypeColor = (type: string): 'primary' | 'secondary' | 'success' | 'warning' => { switch (type) { case 'pdf': return 'primary'; case 'docx': return 'secondary'; case 'txt': return 'success'; case 'md': return 'warning'; default: return 'primary'; } }; if (!candidate) { return You must be logged in as a candidate to view this content.; } return ( <> Documents {documents.length === 0 ? ( No additional documents uploaded ) : ( {documents.map((doc, index) => ( {index > 0 && } {doc.filename} {doc.options?.includeInRag && ( )} } secondary={ {formatFileSize(doc.size)} • {doc?.uploadDate?.toLocaleDateString()} { handleRAGToggle(doc, e.target.checked); }} size="small" /> } label={Include in RAG} /> } /> { handleViewDocument(doc); }} title="View content" > { startRename(doc, doc.filename); }} title="Rename" > { handleDeleteDocument(doc); }} title="Delete" color="error" > ))} )} {/* Document Content Viewer */} {isViewingContent && ( Document Content { setIsViewingContent(false); setSelectedDocument(null); setDocumentContent(''); }} >
                    {documentContent || 'Loading content...'}
                  
)} {/* Rename Dialog */} { setIsRenameDialogOpen(false); }} maxWidth="sm" fullWidth > Rename Document { setEditingName(e.target.value); }} onKeyUp={(e): void => { if (e.key === 'Enter' && editingDocument) { handleRenameDocument(editingDocument, editingName); } }} />
); }; export { DocumentManager };