feat: brain cards show PDF thumbnails, image previews, extracted text

- PDF cards: show first page render as thumbnail with "PDF" badge
- Image cards: show the original uploaded image
- PDF detail sheet: shows screenshot + extracted text in mono font
- Image detail sheet: shows the original image
- Card content shows page count for PDFs, extracted text preview
- Links still open URL on screenshot click, PDFs/images open detail

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-01 18:57:14 -05:00
parent b179386a57
commit 3264aad614

View File

@@ -332,7 +332,7 @@
<div class="masonry"> <div class="masonry">
{#each items as item (item.id)} {#each items as item (item.id)}
<div class="card" class:is-note={item.type === 'note'} class:is-processing={item.processing_status !== 'ready'}> <div class="card" class:is-note={item.type === 'note'} class:is-processing={item.processing_status !== 'ready'}>
<!-- Screenshot for links — clicking opens the URL --> <!-- Thumbnail: screenshot for links/PDFs, original image for images -->
{#if item.type === 'link' && item.assets?.some(a => a.asset_type === 'screenshot')} {#if item.type === 'link' && item.assets?.some(a => a.asset_type === 'screenshot')}
<a class="card-thumb" href={item.url} target="_blank" rel="noopener"> <a class="card-thumb" href={item.url} target="_blank" rel="noopener">
<img src="/api/brain/storage/{item.id}/screenshot/screenshot.png" alt="" loading="lazy" /> <img src="/api/brain/storage/{item.id}/screenshot/screenshot.png" alt="" loading="lazy" />
@@ -343,8 +343,17 @@
</div> </div>
{/if} {/if}
</a> </a>
{:else if item.type === 'pdf' && item.assets?.some(a => a.asset_type === 'screenshot')}
<button class="card-thumb" onclick={() => { selectedItem = item; editingNote = false; }}>
<img src="/api/brain/storage/{item.id}/screenshot/screenshot.png" alt="" loading="lazy" />
<div class="card-type-badge">PDF</div>
</button>
{:else if item.type === 'image' && item.assets?.some(a => a.asset_type === 'original_upload')}
{@const imgAsset = item.assets.find(a => a.asset_type === 'original_upload')}
<button class="card-thumb" onclick={() => { selectedItem = item; editingNote = false; }}>
<img src="/api/brain/storage/{item.id}/original_upload/{imgAsset?.filename}" alt="" loading="lazy" />
</button>
{:else if item.type === 'note'} {:else if item.type === 'note'}
<!-- Note — clicking opens detail for editing -->
<button class="card-note-body" onclick={() => { selectedItem = item; editingNote = false; }}> <button class="card-note-body" onclick={() => { selectedItem = item; editingNote = false; }}>
{(item.raw_content || '').slice(0, 200)}{(item.raw_content || '').length > 200 ? '...' : ''} {(item.raw_content || '').slice(0, 200)}{(item.raw_content || '').length > 200 ? '...' : ''}
</button> </button>
@@ -360,8 +369,14 @@
<div class="card-title">{item.title || 'Untitled'}</div> <div class="card-title">{item.title || 'Untitled'}</div>
{#if item.url} {#if item.url}
<div class="card-domain">{(() => { try { return new URL(item.url).hostname; } catch { return ''; } })()}</div> <div class="card-domain">{(() => { try { return new URL(item.url).hostname; } catch { return ''; } })()}</div>
{:else if item.type === 'pdf'}
<div class="card-domain">PDF document{item.metadata_json?.page_count ? ` · ${item.metadata_json.page_count} page${item.metadata_json.page_count !== 1 ? 's' : ''}` : ''}</div>
{:else if item.type === 'image'}
<div class="card-domain">Image</div>
{/if} {/if}
{#if item.summary && item.type !== 'note'} {#if item.type === 'pdf' && item.extracted_text}
<div class="card-summary">{item.extracted_text.slice(0, 120)}{item.extracted_text.length > 120 ? '...' : ''}</div>
{:else if item.summary && item.type !== 'note'}
<div class="card-summary">{item.summary.slice(0, 100)}{item.summary.length > 100 ? '...' : ''}</div> <div class="card-summary">{item.summary.slice(0, 100)}{item.summary.length > 100 ? '...' : ''}</div>
{/if} {/if}
<div class="card-footer"> <div class="card-footer">
@@ -401,21 +416,36 @@
</button> </button>
</div> </div>
<!-- Link: show screenshot + URL + tags --> <!-- Screenshot/image preview -->
{#if selectedItem.type === 'link'} {#if selectedItem.type === 'link' && selectedItem.assets?.some(a => a.asset_type === 'screenshot')}
{#if selectedItem.assets?.some(a => a.asset_type === 'screenshot')} <a class="detail-screenshot" href={selectedItem.url} target="_blank" rel="noopener">
<a class="detail-screenshot" href={selectedItem.url} target="_blank" rel="noopener"> <img src="/api/brain/storage/{selectedItem.id}/screenshot/screenshot.png" alt="" />
<img src="/api/brain/storage/{selectedItem.id}/screenshot/screenshot.png" alt="" /> </a>
</a> {:else if selectedItem.type === 'pdf' && selectedItem.assets?.some(a => a.asset_type === 'screenshot')}
{/if} <div class="detail-screenshot">
<img src="/api/brain/storage/{selectedItem.id}/screenshot/screenshot.png" alt="" />
</div>
{:else if selectedItem.type === 'image' && selectedItem.assets?.some(a => a.asset_type === 'original_upload')}
{@const imgAsset = selectedItem.assets.find(a => a.asset_type === 'original_upload')}
<div class="detail-screenshot">
<img src="/api/brain/storage/{selectedItem.id}/original_upload/{imgAsset?.filename}" alt="" />
</div>
{/if}
{#if selectedItem.url} {#if selectedItem.url}
<a class="detail-url" href={selectedItem.url} target="_blank" rel="noopener">{selectedItem.url}</a> <a class="detail-url" href={selectedItem.url} target="_blank" rel="noopener">{selectedItem.url}</a>
{/if} {/if}
{#if selectedItem.summary} {#if selectedItem.summary && selectedItem.type !== 'note'}
<div class="detail-summary">{selectedItem.summary}</div> <div class="detail-summary">{selectedItem.summary}</div>
{/if} {/if}
<!-- Extracted text preview for PDFs/docs -->
{#if selectedItem.extracted_text && selectedItem.type !== 'note' && selectedItem.type !== 'link'}
<div class="detail-extracted">
<div class="extracted-label">Extracted text</div>
<div class="extracted-body">{selectedItem.extracted_text.slice(0, 500)}{selectedItem.extracted_text.length > 500 ? '...' : ''}</div>
</div>
{/if} {/if}
<!-- Note: editable content --> <!-- Note: editable content -->
@@ -612,6 +642,20 @@
object-fit: cover; object-fit: cover;
max-height: 240px; max-height: 240px;
} }
.card-type-badge {
position: absolute;
top: 10px;
right: 10px;
background: rgba(30,24,18,0.75);
color: white;
font-size: 0.7rem;
font-weight: 700;
padding: 3px 8px;
border-radius: 6px;
letter-spacing: 0.04em;
backdrop-filter: blur(4px);
}
.card-processing-overlay { .card-processing-overlay {
position: absolute; inset: 0; position: absolute; inset: 0;
background: rgba(30,24,18,0.6); background: rgba(30,24,18,0.6);
@@ -815,6 +859,24 @@
font-style: italic; font-style: italic;
} }
.detail-extracted {
margin-bottom: 16px;
padding: 14px 16px;
background: rgba(255,255,255,0.5);
border-radius: 14px;
border: 1px solid rgba(35,26,17,0.06);
}
.extracted-label {
font-size: 10px; text-transform: uppercase;
letter-spacing: 0.1em; color: #8c7b69;
margin-bottom: 8px;
}
.extracted-body {
font-size: 0.88rem; color: #3d342c; line-height: 1.6;
white-space: pre-wrap; word-break: break-word;
font-family: var(--mono);
}
.detail-meta-line { .detail-meta-line {
display: flex; gap: 12px; font-size: 0.8rem; color: #8c7b69; display: flex; gap: 12px; font-size: 0.8rem; color: #8c7b69;
margin-bottom: 16px; margin-bottom: 16px;