Spaces:
Sleeping
Sleeping
Download frontend/src/components/admin/QuestionsPanel.jsx from chowdary1/ai-interviewer-version-2: direct link, hf CLI and curl.
- Browser
- Download file 49.1 kB
-
https://huggingface.co/spaces/chowdary1/ai-interviewer-version-2/resolve/main/frontend/src/components/admin/QuestionsPanel.jsx
- Command line
-
hf download hf://spaces/chowdary1/ai-interviewer-version-2/frontend/src/components/admin/QuestionsPanel.jsx
-
curl -L -o QuestionsPanel.jsx https://huggingface.co/spaces/chowdary1/ai-interviewer-version-2/resolve/main/frontend/src/components/admin/QuestionsPanel.jsx
49.1 kB
| /** | |
| * 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 = () => ( | |
| <div className="selection-grid"> | |
| <h3 className="selection-title"> | |
| <GitBranch size={20} /> | |
| Select Branch | |
| </h3> | |
| <p className="selection-hint">Choose an engineering branch to view its domains</p> | |
| {branchesLoading ? ( | |
| <div className="loading-state"> | |
| <Loader2 size={32} className="spin" /> | |
| <p>Loading branches...</p> | |
| </div> | |
| ) : ( | |
| <div className="cards-grid branches-grid"> | |
| {branches.map((branch) => { | |
| const Icon = BRANCH_ICONS[branch.id] || Layers; | |
| return ( | |
| <button | |
| key={branch.id} | |
| className="selection-card branch-card" | |
| onClick={() => handleBranchSelect(branch)} | |
| > | |
| <div className="card-icon"> | |
| <Icon size={28} /> | |
| </div> | |
| <div className="card-info"> | |
| <span className="card-name">{branch.name}</span> | |
| <span className="card-short">{branch.shortName}</span> | |
| </div> | |
| <ChevronRight size={20} className="card-arrow" /> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| {/* Stats Overview */} | |
| {stats && !statsLoading && ( | |
| <div className="stats-overview"> | |
| <h4><BarChart2 size={16} /> Question Statistics</h4> | |
| <div className="stats-grid"> | |
| <div className="stat-item highlight"> | |
| <span className="stat-value">{stats.total || 0}</span> | |
| <span className="stat-label">Total Questions</span> | |
| </div> | |
| <div className="stat-item"> | |
| <span className="stat-value">{stats.collected?.by_difficulty?.easy || 0}</span> | |
| <span className="stat-label">Easy</span> | |
| </div> | |
| <div className="stat-item"> | |
| <span className="stat-value">{stats.collected?.by_difficulty?.medium || 0}</span> | |
| <span className="stat-label">Medium</span> | |
| </div> | |
| <div className="stat-item"> | |
| <span className="stat-value">{stats.collected?.by_difficulty?.hard || 0}</span> | |
| <span className="stat-label">Hard</span> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| /** | |
| * Render Step 2: Domain Selection | |
| */ | |
| const renderDomainSelection = () => ( | |
| <div className="selection-grid"> | |
| <h3 className="selection-title"> | |
| <Briefcase size={20} /> | |
| Select Domain | |
| </h3> | |
| <p className="selection-hint">Choose a specific role/domain</p> | |
| {domainsLoading ? ( | |
| <div className="loading-state"> | |
| <Loader2 size={32} className="spin" /> | |
| <p>Loading domains...</p> | |
| </div> | |
| ) : domains.length === 0 ? ( | |
| <div className="empty-state"> | |
| <Briefcase size={48} /> | |
| <p>No domains found for this branch</p> | |
| </div> | |
| ) : ( | |
| <div className="cards-grid domains-grid"> | |
| {domains.map((domain) => ( | |
| <button | |
| key={domain.id} | |
| className="selection-card domain-card" | |
| onClick={() => handleDomainSelect(domain)} | |
| > | |
| <div className="card-info"> | |
| <span className="card-name">{domain.name}</span> | |
| <span className="card-id">{domain.id}</span> | |
| </div> | |
| <ChevronRight size={20} className="card-arrow" /> | |
| </button> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| /** | |
| * Render Step 3: Mode Selection | |
| */ | |
| const renderModeSelection = () => ( | |
| <div className="selection-grid"> | |
| <h3 className="selection-title"> | |
| <BookOpen size={20} /> | |
| Select Interview Mode | |
| </h3> | |
| <p className="selection-hint">Choose difficulty level for questions</p> | |
| <div className="cards-grid modes-grid"> | |
| {MODES.map((mode) => { | |
| const Icon = mode.icon; | |
| return ( | |
| <button | |
| key={mode.id} | |
| className="selection-card mode-card" | |
| onClick={() => handleModeSelect(mode.id)} | |
| style={{ '--mode-color': mode.color }} | |
| > | |
| <div className="card-icon mode-icon"> | |
| <Icon size={28} /> | |
| </div> | |
| <div className="card-info"> | |
| <span className="card-name">{mode.name}</span> | |
| <span className="card-description">{mode.description}</span> | |
| </div> | |
| <ChevronRight size={20} className="card-arrow" /> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| /** | |
| * Render Step 3.5: Source Selection (for Strict/Final Boss) | |
| */ | |
| const renderSourceSelection = () => { | |
| const currentMode = MODES.find(m => m.id === selectedMode); | |
| return ( | |
| <div className="selection-grid"> | |
| <h3 className="selection-title"> | |
| <Database size={20} /> | |
| Select Question Source | |
| </h3> | |
| <p className="selection-hint"> | |
| Choose where to load questions from for <strong style={{ color: currentMode?.color }}>{currentMode?.name}</strong> mode | |
| </p> | |
| <div className="cards-grid source-grid"> | |
| {/* Collected Questions */} | |
| <button | |
| className="selection-card source-card collected" | |
| onClick={() => handleSourceSelect('collected')} | |
| > | |
| <div className="card-icon source-icon"> | |
| <Database size={32} /> | |
| </div> | |
| <div className="card-info"> | |
| <span className="card-name">Collected Questions</span> | |
| <span className="card-description"> | |
| Pre-harvested questions from JSON files. | |
| These are curated interview questions organized by domain. | |
| </span> | |
| </div> | |
| <ChevronRight size={20} className="card-arrow" /> | |
| </button> | |
| {/* RAG Questions */} | |
| <button | |
| className="selection-card source-card rag" | |
| onClick={() => handleSourceSelect('rag')} | |
| > | |
| <div className="card-icon source-icon rag-icon"> | |
| <Brain size={32} /> | |
| </div> | |
| <div className="card-info"> | |
| <span className="card-name">RAG Questions</span> | |
| <span className="card-description"> | |
| Questions extracted from uploaded PDFs via AI training. | |
| Auto-classified into branches and domains. | |
| </span> | |
| <span className="card-badge rag-badge"> | |
| <Brain size={12} /> AI-Trained | |
| </span> | |
| </div> | |
| <ChevronRight size={20} className="card-arrow" /> | |
| </button> | |
| </div> | |
| <div className="source-info"> | |
| <AlertCircle size={16} /> | |
| <span> | |
| 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. | |
| </span> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| /** | |
| * Render Step 4: Questions List | |
| */ | |
| const renderQuestionsList = () => { | |
| const currentMode = MODES.find(m => m.id === selectedMode); | |
| return ( | |
| <div className="questions-section"> | |
| {/* Action Bar */} | |
| <div className="questions-action-bar"> | |
| <div className="search-box"> | |
| <Search size={18} /> | |
| <input | |
| type="text" | |
| placeholder="Search questions..." | |
| value={searchQuery} | |
| onChange={(e) => { | |
| setSearchQuery(e.target.value); | |
| setCurrentPage(1); | |
| }} | |
| /> | |
| {searchQuery && ( | |
| <button | |
| className="clear-search" | |
| onClick={() => setSearchQuery('')} | |
| > | |
| <X size={16} /> | |
| </button> | |
| )} | |
| </div> | |
| {/* Show RAG indicator when viewing RAG questions */} | |
| {selectedSource === 'rag' && ( | |
| <span className="rag-indicator"> | |
| <Brain size={14} /> | |
| RAG Questions | |
| </span> | |
| )} | |
| {/* Only show Add button for collected questions */} | |
| {selectedSource !== 'rag' && ( | |
| <button | |
| className="btn btn-primary add-question-btn" | |
| onClick={() => setShowAddModal(true)} | |
| > | |
| <Plus size={18} /> | |
| Add Question | |
| </button> | |
| )} | |
| </div> | |
| {/* Questions count */} | |
| <div className="questions-count"> | |
| <span>{totalQuestions} questions found</span> | |
| {totalPages > 1 && ( | |
| <span className="page-info"> | |
| Page {currentPage} of {totalPages} | |
| </span> | |
| )} | |
| </div> | |
| {/* Loading state */} | |
| {questionsLoading ? ( | |
| <div className="loading-state"> | |
| <Loader2 size={32} className="spin" /> | |
| <p>Loading questions...</p> | |
| </div> | |
| ) : questions.length === 0 ? ( | |
| <div className="empty-state"> | |
| {selectedSource === 'rag' ? ( | |
| <> | |
| <Brain size={48} /> | |
| <p>No RAG questions found</p> | |
| <span> | |
| 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. | |
| </span> | |
| </> | |
| ) : ( | |
| <> | |
| <HelpCircle size={48} /> | |
| <p>No questions found</p> | |
| <span>Try adjusting your search or add new questions</span> | |
| <button | |
| className="btn btn-primary" | |
| onClick={() => setShowAddModal(true)} | |
| > | |
| <Plus size={18} /> | |
| Add First Question | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| ) : ( | |
| <> | |
| {/* Questions list */} | |
| <div className="questions-list"> | |
| {questions.map((question, index) => ( | |
| <div | |
| key={question.id || index} | |
| className={`question-card ${expandedQuestion === index ? 'expanded' : ''} ${selectedSource === 'rag' ? 'rag-question' : ''}`} | |
| > | |
| <div | |
| className="question-header" | |
| onClick={() => 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 ( | |
| <span | |
| className={`source-badge source-${badgeClass}`} | |
| title={badgeTitle} | |
| data-source={question.source} | |
| > | |
| {badgeText} | |
| </span> | |
| ); | |
| })()} | |
| <span className="question-number">#{(currentPage - 1) * pageSize + index + 1}</span> | |
| <span className="question-text"> | |
| {question.question || question.question_text || '[No question text available]'} | |
| </span> | |
| <div className="question-actions"> | |
| {/* Only show delete for collected questions */} | |
| {selectedSource !== 'rag' && ( | |
| <button | |
| type="button" | |
| className="action-btn delete-btn" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleDeleteQuestion(question.id); | |
| }} | |
| title="Delete question" | |
| > | |
| <Trash2 size={16} /> | |
| </button> | |
| )} | |
| <button type="button" className="expand-btn"> | |
| {expandedQuestion === index ? <ChevronDown size={18} /> : <ChevronRight size={18} />} | |
| </button> | |
| </div> | |
| </div> | |
| {expandedQuestion === index && ( | |
| <div className="question-details"> | |
| {(question.answer || question.expected_answer_points?.length > 0) && ( | |
| <div className="detail-row"> | |
| <span className="detail-label">Expected Answer:</span> | |
| <span className="detail-value"> | |
| {question.answer || question.expected_answer_points?.join(', ') || 'N/A'} | |
| </span> | |
| </div> | |
| )} | |
| <div className="question-meta"> | |
| {/* 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 ( | |
| <span | |
| className={`source-badge source-${badgeClass}`} | |
| title={badgeTitle} | |
| > | |
| {badgeText} | |
| </span> | |
| ); | |
| })()} | |
| {question.difficulty && ( | |
| <span className={`meta-tag difficulty-${question.difficulty}`}> | |
| {question.difficulty} | |
| </span> | |
| )} | |
| {question.question_type && ( | |
| <span className="meta-tag type"> | |
| {question.question_type} | |
| </span> | |
| )} | |
| {/* Show actual source value for debugging */} | |
| <span className="meta-tag" style={{ fontSize: '10px', opacity: 0.7 }}> | |
| src: {question.source || 'none'} | |
| </span> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| {/* Pagination */} | |
| {totalPages > 1 && ( | |
| <div className="questions-pagination"> | |
| <button | |
| className="pagination-btn" | |
| disabled={currentPage === 1} | |
| onClick={() => setCurrentPage(p => Math.max(1, p - 1))} | |
| > | |
| <ChevronLeft size={18} /> Previous | |
| </button> | |
| <div className="pagination-pages"> | |
| {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 ( | |
| <button | |
| key={pageNum} | |
| className={`page-btn ${currentPage === pageNum ? 'active' : ''}`} | |
| onClick={() => setCurrentPage(pageNum)} | |
| > | |
| {pageNum} | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| <button | |
| className="pagination-btn" | |
| disabled={currentPage === totalPages} | |
| onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} | |
| > | |
| Next <ChevronRight size={18} /> | |
| </button> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| /** | |
| * Render Add Question Modal | |
| */ | |
| const renderAddModal = () => { | |
| if (!showAddModal) return null; | |
| return ( | |
| <div className="modal-overlay" onClick={() => setShowAddModal(false)}> | |
| <div className="modal-content" onClick={e => e.stopPropagation()}> | |
| <div className="modal-header"> | |
| <h3>Add New Question</h3> | |
| <button className="modal-close" onClick={() => setShowAddModal(false)}> | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| <div className="modal-body"> | |
| <div className="form-group"> | |
| <label>Question</label> | |
| <textarea | |
| placeholder="Enter the interview question..." | |
| value={newQuestion.question} | |
| onChange={(e) => setNewQuestion({ ...newQuestion, question: e.target.value })} | |
| rows={4} | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label>Expected Answer / Key Points</label> | |
| <textarea | |
| placeholder="Enter expected answer or key points..." | |
| value={newQuestion.answer} | |
| onChange={(e) => setNewQuestion({ ...newQuestion, answer: e.target.value })} | |
| rows={6} | |
| /> | |
| </div> | |
| <div className="form-info"> | |
| <p><strong>Branch:</strong> {branches.find(b => b.id === selectedBranch)?.name}</p> | |
| <p><strong>Domain:</strong> {domains.find(d => d.id === selectedDomain)?.name}</p> | |
| <p><strong>Mode:</strong> {MODES.find(m => m.id === selectedMode)?.name}</p> | |
| </div> | |
| </div> | |
| <div className="modal-footer"> | |
| <button | |
| className="btn btn-secondary" | |
| onClick={() => setShowAddModal(false)} | |
| > | |
| Cancel | |
| </button> | |
| <button | |
| className="btn btn-primary" | |
| onClick={handleAddQuestion} | |
| disabled={addingQuestion || !newQuestion.question.trim()} | |
| > | |
| {addingQuestion ? ( | |
| <><Loader2 size={18} className="spin" /> Adding...</> | |
| ) : ( | |
| <><Save size={18} /> Add Question</> | |
| )} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| /** | |
| * Render main content based on current step | |
| */ | |
| const renderContent = () => { | |
| switch (currentStep) { | |
| case 'branches': | |
| return renderBranchSelection(); | |
| case 'domains': | |
| return renderDomainSelection(); | |
| case 'modes': | |
| return renderModeSelection(); | |
| case 'source': | |
| return renderSourceSelection(); | |
| case 'questions': | |
| return renderQuestionsList(); | |
| default: | |
| return renderBranchSelection(); | |
| } | |
| }; | |
| return ( | |
| <div className="questions-panel"> | |
| {/* Header */} | |
| <header className="panel-header"> | |
| <div className="header-content"> | |
| <div className="header-title"> | |
| <div className="header-icon-wrapper"> | |
| <HelpCircle size={28} /> | |
| </div> | |
| <div> | |
| <h1>Question Browser</h1> | |
| <p>Browse and manage interview questions</p> | |
| </div> | |
| </div> | |
| <button | |
| className="btn btn-secondary" | |
| onClick={() => { | |
| fetchStats(); | |
| if (currentStep === 'questions') fetchQuestions(); | |
| }} | |
| > | |
| <RefreshCw size={18} /> | |
| Refresh | |
| </button> | |
| </div> | |
| {/* Breadcrumb Navigation */} | |
| {currentStep !== 'branches' && ( | |
| <div className="breadcrumb-nav"> | |
| <button className="back-btn" onClick={handleBack}> | |
| <ChevronLeft size={18} /> | |
| Back | |
| </button> | |
| <div className="breadcrumb-path"> | |
| <span | |
| className="breadcrumb-item clickable" | |
| onClick={() => { | |
| setCurrentStep('branches'); | |
| setSelectedBranch(null); | |
| setSelectedDomain(null); | |
| setSelectedMode(null); | |
| }} | |
| > | |
| All Branches | |
| </span> | |
| {getBreadcrumb().map((item, index) => ( | |
| <React.Fragment key={index}> | |
| <ChevronRight size={14} /> | |
| <span className={`breadcrumb-item ${item.type}`}> | |
| {item.label} | |
| </span> | |
| </React.Fragment> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </header> | |
| {/* Error banner */} | |
| {error && ( | |
| <div className="error-banner"> | |
| <AlertCircle size={18} /> | |
| <span>{error}</span> | |
| <button onClick={() => setError(null)}>Dismiss</button> | |
| </div> | |
| )} | |
| {/* Main content */} | |
| <div className="panel-content"> | |
| {renderContent()} | |
| </div> | |
| {/* Add Question Modal */} | |
| {renderAddModal()} | |
| </div> | |
| ); | |
| } | |
| export default QuestionsPanel; | |