From f3d81540527583251eed84110ffc0252422e3523 Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Sun, 21 Jun 2026 23:31:03 +0200 Subject: [PATCH] sidebar redesign + markdown typography overhaul MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sidebar: increase glass opacity (0.92/0.88), flat conversation rows instead of pill shapes, 3-dot context menu (rename/delete/pin/lock), smaller profile avatar (30dp), logout integrated into user card. Add KaizenApi.patchConversation + deleteConversation for CRUD. Markdown: increase line-height from 23sp to 26sp (×1.625 ratio), double paragraph spacing (6dp→12dp), generous heading margins (18dp top, 8dp bottom), wider list item spacing — matches Gemini- level readability. --- .../java/dev/kaizen/app/chat/ChatScreen.kt | 27 +- .../main/java/dev/kaizen/app/chat/Markdown.kt | 34 +- .../main/java/dev/kaizen/app/chat/Sidebar.kt | 340 ++++++++++++++---- .../main/java/dev/kaizen/app/net/KaizenApi.kt | 41 +++ .../dev/kaizen/app/ui/effect/GlassSurface.kt | 2 +- app/src/main/res/values-en/strings.xml | 12 + app/src/main/res/values/strings.xml | 12 + 7 files changed, 371 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index 076aad8..f86f4e7 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -409,7 +409,32 @@ fun ChatScreen( newChat() scope.launch { drawerState.close() } session.logout() - } + }, + onAction = { action -> + val cfg = session.config ?: return@KaizenSidebar + haptics.tick() + scope.launch { + when (action) { + is SidebarAction.Rename -> { + KaizenApi.patchConversation(cfg.baseUrl, cfg.token, action.id, mapOf("title" to action.newTitle)) + refreshConversations() + } + is SidebarAction.Delete -> { + KaizenApi.deleteConversation(cfg.baseUrl, cfg.token, action.id) + if (conversationId == action.id) newChat() + refreshConversations() + } + is SidebarAction.TogglePin -> { + KaizenApi.patchConversation(cfg.baseUrl, cfg.token, action.id, mapOf("pinned" to action.pinned)) + refreshConversations() + } + is SidebarAction.ToggleLock -> { + KaizenApi.patchConversation(cfg.baseUrl, cfg.token, action.id, mapOf("locked" to action.locked)) + refreshConversations() + } + } + } + }, ) }, gesturesEnabled = true // Allows swiping from the screen's left edge to open the drawer diff --git a/app/src/main/java/dev/kaizen/app/chat/Markdown.kt b/app/src/main/java/dev/kaizen/app/chat/Markdown.kt index 16d4ed5..fcbb79f 100644 --- a/app/src/main/java/dev/kaizen/app/chat/Markdown.kt +++ b/app/src/main/java/dev/kaizen/app/chat/Markdown.kt @@ -363,20 +363,20 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier = val isLast = idx == blocks.lastIndex when (block) { is MdBlock.Heading -> { - if (idx > 0) Spacer(Modifier.height(8.dp)) + if (idx > 0) Spacer(Modifier.height(18.dp)) val rendered = inlineMarkdown(block.text + if (isLast) cursor else "", cs.onBackground, inlineCodeColor, inlineCodeBg) Text( text = rendered, color = cs.onBackground, fontSize = when (block.level) { 1 -> 22.sp; 2 -> 19.sp; else -> 17.sp }, fontWeight = FontWeight.Bold, - lineHeight = when (block.level) { 1 -> 28.sp; 2 -> 25.sp; else -> 23.sp }, + lineHeight = when (block.level) { 1 -> 30.sp; 2 -> 27.sp; else -> 24.sp }, ) - Spacer(Modifier.height(4.dp)) + Spacer(Modifier.height(8.dp)) } is MdBlock.CodeFence -> { - if (idx > 0) Spacer(Modifier.height(6.dp)) + if (idx > 0) Spacer(Modifier.height(12.dp)) Box( Modifier .fillMaxWidth() @@ -416,13 +416,13 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier = } } } - Spacer(Modifier.height(6.dp)) + Spacer(Modifier.height(12.dp)) } is MdBlock.ListBlock -> { - if (idx > 0) Spacer(Modifier.height(4.dp)) + if (idx > 0) Spacer(Modifier.height(8.dp)) block.items.forEachIndexed { itemIdx, item -> - Row(Modifier.fillMaxWidth().padding(start = 4.dp, bottom = 3.dp)) { + Row(Modifier.fillMaxWidth().padding(start = 6.dp, bottom = 6.dp)) { val bullet = if (item.ordered) "${item.index}." else "•" Text( bullet, @@ -435,27 +435,27 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier = text = inlineMarkdown(item.text + if (appendCursor) cursor else "", cs.onBackground, inlineCodeColor, inlineCodeBg), color = cs.onBackground, fontSize = 16.sp, - lineHeight = 23.sp, + lineHeight = 26.sp, modifier = Modifier.weight(1f), ) } } - Spacer(Modifier.height(4.dp)) + Spacer(Modifier.height(8.dp)) } is MdBlock.HorizontalRule -> { - if (idx > 0) Spacer(Modifier.height(8.dp)) + if (idx > 0) Spacer(Modifier.height(14.dp)) Box( Modifier .fillMaxWidth() .height(1.dp) - .background(cs.onSurfaceVariant.copy(alpha = 0.25f)) + .background(cs.onSurfaceVariant.copy(alpha = 0.20f)) ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(14.dp)) } is MdBlock.Blockquote -> { - if (idx > 0) Spacer(Modifier.height(6.dp)) + if (idx > 0) Spacer(Modifier.height(12.dp)) Row( Modifier .fillMaxWidth() @@ -474,21 +474,21 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier = text = inlineMarkdown(block.text + if (isLast) cursor else "", cs.onSurfaceVariant, inlineCodeColor, inlineCodeBg), color = cs.onSurfaceVariant, fontSize = 15.sp, - lineHeight = 22.sp, + lineHeight = 24.sp, fontStyle = FontStyle.Italic, modifier = Modifier.weight(1f), ) } - Spacer(Modifier.height(6.dp)) + Spacer(Modifier.height(12.dp)) } is MdBlock.Paragraph -> { - if (idx > 0) Spacer(Modifier.height(6.dp)) + if (idx > 0) Spacer(Modifier.height(12.dp)) Text( text = inlineMarkdown(block.text + if (isLast) cursor else "", cs.onBackground, inlineCodeColor, inlineCodeBg), color = cs.onBackground, fontSize = 16.sp, - lineHeight = 23.sp, + lineHeight = 26.sp, ) } diff --git a/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt b/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt index 156fd56..32fd04c 100644 --- a/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt +++ b/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt @@ -19,25 +19,43 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.Logout +import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Lock +import androidx.compose.material.icons.rounded.LockOpen +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.icons.rounded.PushPin import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Star +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +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.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.kaizen.app.net.ConversationSummary @@ -76,6 +94,13 @@ private fun groupConversations(conversations: List, labels: return groups } +sealed interface SidebarAction { + data class Rename(val id: String, val newTitle: String) : SidebarAction + data class Delete(val id: String) : SidebarAction + data class TogglePin(val id: String, val pinned: Boolean) : SidebarAction + data class ToggleLock(val id: String, val locked: Boolean) : SidebarAction +} + @Composable fun KaizenSidebar( userName: String, @@ -86,18 +111,23 @@ fun KaizenSidebar( onSelectConversation: (ConversationSummary) -> Unit, onOpenSettings: () -> Unit, onLogout: () -> Unit, + onAction: (SidebarAction) -> Unit = {}, modifier: Modifier = Modifier ) { val cs = MaterialTheme.colorScheme val isDark = isSystemInDarkTheme() + var deleteTarget by remember { mutableStateOf(null) } + var renameTarget by remember { mutableStateOf(null) } + var renameText by remember { mutableStateOf("") } + GlassSurface( modifier = modifier .fillMaxHeight() .width(300.dp) .padding(start = 12.dp, top = 12.dp, end = 4.dp, bottom = 12.dp), shape = KaizenShapes.lg, - tintAlpha = GlassTiers.sidebar(isDark), + tintAlpha = if (isDark) 0.88f else 0.92f, shadowStack = KaizenShadows.level3, cornerRadius = 24.dp, ) { @@ -113,46 +143,50 @@ fun KaizenSidebar( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text(stringResource(R.string.sidebar_brand), color = cs.onBackground, fontSize = 20.sp, fontWeight = FontWeight.Bold) + Text( + stringResource(R.string.sidebar_brand), + color = cs.onBackground, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ) Box( modifier = Modifier - .size(36.dp) + .size(34.dp) .clip(KaizenShapes.circle) .background(if (isDark) Color(0x1AFFFFFF) else Color(0x0A000000)) .clickable { onNewChat() }, contentAlignment = Alignment.Center ) { - Icon(Icons.Rounded.Edit, stringResource(R.string.chat_new), tint = cs.onBackground, modifier = Modifier.size(18.dp)) + Icon(Icons.Rounded.Edit, stringResource(R.string.chat_new), tint = cs.onBackground, modifier = Modifier.size(17.dp)) } } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(14.dp)) // Search Row( modifier = Modifier .fillMaxWidth() - .clip(KaizenShapes.sm) - .background(if (isDark) Color(0x12FFFFFF) else Color(0x06000000)) - .border(1.dp, if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f), KaizenShapes.sm) - .padding(horizontal = 12.dp, vertical = 10.dp), + .clip(KaizenShapes.pill) + .background(if (isDark) Color(0x0DFFFFFF) else Color(0x08000000)) + .padding(horizontal = 14.dp, vertical = 9.dp), verticalAlignment = Alignment.CenterVertically ) { - Icon(Icons.Rounded.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.6f), modifier = Modifier.size(18.dp)) + Icon(Icons.Rounded.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.45f), modifier = Modifier.size(16.dp)) Spacer(Modifier.width(8.dp)) - Text(stringResource(R.string.chat_search), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 14.sp) + Text(stringResource(R.string.chat_search), color = cs.onSurfaceVariant.copy(alpha = 0.45f), fontSize = 13.sp) } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(18.dp)) // Conversation list Box(Modifier.weight(1f).fillMaxWidth()) { if (conversations.isEmpty()) { Text( stringResource(R.string.chat_no_conversations), - color = cs.onSurfaceVariant.copy(alpha = 0.5f), - fontSize = 14.sp, - modifier = Modifier.padding(start = 12.dp, top = 12.dp) + color = cs.onSurfaceVariant.copy(alpha = 0.4f), + fontSize = 13.sp, + modifier = Modifier.padding(start = 8.dp, top = 8.dp) ) } else { val labels = DateLabels( @@ -164,16 +198,16 @@ fun KaizenSidebar( val grouped = remember(conversations, labels) { groupConversations(conversations, labels) } LazyColumn( modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(2.dp) ) { grouped.forEach { (dateGroup, items) -> item(key = "header_$dateGroup") { Text( dateGroup, - color = cs.onSurfaceVariant.copy(alpha = 0.45f), + color = cs.onSurfaceVariant.copy(alpha = 0.4f), fontSize = 11.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(start = 12.dp, top = 8.dp, bottom = 4.dp) + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(start = 8.dp, top = 14.dp, bottom = 6.dp) ) } items(items, key = { it.id }) { item -> @@ -181,6 +215,13 @@ fun KaizenSidebar( item = item, selected = item.id == currentId, onClick = { onSelectConversation(item) }, + onRename = { + renameTarget = item + renameText = item.title + }, + onDelete = { deleteTarget = item }, + onTogglePin = { onAction(SidebarAction.TogglePin(item.id, !item.pinned)) }, + onToggleLock = { onAction(SidebarAction.ToggleLock(item.id, !item.locked)) }, ) } } @@ -188,99 +229,242 @@ fun KaizenSidebar( } } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(12.dp)) - // User card + // User card — compact footer with settings + logout Row( modifier = Modifier .fillMaxWidth() .clip(KaizenShapes.md) - .background(if (isDark) Color(0x12FFFFFF) else Color(0x06000000)) - .border(1.dp, if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.04f), KaizenShapes.md) + .background(if (isDark) Color(0x0DFFFFFF) else Color(0x06000000)) .clickable { onOpenSettings() } .padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier - .size(40.dp) + .size(30.dp) .clip(KaizenShapes.circle) .background(if (isDark) Color(0xFF1E293B) else Color(0xFF0F172A)), contentAlignment = Alignment.Center ) { Text( userName.firstOrNull()?.toString()?.uppercase() ?: "A", - color = Color.White, fontSize = 17.sp, fontWeight = FontWeight.Bold + color = Color.White, fontSize = 13.sp, fontWeight = FontWeight.Bold ) } Spacer(Modifier.width(10.dp)) - Column(Modifier.weight(1f)) { - Text(userName, color = cs.onBackground, fontSize = 15.sp, fontWeight = FontWeight.SemiBold) + Text( + userName, + color = cs.onBackground, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier.weight(1f), + ) + Icon( + Icons.Rounded.Settings, + stringResource(R.string.sidebar_settings), + tint = cs.onSurfaceVariant.copy(alpha = 0.5f), + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(6.dp)) + Box( + modifier = Modifier + .size(28.dp) + .clip(KaizenShapes.circle) + .clickable { onLogout() }, + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.AutoMirrored.Rounded.Logout, + stringResource(R.string.sidebar_logout), + tint = cs.onSurfaceVariant.copy(alpha = 0.5f), + modifier = Modifier.size(16.dp), + ) } - Icon(Icons.Rounded.Settings, stringResource(R.string.sidebar_settings), tint = cs.onSurfaceVariant.copy(alpha = 0.7f), modifier = Modifier.size(20.dp)) - } - - Spacer(Modifier.height(10.dp)) - - // Logout - Row( - modifier = Modifier - .fillMaxWidth() - .clip(KaizenShapes.sm) - .clickable { onLogout() } - .padding(horizontal = 12.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.AutoMirrored.Rounded.Logout, stringResource(R.string.sidebar_logout), tint = cs.onSurfaceVariant.copy(alpha = 0.7f), modifier = Modifier.size(18.dp)) - Spacer(Modifier.width(10.dp)) - Text(stringResource(R.string.sidebar_logout), color = cs.onSurfaceVariant.copy(alpha = 0.85f), fontSize = 14.sp, fontWeight = FontWeight.Medium) } } } + + // Delete confirmation dialog + deleteTarget?.let { target -> + AlertDialog( + onDismissRequest = { deleteTarget = null }, + title = { Text(stringResource(R.string.sidebar_delete)) }, + text = { Text(stringResource(R.string.sidebar_delete_confirm)) }, + confirmButton = { + TextButton(onClick = { + onAction(SidebarAction.Delete(target.id)) + deleteTarget = null + }) { + Text(stringResource(R.string.sidebar_confirm), color = Color(0xFFEF4444)) + } + }, + dismissButton = { + TextButton(onClick = { deleteTarget = null }) { + Text(stringResource(R.string.sidebar_cancel)) + } + }, + ) + } + + // Rename dialog + renameTarget?.let { target -> + val focusManager = LocalFocusManager.current + AlertDialog( + onDismissRequest = { renameTarget = null }, + title = { Text(stringResource(R.string.sidebar_rename_title)) }, + text = { + BasicTextField( + value = renameText, + onValueChange = { renameText = it }, + singleLine = true, + textStyle = TextStyle( + color = cs.onBackground, + fontSize = 15.sp, + ), + cursorBrush = SolidColor(cs.onBackground), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { + focusManager.clearFocus() + if (renameText.isNotBlank()) { + onAction(SidebarAction.Rename(target.id, renameText.trim())) + } + renameTarget = null + }), + modifier = Modifier + .fillMaxWidth() + .clip(KaizenShapes.sm) + .background(if (isDark) Color(0x12FFFFFF) else Color(0x08000000)) + .padding(horizontal = 14.dp, vertical = 12.dp), + ) + }, + confirmButton = { + TextButton(onClick = { + if (renameText.isNotBlank()) { + onAction(SidebarAction.Rename(target.id, renameText.trim())) + } + renameTarget = null + }) { + Text(stringResource(R.string.sidebar_save)) + } + }, + dismissButton = { + TextButton(onClick = { renameTarget = null }) { + Text(stringResource(R.string.sidebar_cancel)) + } + }, + ) + } } @Composable -private fun HistoryItemRow(item: ConversationSummary, selected: Boolean, onClick: () -> Unit) { +private fun HistoryItemRow( + item: ConversationSummary, + selected: Boolean, + onClick: () -> Unit, + onRename: () -> Unit, + onDelete: () -> Unit, + onTogglePin: () -> Unit, + onToggleLock: () -> Unit, +) { val cs = MaterialTheme.colorScheme val isDark = isSystemInDarkTheme() val accent = LocalKaizenAccent.current + var showMenu by remember { mutableStateOf(false) } + val itemBg = when { - selected -> accent.primary.copy(alpha = 0.14f) - isDark -> Color(0x0AFFFFFF) - else -> Color(0x05000000) - } - val itemBorder = when { - selected -> accent.primary.copy(alpha = 0.35f) - isDark -> Color.White.copy(alpha = 0.04f) - else -> Color.Black.copy(alpha = 0.03f) + selected -> accent.primary.copy(alpha = if (isDark) 0.14f else 0.10f) + else -> Color.Transparent } - Row( - modifier = Modifier - .fillMaxWidth() - .clip(KaizenShapes.pill) - .background(itemBg) - .border(1.2.dp, itemBorder, KaizenShapes.pill) - .clickable { onClick() } - .padding(horizontal = 16.dp, vertical = 11.dp), - verticalAlignment = Alignment.CenterVertically - ) { - if (item.pinned) { - Icon(Icons.Rounded.Star, null, tint = accent.primary, modifier = Modifier.size(14.dp)) - Spacer(Modifier.width(8.dp)) + Box { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(KaizenShapes.sm) + .background(itemBg) + .clickable { onClick() } + .padding(start = 10.dp, end = 4.dp, top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (item.pinned) { + Icon(Icons.Rounded.Star, null, tint = accent.primary.copy(alpha = 0.7f), modifier = Modifier.size(13.dp)) + Spacer(Modifier.width(6.dp)) + } + Text( + text = if (item.locked) stringResource(R.string.chat_locked) else item.title, + color = if (item.locked) cs.onSurfaceVariant.copy(alpha = 0.45f) else cs.onBackground.copy(alpha = 0.85f), + fontSize = 13.5.sp, + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, + fontStyle = if (item.locked) FontStyle.Italic else FontStyle.Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + if (item.locked) { + Icon(Icons.Rounded.Lock, null, tint = accent.primary.copy(alpha = 0.5f), modifier = Modifier.size(13.dp)) + Spacer(Modifier.width(2.dp)) + } + Box( + modifier = Modifier + .size(28.dp) + .clip(KaizenShapes.circle) + .clickable { showMenu = true }, + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Rounded.MoreVert, + stringResource(R.string.sidebar_more), + tint = cs.onSurfaceVariant.copy(alpha = 0.35f), + modifier = Modifier.size(16.dp), + ) + } } - Text( - text = if (item.locked) stringResource(R.string.chat_locked) else item.title, - color = if (item.locked) cs.onSurfaceVariant.copy(alpha = 0.5f) else cs.onBackground, - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - fontStyle = if (item.locked) FontStyle.Italic else FontStyle.Normal, - modifier = Modifier.weight(1f) - ) - if (item.locked) { - Spacer(Modifier.width(8.dp)) - Icon(Icons.Rounded.Lock, stringResource(R.string.chat_locked_badge), tint = accent.primary, modifier = Modifier.size(15.dp)) + + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + ) { + if (!item.locked) { + DropdownMenuItem( + text = { Text(stringResource(R.string.sidebar_rename), fontSize = 14.sp) }, + onClick = { showMenu = false; onRename() }, + leadingIcon = { Icon(Icons.Rounded.Edit, null, modifier = Modifier.size(18.dp)) }, + ) + } + DropdownMenuItem( + text = { + Text( + stringResource(if (item.pinned) R.string.sidebar_unpin else R.string.sidebar_pin), + fontSize = 14.sp, + ) + }, + onClick = { showMenu = false; onTogglePin() }, + leadingIcon = { Icon(Icons.Rounded.PushPin, null, modifier = Modifier.size(18.dp)) }, + ) + DropdownMenuItem( + text = { + Text( + stringResource(if (item.locked) R.string.sidebar_unlock else R.string.sidebar_lock), + fontSize = 14.sp, + ) + }, + onClick = { showMenu = false; onToggleLock() }, + leadingIcon = { + Icon( + if (item.locked) Icons.Rounded.LockOpen else Icons.Rounded.Lock, + null, + modifier = Modifier.size(18.dp), + ) + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.sidebar_delete), fontSize = 14.sp, color = Color(0xFFEF4444)) }, + onClick = { showMenu = false; onDelete() }, + leadingIcon = { Icon(Icons.Rounded.Delete, null, tint = Color(0xFFEF4444), modifier = Modifier.size(18.dp)) }, + ) } } } diff --git a/app/src/main/java/dev/kaizen/app/net/KaizenApi.kt b/app/src/main/java/dev/kaizen/app/net/KaizenApi.kt index 4a45e7b..fee90a6 100644 --- a/app/src/main/java/dev/kaizen/app/net/KaizenApi.kt +++ b/app/src/main/java/dev/kaizen/app/net/KaizenApi.kt @@ -316,6 +316,47 @@ object KaizenApi { } } + /** PATCH /api/v1/conversations/[id] — update title, locked, pinned. */ + suspend fun patchConversation(baseUrl: String, token: String, id: String, body: Map): Boolean = + withContext(Dispatchers.IO) { + val payload = json.encodeToString(kotlinx.serialization.json.JsonObject( + body.mapValues { (_, v) -> + when (v) { + is String -> kotlinx.serialization.json.JsonPrimitive(v) + is Boolean -> kotlinx.serialization.json.JsonPrimitive(v) + else -> kotlinx.serialization.json.JsonNull + } + } + )).toRequestBody(jsonMedia) + val req = Request.Builder() + .url("$baseUrl/api/v1/conversations/$id") + .patch(payload) + .header("Authorization", "Bearer $token") + .build() + try { + client.newCall(req).execute().use { it.isSuccessful } + } catch (e: Exception) { + Log.w(TAG, "patchConversation($id): ${e.javaClass.simpleName}: ${e.message}") + false + } + } + + /** DELETE /api/v1/conversations/[id] — permanently remove a conversation. */ + suspend fun deleteConversation(baseUrl: String, token: String, id: String): Boolean = + withContext(Dispatchers.IO) { + val req = Request.Builder() + .url("$baseUrl/api/v1/conversations/$id") + .delete() + .header("Authorization", "Bearer $token") + .build() + try { + client.newCall(req).execute().use { it.isSuccessful } + } catch (e: Exception) { + Log.w(TAG, "deleteConversation($id): ${e.javaClass.simpleName}: ${e.message}") + false + } + } + /** POST /api/v1/conversations/[id]/title — fire-and-forget auto-title after the first exchange. */ suspend fun generateTitle(baseUrl: String, token: String, id: String) = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/dev/kaizen/app/ui/effect/GlassSurface.kt b/app/src/main/java/dev/kaizen/app/ui/effect/GlassSurface.kt index 2a252a1..8508ecb 100644 --- a/app/src/main/java/dev/kaizen/app/ui/effect/GlassSurface.kt +++ b/app/src/main/java/dev/kaizen/app/ui/effect/GlassSurface.kt @@ -92,7 +92,7 @@ fun GlassSurface( * Use these constants with [GlassSurface] instead of ad-hoc alpha values. */ object GlassTiers { - fun sidebar(isDark: Boolean) = if (isDark) 0.72f else 0.68f + fun sidebar(isDark: Boolean) = if (isDark) 0.88f else 0.92f fun input(isDark: Boolean) = if (isDark) 0.75f else 0.74f fun pill(isDark: Boolean) = if (isDark) 0.80f else 0.82f fun chip(isDark: Boolean) = if (isDark) 0.85f else 0.90f diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index bef20bf..84ccc9c 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -61,6 +61,18 @@ EARLIER Sign Out Settings + Rename + Delete + Pin + Unpin + Lock + Unlock + More + Rename Chat + Permanently delete this chat? + Cancel + Confirm + Save Select Model diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 49873d9..1763a43 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -63,6 +63,18 @@ FRÜHER Abmelden Einstellungen + Umbenennen + Löschen + Anheften + Lösen + Sperren + Entsperren + Mehr + Chat umbenennen + Chat endgültig löschen? + Abbrechen + Bestätigen + Speichern Modell auswählen