Доки: жесты и режимы общения с ассистентом (design) + TASK

This commit is contained in:
2026-08-23 01:08:38 +03:00
parent 28d0f9b68d
commit f36d04adc1
2 changed files with 107 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
# Задача: жесты и режимы общения с ассистентом на очках (design в docs/gestures-and-modes.md)
Прочитай `docs/gestures-and-modes.md` — там согласованная с пользователем матрица жестов и все решения. Реализуй в `app-glasses`. Механику тулсетов/ассистента (app-phone) НЕ трогать.
## Что сделать
### 1. Переразметка жестов по матрице (MainActivity.kt, collectTempleActions)
| Состояние | Click | DoubleClick | TripleClick | Свайпы/SlideContinuous |
|---|---|---|---|---|
| Кино (listening=false, assistantText пуст) | TOGGLE (как сейчас) | игнор | вызов ассистента (как сейчас) | ±30с (как сейчас) |
| Прослушивание (listening=true) | отправить (как сейчас) | отмена, остаёмся в диалоге (как сейчас) | игнор | игнор |
| Ответ виден (listening=false, assistantText непуст) | **новая фраза**: стереть assistantText, открыть микрофон, listening=true | **выход из режима**: закрыть оверлей, стереть assistantText, фильм на паузе (НЕ трогать play/pause — пауза уже стоит с момента входа) | игнор | **скролл ответа** (плавный SlideContinuous, запасной — дискретные свайпы) |
Важно: состояние «ответ виден» определяется `!listening && assistantText.isNotBlank()`.
### 2. Скролл ответа (плавный)
- SlideContinuous: накапливать `delta` (float) → смещать scrollState текста ответа. Направление: SlideForward = вниз по тексту (к концу), SlideBackward = вверх (к началу) — уточнить знак дельты на железе.
- Ограничить скролл границами текста (0..maxScroll).
- Запасной вариант: SlideForward/SlideBackward — скролл на страницу (например, ±200px или высоту видимой области), если SlideContinuous окажется шумным.
- Текст ответа в оверлее должен быть в scrollable-контейнере (Column + verticalScroll(rememberScrollState()) или LazyColumn — на твой выбор, но скролл обязателен).
### 3. Индикатор «думаю...»
- Состояние thinking: true с момента отправки фразы (Click при прослушивании / SttPhrase ушёл) до прихода ShowText.
- В оверлее: если thinking и assistantText пуст — показывать «думаю...» (вместо «слушаю...» после того как фраза отправлена).
- Как определить: пока listening=true — «слушаю...»/текст фразы; когда listening стал false, а ShowText ещё не пришёл — «думаю...».
### 4. Выход из режима
DoubleClick при «ответ виден»: закрыть оверлей (listening=false, sttText="", assistantText=""), НЕ трогать movieController (фильм остаётся на паузе — он уже на паузе с момента входа в режим).
## Ограничения
- Код только Kotlin
- НЕ трогать app-phone (ассистент, тулсеты, скиллы)
- НЕ трогать MovieController/playback-логику
- Тесты: юнит-тесты на чистую логику разметки жестов (новое состояние «ответ виден», переходы, скролл-дельта → смещение) — в духе GestureCommandsTest. Если логика в MainActivity (не чистая) — вынеси чистую часть (решение по жесту из состояния) в отдельный файл/функцию для тестируемости.
- Сборка: `./gradlew :app-glasses:assembleDebug` — зелёная; `./gradlew :app-glasses:testDebugUnitTest` — зелёный (старые + новые)
- НЕ коммитить
## Проверка
1. `./gradlew :app-glasses:testDebugUnitTest` — зелёные
2. `./gradlew :app-glasses:assembleDebug` — собирается
3. Опиши в ответе: какие файлы создал/изменил, как определил состояние «ответ виден», как работает скролл (SlideContinuous или запасной), что осталось проверить на железе.
+60
View File
@@ -0,0 +1,60 @@
# Жесты и режимы очков (design, согласовано 2026-08-22)
> Согласовано с пользователем. Реализация — opencode по TASK (см. ниже).
> Это документ проекта: сначала думаем, потом хуячим, потом тесты, потом ОБЯЗАТЕЛЬНО мануальные тесты на железе.
## Ограничения системы (факты)
- Жестов всего шесть: Click, DoubleClick, TripleClick, SlideForward, SlideBackward, LongClick (+ SlideContinuous с delta).
- **LongClick НЕ работает** — перехвачен системным ассистентом RayNeo. Не рассчитывать, не использовать.
- **SlideContinuous существует** (SDK: `TempleAction.SlideContinuous(delta: Float, ...)` — подтверждено javap) — плавный скролл возможен, delta = дельту движения пальца. Частоту событий и амплитуду дельт проверить на железе.
- Экран очков — широкий 1280×480 (640×480 на глаз). Текста влезает мало → скролл ответа обязателен для длинных ответов.
- Оверлей общения затемняет кино (72% чёрного) → режим общения ЯВНЫЙ: пользователь видит, что он в чате, а не в кино. Это позволяет переназначать свайпы в режиме общения без конфликта.
- Нет клавиатуры — только жесты и голос. Выход из режима — только жестом.
- LLM думает 5-15с — нужен индикатор «думаю...», иначе экран замирает.
## Общий паттерн жестов (единая семантика через все режимы)
| Жест | Смысл везде |
|---|---|
| **TripleClick** | вызов локального ассистента (переход в режим общения) |
| **DoubleClick** | отмена / назад |
| **Click** | основное действие текущего контекста |
## Матрица жестов по состояниям (ЗАКРЫТА)
| Состояние | Click | DoubleClick | TripleClick |
|---|---|---|---|
| **Кино** | плей/пауза | игнор (назад некуда) | вызов ассистента (пауза + «слушаю...») |
| **Прослушивание** | отправить фразу | отмена, **остаёмся в диалоге** | игнор |
| **Ответ виден** | новая фраза (микрофон открывается) | **выход из режима** | игнор (уже в режиме) |
Ключевой принцип: **клик = «главное действие здесь»**. В кино — плей/пауза, в диалоге — сказать новую фразу, при прослушивании — отправить. Один жест, контекст решает. Двойной — всегда «не хочу/назад». Тройной — всегда «хочу говорить с ассистентом».
Конфликтов нет: подрежимы различимы по состоянию (listening = микрофон открыт / assistantText непуст = ответ виден).
## Решения по деталям
1. **Отмена диктовки (DoubleClick в прослушивании)** — просто отмена, остаёмся в диалоге (назад к «ответу виден», ничего не теряется).
2. **Фильм после выхода (DoubleClick при ответе)** — ВСЕГДА на паузе, независимо от того, был ли он на паузе до входа в режим ассистента.
3. **Скролл ответа** — плавный: SlideContinuous, отслеживать движение пальца (delta), а не факт свайпа с шагом N. Свайпы SlideForward/SlideBackward в режиме общения — тоже скролл (запасной вариант, если SlideContinuous окажется нестабильным на железе).
4. **Новая фраза в режиме «ответ виден»** — Click: микрофон открывается, «слушаю...». Старый ответ с экрана — стереть (история живёт в SQLite на телефоне, не теряется).
5. **Индикатор «думаю...»** — после отправки фразы (между прослушиванием и ответом) показывать «думаю...» вместо замирания на тексте фразы.
## Что делать (TASK для opencode)
В `app-glasses` (MainActivity.kt + оверлей):
1. **Переразметка жестов по матрице**:
- Кино: как сейчас (Click=TOGGLE, TripleClick=вызов, свайпы=±30с, DoubleClick=игнор)
- Прослушивание: как сейчас (Click=отправить, DoubleClick=отмена, TripleClick=игнор)
- Ответ виден (listening=false, assistantText непуст): **Click → новая фраза** (открыть микрофон, стереть assistantText со старта), **DoubleClick → выход из режима** (закрыть оверлей, фильм на паузе, assistantText стереть), **TripleClick → игнор**, свайпы/SlideContinuous → скролл
2. **Скролл ответа**: плавный через SlideContinuous (delta), scrollState для текста ответа; запасной вариант — дискретные свайпы. Ограничить скролл границами текста.
3. **Индикатор «думаю...»**: состояние между отправкой фразы и получением ShowText; показывать в оверлее.
4. **Выход**: DoubleClick при «ответ виден» — закрыть оверлей, фильм остаётся на паузе (не трогать play/pause при выходе, только при входе ставим паузу).
Тесты: юнит-тесты на чистую логику разметки жестов (переходы состояний, скролл-дельта → смещение), в духе GestureCommandsTest. Мануальные тесты — ОБЯЗАТЕЛЬНО на железе (142: очки 7687f645 + телефон AXGL024B05001337): полный цикл кино → тройной тап → фраза → «думаю...» → ответ → скролл → выход → фильм на паузе.
## Смежные договорённости (архитектура агента)
Полная архитектура скиллов/тулсетов — в `TASK-agent-tools.md`. Ключевое для очков: ассистент — локальный, на телефоне; очки — тонкий клиент (микрофон → STT → телефон → LLM → ShowText на очки). Режим общения — поверх любого режима (кино/чат), активируется тройным тапом из любого места.