Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |