| import { useState, useRef } from 'react'; |
| import { Send, Upload, FileText, X, Loader2 } from 'lucide-react'; |
|
|
| export default function QueryInput({ onQuery, onCsvUpload, loading, csvMode = false, selectedNeon = [], selectedComparison = [], comparisonProviders = [] }) { |
| const [query, setQuery] = useState(''); |
| const [selectedFile, setSelectedFile] = useState(null); |
| const fileInputRef = useRef(null); |
|
|
| const handleSubmit = (e) => { |
| e.preventDefault(); |
| if (csvMode && selectedFile) { |
| onCsvUpload(selectedFile); |
| } else if (!csvMode && query.trim()) { |
| onQuery(query.trim()); |
| setQuery(''); |
| } |
| }; |
|
|
| const handleFileDrop = (e) => { |
| e.preventDefault(); |
| const file = e.dataTransfer?.files?.[0]; |
| if (file && file.name.endsWith('.csv')) { |
| setSelectedFile(file); |
| } |
| }; |
|
|
| const handleFileSelect = (e) => { |
| const file = e.target.files?.[0]; |
| if (file) setSelectedFile(file); |
| }; |
|
|
| const handleKeyDown = (e) => { |
| if (e.key === 'Enter' && !e.shiftKey && !csvMode) { |
| e.preventDefault(); |
| handleSubmit(e); |
| } |
| }; |
|
|
| const neonDisplayName = (sel) => { |
| const name = sel.model_id.split('@')[0].split('/').pop(); |
| return `${name} - ${sel.persona_name}`; |
| }; |
|
|
| const comparisonDisplayName = (modelId) => { |
| for (const provider of comparisonProviders) { |
| const model = provider.models.find(m => m.id === modelId); |
| if (model) return model.name; |
| } |
| return modelId.split('/').pop(); |
| }; |
|
|
| return ( |
| <div className="query-input-container"> |
| <form onSubmit={handleSubmit} className="query-form"> |
| {csvMode ? ( |
| <div |
| className={`csv-drop-zone ${selectedFile ? 'has-file' : ''}`} |
| onDragOver={e => e.preventDefault()} |
| onDrop={handleFileDrop} |
| onClick={() => fileInputRef.current?.click()} |
| > |
| <input |
| ref={fileInputRef} |
| type="file" |
| accept=".csv" |
| onChange={handleFileSelect} |
| style={{ display: 'none' }} |
| /> |
| {selectedFile ? ( |
| <div className="csv-file-info"> |
| <FileText size={20} /> |
| <span>{selectedFile.name}</span> |
| <button |
| type="button" |
| className="csv-clear" |
| onClick={(e) => { e.stopPropagation(); setSelectedFile(null); }} |
| > |
| <X size={14} /> |
| </button> |
| </div> |
| ) : ( |
| <div className="csv-placeholder"> |
| <Upload size={24} /> |
| <span>Drop a CSV file here or click to browse</span> |
| <span className="csv-hint">Single column of questions</span> |
| </div> |
| )} |
| </div> |
| ) : ( |
| <textarea |
| className="query-textarea" |
| value={query} |
| onChange={e => setQuery(e.target.value)} |
| onKeyDown={handleKeyDown} |
| placeholder="Ask a question to compare across models..." |
| rows={2} |
| disabled={loading} |
| /> |
| )} |
| <div className="query-submit-row"> |
| <button |
| type="submit" |
| className="query-submit" |
| disabled={loading || (csvMode ? !selectedFile : !query.trim())} |
| > |
| {loading ? <Loader2 size={18} className="spin" /> : <Send size={18} />} |
| {csvMode ? 'Run Batch' : 'Compare'} |
| </button> |
| {(selectedNeon.length > 0 || selectedComparison.length > 0) && ( |
| <div className="selection-summary"> |
| {selectedNeon.length > 0 && ( |
| <div className="selection-line"> |
| <span className="selection-label">Neon.ai Models Selected:</span>{' '} |
| {selectedNeon.map(s => neonDisplayName(s)).join(', ')} <span className="selection-count">({selectedNeon.length})</span> |
| </div> |
| )} |
| {selectedComparison.length > 0 && ( |
| <div className="selection-line"> |
| <span className="selection-label">Comparison Models Selected:</span>{' '} |
| {selectedComparison.map(id => comparisonDisplayName(id)).join(', ')} <span className="selection-count">({selectedComparison.length})</span> |
| </div> |
| )} |
| </div> |
| )} |
| </div> |
| </form> |
| |
| <style>{` |
| .query-input-container { |
| display: flex; |
| flex-direction: column; |
| gap: 8px; |
| } |
| .query-form { |
| display: flex; |
| flex-direction: column; |
| gap: 8px; |
| } |
| .query-textarea { |
| flex: 1; |
| padding: 10px 14px; |
| border: 2px solid var(--query-accent); |
| border-radius: 10px; |
| background: var(--card-bg); |
| color: var(--text-primary); |
| font-family: inherit; |
| font-size: 14px; |
| resize: vertical; |
| min-height: 44px; |
| outline: none; |
| transition: border-color 0.15s; |
| } |
| .query-textarea:focus { |
| border-color: var(--query-accent); |
| } |
| .query-textarea::placeholder { |
| color: var(--text-muted); |
| } |
| .csv-drop-zone { |
| flex: 1; |
| border: 2px dashed var(--border-primary); |
| border-radius: 10px; |
| padding: 20px; |
| text-align: center; |
| cursor: pointer; |
| transition: border-color 0.15s, background 0.15s; |
| background: var(--card-bg); |
| } |
| .csv-drop-zone:hover { |
| border-color: var(--accent-primary); |
| background: var(--accent-light); |
| } |
| .csv-drop-zone.has-file { |
| border-style: solid; |
| border-color: var(--neon-accent); |
| } |
| .csv-placeholder { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 6px; |
| color: var(--text-tertiary); |
| font-size: 13px; |
| } |
| .csv-hint { |
| font-size: 11px; |
| color: var(--text-muted); |
| } |
| .csv-file-info { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| color: var(--neon-accent); |
| font-size: 14px; |
| font-weight: 500; |
| justify-content: center; |
| } |
| .csv-clear { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| width: 22px; |
| height: 22px; |
| border: none; |
| border-radius: 50%; |
| background: var(--bg-tertiary); |
| color: var(--text-muted); |
| } |
| .csv-clear:hover { |
| background: #FEE2E2; |
| color: #EF4444; |
| } |
| .query-submit-row { |
| display: flex; |
| align-items: flex-end; |
| gap: 16px; |
| } |
| .query-submit { |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| padding: 10px 20px; |
| border: none; |
| border-radius: 10px; |
| background: var(--accent-gradient); |
| color: #FFFFFF; |
| font-size: 14px; |
| font-weight: 600; |
| white-space: nowrap; |
| transition: opacity 0.15s, transform 0.1s; |
| flex-shrink: 0; |
| } |
| .selection-summary { |
| display: flex; |
| flex-direction: column; |
| gap: 2px; |
| padding-top: 2px; |
| min-width: 0; |
| } |
| .selection-line { |
| font-size: 14px; |
| font-family: inherit; |
| color: var(--text-muted); |
| line-height: 1.5; |
| word-break: break-word; |
| } |
| .selection-label { |
| font-weight: 600; |
| color: var(--text-secondary); |
| } |
| .selection-count { |
| font-weight: 600; |
| color: var(--text-secondary); |
| } |
| .query-submit:hover:not(:disabled) { |
| opacity: 0.9; |
| transform: translateY(-1px); |
| } |
| .query-submit:disabled { |
| opacity: 0.5; |
| cursor: not-allowed; |
| } |
| |
| @media (max-width: 480px) { |
| .query-textarea { |
| min-height: 48px; |
| font-size: 16px; |
| } |
| .query-submit-row { |
| flex-direction: column; |
| gap: 8px; |
| } |
| .query-submit { |
| align-self: stretch; |
| justify-content: center; |
| padding: 12px 20px; |
| font-size: 15px; |
| } |
| .selection-line { |
| font-size: 13px; |
| } |
| .csv-drop-zone { |
| padding: 16px; |
| } |
| } |
| `}</style> |
| </div> |
| ); |
| } |
|
|