fix(frontend): polish model and credential UX; remove Providers nav

- remove the Providers item from sidebar navigation and locale keys
- simplify chat composer by dropping attach/voice action buttons
- support ReactNode titles in credential cards and add provider brand icons
- refine sheet header/footer styling and device-code footer button hierarchy
- disable “Set default” when a model is unconfigured or already default
This commit is contained in:
wenjie 2026-03-06 19:27:50 +08:00
parent 747c86aa89
commit ce92d1f062
12 changed files with 50 additions and 59 deletions

View file

@ -1,7 +1,6 @@
import { IconChevronRight } from "@tabler/icons-react" import { IconChevronRight } from "@tabler/icons-react"
import { import {
IconAtom, IconAtom,
IconCloud,
IconKey, IconKey,
IconListDetails, IconListDetails,
IconMessageCircle, IconMessageCircle,
@ -39,7 +38,6 @@ const navGroups = [
label: "navigation.model_group", label: "navigation.model_group",
defaultOpen: true, defaultOpen: true,
items: [ items: [
{ title: "navigation.providers", url: "/providers", icon: IconCloud },
{ title: "navigation.models", url: "/models", icon: IconAtom }, { title: "navigation.models", url: "/models", icon: IconAtom },
{ title: "navigation.credentials", url: "/credentials", icon: IconKey }, { title: "navigation.credentials", url: "/credentials", icon: IconKey },
], ],

View file

@ -1,14 +1,9 @@
import { IconArrowUp, IconMicrophone, IconPaperclip } from "@tabler/icons-react" import { IconArrowUp } from "@tabler/icons-react"
import type { KeyboardEvent } from "react" import type { KeyboardEvent } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import TextareaAutosize from "react-textarea-autosize" import TextareaAutosize from "react-textarea-autosize"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
interface ChatComposerProps { interface ChatComposerProps {
input: string input: string
@ -50,35 +45,7 @@ export function ChatComposer({
/> />
<div className="mt-2 flex items-center justify-between px-1"> <div className="mt-2 flex items-center justify-between px-1">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">{/* action buttons */}</div>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground size-8 rounded-full"
disabled={!isConnected}
>
<IconPaperclip className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{t("chat.attach")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground size-8 rounded-full"
disabled={!isConnected}
>
<IconMicrophone className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{t("chat.voice")}</TooltipContent>
</Tooltip>
</div>
<Button <Button
size="icon" size="icon"

View file

@ -1,4 +1,9 @@
import { IconKey, IconLoader2, IconPlayerStopFilled } from "@tabler/icons-react" import {
IconKey,
IconLoader2,
IconPlayerStopFilled,
IconSparkles,
} from "@tabler/icons-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import type { OAuthProviderStatus } from "@/api/oauth" import type { OAuthProviderStatus } from "@/api/oauth"
@ -33,7 +38,14 @@ export function AnthropicCredentialCard({
return ( return (
<CredentialCard <CredentialCard
title="Anthropic" title={
<span className="inline-flex items-center gap-2">
<span className="border-muted inline-flex size-6 items-center justify-center rounded-full border">
<IconSparkles className="size-3.5" />
</span>
<span>Anthropic</span>
</span>
}
description={t( description={t(
"credentials.providers.anthropic.description", "credentials.providers.anthropic.description",
"Uses token login for Claude access.", "Uses token login for Claude access.",

View file

@ -1,4 +1,5 @@
import { import {
IconBrandGoogle,
IconLoader2, IconLoader2,
IconLockOpen, IconLockOpen,
IconPlayerStopFilled, IconPlayerStopFilled,
@ -31,7 +32,14 @@ export function AntigravityCredentialCard({
return ( return (
<CredentialCard <CredentialCard
title="Google Antigravity" title={
<span className="inline-flex items-center gap-2">
<span className="border-muted inline-flex size-6 items-center justify-center rounded-full border">
<IconBrandGoogle className="size-3.5" />
</span>
<span>Google Antigravity</span>
</span>
}
description={t( description={t(
"credentials.providers.antigravity.description", "credentials.providers.antigravity.description",
"Uses browser OAuth for Google Cloud Code Assist.", "Uses browser OAuth for Google Cloud Code Assist.",

View file

@ -5,7 +5,7 @@ import type { OAuthProviderStatus } from "@/api/oauth"
import { ProviderStatusLine } from "./provider-status-line" import { ProviderStatusLine } from "./provider-status-line"
interface CredentialCardProps { interface CredentialCardProps {
title: string title: ReactNode
description: string description: string
status: OAuthProviderStatus["status"] status: OAuthProviderStatus["status"]
authMethod?: string authMethod?: string

View file

@ -33,7 +33,7 @@ export function DeviceCodeSheet({
side="right" side="right"
className="data-[side=right]:!w-full data-[side=right]:sm:!w-[480px] data-[side=right]:sm:!max-w-[480px]" className="data-[side=right]:!w-full data-[side=right]:sm:!w-[480px] data-[side=right]:sm:!max-w-[480px]"
> >
<SheetHeader className="border-b px-6 py-5"> <SheetHeader className="border-b-muted border-b px-6 py-5">
<SheetTitle> <SheetTitle>
{t("credentials.device.title", "OpenAI Device Login")} {t("credentials.device.title", "OpenAI Device Login")}
</SheetTitle> </SheetTitle>
@ -81,15 +81,15 @@ export function DeviceCodeSheet({
)} )}
</div> </div>
<SheetFooter className="border-t px-6 py-4"> <SheetFooter className="border-t-muted border-t px-6 py-4">
<Button variant="outline" asChild disabled={!flow?.verify_url}> <Button variant="ghost" onClick={() => onOpenChange(false)}>
{t("common.cancel", "Close")}
</Button>
<Button asChild disabled={!flow?.verify_url}>
<a href={flow?.verify_url || "#"} target="_blank" rel="noreferrer"> <a href={flow?.verify_url || "#"} target="_blank" rel="noreferrer">
{t("credentials.device.open", "Open Verification Page")} {t("credentials.device.open", "Open Verification Page")}
</a> </a>
</Button> </Button>
<Button onClick={() => onOpenChange(false)}>
{t("common.cancel", "Close")}
</Button>
</SheetFooter> </SheetFooter>
</SheetContent> </SheetContent>
</Sheet> </Sheet>

View file

@ -45,7 +45,14 @@ export function OpenAICredentialCard({
return ( return (
<CredentialCard <CredentialCard
title="OpenAI" title={
<span className="inline-flex items-center gap-2">
<span className="border-muted inline-flex size-6 items-center justify-center rounded-full border">
<IconBrandOpenai className="size-3.5" />
</span>
<span>OpenAI</span>
</span>
}
description={t( description={t(
"credentials.providers.openai.description", "credentials.providers.openai.description",
"Supports browser OAuth, device code, and token login.", "Supports browser OAuth, device code, and token login.",

View file

@ -126,7 +126,7 @@ export function EditModelSheet({
side="right" side="right"
className="flex flex-col gap-0 p-0 data-[side=right]:!w-full data-[side=right]:sm:!w-[560px] data-[side=right]:sm:!max-w-[560px]" className="flex flex-col gap-0 p-0 data-[side=right]:!w-full data-[side=right]:sm:!w-[560px] data-[side=right]:sm:!max-w-[560px]"
> >
<SheetHeader className="border-b px-6 py-5"> <SheetHeader className="border-b-muted border-b px-6 py-5">
<SheetTitle className="text-base"> <SheetTitle className="text-base">
{t("models.edit.title", { name: model?.model_name })} {t("models.edit.title", { name: model?.model_name })}
</SheetTitle> </SheetTitle>
@ -286,7 +286,7 @@ export function EditModelSheet({
</div> </div>
</div> </div>
<SheetFooter className="border-t px-6 py-4"> <SheetFooter className="border-t-muted border-t px-6 py-4">
<Button variant="ghost" onClick={onClose} disabled={saving}> <Button variant="ghost" onClick={onClose} disabled={saving}>
{t("common.cancel")} {t("common.cancel")}
</Button> </Button>

View file

@ -28,6 +28,7 @@ export function ModelCard({
}: ModelCardProps) { }: ModelCardProps) {
const { t } = useTranslation() const { t } = useTranslation()
const isOAuth = model.auth_method === "oauth" const isOAuth = model.auth_method === "oauth"
const canSetDefault = model.configured && !model.is_default
return ( return (
<div <div
@ -78,7 +79,7 @@ export function ModelCard({
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
onClick={() => onSetDefault(model)} onClick={() => onSetDefault(model)}
disabled={settingDefault} disabled={settingDefault || !canSetDefault}
title={t("models.action.setDefault")} title={t("models.action.setDefault")}
> >
{settingDefault ? ( {settingDefault ? (

View file

@ -13,12 +13,12 @@ import { getProviderKey, getProviderLabel } from "./provider-label"
import { ProviderSection } from "./provider-section" import { ProviderSection } from "./provider-section"
const PROVIDER_PRIORITY: Record<string, number> = { const PROVIDER_PRIORITY: Record<string, number> = {
openai: 0, volcengine: 0,
gemini: 1, openai: 1,
anthropic: 2, gemini: 2,
zhipu: 3, anthropic: 3,
deepseek: 4, zhipu: 4,
volcengine: 5, deepseek: 5,
openrouter: 6, openrouter: 6,
qwen: 7, qwen: 7,
moonshot: 8, moonshot: 8,

View file

@ -2,7 +2,6 @@
"navigation": { "navigation": {
"chat": "Chat", "chat": "Chat",
"model_group": "Models", "model_group": "Models",
"providers": "Providers",
"models": "Models", "models": "Models",
"credentials": "Credentials", "credentials": "Credentials",
"services": "Services", "services": "Services",

View file

@ -2,7 +2,6 @@
"navigation": { "navigation": {
"chat": "对话", "chat": "对话",
"model_group": "模型", "model_group": "模型",
"providers": "服务商",
"models": "模型", "models": "模型",
"credentials": "凭据", "credentials": "凭据",
"services": "服务", "services": "服务",