git.iko.soy/opencode-android
on e9b2cc3 help sign in
82 lines 3.2 kB history raw
1package soy.iko.opencode.ui.chat
3import androidx.compose.foundation.background
4import androidx.compose.foundation.layout.Arrangement
5import androidx.compose.foundation.layout.Column
6import androidx.compose.foundation.layout.Row
7import androidx.compose.foundation.layout.fillMaxWidth
8import androidx.compose.foundation.layout.padding
9import androidx.compose.foundation.layout.widthIn
10import androidx.compose.foundation.shape.RoundedCornerShape
11import androidx.compose.material3.MaterialTheme
12import androidx.compose.material3.Text
13import androidx.compose.runtime.Composable
14import androidx.compose.ui.Alignment
15import androidx.compose.ui.Modifier
16import androidx.compose.ui.draw.clip
17import androidx.compose.ui.unit.dp
18import soy.iko.opencode.data.model.AssistantMessage
19import soy.iko.opencode.data.model.MessageWithParts
20import soy.iko.opencode.data.model.UserMessage
22/** A single message: user prompts right-aligned in a bubble, assistant output full-width. */
23@Composable
24fun MessageBubble(message: MessageWithParts, isRunning: Boolean = false, modifier: Modifier = Modifier) {
25 when (message.info) {
26 is UserMessage -> UserBubble(message, modifier)
27 else -> AssistantBlock(message, isRunning, modifier)
28 }
31@Composable
32private fun UserBubble(message: MessageWithParts, modifier: Modifier) {
33 Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
34 Column(
35 modifier = Modifier
36 .widthIn(max = 320.dp)
37 .clip(RoundedCornerShape(16.dp))
38 .background(MaterialTheme.colorScheme.primaryContainer)
39 .padding(12.dp),
40 verticalArrangement = Arrangement.spacedBy(6.dp),
41 ) {
42 for (part in message.parts) PartView(part)
43 }
44 }
47@Composable
48private fun AssistantBlock(message: MessageWithParts, isRunning: Boolean, modifier: Modifier) {
49 Column(
50 modifier = modifier.fillMaxWidth(),
51 verticalArrangement = Arrangement.spacedBy(8.dp),
52 ) {
53 val info = message.info
54 if (info is AssistantMessage && info.modelID != null) {
55 Text(
56 info.modelID,
57 style = MaterialTheme.typography.labelSmall,
58 color = MaterialTheme.colorScheme.onSurfaceVariant,
59 )
60 }
61 for (part in message.parts) {
62 PartView(part, isRunning = isRunning, modifier = Modifier.fillMaxWidth())
63 }
64 if (info is AssistantMessage) {
65 val cost = info.cost
66 val tokens = info.tokens
67 if (info.isComplete && (cost != null || tokens != null)) {
68 val parts = buildList {
69 tokens?.let { add("${it.input}→${it.output} tok") }
70 cost?.takeIf { it > 0 }?.let { add("$%.4f".format(it)) }
71 }
72 if (parts.isNotEmpty()) {
73 Text(
74 parts.joinToString(" • "),
75 style = MaterialTheme.typography.labelSmall,
76 color = MaterialTheme.colorScheme.onSurfaceVariant,
77 )
78 }
79 }
80 }
81 }