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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user