diff --git a/app/public/lottie/brain-collecting.json b/app/public/lottie/brain-collecting.json new file mode 100644 index 000000000..16c881947 --- /dev/null +++ b/app/public/lottie/brain-collecting.json @@ -0,0 +1,202 @@ +{ + "v": "5.7.4", + "fr": 30, + "ip": 0, + "op": 90, + "w": 220, + "h": 220, + "nm": "brain-collecting", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "ping", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { "t": 0, "s": [55], "i": { "x": [0.4], "y": [1] }, "o": { "x": [0.6], "y": [0] } }, + { "t": 90, "s": [0] } + ] + }, + "r": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [110, 110, 0] }, + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { + "a": 1, + "k": [ + { + "t": 0, + "s": [40, 40], + "i": { "x": [0.4, 0.4], "y": [1, 1] }, + "o": { "x": [0.6, 0.6], "y": [0, 0] } + }, + { "t": 90, "s": [150, 150] } + ] + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { "ty": "el", "p": { "a": 0, "k": [0, 0] }, "s": { "a": 0, "k": [120, 120] }, "d": 1 }, + { + "ty": "st", + "c": { "a": 0, "k": [0.29, 0.514, 0.867, 1] }, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 3 }, + "lc": 2, + "lj": 1 + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0] }, + "a": { "a": 0, "k": [0, 0] }, + "s": { "a": 0, "k": [100, 100] }, + "r": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + } + ] + } + ], + "ip": 0, + "op": 90, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "nodes-ring", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { + "a": 1, + "k": [ + { "t": 0, "s": [0], "i": { "x": [0.5], "y": [0.5] }, "o": { "x": [0.5], "y": [0.5] } }, + { "t": 90, "s": [360] } + ] + }, + "p": { "a": 0, "k": [110, 110, 0] }, + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100] } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { "ty": "el", "p": { "a": 0, "k": [68, 0] }, "s": { "a": 0, "k": [16, 16] }, "d": 1 }, + { + "ty": "el", + "p": { "a": 0, "k": [34, 58.89] }, + "s": { "a": 0, "k": [12, 12] }, + "d": 1 + }, + { + "ty": "el", + "p": { "a": 0, "k": [-34, 58.89] }, + "s": { "a": 0, "k": [16, 16] }, + "d": 1 + }, + { "ty": "el", "p": { "a": 0, "k": [-68, 0] }, "s": { "a": 0, "k": [12, 12] }, "d": 1 }, + { + "ty": "el", + "p": { "a": 0, "k": [-34, -58.89] }, + "s": { "a": 0, "k": [16, 16] }, + "d": 1 + }, + { + "ty": "el", + "p": { "a": 0, "k": [34, -58.89] }, + "s": { "a": 0, "k": [12, 12] }, + "d": 1 + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.435, 0.627, 0.902, 1] }, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0] }, + "a": { "a": 0, "k": [0, 0] }, + "s": { "a": 0, "k": [100, 100] }, + "r": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + } + ] + } + ], + "ip": 0, + "op": 90, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "core", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100 }, + "r": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [110, 110, 0] }, + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { + "a": 1, + "k": [ + { + "t": 0, + "s": [78, 78], + "i": { "x": [0.4, 0.4], "y": [1, 1] }, + "o": { "x": [0.6, 0.6], "y": [0, 0] } + }, + { + "t": 45, + "s": [112, 112], + "i": { "x": [0.4, 0.4], "y": [1, 1] }, + "o": { "x": [0.6, 0.6], "y": [0, 0] } + }, + { "t": 90, "s": [78, 78] } + ] + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { "ty": "el", "p": { "a": 0, "k": [0, 0] }, "s": { "a": 0, "k": [56, 56] }, "d": 1 }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.29, 0.514, 0.867, 1] }, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0] }, + "a": { "a": 0, "k": [0, 0] }, + "s": { "a": 0, "k": [100, 100] }, + "r": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + } + ] + } + ], + "ip": 0, + "op": 90, + "st": 0, + "bm": 0 + } + ], + "markers": [] +} diff --git a/app/src/AppRoutes.tsx b/app/src/AppRoutes.tsx index fd8c33444..90eb96db1 100644 --- a/app/src/AppRoutes.tsx +++ b/app/src/AppRoutes.tsx @@ -7,6 +7,7 @@ import PublicRoute from './components/PublicRoute'; import { getIsMobile } from './lib/platform'; import Accounts from './pages/Accounts'; import Activity from './pages/Activity'; +import Brain from './pages/Brain'; import Home from './pages/Home'; import Invites from './pages/Invites'; import Notifications from './pages/Notifications'; @@ -67,6 +68,17 @@ const AppRoutes = () => { iOS AppRoutesIOS still serves /human natively — only desktop redirects. */} } /> + {/* Brain — the centerpiece memory knowledge-graph surface, reached from + the raised center button in the bottom bar. Full-page, graph-only. */} + + + + } + /> + {/* Primary Activity surface — replaces /intelligence (Phase 3). */} ); + case 'brain': + // Two symmetric lobes — reads clearly as a brain. Rendered larger and + // white inside the raised center circle. + return ( + + + + + ); default: return null; } @@ -209,6 +228,96 @@ const BottomTabBar = () => { trackEvent('avatar_menu_item_click', { item_id: itemId }); }; + // One regular pill tab. `iconOnly` renders just the glyph (no label) — used + // for the pinned Home button so it reads as a fixed icon, like the avatar. + // + // When labels are always visible (theme setting), every labelled tab is given + // the SAME fixed width so the row stays symmetric. In the default hover mode + // the label still expands on hover (no fixed width) — unchanged behaviour. + const renderTab = (tab: (typeof tabs)[number], iconOnly = false) => { + const active = isActive(tab.path); + const showBadge = tab.id === 'notifications' && unreadCount > 0; + const showCompanionDot = tab.id === 'settings' && companionActive; + const fixedWidth = !iconOnly && labelsAlwaysVisible; + return ( + + ); + }; + + // The Brain — a raised circular button rising out of the center of the bar. + // The bg-colored ring fakes a notch cut into the pill's top edge. `brain-fab` + // is targeted by the reduced-motion gate in index.css to silence the glow. + const renderBrainButton = () => { + const active = isActive(BRAIN_TAB.path); + const brainTab = { ...BRAIN_TAB, label: t(BRAIN_TAB.labelKey) }; + return ( + + ); + }; + + // Home is pinned to the far-left of the pill behind a divider — mirroring the + // avatar pinned to the far-right behind its own divider. The rest of the row + // splits evenly around the centered Brain button: + // [ home ] | assistant · connections ( 🧠 ) activity · settings | [ avatar ] + const homeTab = tabs[0]; + const leftTabs = tabs.slice(1, 3); + const rightTabs = tabs.slice(3); + return ( // pointer-events-none on the full-width shell so transparent areas (e.g. // beside the centered nav pill) do not steal clicks from sticky footers @@ -233,47 +342,12 @@ const BottomTabBar = () => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setRevealed(false); }}>