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:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user