model selector: cached dropdown filtered by backend
This commit is contained in:
parent
c61a674289
commit
23a2de8fac
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
Reference in New Issue