Compare commits

...

2 commits

Author SHA1 Message Date
Bruno Deanoz
b2df7e9330 feat: modern message action icons — larger, distinctive
Icons swapped to more distinctive Lucide variants:
- Copy → ClipboardCopy (clipboard with arrow, instantly recognizable)
- Edit → SquarePen (filled square with pen, modern)
- Regenerate → RotateCcw (single-arrow rotate, cleaner than RefreshCw)
- Delete/Volume stay (already good)

Size: 36dp touch target, 20dp icons (was 32/18). Tint alpha 0.50→0.55
for better visibility. Added ClipboardCopy, SquarePen, RotateCcw, PenLine
to KaizenIcons registry.
2026-06-23 01:33:59 +02:00
Bruno Deanoz
8f72afd714 feat: edit and resend user messages
Pencil icon on user messages (next to copy, before delete). Tap removes
the edited message and all messages after it from the UI + server, then
puts the text back into the input field for editing and resending.

Only shown on user messages with text content (not voice-only), and only
when not currently streaming.
2026-06-23 01:30:52 +02:00
3 changed files with 38 additions and 10 deletions

View file

@ -447,6 +447,7 @@ fun MessageRow(
onCopy: (String) -> Unit = {},
onDelete: ((String) -> Unit)? = null,
onRegenerate: ((String) -> Unit)? = null,
onEdit: ((String, String) -> Unit)? = null,
onReadAloud: ((String) -> Unit)? = null,
isPlayingTts: Boolean = false,
) {
@ -530,11 +531,11 @@ fun MessageRow(
if (!message.streaming) {
Row(
Modifier.padding(top = 6.dp, end = 4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
MessageActionButton(
icon = if (copied) KaizenIcons.Check else KaizenIcons.Copy,
tint = if (copied) Color(0xFF10B981) else cs.onSurfaceVariant.copy(alpha = 0.50f),
icon = if (copied) KaizenIcons.Check else KaizenIcons.ClipboardCopy,
tint = if (copied) Color(0xFF10B981) else cs.onSurfaceVariant.copy(alpha = 0.55f),
) {
haptics.tick()
copied = true
@ -544,6 +545,13 @@ fun MessageRow(
copied = false
}
}
if (message.content.isNotBlank()) {
onEdit?.let { edit ->
MessageActionButton(KaizenIcons.SquarePen, cs.onSurfaceVariant.copy(alpha = 0.55f)) {
haptics.tick(); edit(message.wireId, message.content)
}
}
}
onDelete?.let { del ->
MessageActionButton(KaizenIcons.Trash, Color(0xFFEF4444).copy(alpha = 0.70f)) {
haptics.tick(); del(message.wireId)
@ -583,11 +591,11 @@ fun MessageRow(
if (!message.streaming && !message.thinking && message.content.isNotBlank()) {
Row(
Modifier.padding(start = 38.dp, top = 6.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
MessageActionButton(
icon = if (copied) KaizenIcons.Check else KaizenIcons.Copy,
tint = if (copied) Color(0xFF10B981) else cs.onSurfaceVariant.copy(alpha = 0.50f),
icon = if (copied) KaizenIcons.Check else KaizenIcons.ClipboardCopy,
tint = if (copied) Color(0xFF10B981) else cs.onSurfaceVariant.copy(alpha = 0.55f),
) {
haptics.tick()
copied = true
@ -600,13 +608,13 @@ fun MessageRow(
onReadAloud?.let { readAloud ->
MessageActionButton(
icon = KaizenIcons.Volume2,
tint = if (isPlayingTts) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.50f),
tint = if (isPlayingTts) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.55f),
) {
haptics.tick(); readAloud(message.content)
}
}
onRegenerate?.let { regen ->
MessageActionButton(KaizenIcons.RefreshCw, cs.onSurfaceVariant.copy(alpha = 0.50f)) {
MessageActionButton(KaizenIcons.RotateCcw, cs.onSurfaceVariant.copy(alpha = 0.55f)) {
haptics.tick(); regen(message.wireId)
}
}
@ -672,12 +680,12 @@ private fun VoiceWaveform(amplitude: Float) {
private fun MessageActionButton(icon: ImageVector, tint: Color, onClick: () -> Unit) {
Box(
Modifier
.size(32.dp)
.size(36.dp)
.clip(KaizenShapes.circle)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center,
) {
Icon(icon, null, tint = tint, modifier = Modifier.size(18.dp))
Icon(icon, null, tint = tint, modifier = Modifier.size(20.dp))
}
}

View file

@ -883,6 +883,22 @@ fun ChatScreen(
send(userContent)
}
} else null,
onEdit = if (message.role == Role.User && !isStreaming) { wireId, content ->
val idx = messages.indexOfFirst { it.wireId == wireId }
if (idx >= 0) {
val cfg = session.config ?: return@MessageRow
val toRemove = messages.subList(idx, messages.size).map { it.wireId }
messages.removeAll { it.wireId in toRemove }
if (conversationId != null) {
scope.launch {
toRemove.forEach { id ->
KaizenApi.deleteMessage(cfg.baseUrl, cfg.token, conversationId!!, id)
}
}
}
input = content
}
} else null,
onReadAloud = if (message.role == Role.Assistant && !message.streaming && message.content.isNotBlank()) { content ->
readAloud(content, message.id, message.wireId)
} else null,

View file

@ -49,13 +49,17 @@ object KaizenIcons {
// ── Conversation management ─────────────────────────────────────────────
val Check: ImageVector get() = Lucide.Check
val ClipboardCopy: ImageVector get() = Lucide.ClipboardCopy
val Copy: ImageVector get() = Lucide.Copy
val Lock: ImageVector get() = Lucide.Lock
val LockOpen: ImageVector get() = Lucide.LockOpen
val Pencil: ImageVector get() = Lucide.Pencil
val PenLine: ImageVector get() = Lucide.PenLine
val Pin: ImageVector get() = Lucide.Pin
val PinOff: ImageVector get() = Lucide.PinOff
val RefreshCw: ImageVector get() = Lucide.RefreshCw
val RotateCcw: ImageVector get() = Lucide.RotateCcw
val SquarePen: ImageVector get() = Lucide.SquarePen
val Star: ImageVector get() = Lucide.Star
val StarOff: ImageVector get() = Lucide.StarOff
val Trash: ImageVector get() = Lucide.Trash2