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 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;