From dc2f4eb6d26022b41fd47cf1ea643aeb8c36254f Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 11 May 2026 22:24:06 +0200 Subject: [PATCH] feat(app): Datei-Manager, Skill-Created-Bubble, Zoom rewriten, Repair-Cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drei groessere Aenderungen in der Android-App. Datei-Manager (Settings → Dateien) - Neuer Eintrag im Settings-Menue → Modal mit Liste - Suche + Filter (Alle / Von ARIA / Vom User) - Per Eintrag: ARIA/USER-Badge, Groesse, Datum, Loeschen-Button - file_list_request via RVS → Bridge → Diagnostic-HTTP → response - file_delete_request loescht serverseitig, file_deleted-Event aktualisiert ALLE Chat-Bubbles (Attachment.deleted = true mit Strikethrough-Name + 🗑️-Icon) Skill-Created-Bubble - Neuer ChatMessage.skillCreated Typ — eigenes Render mit gelbem Border, Skill-Name, Beschreibung, Execution-Mode, Active-Status - Falls Skill-Setup fehlschlug: ⚠ Setup-Fehler-Zeile direkt in der Bubble - Stefan sieht in der Chat-History immer wenn ARIA selbst einen Skill angelegt hat — Transparenz statt schweigend im Hintergrund Pinch-Zoom rewriten (ZoomableImage.tsx) - Multi-Touch-Race-Bugs in der alten Variante geloest: * Touch-Count jetzt aus e.nativeEvent.touches.length statt gestureState.numberActiveTouches (war nicht zuverlaessig) * Re-Snapshot bei JEDEM Finger-Wechsel (1↔2) → keine Spruenge mehr * Doppel-Tap via onPanResponderRelease + Bewegungs-Cap * pointerEvents="none" auf Image-Wrapper → Touches gehen garantiert an PanResponder-View * collapsable={false} verhindert Android-View-Flattening - 2-Finger-Pinch 1x..5x, simultaner Pan via Focal, 1-Finger-Pan nur wenn gezoomt (>1.02x), Doppel-Tap toggelt 1x↔2.5x App SettingsScreen Repair-Section - aria-core-spezifische Buttons raus: 🔧 Reparieren, 🚨 ARIA hart neu, 🧹 Konversation komprimieren (OpenClaw ist abgerissen) - Stattdessen generischer container_restart fuer aria-bridge/brain/qdrant - Repair-Buttons aus der "ARIA denkt..."-Bubble entfernt (nur Abbrechen) ChatScreen - skill_created und file_deleted Handler im RVS-Message-Switch - file_list_response (Modal-State liegt in SettingsScreen) Co-Authored-By: Claude Opus 4.7 (1M context) --- android/src/components/ZoomableImage.tsx | 300 +++++++++++++++-------- android/src/screens/ChatScreen.tsx | 93 +++++-- android/src/screens/SettingsScreen.tsx | 254 ++++++++++++++----- 3 files changed, 457 insertions(+), 190 deletions(-) diff --git a/android/src/components/ZoomableImage.tsx b/android/src/components/ZoomableImage.tsx index 233bbca..9dcfc09 100644 --- a/android/src/components/ZoomableImage.tsx +++ b/android/src/components/ZoomableImage.tsx @@ -1,17 +1,34 @@ /** - * ZoomableImage — Pinch-to-Zoom + Pan fuer das Vollbild-Modal. + * ZoomableImage — Pinch-to-Zoom + Pan fuers Vollbild-Modal. * - * Reine React-Native-Implementation ohne externe Lib: - * - 1 Finger: Pan wenn schon gezoomt - * - 2 Finger: Pinch fuer Zoom + Pan - * - Doppel-Tap: Toggle 1x ↔ 2.5x Zoom + * Reine RN-Implementation, ohne react-native-gesture-handler. * - * Scale wird auf [1, 5] gecapped, Translation auf das verfuegbare - * Image-Volumen (kein Out-of-bounds-Pan). + * - 2 Finger: Pinch (Zoom 1x..5x) + simultaner Pan via Focal-Punkt + * - 1 Finger: Pan wenn schon gezoomt (>1.02x) + * - Doppel-Tap (<300ms zw. zwei Single-Taps): Toggle 1x ↔ 2.5x + * + * Implementierungs-Hinweise zur alten Version (warum's nicht ging): + * - `gestureState.numberActiveTouches` ist nicht zuverlaessig direkt + * nach onPanResponderGrant. Wir lesen Finger-Anzahl jetzt + * ausschliesslich aus `e.nativeEvent.touches.length`. + * - Beim Wechsel von 2 → 1 Fingern bleib die Pinch-Referenz haengen. + * Jetzt: bei jedem Finger-Wechsel re-snapshotten wir die Geste. + * - Animated.Image bekommt jetzt pointerEvents="none" damit der View + * GARANTIERT die Touches abbekommt. + * - useNativeDriver ist bewusst AUS — sonst koennen wir setValue() + * nicht synchron mit dem Pan-Responder zusammen nutzen. */ -import React, { useRef } from 'react'; -import { Animated, PanResponder, View, StyleSheet, ImageStyle, StyleProp } from 'react-native'; +import React, { useMemo, useRef } from 'react'; +import { + Animated, + PanResponder, + GestureResponderEvent, + ImageStyle, + StyleProp, + StyleSheet, + View, +} from 'react-native'; interface Props { uri: string; @@ -20,121 +37,186 @@ interface Props { style?: StyleProp; } +const MIN_SCALE = 1; +const MAX_SCALE = 5; +const DOUBLE_TAP_MS = 300; +const DOUBLE_TAP_DIST = 30; // Bewegung max. damit ein Tap als Tap gilt +const PAN_SLOP_AT_SCALE_1 = 4; // Mikro-Movement nicht als Pan werten + const ZoomableImage: React.FC = ({ uri, containerWidth, containerHeight, style }) => { + // Animated-Werte fuer die Render-Transformation const scale = useRef(new Animated.Value(1)).current; - const translateX = useRef(new Animated.Value(0)).current; - const translateY = useRef(new Animated.Value(0)).current; + const tx = useRef(new Animated.Value(0)).current; + const ty = useRef(new Animated.Value(0)).current; - // Aktuelle Werte (Animated.Value lesen ist async, wir tracken parallel) - const current = useRef({ scale: 1, x: 0, y: 0 }).current; - // State beim Geste-Start (touchStart-Snapshot) - const start = useRef({ scale: 1, x: 0, y: 0, distance: 0, focalX: 0, focalY: 0 }).current; - // Doppel-Tap-Erkennung - const lastTapAt = useRef(0); + // Logische Zustaende — wir lesen Animated.Value nicht zurueck (waere async) + const view = useRef({ scale: 1, x: 0, y: 0 }).current; - const distance = (touches: any[]) => { - const [a, b] = touches; - return Math.hypot(a.pageX - b.pageX, a.pageY - b.pageY); + // Geste-Snapshot: was war zu Beginn dieser Geste-Phase + const gesture = useRef({ + fingers: 0, // aktuelle Finger-Anzahl + startScale: 1, + startX: 0, + startY: 0, + startDist: 0, // Pinch-Referenz-Distanz + startFocalX: 0, + startFocalY: 0, + movedSinceTouch: 0, // fuer Tap-Erkennung + touchStartedAt: 0, + touchStartX: 0, + touchStartY: 0, + }).current; + + // Doppel-Tap + const lastTap = useRef({ at: 0, x: 0, y: 0 }); + + const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v)); + + const applyClamped = (s: number, x: number, y: number) => { + const ns = clamp(s, MIN_SCALE, MAX_SCALE); + // Translation auf das verfuegbare Volumen begrenzen + const maxX = Math.max(0, (containerWidth * ns - containerWidth) / 2); + const maxY = Math.max(0, (containerHeight * ns - containerHeight) / 2); + const nx = clamp(x, -maxX, maxX); + const ny = clamp(y, -maxY, maxY); + view.scale = ns; + view.x = nx; + view.y = ny; + scale.setValue(ns); + tx.setValue(nx); + ty.setValue(ny); }; - const focal = (touches: any[]) => { - const [a, b] = touches; - return { x: (a.pageX + b.pageX) / 2, y: (a.pageY + b.pageY) / 2 }; + const distance = (touches: any[]) => + Math.hypot(touches[0].pageX - touches[1].pageX, touches[0].pageY - touches[1].pageY); + + const focal = (touches: any[]) => ({ + x: (touches[0].pageX + touches[1].pageX) / 2, + y: (touches[0].pageY + touches[1].pageY) / 2, + }); + + // Snapshot vor jedem Phasenwechsel (1↔2 Finger) — verhindert Spruenge + const snapshot = (touches: any[]) => { + gesture.startScale = view.scale; + gesture.startX = view.x; + gesture.startY = view.y; + if (touches.length >= 2) { + gesture.startDist = distance(touches); + const f = focal(touches); + gesture.startFocalX = f.x; + gesture.startFocalY = f.y; + } else if (touches.length === 1) { + gesture.startDist = 0; + gesture.startFocalX = touches[0].pageX; + gesture.startFocalY = touches[0].pageY; + } }; - const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(max, v)); + const responder = useMemo( + () => + PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, - const applyAndClamp = (newScale: number, newX: number, newY: number) => { - const s = clamp(newScale, 1, 5); - // Maximal-Translation: (imgSize * scale - imgSize) / 2 - const maxX = Math.max(0, (containerWidth * s - containerWidth) / 2); - const maxY = Math.max(0, (containerHeight * s - containerHeight) / 2); - const x = clamp(newX, -maxX, maxX); - const y = clamp(newY, -maxY, maxY); - current.scale = s; - current.x = x; - current.y = y; - scale.setValue(s); - translateX.setValue(x); - translateY.setValue(y); - }; + onPanResponderGrant: (e: GestureResponderEvent) => { + const touches = e.nativeEvent.touches as any[]; + gesture.fingers = touches.length; + gesture.movedSinceTouch = 0; + gesture.touchStartedAt = Date.now(); + gesture.touchStartX = touches[0]?.pageX ?? 0; + gesture.touchStartY = touches[0]?.pageY ?? 0; + snapshot(touches); + }, - const responder = useRef( - PanResponder.create({ - onStartShouldSetPanResponder: () => true, - onMoveShouldSetPanResponder: () => true, - onPanResponderGrant: (_e, gestureState) => { - const touches = gestureState as any; - const t = touches.numberActiveTouches || 1; - // Doppel-Tap-Erkennung (nur bei 1 Finger) - if (t === 1) { - const now = Date.now(); - if (now - lastTapAt.current < 280) { - // Doppel-Tap → Zoom-Toggle - if (current.scale > 1.1) { - applyAndClamp(1, 0, 0); - } else { - applyAndClamp(2.5, 0, 0); + onPanResponderMove: (e: GestureResponderEvent, _gs) => { + const touches = e.nativeEvent.touches as any[]; + + // Phasenwechsel? → Re-Snapshot, damit nicht gesprungen wird + if (touches.length !== gesture.fingers) { + gesture.fingers = touches.length; + snapshot(touches); + return; + } + + gesture.movedSinceTouch += 1; + + if (touches.length >= 2) { + // Pinch + Pan via Focal + const d = distance(touches); + if (gesture.startDist === 0) { + // Sicherheitsnetz falls Snapshot gemissed wurde + snapshot(touches); + return; } - lastTapAt.current = 0; - return; - } - lastTapAt.current = now; - } - start.scale = current.scale; - start.x = current.x; - start.y = current.y; - }, - onPanResponderMove: (e, gestureState) => { - const touches = e.nativeEvent.touches; - if (touches.length >= 2) { - // Pinch + Pan - if (start.distance === 0) { - // Initialisiere die Pinch-Referenz beim Uebergang 1→2 Finger - start.distance = distance(touches); + const factor = d / gesture.startDist; const f = focal(touches); - start.focalX = f.x; - start.focalY = f.y; - start.scale = current.scale; - start.x = current.x; - start.y = current.y; - return; + const newScale = clamp(gesture.startScale * factor, MIN_SCALE, MAX_SCALE); + // Focal-basierter Pan: zoomt um den Mittelpunkt der zwei Finger + const newX = gesture.startX + (f.x - gesture.startFocalX); + const newY = gesture.startY + (f.y - gesture.startFocalY); + applyClamped(newScale, newX, newY); + } else if (touches.length === 1 && view.scale > 1.02) { + const dx = touches[0].pageX - gesture.startFocalX; + const dy = touches[0].pageY - gesture.startFocalY; + if (Math.abs(dx) < PAN_SLOP_AT_SCALE_1 && Math.abs(dy) < PAN_SLOP_AT_SCALE_1) return; + applyClamped(view.scale, gesture.startX + dx, gesture.startY + dy); } - const newDistance = distance(touches); - const newFocal = focal(touches); - const scaleFactor = newDistance / start.distance; - const newScale = clamp(start.scale * scaleFactor, 1, 5); - // Pan-Anteil aus Focal-Bewegung - const newX = start.x + (newFocal.x - start.focalX); - const newY = start.y + (newFocal.y - start.focalY); - applyAndClamp(newScale, newX, newY); - } else if (touches.length === 1 && current.scale > 1.05) { - // Single-Finger-Pan nur wenn gezoomt - start.distance = 0; // Reset Pinch-Tracking - applyAndClamp(current.scale, start.x + gestureState.dx, start.y + gestureState.dy); - } - }, - onPanResponderRelease: () => { start.distance = 0; }, - onPanResponderTerminate: () => { start.distance = 0; }, - }), - ).current; + }, + + onPanResponderRelease: (e: GestureResponderEvent) => { + const elapsed = Date.now() - gesture.touchStartedAt; + const dx = (e.nativeEvent.changedTouches?.[0]?.pageX ?? gesture.touchStartX) - gesture.touchStartX; + const dy = (e.nativeEvent.changedTouches?.[0]?.pageY ?? gesture.touchStartY) - gesture.touchStartY; + const wasTap = + elapsed < 280 && + Math.abs(dx) < DOUBLE_TAP_DIST && + Math.abs(dy) < DOUBLE_TAP_DIST; + if (wasTap) { + const now = Date.now(); + if (now - lastTap.current.at < DOUBLE_TAP_MS) { + // Doppel-Tap → Zoom-Toggle + if (view.scale > 1.1) { + applyClamped(1, 0, 0); + } else { + applyClamped(2.5, 0, 0); + } + lastTap.current = { at: 0, x: 0, y: 0 }; + } else { + lastTap.current = { at: now, x: gesture.touchStartX, y: gesture.touchStartY }; + } + } + gesture.fingers = 0; + gesture.startDist = 0; + }, + + onPanResponderTerminate: () => { + gesture.fingers = 0; + gesture.startDist = 0; + }, + }), + [], + ); return ( - - + + + + ); }; diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index e60ccf3..62d562a 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -54,6 +54,7 @@ interface Attachment { uri?: string; // Lokaler Pfad (file://) fuer Anzeige mimeType?: string; serverPath?: string; // Pfad auf dem Server (/shared/uploads/...) fuer Re-Download + deleted?: boolean; // Datei wurde nachtraeglich geloescht (Diagnostic-Manager) } interface ChatMessage { @@ -70,6 +71,14 @@ interface ChatMessage { * gespiegelt damit wir die EXAKT richtige Placeholder-Bubble ersetzen, * auch wenn mehrere Aufnahmen parallel offen sind. */ audioRequestId?: string; + /** Skill-Created-Bubble: ARIA hat einen neuen Skill angelegt */ + skillCreated?: { + name: string; + description: string; + execution: string; + active: boolean; + setupError?: string; + }; } // --- Konstanten --- @@ -386,6 +395,41 @@ const ChatScreen: React.FC = () => { return; } + // skill_created: ARIA hat einen neuen Skill angelegt → eigene Bubble + if (message.type === 'skill_created') { + const p = (message.payload || {}) as any; + const skillMsg: ChatMessage = { + id: nextId(), + sender: 'aria', + text: '', + timestamp: Date.now(), + skillCreated: { + name: String(p.name || '(unbenannt)'), + description: String(p.description || ''), + execution: String(p.execution || 'bash'), + active: p.active !== false, + setupError: p.setup_error ? String(p.setup_error) : undefined, + }, + }; + setMessages(prev => capMessages([...prev, skillMsg])); + return; + } + + // file_deleted: Datei wurde geloescht (vom Diagnostic User) → Bubble updaten + if (message.type === 'file_deleted') { + const p = (message.payload?.path as string) || ''; + if (!p) return; + setMessages(prev => prev.map(m => ({ + ...m, + attachments: m.attachments?.map(a => + a.serverPath === p ? { ...a, deleted: true } : a + ), + }))); + return; + } + + // file_list_response: wird vom Datei-Manager im SettingsScreen verarbeitet. + // file_from_aria: ARIA hat eine Datei rausgegeben → als ARIA-Bubble anzeigen if (message.type === 'file_from_aria') { const p = message.payload || {}; @@ -1038,12 +1082,41 @@ const ChatScreen: React.FC = () => { minute: '2-digit', }); + // Spezial-Bubble: ARIA hat einen Skill erstellt + if (item.skillCreated) { + const s = item.skillCreated; + return ( + + + {'🛠 ARIA hat einen neuen Skill erstellt'} + + + {s.name} + {` (${s.execution}, ${s.active ? 'aktiv' : 'deaktiviert'})`} + + {s.description} + {s.setupError && ( + + {'⚠ Setup-Fehler: '}{s.setupError.slice(0, 200)} + + )} + ARIA-Skill · {time} + + ); + } + return ( {/* Anhang-Vorschau */} {item.attachments?.map((att, idx) => ( - {att.type === 'image' && att.uri ? ( + {att.deleted ? ( + + {'🗑️'} + {att.name} + (geloescht) + + ) : att.type === 'image' && att.uri ? ( setFullscreenImage(att.uri || null)} @@ -1253,24 +1326,6 @@ const ChatScreen: React.FC = () => { : '\uD83D\uDCAD ARIA denkt...'} - rvs.send('doctor_fix' as any, {})}> - {'🔧'} - - { - Alert.alert( - 'ARIA hart neu starten?', - 'Container-Restart (~15s). Laufende Anfragen gehen verloren.', - [ - { text: 'Abbrechen', style: 'cancel' }, - { text: 'Neu starten', style: 'destructive', onPress: () => rvs.send('aria_restart' as any, {}) }, - ], - ); - }} - > - {'🚨'} - Abbrechen diff --git a/android/src/screens/SettingsScreen.tsx b/android/src/screens/SettingsScreen.tsx index 7993421..411a6d4 100644 --- a/android/src/screens/SettingsScreen.tsx +++ b/android/src/screens/SettingsScreen.tsx @@ -98,6 +98,7 @@ const SETTINGS_SECTIONS = [ { id: 'wake_word', icon: '👂', label: 'Wake-Word', desc: 'Wake-Word-Auswahl' }, { id: 'voice_output', icon: '🔊', label: 'Sprachausgabe', desc: 'Stimmen, Pre-Roll, Geschwindigkeit' }, { id: 'storage', icon: '📁', label: 'Speicher', desc: 'Anhang-Speicherort, Auto-Download' }, + { id: 'files', icon: '📂', label: 'Dateien', desc: 'ARIA- und User-Dateien — anzeigen, löschen' }, { id: 'protocol', icon: '📜', label: 'Protokoll', desc: 'Privatsphaere, Backup' }, { id: 'about', icon: 'ℹ️', label: 'Ueber', desc: 'App-Version, Update' }, ] as const; @@ -147,6 +148,13 @@ const SettingsScreen: React.FC = () => { const [xttsVoice, setXttsVoice] = useState(''); const [loadingVoice, setLoadingVoice] = useState(null); const [availableVoices, setAvailableVoices] = useState>([]); + // Datei-Manager + const [fileManagerOpen, setFileManagerOpen] = useState(false); + const [fileManagerFiles, setFileManagerFiles] = useState>([]); + const [fileManagerLoading, setFileManagerLoading] = useState(false); + const [fileManagerError, setFileManagerError] = useState(''); + const [fileManagerSearch, setFileManagerSearch] = useState(''); + const [fileManagerFilter, setFileManagerFilter] = useState<'all' | 'aria' | 'user'>('all'); const [voiceCloneVisible, setVoiceCloneVisible] = useState(false); const [tempPath, setTempPath] = useState(''); // Sub-Screen Navigation: null = Hauptmenue, sonst eine der Section-IDs. @@ -371,6 +379,25 @@ const SettingsScreen: React.FC = () => { setAvailableVoices(voices); } + // Datei-Manager: Liste empfangen + if (message.type === ('file_list_response' as any)) { + const p: any = message.payload || {}; + if (p.ok) { + setFileManagerFiles(p.files || []); + } else { + setFileManagerError(p.error || 'Unbekannter Fehler'); + } + setFileManagerLoading(false); + } + + // Datei-Manager: Datei wurde geloescht (vom Diagnostic oder dieser App) + if (message.type === ('file_deleted' as any)) { + const p: any = message.payload || {}; + if (p.path) { + setFileManagerFiles(prev => prev.filter(f => f.path !== p.path)); + } + } + // Voice wurde gespeichert → Liste neu laden + ggf. auswaehlen if (message.type === ('xtts_voice_saved' as any)) { const name = (message.payload as any).name as string; @@ -564,6 +591,119 @@ const SettingsScreen: React.FC = () => { visible={voiceCloneVisible} onClose={() => setVoiceCloneVisible(false)} /> + {/* Datei-Manager Modal */} + setFileManagerOpen(false)} + > + + + setFileManagerOpen(false)} style={{padding:8}}> + + + Dateien + { + setFileManagerError(''); + setFileManagerLoading(true); + rvs.send('file_list_request' as any, {}); + }} + style={{padding:8}} + > + 🔄 + + + + + + {(['all','aria','user'] as const).map(f => ( + setFileManagerFilter(f)} + style={{ + paddingVertical:6, paddingHorizontal:12, borderRadius:14, + backgroundColor: fileManagerFilter === f ? '#0096FF' : '#1E1E2E', + }} + > + + {f === 'all' ? 'Alle' : f === 'aria' ? 'Von ARIA' : 'Von dir'} + + + ))} + + + {fileManagerLoading ? ( + Lade... + ) : fileManagerError ? ( + {fileManagerError} + ) : ( + + {(() => { + let files = fileManagerFiles; + if (fileManagerFilter === 'aria') files = files.filter(f => f.fromAria); + else if (fileManagerFilter === 'user') files = files.filter(f => !f.fromAria); + if (fileManagerSearch) { + const q = fileManagerSearch.toLowerCase(); + files = files.filter(f => f.name.toLowerCase().includes(q)); + } + if (!files.length) { + return Keine Dateien; + } + const fmtSize = (b: number) => b < 1024 ? `${b} B` : b < 1024*1024 ? `${(b/1024).toFixed(1)} KB` : `${(b/1024/1024).toFixed(1)} MB`; + return files.map(f => ( + + + + + + {f.fromAria ? 'ARIA' : 'USER'} + + + {f.name} + + + {fmtSize(f.size)} · {new Date(f.mtime).toLocaleString('de-DE')} + + + { + Alert.alert( + 'Datei löschen?', + `"${f.name}"\n\nIn allen Chat-Bubbles wird sie als gelöscht markiert.`, + [ + { text: 'Abbrechen', style: 'cancel' }, + { text: 'Löschen', style: 'destructive', onPress: () => { + rvs.send('file_delete_request' as any, { path: f.path }); + ToastAndroid.show('Lösch-Befehl gesendet…', ToastAndroid.SHORT); + }}, + ], + ); + }} + style={{padding:8}} + > + 🗑 + + + )); + })()} + + )} + + {currentSection === null && ( @@ -1288,76 +1428,66 @@ const SettingsScreen: React.FC = () => { - {/* === ARIA Reparatur === */} + {/* === Reparatur === */} Reparatur - Wenn ARIA gar nicht mehr antwortet oder auf jede Anfrage mit - "Antwort ohne Text" zurueckkommt — meistens ein steckengebliebener - Run im aria-core. Dieser Button fuehrt {'“'}openclaw doctor --fix{'”'} - aus und macht ARIA wieder ansprechbar. + Container gezielt neu starten — wenn die Voice-Bridge, das Gehirn + oder die Vector-DB haengt. Restart dauert wenige Sekunden, + laufende Anfragen gehen verloren. - { - rvs.send('doctor_fix' as any, {}); - ToastAndroid.show('Reparatur-Befehl gesendet — Antwort kommt gleich', ToastAndroid.SHORT); - }} - > - {'🔧 ARIA reparieren'} - - - Wenn auch Reparieren nicht hilft — Container hart neu starten. - ARIA ist dann ~15 Sekunden weg und kommt mit frischem State zurueck. - Laufende Anfragen gehen verloren. - - { - Alert.alert( - 'ARIA hart neu starten?', - 'Container-Restart (~15s). Laufende Anfragen gehen verloren.', - [ - { text: 'Abbrechen', style: 'cancel' }, - { text: 'Neu starten', style: 'destructive', onPress: () => { - rvs.send('aria_restart' as any, {}); - ToastAndroid.show('Container-Restart angestossen…', ToastAndroid.LONG); - }}, - ], - ); - }} - > - {'🚨 ARIA hart neu starten'} - - - Konversation komplett zuruecksetzen — alle bisherigen Nachrichten - aus ARIA's Session loeschen + Container neu. Anders als der harte - Restart wird hier auch ARIA's Erinnerung an die laufende - Konversation gewipt. Geschieht automatisch alle 140 Nachrichten - (Bridge-Setting COMPACT_AFTER_MESSAGES). - - { - Alert.alert( - 'Konversation komprimieren?', - 'Alle Nachrichten in ARIAs aktueller Session werden geloescht und der Container neu gestartet. ARIA vergisst den bisherigen Gespraechsverlauf.', - [ - { text: 'Abbrechen', style: 'cancel' }, - { text: 'Komprimieren', style: 'destructive', onPress: () => { - rvs.send('aria_session_reset' as any, {}); - ToastAndroid.show('Session wird zurueckgesetzt…', ToastAndroid.LONG); - }}, - ], - ); - }} - > - {'🧹 Konversation komprimieren'} - + {[ + { name: 'aria-bridge', label: '🚨 aria-bridge neu (Voice + RVS)' }, + { name: 'aria-brain', label: '🚨 aria-brain neu (Agent + Memory)' }, + { name: 'aria-qdrant', label: '🚨 aria-qdrant neu (Vector-DB)' }, + ].map(c => ( + { + Alert.alert( + `${c.name} neu starten?`, + 'Restart in wenigen Sekunden. Laufende Anfragen gehen verloren.', + [ + { text: 'Abbrechen', style: 'cancel' }, + { text: 'Neu starten', style: 'destructive', onPress: () => { + rvs.send('container_restart' as any, { name: c.name }); + ToastAndroid.show(`${c.name} wird neu gestartet…`, ToastAndroid.LONG); + }}, + ], + ); + }} + > + {c.label} + + ))} )} + {/* === Datei-Manager === */} + {currentSection === 'files' && (<> + Dateien + + + Alle Dateien aus /shared/uploads/ + — was ARIA generiert hat und was du hochgeladen hast. + Beim Löschen wird die Bubble in App + Diagnostic als gelöscht markiert. + + { + setFileManagerError(''); + setFileManagerLoading(true); + setFileManagerOpen(true); + rvs.send('file_list_request' as any, {}); + }} + > + {'📂 Datei-Manager öffnen'} + + + )} + {/* === Logs === */} {currentSection === 'protocol' && (<> Protokoll