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
|
// Detail
|
||||||
let selectedItem = $state<BrainItem | null>(null);
|
let selectedItem = $state<BrainItem | null>(null);
|
||||||
|
let editingNote = $state(false);
|
||||||
|
let editNoteContent = $state('');
|
||||||
|
|
||||||
// Folder counts
|
// Folder counts
|
||||||
let folderCounts = $state<Record<string, number>>({});
|
let folderCounts = $state<Record<string, number>>({});
|
||||||
@@ -117,6 +119,26 @@
|
|||||||
searching = false;
|
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) {
|
async function deleteItem(id: string) {
|
||||||
try {
|
try {
|
||||||
await api(`/items/${id}`, { method: 'DELETE' });
|
await api(`/items/${id}`, { method: 'DELETE' });
|
||||||
@@ -309,10 +331,10 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="masonry">
|
<div class="masonry">
|
||||||
{#each items as item (item.id)}
|
{#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}>
|
<div class="card" class:is-note={item.type === 'note'} class:is-processing={item.processing_status !== 'ready'}>
|
||||||
<!-- Screenshot for links -->
|
<!-- Screenshot for links — clicking opens the URL -->
|
||||||
{#if item.type === 'link' && item.assets?.some(a => a.asset_type === 'screenshot')}
|
{#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" />
|
<img src="/api/brain/storage/{item.id}/screenshot/screenshot.png" alt="" loading="lazy" />
|
||||||
{#if item.processing_status !== 'ready'}
|
{#if item.processing_status !== 'ready'}
|
||||||
<div class="card-processing-overlay">
|
<div class="card-processing-overlay">
|
||||||
@@ -320,12 +342,12 @@
|
|||||||
Processing...
|
Processing...
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</a>
|
||||||
{:else if item.type === 'note'}
|
{:else if item.type === 'note'}
|
||||||
<!-- Note shows content directly -->
|
<!-- Note — clicking opens detail for editing -->
|
||||||
<div class="card-note-body">
|
<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 ? '...' : ''}
|
||||||
</div>
|
</button>
|
||||||
{:else if item.processing_status !== 'ready'}
|
{:else if item.processing_status !== 'ready'}
|
||||||
<div class="card-placeholder">
|
<div class="card-placeholder">
|
||||||
<span class="processing-dot"></span>
|
<span class="processing-dot"></span>
|
||||||
@@ -333,8 +355,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Card content -->
|
<!-- Card content — click opens detail -->
|
||||||
<div class="card-content">
|
<button class="card-content" onclick={() => { selectedItem = item; editingNote = false; }}>
|
||||||
<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>
|
||||||
@@ -355,8 +377,8 @@
|
|||||||
<span class="card-date">{formatDate(item.created_at)}</span>
|
<span class="card-date">{formatDate(item.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -383,39 +405,31 @@
|
|||||||
<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.raw_content}
|
{#if selectedItem.type === 'note'}
|
||||||
|
<!-- Editable note content -->
|
||||||
<div class="detail-content">
|
<div class="detail-content">
|
||||||
<div class="content-label">Note</div>
|
{#if editingNote}
|
||||||
<div class="content-body">{selectedItem.raw_content}</div>
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if selectedItem.summary}
|
{#if selectedItem.summary}
|
||||||
<div class="detail-summary">
|
<div class="detail-summary">{selectedItem.summary}</div>
|
||||||
<div class="content-label">AI Summary</div>
|
|
||||||
{selectedItem.summary}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/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}
|
{#if selectedItem.tags && selectedItem.tags.length > 0}
|
||||||
<div class="detail-tags">
|
<div class="detail-tags">
|
||||||
{#each selectedItem.tags as tag}
|
{#each selectedItem.tags as tag}
|
||||||
@@ -424,12 +438,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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">
|
<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}
|
{#if selectedItem.url}
|
||||||
<a class="action-btn" href={selectedItem.url} target="_blank" rel="noopener">Open original</a>
|
<a class="action-btn" href={selectedItem.url} target="_blank" rel="noopener">Open original</a>
|
||||||
{/if}
|
{/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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -568,12 +587,14 @@
|
|||||||
|
|
||||||
.card.is-processing { opacity: 0.7; }
|
.card.is-processing { opacity: 0.7; }
|
||||||
|
|
||||||
/* Card thumbnail */
|
/* Card thumbnail — link opens URL */
|
||||||
.card-thumb {
|
.card-thumb {
|
||||||
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: rgba(244,237,229,0.6);
|
background: rgba(244,237,229,0.6);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.card-thumb img {
|
.card-thumb img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -594,7 +615,7 @@
|
|||||||
}
|
}
|
||||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
||||||
|
|
||||||
/* Note card body */
|
/* Note card body — click opens detail */
|
||||||
.card-note-body {
|
.card-note-body {
|
||||||
padding: 18px 18px 0;
|
padding: 18px 18px 0;
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
@@ -602,6 +623,12 @@
|
|||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
font-family: var(--font);
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-placeholder {
|
.card-placeholder {
|
||||||
@@ -611,10 +638,18 @@
|
|||||||
background: rgba(244,237,229,0.4);
|
background: rgba(244,237,229,0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card content */
|
/* Card content — button for detail click */
|
||||||
.card-content {
|
.card-content {
|
||||||
padding: 14px 18px 16px;
|
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 {
|
.card-title {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
@@ -728,33 +763,41 @@
|
|||||||
.detail-url:hover { color: #1e1812; text-decoration: underline; }
|
.detail-url:hover { color: #1e1812; text-decoration: underline; }
|
||||||
|
|
||||||
.detail-content {
|
.detail-content {
|
||||||
margin-bottom: 20px; padding-bottom: 20px;
|
margin-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;
|
|
||||||
}
|
}
|
||||||
.content-body {
|
.content-body {
|
||||||
|
display: block; width: 100%; text-align: left;
|
||||||
font-size: 1rem; color: #1e1812; line-height: 1.7;
|
font-size: 1rem; color: #1e1812; line-height: 1.7;
|
||||||
white-space: pre-wrap; word-break: break-word;
|
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 {
|
.detail-summary {
|
||||||
font-size: 0.95rem; color: #3d342c; line-height: 1.6;
|
font-size: 0.92rem; color: #5c5046; line-height: 1.6;
|
||||||
margin-bottom: 20px; padding-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
border-bottom: 1px solid rgba(35,26,17,0.08);
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-meta-grid {
|
.detail-meta-line {
|
||||||
display: grid; grid-template-columns: 1fr 1fr;
|
display: flex; gap: 12px; font-size: 0.8rem; color: #8c7b69;
|
||||||
gap: 12px; margin-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.meta-block {
|
|
||||||
background: rgba(255,255,255,0.58); border-radius: 14px;
|
/* meta grid removed — folder/date shown inline */
|
||||||
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; }
|
|
||||||
|
|
||||||
.detail-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
|
.detail-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
|
||||||
.detail-tag {
|
.detail-tag {
|
||||||
@@ -797,6 +840,6 @@
|
|||||||
.signal-strip { grid-template-columns: 1fr 1fr; }
|
.signal-strip { grid-template-columns: 1fr 1fr; }
|
||||||
.masonry { columns: 1; }
|
.masonry { columns: 1; }
|
||||||
.detail-sheet { width: 100%; padding: 20px; }
|
.detail-sheet { width: 100%; padding: 20px; }
|
||||||
.detail-meta-grid { grid-template-columns: 1fr; }
|
/* detail meta grid removed */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user