ui(glasses): отмена первой записи не закрывает оверлей — возврат в «слушаю», диалог жив даже при пустой истории; выход из режима — только явный жест ExitMode

This commit is contained in:
2026-08-27 18:46:29 +03:00
parent da8182443c
commit 412e093174
5 changed files with 78 additions and 16 deletions
@@ -55,7 +55,11 @@ class GlassesApp : Application() {
/** Вертикаль экрана очков (0..1): 1.0 — у верха (как всегда было), 0.0 — нижняя кромка у низа. */
val vertical = MutableStateFlow(1f)
/** Идёт распознавание речи (TripleClick стартовал, Click/DoubleClick/тишина завершили). */
/**
* Идёт распознавание речи (TripleClick стартовал; завершает фразу Click,
* отмена записи DoubleClick НЕ завершает сессию — сразу возвращает в
* «слушаю» снова; 30с тишины/выход из режима закрывают сессию).
*/
val listening = MutableStateFlow(false)
/** Накопленный текст распознавания (для оверлея); обновляется на SttPhrase. */
@@ -368,12 +368,12 @@ class HostConnection(
is SttCancel -> {
when (msg.reason) {
SttCancel.REASON_CANCEL -> {
// Явная отмена диктовки двойным тапом: закрыть микрофон, сбросить
// listening/thinking, НО остаться в режиме (показать последний ответ),
// микрофон заново НЕ открывать, из режима НЕ выходить.
log("stt", "отмена диктовки (двойной тап) — микрофон закрыт, остаёмся в режиме")
GlassesApp.instance.listening.value = false
GlassesApp.instance.thinking.value = false
// Явная отмена диктовки двойным тапом: очки САМИ уже вернулись
// в «слушаю» по жесту (TZ-ui-four п.1: отмена не закрывает чат,
// оверлей остаётся, микрофон готов к следующей записи). Это всего
// подтверждение, что телефон отменил STT-сессию (stt.reset()) —
// состояния (listening/thinking/оверлей) НЕ трогаем.
log("stt", "отмена диктовки (двойной тап) — остался в «слушаю», оверлей на месте")
}
SttCancel.REASON_TIMEOUT -> {
// Авто-отмена по тишине 30с: пользователь молчит/ушёл — ВЫХОДИМ из
@@ -312,12 +312,20 @@ class MainActivity : BaseEventActivity() {
log("stt", "возврат в «слушаю» (SttCancel)")
}
/** DoubleClick («слушаю»): отмена распознавания — остаёмся в разговоре (без выхода). */
/**
* DoubleClick («слушаю»): отмена ТЕКУЩЕЙ записи (TZ-ui-four п.1): возвращаемся
* в «слушаю» — микрофон сразу готов к следующей записи, оверлей ОСТАЁТСЯ на
* экране (даже если истории диалога ещё нет — отмена не закрывает чат).
* Закрытие ассистента — двойной в «думаю»/«ответ виден» ([exitAssistantMode]).
*/
private fun cancelDictation() {
log("gesture", "DoubleClick → отмена распознавания")
log("gesture", "DoubleClick → отмена записи: возврат в «слушаю» (оверлей остаётся)")
val app = GlassesApp.instance
sttMic.stop()
GlassesApp.instance.listening.value = false
GlassesApp.instance.thinking.value = false
app.sttText.value = ""
app.thinking.value = false
app.listening.value = true
sttMic.start(filesDir) { app.listening.value }
lifecycleScope.launch {
GlassesApp.instance.hostConnection.sendToHost(StopStt(cancel = true))
}
@@ -64,7 +64,12 @@ sealed interface GestureDecision {
/** Клик в «думаю»/«ответ виден»: стереть ответ, открыть микрофон (новая фраза). */
data object NewPhrase : GestureDecision
/** Двойной в «слушаю»: отмена распознавания, остаёмся в разговоре (StopStt(cancel=true)). */
/**
* Двойной в «слушаю»: отмена ТЕКУЩЕЙ записи — возврат в «слушаю»
* ([afterCancelDictation]): микрофон снова готов к следующей записи,
* оверлей остаётся на экране даже при пустой истории диалога.
* Сторона очков: StopStt(cancel=true) на телефон.
*/
data object CancelDictation : GestureDecision
/** Двойной в «думаю»/«ответ виден»: выход из режима — закрыть оверлей, фильм на паузе. */
@@ -125,6 +130,18 @@ fun gestureDecision(
}
}
/**
* Состояние после отмены записи ([GestureDecision.CancelDictation], двойной в «слушаю»):
* возврат в «слушаю» — микрофон снова открыт и готов к следующей записи, отменённый
* текст ([AssistantState.sttText]) сбросен. Диалог НЕ прерывается: оверлей остаётся
* на экране даже когда истории диалога ещё нет (empty [AssistantState] → [overlayVisible]=true,
* т.к. listening=true). Закрытие ассистента — отдельное явное действие:
* двойной в «думаю»/«ответ виден» ([GestureDecision.ExitMode]).
* Чистая функция — unit-тесты без Android.
*/
fun afterCancelDictation(state: AssistantState): AssistantState =
state.copy(listening = true, thinking = false, sttText = "")
/** Оверлей общения виден: слушаю / думаю / ответ на экране. В кино — нет. */
fun overlayVisible(state: AssistantState): Boolean =
state.listening || state.thinking || state.assistantText.isNotBlank()