From e87220afb5aa2eace62e78adc987b679977fff27 Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Mon, 22 Jun 2026 00:56:44 +0200 Subject: [PATCH] redesign SourcesBlock: collapsed by default, vertical cards, clickable Sources are now collapsed by default with a "Quellen (N)" header that expands/collapses on tap (matching web's SourcesBlock behavior). Each source renders as a full-width vertical card (not a small 140dp chip) with title, URL in accent color, and optional snippet. Tapping a source opens it in the browser via ACTION_VIEW intent. --- .../java/dev/kaizen/app/chat/StreamBlocks.kt | 81 +++++++++++++------ 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/dev/kaizen/app/chat/StreamBlocks.kt b/app/src/main/java/dev/kaizen/app/chat/StreamBlocks.kt index 2db8da4..ffc96b3 100644 --- a/app/src/main/java/dev/kaizen/app/chat/StreamBlocks.kt +++ b/app/src/main/java/dev/kaizen/app/chat/StreamBlocks.kt @@ -10,8 +10,6 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -50,6 +48,7 @@ import dev.kaizen.app.net.ToolEvent import dev.kaizen.app.R import dev.kaizen.app.ui.shape.KaizenShapes import dev.kaizen.app.ui.theme.LocalKaizenAccent +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @Composable @@ -170,36 +169,55 @@ fun ToolEventsBlock(tools: List, streaming: Boolean = false) { } } -@OptIn(ExperimentalLayoutApi::class) @Composable fun SourcesBlock(sources: List, query: String = "") { val cs = MaterialTheme.colorScheme val isDark = isSystemInDarkTheme() + val accent = LocalKaizenAccent.current + var expanded by remember { mutableStateOf(false) } + val context = LocalContext.current Column { - if (query.isNotBlank()) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(bottom = 6.dp)) { - Icon(Icons.Rounded.Language, null, tint = cs.onSurfaceVariant, modifier = Modifier.size(14.dp)) - Spacer(Modifier.width(6.dp)) - Text(query, color = cs.onSurfaceVariant, fontSize = 12.sp, fontWeight = FontWeight.Medium) - } - } - FlowRow( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), + Row( + Modifier + .clickable { expanded = !expanded } + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, ) { - sources.forEach { source -> - SourceChip(source, isDark) + Icon(Icons.Rounded.Language, null, tint = cs.onSurfaceVariant.copy(alpha = 0.6f), modifier = Modifier.size(13.dp)) + Spacer(Modifier.width(6.dp)) + Text( + stringResource(R.string.stream_sources) + " (${sources.size})", + color = cs.onSurfaceVariant.copy(alpha = 0.6f), + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + ) + Spacer(Modifier.width(4.dp)) + Icon( + if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, + null, tint = cs.onSurfaceVariant.copy(alpha = 0.4f), modifier = Modifier.size(12.dp), + ) + } + AnimatedVisibility(expanded, enter = expandVertically(), exit = shrinkVertically()) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.padding(top = 4.dp)) { + sources.forEach { source -> + SourceCard(source, isDark, accent) { + try { + val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(source.url)) + context.startActivity(intent) + } catch (_: Exception) { } + } + } } } } } @Composable -private fun SourceChip(source: SearchSource, isDark: Boolean) { +private fun SourceCard(source: SearchSource, isDark: Boolean, accent: dev.kaizen.app.ui.theme.themes.KaizenAccentScheme, onClick: () -> Unit) { val cs = MaterialTheme.colorScheme - val bg = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f) - val borderColor = if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.06f) + val bg = if (isDark) Color.White.copy(alpha = 0.05f) else Color.Black.copy(alpha = 0.03f) + val borderColor = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.05f) val domain = remember(source.url) { try { @@ -209,27 +227,38 @@ private fun SourceChip(source: SearchSource, isDark: Boolean) { Column( Modifier - .width(140.dp) - .clip(KaizenShapes.xs) + .fillMaxWidth() + .clip(KaizenShapes.sm) .background(bg) - .border(1.dp, borderColor, KaizenShapes.xs) - .padding(horizontal = 10.dp, vertical = 8.dp), + .border(1.dp, borderColor, KaizenShapes.sm) + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 10.dp), ) { Text( source.title.ifEmpty { domain }, color = cs.onBackground, - fontSize = 12.sp, + fontSize = 13.sp, fontWeight = FontWeight.Medium, maxLines = 2, overflow = TextOverflow.Ellipsis, ) Spacer(Modifier.height(2.dp)) Text( - domain, - color = cs.onSurfaceVariant.copy(alpha = 0.6f), - fontSize = 10.sp, + source.url, + color = accent.primary.copy(alpha = 0.7f), + fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, ) + if (source.snippet.isNotBlank()) { + Spacer(Modifier.height(3.dp)) + Text( + source.snippet, + color = cs.onSurfaceVariant.copy(alpha = 0.5f), + fontSize = 11.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } } }