feat(diag): Memory-Anhaenge in der UI (Stufe B)
Diagnostic-Gehirn-Tab kann jetzt Bilder/Dateien an Memory-Eintraege haengen — drag+drop ueber den File-Input im Memory-Modal. Memory-Modal (Edit-Modus): - Neuer Block "📎 Anhaenge" unter Pinned-Checkbox, nur sichtbar wenn Memory eine ID hat (Edit). Bei "Neue Memory" stattdessen Hinweis "Anhaenge nach Speichern hinzufuegbar". - "⬆ Datei waehlen" oeffnet File-Picker (multiple), Upload via multipart/form-data POST an /memory/{id}/attachments/upload. - Liste zeigt pro Anhang: Thumbnail (Bilder) oder 📄-Icon, Filename, Mime + Groesse, 🗑 Loeschen-Button. - Bild-Thumbnails sind klickbar → openLightbox. - Status-Zeile zeigt Upload-Progress + Erfolgsmeldung. Memory-Liste: - 📎N-Badge erscheint hinter dem Titel wenn N > 0 Anhaenge da sind. Diagnostic-Server: - Brain-Reverse-Proxy-Timeout dynamisch: 120s fuer /attachments-Routen (Upload), 60s sonst (vorher pauschal 30s — zu wenig fuer chat/distill). - multipart-Body wird ueber req.pipe(proxyReq) durchgereicht (FastAPI liest File via UploadFile, Content-Type-Header bleibt erhalten). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+120
-1
@@ -1043,6 +1043,26 @@
|
||||
<input type="checkbox" id="memory-pinned">
|
||||
<span>📌 Pinned (Hot Memory — IMMER im System-Prompt)</span>
|
||||
</label>
|
||||
|
||||
<!-- Anhaenge — nur bei Edit (vorhandene ID) sichtbar -->
|
||||
<div id="memory-attachments-block" style="display:none;margin-top:14px;padding-top:10px;border-top:1px solid #1E1E2E;">
|
||||
<label style="display:flex;align-items:center;justify-content:space-between;font-size:11px;color:#8888AA;margin-bottom:6px;">
|
||||
<span>📎 Anhaenge</span>
|
||||
<span style="color:#555570;font-size:10px;">max 20 MB pro Datei</span>
|
||||
</label>
|
||||
<div id="memory-attachments-list" style="display:flex;flex-direction:column;gap:4px;margin-bottom:6px;font-size:12px;color:#555570;"></div>
|
||||
<div style="display:flex;gap:6px;align-items:center;">
|
||||
<label class="btn secondary" style="padding:4px 10px;font-size:11px;cursor:pointer;margin:0;">
|
||||
⬆ Datei waehlen
|
||||
<input type="file" id="memory-attachment-input" multiple style="display:none;" onchange="uploadMemoryAttachments(this.files)">
|
||||
</label>
|
||||
<span id="memory-attachment-status" style="font-size:11px;color:#555570;"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="memory-attachments-hint" style="display:none;margin-top:10px;padding:6px 8px;background:#0D0D1A;border-radius:4px;color:#555570;font-size:11px;">
|
||||
📎 Anhaenge kannst du nach dem Speichern hinzufuegen (brauchen eine Memory-ID).
|
||||
</div>
|
||||
|
||||
<div id="memory-modal-error" style="color:#FF6B6B;font-size:11px;margin-top:10px;display:none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer" style="padding:10px 16px;border-top:1px solid #1E1E2E;display:flex;justify-content:flex-end;gap:8px;">
|
||||
@@ -3849,9 +3869,11 @@
|
||||
const preview = (m.content || '').slice(0, 140).replace(/\n/g, ' ');
|
||||
const score = withScore && typeof m.score === 'number' ? `<span style="color:#FFD60A;font-size:10px;margin-left:6px;">${m.score.toFixed(2)}</span>` : '';
|
||||
const typeBadge = withScore ? `<span style="color:#0096FF;font-size:10px;margin-right:6px;">${escapeHtml(BRAIN_TYPE_LABELS[m.type] || m.type)}</span>` : '';
|
||||
const attCount = Array.isArray(m.attachments) ? m.attachments.length : 0;
|
||||
const attBadge = attCount > 0 ? `<span style="color:#34C759;font-size:10px;margin-left:6px;" title="${attCount} Anhang${attCount === 1 ? '' : ' / Anhaenge'}">📎${attCount}</span>` : '';
|
||||
return `<div style="padding:6px 0;border-bottom:1px solid #1E1E2E;display:flex;gap:6px;align-items:flex-start;">
|
||||
<div style="flex:1;min-width:0;cursor:pointer;" onclick="openMemoryModal('${m.id}')">
|
||||
<div style="color:#E0E0F0;font-size:12px;">${typeBadge}${pin}<strong>${escapeHtml(m.title || '(ohne Titel)')}</strong>${score}
|
||||
<div style="color:#E0E0F0;font-size:12px;">${typeBadge}${pin}<strong>${escapeHtml(m.title || '(ohne Titel)')}</strong>${score}${attBadge}
|
||||
${m.category ? `<span style="color:#555570;font-weight:normal;font-size:10px;margin-left:6px;">[${escapeHtml(m.category)}]</span>` : ''}
|
||||
</div>
|
||||
<div style="color:#888;font-size:11px;line-height:1.4;">${escapeHtml(preview)}${m.content && m.content.length > 140 ? '...' : ''}</div>
|
||||
@@ -4044,6 +4066,13 @@
|
||||
const errEl = document.getElementById('memory-modal-error');
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const attBlock = document.getElementById('memory-attachments-block');
|
||||
const attHint = document.getElementById('memory-attachments-hint');
|
||||
const attStatus = document.getElementById('memory-attachment-status');
|
||||
if (attStatus) attStatus.textContent = '';
|
||||
const attInput = document.getElementById('memory-attachment-input');
|
||||
if (attInput) attInput.value = '';
|
||||
|
||||
if (id && brainMemoryCache[id]) {
|
||||
const m = brainMemoryCache[id];
|
||||
titleEl.textContent = 'Memory bearbeiten';
|
||||
@@ -4054,6 +4083,10 @@
|
||||
document.getElementById('memory-category').value = m.category || '';
|
||||
document.getElementById('memory-tags').value = (m.tags || []).join(', ');
|
||||
document.getElementById('memory-pinned').checked = !!m.pinned;
|
||||
// Anhang-Block sichtbar — Liste rendern
|
||||
if (attBlock) attBlock.style.display = 'block';
|
||||
if (attHint) attHint.style.display = 'none';
|
||||
renderMemoryAttachmentsList(m.attachments || []);
|
||||
} else {
|
||||
titleEl.textContent = 'Neue Memory';
|
||||
idEl.value = '';
|
||||
@@ -4063,10 +4096,96 @@
|
||||
document.getElementById('memory-category').value = '';
|
||||
document.getElementById('memory-tags').value = '';
|
||||
document.getElementById('memory-pinned').checked = false;
|
||||
// Bei neuem Memory: nur Hinweis, dass Anhaenge nach Save gehen
|
||||
if (attBlock) attBlock.style.display = 'none';
|
||||
if (attHint) attHint.style.display = 'block';
|
||||
}
|
||||
modal.classList.add('open');
|
||||
}
|
||||
|
||||
function renderMemoryAttachmentsList(atts) {
|
||||
const el = document.getElementById('memory-attachments-list');
|
||||
if (!el) return;
|
||||
const id = document.getElementById('memory-edit-id').value;
|
||||
if (!Array.isArray(atts) || atts.length === 0) {
|
||||
el.innerHTML = '<div style="color:#555570;font-size:11px;font-style:italic;">(noch keine Anhaenge)</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = atts.map(a => {
|
||||
const name = escapeHtml(a.name || '?');
|
||||
const mime = a.mime || 'application/octet-stream';
|
||||
const size = a.size ? `${(a.size / 1024).toFixed(0)} KB` : '';
|
||||
const isImage = mime.startsWith('image/');
|
||||
const url = `/api/brain/memory/${encodeURIComponent(id)}/attachments/${encodeURIComponent(a.name)}`;
|
||||
const preview = isImage
|
||||
? `<img src="${url}" style="width:32px;height:32px;object-fit:cover;border-radius:4px;cursor:pointer;" onclick="openLightbox('image','${url}')">`
|
||||
: `<span style="display:inline-block;width:32px;text-align:center;font-size:18px;">📄</span>`;
|
||||
return `<div style="display:flex;align-items:center;gap:8px;padding:4px 6px;background:#0D0D1A;border-radius:4px;">
|
||||
${preview}
|
||||
<a href="${url}" target="_blank" style="flex:1;min-width:0;color:#E0E0F0;text-decoration:none;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${name}">${name}</a>
|
||||
<span style="color:#555570;font-size:10px;flex-shrink:0;">${escapeHtml(mime)}, ${size}</span>
|
||||
<button class="btn secondary" onclick="deleteMemoryAttachment('${encodeURIComponent(a.name)}')" title="Anhang loeschen" style="padding:2px 6px;font-size:10px;color:#FF6B6B;border-color:#FF6B6B;">🗑</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function uploadMemoryAttachments(files) {
|
||||
if (!files || !files.length) return;
|
||||
const id = document.getElementById('memory-edit-id').value;
|
||||
if (!id) return;
|
||||
const status = document.getElementById('memory-attachment-status');
|
||||
let lastResult = null;
|
||||
let n = 0;
|
||||
for (const file of files) {
|
||||
if (status) status.textContent = `⏳ Lade ${file.name} (${(file.size/1024).toFixed(0)} KB)...`;
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append('file', file, file.name);
|
||||
const r = await fetch(`/api/brain/memory/${encodeURIComponent(id)}/attachments/upload`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
if (!r.ok) {
|
||||
const txt = await r.text();
|
||||
throw new Error('HTTP ' + r.status + ': ' + txt.slice(0, 200));
|
||||
}
|
||||
lastResult = await r.json();
|
||||
n += 1;
|
||||
} catch (e) {
|
||||
if (status) status.textContent = `🔴 ${file.name}: ${e.message}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastResult) {
|
||||
brainMemoryCache[id] = lastResult;
|
||||
renderMemoryAttachmentsList(lastResult.attachments || []);
|
||||
if (status) status.textContent = `✓ ${n} Anhang${n === 1 ? '' : '/Anhaenge'} hochgeladen`;
|
||||
// Eingabe-File-List reset damit erneutes Anwaehlen derselben Datei feuert
|
||||
const inp = document.getElementById('memory-attachment-input');
|
||||
if (inp) inp.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMemoryAttachment(filenameEncoded) {
|
||||
const id = document.getElementById('memory-edit-id').value;
|
||||
if (!id) return;
|
||||
const name = decodeURIComponent(filenameEncoded);
|
||||
if (!confirm(`Anhang "${name}" wirklich loeschen?`)) return;
|
||||
try {
|
||||
const r = await fetch(`/api/brain/memory/${encodeURIComponent(id)}/attachments/${filenameEncoded}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
const updated = await r.json();
|
||||
brainMemoryCache[id] = updated;
|
||||
renderMemoryAttachmentsList(updated.attachments || []);
|
||||
const status = document.getElementById('memory-attachment-status');
|
||||
if (status) status.textContent = `✓ "${name}" geloescht`;
|
||||
} catch (e) {
|
||||
alert('Loeschen fehlgeschlagen: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function closeMemoryModal() {
|
||||
document.getElementById('memory-modal').classList.remove('open');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user