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