feat(memory): Anhaenge in App-Bubble + System-Prompt (Stufe C + D)

Stufe C — App:
- ChatMessage.memorySaved.attachments [{name, mime, size, path, localUri}]
- memory_saved-Listener uebernimmt payload.attachments
- renderMessage memorySaved-Bubble zeigt Anhaenge als Tap-Reihen
  (Icon 🖼/📄 + Filename + Hint). Tap → file_request via Bridge,
  beim ersten Mal "(tippen zum Laden)" → nach file_response cached
  + bei Bildern setFullscreenImage, bei anderen openFileWithIntent
- file_response-Handler updated zusaetzlich memorySaved.attachments
  per serverPath-Match
- Styles fuer memoryAttachmentRow/Icon/Name/Meta

Stufe D — System-Prompt:
- prompts._attachments_line: pro Memory eine Zeile
  "📎 Anhaenge: foo.jpg (image/jpeg, 109 KB) — Pfad: /shared/memory-attachments/<id>/"
- Wird in build_hot_memory_section + build_cold_memory_section
  nach dem Content angehangen
- ARIA "weiss" damit dass Anhaenge da sind und kann via Bash darauf
  zugreifen (file, head, base64 …). Echt sehen kann sie sie erst mit
  Multi-Modal-Pipeline (Stufe E)
- memory_save Dispatcher: attachments-Liste auch im memory_saved-Event
  (vermutlich [] beim Save, aber konsistent fuer spaeteres
  Speichern-mit-Anhaengen-Pattern)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 02:45:51 +02:00
