Files
picoclaw/web/frontend/src/hooks/use-chat-models.ts
T
LC 3b3f95c44c feat(web): refine model availability states and preserve API key preview placeholder (#2226)
* feat(web): clarify model availability and status display

- Rename model availability field from configured to available across backend API and frontend usage

- Keep status as reason classification (configured/unconfigured/unreachable) and show unreachable in UI

- Preserve API key preview even when local service is unreachable

- Update backend tests to assert both availability and status semantics

* fix(web): clarify unreachable model status and wording

- Show unreachable status in model cards instead of API key preview when service is down

- Keep API key placeholder preview in model settings whenever an API key is already saved

- Rename model status wording from configured to available across backend, frontend, and i18n

- Update backend model status tests to match renamed status semantics

* style(web): standardize formatting in handleListModels function

* refactor(web): enforce status field as required to follow backend behavior
2026-03-31 22:52:04 +08:00

100 lines
2.5 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { type ModelInfo, getModels, setDefaultModel } from "@/api/models"
interface UseChatModelsOptions {
isConnected: boolean
}
function isLocalModel(model: ModelInfo): boolean {
const isLocalHostBase = Boolean(
model.api_base?.includes("localhost") ||
model.api_base?.includes("127.0.0.1"),
)
return (
model.auth_method === "local" || (!model.auth_method && isLocalHostBase)
)
}
export function useChatModels({ isConnected }: UseChatModelsOptions) {
const [modelList, setModelList] = useState<ModelInfo[]>([])
const [defaultModelName, setDefaultModelName] = useState("")
const setDefaultRequestIdRef = useRef(0)
const loadModels = useCallback(async () => {
try {
const data = await getModels()
setModelList(data.models)
if (data.models.some((m) => m.model_name === data.default_model)) {
setDefaultModelName(data.default_model)
}
} catch {
// silently fail
}
}, [])
useEffect(() => {
const timerId = setTimeout(() => {
void loadModels()
}, 0)
return () => clearTimeout(timerId)
}, [isConnected, loadModels])
const handleSetDefault = useCallback(
async (modelName: string) => {
if (modelName === defaultModelName) return
const requestId = ++setDefaultRequestIdRef.current
try {
await setDefaultModel(modelName)
const data = await getModels()
if (requestId !== setDefaultRequestIdRef.current) {
return
}
setModelList(data.models)
if (data.models.some((m) => m.model_name === data.default_model)) {
setDefaultModelName(data.default_model)
}
} catch (err) {
console.error("Failed to set default model:", err)
}
},
[defaultModelName],
)
const hasAvailableModels = useMemo(
() => modelList.some((m) => m.available),
[modelList],
)
const oauthModels = useMemo(
() => modelList.filter((m) => m.available && m.auth_method === "oauth"),
[modelList],
)
const localModels = useMemo(
() => modelList.filter((m) => m.available && isLocalModel(m)),
[modelList],
)
const apiKeyModels = useMemo(
() =>
modelList.filter(
(m) => m.available && m.auth_method !== "oauth" && !isLocalModel(m),
),
[modelList],
)
return {
defaultModelName,
hasAvailableModels,
apiKeyModels,
oauthModels,
localModels,
handleSetDefault,
}
}