feat: model selector dropdown fetches from OpenRouter API

This commit is contained in:
Patrick Britton 2026-02-08 12:02:53 -06:00
parent 2acf76eab2
commit 7b493bcd67

View file

@ -8,6 +8,59 @@ import {
ChevronDown, ChevronUp, Bot, Clock, AlertCircle, CheckCircle, ExternalLink, ChevronDown, ChevronUp, Bot, Clock, AlertCircle, CheckCircle, ExternalLink,
} from 'lucide-react'; } from 'lucide-react';
// ─── Model Selector (fetches from OpenRouter) ─────────
function ModelSelector({ value, onChange, className }: { value: string; onChange: (v: string) => void; className?: string }) {
const [models, setModels] = useState<{ id: string; name: string }[]>([]);
const [search, setSearch] = useState('');
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
api.listOpenRouterModels().then((data) => {
const list = (data.models || []).map((m: any) => ({ id: m.id, name: m.name || m.id }));
setModels(list);
}).catch(() => {}).finally(() => setLoading(false));
}, []);
const filtered = search
? models.filter((m) => m.id.toLowerCase().includes(search.toLowerCase()) || m.name.toLowerCase().includes(search.toLowerCase()))
: models;
const displayName = models.find((m) => m.id === value)?.name || value;
return (
<div className={`relative ${className || ''}`}>
<button type="button" onClick={() => setOpen(!open)}
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm text-left truncate bg-white hover:bg-warm-50 transition-colors">
{loading ? 'Loading models...' : displayName}
</button>
{open && (
<div className="absolute z-50 mt-1 w-full bg-white border border-warm-300 rounded-lg shadow-lg max-h-64 overflow-hidden flex flex-col">
<input type="text" placeholder="Search models..." value={search} onChange={(e) => setSearch(e.target.value)} autoFocus
className="px-3 py-2 border-b border-warm-200 text-sm outline-none" />
<div className="overflow-y-auto max-h-52">
{filtered.length === 0 ? (
<p className="p-3 text-xs text-gray-500">{loading ? 'Loading...' : 'No models found'}</p>
) : (
filtered.slice(0, 100).map((m) => (
<button key={m.id} type="button"
onClick={() => { onChange(m.id); setOpen(false); setSearch(''); }}
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-brand-50 transition-colors ${
m.id === value ? 'bg-brand-50 text-brand-700 font-medium' : 'text-gray-700'
}`}>
<span className="block truncate font-medium">{m.name}</span>
<span className="block truncate text-[10px] text-gray-400 font-mono">{m.id}</span>
</button>
))
)}
</div>
</div>
)}
</div>
);
}
const DEFAULT_NEWS_SOURCES = [ const DEFAULT_NEWS_SOURCES = [
{ name: 'NPR', rss_url: 'https://feeds.npr.org/1001/rss.xml', enabled: true }, { name: 'NPR', rss_url: 'https://feeds.npr.org/1001/rss.xml', enabled: true },
{ name: 'AP News', rss_url: 'https://rsshub.app/apnews/topics/apf-topnews', enabled: true }, { name: 'AP News', rss_url: 'https://rsshub.app/apnews/topics/apf-topnews', enabled: true },
@ -398,8 +451,7 @@ function CreateAccountForm({ onDone, initialProfile }: { onDone: () => void; ini
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Model</label> <label className="block text-sm font-medium text-gray-700 mb-1">Model</label>
<input type="text" value={modelId} onChange={(e) => setModelId(e.target.value)} <ModelSelector value={modelId} onChange={setModelId} />
className="w-full px-3 py-2 border border-warm-300 rounded-lg text-sm font-mono" />
</div> </div>
</div> </div>
@ -520,8 +572,7 @@ function EditAccountForm({ config, onDone }: { config: Config; onDone: () => voi
<div className="grid grid-cols-4 gap-3 mb-3"> <div className="grid grid-cols-4 gap-3 mb-3">
<div> <div>
<label className="block text-xs font-medium text-gray-600 mb-1">Model</label> <label className="block text-xs font-medium text-gray-600 mb-1">Model</label>
<input type="text" value={modelId} onChange={(e) => setModelId(e.target.value)} <ModelSelector value={modelId} onChange={setModelId} />
className="w-full px-2 py-1.5 border border-warm-300 rounded text-xs font-mono" />
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-600 mb-1">Temperature</label> <label className="block text-xs font-medium text-gray-600 mb-1">Temperature</label>