feat: sidebar redesign — glasier look, functional search, generate title, duplicate

Design: lower tintAlpha (0.80/0.86) for more blob bleed-through, accent-tinted
gradient borders + level1 shadow on active items, specular borders on search bar
and user card, accent gradient avatar, separator line in context menu.

Features: live search filter, "Titel generieren" (Sparkles, manual POST with
{ manual: true }), "Duplizieren" (copies all messages with new IDs + parentId
chain), titleGenerated field on ConversationSummary/Entity (Room v4).
This commit is contained in:
Bruno Deanoz 2026-06-23 00:56:58 +02:00
parent 2f9ada646c
commit 558b1be675
8 changed files with 175 additions and 38 deletions

View file

@ -681,11 +681,9 @@ fun ChatScreen(
} }
is SidebarAction.ToggleLock -> { is SidebarAction.ToggleLock -> {
if (action.locked) { if (action.locked) {
// Locking — no auth needed
KaizenApi.patchConversation(cfg.baseUrl, cfg.token, action.id, mapOf("locked" to true)) KaizenApi.patchConversation(cfg.baseUrl, cfg.token, action.id, mapOf("locked" to true))
refreshConversations() refreshConversations()
} else { } else {
// Unlocking — require biometric or password
val summary = conversations.find { it.id == action.id } val summary = conversations.find { it.id == action.id }
if (summary != null) { if (summary != null) {
requestBiometricOrPassword(summary, forToggle = true) { requestBiometricOrPassword(summary, forToggle = true) {
@ -697,6 +695,40 @@ fun ChatScreen(
} }
} }
} }
is SidebarAction.GenerateTitle -> {
val title = KaizenApi.generateTitle(cfg.baseUrl, cfg.token, action.id, manual = true)
if (title != null) {
chat.conversationRepo.updateTitle(action.id, title)
refreshConversations()
}
}
is SidebarAction.Duplicate -> {
val srcMessages = KaizenApi.fetchMessages(cfg.baseUrl, cfg.token, action.id)
if (srcMessages.isNotEmpty()) {
val srcTitle = conversations.find { it.id == action.id }?.title ?: ""
val newId = KaizenApi.createConversation(cfg.baseUrl, cfg.token, "$srcTitle (2)")
if (newId != null) {
val saveMessages = srcMessages.mapIndexed { i, m ->
dev.kaizen.app.net.SaveMessage(
id = java.util.UUID.randomUUID().toString(),
parentId = if (i == 0) null else null,
role = m.role,
content = m.content,
model = m.model,
attachments = m.attachments.takeIf { it.isNotEmpty() },
reasoning = m.reasoning,
sources = m.sources,
)
}
// Build parentId chain
val chained = saveMessages.mapIndexed { i, m ->
m.copy(parentId = if (i == 0) null else saveMessages[i - 1].id)
}
KaizenApi.saveMessages(cfg.baseUrl, cfg.token, newId, chained)
refreshConversations()
}
}
}
} }
} }
}, },

View file