parent de9b7b46f9
commit d5531521fa
3 changed files with 129 additions and 6 deletions
+96 -6
View File
@@ -89,11 +89,19 @@ interface ChatMessage {
};
/** Memory-Saved-Bubble: ARIA hat etwas via memory_save in die Qdrant-DB gepackt */
memorySaved?: {
id?: string;
title: string;
type: string;
category?: string;
pinned: boolean;
preview?: string;
attachments?: Array<{
name: string;
mime?: string;
size?: number;
path?: string; // Server-Pfad /shared/memory-attachments/<id>/<name>
localUri?: string; // Nach file_request gefuelltes file://-URI
}>;
};
/** Backup-Timestamp aus chat_backup.jsonl auf dem Bridge — Voraussetzung
* zum Loeschen der Bubble via Muelltonne. Lokale Bubbles ohne backupTs
@@ -534,17 +542,25 @@ const ChatScreen: React.FC = () => {
// gepackt — eigene Bubble (gelb wie trigger/skill).
if (message.type === 'memory_saved') {
const p = (message.payload || {}) as any;
const atts = Array.isArray(p.attachments) ? p.attachments.map((a: any) => ({
name: String(a?.name || 'datei'),
mime: a?.mime ? String(a.mime) : undefined,
size: typeof a?.size === 'number' ? a.size : undefined,
path: a?.path ? String(a.path) : undefined,
})) : [];
const memoryMsg: ChatMessage = {
id: nextId(),
sender: 'aria',
text: '',
timestamp: Date.now(),
memorySaved: {
id: p.id ? String(p.id) : undefined,
title: String(p.title || '(ohne Titel)'),
type: String(p.type || 'fact'),
category: p.category ? String(p.category) : undefined,
pinned: !!p.pinned,
preview: p.content_preview ? String(p.content_preview) : undefined,
attachments: atts.length ? atts : undefined,
},
};
setMessages(prev => capMessages([...prev, memoryMsg]));
@@ -595,16 +611,38 @@ const ChatScreen: React.FC = () => {
if (b64 && reqId) {
const fileName = (message.payload.name as string) || 'download';
persistAttachment(b64, reqId, fileName).then(filePath => {
setMessages(prev => prev.map(m => ({
...m,
attachments: m.attachments?.map(a =>
setMessages(prev => prev.map(m => {
// Hauptattachments updaten (Bilder/Files am User-Send / ARIA-File-Bubble)
const updatedAtts = m.attachments?.map(a =>
a.serverPath === serverPath ? { ...a, uri: filePath } : a
),
})));
);
// Memory-Anhang-Match (Bubble vom memory_saved-Event)
const ms = m.memorySaved;
let updatedMs = ms;
if (ms && Array.isArray(ms.attachments)) {
const hit = ms.attachments.some(a => a.path === serverPath);
if (hit) {
updatedMs = {
...ms,
attachments: ms.attachments.map(a =>
a.path === serverPath ? { ...a, localUri: filePath } : a
),
};
}
}
return { ...m, attachments: updatedAtts, memorySaved: updatedMs };
}));
// Wenn der User dieses File explizit oeffnen wollte → Intent-Picker
// (Bilder werden separat via setFullscreenImage in der memorySaved-
// Bubble geoeffnet, das laeuft nicht ueber autoOpenPaths)
if (serverPath && autoOpenPaths.current.has(serverPath)) {
autoOpenPaths.current.delete(serverPath);
openFileWithIntent(filePath.replace(/^file:\/\//, ''), mimeType);
const isImage = (mimeType || '').startsWith('image/');
if (isImage) {
setFullscreenImage(filePath);
} else {
openFileWithIntent(filePath.replace(/^file:\/\//, ''), mimeType);
}
}
}).catch(() => {});
}
@@ -1287,6 +1325,7 @@ const ChatScreen: React.FC = () => {
if (item.memorySaved) {
const m = item.memorySaved;
const catPart = m.category ? ` · [${m.category}]` : '';
const atts = m.attachments || [];
return (
<View style={[styles.messageBubble, styles.ariaBubble, {borderLeftWidth: 3, borderLeftColor: '#FFD60A'}, searchHighlightStyle]}>
<Text style={{color: '#FFD60A', fontWeight: 'bold', fontSize: 14}}>
@@ -1299,6 +1338,35 @@ const ChatScreen: React.FC = () => {
{m.preview ? (
<Text style={{color: '#888', fontSize: 12, marginTop: 4}}>{m.preview}{m.preview.length >= 140 ? '…' : ''}</Text>
) : null}
{atts.map((a, idx) => {
const isImage = (a.mime || '').startsWith('image/');
const icon = isImage ? '🖼️' : '📄';
const sizeStr = a.size ? ` · ${(a.size / 1024).toFixed(0)} KB` : '';
return (
<TouchableOpacity
key={`${item.id}-att-${idx}`}
style={styles.memoryAttachmentRow}
onPress={() => {
if (!a.path) return;
if (a.localUri) {
if (isImage) setFullscreenImage(a.localUri);
else openFileWithIntent(a.localUri.replace(/^file:\/\//, ''), a.mime || '');
} else {
// Datei via Bridge nachladen — file_response hat den
// memorySaved-Match-Path und cached + zeigt direkt
autoOpenPaths.current.add(a.path);
rvs.send('file_request' as any, { serverPath: a.path, requestId: `memAtt_${item.id}_${idx}` });
}
}}
>
<Text style={styles.memoryAttachmentIcon}>{icon}</Text>
<Text style={styles.memoryAttachmentName} numberOfLines={1}>{a.name}</Text>
<Text style={styles.memoryAttachmentMeta}>
{a.localUri ? '(tippen zum oeffnen)' : `(tippen zum Laden${sizeStr})`}
</Text>
</TouchableOpacity>
);
})}
<Text style={{color: '#555570', fontSize: 10, marginTop: 6}}>ARIA-Memory · {time}</Text>
</View>
);
@@ -2065,6 +2133,28 @@ const styles = StyleSheet.create({
playButtonText: {
fontSize: 16,
},
memoryAttachmentRow: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#0D0D1A',
borderRadius: 6,
paddingHorizontal: 8,
paddingVertical: 6,
marginTop: 4,
gap: 6,
},
memoryAttachmentIcon: {
fontSize: 16,
},
memoryAttachmentName: {
flex: 1,
color: '#E0E0F0',
fontSize: 12,
},
memoryAttachmentMeta: {
color: '#555570',
fontSize: 10,
},
bubbleTrash: {
position: 'absolute',
top: 4,