Feat: Implement detailed content previews for library items

Adds functionality to display rich content previews (including truncated or extracted RAG text) when viewing a library item, updating backend indexing and frontend rendering accordingly.
This commit is contained in:
2026-06-15 01:25:15 +02:00
parent 9c84076a85
commit 4a624d2d30
3 changed files with 143 additions and 3 deletions

View File

@@ -75,6 +75,8 @@ export default function LibraryManager({ apiBase, library, jobs, onRefresh }) {
const [textContent, setTextContent] = useState('')
const [websiteTitle, setWebsiteTitle] = useState('')
const [websiteUrl, setWebsiteUrl] = useState('')
const [expandedItemId, setExpandedItemId] = useState(null)
const [contentPreviews, setContentPreviews] = useState({})
const toastTimeoutsRef = useRef(new Map())
const toastIdRef = useRef(0)
const previousLibraryStateRef = useRef(null)
@@ -122,6 +124,28 @@ export default function LibraryManager({ apiBase, library, jobs, onRefresh }) {
setWebsiteUrl('')
}
async function toggleContentPreview(item) {
const itemId = item?.item_id
if (!library || !itemId) return
if (expandedItemId === itemId) {
setExpandedItemId(null)
return
}
setExpandedItemId(itemId)
if (contentPreviews[itemId]) return
setContentPreviews(current => ({ ...current, [itemId]: { loading: true } }))
try {
const response = await fetch(`${apiBase}/libraries/${library.slug}/items/${itemId}`)
const data = await expectJson(response)
setContentPreviews(current => ({ ...current, [itemId]: { data } }))
} catch (error) {
setContentPreviews(current => ({
...current,
[itemId]: { error: String(error?.message || error) }
}))
}
}
async function runAction(fn, successMessage) {
setBusy(true)
setErrorMessage('')
@@ -372,8 +396,11 @@ export default function LibraryManager({ apiBase, library, jobs, onRefresh }) {
const sync = itemSyncMeta(item)
const label = kindLabel(item)
const source = item.kind === 'website' ? item.url : item.kind === 'text' ? 'Written in Heimgeist' : item.path
const itemId = item.item_id || item.sha256 || item.rel
const isExpanded = expandedItemId === itemId
const preview = contentPreviews[itemId]
return (
<article key={item.item_id || item.sha256 || item.rel} className={`library-content-card kind-${item.kind || 'file'}`}>
<article key={itemId} className={`library-content-card kind-${item.kind || 'file'} ${isExpanded ? 'expanded' : ''}`}>
<div className="library-content-card-header">
<span className="library-kind-badge">{label}</span>
<span className={`library-file-sync-label ${sync.status}`}>{sync.label}</span>
@@ -389,7 +416,25 @@ export default function LibraryManager({ apiBase, library, jobs, onRefresh }) {
<div className="library-file-progress-bar" style={{ width: `${sync.progress}%` }} />
</div>
</div>
{isExpanded && (
<div className="library-content-preview">
<div className="library-content-preview-label">
{item.kind === 'website' ? 'Saved website text' : item.kind === 'text' ? 'Stored text' : 'Extracted text used by RAG'}
</div>
{preview?.loading && <div className="muted-copy">Loading content</div>}
{preview?.error && <div className="form-error">{preview.error}</div>}
{preview?.data && (
<>
<pre>{preview.data.content || 'No readable text was extracted from this item.'}</pre>
{preview.data.content_truncated && <div className="muted-copy">Preview shortened. The indexed source contains more text.</div>}
</>
)}
</div>
)}
<div className="library-file-actions">
<button className="button ghost" disabled={busy} onClick={() => toggleContentPreview(item)}>
{isExpanded ? 'Hide Content' : 'View Content'}
</button>
{item.kind === 'text' && <button className="button ghost" disabled={busy} onClick={() => editText(item)}>Edit</button>}
{item.kind === 'website' && <button className="button ghost" onClick={() => desktopApi.openExternalLink(item.url)}>Open</button>}
{item.kind === 'website' && <button className="button ghost" disabled={busy} onClick={() => refreshWebsite(item)}>Refresh</button>}

View File

@@ -1796,6 +1796,43 @@ input:checked + .slider:before {
border-color: color-mix(in srgb, #69c4a1 34%, var(--border));
}
.library-content-card.expanded {
grid-column: 1 / -1;
}
.library-content-preview {
margin-top: 12px;
padding: 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: color-mix(in srgb, var(--bg) 82%, black);
}
.library-content-preview-label {
margin-bottom: 8px;
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.library-content-preview pre {
max-height: 420px;
margin: 0;
overflow: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
color: var(--text);
font: inherit;
font-size: 13px;
line-height: 1.55;
}
.library-content-preview .muted-copy {
margin-top: 8px;
}
.library-kind-badge {
display: inline-flex;
padding: 4px 8px;