| import { useState } from 'react'; |
| import { ChevronDown, ChevronRight, Loader2, User, Sparkles } from 'lucide-react'; |
|
|
| export default function NeonModelSelector({ models, selected, onSelectionChange, loading }) { |
| const [expandedModel, setExpandedModel] = useState(null); |
|
|
| const isModelSelected = (modelId, personaName) => |
| selected.some(s => s.model_id === modelId && s.persona_name === personaName); |
|
|
| const togglePersona = (model, persona) => { |
| const key = { model_id: model.model_id, persona_name: persona.persona_name, system_prompt: persona.system_prompt || '' }; |
| if (isModelSelected(model.model_id, persona.persona_name)) { |
| onSelectionChange(selected.filter(s => !(s.model_id === model.model_id && s.persona_name === persona.persona_name))); |
| } else { |
| onSelectionChange([...selected, key]); |
| } |
| }; |
|
|
| const shortName = (name) => name.split('/').pop(); |
|
|
| if (loading) { |
| return ( |
| <div className="selector-section"> |
| <h3 className="selector-title"><Sparkles size={16} className="selector-title-icon" aria-hidden /> Neon.ai Models</h3> |
| <div className="selector-loading"><Loader2 size={20} className="spin" /> Loading models...</div> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div className="selector-section"> |
| <h3 className="selector-title"><Sparkles size={16} className="selector-title-icon" aria-hidden /> Neon.ai Models</h3> |
| <p className="selector-hint">Select model + persona for comparison</p> |
| <div className="neon-model-list"> |
| {[...models].sort((a, b) => shortName(a.name).localeCompare(shortName(b.name))).map(model => { |
| const isExpanded = expandedModel === model.model_id; |
| const activePersonas = model.personas.filter(p => p.enabled !== false); |
| const selectedCount = selected.filter(s => s.model_id === model.model_id).length; |
| |
| return ( |
| <div key={model.model_id} className="neon-model-card"> |
| <button |
| className="neon-model-header" |
| onClick={() => setExpandedModel(isExpanded ? null : model.model_id)} |
| > |
| <div className="neon-model-info"> |
| <span className="neon-model-name">{shortName(model.name)}</span> |
| <span className="neon-model-version">v{model.version}</span> |
| </div> |
| <div className="neon-model-meta"> |
| {selectedCount > 0 && <span className="badge badge-neon">{selectedCount}</span>} |
| {isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />} |
| </div> |
| </button> |
| {isExpanded && ( |
| <div className="neon-persona-list"> |
| {activePersonas.map(persona => { |
| const checked = isModelSelected(model.model_id, persona.persona_name); |
| return ( |
| <label key={persona.id} className={`neon-persona-item ${checked ? 'selected' : ''}`}> |
| <input |
| type="checkbox" |
| checked={checked} |
| onChange={() => togglePersona(model, persona)} |
| /> |
| <div className="persona-details"> |
| <div className="persona-name"> |
| <User size={12} /> |
| {persona.persona_name} |
| </div> |
| {persona.system_prompt && ( |
| <div className="persona-prompt-preview"> |
| {persona.system_prompt.slice(0, 120)} |
| {persona.system_prompt.length > 120 ? '...' : ''} |
| </div> |
| )} |
| {!persona.system_prompt && ( |
| <div className="persona-prompt-preview">No system prompt (vanilla)</div> |
| )} |
| </div> |
| </label> |
| ); |
| })} |
| </div> |
| )} |
| </div> |
| ); |
| })} |
| </div> |
| |
| <style>{` |
| .selector-section { |
| display: flex; |
| flex-direction: column; |
| gap: 8px; |
| } |
| .selector-title { |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| font-size: 15px; |
| font-weight: 700; |
| color: var(--text-primary); |
| text-decoration: underline; |
| text-underline-offset: 3px; |
| text-decoration-color: var(--border-primary); |
| } |
| .selector-title-icon { |
| height: 16px; |
| width: auto; |
| } |
| .selector-hint { |
| font-size: 12px; |
| color: var(--text-muted); |
| } |
| .selector-loading { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| font-size: 13px; |
| color: var(--text-tertiary); |
| padding: 12px 0; |
| } |
| .neon-model-list { |
| display: flex; |
| flex-direction: column; |
| gap: 6px; |
| } |
| .neon-model-card { |
| border: 1px solid var(--border-primary); |
| border-radius: 8px; |
| overflow: hidden; |
| background: #EAE0FD; |
| } |
| :root[data-theme="dark"] .neon-model-card { |
| background: #2D2640; |
| } |
| .neon-model-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| width: 100%; |
| padding: 10px 12px; |
| background: none; |
| border: none; |
| color: var(--text-primary); |
| font-size: 13px; |
| text-align: left; |
| transition: background 0.15s; |
| } |
| .neon-model-header:hover { |
| background: var(--card-hover); |
| } |
| .neon-model-info { |
| display: flex; |
| align-items: baseline; |
| gap: 6px; |
| } |
| .neon-model-name { |
| font-weight: 600; |
| } |
| .neon-model-version { |
| font-size: 11px; |
| color: var(--text-muted); |
| } |
| .neon-model-meta { |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| color: var(--text-tertiary); |
| } |
| .neon-persona-list { |
| border-top: 1px solid var(--border-muted); |
| padding: 4px; |
| } |
| .neon-persona-item { |
| display: flex; |
| align-items: flex-start; |
| gap: 8px; |
| padding: 8px; |
| border-radius: 6px; |
| cursor: pointer; |
| transition: background 0.15s; |
| } |
| .neon-persona-item:hover { |
| background: var(--card-hover); |
| } |
| .neon-persona-item.selected { |
| background: var(--card-bg); |
| } |
| .neon-persona-item input[type="checkbox"] { |
| margin-top: 2px; |
| accent-color: var(--neon-accent); |
| } |
| .persona-details { |
| flex: 1; |
| min-width: 0; |
| } |
| .persona-name { |
| display: flex; |
| align-items: center; |
| gap: 4px; |
| font-size: 13px; |
| font-weight: 500; |
| color: var(--text-primary); |
| } |
| .persona-prompt-preview { |
| font-size: 11px; |
| color: var(--text-muted); |
| line-height: 1.4; |
| margin-top: 2px; |
| overflow: hidden; |
| display: -webkit-box; |
| -webkit-line-clamp: 2; |
| -webkit-box-orient: vertical; |
| } |
| `}</style> |
| </div> |
| ); |
| } |
|
|