import { useState, useEffect, useCallback, useRef } from 'react'; import { Sun, Moon, Download, RefreshCw, Menu, X, Settings, Send, FileText, Users, Cpu, Ban, Eye, EyeOff, Activity } from 'lucide-react'; import { useTheme } from './contexts/ThemeContext'; import { fetchModels, fetchModelStatuses, runComparisonStream, uploadCsvComparison, downloadHistory } from './utils/api'; import NeonModelSelector from './components/NeonModelSelector'; import ComparisonSelector from './components/ComparisonSelector'; import QueryInput from './components/QueryInput'; import ResultsArea from './components/ResultsArea'; import ModelStatusModal from './components/ModelStatusModal'; import './App.css'; function filterToOnlineModels(neonModels, comparisonProviders, statuses) { const onlineIds = new Set( (statuses || []) .filter((s) => s.status === 'online') .map((s) => s.model_id), ); // If status check returned nothing (e.g. failed), keep the full list. if (!statuses || !statuses.length) { return { neonModels, comparisonProviders }; } const filteredNeon = (neonModels || []).filter((m) => onlineIds.has(m.model_id)); const filteredProviders = (comparisonProviders || []) .map((p) => ({ ...p, models: (p.models || []).filter((m) => onlineIds.has(m.id)), })) .filter((p) => p.models.length > 0); return { neonModels: filteredNeon, comparisonProviders: filteredProviders }; } function App() { const { theme, toggleTheme } = useTheme(); const [neonModels, setNeonModels] = useState([]); const [comparisonProviders, setComparisonProviders] = useState([]); const [selectedNeon, setSelectedNeon] = useState([]); const [selectedComparison, setSelectedComparison] = useState([]); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [modelsLoading, setModelsLoading] = useState(true); const [statusChecking, setStatusChecking] = useState(false); const [modelStatuses, setModelStatuses] = useState([]); const [modelStatusOpen, setModelStatusOpen] = useState(false); const [error, setError] = useState(null); const sessionIdRef = useRef(`session-${Date.now()}-${Math.random().toString(36).slice(2)}`); const [hasHistory, setHasHistory] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false); const [csvMode, setCsvMode] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); const [personaTarget, setPersonaTarget] = useState('neon-only'); const [showPersonaPrompt, setShowPersonaPrompt] = useState(false); const [showPrePromptIndicator, setShowPrePromptIndicator] = useState(false); const settingsRef = useRef(null); useEffect(() => { const handleClickOutside = (e) => { if (settingsRef.current && !settingsRef.current.contains(e.target)) { setSettingsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const applyStatusFilter = useCallback((rawNeon, rawProviders, statuses) => { const { neonModels: filteredNeon, comparisonProviders: filteredProviders } = filterToOnlineModels(rawNeon, rawProviders, statuses); setNeonModels(filteredNeon); setComparisonProviders(filteredProviders); setSelectedNeon((prev) => prev.filter((s) => filteredNeon.some((m) => m.model_id === s.model_id))); const onlineComparisonIds = new Set(filteredProviders.flatMap((p) => p.models.map((m) => m.id))); setSelectedComparison((prev) => prev.filter((id) => onlineComparisonIds.has(id))); }, []); const checkModelStatuses = useCallback(async (rawNeon, rawProviders) => { setStatusChecking(true); try { const data = await fetchModelStatuses(); const statuses = data.statuses || []; setModelStatuses(statuses); applyStatusFilter(rawNeon, rawProviders, statuses); } catch (e) { console.warn('Model status check failed; keeping full model list:', e); setModelStatuses([]); setNeonModels(rawNeon || []); setComparisonProviders(rawProviders || []); } finally { setStatusChecking(false); } }, [applyStatusFilter]); const loadModels = useCallback(async () => { setModelsLoading(true); setError(null); try { const data = await fetchModels(); const rawNeon = data.neon_models || []; const rawProviders = data.comparison_providers || []; // Show unfiltered list briefly while status check runs, then filter. setNeonModels(rawNeon); setComparisonProviders(rawProviders); setModelsLoading(false); await checkModelStatuses(rawNeon, rawProviders); } catch (e) { setError(`Failed to load models: ${e.message}`); setModelsLoading(false); } }, [checkModelStatuses]); useEffect(() => { loadModels(); }, [loadModels]); const handleRefreshStatuses = async () => { try { const data = await fetchModels(); const rawNeon = data.neon_models || []; const rawProviders = data.comparison_providers || []; await checkModelStatuses(rawNeon, rawProviders); } catch (e) { setError(`Failed to refresh model status: ${e.message}`); } }; const handleQuery = async (query) => { if (!selectedNeon.length) { setError('Please select at least one Neon model'); return; } setLoading(true); setError(null); const resultEntry = { query, groups: [], timestamp: Date.now() }; setResults(prev => [resultEntry, ...prev]); const resultIndex = 0; try { await runComparisonStream( query, selectedNeon, selectedComparison, sessionIdRef.current, (groupMeta) => { setResults(prev => { const updated = [...prev]; const entry = { ...updated[resultIndex] }; entry.groups = [...entry.groups, { neon_model_id: groupMeta.neon_model_id, neon_persona: groupMeta.neon_persona, system_prompt: groupMeta.system_prompt || '', query: groupMeta.query, responses: [], }]; updated[resultIndex] = entry; return updated; }); }, (response) => { setResults(prev => { const updated = [...prev]; const entry = { ...updated[resultIndex] }; entry.groups = entry.groups.map((g, gi) => { if (gi !== response.group_index) return g; return { ...g, responses: [...g.responses, response] }; }); updated[resultIndex] = entry; return updated; }); }, () => { setHasHistory(true); }, personaTarget, ); } catch (e) { setError(e.message); } finally { setLoading(false); } }; const handleCsvUpload = async (file) => { if (!selectedNeon.length) { setError('Please select at least one Neon model'); return; } setLoading(true); setError(null); try { const blob = await uploadCsvComparison(file, selectedNeon, selectedComparison, personaTarget); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `comparison_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.csv`; a.click(); URL.revokeObjectURL(url); } catch (e) { setError(e.message); } finally { setLoading(false); } }; const handleDownloadHistory = async () => { try { const blob = await downloadHistory(sessionIdRef.current); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `comparison_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.csv`; a.click(); URL.revokeObjectURL(url); } catch (e) { setError('No history to download yet'); } }; return (
Neon.ai

Neon.ai LLM Comparison Tool

{hasHistory && ( )}
{settingsOpen && (
Query Mode
Pre-prompt
Display
)}
setSidebarOpen(false)} />

Ready to Compare?

Pick at least one Neon.ai model in the sidebar, add comparison models, then enter a question and click Compare.

Ready to Compare?

Pick at least one Neon.ai model using the menu on the upper left, add comparison models, then enter a question and click Compare.

{error && (
{error}
)} 1} comparisonModelOrder={selectedComparison} comparisonProviders={comparisonProviders} showPersonaPrompt={showPersonaPrompt} showPrePromptIndicator={showPrePromptIndicator} />
{modelStatusOpen && ( setModelStatusOpen(false)} onRefresh={handleRefreshStatuses} /> )}
); } export default App;