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>
|
<h2 style="margin:0;">Memories <button class="info-btn" onclick="showInfo('memories')" title="Hot vs. Cold — wie funktioniert das Gedaechtnis?">ℹ</button></h2>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn secondary" onclick="resetBrainFilters();loadBrainMemoryList()" style="padding:4px 10px;font-size:11px;">Aktualisieren</button>
|
<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>
|
<button class="btn" onclick="openMemoryModal()" style="padding:4px 10px;font-size:11px;">+ Neu</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3583,6 +3584,120 @@
|
|||||||
el.innerHTML = (html + extra) || '(Keine bekannten Typen gefunden)';
|
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) {
|
function _updateCategoryDatalist(items) {
|
||||||
const dl = document.getElementById('memory-category-suggestions');
|
const dl = document.getElementById('memory-category-suggestions');
|
||||||
if (!dl) return;
|
if (!dl) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user