Remove outdated process sections from several backlog markdown files and add new backlog items for AI chat step limits, endpoint status config, and API key field UI improvements.
9.3 KiB
Поле «API key»: убрать бесполезный «глазок», поставить Clear на его место
Статус: план, код не менялся. UI-задача на клиенте. Бэкенда не касается.
Суть
В настройках AI-провайдера (Workspace settings → AI) у каждого из трёх
эндпоинтов есть поле PasswordInput для API-ключа. Когда ключ уже сохранён на
сервере, поле показывает плейсхолдер •••• set, а справа — встроенный в
Mantine PasswordInput тогл видимости («глазок»). Под полем отдельной строкой
висит ссылка Clear.
Проблема: «глазок» бессмысленен. Поле ключа — write-only буфер: реальный
ключ в него никогда не загружается (сервер отдаёт только факт «ключ есть»,
hasApiKey, см. ai-provider-settings.tsx:120-130, 154-177). Когда ключ
сохранён, буфер пустой → нажатие «глазка» показывает пустоту. Полезного смысла
нет.
Хотим: в состоянии «ключ сохранён» показывать кнопку Clear прямо на месте «глазка» (в правой секции поля), а не отдельной ссылкой снизу. Сделать это во всех трёх эндпоинтах (Chat / LLM, Embeddings, Voice / STT).
Где править (точные места)
Один файл: ai-provider-settings.tsx
Три одинаковых по структуре блока — <Stack gap={4}> с PasswordInput + ссылкой
<Anchor>Clear</Anchor> снизу:
- Chat / LLM — строки ~433-445 (
apiKey,hasApiKey,handleClearKey). - Embeddings — строки ~538-560 (
embeddingApiKey,hasEmbeddingApiKey,handleClearEmbeddingKey). - Voice / STT — строки ~657-679 (
sttApiKey,hasSttApiKey,handleClearSttKey).
Обработчики очистки (handleClearKey / handleClearEmbeddingKey /
handleClearSttKey, строки 239-255) и вся логика буферов/payload
(buildPayload, строки 179-222) — остаются без изменений. Меняется только
разметка трёх полей.
Ключевой факт Mantine (подтверждён по докам)
У PasswordInput: если передать свой rightSection, встроенный тогл
видимости («глазок») не рендерится (Mantine docs, PasswordInput → «Usage
without visibility toggle»: “When the rightSection prop is used, the
visibility toggle button is not rendered.”).
То есть «поставить Clear на место глазка» = передать в PasswordInput
rightSection с кнопкой Clear. Отдельный костыль для скрытия глазка не нужен.
Рекомендуемое поведение
Показывать Clear в правой секции только когда ключ сохранён И буфер пуст
(hasApiKey && form.values.apiKey.length === 0). Как только пользователь
начинает вводить НОВЫЙ ключ (буфер непустой) — возвращать дефолтный «глазок»:
вот тут он осмыслен (проверить, что набрал). После клика по Clear обработчик
ставит hasApiKey=false → rightSection снова undefined → поле становится
обычным пустым PasswordInput с глазком для ввода свежего ключа. Поведение
самосогласованное.
Альтернатива (проще, но грубее): показывать Clear всегда, пока hasApiKey
(без проверки буфера). Тогда при вводе нового поверх старого глазка не будет.
Допустимо, но теряем удобную проверку набранного. Рекомендуется вариант с
проверкой буфера.
Эскиз правки (на примере Chat-поля; для двух других — аналогично)
Было:
<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"
aria-label={t("Clear")}
onClick={handleClearKey}
>
<IconX size={16} />
</ActionIcon>
</Tooltip>
) : undefined
}
rightSectionPointerEvents="all"
{...form.getInputProps("apiKey")}
/>
Изменения по каждому из трёх блоков:
- Убрать обёртку
<Stack gap={4}>…</Stack>и ссылку<Anchor>Clear</Anchor>снизу (Clear переезжает внутрь поля). После удаленияStackвторой ребёнок<Group grow>— самPasswordInput; раскладка «Model | API key» в две колонки сохраняется. - Подставить свои переменные/обработчики: эндпоинт 2 —
hasEmbeddingApiKey/embeddingApiKey/handleClearEmbeddingKey; эндпоинт 3 —hasSttApiKey/sttApiKey/handleClearSttKey.
Тонкости / на что смотреть
- Импорты. Добавить
ActionIcon,Tooltipиз@mantine/coreиIconXиз@tabler/icons-react(рядом с уже импортируемымIconPencil). После переезда Clear внутрь поляAnchorможет стать неиспользуемым — проверить и убрать из импорта, иначе словим lint-ошибкуno-unused-vars. - Кликабельность правой секции. У
Input/PasswordInputправая секция по умолчанию не всегда принимает клики — задатьrightSectionPointerEvents="all", чтобы клик по Clear срабатывал. - Тип кнопки.
ActionIconрендерит<button>(по умолчаниюtype="button"). Формы как<form onSubmit>тут нет — Save висит на отдельнойtype="button"кнопке (строки 735-744), так что случайного сабмита не будет. Для надёжности можно явно проставитьtype="button". - i18n. Новый строковый ключ не нужен:
t("Clear")уже используется (бывшая ссылка). Тултип иaria-labelпереиспользуют его. Плейсхолдер•••• setне трогаем. - Ширина правой секции. Иконка X помещается в штатный размер секции (как и
глазок). Если решат оставить именно слово «Clear» текстом вместо иконки —
понадобится
rightSectionWidth, иначе текст обрежется. Рекомендуется иконка + тултип (компактно, как глазок). - Доступность. Обязателен
aria-label={t("Clear")}наActionIcon(иконка без видимого текста).
Опционально (вне «трёх эндпоинтов»)
Тот же паттерн «бесполезный глазок + Clear снизу» есть в форме внешнего
MCP-сервера —
ai-mcp-server-form.tsx
(поле Authorization-заголовков, PasswordInput строка ~193, плейсхолдер
•••• set строка ~196, Anchor-Clear строки ~207-209, обработчик
handleClearHeaders). В запросе он не входит в «три эндпоинта», но логически
страдает тем же. Можно причесать заодно для единообразия — отдельным мелким
шагом, по той же схеме.