NeonClary
LLM Comparison Tool: deploy snapshot for Hugging Face Space (orphan history)
08b0543
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>
);
}