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
15 KiB
ИИ клиента CalenTIQ (Фаза 3, волна 3)
Канон продуктового ИИ: умная сетка + опциональные предиктивные подсказки + голосовой центр выполнения без чата. Трекер: Spec#22, эпик Spec#19.
Реализация (Future): Front#75 подсказки, Front#76 голосовой центр. Этот документ — контракт; код подтягивается после него.
Позиция: «ИИ молчит и делает»
ИИ CalenTIQ — не собеседник и не интерфейс. Это слой, который предлагает действия, а человек их подтверждает и исполняет. Никакого свободного текста от ИИ, никакого диалога, никаких уточняющих вопросов, никакой «личности ассистента».
Три следствия:
- Результат любой ИИ-операции — план действий в UI (чипы, ghosts, навигация), а не текст.
- Неопределённость разрешается выбором из кандидатов, не разговором.
- Запись в 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-разбор выдают структурированный план, не текст. Намётка схемы:
{
"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). - 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; минимум один сценарий с конверсией лучше ручного пути.