docs: remove implemented ai-endpoint-status-dot backlog plan
The configured x enabled status dot is implemented and merged via this branch, so the backlog plan is no longer needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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».
|
|
||||||
- Точки рендерятся в заголовках трёх карточек: `<StatusDot status={chatStatus}/>`
|
|
||||||
(~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 (
|
|
||||||
<Box w={9} h={9} style={{ borderRadius: "50%", background: color, flex: "none" }} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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-е состояние «тест упал»?
|
|
||||||
Reference in New Issue
Block a user