From 6565b23deb28242757ce00694ba474f908279c7e Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Wed, 1 Apr 2026 17:42:59 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20brain=20masonry=20card=20grid=20?= =?UTF-8?q?=E2=80=94=20Karakeep-style=20layout=20with=20Atelier=20aestheti?= =?UTF-8?q?cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3-column CSS masonry grid (2 on tablet, 1 on mobile) - Link cards show screenshot thumbnails - Note cards show content body inline - Tags as pills, folder/date in meta footer - Screenshot serving endpoint added to brain API - Auto-polling for pending items (3s interval) - Detail sheet shows raw_content for notes - Warm frosted glass card styling matching Atelier design Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lib/pages/brain/AtelierBrainPage.svelte | 449 ++++++++++++------ services/brain/app/api/routes.py | 20 + services/brain/app/models/schema.py | 2 + 3 files changed, 323 insertions(+), 148 deletions(-) diff --git a/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte b/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte index 22567c1..b8dfacc 100644 --- a/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte +++ b/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte @@ -1,11 +1,15 @@
@@ -220,94 +256,95 @@ {/each} - -
-
- -
-
-
-
Search
-
Find saved items
-
-
{items.length} item{items.length !== 1 ? 's' : ''}
-
-
- - - {#if searchQuery} - - {/if} -
-
- -
-
-
Feed
-
{activeFolder || 'All items'}
-
- {#if activeFolder} - Items the AI classified under {activeFolder.toLowerCase()}. - {:else} - Your complete saved collection, newest first. - {/if} -
-
-
- - {#if loading} -
- {#each [1, 2, 3, 4] as _} -
- {/each} -
- {:else if items.length === 0} -
-
No items yet. Paste a URL or note above to get started.
-
- {:else} -
- {#each items as item (item.id)} - - {/each} -
+ +
+
+ + + {#if searchQuery} + {/if}
+ + {#if loading} +
+ {#each [1, 2, 3, 4, 5, 6] as _} +
+ {/each} +
+ {:else if items.length === 0} +
+
+ +
+
Nothing saved yet
+
Paste a URL or type a note in the capture bar above.
+
+ {:else} +
+ {#each items as item (item.id)} + + {/each} +
+ {/if} +
@@ -330,8 +367,18 @@ {selectedItem.url} {/if} + {#if selectedItem.raw_content} +
+ +
{selectedItem.raw_content}
+
+ {/if} + {#if selectedItem.summary} -
{selectedItem.summary}
+
+ + {selectedItem.summary} +
{/if}
@@ -440,80 +487,170 @@ .signal-value { font-size: clamp(1.6rem, 3vw, 2.2rem); line-height: 0.95; letter-spacing: -0.05em; color: #1e1812; } .signal-note { color: #4f463d; line-height: 1.6; font-size: 0.85rem; } - /* ═══ Layout ═══ */ - .brain-layout { display: grid; gap: 18px; } - .brain-main { - border-radius: 28px; border: 1px solid rgba(35,26,17,0.08); - background: linear-gradient(180deg, rgba(255,252,248,0.84), rgba(244,237,229,0.74)); - padding: 16px; - } - - /* ═══ Toolbar ═══ */ - .toolbar-card { - border-radius: 28px; border: 1px solid rgba(35,26,17,0.08); - background: rgba(255,255,255,0.42); padding: 16px 16px 12px; margin-bottom: 8px; - } - .toolbar-head { display: flex; align-items: end; justify-content: space-between; gap: 12px; margin-bottom: 14px; } - .toolbar-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; color: #7d6f61; } - .toolbar-title { margin-top: 4px; font-size: 1.1rem; letter-spacing: -0.035em; color: #1e1812; } - .toolbar-meta { color: #4f463d; font-size: 0.88rem; } - + /* ═══ Search ═══ */ + .search-section { margin-bottom: 18px; } .search-wrap { position: relative; } - .search-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: #7f7365; pointer-events: none; } + .search-icon { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: #7f7365; pointer-events: none; } .search-input { - width: 100%; padding: 12px 40px 12px 42px; - border-radius: var(--radius); border: 1.5px solid rgba(35,26,17,0.12); - background: rgba(255,255,255,0.92); color: #1e1812; - font-size: var(--text-md); font-family: var(--font); - box-shadow: inset 0 1px 0 rgba(255,255,255,0.5); + width: 100%; padding: 14px 40px 14px 46px; + border-radius: 28px; border: 1px solid rgba(35,26,17,0.08); + background: rgba(255,252,248,0.68); backdrop-filter: blur(14px); + color: #1e1812; font-size: 1rem; font-family: var(--font); } .search-input::placeholder { color: #8b7b6a; } - .search-input:focus { outline: none; border-color: rgba(179,92,50,0.5); box-shadow: 0 0 0 4px rgba(179,92,50,0.08); } - .search-clear { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #7f7365; } + .search-input:focus { outline: none; border-color: rgba(179,92,50,0.4); box-shadow: 0 0 0 4px rgba(179,92,50,0.06); background: rgba(255,255,255,0.9); } + .search-clear { position: absolute; right: 14px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #7f7365; } .search-clear svg { width: 16px; height: 16px; } - /* ═══ Queue ═══ */ - .queue-header { display: flex; align-items: end; justify-content: space-between; gap: 14px; padding: 8px 4px 14px; } - .queue-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; color: #7d6f61; } - .queue-title { margin-top: 4px; font-size: 1.35rem; letter-spacing: -0.04em; color: #1e1812; } - .queue-summary { margin-top: 6px; color: #6a5d50; font-size: 0.94rem; line-height: 1.45; } - - /* ═══ Items ═══ */ - .items-card { - background: rgba(255,255,255,0.82); border-radius: 24px; - border: 1px solid rgba(35,26,17,0.06); overflow: hidden; - box-shadow: 0 16px 44px rgba(42,30,19,0.05); + /* ═══ Masonry grid ═══ */ + .masonry { + columns: 3; + column-gap: 14px; } - .item-row { - display: grid; grid-template-columns: 4px minmax(0, 1fr) auto; - align-items: center; gap: 16px; padding: 18px 16px; - width: 100%; background: none; border: none; text-align: left; - transition: background 160ms ease, transform 160ms ease; + + .card { + break-inside: avoid; + display: flex; + flex-direction: column; + border-radius: 20px; + border: 1px solid rgba(35,26,17,0.08); + background: rgba(255,252,248,0.82); + backdrop-filter: blur(10px); + overflow: hidden; + margin-bottom: 14px; + width: 100%; + text-align: left; + transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease; } - .item-row:hover { background: rgba(255,248,242,0.88); transform: translateX(2px); } - .item-row + .item-row { border-top: 1px solid rgba(35,26,17,0.07); } + .card:hover { + transform: translateY(-3px); + box-shadow: 0 12px 32px rgba(42,30,19,0.08); + border-color: rgba(35,26,17,0.14); + } + .card:active { transform: scale(0.985); } - .row-accent { align-self: stretch; border-radius: 999px; background: rgba(35,26,17,0.08); } - .row-accent.processing { background: linear-gradient(180deg, #f6a33a, #e28615); } - .row-accent.failed { background: linear-gradient(180deg, #ff5d45, #ef3d2f); } + .card.is-processing { opacity: 0.7; } - .item-info { flex: 1; min-width: 0; } - .item-name { font-size: 1rem; font-weight: 700; color: #1e1812; line-height: 1.25; } - .item-meta { display: flex; flex-wrap: wrap; gap: 8px 12px; margin-top: 6px; color: #5d5248; font-size: 0.88rem; } - .meta-folder { font-weight: 600; color: #8c7b69; } - .meta-url { color: #7d6f61; } - .meta-tag { background: rgba(35,26,17,0.06); padding: 1px 8px; border-radius: 999px; font-size: 0.8rem; color: #5d5248; } + /* Card thumbnail */ + .card-thumb { + width: 100%; + position: relative; + overflow: hidden; + background: rgba(244,237,229,0.6); + } + .card-thumb img { + width: 100%; + height: auto; + display: block; + object-fit: cover; + max-height: 240px; + } + .card-processing-overlay { + position: absolute; inset: 0; + background: rgba(30,24,18,0.6); + display: flex; align-items: center; justify-content: center; gap: 6px; + color: white; font-size: 0.85rem; font-weight: 600; + } + .processing-dot { + width: 6px; height: 6px; border-radius: 50%; background: #f6a33a; + animation: pulse 1.5s ease-in-out infinite; + } + @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } - .item-tail { display: flex; align-items: center; gap: 12px; margin-left: auto; } - .conf-dot { width: 8px; height: 8px; border-radius: 50%; } - .conf-dot.high { background: #059669; } - .conf-dot.med { background: #D97706; } - .status-badge { font-size: 11px; font-weight: 700; padding: 5px 10px; border-radius: 999px; flex-shrink: 0; } - .processing-badge { background: rgba(217,119,6,0.12); color: #9a5d09; } - .status-badge.error { background: var(--error-dim); color: var(--error); } - .row-chevron { width: 14px; height: 14px; color: #7d6f61; flex-shrink: 0; opacity: 0.7; } + /* Note card body */ + .card-note-body { + padding: 18px 18px 0; + font-size: 0.92rem; + color: #3d342c; + line-height: 1.65; + white-space: pre-wrap; + word-break: break-word; + } - .empty { padding: 48px; text-align: center; color: #5f564b; font-size: 1rem; } + .card-placeholder { + padding: 32px; + display: flex; align-items: center; justify-content: center; gap: 6px; + color: #8c7b69; font-size: 0.85rem; + background: rgba(244,237,229,0.4); + } + + /* Card content */ + .card-content { + padding: 14px 18px 16px; + } + + .card-title { + font-size: 0.95rem; + font-weight: 700; + color: #1e1812; + line-height: 1.3; + margin-bottom: 4px; + } + + .card-domain { + font-size: 0.8rem; + color: #8c7b69; + margin-bottom: 6px; + } + + .card-summary { + font-size: 0.82rem; + color: #5c5046; + line-height: 1.5; + margin-bottom: 8px; + } + + .card-footer { + display: flex; + flex-direction: column; + gap: 6px; + } + + .card-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; + } + + .card-tag { + background: rgba(35,26,17,0.06); + padding: 2px 8px; + border-radius: 999px; + font-size: 0.72rem; + color: #5d5248; + font-weight: 500; + } + + .card-meta { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.75rem; + color: #8c7b69; + } + + .card-folder { + font-weight: 600; + } + + /* Skeleton cards */ + .skeleton-card { + height: 200px; + background: linear-gradient(90deg, rgba(244,237,229,0.5) 25%, rgba(255,252,248,0.8) 50%, rgba(244,237,229,0.5) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + margin-bottom: 14px; + break-inside: avoid; + } + + /* Empty state */ + .empty-state { + padding: 64px 32px; + text-align: center; + } + .empty-icon { color: #8c7b69; margin-bottom: 16px; } + .empty-title { font-size: 1.2rem; font-weight: 700; color: #1e1812; margin-bottom: 6px; } + .empty-desc { font-size: 0.95rem; color: #6a5d50; } /* ═══ Detail sheet ═══ */ .detail-overlay { @@ -545,8 +682,20 @@ } .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; + } + .content-body { + font-size: 1rem; color: #1e1812; line-height: 1.7; + white-space: pre-wrap; word-break: break-word; + } .detail-summary { - font-size: 1rem; color: #3d342c; line-height: 1.6; + 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); } @@ -591,10 +740,14 @@ @keyframes sheetIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } /* ═══ Mobile ═══ */ + @media (max-width: 1100px) { + .masonry { columns: 2; } + } @media (max-width: 768px) { .brain-command { display: grid; gap: 14px; } .command-actions { justify-items: start; } .signal-strip { grid-template-columns: 1fr 1fr; } + .masonry { columns: 1; } .detail-sheet { width: 100%; padding: 20px; } .detail-meta-grid { grid-template-columns: 1fr; } } diff --git a/services/brain/app/api/routes.py b/services/brain/app/api/routes.py index 049e854..eb1bb1d 100644 --- a/services/brain/app/api/routes.py +++ b/services/brain/app/api/routes.py @@ -19,6 +19,7 @@ from app.models.schema import ( HybridSearchQuery, SearchResult, ConfigOut, ) from app.services.storage import storage +from fastapi.responses import Response from app.worker.tasks import enqueue_process_item router = APIRouter(prefix="/api", tags=["brain"]) @@ -317,3 +318,22 @@ async def hybrid_search( folder=body.folder, tags=body.tags, item_type=body.type, limit=body.limit, ) return SearchResult(items=items, total=len(items), query=body.q) + + +# ── Serve stored files (screenshots, archived HTML) ── + +@router.get("/storage/{item_id}/{asset_type}/{filename}") +async def serve_asset(item_id: str, asset_type: str, filename: str): + """Serve a stored asset file.""" + path = f"{item_id}/{asset_type}/{filename}" + if not storage.exists(path): + raise HTTPException(status_code=404, detail="Asset not found") + + data = storage.read(path) + ct = "application/octet-stream" + if filename.endswith(".png"): ct = "image/png" + elif filename.endswith(".jpg") or filename.endswith(".jpeg"): ct = "image/jpeg" + elif filename.endswith(".html"): ct = "text/html" + elif filename.endswith(".pdf"): ct = "application/pdf" + + return Response(content=data, media_type=ct, headers={"Cache-Control": "public, max-age=3600"}) diff --git a/services/brain/app/models/schema.py b/services/brain/app/models/schema.py index 89c8b37..9d00061 100644 --- a/services/brain/app/models/schema.py +++ b/services/brain/app/models/schema.py @@ -68,6 +68,8 @@ class ItemOut(BaseModel): type: str title: Optional[str] = None url: Optional[str] = None + raw_content: Optional[str] = None + extracted_text: Optional[str] = None folder: Optional[str] = None tags: Optional[list[str]] = None summary: Optional[str] = None