diff --git a/docs/backlog/ai-endpoint-status-dot-config-enabled.md b/docs/backlog/ai-endpoint-status-dot-config-enabled.md deleted file mode 100644 index e7375524..00000000 --- a/docs/backlog/ai-endpoint-status-dot-config-enabled.md +++ /dev/null @@ -1,224 +0,0 @@ -# Индикатор-точка эндпоинта AI: «настроено / включено» вместо «результат теста» - -## Контекст (симптом) - -В админских настройках AI (Workspace settings → AI) у каждой карточки-эндпоинта -(«Chat / LLM», «Embeddings», «Voice / STT») слева от заголовка есть маленькая -цветная точка. Сейчас её цвет означает **результат последнего ручного теста** -кнопкой «Test endpoint», а не состояние настройки: - -- зелёная — тест «Test endpoint» прошёл (`ok`); -- красная — тест упал (`error`); -- серая — тест ещё **не запускали** (`idle`). - -Поэтому на текущем экране у «Embeddings» точка зелёная (по карточке нажимали -«Test endpoint» → «Connection successful»), а у «Voice / STT» — серая, **хотя -тумблер «Voice dictation» включён и эндпоинт настроен**. Тумблеры фич -(`chat` / `search` / `dictation`) и сам факт заполненности полей (модель + -Base URL) на цвет точки сейчас никак не влияют. - -Хотим, чтобы точка читалась с одного взгляда как состояние эндпоинта, без -ручного теста: - -- **зелёная** — корректно настроено **и** включено; -- **жёлтая** — настроено, но **не** включено; -- **серая** — выключено / не настроено (нечего включать). - -## Как сейчас устроено (цепочка) - -Всё в одном файле клиента: -`apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx`. - -- Тип состояния точки: `type CardStatus = "ok" | "error" | "idle";` - — строка ~64. -- Компонент `StatusDot` (строки ~75-90) красит круг: `ok` → `green[6]`, - `error` → `red[6]`, иначе → `gray[5]`. -- Источник статуса — **только** мутации теста (строки ~356-370): - - ```ts - const chatStatus: CardStatus = chatTest.data - ? (chatTest.data.ok ? "ok" : "error") - : "idle"; - // аналогично embedStatus (embedTest), sttStatus (sttTest) - ``` - - `chatTest` / `embedTest` / `sttTest` — это `useTestAiConnectionMutation()` - (строки ~101-104); их `data` появляется только после нажатия «Test endpoint». -- Точки рендерятся в заголовках трёх карточек: `` - (~407), `embedStatus` (~517), `sttStatus` (~634). - -### Какие данные уже доступны в компоненте - -Этого достаточно, чтобы вычислить «настроено» и «включено» синхронно, без сети: - -- **Поля настройки** (живые, из формы) — `form.values`: - `chatModel`, `baseUrl`, `embeddingModel`, `embeddingBaseUrl`, `sttModel`, - `sttBaseUrl`, `sttApiStyle`, и write-only буферы ключей `apiKey`, - `embeddingApiKey`, `sttApiKey`. -- **Наличие сохранённых ключей** — состояния `hasApiKey`, `hasEmbeddingApiKey`, - `hasSttApiKey` (строки ~122-130), синхронизируются с сервером и обновляются - при «Clear» и сохранении. -- **Тумблеры фич** (персистятся в `workspace.settings.ai`) — `chatEnabled` - (`settings.ai.chat`, строка ~108), `searchEnabled` (`settings.ai.search`, - ~111), `dictationEnabled` (`settings.ai.dictation`, ~114). -- **Семантика наследования** (важно для «настроено»): Embeddings и Voice - **наследуют Base URL и ключ от Chat**, если свои не заданы. Это прямо написано - в подзаголовке карточки Chat: «root endpoint — Embeddings and Voice inherit its - URL and key» (строка ~423), и реализовано в `resolveUrl(..., fallback)` - (~373-382). Значит у Embeddings/STT «свой Base URL» не обязателен. - -## Решение (точечное, только клиент) - -Файл: `apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx`. -Перепривязать цвет точки с «результата теста» на пару булевых признаков -**`configured` × `enabled`**. Результат теста остаётся как был — текстом рядом с -кнопкой («Connection successful» / ошибка), точку он больше не красит. - -### 1. Новый тип состояния и чистый хелпер выбора цвета - -```ts -// Three-state endpoint health shown by the header dot. Derived synchronously -// from the form + feature toggle — never from a network probe (the "Test -// endpoint" button still surfaces the live probe result as text). -// "ready" (green) — required fields are filled AND the feature is ON -// "configured" (yellow) — required fields are filled but the feature is OFF -// "off" (gray) — required fields missing (nothing to enable) -type CardStatus = "ready" | "configured" | "off"; - -// Pure + unit-testable. `configured` = the endpoint has everything it needs to -// work; `enabled` = the workspace feature toggle for this endpoint is ON. -function resolveCardStatus(configured: boolean, enabled: boolean): CardStatus { - if (!configured) return "off"; - return enabled ? "ready" : "configured"; -} -``` - -### 2. `StatusDot` — добавить жёлтый - -```ts -function StatusDot({ status }: { status: CardStatus }) { - const theme = useMantineTheme(); - const color = - status === "ready" - ? theme.colors.green[6] - : status === "configured" - ? theme.colors.yellow[6] // Mantine default palette has `yellow` - : theme.colors.gray[5]; - return ( - - ); -} -``` - -### 3. Признак «настроено» для каждой карточки - -Ключ (API key) считаем **необязательным** — локальные серверы (Ollama, speaches -/ faster-whisper-server) работают без ключа, поэтому требовать ключ нельзя. -«Настроено» = задана модель **и** есть Base URL (свой или унаследованный от Chat): - -```ts -const v = form.values; -const chatBase = v.baseUrl.trim(); - -// Chat is the root: needs its own model + base URL. -const chatConfigured = v.chatModel.trim() !== "" && chatBase !== ""; - -// Embeddings / Voice inherit the chat base URL when their own is empty. -const embedConfigured = - v.embeddingModel.trim() !== "" && (v.embeddingBaseUrl.trim() !== "" || chatBase !== ""); -const sttConfigured = - v.sttModel.trim() !== "" && (v.sttBaseUrl.trim() !== "" || chatBase !== ""); -``` - -### 4. Заменить вывод статусов (строки ~356-370) - -```ts -const chatStatus = resolveCardStatus(chatConfigured, chatEnabled); -const embedStatus = resolveCardStatus(embedConfigured, searchEnabled); -const sttStatus = resolveCardStatus(sttConfigured, dictationEnabled); -``` - -`chatTest` / `embedTest` / `sttTest` остаются для текстового результата под -кнопкой «Test endpoint» — их `data` просто больше не участвует в цвете точки. - -### 5. (Рекомендуется) Tooltip на точке — цвет не должен быть единственным сигналом - -Цвет в одиночку недоступен дальтоникам и неочевиден. Обернуть `StatusDot` в -Mantine `Tooltip` с текстовой расшифровкой (через `t(...)`), напр.: -`ready` → «Configured and enabled», `configured` → «Configured but disabled`», -`off` → «Not configured». `Tooltip` уже используется в соседнем -`mcp-settings.tsx`, импорт из `@mantine/core`. - -## Тонкие моменты / edge cases - -- **Источник «настроено» — `form.values` (живой), а не persisted `settings`.** - Тогда точка реагирует прямо при наборе. Минус: тумблер (`*Enabled`) — - персистентный, поэтому после правки полей и **до** «Save endpoints» возможна - кратковременная рассинхронизация (поля изменены, но ещё не сохранены). Это - приемлемо и логично (точка показывает «то, что введено»). Альтернатива — брать - поля из `settings` (тогда точка отражает строго сохранённое состояние, - согласованно с тумблером) — см. «Альтернативы». -- **Включено, но НЕ настроено** (`enabled && !configured`): админ включил фичу, но - не заполнил эндпоинт — реальная мисконфигурация. По строгой трёхцветной схеме - это **серый**, что прячет проблему. Варианты: (а) оставить серым (буквально по - ТЗ); (б) **рекомендуется** — отдельный «warning»-цвет (красный/оранжевый) и - тултип «Enabled but not configured», т.к. фича включена и работать не будет. - Решить в «Открытых вопросах». -- **Судьба красного «тест упал».** Сейчас красный = упавший тест. В новой схеме - цвета красного нет. Падение теста по-прежнему видно текстом под кнопкой, так что - сигнал не теряется. Опционально можно сохранить красный как 4-е состояние-оверрайд - (если тест **явно** запускали и он упал) — но это усложняет модель; по умолчанию - не делаем. -- **`yellow` в теме Mantine** есть в дефолтной палитре (Mantine 8) — `yellow[6]` - валиден; кастомная тема в проекте палитру не переопределяет (использовать - `theme.colors.yellow[6]`). -- **Все три карточки** ведут себя единообразно (одна `StatusDot` + один хелпер), - включая «Chat / LLM», которой нет на скриншоте, но логика та же. -- **Оптимистичные тумблеры**: `*Enabled` обновляются оптимистично и - откатываются при ошибке (`handleToggle*`). Цвет точки следует за состоянием - тумблера автоматически (реактивный `useState`). -- **trim()**: значения могут содержать пробелы — сравнивать после `.trim()` (как - в `resolveUrl`). - -## i18n - -- Новые пользовательские строки (тексты тултипов) **только через `t(...)`** и - добавить ключи в каталог `apps/client/public/locales/en-US/translation.json` - (он английско-ключевой: ключ == значение, напр. `"Configured and enabled"`). - Если используется warning-вариант — добавить и его строку. -- Комментарии в коде — на английском (правило проекта). - -## Тесты - -- `resolveCardStatus` — чистая функция, легко юнит-тестируется (Vitest на - клиенте): `(false, *) → "off"`, `(true, true) → "ready"`, `(true, false) → - "configured"`. Если экспортировать `*Configured`-предикаты как чистые - функции от `form.values` — их тоже можно покрыть (особенно наследование Base - URL у Embeddings/STT). -- Запустить `pnpm --filter client lint` и `pnpm --filter client test`. - -## Альтернативы / расширения (вне базового объёма) - -- **Брать «настроено» из persisted `settings`** (а не `form.values`): точка строго - отражает сохранённое состояние, согласовано с персистентным тумблером, но не - реагирует на ввод до «Save». `settings` (`IAiSettings`) уже содержит - `chatModel`/`embeddingModel`/`baseUrl`/`embeddingBaseUrl`/`sttModel`/ - `sttBaseUrl` + `hasApiKey`/`hasEmbeddingApiKey`/`hasSttApiKey`. -- **«настроено» = «тест прошёл»** вместо «поля заполнены»: точнее («корректно»), - но требует автопрогона теста на загрузке (сеть, латентность, лимиты провайдера) - — против идеи мгновенного индикатора. Не рекомендуется. -- **Учитывать ключ для облачных провайдеров**: если Base URL указывает на - публичный провайдер (OpenAI/OpenRouter), ключ де-факто обязателен. Можно - усложнить предикат (`configured` требует ключ, если host не локальный), но это - хрупкая эвристика — оставляем ключ необязательным. - -## Открытые вопросы (согласовать перед реализацией) - -- [ ] Случай «включено, но не настроено»: серый (буквально по ТЗ) или отдельный - warning-цвет (рекомендуется, чтобы не прятать мисконфигурацию)? -- [ ] Что значит «настроено»: «поля модель + Base URL заполнены» (рекомендуется, - ключ необязателен) — ок? Или требовать ещё и ключ? -- [ ] Источник полей: живой `form.values` (реактивно при вводе, рекомендуется) - или persisted `settings` (строго сохранённое состояние)? -- [ ] Добавлять ли `Tooltip` с текстовой расшифровкой (рекомендуется для - доступности) и сохранять ли красный как 4-е состояние «тест упал»?