99855dc2f6
PRODUCT-AI.md: L0 умность в ячейках (Time Arc v1-v4 shipped); L1 подсказки ghosts kind=hint (Front#75); L2 голос-кнопка mark press-to-talk -> ActionPlan с подтверждением пишущих действий + палитра fallback (Front#76). STT/intent через Back-прокси /v1/ai/*; anti-scope: чат, Composer, AI-home, авто-booking, AiRouter в Front. Ссылки: FrontSpec §3.3 + новый §7.3, README. Refs EventHub/EventHubSpec#22
184 lines
15 KiB
Markdown
184 lines
15 KiB
Markdown
# ИИ клиента CalenTIQ (Фаза 3, волна 3)
|
||
|
||
Канон продуктового ИИ: умная сетка + опциональные предиктивные подсказки + голосовой центр выполнения **без чата**. Трекер: [Spec#22](https://git.sabilin.com/EventHub/EventHubSpec/issues/22), эпик [Spec#19](https://git.sabilin.com/EventHub/EventHubSpec/issues/19).
|
||
|
||
Реализация (Future): [Front#75](https://git.sabilin.com/EventHub/EventHubFront/issues/75) подсказки, [Front#76](https://git.sabilin.com/EventHub/EventHubFront/issues/76) голосовой центр. Этот документ — контракт; код подтягивается после него.
|
||
|
||
## Позиция: «ИИ молчит и делает»
|
||
|
||
ИИ CalenTIQ — не собеседник и не интерфейс. Это слой, который **предлагает действия**, а человек их подтверждает и исполняет. Никакого свободного текста от ИИ, никакого диалога, никаких уточняющих вопросов, никакой «личности ассистента».
|
||
|
||
Три следствия:
|
||
|
||
1. Результат любой ИИ-операции — **план действий в UI** (чипы, ghosts, навигация), а не текст.
|
||
2. Неопределённость разрешается **выбором из кандидатов**, не разговором.
|
||
3. Запись в API — только после явного подтверждения человека.
|
||
|
||
## Уровни ИИ
|
||
|
||
| Уровень | Что | Статус | Где живёт |
|
||
|---------|-----|--------|-----------|
|
||
| **L0 — умность в ячейках** | детерминированный скоринг Time Arc (без LLM, без Back) | shipped (v1–v4) | ячейки сетки |
|
||
| **L1 — предиктивные подсказки** | те же ghosts, `kind: 'hint'` + причина «почему» | Future (Front#75), opt-in | ячейки сетки |
|
||
| **L2 — голосовой центр** | голос → ActionPlan → подтверждение | Future (Front#76), opt-in | кнопка Time Arc mark |
|
||
|
||
Единый визуальный язык: подсказка — **не** тост и **не** баннер, а ghost прямо в сетке. Интеллект всегда в ячейках — подсказки тоже в ячейках. Новый UI-слой не изобретается: расширяется уже shipped-механика ghosts (`copy` / `demand` / `phrase` / `repeat` → `+hint`).
|
||
|
||
## Статус Time Arc v1–v4 (L0, shipped)
|
||
|
||
См. FrontSpec §3.3. Кратко:
|
||
|
||
- **v1 (Front#50):** клиентский скоринг лучшего часа: свободные слоты студии ∩ занятость пользователя × mood. Черновики владельца из прошлой недели. Без LLM и без Back.
|
||
- **v2 (Front#51):** память отказов `eh.timeArc.skips` (штраф часу/мастеру, дуга переезжает); буст мастера с ≥2 confirmed; тепло месяца.
|
||
- **v3:** дуга между календарями — `/following` и `/search` баннеры ближайшего часа.
|
||
- **v4 (Front#53):** demand-черновики по pending occupancy; локальный разбор фразы → phrase-черновик у владельца (long-press). Без LLM / AiRouter.
|
||
|
||
Всё L0 — локальные вычисления Front. Этот канон L0 **не меняет**.
|
||
|
||
## Голос-кнопка (L2, контракт Front#76)
|
||
|
||
### Кнопка
|
||
|
||
- Кнопка = **Time Arc mark** рядом с BrandWordmark в AppShell (см. FrontSpec §3.2) — всегда видна, отдельного слота не занимает.
|
||
- **Press-to-talk**: нажал и держишь → говоришь → отпустил → разбор. Отпустил без звука — ничего не происходит. Никакого «зависшего» прослушивания, таймеров VAD и модалок.
|
||
- Состояния (4): `idle` / `listening` / `processing` / `done` — анимируются **самим mark** (дуга заполняется, пока держишь). Бренд сам себе индикатор.
|
||
|
||
### Палитра быстрых команд (обязательный fallback)
|
||
|
||
Голос и тап — **два канала одного движка действий**. Кнопка при недоступности голоса (offline, mic denied, STT упал) открывает палитру тапабельных типовых действий из того же enum интентов. UX голоса не умирает ни в одном degrade-сценарии.
|
||
|
||
### Результат — план действий
|
||
|
||
После разбора показывается стек action-чипов (план), а не текст. Пишущие действия — с одной кнопкой «Выполнить»; обратимые исполняются сразу (см. классы ниже).
|
||
|
||
## ActionPlan (контракт later)
|
||
|
||
STT + intent-разбор выдают структурированный план, не текст. Намётка схемы:
|
||
|
||
```json
|
||
{
|
||
"version": 1,
|
||
"locale": "ru",
|
||
"confidence": 0.87,
|
||
"actions": [
|
||
{ "id": "a1", "intent": "open_calendar", "target": { "calendar_hint": "аурора" }, "resolved": null },
|
||
{ "id": "a2", "intent": "select_hour", "time": { "hour": 19, "day": "next", "dow": 2 } },
|
||
{ "id": "a3", "intent": "book", "confirm": "required" }
|
||
],
|
||
"candidates": []
|
||
}
|
||
```
|
||
|
||
### Интенты — конечный enum
|
||
|
||
Каждый интент исполняется через **уже существующий** UI-флоу. Новых write-эндпоинтов в Back нет.
|
||
|
||
| Intent | Глаголы (ru / en) | Исполняется через |
|
||
|--------|-------------------|-------------------|
|
||
| `navigate` | перейти, открой, покажи / go, open | роуты FrontSpec §4 |
|
||
| `open_calendar` | календарь + название | `/c/:id`; fuzzy-match по `GET /v1/calendars` + following |
|
||
| `select_hour` | «в 19», «завтра в 11» | курсор `calendarContextStore` + подсветка ячейки |
|
||
| `book` | запиши, записаться / book | существующий флоу Book (mandatory confirm + owner pending) |
|
||
| `create_slot_draft` | создай слот, поставь окно | механика phrase-черновиков v4: голос — ещё один источник фразы |
|
||
| `confirm_booking` / `decline_booking` | подтверди, отклони заявку | `PUT /v1/bookings/:id` через `/bookings` |
|
||
| `follow` / `unfollow` | подпишись, отслеживай | `POST/DELETE /v1/calendars/:id/follow` |
|
||
| `search` | найди, ищи | `/search` с заполненными фильтрами |
|
||
|
||
Всё вне enum — отбрасывается одним тостом, без retry-цикла.
|
||
|
||
### Разбор: rule-first
|
||
|
||
Двухслойный: словарь глаголов + regex покрывает типовые фразы («запиши к Ане завтра в 19» — глагол + имя + время, детерминированно). LLM-провайдер вызывается **только** при miss и обязан вернуть тот же ActionPlan-JSON — никогда свободный текст.
|
||
|
||
`create_slot_draft` у владельца использует локальный разбор фразы из v4 → **полностью офлайн** rule-path.
|
||
|
||
### Resolution и кандидаты (вместо вопросов)
|
||
|
||
Каждый `target.hint` резолвится **на клиенте** по локальным данным (календари, following, bookings, roster). Не резолвится → пишется в `candidates` и показывается чипами на выбор. Уточняющих вопросов нет по контракту.
|
||
|
||
### Классы действий
|
||
|
||
| Класс | Примеры | Подтверждение |
|
||
|-------|---------|----------------|
|
||
| **Обратимые** | `navigate`, `open_calendar`, `select_hour`, `search` | исполняются сразу — навигацию всегда можно отменить «назад» |
|
||
| **Пишущие** | `book`, `create_slot_draft`, `confirm/decline_booking`, `follow` | обязательный preview + явный тап «Выполнить» |
|
||
|
||
Обязательное подтверждение перед записью в API соблюдается буквально: запись в API — это только пишущие действия.
|
||
|
||
### Dry-run preview
|
||
|
||
Пишущие действия рендерятся в сетку **до** «Выполнить»: `create_slot_draft` — ghost-ячейкой, `book` — подсветкой целевого часа. Превью плана — тем же визуальным языком, что результат: пользователь видит ровно то, что получит.
|
||
|
||
### Исполнение
|
||
|
||
Последовательное, стоп на первой ошибке, без авто-продолжения. `book` всегда последний в плане и всегда с confirm; сверху — существующий owner-confirm pending (двойная защита записи).
|
||
|
||
## Предиктивные подсказки (L1, контракт Front#75)
|
||
|
||
- Подсказка = ghost `kind: 'hint'` в сетке + причина «почему».
|
||
- **Причины — шаблоны i18n, не LLM**: «вы обычно по вторникам в 19:00». Объяснимость без провайдера.
|
||
- Сигналы — **local-first**: паттерны bookings, `eh.timeArc.skips`, mood; ничего не уходит провайдерам.
|
||
- Частотный cap: не более N hint в неделю на экран (N уточняет Front#75).
|
||
- Dismiss пишет штраф в `timeArcMemoryStore` — тот же механизм skips; подсказка обучается отказам локально.
|
||
|
||
## Opt-in, privacy, degrade
|
||
|
||
### Opt-in
|
||
|
||
- L1 и L2 — **default off**. Тумблеры в `preferences` (`PATCH /v1/user/me`), как mood; до логина — выключены безусловно.
|
||
- Выключенный L1: ghosts v1–v4 работают как сейчас, `hint` не рисуется. Выключенный L2: mark не реагирует на hold, палитра скрыта.
|
||
|
||
### Приват-инварианты
|
||
|
||
- Транскрипт живёт **только в памяти** на время показа плана; не пишется ни в localStorage, ни в Back.
|
||
- Контекст календаря (события, bookings, предпочтения) **никогда** не уходит провайдерам — только аудио/фраза запроса.
|
||
- Аудио не сохраняется: стриминг в STT, discard после транскрипта.
|
||
|
||
### Degrade-матрица
|
||
|
||
| Сценарий | L1 подсказки | L2 голос |
|
||
|----------|--------------|----------|
|
||
| Offline | работают (local-first) | inert-state + тултип; у owner доступен rule-path `create_slot_draft` |
|
||
| Mic denied / нет микрофона | n/a | открывается палитра быстрых команд |
|
||
| STT недоступен / 5xx / таймаут | n/a | один тост; палитра как fallback |
|
||
| Низкий confidence | n/a | один тост «не расслышал», без retry-цикла |
|
||
| Средний confidence | n/a | чипы-кандидаты вместо прямого исполнения |
|
||
| Target не найден | подсказка не показывается | кандидаты; пустой список → тост |
|
||
| Write-ошибка при исполнении (409/403/402) | n/a | план остановлен; стандартная ошибка флоу (402 → `/subscription`) |
|
||
| Opt-in off | ghosts v1–v4 без изменений | mark без реакции |
|
||
|
||
**Инвариант:** ни одно состояние ИИ-слоя не блокирует первичные флоу (сетка, запись, навигация живут независимо).
|
||
|
||
## Провайдер и Back-прокси (контракт later)
|
||
|
||
- STT и intent-разбор — **продуктовый провайдер** (вендор STT + мелкий LLM для intent-JSON при miss rule-слоя).
|
||
- Front ходит только в прокси Back (`/v1/ai/*`): ключи провайдера не покидают бэкенд, единая точка rate-limit, аудита и смены провайдера. Паттерн идентичен GEO/OpenCage (см. [GEO.md](GEO.md)).
|
||
- Back не персистит аудио и транскрипты.
|
||
- **EventHubAiRouter — не продуктовый путь**: это dev-tooling для coding-агентов (Zed). Продуктовый ИИ-клиент о нём не знает; зависимостей Front → AiRouter нет и не будет.
|
||
|
||
## Anti-scope
|
||
|
||
Вне продукта (зафиксировано):
|
||
|
||
- чат с ИИ в любой форме; Composer; отдельная AI-вкладка / AI-home;
|
||
- авто-booking и любые записи в API без подтверждения человека;
|
||
- свободные текстовые ответы ИИ и уточняющие вопросы;
|
||
- подводка EventHubAiRouter / Zed к клиентскому приложению;
|
||
- персист аудио/транскриптов, отправка контекста календаря провайдерам.
|
||
|
||
## Критерии старта Future-реализаций
|
||
|
||
Метрики для решения (собираются в ходе L0/Front#75):
|
||
|
||
- hint CTR; plan-confirm-rate; plan-abort-rate; доля rule-path vs provider-path.
|
||
|
||
Гейты:
|
||
|
||
- **Подсказки (Front#75):** ≥20% активных пользователей с ≥2 confirmed bookings через ghosts за месяц + явное продуктовое решение.
|
||
- **Голос (Front#76):** после пилота подсказок; STT-провайдер с RU и приемлемым ToS; пилот на IFT; минимум один сценарий с конверсией лучше ручного пути.
|
||
|
||
## Future Stories
|
||
|
||
- [Front#75](https://git.sabilin.com/EventHub/EventHubFront/issues/75) — предиктивные подсказки (ghosts `kind: 'hint'`).
|
||
- [Front#76](https://git.sabilin.com/EventHub/EventHubFront/issues/76) — голосовой центр: mark press-to-talk + палитра + ActionPlan.
|