diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt index 53fc6ad..54a2948 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt @@ -221,6 +221,10 @@ fun MessageRow(message: Message) { Spacer(Modifier.width(10.dp)) Box(Modifier.weight(1f).padding(top = 3.dp)) { Column { + if (message.reasoning.isNotBlank()) { + ReasoningBlock(message.reasoning, streaming = message.streaming) + Spacer(Modifier.height(8.dp)) + } if (message.tools.isNotEmpty()) { ToolEventsBlock(message.tools, streaming = message.streaming) Spacer(Modifier.height(8.dp)) @@ -231,10 +235,6 @@ fun MessageRow(message: Message) { } if (message.thinking) TypingDots() else MarkdownText(text = message.content, streaming = message.streaming) - if (message.reasoning.isNotBlank()) { - Spacer(Modifier.height(8.dp)) - ReasoningBlock(message.reasoning, streaming = message.streaming) - } if (message.sources.isNotEmpty()) { Spacer(Modifier.height(8.dp)) SourcesBlock(message.sources, query = message.query) 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 f86f4e7..25edd84 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -224,6 +224,8 @@ fun ChatScreen( content = m.content, wireId = m.id, attachments = m.attachments, + reasoning = m.reasoning ?: "", + sources = m.sources ?: emptyList(), ), ) } @@ -233,7 +235,6 @@ fun ChatScreen( if (stored.isNotEmpty()) { val entities = stored.mapIndexed { i, m -> m.toEntity(summary.id, i) } chat.messageRepo.cacheMessages(summary.id, entities) - // Only rebuild message list if content actually changed if (stored.size != cachedList.size || stored.zip(cachedList).any { (s, c) -> s.id != c.id || s.content != c.content }) { messages.clear() stored.forEach { m -> @@ -244,6 +245,8 @@ fun ChatScreen( content = m.content, wireId = m.id, attachments = m.attachments, + reasoning = m.reasoning ?: "", + sources = m.sources ?: emptyList(), ), ) } @@ -303,7 +306,10 @@ fun ChatScreen( ) } val idx = messages.indexOfLast { it.id == assistantId } - val finalContent = if (idx >= 0) messages[idx].content else "" + val finalMsg = if (idx >= 0) messages[idx] else null + val finalContent = finalMsg?.content ?: "" + val finalReasoning = finalMsg?.reasoning?.takeIf { it.isNotBlank() } + val finalSources = finalMsg?.sources?.takeIf { it.isNotEmpty() } if (idx >= 0) messages[idx] = messages[idx].copy(streaming = false, thinking = false) val convId = convIdDeferred?.await()?.also { conversationId = it } ?: conversationId @@ -317,11 +323,15 @@ fun ChatScreen( role = "user", content = trimmed, attachments = uploadedAtts.takeIf { it.isNotEmpty() }, ), - SaveMessage(id = assistantWireId, parentId = userMsg.wireId, role = "assistant", content = finalContent, model = cfg.model), + SaveMessage( + id = assistantWireId, parentId = userMsg.wireId, + role = "assistant", content = finalContent, model = cfg.model, + reasoning = finalReasoning, + sources = finalSources, + ), ), ) if (saved) { - // Cache new messages in Room for offline-first val msgCount = messages.size chat.messageRepo.cacheMessages(convId, listOf( MessageEntity( @@ -333,6 +343,8 @@ fun ChatScreen( id = assistantWireId, conversationId = convId, role = "assistant", content = finalContent, attachments = emptyList(), sortOrder = msgCount - 1, + reasoning = finalReasoning, + sources = finalSources ?: emptyList(), ), )) if (wasNewConversation) KaizenApi.generateTitle(cfg.baseUrl, cfg.token, convId) 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 af25bda..2db8da4 100644 --- a/app/src/main/java/dev/kaizen/app/chat/StreamBlocks.kt +++ b/app/src/main/java/dev/kaizen/app/chat/StreamBlocks.kt @@ -79,7 +79,7 @@ fun ReasoningBlock(reasoning: String, streaming: Boolean = false) { Icon(Icons.Rounded.Psychology, null, tint = accent.primary, modifier = Modifier.size(16.dp)) Spacer(Modifier.width(8.dp)) Text( - "Gedankengang", + stringResource(R.string.stream_reasoning), color = cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium, diff --git a/app/src/main/java/dev/kaizen/app/db/Converters.kt b/app/src/main/java/dev/kaizen/app/db/Converters.kt index ca487fe..5abf07f 100644 --- a/app/src/main/java/dev/kaizen/app/db/Converters.kt +++ b/app/src/main/java/dev/kaizen/app/db/Converters.kt @@ -2,6 +2,7 @@ package dev.kaizen.app.db import androidx.room.TypeConverter import dev.kaizen.app.net.Attachment +import dev.kaizen.app.net.SearchSource import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -15,4 +16,12 @@ class Converters { @TypeConverter fun jsonToAttachments(value: String): List = try { json.decodeFromString(value) } catch (_: Exception) { emptyList() } + + @TypeConverter + fun sourcesToJson(sources: List): String = + json.encodeToString(sources) + + @TypeConverter + fun jsonToSources(value: String): List = + try { json.decodeFromString(value) } catch (_: Exception) { emptyList() } } diff --git a/app/src/main/java/dev/kaizen/app/db/Entities.kt b/app/src/main/java/dev/kaizen/app/db/Entities.kt index 9f4587e..004d5b5 100644 --- a/app/src/main/java/dev/kaizen/app/db/Entities.kt +++ b/app/src/main/java/dev/kaizen/app/db/Entities.kt @@ -5,6 +5,7 @@ import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import dev.kaizen.app.net.Attachment +import dev.kaizen.app.net.SearchSource @Entity(tableName = "conversations") data class ConversationEntity( @@ -33,4 +34,6 @@ data class MessageEntity( val content: String, val attachments: List, val sortOrder: Int, + val reasoning: String? = null, + val sources: List = emptyList(), ) diff --git a/app/src/main/java/dev/kaizen/app/db/Mappers.kt b/app/src/main/java/dev/kaizen/app/db/Mappers.kt index b99e5a2..7320dc7 100644 --- a/app/src/main/java/dev/kaizen/app/db/Mappers.kt +++ b/app/src/main/java/dev/kaizen/app/db/Mappers.kt @@ -27,6 +27,8 @@ fun StoredMessage.toEntity(conversationId: String, sortOrder: Int) = MessageEnti content = content, attachments = attachments, sortOrder = sortOrder, + reasoning = reasoning, + sources = sources ?: emptyList(), ) fun MessageEntity.toStoredMessage() = StoredMessage( @@ -34,4 +36,6 @@ fun MessageEntity.toStoredMessage() = StoredMessage( role = role, content = content, attachments = attachments, + reasoning = reasoning, + sources = sources.takeIf { it.isNotEmpty() }, ) 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 fee90a6..310f547 100644 --- a/app/src/main/java/dev/kaizen/app/net/KaizenApi.kt +++ b/app/src/main/java/dev/kaizen/app/net/KaizenApi.kt @@ -79,6 +79,8 @@ data class StoredMessage( val role: String, val content: String = "", val attachments: List = emptyList(), + val reasoning: String? = null, + val sources: List? = null, ) @Serializable private data class ConversationDetail(val messages: List = emptyList()) @@ -93,6 +95,8 @@ data class SaveMessage( val kind: String = "chat", val model: String? = null, val attachments: List? = null, + val reasoning: String? = null, + val sources: List? = null, ) @Serializable private data class SaveMessagesRequest(val messages: List)