feat(chat): Muelltonne pro Bubble — gezielt eine Nachricht loeschen

Stefan kann jetzt einzelne Chat-Bubbles loeschen (mit Rueckfrage).
Die Bubble verschwindet aus chat_backup.jsonl (Bridge), Brain-
Conversation (rolling window + jsonl) und allen Clients (App +
Diagnostic). Genauso wichtig fuer ARIA: der gloeschte Turn ist im
naechsten Chat-Prompt nicht mehr im Window.

Pipeline:
  UI 🗑 + confirm
  → RVS delete_message_request {ts}
  → Bridge._delete_chat_message:
      - chat_backup.jsonl Zeile mit ts entfernen (atomar via tmp+rename)
      - Brain POST /conversation/delete-turn (role+content match)
      - RVS broadcast chat_message_deleted {ts}
  → App + Diagnostic entfernen Bubble lokal per ts-Match

Backend-Aenderungen:
- aria-brain/conversation.py: remove_by_match(role, content, ts_hint)
  + _rewrite_file (atomar). Match nahester Turn bei mehrfach gleichem
  content.
- aria-brain/main.py: POST /conversation/delete-turn (POST statt DELETE
  weil FastAPI keine Bodys auf DELETE erlaubt)
- bridge/aria_bridge.py: HTTP-Listener /internal/delete-chat-message
  + RVS-Handler delete_message_request. _append_chat_backup gibt jetzt
  ts zurueck, _process_core_response packt backupTs ins chat-Event.
- rvs/server.js: ALLOWED_TYPES um delete_message_request +
  chat_message_deleted erweitert.
- diagnostic/server.js: delete_chat_message-Action + chat_message_deleted
  Relay zum Browser.

Frontend-Aenderungen:
- diagnostic/index.html: 🗑 erscheint on-hover in Bubbles mit data-ts,
  confirm()-Dialog, addChat + chat_history setzen data-ts. WS-Listener
  fuer chat_message_deleted entfernt Bubble per data-ts.
- android/ChatScreen.tsx: backupTs in ChatMessage, Muelltonne-Button
  unten rechts in jeder Bubble, Alert-confirm, RVS-Listener fuer
  chat_message_deleted entfernt aus messages-State.

Live-User-Bubbles (sofort gerendert vom eigenen Send) haben noch
keinen backupTs bis der Bridge-Roundtrip durch ist — die Muelltonne
erscheint dort erst nach kurzer Verzoegerung / Reload. Folgekommit
kann das polieren wenn noetig.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 16:42:20 +02:00
parent daf0d44dd7
commit 3f2499b528
7 changed files with 327 additions and 8 deletions
+62
View File
@@ -87,6 +87,11 @@ interface ChatMessage {
fires_at?: string;
condition?: string;
};
/** Backup-Timestamp aus chat_backup.jsonl auf dem Bridge — Voraussetzung
* zum Loeschen der Bubble via Muelltonne. Lokale Bubbles ohne backupTs
* sind noch nicht persistiert (kurzer Race) — Muelltonne erscheint erst
* wenn das chat_backup-Event vom Bridge zurueck kommt. */
backupTs?: number;
}
// --- Konstanten ---
@@ -415,6 +420,16 @@ const ChatScreen: React.FC = () => {
return;
}
// chat_message_deleted: Bridge hat eine Bubble aus chat_backup + Brain
// entfernt. Wir loeschen sie lokal per backupTs-Match.
if (message.type === 'chat_message_deleted') {
const ts = (message.payload || {}).ts;
if (typeof ts !== 'number') return;
console.log(`[Chat] chat_message_deleted ts=${ts}`);
setMessages(prev => prev.filter(m => m.backupTs !== ts));
return;
}
// chat_history_response: kompletter Server-Stand. App ersetzt ihre
// persistierte Chat-History damit. Lokal-only Bubbles (laufende
// Voice-Aufnahmen ohne STT-Result, Skill-Created-Events ohne
@@ -440,6 +455,7 @@ const ChatScreen: React.FC = () => {
text: m.text || '',
timestamp: m.ts || Date.now(),
attachments: attachments.length ? attachments : undefined,
backupTs: typeof m.ts === 'number' ? m.ts : undefined,
};
});
const maxTs = incoming.reduce((mx: number, m: any) => Math.max(mx, m.ts || 0), 0);
@@ -654,6 +670,7 @@ const ChatScreen: React.FC = () => {
timestamp: ts,
attachments: message.payload.attachments as Attachment[] | undefined,
messageId: (message.payload.messageId as string) || undefined,
backupTs: (message.payload.backupTs as number) || undefined,
};
return capMessages([...prev, ariaMsg]);
});
@@ -1386,11 +1403,41 @@ const ChatScreen: React.FC = () => {
<Text style={styles.playButtonText}>{'\uD83D\uDD0A'}</Text>
</TouchableOpacity>
)}
{item.backupTs ? (
<TouchableOpacity
style={styles.bubbleTrash}
hitSlop={{top:6,bottom:6,left:6,right:6}}
onPress={() => confirmDeleteBubble(item)}
>
<Text style={styles.bubbleTrashIcon}>{'🗑'}</Text>
</TouchableOpacity>
) : null}
<Text style={styles.timestamp}>{time}</Text>
</View>
);
};
const confirmDeleteBubble = (item: ChatMessage) => {
const ts = item.backupTs;
if (!ts) return;
const preview = (item.text || '').slice(0, 80) || '(leere Bubble)';
Alert.alert(
'Bubble loeschen?',
`"${preview}${item.text && item.text.length > 80 ? '…' : ''}"\n\nWird aus chat_backup, Brain-Konversation und allen Clients entfernt.`,
[
{ text: 'Abbrechen', style: 'cancel' },
{
text: 'Loeschen',
style: 'destructive',
onPress: () => {
console.log(`[Chat] delete_message_request ts=${ts}`);
rvs.send('delete_message_request' as any, { ts });
},
},
],
);
};
const connectionDotColor =
connectionState === 'connected' ? '#34C759' :
connectionState === 'connecting' ? '#FFD60A' : '#FF3B30';
@@ -1967,6 +2014,21 @@ const styles = StyleSheet.create({
playButtonText: {
fontSize: 16,
},
bubbleTrash: {
position: 'absolute',
top: 4,
right: 6,
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: 'rgba(255,59,48,0.18)',
alignItems: 'center',
justifyContent: 'center',
},
bubbleTrashIcon: {
fontSize: 12,
color: '#FF6B6B',
},
fullscreenOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.95)',