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 2548a83..6a9908a 100644 --- a/app/src/main/java/dev/kaizen/app/chat/StreamBlocks.kt +++ b/app/src/main/java/dev/kaizen/app/chat/StreamBlocks.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -17,6 +18,8 @@ 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.rememberScrollState import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -169,6 +172,17 @@ fun ToolEventsBlock(tools: List, streaming: Boolean = false) { } } +private val SOURCE_COLORS = listOf( + Color(0xFF6366F1), // indigo + Color(0xFF8B5CF6), // violet + Color(0xFF06B6D4), // cyan + Color(0xFF10B981), // emerald + Color(0xFFF59E0B), // amber + Color(0xFFEF4444), // red + Color(0xFFEC4899), // pink + Color(0xFF3B82F6), // blue +) + @Composable fun SourcesBlock(sources: List, query: String = "") { val cs = MaterialTheme.colorScheme @@ -178,34 +192,77 @@ fun SourcesBlock(sources: List, query: String = "") { val context = LocalContext.current Column { - Row( - Modifier - .clickable { expanded = !expanded } - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(KaizenIcons.Globe, null, tint = cs.onSurfaceVariant.copy(alpha = 0.55f), modifier = Modifier.size(14.dp)) - Spacer(Modifier.width(6.dp)) - Text( - stringResource(R.string.stream_sources) + " (${sources.size})", - color = cs.onSurfaceVariant.copy(alpha = 0.55f), - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - ) - Spacer(Modifier.width(4.dp)) - Icon( - if (expanded) KaizenIcons.ChevronUp else KaizenIcons.ChevronDown, - null, tint = cs.onSurfaceVariant.copy(alpha = 0.35f), modifier = Modifier.size(13.dp), - ) + if (query.isNotBlank()) { + Row( + Modifier.padding(bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(KaizenIcons.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.45f), modifier = Modifier.size(13.dp)) + Spacer(Modifier.width(5.dp)) + Text( + query, + color = cs.onSurfaceVariant.copy(alpha = 0.55f), + fontSize = 12.sp, + fontWeight = FontWeight.W300, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } - 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) { + + Row( + Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + sources.forEachIndexed { idx, source -> + val color = SOURCE_COLORS[idx % SOURCE_COLORS.size] + val domain = remember(source.url) { + try { java.net.URI(source.url).host?.removePrefix("www.") ?: source.url } + catch (_: Exception) { source.url } + } + SourceChip( + index = idx + 1, + domain = domain, + color = color, + isDark = isDark, + onClick = { try { - val intent = android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(source.url)) - context.startActivity(intent) - } catch (_: Exception) { } + context.startActivity(android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(source.url))) + } catch (_: Exception) {} + }, + ) + } + } + + if (sources.any { it.title.isNotBlank() || it.snippet.isNotBlank() }) { + Spacer(Modifier.height(6.dp)) + Row( + Modifier + .clickable { expanded = !expanded } + .padding(vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + if (expanded) stringResource(R.string.stream_sources_hide) else stringResource(R.string.stream_sources_show), + color = cs.onSurfaceVariant.copy(alpha = 0.45f), + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + ) + Spacer(Modifier.width(3.dp)) + Icon( + if (expanded) KaizenIcons.ChevronUp else KaizenIcons.ChevronDown, + null, tint = cs.onSurfaceVariant.copy(alpha = 0.35f), modifier = Modifier.size(12.dp), + ) + } + AnimatedVisibility(expanded, enter = expandVertically(), exit = shrinkVertically()) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 6.dp)) { + sources.forEachIndexed { idx, source -> + val color = SOURCE_COLORS[idx % SOURCE_COLORS.size] + SourceDetailCard(source, idx + 1, color, isDark) { + try { + context.startActivity(android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(source.url))) + } catch (_: Exception) {} + } } } } @@ -214,58 +271,105 @@ fun SourcesBlock(sources: List, query: String = "") { } @Composable -private fun SourceCard(source: SearchSource, isDark: Boolean, accent: dev.kaizen.app.ui.theme.themes.KaizenAccentScheme, onClick: () -> Unit) { +private fun SourceChip(index: Int, domain: String, color: Color, isDark: Boolean, onClick: () -> Unit) { val cs = MaterialTheme.colorScheme - val bg = if (isDark) Color.White.copy(alpha = 0.05f) else Color.Black.copy(alpha = 0.03f) - val borderBrush = remember(isDark) { - Brush.verticalGradient( - listOf( - if (isDark) Color.White.copy(alpha = 0.10f) else Color.Black.copy(alpha = 0.06f), - if (isDark) Color.White.copy(alpha = 0.03f) else Color.Black.copy(alpha = 0.02f), - ) - ) - } + val chipBg = color.copy(alpha = if (isDark) 0.12f else 0.08f) + val chipBorder = color.copy(alpha = if (isDark) 0.25f else 0.18f) - val domain = remember(source.url) { - try { - java.net.URI(source.url).host?.removePrefix("www.") ?: source.url - } catch (_: Exception) { source.url } - } - - Column( + Row( Modifier - .fillMaxWidth() - .clip(KaizenShapes.md) - .background(bg) - .border(0.8.dp, borderBrush, KaizenShapes.md) + .clip(KaizenShapes.pill) + .background(chipBg) + .border(0.6.dp, chipBorder, KaizenShapes.pill) .clickable(onClick = onClick) - .padding(horizontal = 14.dp, vertical = 10.dp), + .padding(start = 3.dp, end = 10.dp, top = 5.dp, bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Text( - source.title.ifEmpty { domain }, - color = cs.onBackground, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - Spacer(Modifier.height(2.dp)) + Box( + Modifier + .size(18.dp) + .clip(KaizenShapes.circle) + .background(color.copy(alpha = if (isDark) 0.30f else 0.20f)), + contentAlignment = Alignment.Center, + ) { + Text( + "$index", + color = if (isDark) Color.White.copy(alpha = 0.90f) else color, + fontSize = 10.sp, + fontWeight = FontWeight.W600, + ) + } + Spacer(Modifier.width(6.dp)) Text( domain, - color = accent.primary.copy(alpha = 0.65f), - fontSize = 11.sp, + color = cs.onSurfaceVariant.copy(alpha = 0.70f), + fontSize = 12.sp, + fontWeight = FontWeight.W400, maxLines = 1, overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 140.dp), ) - if (source.snippet.isNotBlank()) { - Spacer(Modifier.height(3.dp)) + } +} + +@Composable +private fun SourceDetailCard(source: SearchSource, index: Int, color: Color, isDark: Boolean, onClick: () -> Unit) { + val cs = MaterialTheme.colorScheme + val bg = if (isDark) Color.White.copy(alpha = 0.04f) else Color.Black.copy(alpha = 0.02f) + val leftBar = color.copy(alpha = if (isDark) 0.50f else 0.40f) + + val domain = remember(source.url) { + try { java.net.URI(source.url).host?.removePrefix("www.") ?: source.url } + catch (_: Exception) { source.url } + } + + Row( + Modifier + .fillMaxWidth() + .clip(KaizenShapes.sm) + .background(bg) + .clickable(onClick = onClick), + ) { + Box(Modifier.width(3.dp).height(if (source.snippet.isNotBlank()) 60.dp else 40.dp).background(leftBar)) + Column(Modifier.padding(start = 10.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + "$index", + color = color, + fontSize = 11.sp, + fontWeight = FontWeight.W600, + ) + Spacer(Modifier.width(6.dp)) + Text( + source.title.ifEmpty { domain }, + color = cs.onBackground, + fontSize = 13.sp, + fontWeight = FontWeight.W400, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } Text( - source.snippet, - color = cs.onSurfaceVariant.copy(alpha = 0.50f), + domain, + color = color.copy(alpha = 0.70f), fontSize = 11.sp, - maxLines = 2, + fontWeight = FontWeight.W300, + maxLines = 1, overflow = TextOverflow.Ellipsis, ) + if (source.snippet.isNotBlank()) { + Spacer(Modifier.height(3.dp)) + Text( + source.snippet, + color = cs.onSurfaceVariant.copy(alpha = 0.50f), + fontSize = 11.sp, + fontWeight = FontWeight.W300, + lineHeight = 16.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } } } } diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 9e66cf6..33cae2c 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -33,6 +33,8 @@ Reasoning Sources + Show details + Hide details Processing … Analyzing %1$s … diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fd3fff..49bd655 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,6 +35,8 @@ Gedankengang Quellen + Details anzeigen + Details ausblenden Verarbeitet … Analysiert %1$s …