@ -36,6 +36,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
@ -48,8 +49,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import dev.kaizen.app.net.ConversationSummary import dev.kaizen.app.net.ConversationSummary
import dev.kaizen.app.ui.effect.GlassSurface import dev.kaizen.app.ui.effect.GlassSurface
import dev.kaizen.app.ui.effect.GlassTiers
import dev.kaizen.app.ui.effect.KaizenShadows import dev.kaizen.app.ui.effect.KaizenShadows
import dev.kaizen.app.ui.effect.kaizenShadow
import dev.kaizen.app.ui.effect.innerTopHighlight
import dev.kaizen.app.ui.shape.KaizenShapes import dev.kaizen.app.ui.shape.KaizenShapes
import dev.kaizen.app.R import dev.kaizen.app.R
import dev.kaizen.app.ui.theme.LocalKaizenAccent import dev.kaizen.app.ui.theme.LocalKaizenAccent
@ -88,6 +90,8 @@ sealed interface SidebarAction {
data class Delete(val id: String) : SidebarAction data class Delete(val id: String) : SidebarAction
data class TogglePin(val id: String, val pinned: Boolean) : SidebarAction data class TogglePin(val id: String, val pinned: Boolean) : SidebarAction
data class ToggleLock(val id: String, val locked: Boolean) : SidebarAction data class ToggleLock(val id: String, val locked: Boolean) : SidebarAction
data class GenerateTitle(val id: String) : SidebarAction
data class Duplicate(val id: String) : SidebarAction
} }
@Composable @Composable
@ -105,10 +109,12 @@ fun KaizenSidebar(
) { ) {
val cs = MaterialTheme.colorScheme val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme() val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
var deleteTarget by remember { mutableStateOf<ConversationSummary?>(null) } var deleteTarget by remember { mutableStateOf<ConversationSummary?>(null) }
var renameTarget by remember { mutableStateOf<ConversationSummary?>(null) } var renameTarget by remember { mutableStateOf<ConversationSummary?>(null) }
var renameText by remember { mutableStateOf("") } var renameText by remember { mutableStateOf("") }
var searchQuery by remember { mutableStateOf("") }
GlassSurface( GlassSurface(
modifier = modifier modifier = modifier
@ -116,7 +122,7 @@ fun KaizenSidebar(
.width(300.dp) .width(300.dp)
.padding(start = 12.dp, top = 12.dp, end = 4.dp, bottom = 12.dp), .padding(start = 12.dp, top = 12.dp, end = 4.dp, bottom = 12.dp),
shape = KaizenShapes.lg, shape = KaizenShapes.lg,
tintAlpha = if (isDark) 0.88f else 0.92f, tintAlpha = if (isDark) 0.80f else 0.86f,
shadowStack = KaizenShadows.level3, shadowStack = KaizenShadows.level3,
cornerRadius = 24.dp, cornerRadius = 24.dp,
) { ) {
@ -126,7 +132,7 @@ fun KaizenSidebar(
.statusBarsPadding() .statusBarsPadding()
.padding(16.dp) .padding(16.dp)
) { ) {
// Header // ── Header ──
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@ -152,25 +158,70 @@ fun KaizenSidebar(
Spacer(Modifier.height(14.dp)) Spacer(Modifier.height(14.dp))
// Search // ── Search (functional) ──
val searchBorder = remember(isDark) {
if (isDark) Brush.verticalGradient(
listOf(Color.White.copy(alpha = 0.10f), Color.White.copy(alpha = 0.03f))
) else Brush.verticalGradient(
listOf(Color.White.copy(alpha = 0.50f), Color.Black.copy(alpha = 0.04f))
)
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(KaizenShapes.pill) .clip(KaizenShapes.pill)
.background(if (isDark) Color(0x0DFFFFFF) else Color(0x08000000)) .border(0.8.dp, searchBorder, KaizenShapes.pill)
.background(if (isDark) Color(0x12FFFFFF) else Color(0x08000000))
.padding(horizontal = 14.dp, vertical = 9.dp), .padding(horizontal = 14.dp, vertical = 9.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(KaizenIcons.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.45f), modifier = Modifier.size(16.dp)) Icon(KaizenIcons.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.40f), modifier = Modifier.size(15.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.chat_search), color = cs.onSurfaceVariant.copy(alpha = 0.45f), fontSize = 13.sp) BasicTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
singleLine = true,
textStyle = TextStyle(
color = cs.onBackground,
fontSize = 13.sp,
),
cursorBrush = SolidColor(cs.onSurfaceVariant),
decorationBox = { innerTextField ->
Box {
if (searchQuery.isEmpty()) {
Text(
stringResource(R.string.chat_search),
color = cs.onSurfaceVariant.copy(alpha = 0.40f),
fontSize = 13.sp,
)
}
innerTextField()
}
},
modifier = Modifier.weight(1f),
)
if (searchQuery.isNotEmpty()) {
Icon(
KaizenIcons.X,
null,
tint = cs.onSurfaceVariant.copy(alpha = 0.40f),
modifier = Modifier
.size(14.dp)
.clickable { searchQuery = "" },
)
}
} }
Spacer(Modifier.height(18.dp)) Spacer(Modifier.height(18.dp))
// Conversation list // ── Conversation list ──
val filteredConversations = remember(conversations, searchQuery) {
if (searchQuery.isBlank()) conversations
else conversations.filter { it.title.contains(searchQuery, ignoreCase = true) }
}
Box(Modifier.weight(1f).fillMaxWidth()) { Box(Modifier.weight(1f).fillMaxWidth()) {
if (conversations.isEmpty()) { if (filteredConversations.isEmpty()) {
Text( Text(
stringResource(R.string.chat_no_conversations), stringResource(R.string.chat_no_conversations),
color = cs.onSurfaceVariant.copy(alpha = 0.4f), color = cs.onSurfaceVariant.copy(alpha = 0.4f),
@ -184,7 +235,7 @@ fun KaizenSidebar(
earlier = stringResource(R.string.sidebar_earlier), earlier = stringResource(R.string.sidebar_earlier),
pinned = stringResource(R.string.sidebar_pinned), pinned = stringResource(R.string.sidebar_pinned),
) )
val grouped = remember(conversations, labels) { groupConversations(conversations, labels) } val grouped = remember(filteredConversations, labels) { groupConversations(filteredConversations, labels) }
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
@ -193,9 +244,10 @@ fun KaizenSidebar(
item(key = "header_$dateGroup") { item(key = "header_$dateGroup") {
Text( Text(
dateGroup, dateGroup,
color = cs.onSurfaceVariant.copy(alpha = 0.4f), color = cs.onSurfaceVariant.copy(alpha = 0.35f),
fontSize = 11.sp, fontSize = 10.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
modifier = Modifier.padding(start = 8.dp, top = 14.dp, bottom = 6.dp) modifier = Modifier.padding(start = 8.dp, top = 14.dp, bottom = 6.dp)
) )
} }
@ -211,6 +263,8 @@ fun KaizenSidebar(
onDelete = { deleteTarget = item }, onDelete = { deleteTarget = item },
onTogglePin = { onAction(SidebarAction.TogglePin(item.id, !item.pinned)) }, onTogglePin = { onAction(SidebarAction.TogglePin(item.id, !item.pinned)) },
onToggleLock = { onAction(SidebarAction.ToggleLock(item.id, !item.locked)) }, onToggleLock = { onAction(SidebarAction.ToggleLock(item.id, !item.locked)) },
onGenerateTitle = { onAction(SidebarAction.GenerateTitle(item.id)) },
onDuplicate = { onAction(SidebarAction.Duplicate(item.id)) },
) )
} }
} }
@ -220,12 +274,22 @@ fun KaizenSidebar(
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
// User card — compact footer with settings + logout // ── User card ──
val cardBorder = remember(isDark) {
if (isDark) Brush.verticalGradient(
listOf(Color.White.copy(alpha = 0.12f), Color.White.copy(alpha = 0.03f))
) else Brush.verticalGradient(
listOf(Color.White.copy(alpha = 0.50f), Color.Black.copy(alpha = 0.04f))
)
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(KaizenShapes.md) .clip(KaizenShapes.md)
.background(if (isDark) Color(0x0DFFFFFF) else Color(0x06000000)) .kaizenShadow(stack = KaizenShadows.level1, cornerRadius = 12.dp)
.background(if (isDark) Color(0x14FFFFFF) else Color(0x0A000000))
.border(0.8.dp, cardBorder, KaizenShapes.md)
.innerTopHighlight()
.clickable { onOpenSettings() } .clickable { onOpenSettings() }
.padding(horizontal = 12.dp, vertical = 10.dp), .padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -234,7 +298,11 @@ fun KaizenSidebar(
modifier = Modifier modifier = Modifier
.size(30.dp) .size(30.dp)
.clip(KaizenShapes.circle) .clip(KaizenShapes.circle)
.background(if (isDark) Color(0xFF1E293B) else Color(0xFF0F172A)), .background(
Brush.verticalGradient(
listOf(accent.primary.copy(alpha = 0.8f), accent.primary.copy(alpha = 0.6f))
)
),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
@ -253,8 +321,8 @@ fun KaizenSidebar(
Icon( Icon(
KaizenIcons.Settings, KaizenIcons.Settings,
stringResource(R.string.sidebar_settings), stringResource(R.string.sidebar_settings),
tint = cs.onSurfaceVariant.copy(alpha = 0.5f), tint = cs.onSurfaceVariant.copy(alpha = 0.45f),
modifier = Modifier.size(18.dp), modifier = Modifier.size(17.dp),
) )
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
Box( Box(
@ -267,8 +335,8 @@ fun KaizenSidebar(
Icon( Icon(
KaizenIcons.LogOut, KaizenIcons.LogOut,
stringResource(R.string.sidebar_logout), stringResource(R.string.sidebar_logout),
tint = cs.onSurfaceVariant.copy(alpha = 0.5f), tint = cs.onSurfaceVariant.copy(alpha = 0.45f),
modifier = Modifier.size(16.dp), modifier = Modifier.size(15.dp),
) )
} }
} }
@ -356,6 +424,8 @@ private fun HistoryItemRow(
onDelete: () -> Unit, onDelete: () -> Unit,
onTogglePin: () -> Unit, onTogglePin: () -> Unit,
onToggleLock: () -> Unit, onToggleLock: () -> Unit,
onGenerateTitle: () -> Unit,
onDuplicate: () -> Unit,
) { ) {
val cs = MaterialTheme.colorScheme val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme() val isDark = isSystemInDarkTheme()
@ -363,17 +433,31 @@ private fun HistoryItemRow(
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
val itemBg = when { val itemBorder = if (selected) {
selected -> accent.primary.copy(alpha = if (isDark) 0.14f else 0.10f) remember(isDark, accent) {
else -> Color.Transparent if (isDark) Brush.verticalGradient(
listOf(accent.primary.copy(alpha = 0.25f), accent.primary.copy(alpha = 0.06f))
) else Brush.verticalGradient(
listOf(accent.primary.copy(alpha = 0.20f), accent.primary.copy(alpha = 0.05f))
)
} }
} else null
Box { Box {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(KaizenShapes.sm) .clip(KaizenShapes.sm)
.background(itemBg) .then(
if (selected) {
Modifier
.kaizenShadow(stack = KaizenShadows.level1, cornerRadius = 10.dp)
.background(accent.primary.copy(alpha = if (isDark) 0.12f else 0.08f))
.border(0.8.dp, itemBorder!!, KaizenShapes.sm)
} else {
Modifier
}
)
.clickable { onClick() } .clickable { onClick() }
.padding(start = 10.dp, end = 4.dp, top = 8.dp, bottom = 8.dp), .padding(start = 10.dp, end = 4.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -384,8 +468,8 @@ private fun HistoryItemRow(
} }
Text( Text(
text = if (item.locked) stringResource(R.string.chat_locked) else item.title, 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), color = if (item.locked) cs.onSurfaceVariant.copy(alpha = 0.45f) else cs.onBackground.copy(alpha = if (selected) 0.95f else 0.80f),
fontSize = 13.5.sp, fontSize = 13.sp,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
fontStyle = if (item.locked) FontStyle.Italic else FontStyle.Normal, fontStyle = if (item.locked) FontStyle.Italic else FontStyle.Normal,
maxLines = 1, maxLines = 1,
@ -406,8 +490,8 @@ private fun HistoryItemRow(
Icon( Icon(
KaizenIcons.MoreVertical, KaizenIcons.MoreVertical,
stringResource(R.string.sidebar_more), stringResource(R.string.sidebar_more),
tint = cs.onSurfaceVariant.copy(alpha = 0.35f), tint = cs.onSurfaceVariant.copy(alpha = 0.30f),
modifier = Modifier.size(16.dp), modifier = Modifier.size(15.dp),
) )
} }
} }
@ -416,18 +500,30 @@ private fun HistoryItemRow(
expanded = showMenu, expanded = showMenu,
onDismissRequest = { showMenu = false }, onDismissRequest = { showMenu = false },
containerColor = if (isDark) Color(0xFF1A2030) else Color(0xFFF8F9FB), containerColor = if (isDark) Color(0xFF1A2030) else Color(0xFFF8F9FB),
shadowElevation = 12.dp, shadowElevation = 16.dp,
shape = KaizenShapes.md, shape = KaizenShapes.md,
) { ) {
if (!item.locked) { if (!item.locked) {
MenuRow(KaizenIcons.Pencil, stringResource(R.string.sidebar_rename), cs.onBackground) { showMenu = false; onRename() } MenuRow(KaizenIcons.Pencil, stringResource(R.string.sidebar_rename), cs.onBackground) { showMenu = false; onRename() }
} }
if (!item.titleGenerated) {
MenuRow(KaizenIcons.Sparkles, stringResource(R.string.sidebar_generate_title), cs.onBackground) { showMenu = false; onGenerateTitle() }
}
MenuRow(KaizenIcons.Pin, stringResource(if (item.pinned) R.string.sidebar_unpin else R.string.sidebar_pin), cs.onBackground) { showMenu = false; onTogglePin() } MenuRow(KaizenIcons.Pin, stringResource(if (item.pinned) R.string.sidebar_unpin else R.string.sidebar_pin), cs.onBackground) { showMenu = false; onTogglePin() }
MenuRow(KaizenIcons.Copy, stringResource(R.string.sidebar_duplicate), cs.onBackground) { showMenu = false; onDuplicate() }
MenuRow( MenuRow(
if (item.locked) KaizenIcons.LockOpen else KaizenIcons.Lock, if (item.locked) KaizenIcons.LockOpen else KaizenIcons.Lock,
stringResource(if (item.locked) R.string.sidebar_unlock else R.string.sidebar_lock), stringResource(if (item.locked) R.string.sidebar_unlock else R.string.sidebar_lock),
cs.onBackground, cs.onBackground,
) { showMenu = false; onToggleLock() } ) { showMenu = false; onToggleLock() }
// Separator
Box(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
.height(0.5.dp)
.background(if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.06f))
)
MenuRow(KaizenIcons.Trash, stringResource(R.string.sidebar_delete), Color(0xFFEF4444)) { showMenu = false; onDelete() } MenuRow(KaizenIcons.Trash, stringResource(R.string.sidebar_delete), Color(0xFFEF4444)) { showMenu = false; onDelete() }
} }
} }
@ -439,11 +535,11 @@ private fun MenuRow(icon: ImageVector, label: String, tint: Color, onClick: () -
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 11.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(icon, null, tint = tint.copy(alpha = 0.7f), modifier = Modifier.size(18.dp)) Icon(icon, null, tint = tint.copy(alpha = 0.6f), modifier = Modifier.size(16.dp))
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))
Text(label, color = tint, fontSize = 14.sp, fontWeight = FontWeight.Medium) Text(label, color = tint.copy(alpha = 0.9f), fontSize = 13.5.sp, fontWeight = FontWeight.Medium)
} }
} }

