From 4592e35732942afcfb5730db9a551ab5168359ed Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Fri, 3 Apr 2026 00:56:29 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20major=20platform=20expansion=20?= =?UTF-8?q?=E2=80=94=20Brain=20service,=20RSS=20reader,=20iOS=20app,=20AI?= =?UTF-8?q?=20assistants,=20Firefox=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brain Service: - Playwright stealth crawler replacing browserless (og:image, Readability, Reddit JSON API) - AI classification with tag definitions and folder assignment - YouTube video download via yt-dlp - Karakeep migration complete (96 items) - Taxonomy management (folders with icons/colors, tags) - Discovery shuffle, sort options, search (Meilisearch + pgvector) - Item tag/folder editing, card color accents RSS Reader Service: - Custom FastAPI reader replacing Miniflux - Feed management (add/delete/refresh), category support - Full article extraction via Readability - Background content fetching for new entries - Mark all read with confirmation - Infinite scroll, retention cleanup (30/60 day) - 17 feeds migrated from Miniflux iOS App (SwiftUI): - Native iOS 17+ app with @Observable architecture - Cookie-based auth, configurable gateway URL - Dashboard with custom background photo + frosted glass widgets - Full fitness module (today/templates/goals/food library) - AI assistant chat (fitness + brain, raw JSON state management) - 120fps ProMotion support AI Assistants (Gateway): - Unified dispatcher with fitness/brain domain detection - Fitness: natural language food logging, photo analysis, multi-item splitting - Brain: save/append/update/delete notes, search & answer, undo support - Madiha user gets fitness-only (brain disabled) Firefox Extension: - One-click save to Brain from any page - Login with platform credentials - Right-click context menu (save page/link/image) - Notes field for URL saves - Signed and published on AMO Other: - Reader bookmark button routes to Brain (was Karakeep) - Fitness food library with "Add" button + add-to-meal popup - Kindle send file size check (25MB SMTP2GO limit) - Atelier UI as default (useAtelierShell=true) - Mobile upload box in nav drawer Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 25 + docker-compose.yml | 3 + extensions/brain-firefox/background.js | 250 +++ extensions/brain-firefox/brain-firefox.xpi | Bin 0 -> 8387 bytes extensions/brain-firefox/manifest.json | 49 + extensions/brain-firefox/popup.html | 259 +++ extensions/brain-firefox/popup.js | 202 +++ extensions/brain-firefox/test-popup.html | 8 + frontend-v2/src/app.css | 7 +- frontend-v2/src/app.html | 3 + .../assistant/BrainAssistantDrawer.svelte | 480 +++++ .../assistant/FitnessAssistantDrawer.svelte | 163 +- .../src/lib/components/layout/AppShell.svelte | 59 +- .../lib/pages/brain/AtelierBrainPage.svelte | 591 +++++- .../pages/fitness/AtelierFitnessPage.svelte | 288 ++- .../lib/pages/reader/AtelierReaderPage.svelte | 437 +++-- .../src/routes/(app)/+layout.server.ts | 4 +- frontend-v2/src/routes/(app)/+layout.svelte | 3 +- frontend-v2/src/routes/assistant/+server.ts | 140 ++ .../src/routes/assistant/brain/+server.ts | 799 +++++++++ frontend-v2/static/manifest.json | 21 + gateway/Dockerfile | 2 +- gateway/assistant.py | 1580 +++++++++++++++++ gateway/config.py | 1 + gateway/database.py | 8 +- gateway/integrations/kindle.py | 20 +- gateway/proxy.py | 2 +- gateway/server.py | 25 +- .../Platform.xcodeproj/project.pbxproj | 467 +++++ .../contents.xcworkspacedata | 7 + .../AccentColor.colorset/Contents.json | 20 + .../AppIcon.appiconset/Contents.json | 13 + .../Platform/Assets.xcassets/Contents.json | 6 + ios/Platform/Platform/Config.swift | 5 + ios/Platform/Platform/ContentView.swift | 43 + ios/Platform/Platform/Core/APIClient.swift | 145 ++ ios/Platform/Platform/Core/AuthManager.swift | 104 ++ .../Assistant/AssistantChatView.swift | 285 +++ .../Assistant/AssistantViewModel.swift | 150 ++ .../Platform/Features/Auth/LoginView.swift | 80 + .../Features/Fitness/API/FitnessAPI.swift | 56 + .../Fitness/Models/FitnessModels.swift | 328 ++++ .../Repository/FitnessRepository.swift | 72 + .../ViewModels/FoodSearchViewModel.swift | 51 + .../Fitness/ViewModels/GoalsViewModel.swift | 54 + .../ViewModels/TemplatesViewModel.swift | 37 + .../Fitness/ViewModels/TodayViewModel.swift | 36 + .../Features/Fitness/Views/AddFoodSheet.swift | 171 ++ .../Fitness/Views/EntryDetailView.swift | 160 ++ .../Fitness/Views/FitnessTabView.swift | 83 + .../Fitness/Views/FoodLibraryView.swift | 82 + .../Fitness/Views/FoodSearchView.swift | 119 ++ .../Features/Fitness/Views/GoalsView.swift | 85 + .../Fitness/Views/MealSectionView.swift | 131 ++ .../Fitness/Views/TemplatesView.swift | 103 ++ .../Features/Fitness/Views/TodayView.swift | 96 + .../Platform/Features/Home/HomeView.swift | 161 ++ .../Features/Home/HomeViewModel.swift | 54 + ios/Platform/Platform/Info.plist | 10 + ios/Platform/Platform/PlatformApp.swift | 13 + .../Shared/Components/LoadingView.swift | 66 + .../Platform/Shared/Components/MacroBar.swift | 44 + .../Shared/Components/MacroRing.swift | 83 + .../Shared/Extensions/Color+Extensions.swift | 47 + .../Shared/Extensions/Date+Extensions.swift | 33 + services/brain/Dockerfile.worker | 4 +- services/brain/app/api/routes.py | 157 +- services/brain/app/config.py | 18 +- services/brain/app/main.py | 2 +- services/brain/app/models/item.py | 22 + services/brain/app/models/schema.py | 20 + services/brain/app/models/taxonomy.py | 27 +- services/brain/app/services/classify.py | 59 +- services/brain/app/services/ingest.py | 284 +-- services/brain/app/worker/tasks.py | 123 +- services/brain/crawler/Dockerfile | 20 + services/brain/crawler/package.json | 24 + services/brain/crawler/server.js | 370 ++++ services/brain/docker-compose.yml | 19 +- services/brain/migrate_karakeep.py | 254 +++ services/fitness/server.py | 18 + services/reader/Dockerfile.api | 22 + services/reader/Dockerfile.worker | 18 + services/reader/app/__init__.py | 0 services/reader/app/api/__init__.py | 0 services/reader/app/api/categories.py | 49 + services/reader/app/api/deps.py | 21 + services/reader/app/api/entries.py | 264 +++ services/reader/app/api/feeds.py | 242 +++ services/reader/app/config.py | 23 + services/reader/app/database.py | 18 + services/reader/app/main.py | 43 + services/reader/app/models.py | 74 + services/reader/app/worker/__init__.py | 0 services/reader/app/worker/tasks.py | 363 ++++ services/reader/docker-compose.yml | 43 + services/reader/requirements.txt | 11 + 97 files changed, 11009 insertions(+), 532 deletions(-) create mode 100644 extensions/brain-firefox/background.js create mode 100644 extensions/brain-firefox/brain-firefox.xpi create mode 100644 extensions/brain-firefox/manifest.json create mode 100644 extensions/brain-firefox/popup.html create mode 100644 extensions/brain-firefox/popup.js create mode 100644 extensions/brain-firefox/test-popup.html create mode 100644 frontend-v2/src/lib/components/assistant/BrainAssistantDrawer.svelte create mode 100644 frontend-v2/src/routes/assistant/+server.ts create mode 100644 frontend-v2/src/routes/assistant/brain/+server.ts create mode 100644 frontend-v2/static/manifest.json create mode 100644 gateway/assistant.py create mode 100644 ios/Platform/Platform.xcodeproj/project.pbxproj create mode 100644 ios/Platform/Platform.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Platform/Platform/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ios/Platform/Platform/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/Platform/Platform/Assets.xcassets/Contents.json create mode 100644 ios/Platform/Platform/Config.swift create mode 100644 ios/Platform/Platform/ContentView.swift create mode 100644 ios/Platform/Platform/Core/APIClient.swift create mode 100644 ios/Platform/Platform/Core/AuthManager.swift create mode 100644 ios/Platform/Platform/Features/Assistant/AssistantChatView.swift create mode 100644 ios/Platform/Platform/Features/Assistant/AssistantViewModel.swift create mode 100644 ios/Platform/Platform/Features/Auth/LoginView.swift create mode 100644 ios/Platform/Platform/Features/Fitness/API/FitnessAPI.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Models/FitnessModels.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Repository/FitnessRepository.swift create mode 100644 ios/Platform/Platform/Features/Fitness/ViewModels/FoodSearchViewModel.swift create mode 100644 ios/Platform/Platform/Features/Fitness/ViewModels/GoalsViewModel.swift create mode 100644 ios/Platform/Platform/Features/Fitness/ViewModels/TemplatesViewModel.swift create mode 100644 ios/Platform/Platform/Features/Fitness/ViewModels/TodayViewModel.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Views/AddFoodSheet.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Views/EntryDetailView.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Views/FitnessTabView.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Views/FoodLibraryView.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Views/FoodSearchView.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Views/GoalsView.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Views/MealSectionView.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Views/TemplatesView.swift create mode 100644 ios/Platform/Platform/Features/Fitness/Views/TodayView.swift create mode 100644 ios/Platform/Platform/Features/Home/HomeView.swift create mode 100644 ios/Platform/Platform/Features/Home/HomeViewModel.swift create mode 100644 ios/Platform/Platform/Info.plist create mode 100644 ios/Platform/Platform/PlatformApp.swift create mode 100644 ios/Platform/Platform/Shared/Components/LoadingView.swift create mode 100644 ios/Platform/Platform/Shared/Components/MacroBar.swift create mode 100644 ios/Platform/Platform/Shared/Components/MacroRing.swift create mode 100644 ios/Platform/Platform/Shared/Extensions/Color+Extensions.swift create mode 100644 ios/Platform/Platform/Shared/Extensions/Date+Extensions.swift create mode 100644 services/brain/crawler/Dockerfile create mode 100644 services/brain/crawler/package.json create mode 100644 services/brain/crawler/server.js create mode 100644 services/brain/migrate_karakeep.py create mode 100644 services/reader/Dockerfile.api create mode 100644 services/reader/Dockerfile.worker create mode 100644 services/reader/app/__init__.py create mode 100644 services/reader/app/api/__init__.py create mode 100644 services/reader/app/api/categories.py create mode 100644 services/reader/app/api/deps.py create mode 100644 services/reader/app/api/entries.py create mode 100644 services/reader/app/api/feeds.py create mode 100644 services/reader/app/config.py create mode 100644 services/reader/app/database.py create mode 100644 services/reader/app/main.py create mode 100644 services/reader/app/models.py create mode 100644 services/reader/app/worker/__init__.py create mode 100644 services/reader/app/worker/tasks.py create mode 100644 services/reader/docker-compose.yml create mode 100644 services/reader/requirements.txt diff --git a/.gitignore b/.gitignore index 8367d6b..e0f1c3f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,28 @@ gateway/data/ # Test artifacts test-results/ + +# Brain storage (user data, not code) +services/brain/storage/ +services/brain/data/ + +# Reader data +services/reader/data/ + +# Screenshots (uploaded images, not code) +screenshots/*.png +screenshots/*.jpg +screenshots/*.jpeg + +# iOS build artifacts +ios/Platform/Platform.xcodeproj/xcuserdata/ +ios/Platform/Platform.xcodeproj/project.xcworkspace/xcuserdata/ +ios/Platform.bak/ + +# Node modules +**/node_modules/ + +# Temp files +*.pyc +__pycache__/ +.DS_Store diff --git a/docker-compose.yml b/docker-compose.yml index d7f8641..31214b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,8 @@ services: - IMMICH_API_KEY=${IMMICH_API_KEY} - KARAKEEP_URL=${KARAKEEP_URL:-http://192.168.1.42:3005} - KARAKEEP_API_KEY=${KARAKEEP_API_KEY} + - OPENAI_API_KEY=${OPENAI_API_KEY} + - OPENAI_MODEL=${OPENAI_MODEL:-gpt-5.2} - BODY_SIZE_LIMIT=52428800 - TZ=${TZ:-America/Chicago} networks: @@ -64,6 +66,7 @@ services: - TASKS_BACKEND_URL=http://tasks-service:8098 - TASKS_SERVICE_API_KEY=${TASKS_SERVICE_API_KEY} - BRAIN_BACKEND_URL=http://brain-api:8200 + - READER_BACKEND_URL=http://reader-api:8300 - QBITTORRENT_HOST=${QBITTORRENT_HOST:-192.168.1.42} - QBITTORRENT_PORT=${QBITTORRENT_PORT:-8080} - QBITTORRENT_USERNAME=${QBITTORRENT_USERNAME:-admin} diff --git a/extensions/brain-firefox/background.js b/extensions/brain-firefox/background.js new file mode 100644 index 0000000..6d2e60b --- /dev/null +++ b/extensions/brain-firefox/background.js @@ -0,0 +1,250 @@ +"use strict"; + +var API_BASE = "https://dash.quadjourney.com"; + +console.log("[Brain] Background script loaded"); + +// ── Auth helpers ── + +function getSession() { + return browser.storage.local.get("brainSession").then(function(data) { + return data.brainSession || null; + }); +} + +function apiRequest(path, opts) { + return getSession().then(function(session) { + if (!session) throw new Error("Not logged in"); + + opts = opts || {}; + opts.headers = opts.headers || {}; + opts.headers["Cookie"] = "platform_session=" + session; + opts.credentials = "include"; + + return fetch(API_BASE + path, opts).then(function(resp) { + if (resp.status === 401 || resp.status === 403) { + browser.storage.local.remove("brainSession"); + throw new Error("Session expired"); + } + if (!resp.ok) throw new Error("API error: " + resp.status); + return resp.json(); + }); + }); +} + +// ── Save functions ── + +function saveLink(url, title, note) { + var body = { type: "link", url: url }; + if (title) body.title = title; + if (note && note.trim()) body.raw_content = note.trim(); + return apiRequest("/api/brain/items", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); +} + +function saveNote(text) { + return apiRequest("/api/brain/items", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type: "note", raw_content: text }), + }); +} + +function saveImage(imageUrl) { + return fetch(imageUrl).then(function(resp) { + if (!resp.ok) throw new Error("Failed to download image"); + return resp.blob(); + }).then(function(blob) { + var parts = imageUrl.split("/"); + var filename = (parts[parts.length - 1] || "image.jpg").split("?")[0]; + var formData = new FormData(); + formData.append("file", blob, filename); + + return getSession().then(function(session) { + if (!session) throw new Error("Not logged in"); + return fetch(API_BASE + "/api/brain/items/upload", { + method: "POST", + headers: { "Cookie": "platform_session=" + session }, + credentials: "include", + body: formData, + }); + }); + }).then(function(resp) { + if (!resp.ok) throw new Error("Upload failed: " + resp.status); + return resp.json(); + }); +} + +// ── Badge helpers ── + +function showBadge(tabId, text, color, duration) { + browser.action.setBadgeText({ text: text, tabId: tabId }); + browser.action.setBadgeBackgroundColor({ color: color, tabId: tabId }); + setTimeout(function() { + browser.action.setBadgeText({ text: "", tabId: tabId }); + }, duration || 2000); +} + +// ── Context menus ── + +browser.runtime.onInstalled.addListener(function() { + console.log("[Brain] Creating context menus"); + browser.contextMenus.create({ + id: "save-page", + title: "Save page to Brain", + contexts: ["page"], + }); + browser.contextMenus.create({ + id: "save-link", + title: "Save link to Brain", + contexts: ["link"], + }); + browser.contextMenus.create({ + id: "save-image", + title: "Save image to Brain", + contexts: ["image"], + }); +}); + +browser.contextMenus.onClicked.addListener(function(info, tab) { + getSession().then(function(session) { + if (!session) { + browser.action.openPopup(); + return; + } + + var promise; + if (info.menuItemId === "save-page") { + promise = saveLink(tab.url, tab.title); + } else if (info.menuItemId === "save-link") { + promise = saveLink(info.linkUrl, info.linkText); + } else if (info.menuItemId === "save-image") { + promise = saveImage(info.srcUrl); + } + + if (promise) { + promise.then(function() { + showBadge(tab.id, "OK", "#059669", 2000); + }).catch(function(e) { + showBadge(tab.id, "ERR", "#DC2626", 3000); + console.error("[Brain] Save failed:", e); + }); + } + }); +}); + +// ── Keyboard shortcut ── + +browser.commands.onCommand.addListener(function(command) { + if (command === "save-page") { + browser.tabs.query({ active: true, currentWindow: true }).then(function(tabs) { + var tab = tabs[0]; + if (!tab || !tab.url) return; + + getSession().then(function(session) { + if (!session) { + browser.action.openPopup(); + return; + } + saveLink(tab.url, tab.title).then(function() { + showBadge(tab.id, "OK", "#059669", 2000); + }).catch(function(e) { + showBadge(tab.id, "ERR", "#DC2626", 3000); + }); + }); + }); + } +}); + +// ── Message handler (from popup) ── + +browser.runtime.onMessage.addListener(function(msg, sender, sendResponse) { + console.log("[Brain] Got message:", msg.action); + + if (msg.action === "login") { + fetch(API_BASE + "/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: msg.username, + password: msg.password, + }), + credentials: "include", + }).then(function(resp) { + if (!resp.ok) { + sendResponse({ success: false, error: "Invalid credentials" }); + return; + } + // Wait for cookie to be set + return new Promise(function(r) { setTimeout(r, 300); }).then(function() { + return browser.cookies.get({ + url: API_BASE, + name: "platform_session", + }); + }).then(function(cookie) { + if (cookie && cookie.value) { + return browser.storage.local.set({ brainSession: cookie.value }).then(function() { + sendResponse({ success: true }); + }); + } + sendResponse({ success: false, error: "Login OK but could not capture session" }); + }); + }).catch(function(e) { + console.error("[Brain] Login error:", e); + sendResponse({ success: false, error: e.message || "Connection failed" }); + }); + return true; + } + + if (msg.action === "logout") { + browser.storage.local.remove("brainSession").then(function() { + sendResponse({ success: true }); + }); + return true; + } + + if (msg.action === "check-auth") { + getSession().then(function(session) { + if (!session) { + sendResponse({ authenticated: false }); + return; + } + return apiRequest("/api/auth/me").then(function(data) { + sendResponse({ authenticated: true, user: data.user || data }); + }).catch(function() { + sendResponse({ authenticated: false }); + }); + }); + return true; + } + + if (msg.action === "save-link") { + saveLink(msg.url, msg.title, msg.note).then(function(result) { + sendResponse(result); + }).catch(function(e) { + sendResponse({ error: e.message }); + }); + return true; + } + + if (msg.action === "save-note") { + saveNote(msg.text).then(function(result) { + sendResponse(result); + }).catch(function(e) { + sendResponse({ error: e.message }); + }); + return true; + } + + if (msg.action === "save-image") { + saveImage(msg.url).then(function(result) { + sendResponse(result); + }).catch(function(e) { + sendResponse({ error: e.message }); + }); + return true; + } +}); diff --git a/extensions/brain-firefox/brain-firefox.xpi b/extensions/brain-firefox/brain-firefox.xpi new file mode 100644 index 0000000000000000000000000000000000000000..55c190ab4979586aa204044b315a96eb3c221446 GIT binary patch literal 8387 zcmbW6WmH_twzj)*2@b*C9U8ab5*z{v?(QDkU4lEo9U6BH1a~K-vEUXUxCHpf-bc2a zbH}~k=uvB|{;{4>t7^{m);p)N95f6z002M$1o#B%5DK;X4*~%I5DWl-0w4m|8roT! znL4|$SUKCSwJ(J<=M z80Ms{9&fFLmDq<$Qqqod(0%en!2yAcMe*m(xch3R$4B-7Lc@lr7O-llCRl7}6SzG^Cr0gmRdO zzWhd6nrjDzt;f4G;d3UBTs4iSC&q9SMg|pfafhH}M9K%FoD$~uh?|BA|Dw}XO;E+A z2*|hMR_kQ>`l)1F?nK*LgC+SB$ZJ2?vv~DI7RWMlTfH3pYdF6?s|ONlennZSZ9Df+ ztgz;Orstkft_7bgmA5PI4X}UvRjGdEaoqy84+%T>A-!?E?_^774#@6=k`i;mCEFO{ zMPAXnG*LQtXKFPaq$21g)#WuxdD*~&B(2FgCy95Uf#e1oGZ!?%$QuLo+Q;JD{CjnC z(W|LpuijgQVUws0%#jH`wpYi>Us6Xu$QMffDC@S@d6<~3CjP!zaS0D48noL9nRt|R z;Ahua*gvsHN;60l1zoOx_eV`6GclT{DJOD69_MWN|9eiV$WIVi@tnCh-NiKq^T;7*SEV2C&pi z^#oah2DmG9uU?_yiEonm)N%?P!Yfn#n8VIb6Ou1vhr`tiTrfK1n^Pk@Q0oPlp64W9Qvg+`uA7&!`|d z=%r}w+!Y?(gDu+5jw?mAHzJm?_OWs5

