NeonClary
LLM Comparison Tool: deploy snapshot for Hugging Face Space (orphan history)
08b0543
import { useState, useEffect, useCallback, useRef } from 'react';
import { Sun, Moon, Download, RefreshCw, Menu, X, Settings, Send, FileText, Users, Cpu, Ban, Eye, EyeOff, Sparkles } from 'lucide-react';
import { useTheme } from './contexts/ThemeContext';
import { fetchModels, 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 './App.css';
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 [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 loadModels = useCallback(async () => {
setModelsLoading(true);
setError(null);
try {
const data = await fetchModels();
setNeonModels(data.neon_models || []);
setComparisonProviders(data.comparison_providers || []);
} catch (e) {
setError(`Failed to load models: ${e.message}`);
} finally {
setModelsLoading(false);
}
}, []);
useEffect(() => { loadModels(); }, [loadModels]);
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">
<Sparkles size={28} className="app-logo" strokeWidth={1.75} />
</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 ? '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>
)}
</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}
/>
<ComparisonSelector
providers={comparisonProviders}
selected={selectedComparison}
onSelectionChange={setSelectedComparison}
/>
</aside>
<section className="content">
<p className="content-hint content-hint-desktop">Select Neon.ai models and comparison models, then add your question and click 'Compare'</p>
<p className="content-hint content-hint-mobile">Select Neon.ai models and comparison models using the menu on the upper left, then add your question and click 'Compare'</p>
<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>
</div>
);
}
export default App;