/** * 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 = () => (

Select Branch

Choose an engineering branch to view its domains

{branchesLoading ? (

Loading branches...

) : (
{branches.map((branch) => { const Icon = BRANCH_ICONS[branch.id] || Layers; return ( ); })}
)} {/* Stats Overview */} {stats && !statsLoading && (

Question Statistics

{stats.total || 0} Total Questions
{stats.collected?.by_difficulty?.easy || 0} Easy
{stats.collected?.by_difficulty?.medium || 0} Medium
{stats.collected?.by_difficulty?.hard || 0} Hard
)}
); /** * Render Step 2: Domain Selection */ const renderDomainSelection = () => (

Select Domain

Choose a specific role/domain

{domainsLoading ? (

Loading domains...

) : domains.length === 0 ? (

No domains found for this branch

) : (
{domains.map((domain) => ( ))}
)}
); /** * Render Step 3: Mode Selection */ const renderModeSelection = () => (

Select Interview Mode

Choose difficulty level for questions

{MODES.map((mode) => { const Icon = mode.icon; return ( ); })}
); /** * Render Step 3.5: Source Selection (for Strict/Final Boss) */ const renderSourceSelection = () => { const currentMode = MODES.find(m => m.id === selectedMode); return (

Select Question Source

Choose where to load questions from for {currentMode?.name} mode

{/* Collected Questions */} {/* RAG Questions */}
RAG questions are generated when you upload PDFs and click "Train RAG" in the RAG Management panel. The system automatically extracts and classifies questions into the correct branch and domain.
); }; /** * Render Step 4: Questions List */ const renderQuestionsList = () => { const currentMode = MODES.find(m => m.id === selectedMode); return (
{/* Action Bar */}
{ setSearchQuery(e.target.value); setCurrentPage(1); }} /> {searchQuery && ( )}
{/* Show RAG indicator when viewing RAG questions */} {selectedSource === 'rag' && ( RAG Questions )} {/* Only show Add button for collected questions */} {selectedSource !== 'rag' && ( )}
{/* Questions count */}
{totalQuestions} questions found {totalPages > 1 && ( Page {currentPage} of {totalPages} )}
{/* Loading state */} {questionsLoading ? (

Loading questions...

) : questions.length === 0 ? (
{selectedSource === 'rag' ? ( <>

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 )}
) : ( <> {/* Questions list */}
{questions.map((question, index) => (
setExpandedQuestion(expandedQuestion === index ? null : index)} > {/* Source Badge: R=RAG, D=Database, A=API, F=Fallback - Always visible */} {(() => { // Determine badge class and text let badgeClass = 'database'; let badgeText = 'D'; let badgeTitle = 'D = Database Question (collected)'; if (selectedSource === 'rag') { badgeClass = 'rag'; badgeText = 'R'; badgeTitle = 'R = RAG Question (from trained PDFs)'; } else if (question.source === 'openai_api') { badgeClass = 'api'; badgeText = 'A'; badgeTitle = 'A = API Generated Question (OpenAI)'; } else if (question.source === 'rag_engine') { badgeClass = 'rag'; badgeText = 'R'; badgeTitle = 'R = RAG Question (from trained PDFs)'; } else if (question.source === 'fallback') { badgeClass = 'fallback'; badgeText = 'F'; badgeTitle = 'F = Fallback Question (template)'; } return ( {badgeText} ); })()} #{(currentPage - 1) * pageSize + index + 1} {question.question || question.question_text || '[No question text available]'}
{/* Only show delete for collected questions */} {selectedSource !== 'rag' && ( )}
{expandedQuestion === index && (
{(question.answer || question.expected_answer_points?.length > 0) && (
Expected Answer: {question.answer || question.expected_answer_points?.join(', ') || 'N/A'}
)}
{/* Source Badge in expanded view */} {(() => { let badgeClass = 'database'; let badgeText = 'D'; let badgeTitle = 'Database'; if (selectedSource === 'rag') { badgeClass = 'rag'; badgeText = 'R'; badgeTitle = 'RAG'; } else if (question.source === 'openai_api') { badgeClass = 'api'; badgeText = 'A'; badgeTitle = 'API Generated'; } else if (question.source === 'rag_engine') { badgeClass = 'rag'; badgeText = 'R'; badgeTitle = 'RAG'; } else if (question.source === 'fallback') { badgeClass = 'fallback'; badgeText = 'F'; badgeTitle = 'Fallback'; } return ( {badgeText} ); })()} {question.difficulty && ( {question.difficulty} )} {question.question_type && ( {question.question_type} )} {/* Show actual source value for debugging */} src: {question.source || 'none'}
)}
))}
{/* Pagination */} {totalPages > 1 && (
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { let pageNum; if (totalPages <= 5) { pageNum = i + 1; } else if (currentPage <= 3) { pageNum = i + 1; } else if (currentPage >= totalPages - 2) { pageNum = totalPages - 4 + i; } else { pageNum = currentPage - 2 + i; } return ( ); })}
)} )}
); }; /** * Render Add Question Modal */ const renderAddModal = () => { if (!showAddModal) return null; return (
setShowAddModal(false)}>
e.stopPropagation()}>

Add New Question