feat(diag): Memory-Druckansicht — Strg+P → als PDF
Neuer Button "📄 Drucken / PDF" im Gehirn-Tab oeffnet eine sauber formatierte Print-View in neuem Tab. Druck-CSS optimiert (page-break- inside:avoid pro Entry, schwarze Borders fuer Print, Action-Bar wird versteckt). Aktueller Type+Pinned-Filter wird respektiert. Browser-eigenes "Als PDF speichern" greift dann — kein Tool noetig. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -812,6 +812,7 @@
|
||||
<h2 style="margin:0;">Memories <button class="info-btn" onclick="showInfo('memories')" title="Hot vs. Cold — wie funktioniert das Gedaechtnis?">ℹ</button></h2>
|
||||
<div>
|
||||
<button class="btn secondary" onclick="resetBrainFilters();loadBrainMemoryList()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
|
||||
<button class="btn secondary" onclick="printBrainMemory()" style="padding:4px 10px;font-size:11px;" title="Druckbare Ansicht öffnen — dort dann Strg+P → Als PDF speichern">📄 Drucken / PDF</button>
|
||||
<button class="btn" onclick="openMemoryModal()" style="padding:4px 10px;font-size:11px;">+ Neu</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3583,6 +3584,120 @@
|
||||
el.innerHTML = (html + extra) || '(Keine bekannten Typen gefunden)';
|
||||
}
|
||||
|
||||
async function printBrainMemory() {
|
||||
// Aktuellen Filter respektieren, damit Stefan z.B. "nur pinned" drucken kann.
|
||||
const typeFilter = document.getElementById('brain-filter-type')?.value || '';
|
||||
const pinnedFilter = document.getElementById('brain-filter-pinned')?.value || 'all';
|
||||
try {
|
||||
const params = new URLSearchParams({ limit: '2000' });
|
||||
if (typeFilter) params.set('type', typeFilter);
|
||||
const r = await fetch('/api/brain/memory/list?' + params.toString());
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
let items = await r.json();
|
||||
if (pinnedFilter === 'pinned') items = items.filter(m => m.pinned);
|
||||
else if (pinnedFilter === 'cold') items = items.filter(m => !m.pinned);
|
||||
|
||||
// Items nach Type gruppieren, Reihenfolge aus BRAIN_TYPE_ORDER
|
||||
const byType = {};
|
||||
items.forEach(m => { (byType[m.type] = byType[m.type] || []).push(m); });
|
||||
const knownTypes = BRAIN_TYPE_ORDER.filter(t => byType[t]);
|
||||
const unknownTypes = Object.keys(byType).filter(t => !BRAIN_TYPE_ORDER.includes(t));
|
||||
const allTypes = [...knownTypes, ...unknownTypes];
|
||||
|
||||
const filterDesc = [
|
||||
typeFilter ? `Typ: ${BRAIN_TYPE_LABELS[typeFilter] || typeFilter}` : 'alle Typen',
|
||||
pinnedFilter === 'pinned' ? 'nur pinned' : (pinnedFilter === 'cold' ? 'nur cold' : 'pinned + cold'),
|
||||
].join(' · ');
|
||||
const printedAt = new Date().toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' });
|
||||
|
||||
const escapeForHtml = (s) => String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
const renderItem = (m) => {
|
||||
const pin = m.pinned ? '📌 ' : '';
|
||||
const cat = m.category ? `<span class="cat">[${escapeForHtml(m.category)}]</span>` : '';
|
||||
const tags = (m.tags || []).length
|
||||
? `<div class="tags">${m.tags.map(t => `<span class="tag">${escapeForHtml(t)}</span>`).join(' ')}</div>`
|
||||
: '';
|
||||
return `
|
||||
<div class="entry">
|
||||
<div class="entry-title">${pin}<strong>${escapeForHtml(m.title || '(ohne Titel)')}</strong> ${cat}</div>
|
||||
<div class="entry-content">${escapeForHtml(m.content || '')}</div>
|
||||
${tags}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
const sections = allTypes.map(t => {
|
||||
const label = BRAIN_TYPE_LABELS[t] || t;
|
||||
const fixed = BRAIN_TYPE_INFO[t]?.fixed ? '<span class="fixed-marker">FEST im System-Prompt</span>' : '';
|
||||
const entries = byType[t].map(renderItem).join('');
|
||||
return `
|
||||
<section class="type-section">
|
||||
<h2>${escapeForHtml(label)} <span class="count">(${byType[t].length})</span> ${fixed}</h2>
|
||||
${entries}
|
||||
</section>`;
|
||||
}).join('');
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ARIA Gehirn — Druckansicht (${printedAt})</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; color: #111; background: #fff; padding: 24px; max-width: 920px; margin: 0 auto; line-height: 1.45; }
|
||||
header { border-bottom: 2px solid #0096FF; padding-bottom: 10px; margin-bottom: 18px; display: flex; justify-content: space-between; align-items: baseline; gap: 16px; flex-wrap: wrap; }
|
||||
header h1 { font-size: 22px; margin: 0; color: #0096FF; }
|
||||
header .meta { font-size: 11px; color: #666; }
|
||||
.summary { font-size: 12px; color: #444; margin-bottom: 18px; }
|
||||
.type-section { margin-bottom: 22px; page-break-inside: auto; }
|
||||
.type-section h2 { font-size: 15px; color: #0096FF; border-bottom: 1px solid #0096FF44; padding-bottom: 4px; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; page-break-after: avoid; }
|
||||
.type-section h2 .count { color: #888; font-weight: normal; font-size: 12px; margin-left: 6px; }
|
||||
.fixed-marker { background: #0096FF; color: #fff; font-size: 9px; padding: 2px 6px; border-radius: 3px; vertical-align: middle; margin-left: 8px; letter-spacing: 0.4px; }
|
||||
.entry { padding: 8px 0; border-bottom: 1px solid #eee; page-break-inside: avoid; }
|
||||
.entry:last-child { border-bottom: none; }
|
||||
.entry-title { font-size: 13px; margin-bottom: 4px; }
|
||||
.entry-title .cat { color: #888; font-size: 10px; font-weight: normal; margin-left: 6px; }
|
||||
.entry-content { font-size: 12px; color: #222; white-space: pre-wrap; word-wrap: break-word; }
|
||||
.tags { margin-top: 4px; }
|
||||
.tag { display: inline-block; background: #f0f0f5; color: #555; font-size: 9px; padding: 1px 5px; border-radius: 8px; margin-right: 3px; }
|
||||
.actions { position: fixed; top: 12px; right: 12px; }
|
||||
.actions button { background: #0096FF; color: #fff; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; }
|
||||
.empty { color: #888; font-style: italic; padding: 20px 0; }
|
||||
@media print {
|
||||
.actions { display: none; }
|
||||
body { padding: 0; max-width: none; }
|
||||
header { border-bottom-color: #000; }
|
||||
.type-section h2 { color: #000; border-bottom-color: #000; }
|
||||
.type-section h2 { page-break-after: avoid; }
|
||||
.entry { page-break-inside: avoid; }
|
||||
.fixed-marker { background: #000; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="actions"><button onclick="window.print()">🖨️ Drucken / als PDF</button></div>
|
||||
<header>
|
||||
<h1>ARIA Gehirn — Druckansicht</h1>
|
||||
<div class="meta">${escapeForHtml(printedAt)}</div>
|
||||
</header>
|
||||
<div class="summary">Filter: ${escapeForHtml(filterDesc)} · ${items.length} Eintrag${items.length === 1 ? '' : 'e'}</div>
|
||||
${sections || '<div class="empty">Keine Eintraege fuer diesen Filter.</div>'}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const win = window.open('', '_blank');
|
||||
if (!win) {
|
||||
alert('Popup blockiert — bitte Popups für Diagnostic erlauben und nochmal klicken.');
|
||||
return;
|
||||
}
|
||||
win.document.open();
|
||||
win.document.write(html);
|
||||
win.document.close();
|
||||
} catch (e) {
|
||||
alert('Druckansicht konnte nicht geladen werden: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function _updateCategoryDatalist(items) {
|
||||
const dl = document.getElementById('memory-category-suggestions');
|
||||
if (!dl) return;
|
||||
|
||||
Reference in New Issue
Block a user