From 0343ec9b2816c664ab17e2642ffecbcf34372ccd Mon Sep 17 00:00:00 2001 From: Marcelo Ceccon Date: Sat, 25 Apr 2026 20:35:48 +0000 Subject: [PATCH 1/2] feat(ui): glassmorphic enterprise redesign with cosmic dark theme Premium dark-mode interface using sophisticated glassmorphism over a deep-navy cosmic background with vibrant orange (#FF6200) accents. Design - New cosmic shell: layered radial gradients, single GPU-composited ray sweep, twinkling starfield, two slow drifting orbs over a fixed background image. - Glass utility scale (.glass / .glass-soft / .glass-strong / .glass-input / .glass-pill / .glass-fixed) with luminous blue borders, inner highlight, and layered drop shadows. - Custom premium SVG hero illustrations per major panel (consensus nodes in glass cylinders, concentric rings, trajectory glow, coin stack, opposing nodes, orbiting roundtable, circuit config) with subtle animated highlights. - Orange CTA system (.btn-orange) with hover lift + glow, ghost buttons, refined glass toggles, shimmering progress bar, animated shine sweep on primary actions. - New header with luminous logo mark, live participants pill, and responsive subtitle. Layout & responsiveness - Header height unified via --rt-header-h CSS variable (65px / 73px responsive) so sidebar sticky top and right-rail offset always match. - Below lg the sidebar collapses behind a hamburger and slides in as a dialog drawer (backdrop, ESC close, body-scroll lock). - Hero card, prompt textarea, response cards, judge card, and progress bar scale paddings/font sizes from sm: upward. - Floating right rail stays xl-only; its panels reappear inline in the sidebar at smaller breakpoints. Performance - Removed stacked backdrop-filter from inline cards (the dominant paint cost when scrolling); kept it only on transient/fixed surfaces (header, sticky progress bar, modal, drawer, toast, back-to-top). Card alpha bumped to compensate visually. - Coalesced SSE token events through a per-participant rAF buffer in processEvent so React/paint work is capped at ~60Hz instead of running on every chunk; explicit flushes on round/participant boundaries prevent dropped trailing characters. - Memoized StreamingCard + Header, stabilized inline boxShadow via useMemo keyed on persona color to avoid object churn. - Result cards opt into content-visibility: auto and contain so offscreen completed cards skip layout/paint entirely. - Cosmic background animations slowed and consolidated (one ray sweep, two orbs at 56px blur dropping to 40px on phones), all GPU-composited (translate3d / will-change / contain). Honors prefers-reduced-motion. - body switched from overflow-x: hidden to overflow-x: clip so it no longer establishes a scroll container that derails sticky. - Aside given self-start and CSS-variable-based top/max-height for reliable sticky behavior inside the flex parent. Accessibility - Onboarding modal and mobile drawer marked as role="dialog" with aria-modal and aria-labelledby/aria-label. - Copy buttons, hamburger, drawer close, and chart all carry aria-label; decorative streaming cursor is aria-hidden. - Reduced-motion media query disables ambient animations. Tests - Updated 22 assertions to match redesigned copy (model/persona selectors, "Seat at the Table", "Convene the RoundTable", "Reset session", "Final Consensus" + score split, "tok" short label). - Confidence trajectory test now scopes path queries to the chart SVG so it ignores the new hero illustration above it. - All 207 tests pass; lint clean; production build clean. --- README.md | 8 +- app/globals.css | 477 ++++++++++++++++++-- app/layout.tsx | 50 ++- app/page.tsx | 550 ++++++++++++++++------- components/AISelector.tsx | 184 +++++--- components/BackToTop.tsx | 39 +- components/ConfidenceTrajectory.tsx | 217 ++++----- components/ConfigPanel.tsx | 87 ++-- components/CostMeter.tsx | 64 ++- components/DisagreementPanel.tsx | 119 ++--- components/HeroArt.tsx | 654 ++++++++++++++++++++++++++++ components/JudgeCard.tsx | 23 +- components/MessageFlowDiagram.tsx | 97 +++-- components/PromptLibrary.tsx | 13 +- components/ResultPanel.tsx | 185 +++++--- components/SessionMenu.tsx | 17 +- public/background.jpg | Bin 0 -> 125671 bytes screenshots/newscreenshot.png | Bin 2049671 -> 0 bytes screenshots/screenshot.jpeg | Bin 0 -> 1155977 bytes tailwind.config.ts | 81 +++- tests/ai-selector-extended.test.tsx | 15 +- tests/components-extended.test.tsx | 10 +- tests/components.test.tsx | 11 +- tests/new-components.test.tsx | 13 +- tests/page.test.tsx | 13 +- 25 files changed, 2244 insertions(+), 683 deletions(-) create mode 100644 components/HeroArt.tsx create mode 100644 public/background.jpg delete mode 100644 screenshots/newscreenshot.png create mode 100644 screenshots/screenshot.jpeg diff --git a/README.md b/README.md index 26e721b..b4bbd59 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,10 @@ No database. No auth. No external services. Just add your API keys and go. --- +## Screenshot + +![Screenshot of Web Interface](screenshots/screenshot.jpg) + ## Consensus Validation Protocol ### Purpose @@ -225,10 +229,6 @@ This is experimental, it has no authentication protection, if you publish this w --- -## Screenshot - -![Screenshot of Web Interface](screenshots/newscreenshot.png) - ## Features | Feature | Description | diff --git a/app/globals.css b/app/globals.css index 784663f..ab0a0ad 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,82 +2,505 @@ @tailwind components; @tailwind utilities; +/* ── Root color tokens ──────────────────────────────────── */ +:root { + --rt-bg: #02070f; + --rt-bg-2: #050d22; + --rt-bg-3: #081634; + --rt-blue: #003087; + --rt-blue-soft: #4d7ac7; + --rt-orange: #ff6200; + --rt-orange-soft: #ff9a4d; + --rt-text: #f1f5ff; + --rt-muted: #8b9cb8; + --rt-header-h: 65px; +} + +@media (min-width: 640px) { + :root { + --rt-header-h: 73px; + } +} + +html, +body { + background: var(--rt-bg); + color: var(--rt-text); + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + font-feature-settings: "ss01", "cv11", "cv02"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.005em; +} + +/* ── Cosmic background scaffold ─────────────────────────── */ +.cosmic-shell { + position: relative; + min-height: 100vh; + isolation: isolate; + background: + radial-gradient(ellipse 90% 60% at 18% 4%, rgba(0, 48, 135, 0.38), transparent 65%), + radial-gradient(ellipse 70% 50% at 88% 12%, rgba(255, 98, 0, 0.10), transparent 60%), + radial-gradient(ellipse 100% 70% at 50% 110%, rgba(0, 48, 135, 0.30), transparent 70%), + linear-gradient(180deg, #02070f 0%, #030a1c 35%, #050e26 70%, #02070f 100%); +} + +/* Static orange top-glow — paint-once. */ +.cosmic-shell::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 55vh; + pointer-events: none; + z-index: 0; + background: + radial-gradient(ellipse 60% 22% at 30% 8%, rgba(255, 98, 0, 0.22), transparent 72%), + radial-gradient(ellipse 55% 18% at 65% 14%, rgba(255, 154, 77, 0.18), transparent 75%), + radial-gradient(ellipse 50% 14% at 50% 22%, rgba(255, 98, 0, 0.10), transparent 80%); + filter: blur(2px); + opacity: 0.95; + contain: paint; +} + +/* Single, slow, GPU-composited ray sweep — animates only `transform`. */ +.cosmic-shell::after { + content: ""; + position: absolute; + inset: -10% -20%; + pointer-events: none; + z-index: 0; + background-image: + linear-gradient( + 105deg, + transparent 38%, + rgba(255, 138, 51, 0.10) 48%, + rgba(255, 98, 0, 0.16) 50%, + rgba(255, 138, 51, 0.10) 52%, + transparent 62% + ), + linear-gradient( + 98deg, + transparent 32%, + rgba(77, 122, 199, 0.08) 49%, + rgba(126, 165, 230, 0.14) 50%, + rgba(77, 122, 199, 0.08) 51%, + transparent 68% + ); + opacity: 0.6; + will-change: transform; + transform: translate3d(0, 0, 0); + animation: raySlide 36s linear infinite; + contain: paint; +} + +@keyframes raySlide { + 0% { + transform: translate3d(-3%, 0, 0); + } + 100% { + transform: translate3d(3%, 0, 0); + } +} + +/* Subtle starfield — opacity-only twinkle, very cheap. */ +.cosmic-stars { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 0; + background-image: + radial-gradient(1.5px 1.5px at 12% 18%, rgba(241, 245, 255, 0.55), transparent 60%), + radial-gradient(1px 1px at 22% 64%, rgba(241, 245, 255, 0.4), transparent 60%), + radial-gradient(1.5px 1.5px at 38% 28%, rgba(255, 154, 77, 0.45), transparent 60%), + radial-gradient(1px 1px at 48% 82%, rgba(241, 245, 255, 0.35), transparent 60%), + radial-gradient(2px 2px at 62% 12%, rgba(241, 245, 255, 0.5), transparent 60%), + radial-gradient(1px 1px at 72% 56%, rgba(126, 165, 230, 0.45), transparent 60%), + radial-gradient(1.5px 1.5px at 84% 34%, rgba(241, 245, 255, 0.4), transparent 60%), + radial-gradient(1px 1px at 92% 78%, rgba(255, 154, 77, 0.5), transparent 60%), + radial-gradient(1px 1px at 8% 88%, rgba(241, 245, 255, 0.4), transparent 60%), + radial-gradient(1.5px 1.5px at 56% 46%, rgba(241, 245, 255, 0.35), transparent 60%); + animation: twinkle 12s ease-in-out infinite alternate; + will-change: opacity; +} + +@keyframes twinkle { + 0% { + opacity: 0.55; + } + 100% { + opacity: 0.9; + } +} + +/* Floating orbs — translate-only, GPU layer. */ +.cosmic-orbs { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 0; + overflow: hidden; + contain: layout paint; +} +.cosmic-orbs::before, +.cosmic-orbs::after { + content: ""; + position: absolute; + border-radius: 50%; + filter: blur(56px); + opacity: 0.45; + will-change: transform; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; +} +.cosmic-orbs::before { + width: 460px; + height: 460px; + top: -160px; + left: -120px; + background: radial-gradient(circle, rgba(0, 48, 135, 0.6), transparent 70%); + animation: orbFloat1 40s ease-in-out infinite; +} +.cosmic-orbs::after { + width: 400px; + height: 400px; + bottom: -160px; + right: -100px; + background: radial-gradient(circle, rgba(255, 98, 0, 0.28), transparent 70%); + animation: orbFloat2 48s ease-in-out infinite; +} + +@keyframes orbFloat1 { + 0%, 100% { transform: translate3d(0, 0, 0); } + 50% { transform: translate3d(40px, 30px, 0); } +} +@keyframes orbFloat2 { + 0%, 100% { transform: translate3d(0, 0, 0); } + 50% { transform: translate3d(-36px, -22px, 0); } +} + +/* ── Reduced-motion override ───────────────────────────── */ +@media (prefers-reduced-motion: reduce) { + .cosmic-shell::after, + .cosmic-stars, + .cosmic-orbs::before, + .cosmic-orbs::after, + .progress-orange, + .animate-in, + .shine::after { + animation: none !important; + transition: none !important; + } + .cosmic-stars { opacity: 0.7; } +} + +/* Phones: lower the cost ceiling further. */ +@media (hover: none) and (max-width: 640px) { + .cosmic-shell::after { animation-duration: 60s; opacity: 0.45; } + .cosmic-orbs::before { filter: blur(40px); } + .cosmic-orbs::after { filter: blur(40px); } +} + +/* ── Glass utilities ───────────────────────────────────── + IMPORTANT: backdrop-filter is the most expensive CSS effect + when stacked across many elements that paint together with + scrolling/animated content behind them. We restrict it to + transient or single-instance fixed layers (header, modal, + drawer, toaster, sticky progress bar) and use opaque-ish + gradients with a luminous border for the rest of the cards. + Visually it reads as glass against our largely-static cosmic + background while keeping compositing cheap. */ + +.glass { + background: + linear-gradient(135deg, rgba(8, 22, 52, 0.85) 0%, rgba(4, 12, 30, 0.92) 100%); + border: 1px solid rgba(77, 122, 199, 0.22); + border-radius: 22px; + box-shadow: + 0 12px 32px -10px rgba(0, 0, 0, 0.6), + inset 0 1px 0 0 rgba(255, 255, 255, 0.05); +} + +.glass-soft { + background: + linear-gradient(135deg, rgba(8, 22, 52, 0.78) 0%, rgba(4, 12, 30, 0.85) 100%); + border: 1px solid rgba(77, 122, 199, 0.16); + border-radius: 18px; + box-shadow: + 0 6px 20px -8px rgba(0, 0, 0, 0.5), + inset 0 1px 0 0 rgba(255, 255, 255, 0.04); +} + +.glass-strong { + background: + linear-gradient(135deg, rgba(10, 26, 60, 0.92) 0%, rgba(5, 14, 36, 0.96) 100%); + border: 1px solid rgba(77, 122, 199, 0.32); + border-radius: 24px; + box-shadow: + 0 20px 48px -16px rgba(0, 0, 0, 0.7), + inset 0 1px 0 0 rgba(255, 255, 255, 0.06); +} + +.glass-input { + background: linear-gradient(135deg, rgba(2, 8, 22, 0.85), rgba(4, 12, 30, 0.78)); + border: 1px solid rgba(77, 122, 199, 0.20); + border-radius: 14px; + transition: border-color 200ms ease, box-shadow 200ms ease; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); +} +.glass-input:hover { + border-color: rgba(77, 122, 199, 0.4); +} +.glass-input:focus, +.glass-input:focus-within { + border-color: rgba(255, 98, 0, 0.6); + box-shadow: + 0 0 0 3px rgba(255, 98, 0, 0.08), + 0 0 18px -6px rgba(255, 98, 0, 0.35), + inset 0 1px 0 rgba(255, 255, 255, 0.05); + outline: none; +} + +/* Small inline pills are infrequent + tiny — cheap to blur. */ +.glass-pill { + background: linear-gradient(135deg, rgba(8, 22, 52, 0.55), rgba(4, 12, 30, 0.55)); + border: 1px solid rgba(77, 122, 199, 0.22); + border-radius: 999px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +/* For genuinely fixed/sticky surfaces that benefit from a real + blur of whatever scrolls behind. Use sparingly. */ +.glass-fixed { + background: linear-gradient(180deg, rgba(2, 7, 15, 0.7), rgba(2, 7, 15, 0.55)); + border: 1px solid rgba(77, 122, 199, 0.18); + backdrop-filter: blur(20px) saturate(140%); + -webkit-backdrop-filter: blur(20px) saturate(140%); +} + +.glass-divider { + height: 1px; + background: linear-gradient(90deg, transparent, rgba(77, 122, 199, 0.4), transparent); +} + +.btn-orange { + background: linear-gradient(180deg, #ff8a3a 0%, #ff6200 60%, #e25400 100%); + color: #fff; + border-radius: 14px; + border: 1px solid rgba(255, 154, 77, 0.55); + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.18) inset, + 0 -1px 0 rgba(0, 0, 0, 0.25) inset, + 0 8px 22px -6px rgba(255, 98, 0, 0.55); + transition: transform 180ms ease, box-shadow 180ms ease; + font-weight: 600; + letter-spacing: 0.005em; +} +.btn-orange:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: + 0 1px 0 rgba(255, 255, 255, 0.22) inset, + 0 -1px 0 rgba(0, 0, 0, 0.25) inset, + 0 12px 28px -6px rgba(255, 98, 0, 0.7); +} +.btn-orange:active:not(:disabled) { + transform: translateY(0); +} +.btn-orange:disabled { + opacity: 0.35; + cursor: not-allowed; + filter: saturate(0.6); +} + +.btn-ghost { + background: linear-gradient(135deg, rgba(8, 22, 52, 0.7), rgba(4, 12, 30, 0.7)); + border: 1px solid rgba(77, 122, 199, 0.25); + border-radius: 12px; + color: var(--rt-text); + transition: border-color 180ms ease, color 180ms ease, box-shadow 180ms ease; +} +.btn-ghost:hover:not(:disabled) { + border-color: rgba(255, 98, 0, 0.45); + box-shadow: 0 0 14px -4px rgba(255, 98, 0, 0.35); + color: var(--rt-orange-soft); +} + +/* Toggle */ +.glass-toggle { + width: 32px; + height: 18px; + border-radius: 999px; + background: linear-gradient(135deg, rgba(8, 22, 52, 0.65), rgba(4, 12, 30, 0.7)); + border: 1px solid rgba(77, 122, 199, 0.3); + position: relative; + transition: background 200ms ease, border-color 200ms ease, box-shadow 200ms ease; + flex-shrink: 0; +} +.glass-toggle.on { + background: linear-gradient(135deg, #ff8a3a, #ff6200); + border-color: rgba(255, 154, 77, 0.7); + box-shadow: 0 0 12px -2px rgba(255, 98, 0, 0.6); +} +.glass-toggle .knob { + position: absolute; + top: 50%; + left: 2px; + transform: translateY(-50%); + width: 12px; + height: 12px; + border-radius: 50%; + background: #f1f5ff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); + transition: left 200ms cubic-bezier(0.16, 1, 0.3, 1); +} +.glass-toggle.on .knob { + left: 16px; + background: #ffffff; +} + +/* Section header */ +.section-label { + font-size: 9.5px; + font-weight: 600; + color: rgba(139, 156, 184, 0.85); + text-transform: uppercase; + letter-spacing: 0.18em; + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +/* Animated orange progress bar — single GPU-composited + background-position shift. Cheap. */ +.progress-orange { + background: linear-gradient(90deg, #ff6200 0%, #ff9a4d 50%, #ff6200 100%); + background-size: 200% 100%; + animation: shimmer 2.4s linear infinite; + box-shadow: 0 0 12px rgba(255, 98, 0, 0.45); + will-change: background-position; +} + +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + /* ── Scrollbar ──────────────────────────────────────── */ ::-webkit-scrollbar { - width: 5px; + width: 6px; + height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { - background: #47556860; + background: rgba(77, 122, 199, 0.3); border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { - background: #47556890; + background: rgba(255, 98, 0, 0.5); } * { - scrollbar-color: #47556860 transparent; + scrollbar-color: rgba(77, 122, 199, 0.3) transparent; } /* ── Prose / Markdown ───────────────────────────────── */ .prose pre { - background: #0f172a !important; - border: 1px solid #334155; - border-radius: 8px; + background: rgba(2, 7, 15, 0.85) !important; + border: 1px solid rgba(77, 122, 199, 0.2); + border-radius: 12px; } .prose code { font-size: 0.85em; + color: var(--rt-orange-soft); } .prose a { - color: #93c5fd; + color: var(--rt-orange-soft); text-decoration: none; } .prose a:hover { text-decoration: underline; + color: var(--rt-orange); } .prose blockquote { - border-left-color: #60a5fa; + border-left-color: var(--rt-orange); color: #cbd5e1; } .prose strong { - color: #f1f5f9; + color: var(--rt-text); } .prose h1, .prose h2, .prose h3 { - color: #f1f5f9; + color: var(--rt-text); } .prose li::marker { - color: #64748b; + color: rgba(255, 98, 0, 0.6); } /* ── Animations ─────────────────────────────────────── */ -@keyframes shimmer { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } -} - .animate-in { - animation: fadeInUp 350ms cubic-bezier(0.16, 1, 0.3, 1) both; + animation: fadeInUp 380ms cubic-bezier(0.16, 1, 0.3, 1) both; } @keyframes fadeInUp { from { opacity: 0; - transform: translateY(8px) scale(0.98); + transform: translate3d(0, 8px, 0); } to { opacity: 1; - transform: translateY(0) scale(1); + transform: translate3d(0, 0, 0); } } -/* ── Selection ──────────────────────────────────────── */ +/* Result card containment: completed cards are static once done, + so the browser can skip layout/paint when scrolled out of view. */ +.card-result { + contain: layout paint style; + content-visibility: auto; + contain-intrinsic-size: auto 360px; +} + +/* Streaming card stays painted (its content keeps changing) but + its layout/paint are still contained to itself. */ +.card-streaming { + contain: layout paint style; +} + +/* Selection */ ::selection { - background: rgba(96, 165, 250, 0.25); - color: #f1f5f9; + background: rgba(255, 98, 0, 0.35); + color: #fff; +} + +/* Focus ring */ +*:focus-visible { + outline: 1px solid rgba(255, 98, 0, 0.7); + outline-offset: 2px; + border-radius: 6px; +} + +/* Subtle shine sweep on hover */ +.shine { + position: relative; + overflow: hidden; +} +.shine::after { + content: ""; + position: absolute; + top: 0; + left: -100%; + height: 100%; + width: 80%; + background: linear-gradient( + 100deg, + transparent 0%, + rgba(255, 255, 255, 0.06) 50%, + transparent 100% + ); + transition: left 700ms ease; + pointer-events: none; +} +.shine:hover::after { + left: 120%; } diff --git a/app/layout.tsx b/app/layout.tsx index 4236ad1..e277b20 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Toaster } from "sonner"; import "./globals.css"; @@ -17,19 +17,59 @@ export const metadata: Metadata = { manifest: "/site.webmanifest", }; +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 5, + viewportFit: "cover", + themeColor: "#02070F", +}; + export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + + {/* ── Fixed cosmic atmosphere — paint once, GPU-composited ───────── */} +
+
+ {/* Faint starfield (opacity-only twinkle) */} +
+ {/* Two slow drifting orbs (translate-only, smaller blur) */} +
+ {children} diff --git a/app/page.tsx b/app/page.tsx index b3aa23c..a73f541 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,7 @@ "use client"; // ───────────────────────────────────────────────────────────── -// RoundTable — Main Page (Clean Dashboard) +// RoundTable — Main Page (Glassmorphic Enterprise Dashboard) // ───────────────────────────────────────────────────────────── import { useEffect, useCallback, useState } from "react"; @@ -15,6 +15,7 @@ import DisagreementPanel from "@/components/DisagreementPanel"; import CostMeter from "@/components/CostMeter"; import ConfigPanel from "@/components/ConfigPanel"; import PromptLibrary from "@/components/PromptLibrary"; +import { ConsensusNodesArt, ConfigArt } from "@/components/HeroArt"; import { toast } from "sonner"; import { Play, @@ -22,12 +23,15 @@ import { Settings2, Minus, Plus, - Hexagon, Square, Users, ArrowRight, Sparkles, Eye, + Layers, + Cpu, + Menu, + X, } from "lucide-react"; import type { ConsensusEvent, ConsensusRequest } from "@/lib/types"; import { decodeSnapshotFromHash } from "@/lib/session"; @@ -51,6 +55,27 @@ export default function HomePage() { const loadSnapshot = useArenaStore((s) => s.loadSnapshot); const [showOnboarding, setShowOnboarding] = useState(true); + const [drawerOpen, setDrawerOpen] = useState(false); + + // Lock body scroll when the mobile drawer is open + useEffect(() => { + if (typeof document === "undefined") return; + const prev = document.body.style.overflow; + if (drawerOpen) document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = prev; + }; + }, [drawerOpen]); + + // Close drawer on Escape + useEffect(() => { + if (!drawerOpen) return; + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape") setDrawerOpen(false); + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [drawerOpen]); useEffect(() => { if (!showOnboarding) return; @@ -76,7 +101,6 @@ export default function HomePage() { }); }, [setAvailableModels, setModelsLoading]); - // Load a shared snapshot from the URL hash, if present useEffect(() => { if (typeof window === "undefined") return; if (!window.location.hash) return; @@ -114,12 +138,12 @@ export default function HomePage() { return; } - // Clear any URL hash from a previously loaded shared view if (typeof window !== "undefined" && window.location.hash) { history.replaceState(null, "", window.location.pathname); } const controller = state.startConsensus(); + setDrawerOpen(false); toast.info("Consensus started — Esc to cancel"); const body: ConsensusRequest = { @@ -178,20 +202,144 @@ export default function HomePage() { reset(); }, [reset]); + // ── Sidebar content (used both inline at lg+ and inside the mobile drawer) ── + const sidebarContent = ( +
+
+ +
+
+
+ +
+
+

+ Configuration +

+

Provider, model & persona

+
+
+ +
+
+ +
+
+

+ Rounds +

+ + {options.engine === "blind-jury" ? "Locked at 1" : "1–10"} + +
+
+ +
+ + {options.engine === "blind-jury" ? 1 : options.rounds} + +

+ debate rounds +

+
+ +
+
+ +
+
+ +

Protocol & Engine

+
+ +
+ + {/* Sub-xl panels: visible whenever the right rail is hidden */} +
+ + + +
+ + {isRunning && ( +
+
+

+ + + + + Round {currentRound} of {options.rounds} +

+ +
+
+
+
+

+ Press{" "} + + Esc + {" "} + to cancel +

+
+ )} + + {finalScore !== null && ( +
+

+ Final Consensus +

+

+ {finalScore}% +

+ +
+ )} +
+ ); + return ( -
+
{/* Shared-view banner */} {sharedView && ( -
-
- - Viewing a shared session. Reset to start your own run. +
+
+ + Viewing a shared session.
)} @@ -199,19 +347,33 @@ export default function HomePage() { {/* Onboarding */} {showOnboarding && participants.length === 0 && !sharedView && (
setShowOnboarding(false)} + role="dialog" + aria-modal="true" + aria-labelledby="onboarding-title" > -
-
- +
+
+
+
+ +
-

Add Participants to Begin

-

- Select AI providers and models from the sidebar, assign personas, then enter a prompt. -

-
- Sidebar +
+

+ Convene the RoundTable +

+

+ Open the panel, pick a model and persona, then enter your prompt to start a multi-AI + consensus debate. +

+
+
+ Panel Model @@ -224,150 +386,176 @@ export default function HomePage() { )} {/* Header */} -
-
- -
-

- RoundTable -

-

- Multi-AI Consensus Playground -

+
+
+
+ {/* Mobile menu button — visible below lg */} + + + {/* Logo mark */} +
+
+
+ + + + + +
+
+
+

+ RoundTable +

+

+ Multi-AI Consensus Playground +

+

+ Consensus Playground +

+
+
+ +
+
+ + Live + + {participants.length} participants +
+
+ + {participants.length} +
+ + Protocol inspired by askgrokmcp → +
- - Protocol inspired by askgrokmcp -
{/* Main Layout */} -
- {/* Sidebar */} - +
+ )} + + {/* ── Center Main ──────────────────────────────────── */} +
+ {/* Hero command card */} +
+ +
+
+

+ Consensus Console

- - )} - - {finalScore !== null && ( -
-

- Final Consensus: {finalScore}% +

+ Pose a question to the table. +

+

+ Multiple AI minds debate, refine, and converge — surfacing both consensus and + productive disagreement.

- -
- )} -
- - - {/* Center */} -
-
-
-