mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
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
This commit is contained in:
@@ -10,19 +10,19 @@ import { useTranslation } from "react-i18next"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
interface ChatEmptyStateProps {
|
||||
hasConfiguredModels: boolean
|
||||
hasAvailableModels: boolean
|
||||
defaultModelName: string
|
||||
isConnected: boolean
|
||||
}
|
||||
|
||||
export function ChatEmptyState({
|
||||
hasConfiguredModels,
|
||||
hasAvailableModels,
|
||||
defaultModelName,
|
||||
isConnected,
|
||||
}: ChatEmptyStateProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!hasConfiguredModels) {
|
||||
if (!hasAvailableModels) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 opacity-70">
|
||||
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-amber-500/10 text-amber-500">
|
||||
|
||||
@@ -39,7 +39,7 @@ export function ChatPage() {
|
||||
|
||||
const {
|
||||
defaultModelName,
|
||||
hasConfiguredModels,
|
||||
hasAvailableModels,
|
||||
apiKeyModels,
|
||||
oauthModels,
|
||||
localModels,
|
||||
@@ -94,7 +94,7 @@ export function ChatPage() {
|
||||
hasScrolled ? "shadow-sm" : "shadow-none"
|
||||
}`}
|
||||
titleExtra={
|
||||
hasConfiguredModels && (
|
||||
hasAvailableModels && (
|
||||
<ModelSelector
|
||||
defaultModelName={defaultModelName}
|
||||
apiKeyModels={apiKeyModels}
|
||||
@@ -140,7 +140,7 @@ export function ChatPage() {
|
||||
<div className="mx-auto flex w-full max-w-250 flex-col gap-8 pb-8">
|
||||
{messages.length === 0 && !isTyping && (
|
||||
<ChatEmptyState
|
||||
hasConfiguredModels={hasConfiguredModels}
|
||||
hasAvailableModels={hasAvailableModels}
|
||||
defaultModelName={defaultModelName}
|
||||
isConnected={isGatewayRunning}
|
||||
/>
|
||||
|
||||
@@ -133,9 +133,10 @@ export function EditModelSheet({
|
||||
}
|
||||
|
||||
const isOAuth = model?.auth_method === "oauth"
|
||||
const apiKeyPlaceholder = model?.configured
|
||||
const hasSavedAPIKey = Boolean(model?.api_key)
|
||||
const apiKeyPlaceholder = hasSavedAPIKey
|
||||
? maskedSecretPlaceholder(
|
||||
model.api_key,
|
||||
model?.api_key ?? "",
|
||||
t("models.field.apiKeyPlaceholderSet"),
|
||||
)
|
||||
: t("models.field.apiKeyPlaceholder")
|
||||
@@ -161,7 +162,7 @@ export function EditModelSheet({
|
||||
<Field
|
||||
label={t("models.field.apiKey")}
|
||||
hint={
|
||||
model?.configured ? t("models.edit.apiKeyHint") : undefined
|
||||
hasSavedAPIKey ? t("models.edit.apiKeyHint") : undefined
|
||||
}
|
||||
>
|
||||
<KeyInput
|
||||
|
||||
@@ -28,14 +28,16 @@ export function ModelCard({
|
||||
}: ModelCardProps) {
|
||||
const { t } = useTranslation()
|
||||
const isOAuth = model.auth_method === "oauth"
|
||||
const status = model.status
|
||||
const statusLabel = t(`models.status.${status}`)
|
||||
const canSetDefault =
|
||||
model.configured && !model.is_default && !model.is_virtual
|
||||
model.available && !model.is_default && !model.is_virtual
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"group/card hover:bg-muted/30 relative flex w-full max-w-[36rem] flex-col gap-3 justify-self-start rounded-xl border p-4 transition-colors hover:shadow-xs",
|
||||
model.configured
|
||||
model.available
|
||||
? "border-border/60 bg-card"
|
||||
: "border-border/50 bg-card/60",
|
||||
].join(" ")}
|
||||
@@ -47,15 +49,13 @@ export function ModelCard({
|
||||
"mt-0.5 h-2 w-2 shrink-0 rounded-full",
|
||||
model.is_default
|
||||
? "bg-green-400 shadow-[0_0_0_2px_rgba(74,222,128,0.35)]"
|
||||
: model.configured
|
||||
: status === "available"
|
||||
? "bg-green-500"
|
||||
: status === "unreachable"
|
||||
? "bg-amber-500"
|
||||
: "bg-muted-foreground/25",
|
||||
].join(" ")}
|
||||
title={
|
||||
model.configured
|
||||
? t("models.status.configured")
|
||||
: t("models.status.unconfigured")
|
||||
}
|
||||
title={statusLabel}
|
||||
/>
|
||||
<span className="text-foreground truncate text-sm font-semibold">
|
||||
{model.model_name}
|
||||
@@ -127,14 +127,14 @@ export function ModelCard({
|
||||
<span className="text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-[10px] font-medium">
|
||||
OAuth
|
||||
</span>
|
||||
) : model.configured && model.api_key ? (
|
||||
) : status === "available" && model.api_key ? (
|
||||
<span className="text-muted-foreground/70 flex items-center gap-1 font-mono text-[11px]">
|
||||
<IconKey className="size-3" />
|
||||
{model.api_key}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground/50 text-[11px]">
|
||||
{t("models.status.unconfigured")}
|
||||
{statusLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ interface ProviderGroup {
|
||||
label: string
|
||||
models: ModelInfo[]
|
||||
hasDefault: boolean
|
||||
configuredCount: number
|
||||
availableCount: number
|
||||
}
|
||||
|
||||
export function ModelsPage() {
|
||||
@@ -62,8 +62,8 @@ export function ModelsPage() {
|
||||
const sorted = [...data.models].sort((a, b) => {
|
||||
if (a.is_default && !b.is_default) return -1
|
||||
if (!a.is_default && b.is_default) return 1
|
||||
if (a.configured && !b.configured) return -1
|
||||
if (!a.configured && b.configured) return 1
|
||||
if (a.available && !b.available) return -1
|
||||
if (!a.available && b.available) return 1
|
||||
return a.model_name.localeCompare(b.model_name)
|
||||
})
|
||||
setModels(sorted)
|
||||
@@ -107,23 +107,23 @@ export function ModelsPage() {
|
||||
|
||||
const providerGroups: ProviderGroup[] = Object.entries(grouped)
|
||||
.map(([key, group]) => {
|
||||
const configuredCount = group.models.filter(
|
||||
(model) => model.configured,
|
||||
const availableCount = group.models.filter(
|
||||
(model) => model.available,
|
||||
).length
|
||||
return {
|
||||
key,
|
||||
label: group.label,
|
||||
models: group.models,
|
||||
hasDefault: group.models.some((model) => model.is_default),
|
||||
configuredCount,
|
||||
availableCount,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.hasDefault && !b.hasDefault) return -1
|
||||
if (!a.hasDefault && b.hasDefault) return 1
|
||||
|
||||
if (a.configuredCount !== b.configuredCount) {
|
||||
return b.configuredCount - a.configuredCount
|
||||
if (a.availableCount !== b.availableCount) {
|
||||
return b.availableCount - a.availableCount
|
||||
}
|
||||
|
||||
const aPriority = PROVIDER_PRIORITY[a.key] ?? Number.MAX_SAFE_INTEGER
|
||||
|
||||
Reference in New Issue
Block a user