Добавляем свёрнутый блок результата тула в чат с раскрытием по тапу
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package pw.binom.viewmate.phone.ui
|
package pw.binom.viewmate.phone.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -12,7 +13,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
@@ -20,7 +23,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Build
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -34,6 +42,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -44,6 +53,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import pw.binom.viewmate.phone.PhoneApp
|
import pw.binom.viewmate.phone.PhoneApp
|
||||||
|
import pw.binom.viewmate.phone.agent.ChatMessage
|
||||||
import pw.binom.viewmate.phone.agent.ChatRole
|
import pw.binom.viewmate.phone.agent.ChatRole
|
||||||
import pw.binom.viewmate.phone.agent.LlmModelChoice
|
import pw.binom.viewmate.phone.agent.LlmModelChoice
|
||||||
import pw.binom.viewmate.phone.agent.Session
|
import pw.binom.viewmate.phone.agent.Session
|
||||||
@@ -61,7 +71,7 @@ fun ChatScreen() {
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var sessions by remember { mutableStateOf<List<Session>>(emptyList()) }
|
var sessions by remember { mutableStateOf<List<Session>>(emptyList()) }
|
||||||
var messages by remember { mutableStateOf<List<Pair<ChatRole, String>>>(emptyList()) }
|
var messages by remember { mutableStateOf<List<UiMsg>>(emptyList()) }
|
||||||
var input by remember { mutableStateOf("") }
|
var input by remember { mutableStateOf("") }
|
||||||
var sessionMenu by remember { mutableStateOf(false) }
|
var sessionMenu by remember { mutableStateOf(false) }
|
||||||
var activeId by remember { mutableStateOf<Long?>(null) }
|
var activeId by remember { mutableStateOf<Long?>(null) }
|
||||||
@@ -74,7 +84,7 @@ fun ChatScreen() {
|
|||||||
val all = dao.listSessions()
|
val all = dao.listSessions()
|
||||||
val target = activeId ?: all.firstOrNull()?.id
|
val target = activeId ?: all.firstOrNull()?.id
|
||||||
val history = target?.let { dao.messagesForSession(it) } ?: emptyList()
|
val history = target?.let { dao.messagesForSession(it) } ?: emptyList()
|
||||||
all to history.map { it.role to it.content }
|
all to history.map(::uiMsgOf)
|
||||||
}
|
}
|
||||||
sessions = sess
|
sessions = sess
|
||||||
activeId = activeId ?: sess.firstOrNull()?.id
|
activeId = activeId ?: sess.firstOrNull()?.id
|
||||||
@@ -102,7 +112,7 @@ fun ChatScreen() {
|
|||||||
delay(2_000)
|
delay(2_000)
|
||||||
val id = activeId
|
val id = activeId
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
val fresh = withContext(Dispatchers.IO) { app.chatDao.messagesForSession(id).map { it.role to it.content } }
|
val fresh = withContext(Dispatchers.IO) { app.chatDao.messagesForSession(id).map(::uiMsgOf) }
|
||||||
if (fresh != messages) messages = fresh
|
if (fresh != messages) messages = fresh
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.IO) { app.chatDao.listSessions() }.let { s -> if (s != sessions) sessions = s }
|
withContext(Dispatchers.IO) { app.chatDao.listSessions() }.let { s -> if (s != sessions) sessions = s }
|
||||||
@@ -176,7 +186,7 @@ fun ChatScreen() {
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
app.resetChatSession()
|
app.resetChatSession()
|
||||||
activeId?.let { id ->
|
activeId?.let { id ->
|
||||||
val history = app.chatDao.messagesForSession(id).map { it.role to it.content }
|
val history = app.chatDao.messagesForSession(id).map(::uiMsgOf)
|
||||||
messages = history
|
messages = history
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
@@ -231,8 +241,11 @@ fun ChatScreen() {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
|
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
|
||||||
items(messages) { (role, content) ->
|
items(messages) { msg ->
|
||||||
MessageBubble(role = role, text = content)
|
when (msg.role) {
|
||||||
|
ChatRole.TOOL -> ToolBubble(msg)
|
||||||
|
else -> MessageBubble(role = msg.role, text = msg.content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,7 +268,7 @@ fun ChatScreen() {
|
|||||||
val text = input.trim()
|
val text = input.trim()
|
||||||
if (text.isEmpty()) return@Button
|
if (text.isEmpty()) return@Button
|
||||||
input = ""
|
input = ""
|
||||||
messages = messages + (ChatRole.USER to text)
|
messages = messages + UiMsg(ChatRole.USER, text)
|
||||||
app.sendToAssistant(text)
|
app.sendToAssistant(text)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -290,6 +303,71 @@ private fun MessageBubble(role: ChatRole, text: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Элемент ленты: tool-сообщения дополнительно несут имя тула (в БД toolCallId = имя тула). */
|
||||||
|
private data class UiMsg(
|
||||||
|
val role: ChatRole,
|
||||||
|
val content: String,
|
||||||
|
val tool: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun uiMsgOf(m: ChatMessage) = UiMsg(
|
||||||
|
role = m.role,
|
||||||
|
content = m.content,
|
||||||
|
tool = if (m.role == ChatRole.TOOL) m.toolCallId else null,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Системный блок результата тула (ChatRole.TOOL): свёрнут по умолчанию —
|
||||||
|
* иконка + «тул · имя», тап раскрывает текст результата. Система видна
|
||||||
|
* по требованию, но не шумит в основной переписке.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ToolBubble(msg: UiMsg) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = 340.dp)
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.background(PhoneCard)
|
||||||
|
.clickable { expanded = !expanded }
|
||||||
|
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Build,
|
||||||
|
contentDescription = "тул",
|
||||||
|
tint = PhoneMuted,
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(6.dp))
|
||||||
|
Text(
|
||||||
|
text = "тул · ${msg.tool ?: "результат"}",
|
||||||
|
color = PhoneMuted,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Icon(
|
||||||
|
imageVector = if (expanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown,
|
||||||
|
contentDescription = if (expanded) "свернуть" else "развернуть",
|
||||||
|
tint = PhoneMuted,
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (expanded) {
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = msg.content,
|
||||||
|
color = PhoneTextDim,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Фиксированный порог ступенчатого свайпа переключения сессий (TZ-ui-four п.3):
|
* Фиксированный порог ступенчатого свайпа переключения сессий (TZ-ui-four п.3):
|
||||||
* жест «перетолкал» ленту на него — смена ровно на одну сессию; меньше — назад.
|
* жест «перетолкал» ленту на него — смена ровно на одну сессию; меньше — назад.
|
||||||
|
|||||||
Reference in New Issue
Block a user