/** * Memory-Detail-Modal — Anzeige + Edit eines einzelnen Memory-Eintrags. * * Zwei Modi: * - read-only: zeigt alle Felder + Anhang-Vorschau (Klick auf Bild = Vollbild) * - edit: Form mit Save/Delete/Anhang-hochladen * * Memory-Daten werden beim Oeffnen aus dem Brain (via brainApi → RVS) frisch * gezogen. Optimistic Updates sind explizit nicht da — der DB-Stand ist die * Truth. */ import React, { useEffect, useState } from 'react'; import { ActivityIndicator, Alert, Image, Modal, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View, } from 'react-native'; import DocumentPicker, { DocumentPickerResponse } from 'react-native-document-picker'; import RNFS from 'react-native-fs'; import brainApi, { Memory, MemoryAttachment } from '../services/brainApi'; interface Props { memoryId: string | null; visible: boolean; onClose: () => void; onDeleted?: (id: string) => void; } const TYPE_OPTIONS = [ { value: 'identity', label: 'identity (FEST)' }, { value: 'rule', label: 'rule (FEST)' }, { value: 'preference', label: 'preference (FEST)' }, { value: 'tool', label: 'tool (FEST)' }, { value: 'skill', label: 'skill (FEST)' }, { value: 'fact', label: 'fact (Cold)' }, { value: 'conversation', label: 'conversation (Cold)' }, { value: 'reminder', label: 'reminder (Cold)' }, ]; export const MemoryDetailModal: React.FC = ({ memoryId, visible, onClose, onDeleted }) => { const [memory, setMemory] = useState(null); const [loading, setLoading] = useState(false); const [editing, setEditing] = useState(false); const [saving, setSaving] = useState(false); const [err, setErr] = useState(null); const [busy, setBusy] = useState(null); // Edit-Felder const [eTitle, setETitle] = useState(''); const [eContent, setEContent] = useState(''); const [eCategory, setECategory] = useState(''); const [eTags, setETags] = useState(''); const [ePinned, setEPinned] = useState(false); // Bild-Vollbild const [fullscreen, setFullscreen] = useState(null); // Memory laden beim Oeffnen useEffect(() => { if (!visible || !memoryId) { setMemory(null); setEditing(false); setErr(null); return; } setLoading(true); setErr(null); brainApi.getMemory(memoryId) .then(m => { setMemory(m); setETitle(m.title || ''); setEContent(m.content || ''); setECategory(m.category || ''); setETags((m.tags || []).join(', ')); setEPinned(!!m.pinned); }) .catch(e => setErr(String(e?.message || e))) .finally(() => setLoading(false)); }, [visible, memoryId]); const reload = () => { if (!memoryId) return; setLoading(true); brainApi.getMemory(memoryId) .then(m => setMemory(m)) .catch(e => setErr(String(e?.message || e))) .finally(() => setLoading(false)); }; const onSave = async () => { if (!memoryId) return; setSaving(true); setErr(null); try { const tags = eTags.split(',').map(t => t.trim()).filter(Boolean); const m = await brainApi.updateMemory(memoryId, { title: eTitle.trim(), content: eContent.trim(), category: eCategory.trim(), tags, pinned: ePinned, }); setMemory(m); setEditing(false); } catch (e: any) { setErr(String(e?.message || e)); } finally { setSaving(false); } }; const onDelete = () => { if (!memoryId || !memory) return; Alert.alert( 'Memory loeschen?', `"${memory.title}"\n\nWird permanent aus der DB entfernt, inkl. aller Anhaenge.`, [ { text: 'Abbrechen', style: 'cancel' }, { text: 'Loeschen', style: 'destructive', onPress: async () => { try { await brainApi.deleteMemory(memoryId); if (onDeleted) onDeleted(memoryId); onClose(); } catch (e: any) { Alert.alert('Fehler', String(e?.message || e)); } }, }, ], ); }; const onPickAndUpload = async () => { if (!memoryId) return; try { const picked: DocumentPickerResponse[] = await DocumentPicker.pick({ type: [DocumentPicker.types.images, DocumentPicker.types.pdf, DocumentPicker.types.allFiles], copyTo: 'cachesDirectory', }); for (const f of picked) { setBusy(`Lade ${f.name}…`); // RNFS lesen → base64 → API const localPath = (f.fileCopyUri || f.uri).replace(/^file:\/\//, ''); const b64 = await RNFS.readFile(localPath, 'base64'); await brainApi.uploadAttachment(memoryId, f.name || 'datei', b64); } setBusy(null); reload(); } catch (e: any) { setBusy(null); if (DocumentPicker.isCancel(e)) return; Alert.alert('Upload-Fehler', String(e?.message || e)); } }; const onDeleteAttachment = (att: MemoryAttachment) => { if (!memoryId) return; Alert.alert( 'Anhang loeschen?', `"${att.name}"`, [ { text: 'Abbrechen', style: 'cancel' }, { text: 'Loeschen', style: 'destructive', onPress: async () => { try { const m = await brainApi.deleteAttachment(memoryId, att.name); setMemory(m); } catch (e: any) { Alert.alert('Fehler', String(e?.message || e)); } }, }, ], ); }; const onTapAttachment = async (att: MemoryAttachment) => { if (!memoryId) return; if ((att.mime || '').startsWith('image/')) { try { setBusy('Lade Bild…'); const data = await brainApi.getAttachmentBytes(memoryId, att.name); // Temp-File schreiben damit es zeigen kann const safe = att.name.replace(/[^A-Za-z0-9._-]/g, '_'); const localPath = `${RNFS.CachesDirectoryPath}/memory_${memoryId}_${safe}`; await RNFS.writeFile(localPath, data.base64, 'base64'); setBusy(null); setFullscreen('file://' + localPath); } catch (e: any) { setBusy(null); Alert.alert('Fehler', String(e?.message || e)); } } else { Alert.alert('Anhang', `${att.name}\n${att.mime}\n${att.size} Byte\n\nPfad: ${att.path}`); } }; return ( {editing ? 'Memory bearbeiten' : 'Memory-Detail'} × {loading ? ( ) : err && !memory ? ( {err} ) : memory ? ( editing ? ( Typ {memory.type} (kann hier nicht geaendert werden) Titel Inhalt Kategorie Tags (komma-getrennt) 📌 Pinned (immer im System-Prompt) {err ? {err} : null} setEditing(false)} disabled={saving}> Abbrechen {saving ? 'Speichere…' : 'Speichern'} ) : ( {memory.pinned ? '📌 ' : ''}{memory.title} setEditing(true)} style={s.iconBtn}> {memory.type}{memory.category ? ` · [${memory.category}]` : ''} {(memory.tags || []).length > 0 ? ( {memory.tags.map(t => {t})} ) : null} {memory.content} 📎 Anhaenge {(memory.attachments || []).length === 0 ? ( (keine) ) : ( (memory.attachments || []).map((a) => { const isImage = (a.mime || '').startsWith('image/'); return ( onTapAttachment(a)}> {isImage ? '🖼️' : '📄'} {a.name} {a.mime} · {Math.round(a.size/1024)} KB onDeleteAttachment(a)} style={s.attDelete}> 🗑 ); }) )} ⬆ Datei anhaengen {busy ? {busy} : null} angelegt: {(memory.created_at || '').slice(0,16).replace('T',' ')}{'\n'} geaendert: {(memory.updated_at || '').slice(0,16).replace('T',' ')}{'\n'} id: {memory.id} 🗑 Memory komplett loeschen ) ) : null} setFullscreen(null)}> setFullscreen(null)}> {fullscreen ? : null} ); }; const s = StyleSheet.create({ backdrop: { flex:1, backgroundColor:'rgba(0,0,0,0.75)', justifyContent:'flex-end' }, box: { backgroundColor:'#0D0D1A', borderTopLeftRadius:12, borderTopRightRadius:12, maxHeight:'92%' }, header: { flexDirection:'row', justifyContent:'space-between', alignItems:'center', padding:14, borderBottomColor:'#1E1E2E', borderBottomWidth:1 }, title: { color:'#FFD60A', fontWeight:'bold', fontSize:15 }, closeX: { color:'#8888AA', fontSize:24, paddingHorizontal:6 }, body: { padding:14 }, err: { color:'#FF6B6B', fontSize:12, marginTop:8 }, label: { color:'#8888AA', fontSize:11, marginBottom:3, marginTop:8 }, input: { backgroundColor:'#080810', borderColor:'#1E1E2E', borderWidth:1, borderRadius:4, padding:8, color:'#E0E0F0', fontSize:13 }, bigTitle: { color:'#E0E0F0', fontWeight:'bold', fontSize:16, flex:1, marginRight:6 }, iconBtn: { padding:6, backgroundColor:'#1E1E2E', borderRadius:6 }, iconBtnText: { color:'#0096FF', fontSize:14 }, meta: { color:'#8888AA', fontSize:11, marginTop:4 }, tagsRow: { flexDirection:'row', flexWrap:'wrap', gap:4, marginTop:6 }, tag: { backgroundColor:'#1E1E2E', color:'#8888AA', fontSize:10, paddingHorizontal:6, paddingVertical:2, borderRadius:8 }, contentBlock: { color:'#E0E0F0', fontSize:13, marginTop:12, lineHeight:18 }, sectionHead: { color:'#0096FF', fontSize:11, marginTop:14, marginBottom:6, textTransform:'uppercase', letterSpacing:0.5 }, attRow: { flexDirection:'row', alignItems:'center', backgroundColor:'#080810', padding:8, borderRadius:6, marginBottom:4, gap:6 }, attDelete: { padding:4 }, timestamps: { color:'#555570', fontSize:10, marginTop:12, fontFamily:'monospace' }, btn: { paddingVertical:10, paddingHorizontal:14, borderRadius:6, alignItems:'center' }, btnPrimary: { backgroundColor:'#0096FF' }, btnSecondary: { backgroundColor:'#1E1E2E' }, btnDanger: { backgroundColor:'#3B1010', borderWidth:1, borderColor:'#FF6B6B' }, btnText: { color:'#fff', fontSize:13, fontWeight:'600' }, fsBack: { flex:1, backgroundColor:'rgba(0,0,0,0.95)', justifyContent:'center', alignItems:'center' }, fsImg: { width:'95%', height:'85%' }, }); export default MemoryDetailModal;