model selector: cached dropdown filtered by backend

This commit is contained in:
Levi Neely 2026-05-21 18:49:38 +02:00
parent c61a674289
commit 23a2de8fac
3 changed files with 100 additions and 19 deletions

View File

@ -1,11 +1,13 @@
import { useState, useEffect } from 'preact/hooks'
import { getBackends, getAgents } from '../lib/api'
import { getBackends, getAgents, fetchModels, loadModelsCache } from '../lib/api'
interface CreateParams {
export interface CreateParams {
name?: string
cwd: string
backend: string
model: string
agent: string
[key: string]: string | undefined
}
interface Props {
@ -13,29 +15,33 @@ interface Props {
onCreate: (p: CreateParams) => Promise<void>
}
const DEFAULT_MODELS: Record<string, string> = {
anthropic: 'claude-opus-4-5',
openai: 'gpt-4o',
ollama: 'llama3',
codewhisperer: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
copilot: 'gpt-4o',
}
const ADVANCED_FIELDS = [
'maxTokens', 'maxCompletionTokens', 'temperature', 'topP', 'topK',
'minP', 'topA', 'frequencyPenalty', 'presencePenalty', 'repetitionPenalty',
'reasoning', 'reasoningEffort', 'includeReasoning', 'responseFormat',
'stop', 'verbosity',
] as const
export function NewSession({ onClose, onCreate }: Props) {
const [name, setName] = useState('')
const [cwd, setCwd] = useState('~')
const [backend, setBackend] = useState('anthropic')
const [model, setModel] = useState(DEFAULT_MODELS['anthropic'])
const [model, setModel] = useState('')
const [agent, setAgent] = useState('default')
const [backends, setBackends] = useState<string[]>([])
const [agents, setAgents] = useState<string[]>(['default'])
const [modelCache, setModelCache] = useState(loadModelsCache)
const [advanced, setAdvanced] = useState<Record<string, string>>({})
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchModels().then(setModelCache).catch(() => {})
getBackends().then(bs => {
if (bs.length > 0) {
setBackends(bs)
setBackend(bs[0])
setModel(DEFAULT_MODELS[bs[0]] ?? '')
}
}).catch(() => {})
getAgents().then(as => {
@ -43,17 +49,36 @@ export function NewSession({ onClose, onCreate }: Props) {
}).catch(() => {})
}, [])
// Models for selected backend, sorted
const models = (modelCache[backend] ?? []).slice().sort()
// Auto-select first model when backend or cache changes
useEffect(() => {
if (models.length) setModel(models[0])
}, [backend, modelCache])
const handleBackend = (b: string) => {
setBackend(b)
setModel(DEFAULT_MODELS[b] ?? '')
const cached = modelCache[b]
if (cached?.length) setModel(cached[0])
}
const setAdv = (key: string, val: string) =>
setAdvanced(prev => ({ ...prev, [key]: val }))
const handleSubmit = async (e: Event) => {
e.preventDefault()
if (!cwd.trim() || !model.trim()) return
setLoading(true)
try {
await onCreate({ cwd: cwd.trim(), backend, model: model.trim(), agent })
const params: CreateParams = {
...(name.trim() ? { name: name.trim() } : {}),
cwd: cwd.trim(), backend, model: model.trim(), agent,
}
for (const [k, v] of Object.entries(advanced)) {
if (v.trim()) params[k] = v.trim()
}
await onCreate(params)
} finally {
setLoading(false)
}
@ -72,6 +97,17 @@ export function NewSession({ onClose, onCreate }: Props) {
</div>
<form onSubmit={handleSubmit} style="display:flex;flex-direction:column;gap:18px;">
<div class="field">
<label>name</label>
<input
type="text"
value={name}
onInput={e => setName((e.target as HTMLInputElement).value)}
placeholder="optional"
autofocus
/>
</div>
<div class="field">
<label>working directory</label>
<input
@ -80,7 +116,6 @@ export function NewSession({ onClose, onCreate }: Props) {
onInput={e => setCwd((e.target as HTMLInputElement).value)}
placeholder="~/src/myproject"
required
autofocus
/>
</div>
@ -109,15 +144,33 @@ export function NewSession({ onClose, onCreate }: Props) {
<div class="field">
<label>model</label>
<input
type="text"
<select
value={model}
onInput={e => setModel((e.target as HTMLInputElement).value)}
placeholder="model name"
onChange={e => setModel((e.target as HTMLSelectElement).value)}
required
/>
>
{models.map(m => (
<option key={m} value={m}>{m}</option>
))}
</select>
</div>
<details class="advanced-section">
<summary>Advanced</summary>
<div style="display:flex;flex-direction:column;gap:14px;padding-top:12px;">
{ADVANCED_FIELDS.map(key => (
<div class="field" key={key}>
<label>{key}</label>
<input
type="text"
value={advanced[key] ?? ''}
onInput={e => setAdv(key, (e.target as HTMLInputElement).value)}
/>
</div>
))}
</div>
</details>
<div class="modal-actions">
<button type="button" class="btn btn-ghost" onClick={onClose}>cancel</button>
<button

View File

@ -48,10 +48,12 @@ export async function sendCtl(id: string, ctl: string): Promise<void> {
}
export async function createSession(params: {
name?: string
cwd: string
backend: string
model: string
agent: string
[key: string]: string | undefined
}): Promise<void> {
await fetch('/s/new', {
method: 'POST',
@ -60,6 +62,31 @@ export async function createSession(params: {
})
}
// Fetch models grouped by backend. Persisted in localStorage.
const MODELS_CACHE_KEY = 'ollie:models'
export function loadModelsCache(): Record<string, string[]> {
try {
const raw = localStorage.getItem(MODELS_CACHE_KEY)
if (raw) return JSON.parse(raw)
} catch {}
return {}
}
export async function fetchModels(): Promise<Record<string, string[]>> {
const r = await fetch('/models')
if (!r.ok) return loadModelsCache()
const data: {backend: string, models: string[]}[] = await r.json()
const out: Record<string, string[]> = {}
for (const entry of data) {
out[entry.backend] = entry.models
}
localStorage.setItem(MODELS_CACHE_KEY, JSON.stringify(out))
return out
}
// Kick off fetch in background to refresh cache.
fetchModels()
export async function getBackends(): Promise<string[]> {
const r = await fetch('/backends')
if (!r.ok) return []

View File

@ -12,6 +12,7 @@ export default defineConfig({
'/t/': 'http://localhost:8080',
'/sk/': 'http://localhost:8080',
'/backends': 'http://localhost:8080',
'/models': 'http://localhost:8080',
'/open': 'http://localhost:8080',
'/openapi.json': 'http://localhost:8080',
},