Добавляем свёрнутый блок результата тула в чат с раскрытием по тапу
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package pw.binom.viewmate.phone.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
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.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -34,6 +42,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -44,6 +53,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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.LlmModelChoice
|
||||
import pw.binom.viewmate.phone.agent.Session
|
||||
@@ -61,7 +71,7 @@ fun ChatScreen() {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
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 sessionMenu by remember { mutableStateOf(false) }
|
||||
var activeId by remember { mutableStateOf<Long?>(null) }
|
||||
@@ -74,7 +84,7 @@ fun ChatScreen() {
|
||||
val all = dao.listSessions()
|
||||
val target = activeId ?: all.firstOrNull()?.id
|
||||
val history = target?.let { dao.messagesForSession(it) } ?: emptyList()
|
||||
all to history.map { it.role to it.content }
|
||||
all to history.map(::uiMsgOf)
|
||||
}
|
||||
sessions = sess
|
||||
activeId = activeId ?: sess.firstOrNull()?.id
|
||||
@@ -102,7 +112,7 @@ fun ChatScreen() {
|
||||
delay(2_000)
|
||||
val id = activeId
|
||||
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
|
||||
}
|
||||
withContext(Dispatchers.IO) { app.chatDao.listSessions() }.let { s -> if (s != sessions) sessions = s }
|
||||
@@ -176,7 +186,7 @@ fun ChatScreen() {
|
||||
TextButton(onClick = {
|
||||
app.resetChatSession()
|
||||
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
|
||||
}
|
||||
}) {
|
||||
@@ -231,8 +241,11 @@ fun ChatScreen() {
|
||||
)
|
||||
} else {
|
||||
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
|
||||
items(messages) { (role, content) ->
|
||||
MessageBubble(role = role, text = content)
|
||||
items(messages) { msg ->
|
||||
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()
|
||||
if (text.isEmpty()) return@Button
|
||||
input = ""
|
||||
messages = messages + (ChatRole.USER to text)
|
||||
messages = messages + UiMsg(ChatRole.USER, 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):
|
||||
* жест «перетолкал» ленту на него — смена ровно на одну сессию; меньше — назад.
|
||||
|
||||
Reference in New Issue
Block a user