View file

@ -15,6 +15,7 @@ data class ConversationEntity(
val pinned: Boolean, val pinned: Boolean,
val updatedAt: String?, val updatedAt: String?,
val cachedAt: Long, val cachedAt: Long,
val titleGenerated: Boolean = false,
) )
@Entity( @Entity(

View file

@ -8,7 +8,7 @@ import androidx.room.TypeConverters
@Database( @Database(
entities = [ConversationEntity::class, MessageEntity::class], entities = [ConversationEntity::class, MessageEntity::class],
version = 3, version = 4,
exportSchema = false, exportSchema = false,
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)

View file

@ -10,6 +10,7 @@ fun ConversationSummary.toEntity() = ConversationEntity(
pinned = pinned, pinned = pinned,
updatedAt = updatedAt, updatedAt = updatedAt,
cachedAt = System.currentTimeMillis(), cachedAt = System.currentTimeMillis(),
titleGenerated = titleGenerated,
) )
fun ConversationEntity.toSummary() = ConversationSummary( fun ConversationEntity.toSummary() = ConversationSummary(
@ -18,6 +19,7 @@ fun ConversationEntity.toSummary() = ConversationSummary(
locked = locked, locked = locked,
pinned = pinned, pinned = pinned,
updatedAt = updatedAt, updatedAt = updatedAt,
titleGenerated = titleGenerated,
) )
fun StoredMessage.toEntity(conversationId: String, sortOrder: Int) = MessageEntity( fun StoredMessage.toEntity(conversationId: String, sortOrder: Int) = MessageEntity(

View file

@ -61,6 +61,7 @@ data class ConversationSummary(
val locked: Boolean = false, val locked: Boolean = false,
val pinned: Boolean = false, val pinned: Boolean = false,
val updatedAt: String? = null, val updatedAt: String? = null,
val titleGenerated: Boolean = false,
) )
@Serializable private data class ConversationsResponse(val conversations: List<ConversationSummary> = emptyList()) @Serializable private data class ConversationsResponse(val conversations: List<ConversationSummary> = emptyList())
@ -446,11 +447,12 @@ object KaizenApi {
} }
/** POST /api/v1/conversations/[id]/title — returns the generated title or null. */ /** POST /api/v1/conversations/[id]/title — returns the generated title or null. */
suspend fun generateTitle(baseUrl: String, token: String, id: String): String? = suspend fun generateTitle(baseUrl: String, token: String, id: String, manual: Boolean = false): String? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val body = if (manual) """{"manual":true}""" else "{}"
val req = Request.Builder() val req = Request.Builder()
.url("$baseUrl/api/v1/conversations/$id/title") .url("$baseUrl/api/v1/conversations/$id/title")
.post("{}".toRequestBody(jsonMedia)) .post(body.toRequestBody(jsonMedia))
.header("Authorization", "Bearer $token") .header("Authorization", "Bearer $token")
.build() .build()
try { try {

View file

@ -74,6 +74,8 @@
<string name="sidebar_lock">Lock</string> <string name="sidebar_lock">Lock</string>
<string name="sidebar_unlock">Unlock</string> <string name="sidebar_unlock">Unlock</string>
<string name="sidebar_more">More</string> <string name="sidebar_more">More</string>
<string name="sidebar_generate_title">Generate Title</string>
<string name="sidebar_duplicate">Duplicate</string>
<string name="sidebar_rename_title">Rename Chat</string> <string name="sidebar_rename_title">Rename Chat</string>
<string name="sidebar_delete_confirm">Permanently delete this chat?</string> <string name="sidebar_delete_confirm">Permanently delete this chat?</string>
<string name="sidebar_cancel">Cancel</string> <string name="sidebar_cancel">Cancel</string>

View file

@ -76,6 +76,8 @@
<string name="sidebar_lock">Sperren</string> <string name="sidebar_lock">Sperren</string>
<string name="sidebar_unlock">Entsperren</string> <string name="sidebar_unlock">Entsperren</string>
<string name="sidebar_more">Mehr</string> <string name="sidebar_more">Mehr</string>
<string name="sidebar_generate_title">Titel generieren</string>
<string name="sidebar_duplicate">Duplizieren</string>
<string name="sidebar_rename_title">Chat umbenennen</string> <string name="sidebar_rename_title">Chat umbenennen</string>
<string name="sidebar_delete_confirm">Chat endgültig löschen?</string> <string name="sidebar_delete_confirm">Chat endgültig löschen?</string>
<string name="sidebar_cancel">Abbrechen</string> <string name="sidebar_cancel">Abbrechen</string>