fix: brain UX — links open URL, notes are editable, remove meta grid

- Link card screenshots now open the original URL in new tab
- Card content area opens the detail sheet
- Notes: clicking the note body in detail sheet enters edit mode
- Removed Folder/Confidence/Status/Saved meta grid from detail
- Replaced with single inline folder + date line
- Tags still clickable for filtering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-01 17:56:04 -05:00
parent b26392a2ca
commit 6694795726

View File

@@ -40,6 +40,8 @@
// Detail
let selectedItem = $state<BrainItem | null>(null);
let editingNote = $state(false);
let editNoteContent = $state('');
// Folder counts
let folderCounts = $state<Record<string, number>>({});
@@ -117,6 +119,26 @@
searching = false;
}
function startEditNote() {
if (!selectedItem) return;
editNoteContent = selectedItem.raw_content || '';
editingNote = true;
}
async function saveNote() {
if (!selectedItem) return;
try {
await api(`/items/${selectedItem.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ raw_content: editNoteContent }),
});
selectedItem.raw_content = editNoteContent;
editingNote = false;
await loadItems();
} catch { /* silent */ }
}
async function deleteItem(id: string) {
try {
await api(`/items/${id}`, { method: 'DELETE' });
@@ -309,10 +331,10 @@
{:else}
<div class="masonry">
{#each items as item (item.id)}
<button class="card" class:is-note={item.type === 'note'} class:is-processing={item.processing_status !== 'ready'} onclick={() => selectedItem = item}>
<!-- Screenshot for links -->
<div class="card" class:is-note={item.type === 'note'} class:is-processing={item.processing_status !== 'ready'}>
<!-- Screenshot for links — clicking opens the URL -->
{#if item.type === 'link' && item.assets?.some(a => a.asset_type === 'screenshot')}
<div class="card-thumb">
<a class="card-thumb" href={item.url} target="_blank" rel="noopener">
<img src="/api/brain/storage/{item.id}/screenshot/screenshot.png" alt="" loading="lazy" />
{#if item.processing_status !== 'ready'}
<div class="card-processing-overlay">
@@ -320,12 +342,12 @@
Processing...
</div>
{/if}
</div>
</a>
{:else if item.type === 'note'}
<!-- Note shows content directly -->
<div class="card-note-body">
<!-- Note — clicking opens detail for editing -->
<button class="card-note-body" onclick={() => { selectedItem = item; editingNote = false; }}>
{(item.raw_content || '').slice(0, 200)}{(item.raw_content || '').length > 200 ? '...' : ''}
</div>
</button>
{:else if item.processing_status !== 'ready'}
<div class="card-placeholder">
<span class="processing-dot"></span>
@@ -333,8 +355,8 @@
</div>
{/if}
<!-- Card content -->
<div class="card-content">
<!-- Card content — click opens detail -->
<button class="card-content" onclick={() => { selectedItem = item; editingNote = false; }}>
<div class="card-title">{item.title || 'Untitled'}</div>
{#if item.url}
<div class="card-domain">{(() => { try { return new URL(item.url).hostname; } catch { return ''; } })()}</div>
@@ -355,8 +377,8 @@
<span class="card-date">{formatDate(item.created_at)}</span>
</div>
</div>
</div>
</button>
</button>
</div>
{/each}
</div>
{/if}
@@ -383,39 +405,31 @@
<a class="detail-url" href={selectedItem.url} target="_blank" rel="noopener">{selectedItem.url}</a>
{/if}
{#if selectedItem.raw_content}
{#if selectedItem.type === 'note'}
<!-- Editable note content -->
<div class="detail-content">
<div class="content-label">Note</div>
<div class="content-body">{selectedItem.raw_content}</div>
{#if editingNote}
<textarea
class="note-editor"
bind:value={editNoteContent}
onkeydown={(e) => { if (e.key === 'Escape') editingNote = false; }}
></textarea>
<div class="note-editor-actions">
<button class="action-btn" onclick={saveNote}>Save</button>
<button class="action-btn ghost" onclick={() => editingNote = false}>Cancel</button>
</div>
{:else}
<button class="content-body clickable" onclick={startEditNote}>
{selectedItem.raw_content || 'Empty note — click to edit'}
</button>
{/if}
</div>
{/if}
{#if selectedItem.summary}
<div class="detail-summary">
<div class="content-label">AI Summary</div>
{selectedItem.summary}
</div>
<div class="detail-summary">{selectedItem.summary}</div>
{/if}
<div class="detail-meta-grid">
<div class="meta-block">
<div class="meta-label">Folder</div>
<div class="meta-value">{selectedItem.folder || '—'}</div>
</div>
<div class="meta-block">
<div class="meta-label">Confidence</div>
<div class="meta-value">{selectedItem.confidence ? (selectedItem.confidence * 100).toFixed(0) + '%' : '—'}</div>
</div>
<div class="meta-block">
<div class="meta-label">Status</div>
<div class="meta-value">{selectedItem.processing_status}</div>
</div>
<div class="meta-block">
<div class="meta-label">Saved</div>
<div class="meta-value">{new Date(selectedItem.created_at).toLocaleDateString()}</div>
</div>
</div>
{#if selectedItem.tags && selectedItem.tags.length > 0}
<div class="detail-tags">
{#each selectedItem.tags as tag}
@@ -424,12 +438,17 @@
</div>
{/if}
<div class="detail-meta-line">
{#if selectedItem.folder}<span>{selectedItem.folder}</span>{/if}
<span>{formatDate(selectedItem.created_at)}</span>
</div>
<div class="detail-actions">
<button class="action-btn" onclick={() => reprocessItem(selectedItem.id)}>Reprocess</button>
<button class="action-btn ghost" onclick={() => { if (confirm('Delete this item?')) deleteItem(selectedItem.id); }}>Delete</button>
{#if selectedItem.url}
<a class="action-btn" href={selectedItem.url} target="_blank" rel="noopener">Open original</a>
{/if}
<button class="action-btn ghost" onclick={() => reprocessItem(selectedItem.id)}>Reclassify</button>
<button class="action-btn ghost" onclick={() => { if (confirm('Delete this item?')) deleteItem(selectedItem.id); }}>Delete</button>
</div>
</div>
</div>
@@ -568,12 +587,14 @@
.card.is-processing { opacity: 0.7; }
/* Card thumbnail */
/* Card thumbnail — link opens URL */
.card-thumb {
display: block;
width: 100%;
position: relative;
overflow: hidden;
background: rgba(244,237,229,0.6);
cursor: pointer;
}
.card-thumb img {
width: 100%;
@@ -594,7 +615,7 @@
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
/* Note card body */
/* Note card body — click opens detail */
.card-note-body {
padding: 18px 18px 0;
font-size: 0.92rem;
@@ -602,6 +623,12 @@
line-height: 1.65;
white-space: pre-wrap;
word-break: break-word;
background: none;
border: none;
text-align: left;
font-family: var(--font);
cursor: pointer;
width: 100%;
}
.card-placeholder {
@@ -611,10 +638,18 @@
background: rgba(244,237,229,0.4);
}
/* Card content */
/* Card content — button for detail click */
.card-content {
padding: 14px 18px 16px;
background: none;
border: none;
text-align: left;
font-family: var(--font);
cursor: pointer;
width: 100%;
transition: background 160ms;
}
.card-content:hover { background: rgba(255,248,242,0.5); }
.card-title {
font-size: 0.95rem;
@@ -728,33 +763,41 @@
.detail-url:hover { color: #1e1812; text-decoration: underline; }
.detail-content {
margin-bottom: 20px; padding-bottom: 20px;
border-bottom: 1px solid rgba(35,26,17,0.08);
}
.content-label {
font-size: 11px; text-transform: uppercase;
letter-spacing: 0.12em; color: #7d6f61; margin-bottom: 8px;
margin-bottom: 20px;
}
.content-body {
display: block; width: 100%; text-align: left;
font-size: 1rem; color: #1e1812; line-height: 1.7;
white-space: pre-wrap; word-break: break-word;
background: none; border: none; font-family: var(--font);
padding: 14px 16px; border-radius: 14px;
transition: background 160ms;
}
.content-body.clickable { cursor: text; }
.content-body.clickable:hover { background: rgba(255,255,255,0.6); }
.note-editor {
width: 100%; min-height: 200px; padding: 14px 16px;
border-radius: 14px; border: 1.5px solid rgba(179,92,50,0.3);
background: rgba(255,255,255,0.8); color: #1e1812;
font-size: 1rem; font-family: var(--font); line-height: 1.7;
resize: vertical; outline: none;
}
.note-editor:focus { border-color: rgba(179,92,50,0.5); box-shadow: 0 0 0 4px rgba(179,92,50,0.06); }
.note-editor-actions { display: flex; gap: 8px; margin-top: 10px; }
.detail-summary {
font-size: 0.95rem; color: #3d342c; line-height: 1.6;
margin-bottom: 20px; padding-bottom: 20px;
border-bottom: 1px solid rgba(35,26,17,0.08);
font-size: 0.92rem; color: #5c5046; line-height: 1.6;
margin-bottom: 16px;
font-style: italic;
}
.detail-meta-grid {
display: grid; grid-template-columns: 1fr 1fr;
gap: 12px; margin-bottom: 20px;
.detail-meta-line {
display: flex; gap: 12px; font-size: 0.8rem; color: #8c7b69;
margin-bottom: 16px;
}
.meta-block {
background: rgba(255,255,255,0.58); border-radius: 14px;
padding: 14px; border: 1px solid rgba(35,26,17,0.06);
}
.meta-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; color: #7d6f61; margin-bottom: 4px; }
.meta-value { font-size: 1rem; font-weight: 600; color: #1e1812; }
/* meta grid removed — folder/date shown inline */
.detail-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
.detail-tag {
@@ -797,6 +840,6 @@
.signal-strip { grid-template-columns: 1fr 1fr; }
.masonry { columns: 1; }
.detail-sheet { width: 100%; padding: 20px; }
.detail-meta-grid { grid-template-columns: 1fr; }
/* detail meta grid removed */
}
</style>