Очки: история диалога в свайпе + распознавание в LLM только по клику (касяки 1-3)
This commit is contained in:
@@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import pw.binom.viewmate.glasses.ChatEntry
|
||||
import pw.binom.viewmate.glasses.GlassesApp
|
||||
import pw.binom.viewmate.glasses.gestures.AssistantState
|
||||
import pw.binom.viewmate.glasses.gestures.overlayHeader
|
||||
@@ -35,7 +36,8 @@ import pw.binom.viewmate.glasses.gestures.scrollOffset
|
||||
* Оверлей режима общения с ассистентом: затемняет 72% чёрным ЛЮБУЮ картинку
|
||||
* (кино или экран подключения) — режим общения явный (docs/gestures-and-modes.md).
|
||||
* По глазам ([Binocular]), как все экраны. Состояния: «слушаю»/«думаю...» — заголовок,
|
||||
* «ответ виден» — прокручиваемый ответ (дельта скролла от жестов — [GlassesApp.answerScrollDelta]).
|
||||
* «ответ виден» — прокручиваемая история диалога (дельта скролла от жестов —
|
||||
* [GlassesApp.answerScrollDelta]).
|
||||
*/
|
||||
@Composable
|
||||
fun AssistantOverlay() {
|
||||
@@ -44,14 +46,15 @@ fun AssistantOverlay() {
|
||||
val thinking by app.thinking.collectAsState()
|
||||
val sttText by app.sttText.collectAsState()
|
||||
val assistantText by app.assistantText.collectAsState()
|
||||
val chatHistory by app.chatHistory.collectAsState()
|
||||
val state = AssistantState(listening, thinking, sttText, assistantText)
|
||||
if (!overlayVisible(state)) return
|
||||
overlayBox(app, state)
|
||||
overlayBox(app, state, chatHistory)
|
||||
}
|
||||
|
||||
/** Поглазник: тот же scale/offset-контейнер, что у экранов, но с затемнением вместо чёрного. */
|
||||
@Composable
|
||||
private fun overlayBox(app: GlassesApp, state: AssistantState) {
|
||||
private fun overlayBox(app: GlassesApp, state: AssistantState, chatHistory: List<ChatEntry>) {
|
||||
Binocular(scale = false) {
|
||||
val scale by app.scale.collectAsState()
|
||||
val screenWidthPx = BinocularScreenSize.current.width
|
||||
@@ -66,42 +69,45 @@ private fun overlayBox(app: GlassesApp, state: AssistantState) {
|
||||
.offset(x = scaleOffsetX(side, screenWidthPx, scale).px)
|
||||
.background(Color.Black.copy(alpha = 0.72f)),
|
||||
) {
|
||||
AssistantOverlayContent(state)
|
||||
AssistantOverlayContent(state, chatHistory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AssistantOverlayContent(state: AssistantState) {
|
||||
private fun AssistantOverlayContent(state: AssistantState, chatHistory: List<ChatEntry>) {
|
||||
val app = GlassesApp.instance
|
||||
val hasAnswer = state.assistantText.isNotBlank()
|
||||
val hasHistory = chatHistory.isNotEmpty()
|
||||
val fallbackAnswer = chatHistory.isEmpty() && state.assistantText.isNotBlank()
|
||||
val header = overlayHeader(state)
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
// Новый ответ приходит с обнулением answerScrollDelta (ShowText) — к началу текста.
|
||||
LaunchedEffect(state.assistantText) {
|
||||
scrollState.scrollTo(0)
|
||||
// Новый ответ (ShowText) или пополнившаяся история — к последнему сообщению.
|
||||
LaunchedEffect(chatHistory.size, state.assistantText) {
|
||||
if (hasHistory || state.assistantText.isNotBlank()) {
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Накопленная дельта скролла от жестов: каждый глаз — свой [LaunchedEffect],
|
||||
// аккумулятор (answerScrollDelta) общий в GlassesApp. Цель — чистый [scrollOffset].
|
||||
LaunchedEffect(Unit) {
|
||||
app.answerScrollDelta.collect { pending ->
|
||||
if (pending == 0f) return@collect
|
||||
val target = scrollOffset(scrollState.value, pending, scrollState.maxValue)
|
||||
app.answerScrollDelta.value = 0f
|
||||
if (target != scrollState.value) {
|
||||
scrollState.animateScrollTo(target, tween(150))
|
||||
}
|
||||
// Накопленная дельта скролла от жестов: каждый глаз — свой [LaunchedEffect],
|
||||
// аккумулятор (answerScrollDelta) общий в GlassesApp. Цель — чистый [scrollOffset].
|
||||
LaunchedEffect(Unit) {
|
||||
app.answerScrollDelta.collect { pending ->
|
||||
if (pending == 0f) return@collect
|
||||
val target = scrollOffset(scrollState.value, pending, scrollState.maxValue)
|
||||
app.answerScrollDelta.value = 0f
|
||||
if (target != scrollState.value) {
|
||||
scrollState.animateScrollTo(target, tween(150))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = if (hasAnswer) Arrangement.Top else Arrangement.Center,
|
||||
verticalArrangement = if (hasHistory || fallbackAnswer) Arrangement.Top else Arrangement.Center,
|
||||
) {
|
||||
if (!header.isNullOrBlank()) {
|
||||
Text(
|
||||
@@ -111,7 +117,28 @@ private fun AssistantOverlayContent(state: AssistantState) {
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
if (hasAnswer) {
|
||||
if (hasHistory) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
chatHistory.forEach { entry ->
|
||||
Text(
|
||||
text = if (entry.role == "user") "я: ${entry.text}" else "ассистент: ${entry.text}",
|
||||
color = if (entry.role == "user") Color.White else Color(0xFFB0BEC5),
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (fallbackAnswer) {
|
||||
// Старый клиент телефона не шлёт историю: показываем одиночный ответ.
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -120,12 +147,15 @@ private fun AssistantOverlayContent(state: AssistantState) {
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
Text(
|
||||
text = state.assistantText,
|
||||
text = "ассистент: ${state.assistantText}",
|
||||
color = Color(0xFFB0BEC5),
|
||||
textAlign = TextAlign.Center,
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user