Compare commits

..

2 commits

Author SHA1 Message Date
Bruno Deanoz
44ff82a66d fix: voice messages appear in wrong order after reopening chat
Voice auto-send was saving messages to the server but not caching them
in Room with correct sortOrder. When navigating away and back, the app
loaded from Room cache (empty for voice messages) then from server,
where the tree-based ordering could differ from insertion order.

Now caches both user and assistant messages in Room with proper sortOrder
after the server save succeeds, matching the normal send() flow.
2026-06-23 01:28:53 +02:00
Bruno Deanoz
1899f2cff1 fix: voice messages show as compact pill instead of file attachment chip
Audio-only user messages (no text, only audio attachments) now render as
a clean "🎤 Sprachnachricht" pill instead of the ugly filename chip
(voice_1782170416740.m4a). Detects via attachments.all { kind == "audio" }
&& content.isBlank(). Regular file attachments are unaffected.
2026-06-23 01:24:56 +02:00
4 changed files with 48 additions and 8 deletions

View file

@ -489,6 +489,8 @@ fun MessageRow(
)
}
val isVoiceOnly = hasAttachments && message.content.isBlank() && message.attachments.all { it.kind == "audio" }
Box(
Modifier
.widthIn(max = 300.dp)
@ -499,12 +501,28 @@ fun MessageRow(
.border(1.2.dp, borderBrush, shape),
) {
Column {
if (hasAttachments) AttachmentGrid(message.attachments, Modifier.padding(6.dp))
if (message.content.isNotBlank()) {
Text(
message.content, color = cs.onBackground, fontSize = 16.sp, lineHeight = 26.sp,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = if (hasAttachments) 4.dp else 13.dp, bottom = 13.dp),
)
if (isVoiceOnly) {
Row(
Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(KaizenIcons.Mic, null, tint = accent.primary.copy(alpha = 0.7f), modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text(
stringResource(R.string.chat_voice_message),
color = cs.onBackground.copy(alpha = 0.70f),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
)
}
} else {
if (hasAttachments) AttachmentGrid(message.attachments, Modifier.padding(6.dp))
if (message.content.isNotBlank()) {
Text(
message.content, color = cs.onBackground, fontSize = 16.sp, lineHeight = 26.sp,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = if (hasAttachments) 4.dp else 13.dp, bottom = 13.dp),
)
}
}
}
}

View file

@ -283,9 +283,29 @@ fun ChatScreen(
val cid = convIdDeferred?.await() ?: conversationId ?: return@launch
if (conversationId == null) conversationId = cid
val assistantMsg = messages.lastOrNull { it.id == assistantId } ?: return@launch
val finalContent = assistantMsg.content
val finalReasoning = assistantMsg.reasoning.takeIf { it.isNotBlank() }
val finalSources = assistantMsg.sources.takeIf { it.isNotEmpty() }
val userSave = SaveMessage(id = userMsg.wireId, parentId = userParentId, role = "user", content = "", attachments = listOf(att))
val assistantSave = SaveMessage(id = assistantMsg.wireId, parentId = userMsg.wireId, role = "assistant", content = assistantMsg.content, model = cfg.model, reasoning = assistantMsg.reasoning.takeIf { it.isNotBlank() }, sources = assistantMsg.sources.takeIf { it.isNotEmpty() }?.map { dev.kaizen.app.net.SearchSource(it.title, it.url, it.snippet) })
KaizenApi.saveMessages(cfg.baseUrl, cfg.token, cid, listOf(userSave, assistantSave))
val assistantSave = SaveMessage(id = assistantWireId, parentId = userMsg.wireId, role = "assistant", content = finalContent, model = cfg.model, reasoning = finalReasoning, sources = finalSources?.map { dev.kaizen.app.net.SearchSource(it.title, it.url, it.snippet) })
val saved = KaizenApi.saveMessages(cfg.baseUrl, cfg.token, cid, listOf(userSave, assistantSave))
if (saved) {
val msgCount = messages.size
chat.messageRepo.cacheMessages(cid, listOf(
dev.kaizen.app.db.MessageEntity(
id = userMsg.wireId, conversationId = cid,
role = "user", content = "",
attachments = listOf(att), sortOrder = msgCount - 2,
),
dev.kaizen.app.db.MessageEntity(
id = assistantWireId, conversationId = cid,
role = "assistant", content = finalContent,
attachments = emptyList(), sortOrder = msgCount - 1,
reasoning = finalReasoning,
sources = finalSources?.map { dev.kaizen.app.net.SearchSource(it.title, it.url, it.snippet) } ?: emptyList(),
),
))
}
if (convIdDeferred != null) {
chat.conversationRepo.insertOptimistic(cid, "(Voice)")
val title = KaizenApi.generateTitle(cfg.baseUrl, cfg.token, cid)

View file

@ -20,6 +20,7 @@
<string name="chat_open_menu">Open menu</string>
<string name="chat_stop">Stop response</string>
<string name="chat_recording">Recording …</string>
<string name="chat_voice_message">Voice message</string>
<string name="chat_mic_permission">Microphone access required</string>
<string name="chat_read_aloud">Read aloud</string>
<string name="chat_tts_error">Read aloud failed</string>

View file

@ -22,6 +22,7 @@
<string name="chat_open_menu">Menü öffnen</string>
<string name="chat_stop">Antwort stoppen</string>
<string name="chat_recording">Aufnahme läuft …</string>
<string name="chat_voice_message">Sprachnachricht</string>
<string name="chat_mic_permission">Mikrofonzugriff wird benötigt</string>
<string name="chat_read_aloud">Vorlesen</string>
<string name="chat_tts_error">Vorlesen fehlgeschlagen</string>