feat: model selector dropdown fetches from OpenRouter API
This commit is contained in:
parent
2acf76eab2
commit
7b493bcd67
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue