NeonClary
LLM Comparison Tool: deploy snapshot for Hugging Face Space (orphan history)
08b0543
import { useState, useMemo, useEffect } from 'react';
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Cpu } from 'lucide-react';
import ResponseBlock from './ResponseBlock';
import ResponseCarousel from './ResponseCarousel';
function buildModelLookup(providers) {
const map = {};
for (const p of providers) {
for (const m of p.models) {
map[m.id] = { name: m.name, params: m.params || '' };
}
}
return map;
}
function useWindowWidth() {
const [width, setWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1200);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return width;
}
export default function ResultsArea({ results, multipleNeon, comparisonModelOrder = [], comparisonProviders = [], showPersonaPrompt = false, showPrePromptIndicator = false }) {
const [carouselOffset, setCarouselOffset] = useState(0);
const screenWidth = useWindowWidth();
const modelLookup = useMemo(() => buildModelLookup(comparisonProviders), [comparisonProviders]);
const orderedComparisonIds = comparisonModelOrder;
const sortComparisons = (responses) => {
const byId = {};
for (const r of responses) byId[r.model_id] = r;
const ordered = [];
for (const id of orderedComparisonIds) {
if (byId[id]) ordered.push(byId[id]);
}
for (const r of responses) {
if (!orderedComparisonIds.includes(r.model_id)) ordered.push(r);
}
return ordered;
};
const totalComparisons = orderedComparisonIds.length;
const isMobile = screenWidth <= 480;
const isTablet = screenWidth <= 900;
const visible = isMobile ? 1 : (isTablet ? 1 : 2);
const maxOffset = Math.max(0, totalComparisons - visible);
const skipStart = () => setCarouselOffset(0);
const prev = () => setCarouselOffset(o => Math.max(0, o - 1));
const next = () => setCarouselOffset(o => Math.min(maxOffset, o + 1));
const skipEnd = () => setCarouselOffset(maxOffset);
const visibleComparisonIds = orderedComparisonIds.slice(carouselOffset, carouselOffset + visible);
const visibleColumnHeaders = visibleComparisonIds.map(id => {
const info = modelLookup[id];
return info ? { name: info.name, params: info.params } : { name: id.split('/').pop(), params: '' };
});
if (!results.length) {
return null;
}
return (
<div className="results-area">
{results.map((result, ri) => (
<div key={ri} className="result-group">
{multipleNeon ? (
<>
<div className="result-sticky-header">
<div className="result-query">
<span className="result-query-label">Q:</span>
{result.query}
</div>
{showPersonaPrompt && result.groups.map((group, gi) => {
if (!group.system_prompt) return null;
const rawId = (group.neon_model_id || '').split('@')[0];
const modelName = rawId.includes('/') ? rawId.split('/').pop() : rawId;
const persona = group.neon_persona || '';
const label = `${modelName} - ${persona} pre-prompt:`;
return (
<div key={gi} className="result-query result-preprompt">
<span className="result-query-label">{label}</span>
{group.system_prompt}
</div>
);
})}
{totalComparisons > visible && (
<div className="carousel-unified-controls">
<button className="carousel-btn" onClick={skipStart} disabled={carouselOffset === 0} title="First">
<ChevronsLeft size={16} />
</button>
<button className="carousel-btn" onClick={prev} disabled={carouselOffset === 0} title="Previous">
<ChevronLeft size={16} />
</button>
<span className="carousel-indicator">
{carouselOffset + 1}–{Math.min(carouselOffset + visible, totalComparisons)} of {totalComparisons}
</span>
<button className="carousel-btn" onClick={next} disabled={carouselOffset >= maxOffset} title="Next">
<ChevronRight size={16} />
</button>
<button className="carousel-btn" onClick={skipEnd} disabled={carouselOffset >= maxOffset} title="Last">
<ChevronsRight size={16} />
</button>
</div>
)}
<div className="column-headers">
<div className="column-header neon-column-header">
<Cpu size={16} />
<span className="col-header-name">Neon.ai</span>
<span className="col-header-params">24B (quantized)</span>
</div>
<div className="column-headers-comparison">
{visibleColumnHeaders.map((col, i) => (
<div key={visibleComparisonIds[i]} className="column-header comparison-column-header">
<span className="col-header-name">{col.name}</span>
{col.params && <span className="col-header-params">{col.params}</span>}
</div>
))}
</div>
</div>
</div>
{result.groups.map((group, gi) => {
const neonResponse = group.responses.find(r => r.is_neon);
const comparisonResponses = sortComparisons(group.responses.filter(r => !r.is_neon));
return (
<div key={gi} className="carousel-wrapper">
{neonResponse ? (
<ResponseCarousel
neonResponse={neonResponse}
comparisonResponses={comparisonResponses}
offset={carouselOffset}
showPrePromptIndicator={showPrePromptIndicator}
/>
) : (
<div className="result-flat-grid">
{comparisonResponses.map((r, ci) => (
<ResponseBlock key={ci} response={r} showBadge={false} isComparisonEmphasis={true} showPrePromptIndicator={showPrePromptIndicator} />
))}
{group.responses.length === 0 && (
<div className="response-pending">Waiting for responses...</div>
)}
</div>
)}
</div>
);
})}
</>
) : (
<>
<div className="result-query">
<span className="result-query-label">Q:</span>
{result.query}
</div>
{showPersonaPrompt && result.groups[0]?.system_prompt && (() => {
const g = result.groups[0];
const rawId = (g.neon_model_id || '').split('@')[0];
const modelName = rawId.includes('/') ? rawId.split('/').pop() : rawId;
const persona = g.neon_persona || '';
const label = `${modelName} - ${persona} pre-prompt:`;
return (
<div className="result-query result-preprompt">
<span className="result-query-label">{label}</span>
{g.system_prompt}
</div>
);
})()}
{result.groups.map((group, gi) => {
const neonResponse = group.responses.find(r => r.is_neon);
const comparisonResponses = sortComparisons(group.responses.filter(r => !r.is_neon));
return (
<div key={gi} className="result-flat-grid">
{neonResponse && (
<ResponseBlock
response={neonResponse}
isNeonEmphasis={true}
showBadge={false}
showModelName={true}
showPersona={false}
showPrePromptIndicator={showPrePromptIndicator}
/>
)}
{comparisonResponses.map((r, ci) => (
<ResponseBlock key={ci} response={r} showBadge={false} isComparisonEmphasis={true} showPrePromptIndicator={showPrePromptIndicator} />
))}
{group.responses.length === 0 && (
<div className="response-pending">Waiting for responses...</div>
)}
</div>
);
})}
</>
)}
</div>
))}
<style>{`
.results-area {
display: flex;
flex-direction: column;
gap: 28px;
}
.result-group {
display: flex;
flex-direction: column;
gap: 14px;
}
.result-sticky-header {
position: sticky;
top: 0;
z-index: 10;
background: var(--bg-gradient);
padding-bottom: 6px;
display: flex;
flex-direction: column;
gap: 8px;
}
.result-query {
font-size: 15px;
font-weight: 500;
color: var(--text-primary);
padding: 10px 14px;
background: var(--bg-tertiary);
border-radius: 10px;
border-left: 4px solid var(--query-accent);
}
.result-query-label {
font-weight: 700;
color: var(--query-accent);
margin-right: 6px;
}
.result-preprompt {
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
border-left-color: var(--neon-border);
}
.result-preprompt .result-query-label {
color: var(--neon-accent);
}
.carousel-unified-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 4px 0;
}
.carousel-btn {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border: 1px solid var(--border-primary);
border-radius: 6px;
background: var(--card-bg);
color: var(--text-secondary);
transition: all 0.15s;
}
.carousel-btn:hover:not(:disabled) {
background: var(--bg-tertiary);
border-color: var(--accent-primary);
color: var(--accent-primary);
}
.carousel-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.carousel-indicator {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
min-width: 80px;
text-align: center;
font-weight: 500;
}
/* Column headers */
.column-headers {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 12px;
}
.column-header {
padding: 8px 12px;
border-radius: 8px;
font-size: 15px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.neon-column-header {
color: #111827;
background: var(--neon-bg);
border: 2px solid var(--neon-border);
}
:root[data-theme="dark"] .neon-column-header {
color: #F9FAFB;
}
.column-headers-comparison {
display: flex;
gap: 12px;
min-width: 0;
}
.comparison-column-header {
flex: 1;
min-width: 0;
background: var(--comp-bg);
border: 2px solid var(--comp-border);
color: var(--text-primary);
}
.col-header-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.col-header-params {
font-weight: 400;
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
margin-left: auto;
}
.result-flat-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
gap: 12px;
}
.response-pending {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
color: var(--text-muted);
font-size: 13px;
font-style: italic;
border: 1px dashed var(--border-primary);
border-radius: 12px;
grid-column: 1 / -1;
}
/* Responsive: tablet */
@media (max-width: 900px) {
.result-sticky-header {
position: relative;
}
.column-headers {
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.column-header {
font-size: 13px;
padding: 6px 10px;
gap: 4px;
}
.col-header-params {
font-size: 10px;
}
.carousel-btn {
width: 36px;
height: 36px;
}
.result-flat-grid {
grid-template-columns: 1fr;
}
}
/* Responsive: phone */
@media (max-width: 480px) {
.result-query {
font-size: 13px;
padding: 8px 10px;
}
.column-headers {
grid-template-columns: 1fr;
gap: 6px;
}
.column-headers-comparison {
flex-direction: column;
gap: 6px;
}
.carousel-btn {
width: 40px;
height: 40px;
}
.carousel-indicator {
font-size: 13px;
}
}
`}</style>
</div>
);
}