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:
parent
747c86aa89
commit
ce92d1f062
12 changed files with 50 additions and 59 deletions
|
|
@ -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 },
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"chat": "对话",
|
"chat": "对话",
|
||||||
"model_group": "模型",
|
"model_group": "模型",
|
||||||
"providers": "服务商",
|
|
||||||
"models": "模型",
|
"models": "模型",
|
||||||
"credentials": "凭据",
|
"credentials": "凭据",
|
||||||
"services": "服务",
|
"services": "服务",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue