/** * Questions Browser Panel - Admin Panel for Managing Interview Questions * * FLOW: * 1. Click Questions → See all branches * 2. Click Branch → See all domains for that branch * 3. Click Domain → See all modes (friendly, balanced, strict, final_boss) * 4. Click Mode → See questions with Add/Remove buttons * 5. Add Question → Form with question and answer fields */ import React, { useState, useEffect, useCallback } from 'react'; import { HelpCircle, Search, ChevronRight, ChevronDown, Loader2, RefreshCw, AlertCircle, BookOpen, Smile, Scale, Target, Skull, Plus, Trash2, X, GitBranch, Briefcase, BarChart2, ChevronLeft, Save, Cpu, Zap, Settings, Building, Layers, PenTool, Database, Brain } from 'lucide-react'; import { getToken } from '../../services/api'; import './QuestionsPanel.css'; const API_BASE = import.meta.env.VITE_API_URL || ''; // Branch icons mapping const BRANCH_ICONS = { computer_science: Cpu, electronics: Zap, electrical: Zap, mechanical: Settings, civil: Building, architecture: PenTool, }; // Interview modes with their metadata const MODES = [ { id: 'friendly', name: 'Friendly', icon: Smile, color: '#10b981', description: 'Easy conversational questions' }, { id: 'balanced', name: 'Balanced', icon: Scale, color: '#3b82f6', description: 'Mix of easy and moderate questions' }, { id: 'strict', name: 'Strict', icon: Target, color: '#f59e0b', description: 'Technical deep-dive questions' }, { id: 'final_boss', name: 'Final Boss', icon: Skull, color: '#ef4444', description: 'Most challenging questions' }, ]; function QuestionsPanel() { // Navigation state - tracks current step const [currentStep, setCurrentStep] = useState('branches'); // branches, domains, modes, source, questions // Selection state const [selectedBranch, setSelectedBranch] = useState(null); const [selectedDomain, setSelectedDomain] = useState(null); const [selectedMode, setSelectedMode] = useState(null); const [selectedSource, setSelectedSource] = useState('collected'); // 'collected' or 'rag' // Data state const [branches, setBranches] = useState([]); const [domains, setDomains] = useState([]); const [questions, setQuestions] = useState([]); const [stats, setStats] = useState(null); // Loading states const [branchesLoading, setBranchesLoading] = useState(true); const [domainsLoading, setDomainsLoading] = useState(false); const [questionsLoading, setQuestionsLoading] = useState(false); const [statsLoading, setStatsLoading] = useState(true); // Search and pagination const [searchQuery, setSearchQuery] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [totalQuestions, setTotalQuestions] = useState(0); const [totalPages, setTotalPages] = useState(1); const pageSize = 20; // UI state const [error, setError] = useState(null); const [expandedQuestion, setExpandedQuestion] = useState(null); // Add Question Modal state const [showAddModal, setShowAddModal] = useState(false); const [newQuestion, setNewQuestion] = useState({ question: '', answer: '' }); const [addingQuestion, setAddingQuestion] = useState(false); /** * Fetch overall statistics */ const fetchStats = useCallback(async () => { setStatsLoading(true); try { const token = getToken(); const response = await fetch(`${API_BASE}/api/admin/questions/stats`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); if (data.success) { setStats(data.stats); } } } catch (err) { console.error('Error fetching stats:', err); } finally { setStatsLoading(false); } }, []); /** * Fetch available branches */ const fetchBranches = useCallback(async () => { setBranchesLoading(true); setError(null); try { const token = getToken(); const response = await fetch(`${API_BASE}/api/admin/questions/branches`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); if (data.success) { setBranches(data.branches || []); } } else { throw new Error('Failed to fetch branches'); } } catch (err) { console.error('Error fetching branches:', err); setError('Failed to load branches'); } finally { setBranchesLoading(false); } }, []); /** * Fetch domains for a specific branch */ const fetchDomains = useCallback(async (branchId) => { setDomainsLoading(true); setError(null); try { const token = getToken(); const url = `${API_BASE}/api/admin/questions/domains?branch=${branchId}`; const response = await fetch(url, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); if (data.success) { setDomains(data.domains || []); } } else { throw new Error('Failed to fetch domains'); } } catch (err) { console.error('Error fetching domains:', err); setError('Failed to load domains'); } finally { setDomainsLoading(false); } }, []); /** * Fetch questions based on current selection */ const fetchQuestions = useCallback(async () => { if (!selectedMode || !selectedBranch || !selectedDomain) return; setQuestionsLoading(true); setError(null); try { const token = getToken(); const params = new URLSearchParams({ mode: selectedMode, page: currentPage.toString(), limit: pageSize.toString(), branch: selectedBranch, domain: selectedDomain }); if (searchQuery) params.append('search', searchQuery); // Use RAG endpoint for RAG source, otherwise use collected questions const endpoint = selectedSource === 'rag' ? `${API_BASE}/api/admin/questions/rag?${params}` : `${API_BASE}/api/admin/questions/all?${params}`; const response = await fetch(endpoint, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); if (data.success) { // DEBUG: Log question sources console.log('[QuestionsPanel] Received questions:', data.questions?.length); console.log('[QuestionsPanel] Full first question:', data.questions?.[0]); console.log('[QuestionsPanel] Sample sources:', data.questions?.slice(0, 3).map(q => ({ id: q.id, source: q.source, question: q.question?.substring(0, 50) + '...', question_text: q.question_text?.substring(0, 50) + '...', }))); setQuestions(data.questions || []); setTotalQuestions(data.total || 0); setTotalPages(data.total_pages || 1); } } else { throw new Error('Failed to fetch questions'); } } catch (err) { console.error('Error fetching questions:', err); setError('Failed to load questions'); setQuestions([]); } finally { setQuestionsLoading(false); } }, [selectedMode, selectedBranch, selectedDomain, searchQuery, currentPage, pageSize, selectedSource]); // Initial load - fetch branches useEffect(() => { fetchStats(); fetchBranches(); }, [fetchStats, fetchBranches]); // Fetch questions when on questions step useEffect(() => { if (currentStep === 'questions' && selectedMode) { fetchQuestions(); } }, [currentStep, selectedMode, fetchQuestions, currentPage, searchQuery]); /** * Handle branch selection */ const handleBranchSelect = (branch) => { setSelectedBranch(branch.id); setSelectedDomain(null); setSelectedMode(null); setQuestions([]); fetchDomains(branch.id); setCurrentStep('domains'); }; /** * Handle domain selection */ const handleDomainSelect = (domain) => { setSelectedDomain(domain.id); setSelectedMode(null); setQuestions([]); setCurrentStep('modes'); }; /** * Handle mode selection */ const handleModeSelect = (modeId) => { setSelectedMode(modeId); setCurrentPage(1); setSearchQuery(''); // For strict and final_boss, show source selection // For friendly and balanced, go directly to questions (collected only) if (modeId === 'strict' || modeId === 'final_boss') { setCurrentStep('source'); } else { setSelectedSource('collected'); setCurrentStep('questions'); } }; /** * Handle source selection (Collected vs RAG) */ const handleSourceSelect = (source) => { setSelectedSource(source); setCurrentPage(1); setSearchQuery(''); setCurrentStep('questions'); }; /** * Go back to previous step */ const handleBack = () => { if (currentStep === 'questions') { // If came from source selection, go back there if (selectedMode === 'strict' || selectedMode === 'final_boss') { setCurrentStep('source'); } else { setCurrentStep('modes'); setSelectedMode(null); } setQuestions([]); } else if (currentStep === 'source') { setCurrentStep('modes'); setSelectedMode(null); setSelectedSource('collected'); } else if (currentStep === 'modes') { setCurrentStep('domains'); setSelectedDomain(null); } else if (currentStep === 'domains') { setCurrentStep('branches'); setSelectedBranch(null); setDomains([]); } }; /** * Get breadcrumb path */ const getBreadcrumb = () => { const parts = []; if (selectedBranch) { const branch = branches.find(b => b.id === selectedBranch); parts.push({ label: branch?.shortName || branch?.name || selectedBranch, type: 'branch' }); } if (selectedDomain) { const domain = domains.find(d => d.id === selectedDomain); parts.push({ label: domain?.name || selectedDomain, type: 'domain' }); } if (selectedMode) { const mode = MODES.find(m => m.id === selectedMode); parts.push({ label: mode?.name || selectedMode, type: 'mode' }); } if (currentStep === 'questions' && (selectedMode === 'strict' || selectedMode === 'final_boss')) { parts.push({ label: selectedSource === 'rag' ? 'RAG Questions' : 'Collected Questions', type: 'source' }); } return parts; }; /** * Handle add question */ const handleAddQuestion = async () => { if (!newQuestion.question.trim()) { alert('Please enter a question'); return; } setAddingQuestion(true); try { const token = getToken(); const response = await fetch(`${API_BASE}/api/admin/questions/add`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ question: newQuestion.question, answer: newQuestion.answer, branch: selectedBranch, domain: selectedDomain, mode: selectedMode, difficulty: selectedMode === 'friendly' ? 'easy' : selectedMode === 'final_boss' ? 'hard' : 'medium' }) }); if (response.ok) { setShowAddModal(false); setNewQuestion({ question: '', answer: '' }); // Refresh questions fetchQuestions(); } else { const data = await response.json(); alert(data.detail || 'Failed to add question'); } } catch (err) { console.error('Error adding question:', err); alert('Failed to add question'); } finally { setAddingQuestion(false); } }; /** * Handle delete question */ const handleDeleteQuestion = async (questionId) => { if (!confirm('Are you sure you want to delete this question?')) { return; } try { const token = getToken(); const response = await fetch(`${API_BASE}/api/admin/questions/${questionId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (response.ok) { // Remove from local state setQuestions(questions.filter(q => q.id !== questionId)); setTotalQuestions(prev => prev - 1); } else { const data = await response.json(); alert(data.detail || 'Failed to delete question'); } } catch (err) { console.error('Error deleting question:', err); alert('Failed to delete question'); } }; /** * Render Step 1: Branch Selection */ const renderBranchSelection = () => (
Choose an engineering branch to view its domains
{branchesLoading ? (Loading branches...
Choose a specific role/domain
{domainsLoading ? (Loading domains...
No domains found for this branch
Choose difficulty level for questions
Choose where to load questions from for {currentMode?.name} mode
Loading questions...
No RAG questions found
Upload PDFs in the RAG Management panel and click "Train RAG" to generate questions. Questions will be auto-classified into the correct branch and domain. > ) : ( <>No questions found
Try adjusting your search or add new questions > )}Branch: {branches.find(b => b.id === selectedBranch)?.name}
Domain: {domains.find(d => d.id === selectedDomain)?.name}
Mode: {MODES.find(m => m.id === selectedMode)?.name}
Browse and manage interview questions