chowdary1's picture
Fix: Remove ALL localhost:8000 hardcoded URLs - use relative paths for production
91be275
Raw History Blame Contribute Delete
92 kB
/**
* RAG Questions Panel - Admin PDF Upload & RAG Training
*
* This panel allows admins to:
* 1. Upload interview question PDFs
* 2. Train RAG on uploaded PDFs with intelligent classification
* 3. View RAG statistics and classification breakdown
* 4. Search existing questions
* 5. Manage vector collections
*
* INTELLIGENT CLASSIFICATION:
* - Auto-detects branch (CSE, ECE, EEE, ME, CE, Architecture)
* - Auto-detects domain (ML Engineer, VLSI Engineer, etc.)
* - Optional manual override for specific branch/domain
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
Database, Upload, FileText, Trash2, Play, Search,
RefreshCw, Loader2, CheckCircle, XCircle, AlertCircle,
BookOpen, Brain, Zap, BarChart2, FolderOpen, Download,
ChevronDown, ChevronRight, Filter, Clock, Hash, Tag,
GitBranch, Briefcase, Settings, X, PieChart, FileCheck, Eye,
AlertTriangle, Shield, Lock
} from 'lucide-react';
import { getToken } from '../../services/api';
import './RAGQuestionsPanel.css';
const API_BASE = import.meta.env.VITE_API_URL || '';
function RAGQuestionsPanel() {
// State for PDFs
const [pdfs, setPdfs] = useState([]);
const [pdfsLoading, setPdfsLoading] = useState(true);
const [uploadProgress, setUploadProgress] = useState(null);
// State for RAG stats
const [ragStats, setRagStats] = useState(null);
const [statsLoading, setStatsLoading] = useState(true);
// State for collections
const [collections, setCollections] = useState([]);
// State for taxonomy (branches & domains)
const [taxonomy, setTaxonomy] = useState({ branches: [], domains: [] });
const [taxonomyLoading, setTaxonomyLoading] = useState(true);
// State for training
const [trainingPdfId, setTrainingPdfId] = useState(null);
const [trainAllLoading, setTrainAllLoading] = useState(false);
const [trainResult, setTrainResult] = useState(null);
const [showTrainingSummary, setShowTrainingSummary] = useState(false);
const [selectedPdfSummary, setSelectedPdfSummary] = useState(null);
const [summaryLoading, setSummaryLoading] = useState(false);
// Training configuration
const [trainConfig, setTrainConfig] = useState({
branch: '', // Empty = auto-detect
domain: '', // Empty = auto-detect
company: '',
company_level: 'product',
});
const [showTrainConfig, setShowTrainConfig] = useState(false);
const [configPdfId, setConfigPdfId] = useState(null);
// State for search
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState(null);
const [searchLoading, setSearchLoading] = useState(false);
const [searchFilters, setSearchFilters] = useState({
question_type: '',
difficulty: '',
branch: '',
domain: '',
});
// State for UI
const [activeView, setActiveView] = useState('upload'); // 'upload', 'stats', 'search'
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
// State for delete functionality
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showClearAllModal, setShowClearAllModal] = useState(false);
const [allQuestions, setAllQuestions] = useState([]);
const [questionsLoading, setQuestionsLoading] = useState(false);
const [selectedQuestions, setSelectedQuestions] = useState(new Set());
const [deleteLoading, setDeleteLoading] = useState(false);
const [clearAllPassword, setClearAllPassword] = useState('');
const [confirmDataLoss, setConfirmDataLoss] = useState(false);
const [confirmNoBackup, setConfirmNoBackup] = useState(false);
const [clearAllLoading, setClearAllLoading] = useState(false);
// Password lockout state
const [lockoutStatus, setLockoutStatus] = useState({
is_locked: false,
remaining_attempts: 5,
time_remaining_seconds: null,
stage: 0,
});
const [lockoutCountdown, setLockoutCountdown] = useState(null);
/**
* Fetch taxonomy (branches & domains)
*/
const fetchTaxonomy = useCallback(async () => {
setTaxonomyLoading(true);
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/taxonomy`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setTaxonomy({
branches: data.branches || [],
domains: data.domains || [],
});
}
}
} catch (err) {
console.error('Error fetching taxonomy:', err);
} finally {
setTaxonomyLoading(false);
}
}, []);
/**
* Get domains filtered by selected branch
*/
const getFilteredDomains = (branchId) => {
if (!branchId) return taxonomy.domains;
return taxonomy.domains.filter(d => d.branch === branchId);
};
/**
* Fetch uploaded PDFs
*/
const fetchPdfs = useCallback(async () => {
setPdfsLoading(true);
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/pdfs`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setPdfs(data.pdfs || []);
}
}
} catch (err) {
console.error('Error fetching PDFs:', err);
setError('Failed to fetch PDFs');
} finally {
setPdfsLoading(false);
}
}, []);
/**
* Fetch RAG statistics
*/
const fetchRagStats = useCallback(async () => {
setStatsLoading(true);
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/stats`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setRagStats(data);
}
}
} catch (err) {
console.error('Error fetching RAG stats:', err);
} finally {
setStatsLoading(false);
}
}, []);
/**
* Fetch collections
*/
const fetchCollections = useCallback(async () => {
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/collections`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setCollections(data.collections || []);
}
}
} catch (err) {
console.error('Error fetching collections:', err);
}
}, []);
/**
* Upload PDF
*/
const handleUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
if (!file.name.endsWith('.pdf')) {
setError('Please upload a PDF file');
return;
}
setUploadProgress({ status: 'uploading', filename: file.name });
setError(null);
setSuccess(null);
try {
const token = getToken();
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${API_BASE}/api/admin/rag/upload-pdf`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
const data = await response.json();
if (response.ok && data.success) {
setUploadProgress({ status: 'success', filename: file.name });
setSuccess(`PDF "${file.name}" uploaded successfully!`);
fetchPdfs();
} else {
setUploadProgress({ status: 'error', filename: file.name });
setError(data.detail || 'Upload failed');
}
} catch (err) {
console.error('Upload error:', err);
setUploadProgress({ status: 'error', filename: file.name });
setError('Upload failed: ' + err.message);
}
// Clear upload input
event.target.value = '';
// Clear progress after 3 seconds
setTimeout(() => setUploadProgress(null), 3000);
};
/**
* Train RAG on a specific PDF with intelligent classification
*/
const trainPdf = async (pdfId, config = null) => {
const useConfig = config || trainConfig;
setTrainingPdfId(pdfId);
setTrainResult(null);
setError(null);
setShowTrainConfig(false);
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/train`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
pdf_id: pdfId,
branch: useConfig.branch || null,
domain: useConfig.domain || null,
company: useConfig.company || null,
company_level: useConfig.company_level || 'product',
})
});
const data = await response.json();
if (response.ok && data.success) {
setTrainResult(data);
setShowTrainingSummary(true); // Show detailed summary modal
// Build success message with classification info
let msg = `Training complete! Found ${data.questions_found} questions, stored ${data.questions_stored}.`;
setSuccess(msg);
fetchPdfs();
fetchRagStats();
fetchCollections();
} else {
setError(data.detail || 'Training failed');
}
} catch (err) {
console.error('Training error:', err);
setError('Training failed: ' + err.message);
} finally {
setTrainingPdfId(null);
setConfigPdfId(null);
// Reset config
setTrainConfig({
branch: '',
domain: '',
company: '',
company_level: 'product',
});
}
};
/**
* Open training config dialog
*/
const openTrainConfig = (pdfId) => {
setConfigPdfId(pdfId);
setShowTrainConfig(true);
setTrainConfig({
branch: '',
domain: '',
company: '',
company_level: 'product',
});
};
/**
* Train all untrained PDFs
*/
const trainAllPdfs = async () => {
setTrainAllLoading(true);
setTrainResult(null);
setError(null);
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/train-all`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok && data.success) {
setTrainResult(data);
setShowTrainingSummary(true); // Show detailed summary modal
setSuccess(`Batch training complete! Processed ${data.pdfs_processed} PDFs, found ${data.total_questions_found} questions.`);
fetchPdfs();
fetchRagStats();
fetchCollections();
} else {
setError(data.detail || 'Batch training failed');
}
} catch (err) {
console.error('Batch training error:', err);
setError('Batch training failed: ' + err.message);
} finally {
setTrainAllLoading(false);
}
};
/**
* Delete PDF
*/
const deletePdf = async (pdfId) => {
if (!window.confirm('Are you sure you want to delete this PDF?')) return;
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/pdfs/${pdfId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
setSuccess('PDF deleted successfully');
fetchPdfs();
} else {
const data = await response.json();
setError(data.detail || 'Delete failed');
}
} catch (err) {
console.error('Delete error:', err);
setError('Delete failed: ' + err.message);
}
};
/**
* Fetch all questions for deletion management
*/
const fetchAllQuestions = async () => {
setQuestionsLoading(true);
setError(null);
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/questions/list?limit=500`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
console.log('Fetched questions:', data);
setAllQuestions(data.questions || []);
} else {
const errorData = await response.json().catch(() => ({}));
console.error('Error response:', response.status, errorData);
setError(`Failed to fetch questions: ${errorData.detail || response.statusText}`);
}
} catch (err) {
console.error('Error fetching questions:', err);
setError(`Failed to fetch questions: ${err.message}`);
} finally {
setQuestionsLoading(false);
}
};
/**
* Open delete questions modal
*/
const openDeleteModal = () => {
setShowDeleteModal(true);
setSelectedQuestions(new Set());
fetchAllQuestions();
};
/**
* Toggle question selection
*/
const toggleQuestionSelection = (chunkId) => {
const newSelected = new Set(selectedQuestions);
if (newSelected.has(chunkId)) {
newSelected.delete(chunkId);
} else {
newSelected.add(chunkId);
}
setSelectedQuestions(newSelected);
};
/**
* Select all questions
*/
const selectAllQuestions = () => {
if (selectedQuestions.size === allQuestions.length) {
setSelectedQuestions(new Set());
} else {
setSelectedQuestions(new Set(allQuestions.map(q => q.chunk_id)));
}
};
/**
* Delete selected questions
*/
const deleteSelectedQuestions = async () => {
if (selectedQuestions.size === 0) return;
if (!window.confirm(`Are you sure you want to delete ${selectedQuestions.size} questions?`)) return;
setDeleteLoading(true);
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/questions/bulk`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
chunk_ids: Array.from(selectedQuestions)
})
});
const data = await response.json();
if (response.ok && data.success) {
setSuccess(`Successfully deleted ${data.deleted_count} questions`);
setSelectedQuestions(new Set());
fetchAllQuestions();
fetchRagStats();
} else {
setError(data.detail || 'Failed to delete questions');
}
} catch (err) {
console.error('Delete error:', err);
setError('Failed to delete questions: ' + err.message);
} finally {
setDeleteLoading(false);
}
};
/**
* Fetch lockout status
*/
const fetchLockoutStatus = async () => {
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/clear-all/status`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setLockoutStatus(data);
if (data.is_locked && data.time_remaining_seconds) {
setLockoutCountdown(data.time_remaining_seconds);
}
}
} catch (err) {
console.error('Error fetching lockout status:', err);
}
};
/**
* Open clear all modal
*/
const openClearAllModal = () => {
setShowClearAllModal(true);
setClearAllPassword('');
setConfirmDataLoss(false);
setConfirmNoBackup(false);
fetchLockoutStatus();
};
/**
* Clear entire vector database
*/
const clearAllVectorDatabase = async () => {
// Check lockout before even trying
if (lockoutStatus.is_locked) {
setError('You are currently locked out. Please wait.');
return;
}
setClearAllLoading(true);
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/clear-all`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
password: clearAllPassword,
confirm_data_loss: confirmDataLoss,
confirm_no_backup: confirmNoBackup
})
});
const data = await response.json();
if (response.ok && data.success) {
setSuccess(`Vector database cleared! ${data.questions_deleted} questions deleted. ${data.pdfs_reset || 0} PDFs reset.`);
setShowClearAllModal(false);
setClearAllPassword('');
setConfirmDataLoss(false);
setConfirmNoBackup(false);
setLockoutStatus({ is_locked: false, remaining_attempts: 5, time_remaining_seconds: null, stage: 0 });
// Refresh ALL data
fetchRagStats();
fetchCollections();
fetchPdfs(); // Also refresh PDFs to show untrained status
setAllQuestions([]); // Clear local questions cache
} else {
// Handle lockout response (429)
if (response.status === 429) {
const timeRemaining = response.headers.get('X-Lockout-Remaining');
if (timeRemaining) {
setLockoutCountdown(parseInt(timeRemaining));
setLockoutStatus(prev => ({
...prev,
is_locked: true,
time_remaining_seconds: parseInt(timeRemaining),
remaining_attempts: 0,
}));
}
} else if (response.status === 403) {
// Wrong password - update remaining attempts
const remaining = response.headers.get('X-Remaining-Attempts');
if (remaining !== null) {
setLockoutStatus(prev => ({
...prev,
remaining_attempts: parseInt(remaining),
}));
}
}
setError(data.detail || 'Failed to clear vector database');
setClearAllPassword('');
}
} catch (err) {
console.error('Clear error:', err);
setError('Failed to clear vector database: ' + err.message);
} finally {
setClearAllLoading(false);
}
};
// Countdown timer effect for lockout
React.useEffect(() => {
if (lockoutCountdown && lockoutCountdown > 0) {
const timer = setInterval(() => {
setLockoutCountdown(prev => {
if (prev <= 1) {
clearInterval(timer);
setLockoutStatus(prevStatus => ({
...prevStatus,
is_locked: false,
}));
fetchLockoutStatus(); // Refresh status from server
return null;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}
}, [lockoutCountdown]);
/**
* View PDF Summary (for already trained PDFs)
*/
const viewPdfSummary = async (pdfId) => {
setSummaryLoading(true);
setSelectedPdfSummary(null);
setShowTrainingSummary(true);
try {
const token = getToken();
const response = await fetch(`${API_BASE}/api/admin/rag/pdfs/${pdfId}/details`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
console.log('PDF Details Response:', data);
console.log('Classification:', data.classification);
console.log('Questions Detail:', data.classification?.questions_detail);
// Transform to match trainResult format for the modal
setSelectedPdfSummary({
pdfName: data.original_filename,
questions_found: data.questions_extracted,
questions_stored: data.questions_extracted,
trained_at: data.trained_at,
branch: data.branch,
domain: data.domain,
classification: data.classification,
question_types: data.question_types,
difficulties: data.difficulties,
});
} else {
const data = await response.json();
setError(data.detail || 'Failed to load PDF details');
setShowTrainingSummary(false);
}
} catch (err) {
console.error('Error fetching PDF details:', err);
setError('Failed to load PDF details: ' + err.message);
setShowTrainingSummary(false);
} finally {
setSummaryLoading(false);
}
};
/**
* Search questions
*/
const searchQuestions = async () => {
if (!searchQuery.trim()) return;
setSearchLoading(true);
setSearchResults(null);
try {
const token = getToken();
const params = new URLSearchParams({
query: searchQuery,
top_k: '10'
});
if (searchFilters.question_type) {
params.append('question_type', searchFilters.question_type);
}
if (searchFilters.difficulty) {
params.append('difficulty', searchFilters.difficulty);
}
const response = await fetch(`${API_BASE}/api/admin/rag/search?${params}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
setSearchResults(data);
}
} catch (err) {
console.error('Search error:', err);
setError('Search failed: ' + err.message);
} finally {
setSearchLoading(false);
}
};
// Initial fetch
useEffect(() => {
fetchPdfs();
fetchRagStats();
fetchCollections();
fetchTaxonomy();
}, [fetchPdfs, fetchRagStats, fetchCollections, fetchTaxonomy]);
// Clear messages after delay
useEffect(() => {
if (success) {
const timer = setTimeout(() => setSuccess(null), 5000);
return () => clearTimeout(timer);
}
}, [success]);
useEffect(() => {
if (error) {
const timer = setTimeout(() => setError(null), 5000);
return () => clearTimeout(timer);
}
}, [error]);
/**
* Format file size
*/
const formatFileSize = (bytes) => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
};
/**
* Format domain/branch name from snake_case to Title Case
*/
const formatName = (name) => {
if (!name) return '';
return name
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
/**
* Get branch name from domain (lookup from taxonomy)
*/
const getBranchForDomain = (domainId) => {
const domain = taxonomy.domains.find(d => d.id === domainId);
if (domain) {
const branch = taxonomy.branches.find(b => b.id === domain.branch);
return branch ? branch.name : formatName(domain.branch);
}
return null;
};
const untrainedPdfs = pdfs.filter(p => !p.is_trained);
return (
<div className="panel-container rag-panel">
{/* Header */}
<header className="panel-header">
<div className="header-content">
<div className="header-title">
<div className="header-icon-wrapper brain">
<Brain size={28} />
</div>
<div>
<h1>RAG Question Manager</h1>
<p>Upload PDFs, train RAG, and manage interview questions</p>
</div>
</div>
<button
type="button"
className="btn btn-secondary"
onClick={(e) => { e.stopPropagation(); fetchPdfs(); fetchRagStats(); fetchCollections(); fetchTaxonomy(); }}
>
<RefreshCw size={18} />
Refresh
</button>
</div>
</header>
{/* Alerts */}
{error && (
<div className="alert alert-error">
<XCircle size={18} />
<span>{error}</span>
</div>
)}
{success && (
<div className="alert alert-success">
<CheckCircle size={18} />
<span>{success}</span>
</div>
)}
{/* Stats Cards */}
<div className="stats-grid rag-stats">
<div className="stat-card">
<div className="stat-icon upload">
<FileText size={24} />
</div>
<div className="stat-content">
<span className="stat-value">{pdfs.length}</span>
<span className="stat-label">PDFs Uploaded</span>
</div>
</div>
<div className="stat-card">
<div className="stat-icon trained">
<CheckCircle size={24} />
</div>
<div className="stat-content">
<span className="stat-value">{pdfs.filter(p => p.is_trained).length}</span>
<span className="stat-label">PDFs Trained</span>
</div>
</div>
<div className="stat-card">
<div className="stat-icon questions">
<BookOpen size={24} />
</div>
<div className="stat-content">
<span className="stat-value">
{ragStats?.total_documents || pdfs.reduce((sum, p) => sum + (p.questions_extracted || 0), 0)}
</span>
<span className="stat-label">Questions in RAG</span>
</div>
</div>
<div className="stat-card">
<div className="stat-icon collections">
<Database size={24} />
</div>
<div className="stat-content">
<span className="stat-value">{collections.length}</span>
<span className="stat-label">Collections</span>
</div>
</div>
</div>
{/* Danger Zone - Delete Actions */}
<div className="danger-zone">
<div className="danger-zone-header">
<AlertTriangle size={20} />
<span>Danger Zone</span>
</div>
<div className="danger-zone-actions">
<button
type="button"
className="btn btn-warning"
onClick={(e) => { e.stopPropagation(); openDeleteModal(); }}
>
<Trash2 size={16} />
Delete Specific Questions
</button>
<button
type="button"
className="btn btn-danger"
onClick={(e) => { e.stopPropagation(); openClearAllModal(); }}
>
<AlertTriangle size={16} />
Clear Entire Vector Database
</button>
</div>
</div>
{/* View Tabs */}
<div className="view-tabs">
<button
type="button"
className={`tab-btn ${activeView === 'upload' ? 'active' : ''}`}
onClick={(e) => { e.stopPropagation(); setActiveView('upload'); }}
>
<Upload size={18} />
Upload & Train
</button>
<button
type="button"
className={`tab-btn ${activeView === 'stats' ? 'active' : ''}`}
onClick={(e) => { e.stopPropagation(); setActiveView('stats'); }}
>
<BarChart2 size={18} />
RAG Statistics
</button>
<button
type="button"
className={`tab-btn ${activeView === 'search' ? 'active' : ''}`}
onClick={(e) => { e.stopPropagation(); setActiveView('search'); }}
>
<Search size={18} />
Search Questions
</button>
</div>
{/* Upload & Train View */}
{activeView === 'upload' && (
<div className="rag-content">
{/* Upload Section */}
<section className="upload-section">
<h3><Upload size={20} /> Upload Interview PDFs</h3>
<div className="upload-zone">
<input
type="file"
id="pdf-upload"
accept=".pdf"
onChange={handleUpload}
style={{ display: 'none' }}
/>
<label htmlFor="pdf-upload" className="upload-label">
<FileText size={48} />
<span className="upload-text">Click to upload PDF</span>
<span className="upload-hint">Interview questions will be automatically extracted</span>
</label>
{uploadProgress && (
<div className={`upload-progress ${uploadProgress.status}`}>
{uploadProgress.status === 'uploading' && <Loader2 size={18} className="spin" />}
{uploadProgress.status === 'success' && <CheckCircle size={18} />}
{uploadProgress.status === 'error' && <XCircle size={18} />}
<span>{uploadProgress.filename}</span>
</div>
)}
</div>
</section>
{/* Train All Button */}
{untrainedPdfs.length > 0 && (
<div className="train-all-section">
<button
type="button"
className="btn btn-primary train-all-btn"
onClick={(e) => { e.stopPropagation(); trainAllPdfs(); }}
disabled={trainAllLoading}
>
{trainAllLoading ? (
<><Loader2 size={18} className="spin" /> Training All...</>
) : (
<><Zap size={18} /> Train All Untrained PDFs ({untrainedPdfs.length})</>
)}
</button>
</div>
)}
{/* PDF List */}
<section className="pdf-list-section">
<h3><FolderOpen size={20} /> Uploaded PDFs</h3>
{pdfsLoading ? (
<div className="loading-state">
<Loader2 size={32} className="spin" />
<p>Loading PDFs...</p>
</div>
) : pdfs.length === 0 ? (
<div className="empty-state">
<FileText size={48} />
<p>No PDFs uploaded yet</p>
<span>Upload interview question PDFs to train RAG</span>
</div>
) : (
<div className="pdf-grid">
{pdfs.map((pdf) => (
<div key={pdf.pdf_id} className={`pdf-card ${pdf.is_trained ? 'trained' : 'untrained'}`}>
<div className="pdf-icon">
<FileText size={32} />
{pdf.is_trained && (
<span className="trained-badge">
<CheckCircle size={14} />
</span>
)}
</div>
<div className="pdf-info">
<h4 title={pdf.original_filename}>{pdf.original_filename}</h4>
<div className="pdf-meta">
<span><Hash size={12} /> {formatFileSize(pdf.size_bytes)}</span>
{pdf.is_trained && (
<span><BookOpen size={12} /> {pdf.questions_extracted} questions</span>
)}
</div>
</div>
<div className="pdf-actions">
{!pdf.is_trained ? (
<>
<button
type="button"
className="btn btn-sm btn-primary"
onClick={(e) => { e.stopPropagation(); trainPdf(pdf.pdf_id); }}
disabled={trainingPdfId === pdf.pdf_id}
title="Train with auto-classification"
>
{trainingPdfId === pdf.pdf_id ? (
<><Loader2 size={14} className="spin" /> Training...</>
) : (
<><Play size={14} /> Train</>
)}
</button>
<button
type="button"
className="btn btn-sm btn-secondary"
onClick={(e) => { e.stopPropagation(); openTrainConfig(pdf.pdf_id); }}
disabled={trainingPdfId === pdf.pdf_id}
title="Configure classification options"
>
<Settings size={14} />
</button>
</>
) : (
<>
<button
type="button"
className="btn btn-sm btn-info"
onClick={(e) => { e.stopPropagation(); viewPdfSummary(pdf.pdf_id); }}
title="View training summary"
>
<Eye size={14} /> Summary
</button>
<span className="status-trained">
<CheckCircle size={14} /> Trained
</span>
</>
)}
<button
type="button"
className="btn btn-sm btn-danger"
onClick={(e) => { e.stopPropagation(); deletePdf(pdf.pdf_id); }}
>
<Trash2 size={14} />
</button>
</div>
</div>
))}
</div>
)}
</section>
{/* Training Configuration Modal */}
{showTrainConfig && configPdfId && (
<div className="modal-overlay" onClick={() => setShowTrainConfig(false)}>
<div className="train-config-modal" onClick={e => e.stopPropagation()}>
<h3><Settings size={20} /> Training Configuration</h3>
<p className="modal-subtitle">Configure classification for this PDF. Leave empty for auto-detection.</p>
<div className="config-form">
<div className="form-group">
<label><GitBranch size={14} /> Branch (optional)</label>
<select
value={trainConfig.branch}
onChange={(e) => setTrainConfig({
...trainConfig,
branch: e.target.value,
domain: '' // Reset domain when branch changes
})}
>
<option value="">Auto-detect branch</option>
{taxonomy.branches.map(b => (
<option key={b.id} value={b.id}>{b.name} ({b.shortName})</option>
))}
</select>
</div>
<div className="form-group">
<label><Briefcase size={14} /> Domain/Role (optional)</label>
<select
value={trainConfig.domain}
onChange={(e) => setTrainConfig({
...trainConfig,
domain: e.target.value
})}
>
<option value="">Auto-detect domain</option>
{getFilteredDomains(trainConfig.branch).map(d => (
<option key={d.id} value={d.id}>{d.name}</option>
))}
</select>
</div>
<div className="form-group">
<label><Tag size={14} /> Company (optional)</label>
<input
type="text"
value={trainConfig.company}
onChange={(e) => setTrainConfig({
...trainConfig,
company: e.target.value
})}
placeholder="e.g., Google, Amazon"
/>
</div>
<div className="form-group">
<label><BarChart2 size={14} /> Company Level</label>
<select
value={trainConfig.company_level}
onChange={(e) => setTrainConfig({
...trainConfig,
company_level: e.target.value
})}
>
<option value="product">Product-based</option>
<option value="service">Service-based</option>
<option value="startup">Startup</option>
</select>
</div>
</div>
<div className="modal-actions">
<button
type="button"
className="btn btn-secondary"
onClick={(e) => { e.stopPropagation(); setShowTrainConfig(false); }}
>
Cancel
</button>
<button
type="button"
className="btn btn-primary"
onClick={(e) => { e.stopPropagation(); trainPdf(configPdfId, trainConfig); }}
>
<Play size={14} /> Train PDF
</button>
</div>
</div>
</div>
)}
{/* Training Result */}
{trainResult && (
<section className="train-result">
<h3><CheckCircle size={20} /> Training Result</h3>
<div className="result-grid">
<div className="result-item">
<span className="result-label">Questions Found</span>
<span className="result-value">{trainResult.questions_found}</span>
</div>
<div className="result-item">
<span className="result-label">Questions Stored</span>
<span className="result-value">{trainResult.questions_stored}</span>
</div>
{trainResult.question_types && Object.keys(trainResult.question_types).length > 0 && (
<div className="result-breakdown">
<span className="result-label">By Type:</span>
<div className="breakdown-tags">
{Object.entries(trainResult.question_types).map(([type, count]) => (
<span key={type} className="tag">{type}: {count}</span>
))}
</div>
</div>
)}
{/* Classification breakdown */}
{trainResult.classification && (
<>
{trainResult.classification.branches && Object.keys(trainResult.classification.branches).length > 0 && (
<div className="result-breakdown classification">
<span className="result-label"><GitBranch size={14} /> By Branch:</span>
<div className="breakdown-tags">
{Object.entries(trainResult.classification.branches).map(([branch, count]) => (
<span key={branch} className="tag tag-branch">{branch}: {count}</span>
))}
</div>
</div>
)}
{trainResult.classification.domains && Object.keys(trainResult.classification.domains).length > 0 && (
<div className="result-breakdown classification">
<span className="result-label"><Briefcase size={14} /> By Domain:</span>
<div className="breakdown-tags">
{Object.entries(trainResult.classification.domains)
.sort((a, b) => b[1] - a[1])
.slice(0, 6)
.map(([domain, count]) => (
<span key={domain} className="tag tag-domain">{domain}: {count}</span>
))}
{Object.keys(trainResult.classification.domains).length > 6 && (
<span className="tag tag-more">+{Object.keys(trainResult.classification.domains).length - 6} more</span>
)}
</div>
</div>
)}
</>
)}
</div>
</section>
)}
</div>
)}
{/* Stats View */}
{activeView === 'stats' && (
<div className="rag-content">
<section className="stats-section">
<h3><BarChart2 size={20} /> RAG Configuration</h3>
{statsLoading ? (
<div className="loading-state">
<Loader2 size={32} className="spin" />
<p>Loading statistics...</p>
</div>
) : ragStats ? (
<div className="stats-details">
<div className="stat-row">
<span>Vector Store</span>
<span className="stat-value-text">{ragStats.vector_store?.type || 'FAISS'}</span>
</div>
<div className="stat-row">
<span>Questions Collection</span>
<span className="stat-value-text">{ragStats.questions_collection || 'interview_questions'}</span>
</div>
<div className="stat-row">
<span>Embedding Model</span>
<span className="stat-value-text">{ragStats.embedding_model || 'models/embedding-001'}</span>
</div>
<div className="stat-row">
<span>Total Documents</span>
<span className="stat-value-text">{ragStats.total_documents || 0}</span>
</div>
</div>
) : (
<div className="empty-state">
<AlertCircle size={48} />
<p>Could not load RAG statistics</p>
</div>
)}
</section>
<section className="collections-section">
<h3><Database size={20} /> Vector Collections</h3>
{collections.length === 0 ? (
<div className="empty-state small">
<Database size={32} />
<p>No collections yet</p>
</div>
) : (
<div className="collections-list">
{collections.map((col) => (
<div key={col.name} className="collection-item">
<div className="collection-info">
<span className="collection-name">{col.name}</span>
<span className="collection-count">{col.document_count} documents</span>
</div>
</div>
))}
</div>
)}
</section>
</div>
)}
{/* Search View */}
{activeView === 'search' && (
<div className="rag-content">
<section className="search-section">
<h3><Search size={20} /> Search Questions in RAG</h3>
<div className="search-form">
<div className="search-input-wrap">
<Search size={18} />
<input
type="text"
placeholder="Search for interview questions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && searchQuestions()}
/>
</div>
<div className="search-filters">
<select
value={searchFilters.question_type}
onChange={(e) => setSearchFilters(f => ({ ...f, question_type: e.target.value }))}
>
<option value="">All Types</option>
<option value="technical">Technical</option>
<option value="behavioral">Behavioral</option>
<option value="system_design">System Design</option>
<option value="coding">Coding</option>
</select>
<select
value={searchFilters.difficulty}
onChange={(e) => setSearchFilters(f => ({ ...f, difficulty: e.target.value }))}
>
<option value="">All Difficulties</option>
<option value="easy">Easy</option>
<option value="medium">Medium</option>
<option value="hard">Hard</option>
<option value="extreme">Extreme</option>
</select>
</div>
<button
type="button"
className="btn btn-primary"
onClick={(e) => { e.stopPropagation(); searchQuestions(); }}
disabled={searchLoading || !searchQuery.trim()}
>
{searchLoading ? (
<><Loader2 size={18} className="spin" /> Searching...</>
) : (
<><Search size={18} /> Search</>
)}
</button>
</div>
{/* Search Results */}
{searchResults && (
<div className="search-results">
<h4>Found {searchResults.results?.length || 0} questions</h4>
{searchResults.results?.map((result, index) => (
<div key={index} className="result-card">
<div className="result-question">
<BookOpen size={16} />
<span>{result.question || result.content}</span>
</div>
<div className="result-meta">
{result.score && (
<span className="score">
Relevance: {(result.score * 100).toFixed(1)}%
</span>
)}
{result.metadata?.question_type && (
<span className="tag type">{result.metadata.question_type}</span>
)}
{result.metadata?.difficulty && (
<span className="tag difficulty">{result.metadata.difficulty}</span>
)}
</div>
</div>
))}
</div>
)}
</section>
</div>
)}
{/* Training Summary Modal */}
{showTrainingSummary && (trainResult || selectedPdfSummary) && (
<div className="modal-overlay" onClick={() => { setShowTrainingSummary(false); setSelectedPdfSummary(null); }}>
<div className="modal training-summary-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<div className="modal-title">
<FileCheck size={24} />
<h2>{selectedPdfSummary ? 'PDF Summary' : 'Training Summary'}</h2>
{selectedPdfSummary?.pdfName && (
<span className="modal-subtitle-inline">{selectedPdfSummary.pdfName}</span>
)}
</div>
<button
type="button"
className="modal-close"
onClick={() => { setShowTrainingSummary(false); setSelectedPdfSummary(null); }}
>
<X size={20} />
</button>
</div>
<div className="modal-body">
{summaryLoading ? (
<div className="loading-state">
<Loader2 size={48} className="spin" />
<p>Loading summary...</p>
</div>
) : (() => {
const summary = selectedPdfSummary || trainResult;
if (!summary) return null;
return (
<>
{/* Quick Stats */}
<div className="summary-stats">
<div className="summary-stat-card highlight">
<div className="summary-stat-icon">
<BookOpen size={32} />
</div>
<div className="summary-stat-content">
<span className="summary-stat-value">{summary.questions_stored || summary.total_questions_stored || 0}</span>
<span className="summary-stat-label">Questions Extracted</span>
</div>
</div>
<div className="summary-stat-card">
<div className="summary-stat-icon">
<FileText size={28} />
</div>
<div className="summary-stat-content">
<span className="summary-stat-value">{summary.questions_found || summary.total_questions_found || 0}</span>
<span className="summary-stat-label">Questions Found</span>
</div>
</div>
{summary.pdfs_processed && (
<div className="summary-stat-card">
<div className="summary-stat-icon">
<FolderOpen size={28} />
</div>
<div className="summary-stat-content">
<span className="summary-stat-value">{summary.pdfs_processed}</span>
<span className="summary-stat-label">PDFs Processed</span>
</div>
</div>
)}
</div>
{/* Domain Breakdown */}
{summary.classification?.domains && Object.keys(summary.classification.domains).length > 0 && (
<div className="summary-section">
<h3>
<Briefcase size={18} />
Questions by Domain
</h3>
<div className="domain-breakdown">
{Object.entries(summary.classification.domains)
.sort((a, b) => b[1] - a[1])
.map(([domain, count]) => {
const total = summary.questions_stored || summary.total_questions_stored || 1;
const percentage = Math.round((count / total) * 100);
return (
<div key={domain} className="domain-item">
<div className="domain-info">
<span className="domain-name">{formatName(domain)}</span>
<span className="domain-count">{count} questions</span>
</div>
<div className="domain-bar">
<div
className="domain-bar-fill"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="domain-percentage">{percentage}%</span>
</div>
);
})}
</div>
</div>
)}
{/* Branch Breakdown */}
{summary.classification?.branches && Object.keys(summary.classification.branches).length > 0 && (
<div className="summary-section">
<h3>
<GitBranch size={18} />
Questions by Branch
</h3>
<div className="branch-breakdown">
{Object.entries(summary.classification.branches)
.sort((a, b) => b[1] - a[1])
.map(([branch, count]) => (
<div key={branch} className="branch-badge">
<span className="branch-name">{formatName(branch)}</span>
<span className="branch-count">{count}</span>
</div>
))}
</div>
</div>
)}
{/* Question Types */}
{summary.question_types && Object.keys(summary.question_types).length > 0 && (
<div className="summary-section">
<h3>
<Tag size={18} />
Question Types
</h3>
<div className="type-breakdown">
{Object.entries(summary.question_types)
.sort((a, b) => b[1] - a[1])
.map(([type, count]) => (
<div key={type} className="type-badge">
<span className="type-name">{formatName(type)}</span>
<span className="type-count">{count}</span>
</div>
))}
</div>
</div>
)}
{/* Difficulty Distribution */}
{summary.difficulties && Object.keys(summary.difficulties).length > 0 && (
<div className="summary-section">
<h3>
<BarChart2 size={18} />
Difficulty Distribution
</h3>
<div className="difficulty-breakdown">
{Object.entries(summary.difficulties)
.map(([difficulty, count]) => (
<div key={difficulty} className={`difficulty-badge ${difficulty}`}>
<span className="difficulty-name">{formatName(difficulty)}</span>
<span className="difficulty-count">{count}</span>
</div>
))}
</div>
</div>
)}
{/* Extracted Questions List */}
{summary.classification?.questions_detail && summary.classification.questions_detail.length > 0 && (
<div className="summary-section questions-list-section">
<h3>
<BookOpen size={18} />
Extracted Questions ({summary.classification.questions_detail.length})
</h3>
<div className="questions-list">
{summary.classification.questions_detail.map((q, idx) => (
<div key={idx} className="question-item">
{/* Source Badge: R = RAG (always R since these are from PDFs) */}
<span
className="source-badge source-rag"
title="R = RAG Question (from trained PDF)"
>
R
</span>
<div className="question-number">{q.question_number}</div>
<div className="question-content">
<p className="question-text">{q.question_text}</p>
<div className="question-meta">
<span className="tag tag-branch">{q.branch_name}</span>
<span className="tag tag-domain">{q.domain_name}</span>
<span className={`tag tag-type ${q.question_type}`}>{formatName(q.question_type)}</span>
<span className={`tag tag-difficulty ${q.difficulty}`}>{formatName(q.difficulty)}</span>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* No Classification Data Message */}
{(!summary.classification ||
((!summary.classification.domains || Object.keys(summary.classification.domains).length === 0) &&
(!summary.classification.branches || Object.keys(summary.classification.branches).length === 0))) && (
<div className="empty-state small">
<AlertCircle size={32} />
<p>No classification data available</p>
<span>This PDF may have been trained before classification was added</span>
</div>
)}
</>
);
})()}
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={() => { setShowTrainingSummary(false); setSelectedPdfSummary(null); }}
>
<CheckCircle size={16} />
Done
</button>
</div>
</div>
</div>
)}
{/* Delete Specific Questions Modal */}
{showDeleteModal && (
<div className="modal-overlay" onClick={() => setShowDeleteModal(false)}>
<div className="modal-content delete-modal large" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>
<Trash2 size={24} />
Delete Questions
</h2>
<button
type="button"
className="close-btn"
onClick={() => setShowDeleteModal(false)}
>
<X size={20} />
</button>
</div>
<div className="modal-body">
{error && (
<div className="alert alert-error" style={{ marginBottom: '16px' }}>
<AlertCircle size={18} />
{error}
</div>
)}
{questionsLoading ? (
<div className="loading-state">
<Loader2 size={32} className="spin" />
<p>Loading questions...</p>
</div>
) : allQuestions.length === 0 ? (
<div className="empty-state">
<Database size={48} />
<p>No questions in vector database</p>
<span style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
Train a PDF first to add questions
</span>
</div>
) : (
<>
<div className="delete-modal-toolbar">
<div className="selection-info">
<span>{selectedQuestions.size} of {allQuestions.length} selected</span>
</div>
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={(e) => { e.stopPropagation(); selectAllQuestions(); }}
>
{selectedQuestions.size === allQuestions.length ? 'Deselect All' : 'Select All'}
</button>
</div>
<div className="questions-delete-list">
{allQuestions.map((q) => (
<div
key={q.chunk_id}
className={`question-delete-item ${selectedQuestions.has(q.chunk_id) ? 'selected' : ''}`}
onClick={() => toggleQuestionSelection(q.chunk_id)}
>
<input
type="checkbox"
checked={selectedQuestions.has(q.chunk_id)}
onChange={() => toggleQuestionSelection(q.chunk_id)}
onClick={(e) => e.stopPropagation()}
/>
{/* Source Badge: R = RAG (always R since these are from PDFs) */}
<span
className="source-badge source-rag"
title="R = RAG Question (from trained PDF)"
>
R
</span>
<div className="question-delete-content">
<p className="question-text">{q.content}</p>
<div className="question-meta">
<span className="tag tag-branch">{q.branch_name}</span>
<span className="tag tag-domain">{q.domain_name}</span>
<span className="tag tag-source">{q.source_file}</span>
</div>
</div>
</div>
))}
</div>
</>
)}
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={() => setShowDeleteModal(false)}
>
Cancel
</button>
<button
type="button"
className="btn btn-danger"
onClick={(e) => { e.stopPropagation(); deleteSelectedQuestions(); }}
disabled={selectedQuestions.size === 0 || deleteLoading}
>
{deleteLoading ? (
<>
<Loader2 size={16} className="spin" />
Deleting...
</>
) : (
<>
<Trash2 size={16} />
Delete {selectedQuestions.size} Questions
</>
)}
</button>
</div>
</div>
</div>
)}
{/* Clear All Vector Database Modal */}
{showClearAllModal && (
<div className="modal-overlay" onClick={() => setShowClearAllModal(false)}>
<div className="modal-content clear-all-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header danger">
<h2>
<AlertTriangle size={24} />
Clear Entire Vector Database
</h2>
<button
type="button"
className="close-btn"
onClick={() => setShowClearAllModal(false)}
>
<X size={20} />
</button>
</div>
<div className="modal-body">
<div className="danger-warning">
<div className="warning-icon">
<Shield size={48} />
</div>
<h3>⚠️ This action is IRREVERSIBLE!</h3>
<p>
You are about to delete <strong>ALL questions</strong> from the vector database.
This includes all questions extracted from PDFs.
</p>
<p>
Current database contains approximately <strong>{ragStats?.total_documents || 0} questions</strong>.
</p>
</div>
<div className="confirmation-checkboxes">
<label className="checkbox-label danger">
<input
type="checkbox"
checked={confirmDataLoss}
onChange={(e) => setConfirmDataLoss(e.target.checked)}
/>
<span>I understand that all RAG questions will be permanently deleted</span>
</label>
<label className="checkbox-label danger">
<input
type="checkbox"
checked={confirmNoBackup}
onChange={(e) => setConfirmNoBackup(e.target.checked)}
/>
<span>I understand that this action cannot be undone and there is no backup</span>
</label>
</div>
<div className="password-section">
<label>
<Lock size={16} />
Enter admin password to confirm:
</label>
<input
type="password"
value={clearAllPassword}
onChange={(e) => setClearAllPassword(e.target.value)}
placeholder="Enter deletion password"
className="password-input"
disabled={lockoutStatus.is_locked}
/>
{/* Lockout Warning */}
{lockoutStatus.is_locked && lockoutCountdown ? (
<div className="lockout-warning" style={{
marginTop: '12px',
padding: '12px',
background: 'rgba(239, 68, 68, 0.15)',
border: '1px solid rgba(239, 68, 68, 0.4)',
borderRadius: '8px',
color: '#ef4444',
fontSize: '14px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}>
<AlertTriangle size={18} />
<div>
<strong>🔒 Account Locked</strong>
<p style={{ margin: '4px 0 0 0', fontSize: '13px' }}>
Too many failed attempts. Try again in: <strong>
{lockoutCountdown >= 3600
? `${Math.floor(lockoutCountdown / 3600)}h ${Math.floor((lockoutCountdown % 3600) / 60)}m`
: lockoutCountdown >= 60
? `${Math.floor(lockoutCountdown / 60)}m ${lockoutCountdown % 60}s`
: `${lockoutCountdown}s`
}
</strong>
</p>
</div>
</div>
) : lockoutStatus.remaining_attempts < 5 && (
<div className="attempts-warning" style={{
marginTop: '12px',
padding: '10px 12px',
background: lockoutStatus.remaining_attempts <= 2
? 'rgba(239, 68, 68, 0.1)'
: 'rgba(245, 158, 11, 0.1)',
border: `1px solid ${lockoutStatus.remaining_attempts <= 2
? 'rgba(239, 68, 68, 0.3)'
: 'rgba(245, 158, 11, 0.3)'}`,
borderRadius: '6px',
color: lockoutStatus.remaining_attempts <= 2 ? '#ef4444' : '#f59e0b',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}>
<AlertTriangle size={16} />
<span>
<strong>{lockoutStatus.remaining_attempts}</strong> attempt{lockoutStatus.remaining_attempts !== 1 ? 's' : ''} remaining
{lockoutStatus.stage === 0 && ' (then 2 min lockout)'}
{lockoutStatus.stage === 1 && ' (then 10 min lockout)'}
{lockoutStatus.stage >= 2 && ' (then 24 hour lockout)'}
</span>
</div>
)}
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={() => setShowClearAllModal(false)}
>
Cancel
</button>
<button
type="button"
className="btn btn-danger"
onClick={(e) => { e.stopPropagation(); clearAllVectorDatabase(); }}
disabled={!confirmDataLoss || !confirmNoBackup || !clearAllPassword || clearAllLoading || lockoutStatus.is_locked}
>
{clearAllLoading ? (
<>
<Loader2 size={16} className="spin" />
Clearing...
</>
) : lockoutStatus.is_locked ? (
<>
<Lock size={16} />
Locked
</>
) : (
<>
<AlertTriangle size={16} />
Clear All Data
</>
)}
</button>
</div>
</div>
</div>
)}
</div>
);
}
export default RAGQuestionsPanel;