# ИИ клиента 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.