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
+51 -4
View File
@@ -67,7 +67,13 @@
padding: 12px; margin-bottom: 8px; display: flex; flex-direction: column; gap: 8px; }
.chat-msg { padding: 10px 14px; border-radius: 14px; font-size: 14px; line-height: 1.5;
word-wrap: break-word; max-width: 80%; white-space: pre-wrap;
box-shadow: 0 1px 2px rgba(0,0,0,0.4); }
box-shadow: 0 1px 2px rgba(0,0,0,0.4); position: relative; }
.chat-msg .bubble-trash { position:absolute; top:4px; right:6px; background:rgba(255,59,48,0.15);
color:#FF6B6B; border:none; border-radius:50%; width:22px; height:22px;
font-size:12px; line-height:18px; padding:0; cursor:pointer; opacity:0;
transition:opacity 0.15s; }
.chat-msg:hover .bubble-trash { opacity: 1; }
.chat-msg .bubble-trash:hover { background:#FF3B30; color:#fff; }
.chat-msg.sent { background: #0096FF; color: #fff; align-self: flex-end;
border-bottom-right-radius: 4px; }
.chat-msg.received { background: #1E1E2E; color: #E8E8F0; align-self: flex-start;
@@ -1378,7 +1384,23 @@
chatType = 'sent';
label = `via RVS (${sender})`;
}
addChat(chatType, p.text || '?', label, { location: p.location });
addChat(chatType, p.text || '?', label, {
location: p.location,
ttsText: p.ttsText,
backupTs: p.backupTs,
});
return;
}
if (msg.type === 'chat_message_deleted') {
// Bridge meldet: Bubble wurde aus chat_backup + Brain entfernt.
// Bubble lokal entfernen (data-ts-Match in beiden Chat-Boxen).
const ts = msg.payload?.ts;
if (!ts) return;
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
const el = box.querySelector(`.chat-msg[data-ts="${ts}"]`);
if (el) el.remove();
}
return;
}
if (msg.type === 'proxy_result') {
@@ -1453,6 +1475,7 @@
}
const el = document.createElement('div');
el.className = `chat-msg ${m.type}`;
if (m.ts) el.dataset.ts = String(m.ts);
// [FILE: ...]-Marker rausfiltern (gleicher Filter wie addChat)
const cleaned = (m.text || '').replace(/\[FILE:\s*\/shared\/uploads\/[^\]]+\]/gi, '').replace(/\n{3,}/g, '\n\n').trim();
const escaped = escapeHtml(cleaned);
@@ -1463,7 +1486,10 @@
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
});
const time = m.ts ? new Date(m.ts).toLocaleTimeString('de-DE') : '?';
el.innerHTML = `${linked}<div class="meta">${escapeHtml(m.meta)}${time}</div>`;
const trashBtn = m.ts
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${m.ts})">🗑</button>`
: '';
el.innerHTML = `${trashBtn}${linked}<div class="meta">${escapeHtml(m.meta)}${time}</div>`;
chatBox.appendChild(el);
}
chatBox.scrollTop = chatBox.scrollHeight;
@@ -1492,6 +1518,22 @@
}
}
/** Loescht eine einzelne Chat-Bubble (mit Rueckfrage).
* Backend (Bridge) raeumt chat_backup.jsonl + Brain-Conversation
* und broadcastet danach chat_message_deleted — wir entfernen die
* Bubble lokal erst dann, nicht optimistisch. */
function deleteDiagBubble(ts) {
if (!ts) return;
let preview = '';
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
if (!box) continue;
const el = box.querySelector(`.chat-msg[data-ts="${ts}"]`);
if (el) { preview = (el.textContent || '').slice(0, 80); break; }
}
if (!confirm(`Diese Bubble wirklich loeschen?\n\n"${preview}…"\n\nWird aus chat_backup, Brain-Konversation und allen Clients entfernt.`)) return;
send({ action: 'delete_chat_message', ts });
}
function sendDiagAttachments() {
// Alle pending Dateien an RVS senden
for (const f of diagPendingFiles) {
@@ -1781,7 +1823,11 @@
gpsBlock = `<div style="margin-top:6px;padding:4px 8px;background:rgba(52,199,89,0.08);border-left:2px solid #34C759;font-size:11px;color:#88BB99;"><span style="color:#34C759;font-weight:bold;">📍 GPS:</span> <a href="${mapLink}" target="_blank" rel="noopener" style="color:#88BB99;text-decoration:underline;">${lat}, ${lon}</a></div>`;
}
}
const html = `${linked}${ttsBlock}${gpsBlock}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`;
const backupTs = options && options.backupTs;
const trashBtn = backupTs
? `<button class="bubble-trash" title="Diese Bubble loeschen" onclick="deleteDiagBubble(${backupTs})">🗑</button>`
: '';
const html = `${trashBtn}${linked}${ttsBlock}${gpsBlock}<div class="meta">${escapeHtml(meta)}${new Date().toLocaleTimeString('de-DE')}</div>`;
// Thinking-Indikator ausblenden bei neuer Nachricht
updateThinkingIndicator({ activity: 'idle' });
@@ -1791,6 +1837,7 @@
if (!box) continue;
const el = document.createElement('div');
el.className = `chat-msg ${type}`;
if (backupTs) el.dataset.ts = String(backupTs);
el.innerHTML = html;
box.appendChild(el);
box.scrollTop = box.scrollHeight;
+17
View File
@@ -617,6 +617,12 @@ function connectRVS(forcePlain) {
// Mode-Broadcast von der Bridge → an Browser-Clients weiterreichen
log("info", "rvs", `Mode-Broadcast: ${msg.payload?.mode} (${msg.payload?.name})`);
broadcast({ type: "mode", payload: msg.payload });
} else if (msg.type === "chat_message_deleted") {
// Bridge meldet: Bubble wurde aus chat_backup + Brain entfernt.
// An Browser-Clients weiterreichen damit sie die Bubble lokal entfernen.
const ts = msg.payload?.ts;
log("info", "rvs", `chat_message_deleted ts=${ts}`);
broadcast({ type: "chat_message_deleted", payload: msg.payload });
} else if (msg.type === "voice_ready") {
// XTTS-Bridge meldet Stimme fertig geladen → an Browser durchreichen
const v = msg.payload?.voice || "";
@@ -1835,6 +1841,17 @@ wss.on("connection", (ws) => {
// Weiterleiten an XTTS-Bridge, die antwortet mit neuer Liste
sendToRVS_raw({ type: "xtts_delete_voice", payload: { name: msg.name }, timestamp: Date.now() });
log("info", "server", `Voice-Delete '${msg.name}' an XTTS-Bridge gesendet`);
} else if (msg.action === "delete_chat_message") {
// Bubble loeschen — Bridge raeumt chat_backup.jsonl + Brain-conversation
// + broadcastet chat_message_deleted via RVS.
const ts = Number(msg.ts);
if (!Number.isFinite(ts)) {
ws.send(JSON.stringify({ type: "log", level: "error", source: "server",
message: `delete_chat_message: ungueltiges ts=${msg.ts}` }));
return;
}
sendToRVS_raw({ type: "delete_message_request", payload: { ts }, timestamp: Date.now() });
log("info", "server", `delete_message_request ts=${ts} an Bridge gesendet`);
} else if (msg.action === "set_mode") {
// Mode-Wechsel → Bridge bearbeitet und broadcastet an alle Clients
sendToRVS_raw({ type: "mode", payload: { mode: msg.mode }, timestamp: Date.now() });