NeonClary's picture
Upload folder using huggingface_hub
060fd46 verified
Raw History Blame Contribute Delete
15.9 kB
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 (
<div className="app">
<header className="app-header">
<div className="header-left">
<button className="icon-btn sidebar-toggle" onClick={() => setSidebarOpen(o => !o)} title="Toggle model selection">
{sidebarOpen ? <X size={18} /> : <Menu size={18} />}
</button>
<a href="https://www.neon.ai/" target="_blank" rel="noopener noreferrer" className="header-brand-link" aria-label="Neon.ai">
<img src={`${process.env.PUBLIC_URL}/neon-logo.png`} alt="Neon.ai" className="app-logo" />
</a>
<h1 className="app-title"><a href="https://www.neon.ai/" target="_blank" rel="noopener noreferrer" className="app-title-link">Neon.ai</a> LLM Comparison Tool</h1>
</div>
<div className="header-right">
<button className="icon-btn" onClick={loadModels} title="Refresh models">
<RefreshCw size={18} className={modelsLoading || statusChecking ? 'spin' : ''} />
</button>
{hasHistory && (
<button className="icon-btn" onClick={handleDownloadHistory} title="Download session history as CSV">
<Download size={18} />
</button>
)}
<button className="icon-btn" onClick={toggleTheme} title="Toggle theme">
{theme === 'light' ? <Moon size={18} /> : <Sun size={18} />}
</button>
<div className="settings-wrapper" ref={settingsRef}>
<button className="icon-btn" onClick={() => setSettingsOpen(o => !o)} title="Settings">
<Settings size={18} />
</button>
{settingsOpen && (
<div className="settings-dropdown">
<div className="settings-section">
<span className="settings-label">Query Mode</span>
<button
className={`settings-option ${!csvMode ? 'active' : ''}`}
onClick={() => setCsvMode(false)}
>
<Send size={14} /> Single Query
</button>
<button
className={`settings-option ${csvMode ? 'active' : ''}`}
onClick={() => setCsvMode(true)}
>
<FileText size={14} /> CSV Batch
</button>
</div>
<div className="settings-section">
<span className="settings-label">Pre-prompt</span>
<button
className={`settings-option ${personaTarget === 'all' ? 'active' : ''}`}
onClick={() => setPersonaTarget('all')}
>
<Users size={14} /> All models
</button>
<button
className={`settings-option ${personaTarget === 'neon-only' ? 'active' : ''}`}
onClick={() => setPersonaTarget('neon-only')}
>
<Cpu size={14} /> Neon.ai models only
</button>
<button
className={`settings-option ${personaTarget === 'none' ? 'active' : ''}`}
onClick={() => setPersonaTarget('none')}
>
<Ban size={14} /> No models
</button>
</div>
<div className="settings-section">
<span className="settings-label">Display</span>
<button
className={`settings-option ${showPersonaPrompt ? 'active' : ''}`}
onClick={() => setShowPersonaPrompt(v => !v)}
>
{showPersonaPrompt ? <Eye size={14} /> : <EyeOff size={14} />} Show pre-prompt text
</button>
<button
className={`settings-option ${showPrePromptIndicator ? 'active' : ''}`}
onClick={() => setShowPrePromptIndicator(v => !v)}
>
{showPrePromptIndicator ? <Eye size={14} /> : <EyeOff size={14} />} Show pre-prompt status
</button>
</div>
<div className="settings-section">
<button
className="settings-option"
onClick={() => {
setSettingsOpen(false);
setModelStatusOpen(true);
}}
>
<Activity size={14} /> Model Status
</button>
</div>
</div>
)}
</div>
</div>
</header>
<main className="app-main">
<div className={`sidebar-overlay ${sidebarOpen ? 'visible' : ''}`} onClick={() => setSidebarOpen(false)} />
<aside className={`sidebar ${sidebarOpen ? 'open' : ''}`}>
<h2 className="sidebar-title">AI Models</h2>
<NeonModelSelector
models={neonModels}
selected={selectedNeon}
onSelectionChange={setSelectedNeon}
loading={modelsLoading || statusChecking}
/>
<ComparisonSelector
providers={comparisonProviders}
selected={selectedComparison}
onSelectionChange={setSelectedComparison}
loading={modelsLoading || statusChecking}
/>
</aside>
<section className="content">
<div className="content-hint content-hint-desktop">
<p className="content-hint-lead">Ready to Compare?</p>
<p className="content-hint-body">Pick at least one Neon.ai model in the sidebar, add comparison models, then enter a question and click Compare.</p>
</div>
<div className="content-hint content-hint-mobile">
<p className="content-hint-lead">Ready to Compare?</p>
<p className="content-hint-body">Pick at least one Neon.ai model using the menu on the upper left, add comparison models, then enter a question and click Compare.</p>
</div>
<QueryInput
onQuery={handleQuery}
onCsvUpload={handleCsvUpload}
loading={loading}
csvMode={csvMode}
selectedNeon={selectedNeon}
selectedComparison={selectedComparison}
comparisonProviders={comparisonProviders}
/>
{error && (
<div className="error-banner">
{error}
<button onClick={() => setError(null)}>&times;</button>
</div>
)}
<ResultsArea
results={results}
multipleNeon={selectedNeon.length > 1}
comparisonModelOrder={selectedComparison}
comparisonProviders={comparisonProviders}
showPersonaPrompt={showPersonaPrompt}
showPrePromptIndicator={showPrePromptIndicator}
/>
</section>
</main>
<footer className="app-footer">
Copyright Neon.ai. All rights reserved.{' '}
<a href="https://www.neon.ai/contact" target="_blank" rel="noopener noreferrer">Patents and licensing</a>
</footer>
{modelStatusOpen && (
<ModelStatusModal
statuses={modelStatuses}
loading={statusChecking}
onClose={() => setModelStatusOpen(false)}
onRefresh={handleRefreshStatuses}
/>
)}
</div>
);
}
export default App;