fix: rewrite ToolEventsBlock to match web frontend logic

Parse tool events into thinking/analyzing/inProgress/completed states
instead of showing all as generic "Tool" rows. Handles type "thinking"
(shows "Verarbeitet…"), "analyzing" (shows filename), "start"/"end"/
"error" with proper icons and elapsed time. Empty/thinking-only events
no longer show a confusing wrench icon.
This commit is contained in:
Bruno Deanoz 2026-06-21 18:40:19 +02:00
parent cad445f87c
commit 5c54b9b9e3
3 changed files with 61 additions and 39 deletions

View file

@ -26,7 +26,9 @@ import androidx.compose.material.icons.rounded.ExpandLess
import androidx.compose.material.icons.rounded.ExpandMore import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material.icons.rounded.Language import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Psychology import androidx.compose.material.icons.rounded.Psychology
import androidx.compose.material.icons.rounded.AutoAwesome
import androidx.compose.material.icons.rounded.Build import androidx.compose.material.icons.rounded.Build
import androidx.compose.material.icons.rounded.Description
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -45,8 +47,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import dev.kaizen.app.net.SearchSource import dev.kaizen.app.net.SearchSource
import dev.kaizen.app.net.ToolEvent import dev.kaizen.app.net.ToolEvent
import dev.kaizen.app.R
import dev.kaizen.app.ui.shape.KaizenShapes import dev.kaizen.app.ui.shape.KaizenShapes
import dev.kaizen.app.ui.theme.LocalKaizenAccent import dev.kaizen.app.ui.theme.LocalKaizenAccent
import androidx.compose.ui.res.stringResource
@Composable @Composable
fun ReasoningBlock(reasoning: String, streaming: Boolean = false) { fun ReasoningBlock(reasoning: String, streaming: Boolean = false) {
@ -103,51 +107,65 @@ fun ToolEventsBlock(tools: List<ToolEvent>, streaming: Boolean = false) {
val cs = MaterialTheme.colorScheme val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme() val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current val accent = LocalKaizenAccent.current
val bg = if (isDark) Color.White.copy(alpha = 0.04f) else Color.Black.copy(alpha = 0.02f)
val borderColor = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f)
Column( val completed = mutableListOf<ToolEvent>()
Modifier val inProgress = mutableListOf<String>()
.fillMaxWidth() val seen = mutableSetOf<String>()
.clip(KaizenShapes.sm) var thinking = false
.background(bg) var analyzing: String? = null
.border(1.dp, borderColor, KaizenShapes.sm)
.padding(horizontal = 12.dp, vertical = 8.dp), for (ev in tools) {
verticalArrangement = Arrangement.spacedBy(4.dp), when (ev.type) {
) { "thinking" -> { thinking = true; analyzing = null }
tools.forEach { tool -> "analyzing" -> { analyzing = ev.name; thinking = false }
"start" -> {
analyzing = null; thinking = false
if (ev.name !in seen) { seen.add(ev.name); inProgress.add(ev.name) }
}
"end", "error" -> {
inProgress.remove(ev.name)
completed.add(ev)
}
}
}
val hasContent = analyzing != null || thinking || completed.isNotEmpty() || inProgress.isNotEmpty()
if (!hasContent) return
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
if (analyzing != null) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
val icon = when (tool.type) { Icon(Icons.Rounded.Description, null, tint = accent.primary.copy(alpha = 0.6f), modifier = Modifier.size(14.dp))
"end" -> Icons.Rounded.Check Spacer(Modifier.width(6.dp))
"error" -> Icons.Rounded.Error Text(stringResource(R.string.stream_tool_analyzing, analyzing!!), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp)
else -> Icons.Rounded.Build
} }
val tint = when (tool.type) {
"end" -> Color(0xFF10B981)
"error" -> Color(0xFFEF4444)
else -> accent.primary
} }
if (thinking && analyzing == null && completed.isEmpty() && inProgress.isEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Rounded.AutoAwesome, null, tint = accent.primary.copy(alpha = 0.6f), modifier = Modifier.size(14.dp))
Spacer(Modifier.width(6.dp))
Text(stringResource(R.string.stream_tool_thinking), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp)
}
}
completed.forEach { tool ->
Row(verticalAlignment = Alignment.CenterVertically) {
val icon = if (tool.type == "error") Icons.Rounded.Error else Icons.Rounded.Check
val tint = if (tool.type == "error") Color(0xFFEF4444) else Color(0xFF10B981)
Icon(icon, null, tint = tint, modifier = Modifier.size(14.dp)) Icon(icon, null, tint = tint, modifier = Modifier.size(14.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(6.dp))
Text( Text(tool.name, color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp)
tool.name.ifEmpty { "Tool" },
color = cs.onBackground,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
)
if (tool.elapsed != null) { if (tool.elapsed != null) {
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(4.dp))
Text( Text("${tool.elapsed}ms", color = cs.onSurfaceVariant.copy(alpha = 0.35f), fontSize = 11.sp)
"${tool.elapsed}ms",
color = cs.onSurfaceVariant.copy(alpha = 0.5f),
fontSize = 11.sp,
)
} }
if (tool.error != null) {
Spacer(Modifier.width(6.dp))
Text(tool.error, color = Color(0xFFEF4444), fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
} }
} }
inProgress.forEach { name ->
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Rounded.Build, null, tint = accent.primary.copy(alpha = 0.6f), modifier = Modifier.size(14.dp))
Spacer(Modifier.width(6.dp))
Text(name, color = cs.onSurfaceVariant.copy(alpha = 0.7f), fontSize = 12.sp, fontWeight = FontWeight.Medium)
}
} }
} }
} }

View file

@ -22,6 +22,8 @@
<!-- Stream blocks --> <!-- Stream blocks -->
<string name="stream_reasoning">Reasoning</string> <string name="stream_reasoning">Reasoning</string>
<string name="stream_sources">Sources</string> <string name="stream_sources">Sources</string>
<string name="stream_tool_thinking">Processing …</string>
<string name="stream_tool_analyzing">Analyzing %1$s …</string>
<!-- Chat errors --> <!-- Chat errors -->
<string name="error_session_expired">Session expired. Please sign in again.</string> <string name="error_session_expired">Session expired. Please sign in again.</string>

View file

@ -24,6 +24,8 @@
<!-- Stream blocks --> <!-- Stream blocks -->
<string name="stream_reasoning">Gedankengang</string> <string name="stream_reasoning">Gedankengang</string>
<string name="stream_sources">Quellen</string> <string name="stream_sources">Quellen</string>
<string name="stream_tool_thinking">Verarbeitet …</string>
<string name="stream_tool_analyzing">Analysiert %1$s …</string>
<!-- Chat errors --> <!-- Chat errors -->
<string name="error_session_expired">Sitzung abgelaufen. Bitte erneut anmelden.</string> <string name="error_session_expired">Sitzung abgelaufen. Bitte erneut anmelden.</string>