Download frontend/src/components/admin/RAGQuestionsPanel.jsx from chowdary1/ai-interviewer-version-2: direct link, hf CLI and curl.
- Browser
- Download file 92 kB
-
https://huggingface.co/spaces/chowdary1/ai-interviewer-version-2/resolve/main/frontend/src/components/admin/RAGQuestionsPanel.jsx
- Command line
-
hf download hf://spaces/chowdary1/ai-interviewer-version-2/frontend/src/components/admin/RAGQuestionsPanel.jsx
-
curl -L -o RAGQuestionsPanel.jsx https://huggingface.co/spaces/chowdary1/ai-interviewer-version-2/resolve/main/frontend/src/components/admin/RAGQuestionsPanel.jsx
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; |