cVL~JBX3L8tkiucqiS(v8dk`2NWrBpg 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 + + + + + + + +

+ + + + + + + + + + diff --git a/extensions/brain-firefox/popup.js b/extensions/brain-firefox/popup.js new file mode 100644 index 0000000..caa8ccd --- /dev/null +++ b/extensions/brain-firefox/popup.js @@ -0,0 +1,202 @@ +const loginView = document.getElementById("login-view"); +const saveView = document.getElementById("save-view"); +const successView = document.getElementById("success-view"); + +const loginBtn = document.getElementById("login-btn"); +const loginUser = document.getElementById("login-user"); +const loginPass = document.getElementById("login-pass"); +const loginError = document.getElementById("login-error"); +const logoutBtn = document.getElementById("logout-btn"); +const userName = document.getElementById("user-name"); + +const tabLink = document.getElementById("tab-link"); +const tabNote = document.getElementById("tab-note"); +const linkMode = document.getElementById("link-mode"); +const noteMode = document.getElementById("note-mode"); + +const pageTitle = document.getElementById("page-title"); +const pageUrl = document.getElementById("page-url"); +const noteToggleBtn = document.getElementById("note-toggle-btn"); +const linkNote = document.getElementById("link-note"); +const saveLinkBtn = document.getElementById("save-link-btn"); + +const noteText = document.getElementById("note-text"); +const saveNoteBtn = document.getElementById("save-note-btn"); +const saveError = document.getElementById("save-error"); + +let currentTab = null; + +// ── View management ── + +function showView(view) { + loginView.classList.add("hidden"); + saveView.classList.add("hidden"); + successView.classList.add("hidden"); + view.classList.remove("hidden"); +} + +function showError(el, msg) { + el.textContent = msg; + el.classList.remove("hidden"); +} +function hideError(el) { + el.classList.add("hidden"); +} + +// ── Init ── + +async function init() { + const resp = await browser.runtime.sendMessage({ action: "check-auth" }); + if (resp.authenticated) { + userName.textContent = resp.user?.display_name || resp.user?.username || ""; + showView(saveView); + await loadCurrentTab(); + } else { + showView(loginView); + loginUser.focus(); + } +} + +async function loadCurrentTab() { + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + if (tab) { + currentTab = tab; + pageTitle.textContent = tab.title || "Untitled"; + try { + pageUrl.textContent = new URL(tab.url).hostname; + } catch { + pageUrl.textContent = tab.url; + } + } +} + +// ── Login ── + +loginBtn.addEventListener("click", async () => { + const user = loginUser.value.trim(); + const pass = loginPass.value; + if (!user || !pass) return; + + loginBtn.disabled = true; + loginBtn.textContent = "Signing in..."; + hideError(loginError); + + try { + const resp = await browser.runtime.sendMessage({ + action: "login", + username: user, + password: pass, + }); + + if (resp && resp.success) { + const auth = await browser.runtime.sendMessage({ action: "check-auth" }); + userName.textContent = auth.user?.display_name || auth.user?.username || ""; + showView(saveView); + await loadCurrentTab(); + } else { + showError(loginError, (resp && resp.error) || "Login failed"); + } + } catch (e) { + showError(loginError, "Error: " + (e.message || e)); + } + + loginBtn.disabled = false; + loginBtn.textContent = "Sign in"; +}); + +loginPass.addEventListener("keydown", (e) => { + if (e.key === "Enter") loginBtn.click(); +}); + +// ── Logout ── + +logoutBtn.addEventListener("click", async () => { + await browser.runtime.sendMessage({ action: "logout" }); + showView(loginView); + loginUser.value = ""; + loginPass.value = ""; +}); + +// ── Mode tabs ── + +tabLink.addEventListener("click", () => { + tabLink.classList.add("active"); + tabNote.classList.remove("active"); + linkMode.classList.remove("hidden"); + noteMode.classList.add("hidden"); + hideError(saveError); +}); + +tabNote.addEventListener("click", () => { + tabNote.classList.add("active"); + tabLink.classList.remove("active"); + noteMode.classList.remove("hidden"); + linkMode.classList.add("hidden"); + hideError(saveError); + noteText.focus(); +}); + +// ── Note toggle on link mode ── + +noteToggleBtn.addEventListener("click", () => { + linkNote.classList.toggle("hidden"); + if (!linkNote.classList.contains("hidden")) { + noteToggleBtn.textContent = "- Hide note"; + linkNote.focus(); + } else { + noteToggleBtn.textContent = "+ Add a note"; + } +}); + +// ── Save link ── + +saveLinkBtn.addEventListener("click", async () => { + if (!currentTab?.url) return; + saveLinkBtn.disabled = true; + saveLinkBtn.textContent = "Saving..."; + hideError(saveError); + + try { + await browser.runtime.sendMessage({ + action: "save-link", + url: currentTab.url, + title: currentTab.title, + note: linkNote.value.trim() || undefined, + }); + + showView(successView); + setTimeout(() => window.close(), 1500); + } catch (e) { + showError(saveError, e.message || "Save failed"); + saveLinkBtn.disabled = false; + saveLinkBtn.textContent = "Save page"; + } +}); + +// ── Save note ── + +saveNoteBtn.addEventListener("click", async () => { + const text = noteText.value.trim(); + if (!text) return; + saveNoteBtn.disabled = true; + saveNoteBtn.textContent = "Saving..."; + hideError(saveError); + + try { + await browser.runtime.sendMessage({ action: "save-note", text }); + showView(successView); + setTimeout(() => window.close(), 1500); + } catch (e) { + showError(saveError, e.message || "Save failed"); + saveNoteBtn.disabled = false; + saveNoteBtn.textContent = "Save note"; + } +}); + +// ── Start ── +init().catch((e) => { + console.error("[Brain popup] Init failed:", e); + // Show login view as fallback + showView(loginView); + loginUser.focus(); +}); diff --git a/extensions/brain-firefox/test-popup.html b/extensions/brain-firefox/test-popup.html new file mode 100644 index 0000000..7adb8ed --- /dev/null +++ b/extensions/brain-firefox/test-popup.html @@ -0,0 +1,8 @@ + + + + +

Brain Extension

+

If you see this, the popup works.

+ + diff --git a/frontend-v2/src/app.css b/frontend-v2/src/app.css index da5f984..ad33435 100644 --- a/frontend-v2/src/app.css +++ b/frontend-v2/src/app.css @@ -12,8 +12,9 @@ ═══════════════════════════════════════════════ */ @layer base { - html, body { overflow-x: hidden; } - body { padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); } + html { background-color: #f5efe6; } + body { overflow-x: clip; } + body { padding-bottom: env(safe-area-inset-bottom); } :root { /* ── Fonts ── */ --font: 'Outfit', -apple-system, system-ui, sans-serif; @@ -106,7 +107,7 @@ /* ── LIGHT MODE — Zinc + Emerald ── */ :root { - --canvas: #FAFAFA; + --canvas: #f5efe6; --surface: #FFFFFF; --surface-secondary: #F4F4F5; --card: #FFFFFF; diff --git a/frontend-v2/src/app.html b/frontend-v2/src/app.html index bd524fe..2ee60cb 100644 --- a/frontend-v2/src/app.html +++ b/frontend-v2/src/app.html @@ -3,6 +3,9 @@ + + + diff --git a/frontend-v2/src/lib/components/assistant/BrainAssistantDrawer.svelte b/frontend-v2/src/lib/components/assistant/BrainAssistantDrawer.svelte new file mode 100644 index 0000000..779df73 --- /dev/null +++ b/frontend-v2/src/lib/components/assistant/BrainAssistantDrawer.svelte @@ -0,0 +1,480 @@ + + +{#if open} + + + +{/if} + + diff --git a/frontend-v2/src/lib/components/assistant/FitnessAssistantDrawer.svelte b/frontend-v2/src/lib/components/assistant/FitnessAssistantDrawer.svelte index a6a117b..c46a281 100644 --- a/frontend-v2/src/lib/components/assistant/FitnessAssistantDrawer.svelte +++ b/frontend-v2/src/lib/components/assistant/FitnessAssistantDrawer.svelte @@ -1,16 +1,26 @@ {#if open} - {/each} - {#if drafts.length > 1} + {#if activeDrafts().length > 1}
@@ -298,12 +341,12 @@
Ready to add
-
{drafts.length} items
+
{activeDrafts().length} items
-
{drafts[0]?.meal_type || 'meal'}
+
{activeDrafts()[0]?.meal_type || 'meal'}
- {#each drafts as item} + {#each activeDrafts() as item}
{item.food_name}
{Math.round(item.calories || 0)} cal
@@ -311,7 +354,7 @@ {/each}
- {drafts.reduce((sum, item) => sum + Math.round(item.calories || 0), 0)} cal total + {activeDrafts().reduce((sum, item) => sum + Math.round(item.calories || 0), 0)} cal total
- {:else if draft?.food_name} + {:else if activeDraft()?.food_name}
@@ -332,19 +375,19 @@
Ready to add
-
{draft.food_name}
+
{activeDraft()?.food_name}
- {#if draft.meal_type} -
{draft.meal_type}
+ {#if activeDraft()?.meal_type} +
{activeDraft()?.meal_type}
{/if}
- {Math.round(draft.calories || 0)} cal - {Math.round(draft.protein || 0)}p - {Math.round(draft.carbs || 0)}c - {Math.round(draft.fat || 0)}f - {Math.round(draft.sugar || 0)} sugar - {Math.round(draft.fiber || 0)} fiber + {Math.round(activeDraft()?.calories || 0)} cal + {Math.round(activeDraft()?.protein || 0)}p + {Math.round(activeDraft()?.carbs || 0)}c + {Math.round(activeDraft()?.fat || 0)}f + {Math.round(activeDraft()?.sugar || 0)} sugar + {Math.round(activeDraft()?.fiber || 0)} fiber
@@ -410,7 +453,7 @@ class="compose-input" bind:value={input} bind:this={composerEl} - placeholder="Add 2 boiled eggs for breakfast..." + placeholder={allowBrain ? 'Log food, save a note, ask Brain...' : 'Log food or ask about calories...'} onkeydown={handleKeydown} type="text" /> @@ -537,9 +580,12 @@ .assistant-body { min-height: 0; overflow: hidden; + display: flex; + flex-direction: column; } .thread { + flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain; @@ -549,6 +595,7 @@ display: flex; flex-direction: column; gap: 10px; + scrollbar-gutter: stable; } .bundle-card { @@ -642,6 +689,30 @@ color: #f8f4ee; } + .source-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + .source-chip { + display: grid; + gap: 2px; + padding: 8px 10px; + border-radius: 14px; + text-decoration: none; + background: rgba(255, 255, 255, 0.76); + border: 1px solid rgba(36, 26, 18, 0.08); + color: #241c14; + } + + .source-chip small { + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #7a6a5b; + } + .image-bubble { width: min(240px, 100%); padding: 8px; diff --git a/frontend-v2/src/lib/components/layout/AppShell.svelte b/frontend-v2/src/lib/components/layout/AppShell.svelte index 704256d..a0bacca 100644 --- a/frontend-v2/src/lib/components/layout/AppShell.svelte +++ b/frontend-v2/src/lib/components/layout/AppShell.svelte @@ -12,6 +12,7 @@ LibraryBig, Menu, Package2, + MessageSquare, Search, Settings2, SquareCheckBig, @@ -193,8 +194,9 @@
ops workspace
- @@ -220,7 +222,8 @@ {/each} -
+ +
e.preventDefault()} ondrop={onRailDrop}>
@@ -230,10 +233,6 @@
-
- - {new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(new Date())} -
{/if} @@ -250,13 +249,14 @@ radial-gradient(circle at top left, rgba(214, 120, 58, 0.08), transparent 22%), radial-gradient(circle at 85% 10%, rgba(65, 91, 82, 0.12), transparent 20%), linear-gradient(180deg, #f5efe6 0%, #efe6da 48%, #ede7de 100%); + background-color: #f5efe6; } .shell { --shell-ink: #1e1812; --shell-muted: #6b6256; --shell-line: rgba(35, 26, 17, 0.11); - min-height: 100vh; + min-height: 100dvh; display: grid; grid-template-columns: 270px minmax(0, 1fr); position: relative; @@ -525,10 +525,17 @@ } .command-trigger.mobile { - width: 40px; height: 40px; justify-content: center; - padding: 0; + padding: 0 12px; + gap: 6px; + font-weight: 700; + color: var(--shell-ink); + } + + .command-trigger.mobile span { + font-size: 0.78rem; + letter-spacing: 0.02em; } .mobile-menu-btn { @@ -557,33 +564,15 @@ top: 0; left: 0; bottom: 0; - width: min(300px, 82vw); - padding: 16px 14px; - padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); + width: min(320px, 86vw); + padding: 18px 16px; background: linear-gradient(180deg, rgba(250, 246, 239, 0.96), rgba(244, 237, 228, 0.94)); border-right: 1px solid var(--shell-line); backdrop-filter: blur(20px); z-index: 40; - display: flex; - flex-direction: column; - gap: 12px; - overflow-y: auto; - } - .mobile-nav-list { - flex: 1; - display: flex; - flex-direction: column; - gap: 2px; - } - .mobile-nav-list a { - padding: 10px 12px; - } - .mobile-nav-bottom { display: grid; - gap: 8px; - padding-top: 12px; - border-top: 1px solid var(--shell-line); - flex-shrink: 0; + align-content: start; + gap: 18px; } .mobile-nav-head { @@ -634,5 +623,11 @@ .mobile-menu-btn { display: inline-flex; } + + .mobile-nav-upload { + margin-top: auto; + padding-top: 16px; + border-top: 1px solid var(--shell-line); + } } diff --git a/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte b/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte index 35756a8..1acc50d 100644 --- a/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte +++ b/frontend-v2/src/lib/pages/brain/AtelierBrainPage.svelte @@ -1,8 +1,23 @@ @@ -566,7 +589,32 @@
-
Feeds
+
+ Feeds +
+ + +
+
+ + {#if showFeedManager} +
+ { if (e.key === 'Enter') addFeed(); }} /> + +
+ {/if} + {#each feedCategories as cat, i}
+ {#if showFeedManager} + + {/if} - +
{/each}
{/if} @@ -598,7 +656,7 @@ {/if} -
+
{article.timeAgo} @@ -713,6 +771,11 @@ {#if filteredArticles.length === 0}
No articles to show
{/if} + {#if hasMore && !loading} + + {/if}
@@ -737,11 +800,11 @@
-