feat(ai-settings): replace useless API-key eye with in-field Clear

The API-key PasswordInput is a write-only buffer — the saved key never
loads back, so the built-in visibility toggle reveals nothing. When a key
is saved and the buffer is empty, render a red Clear (X) ActionIcon in the
field's rightSection (which suppresses the Mantine eye); while typing a new
key the default eye returns. Applied to all three AI endpoints (Chat,
Embeddings, Voice/STT). Handlers and payload logic unchanged.

Implements docs/backlog/api-key-field-clear-in-place-of-eye.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-20 05:05:34 +03:00
parent c8af637654
commit 3299bde929

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { z } from "zod/v4";
import {
Anchor,
ActionIcon,
Badge,
Box,
Button,
@@ -15,12 +15,13 @@ import {
Text,
Textarea,
TextInput,
Tooltip,
useMantineTheme,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { useDisclosure } from "@mantine/hooks";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { IconPencil } from "@tabler/icons-react";
import { IconPencil, IconX } from "@tabler/icons-react";
import { useAtom } from "jotai";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
@@ -430,19 +431,34 @@ export default function AiProviderSettings() {
disabled={isLoading}
{...form.getInputProps("chatModel")}
/>
<Stack gap={4}>
<PasswordInput
label={t("API key")}
placeholder={hasApiKey ? t("•••• set") : ""}
autoComplete="off"
{...form.getInputProps("apiKey")}
/>
{hasApiKey && (
<Anchor component="button" type="button" c="red" size="xs" onClick={handleClearKey}>
{t("Clear")}
</Anchor>
)}
</Stack>
{/* The key field is write-only: the stored key never loads back, so
the built-in visibility toggle reveals nothing. Replace it with a
Clear action in the right section. Passing rightSection suppresses
the eye (Mantine). While typing a new key (buffer non-empty) fall
back to the default eye. */}
<PasswordInput
label={t("API key")}
placeholder={hasApiKey ? t("•••• set") : ""}
autoComplete="off"
rightSection={
hasApiKey && form.values.apiKey.length === 0 ? (
<Tooltip label={t("Clear")}>
<ActionIcon
variant="subtle"
color="red"
size="sm"
type="button"
aria-label={t("Clear")}
onClick={handleClearKey}
>
<IconX size={16} />
</ActionIcon>
</Tooltip>
) : undefined
}
rightSectionPointerEvents="all"
{...form.getInputProps("apiKey")}
/>
</Group>
<TextInput
@@ -535,29 +551,35 @@ export default function AiProviderSettings() {
disabled={isLoading}
{...form.getInputProps("embeddingModel")}
/>
<Stack gap={4}>
<PasswordInput
label={t("Embedding API key")}
placeholder={
hasEmbeddingApiKey
? t("•••• set")
: t("Leave empty to use the chat API key")
}
autoComplete="off"
{...form.getInputProps("embeddingApiKey")}
/>
{hasEmbeddingApiKey && (
<Anchor
component="button"
type="button"
c="red"
size="xs"
onClick={handleClearEmbeddingKey}
>
{t("Clear")}
</Anchor>
)}
</Stack>
{/* Write-only key field: Clear replaces the useless visibility
toggle in the right section (see the Chat block for the rationale). */}
<PasswordInput
label={t("Embedding API key")}
placeholder={
hasEmbeddingApiKey
? t("•••• set")
: t("Leave empty to use the chat API key")
}
autoComplete="off"
rightSection={
hasEmbeddingApiKey && form.values.embeddingApiKey.length === 0 ? (
<Tooltip label={t("Clear")}>
<ActionIcon
variant="subtle"
color="red"
size="sm"
type="button"
aria-label={t("Clear")}
onClick={handleClearEmbeddingKey}
>
<IconX size={16} />
</ActionIcon>
</Tooltip>
) : undefined
}
rightSectionPointerEvents="all"
{...form.getInputProps("embeddingApiKey")}
/>
</Group>
<TextInput
@@ -654,29 +676,35 @@ export default function AiProviderSettings() {
disabled={isLoading}
{...form.getInputProps("sttModel")}
/>
<Stack gap={4}>
<PasswordInput
label={t("API key")}
placeholder={
hasSttApiKey
? t("•••• set")
: t("Leave empty to use the chat API key")
}
autoComplete="off"
{...form.getInputProps("sttApiKey")}
/>
{hasSttApiKey && (
<Anchor
component="button"
type="button"
c="red"
size="xs"
onClick={handleClearSttKey}
>
{t("Clear")}
</Anchor>
)}
</Stack>
{/* Write-only key field: Clear replaces the useless visibility
toggle in the right section (see the Chat block for the rationale). */}
<PasswordInput
label={t("API key")}
placeholder={
hasSttApiKey
? t("•••• set")
: t("Leave empty to use the chat API key")
}
autoComplete="off"
rightSection={
hasSttApiKey && form.values.sttApiKey.length === 0 ? (
<Tooltip label={t("Clear")}>
<ActionIcon
variant="subtle"
color="red"
size="sm"
type="button"
aria-label={t("Clear")}
onClick={handleClearSttKey}
>
<IconX size={16} />
</ActionIcon>
</Tooltip>
) : undefined
}
rightSectionPointerEvents="all"
{...form.getInputProps("sttApiKey")}
/>
</Group>
<Select