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 { 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
|
cwd: string
|
||||||
backend: string
|
backend: string
|
||||||
model: string
|
model: string
|
||||||
agent: string
|
agent: string
|
||||||
|
[key: string]: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -13,29 +15,33 @@ interface Props {
|
||||||
onCreate: (p: CreateParams) => Promise<void>
|
onCreate: (p: CreateParams) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_MODELS: Record<string, string> = {
|
|
||||||
anthropic: 'claude-opus-4-5',
|
|
||||||
openai: 'gpt-4o',
|
const ADVANCED_FIELDS = [
|
||||||
ollama: 'llama3',
|
'maxTokens', 'maxCompletionTokens', 'temperature', 'topP', 'topK',
|
||||||
codewhisperer: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
|
'minP', 'topA', 'frequencyPenalty', 'presencePenalty', 'repetitionPenalty',
|
||||||
copilot: 'gpt-4o',
|
'reasoning', 'reasoningEffort', 'includeReasoning', 'responseFormat',
|
||||||
}
|
'stop', 'verbosity',
|
||||||
|
] as const
|
||||||
|
|
||||||
export function NewSession({ onClose, onCreate }: Props) {
|
export function NewSession({ onClose, onCreate }: Props) {
|
||||||
|
const [name, setName] = useState('')
|
||||||
const [cwd, setCwd] = useState('~')
|
const [cwd, setCwd] = useState('~')
|
||||||
const [backend, setBackend] = useState('anthropic')
|
const [backend, setBackend] = useState('anthropic')
|
||||||
const [model, setModel] = useState(DEFAULT_MODELS['anthropic'])
|
const [model, setModel] = useState('')
|
||||||
const [agent, setAgent] = useState('default')
|
const [agent, setAgent] = useState('default')
|
||||||
const [backends, setBackends] = useState<string[]>([])
|
const [backends, setBackends] = useState<string[]>([])
|
||||||
const [agents, setAgents] = useState<string[]>(['default'])
|
const [agents, setAgents] = useState<string[]>(['default'])
|
||||||
|
const [modelCache, setModelCache] = useState(loadModelsCache)
|
||||||
|
const [advanced, setAdvanced] = useState<Record<string, string>>({})
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
fetchModels().then(setModelCache).catch(() => {})
|
||||||
getBackends().then(bs => {
|
getBackends().then(bs => {
|
||||||
if (bs.length > 0) {
|
if (bs.length > 0) {
|
||||||
setBackends(bs)
|
setBackends(bs)
|
||||||
setBackend(bs[0])
|
setBackend(bs[0])
|
||||||
setModel(DEFAULT_MODELS[bs[0]] ?? '')
|
|
||||||
}
|
}
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
getAgents().then(as => {
|
getAgents().then(as => {
|
||||||
|
|
@ -43,17 +49,36 @@ export function NewSession({ onClose, onCreate }: Props) {
|
||||||
}).catch(() => {})
|
}).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) => {
|
const handleBackend = (b: string) => {
|
||||||
setBackend(b)
|
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) => {
|
const handleSubmit = async (e: Event) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!cwd.trim() || !model.trim()) return
|
if (!cwd.trim() || !model.trim()) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
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 {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +97,17 @@ export function NewSession({ onClose, onCreate }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} style="display:flex;flex-direction:column;gap:18px;">
|
<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">
|
<div class="field">
|
||||||
<label>working directory</label>
|
<label>working directory</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -80,7 +116,6 @@ export function NewSession({ onClose, onCreate }: Props) {
|
||||||
onInput={e => setCwd((e.target as HTMLInputElement).value)}
|
onInput={e => setCwd((e.target as HTMLInputElement).value)}
|
||||||
placeholder="~/src/myproject"
|
placeholder="~/src/myproject"
|
||||||
required
|
required
|
||||||
autofocus
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -109,15 +144,33 @@ export function NewSession({ onClose, onCreate }: Props) {
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>model</label>
|
<label>model</label>
|
||||||
<input
|
<select
|
||||||
type="text"
|
|
||||||
value={model}
|
value={model}
|
||||||
onInput={e => setModel((e.target as HTMLInputElement).value)}
|
onChange={e => setModel((e.target as HTMLSelectElement).value)}
|
||||||
placeholder="model name"
|
|
||||||
required
|
required
|
||||||
/>
|
>
|
||||||
|
{models.map(m => (
|
||||||
|
<option key={m} value={m}>{m}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</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">
|
<div class="modal-actions">
|
||||||
<button type="button" class="btn btn-ghost" onClick={onClose}>cancel</button>
|
<button type="button" class="btn btn-ghost" onClick={onClose}>cancel</button>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,12 @@ export async function sendCtl(id: string, ctl: string): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createSession(params: {
|
export async function createSession(params: {
|
||||||
|
name?: string
|
||||||
cwd: string
|
cwd: string
|
||||||
backend: string
|
backend: string
|
||||||
model: string
|
model: string
|
||||||
agent: string
|
agent: string
|
||||||
|
[key: string]: string | undefined
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await fetch('/s/new', {
|
await fetch('/s/new', {
|
||||||
method: 'POST',
|
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[]> {
|
export async function getBackends(): Promise<string[]> {
|
||||||
const r = await fetch('/backends')
|
const r = await fetch('/backends')
|
||||||
if (!r.ok) return []
|
if (!r.ok) return []
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export default defineConfig({
|
||||||
'/t/': 'http://localhost:8080',
|
'/t/': 'http://localhost:8080',
|
||||||
'/sk/': 'http://localhost:8080',
|
'/sk/': 'http://localhost:8080',
|
||||||
'/backends': 'http://localhost:8080',
|
'/backends': 'http://localhost:8080',
|
||||||
|
'/models': 'http://localhost:8080',
|
||||||
'/open': 'http://localhost:8080',
|
'/open': 'http://localhost:8080',
|
||||||
'/openapi.json': 'http://localhost:8080',
|
'/openapi.json': 'http://localhost:8080',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Reference in New Issue