From 62820318104888c3e98162876263d99694cccf3f Mon Sep 17 00:00:00 2001 From: aseckin Date: Sat, 27 Dec 2025 13:30:49 +0100 Subject: [PATCH 01/27] Initialized FutureEval branding --- .../(main)/futureeval/assets/FE-logo-dark.svg | 25 ++++++++ .../futureeval/assets/FE-logo-light.svg | 25 ++++++++ .../components/futureeval-bulletin.tsx | 29 +++++++++ .../components/futureeval-container.tsx | 22 +++++++ .../futureeval/components/futureeval-hero.tsx | 31 ++++++++++ .../futureeval-leaderboard-hero.tsx | 29 +++++++++ .../components/futureeval-screen.tsx | 26 ++++++++ .../components/futureeval-tabs-shell.tsx | 55 +++++++++++++++++ .../futureeval/components/futureeval-tabs.tsx | 61 +++++++++++++++++++ .../src/app/(main)/futureeval/info/page.tsx | 6 +- .../(main)/futureeval/leaderboard/page.tsx | 14 ++--- .../src/app/(main)/futureeval/news/page.tsx | 6 +- front_end/src/app/(main)/futureeval/page.tsx | 4 +- front_end/src/constants/colors.ts | 27 ++++++++ 14 files changed, 344 insertions(+), 16 deletions(-) create mode 100644 front_end/src/app/(main)/futureeval/assets/FE-logo-dark.svg create mode 100644 front_end/src/app/(main)/futureeval/assets/FE-logo-light.svg create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-container.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-hero.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx diff --git a/front_end/src/app/(main)/futureeval/assets/FE-logo-dark.svg b/front_end/src/app/(main)/futureeval/assets/FE-logo-dark.svg new file mode 100644 index 0000000000..c085a86b36 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/assets/FE-logo-dark.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front_end/src/app/(main)/futureeval/assets/FE-logo-light.svg b/front_end/src/app/(main)/futureeval/assets/FE-logo-light.svg new file mode 100644 index 0000000000..b4cdc55ed5 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/assets/FE-logo-light.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx new file mode 100644 index 0000000000..76adf288cf --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx @@ -0,0 +1,29 @@ +"use client"; + +import Bulletin from "@/app/(main)/components/bulletin"; +import { useModal } from "@/contexts/modal_context"; + +const FutureEvalBulletin: React.FC = () => { + const { setCurrentModal } = useModal(); + return ( + + Congratulations for finding this page! It's still a Work In + Progress, so please take everything presented here with a grain of + salt. If you would like to know more, feel free to{" "} + setCurrentModal({ type: "contactUs" })} + > + contact us + + . +

+ } + /> + ); +}; + +export default FutureEvalBulletin; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx new file mode 100644 index 0000000000..fe0d2c6275 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx @@ -0,0 +1,22 @@ +import { HTMLAttributes } from "react"; + +import cn from "@/utils/core/cn"; + +type Props = HTMLAttributes; + +const FutureEvalContainer: React.FC = ({ className, children }) => { + return ( +
+
+ {children} +
+
+ ); +}; + +export default FutureEvalContainer; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-hero.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-hero.tsx new file mode 100644 index 0000000000..a1460e7252 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-hero.tsx @@ -0,0 +1,31 @@ +import Image from "next/image"; + +import FELogoDark from "../assets/FE-logo-dark.svg?url"; +import FELogoLight from "../assets/FE-logo-light.svg?url"; + +const FutureEvalHero: React.FC = () => { + return ( +
+ {/* Light mode logo */} + FutureEval + {/* Dark mode logo */} + FutureEval +
+ ); +}; + +export default FutureEvalHero; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx new file mode 100644 index 0000000000..4b6c185262 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx @@ -0,0 +1,29 @@ +"use client"; + +import Link from "next/link"; +import { useTranslations } from "next-intl"; + +const FutureEvalLeaderboardHero: React.FC = () => { + const t = useTranslations(); + + return ( +
+ + {t("aibLbBrandLink")} + + +

+ {t("aibLbTitle")} +

+ +
+ {t("aibLbSubtitle")} +
+
+ ); +}; + +export default FutureEvalLeaderboardHero; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx new file mode 100644 index 0000000000..4ae53e7e27 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx @@ -0,0 +1,26 @@ +import { LeaderboardDetails } from "@/types/scoring"; + +import FutureEvalBulletin from "./futureeval-bulletin"; +import FutureEvalContainer from "./futureeval-container"; +import FutureEvalHero from "./futureeval-hero"; +import FutureEvalTabs from "./futureeval-tabs"; +import { Section } from "./futureeval-tabs-shell"; +import { AIBLeaderboardProvider } from "../../aib/components/aib/leaderboard/aib-leaderboard-provider"; + +type Props = { leaderboard: LeaderboardDetails; current: Section["value"] }; + +const FutureEvalScreen: React.FC = ({ leaderboard, current }) => { + return ( + +
+ + + + + +
+
+ ); +}; + +export default FutureEvalScreen; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx new file mode 100644 index 0000000000..05a1f01628 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx @@ -0,0 +1,55 @@ +"use client"; + +import React from "react"; + +import { Tabs, TabsList, TabsSection, TabsTab } from "@/components/ui/tabs"; + +export type Section = { + value: "benchmark" | "info" | "news"; + href: string; + icon: React.ReactNode; + label: string; + content: React.ReactNode; +}; + +type Props = { + current: Section["value"]; + sections: Section[]; +}; + +const FutureEvalTabsShell: React.FC = ({ current, sections }) => { + return ( + + + {sections.map((tab) => ( + + {tab.label} + + ))} + + {sections.map((tab) => ( + } + > + {tab.content} + + ))} + + ); +}; + +export default FutureEvalTabsShell; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx new file mode 100644 index 0000000000..68e19171a1 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx @@ -0,0 +1,61 @@ +import { faCircle } from "@fortawesome/free-regular-svg-icons"; +import { faBook, faBullseye, faInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { getTranslations } from "next-intl/server"; +import React from "react"; + +import FutureEvalTabsShell, { Section } from "./futureeval-tabs-shell"; +import AIBBenchmarkTab from "../../aib/components/aib/tabs/benchmark/aib-benchmark-tab"; +import AIBInfoTab from "../../aib/components/aib/tabs/info/aib-info-tab"; +import AIBNewsTab from "../../aib/components/aib/tabs/news/aib-news-tab"; + +type Props = { + current: Section["value"]; +}; + +const FutureEvalTabs: React.FC = async ({ current }) => { + const t = await getTranslations(); + + const sections: Section[] = [ + { + value: "benchmark", + href: "/futureeval", + icon: ( + + ), + label: t("aibTabsBenchmark"), + content: , + }, + { + value: "info", + href: "/futureeval/info", + icon: ( + + + + + ), + label: t("aibTabsInfo"), + content: , + }, + { + value: "news", + href: "/futureeval/news", + icon: ( + + ), + label: t("aibTabsNews"), + content: , + }, + ]; + + return ; +}; + +export default FutureEvalTabs; diff --git a/front_end/src/app/(main)/futureeval/info/page.tsx b/front_end/src/app/(main)/futureeval/info/page.tsx index 5ba4cc4660..8d2479a4a6 100644 --- a/front_end/src/app/(main)/futureeval/info/page.tsx +++ b/front_end/src/app/(main)/futureeval/info/page.tsx @@ -1,9 +1,9 @@ import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; -import AIBScreen from "../../aib/components/aib/aib-screen"; +import FutureEvalScreen from "../components/futureeval-screen"; export const metadata = { - title: "About AIB | Metaculus", + title: "About FutureEval | Metaculus", description: "Join the AI Forecasting Benchmark (AIB) tournament on Metaculus. Test your AI bot's ability to make accurate probabilistic forecasts on real-world questions. $30,000 prize pool per quarter. Register your bot and compete against the best AI forecasters.", }; @@ -15,5 +15,5 @@ export default async function FutureEvalInfoPage() { "manual", "Global Bot Leaderboard" ); - return ; + return ; } diff --git a/front_end/src/app/(main)/futureeval/leaderboard/page.tsx b/front_end/src/app/(main)/futureeval/leaderboard/page.tsx index 29be0bc565..434dfbff05 100644 --- a/front_end/src/app/(main)/futureeval/leaderboard/page.tsx +++ b/front_end/src/app/(main)/futureeval/leaderboard/page.tsx @@ -1,8 +1,8 @@ import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; -import AIBContainer from "../../aib/components/aib/aib-container"; -import AIBLeaderboardHero from "../../aib/components/aib/leaderboard/aib-leaderboard-hero"; import AIBLeaderboardTable from "../../aib/components/aib/leaderboard/aib-leaderboard-table"; +import FutureEvalContainer from "../components/futureeval-container"; +import FutureEvalLeaderboardHero from "../components/futureeval-leaderboard-hero"; export const metadata = { title: "Top Model Leaderboards | Metaculus", @@ -17,19 +17,17 @@ export default async function FutureEvalLeaderboardsPage() { "Global Bot Leaderboard" ); - console.log("LEADERBOARD DATA", data); - return ( - - + + {data?.entries?.length ? ( ) : ( -
+
Leaderboard data not currently available, please check back soon!
)} - + ); } diff --git a/front_end/src/app/(main)/futureeval/news/page.tsx b/front_end/src/app/(main)/futureeval/news/page.tsx index 5c00bf079f..56d38954f3 100644 --- a/front_end/src/app/(main)/futureeval/news/page.tsx +++ b/front_end/src/app/(main)/futureeval/news/page.tsx @@ -1,9 +1,9 @@ import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; -import AIBScreen from "../../aib/components/aib/aib-screen"; +import FutureEvalScreen from "../components/futureeval-screen"; export const metadata = { - title: "AIB News | Metaculus", + title: "FutureEval News | Metaculus", description: "Join the AI Forecasting Benchmark (AIB) tournament on Metaculus. Test your AI bot's ability to make accurate probabilistic forecasts on real-world questions. $30,000 prize pool per quarter. Register your bot and compete against the best AI forecasters.", }; @@ -15,5 +15,5 @@ export default async function FutureEvalNewsPage() { "manual", "Global Bot Leaderboard" ); - return ; + return ; } diff --git a/front_end/src/app/(main)/futureeval/page.tsx b/front_end/src/app/(main)/futureeval/page.tsx index b2c5a4d239..56f702e8f3 100644 --- a/front_end/src/app/(main)/futureeval/page.tsx +++ b/front_end/src/app/(main)/futureeval/page.tsx @@ -1,6 +1,6 @@ import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; -import AIBScreen from "../aib/components/aib/aib-screen"; +import FutureEvalScreen from "./components/futureeval-screen"; export const metadata = { title: "AI Forecasting Benchmark Tournament | Metaculus", @@ -15,5 +15,5 @@ export default async function FutureEvalPage() { "manual", "Global Bot Leaderboard" ); - return ; + return ; } diff --git a/front_end/src/constants/colors.ts b/front_end/src/constants/colors.ts index af8dabb1b8..942bbfa4bc 100644 --- a/front_end/src/constants/colors.ts +++ b/front_end/src/constants/colors.ts @@ -189,6 +189,33 @@ export const METAC_COLORS = { bell: { DEFAULT: "#b79d00", dark: "#dac024" }, twitter: { DEFAULT: "#1da1f2" }, + // FutureEval brand colors + violet: { + 50: { DEFAULT: "#f5f3ff", dark: "#2f0d68" }, + 100: { DEFAULT: "#ede9fe", dark: "#4d179a" }, + 200: { DEFAULT: "#ddd6ff", dark: "#5d0ec0" }, + 300: { DEFAULT: "#c4b4ff", dark: "#7008e7" }, + 400: { DEFAULT: "#a684ff", dark: "#7f22fe" }, + 500: { DEFAULT: "#8e51ff", dark: "#8e51ff" }, + 600: { DEFAULT: "#7f22fe", dark: "#a684ff" }, + 700: { DEFAULT: "#7008e7", dark: "#c4b4ff" }, + 800: { DEFAULT: "#5d0ec0", dark: "#ddd6ff" }, + 900: { DEFAULT: "#4d179a", dark: "#ede9fe" }, + 950: { DEFAULT: "#2f0d68", dark: "#f5f3ff" }, + }, + lime: { + 50: { DEFAULT: "#f7fee7", dark: "#192e03" }, + 100: { DEFAULT: "#ecfcca", dark: "#35530e" }, + 200: { DEFAULT: "#d8f999", dark: "#3c6300" }, + 300: { DEFAULT: "#bbf451", dark: "#497d00" }, + 400: { DEFAULT: "#9ae600", dark: "#5ea500" }, + 500: { DEFAULT: "#7ccf00", dark: "#7ccf00" }, + 600: { DEFAULT: "#5ea500", dark: "#9ae600" }, + 700: { DEFAULT: "#497d00", dark: "#bbf451" }, + 800: { DEFAULT: "#3c6300", dark: "#d8f999" }, + 900: { DEFAULT: "#35530e", dark: "#ecfcca" }, + 950: { DEFAULT: "#192e03", dark: "#f7fee7" }, + }, } as const; export const MULTIPLE_CHOICE_COLOR_SCALE = Object.values( From bab7e42ab9efbde43a1f5a31421f2951b5737849 Mon Sep 17 00:00:00 2001 From: aseckin Date: Sat, 27 Dec 2025 15:00:16 +0100 Subject: [PATCH 02/27] Redesigned Model Leaderboard --- .../benchmark/futureeval-benchmark-tab.tsx | 30 +++ .../benchmark/futureeval-model-bar.tsx | 134 +++++++++++++ .../benchmark/futureeval-model-benchmark.tsx | 143 +++++++++++++ .../components/futureeval-bulletin.tsx | 2 +- .../components/futureeval-container.tsx | 2 +- .../futureeval-leaderboard-table.tsx | 189 ++++++++++++++++++ .../components/futureeval-screen.tsx | 2 +- .../components/futureeval-tabs-shell.tsx | 10 +- .../futureeval/components/futureeval-tabs.tsx | 4 +- .../(main)/futureeval/leaderboard/page.tsx | 8 +- front_end/tailwind.config.ts | 6 + 11 files changed, 516 insertions(+), 14 deletions(-) create mode 100644 front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-table.tsx diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx new file mode 100644 index 0000000000..1c0e6bef15 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx @@ -0,0 +1,30 @@ +import { + AIBBenchmarkForecastingPerformanceHeader, + AIBBenchmarkProsVsBotsSectionHeader, +} from "@/app/(main)/aib/components/aib/tabs/benchmark/aib-benchmark-subsection-header"; +import AIBBenchmarkForecastingPerformance from "@/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-forecasting-performance"; +import { AIBProsVsBotsDiffExample } from "@/app/(main)/aib/components/aib/tabs/benchmark/pros-vs-bots/aib-pros-vs-bots-comparison"; + +import FutureEvalModelBenchmark from "./futureeval-model-benchmark"; + +const FutureEvalBenchmarkTab: React.FC = () => { + return ( + <> +
+ +
+ +
+ + +
+ +
+ + +
+ + ); +}; + +export default FutureEvalBenchmarkTab; diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx new file mode 100644 index 0000000000..1035cd4f49 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { FloatingPortal } from "@floating-ui/react"; +import { StaticImageData } from "next/image"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { useState } from "react"; + +import { LightDarkIcon } from "@/app/(main)/aib/components/aib/light-dark-icon"; +import cn from "@/utils/core/cn"; + +type Props = { + heightPct: number; + model: { + id: string; + name: string; + score: number; + contributionCount: number; + iconLight?: StaticImageData | string; + iconDark?: StaticImageData | string; + isAggregate?: boolean; + }; +}; + +const FutureEvalModelBar: React.FC = ({ heightPct, model }) => { + const t = useTranslations(); + const router = useRouter(); + const score = Math.round(model.score * 100) / 100; + const [isHovered, setIsHovered] = useState(false); + const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); + + const handleClick = () => { + router.push( + `/futureeval/leaderboard?highlight=${encodeURIComponent(model.id)}` + ); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + setMousePos({ x: e.clientX, y: e.clientY }); + }; + + return ( + <> +
+ {/* Bar area - flex-1 takes remaining height, aligns bar at bottom */} +
+ {/* Score label - sits above the bar */} + + {score} + + + {/* The actual bar with 1px border - hover states and tooltip trigger */} +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onMouseMove={handleMouseMove} + > + {/* Model icon at the TOP of the bar */} + {(model.iconLight || model.iconDark) && ( + + )} +
+ + {/* Small baseline at the bottom */} +
+
+ + {/* Model name below bar - rotated 45 degrees with connecting line */} +
+ {/* Connecting line - centered */} +
+ {/* Rotated label - starts at end of line */} + + {model.name} + +
+
+ + {/* Cursor-following tooltip */} + {isHovered && ( + +
+
+
+ + {t("aibScore")}: + + + {score} + +
+
+ + {t("aibLbThForecasts")}: + + + {model.contributionCount} + +
+
+
+
+ )} + + ); +}; + +export default FutureEvalModelBar; diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx new file mode 100644 index 0000000000..5be4fe6b5c --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx @@ -0,0 +1,143 @@ +"use client"; + +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import React, { useMemo } from "react"; + +import ReusableGradientCarousel from "@/components/gradient-carousel"; + +import FutureEvalModelBar from "./futureeval-model-bar"; +import { useAIBLeaderboard } from "../../../aib/components/aib/leaderboard/aib-leaderboard-provider"; +import { + aggregateKind, + entryIconPair, + entryLabel, + isAggregate, + shouldDisplayEntry, +} from "../../../aib/components/aib/leaderboard/utils"; + +const MAX_VISIBLE_BOTS = 18; // 18 bots + up to 2 aggregates = ~20 total +const MIN_HEIGHT_PCT = 20; +const MAX_HEIGHT_PCT = 100; + +const FutureEvalModelBenchmark: React.FC = () => { + const t = useTranslations(); + const { leaderboard } = useAIBLeaderboard(); + + const entries = useMemo(() => { + const allEntries = leaderboard.entries ?? []; + + // Get aggregate entries (Community Prediction and Pros) + const aggregates = allEntries.filter((e) => { + if (!isAggregate(e)) return false; + const kind = aggregateKind(e); + return kind === "community"; + }); + + // Get bot entries that should be displayed + const bots = allEntries + .filter((e) => !isAggregate(e) && shouldDisplayEntry(e, 300)) + .sort((a, b) => { + if (a.rank != null && b.rank != null) return a.rank - b.rank; + return b.score - a.score; + }) + .slice(0, MAX_VISIBLE_BOTS); + + // Combine and sort by score (highest first) + const combined = [...aggregates, ...bots]; + combined.sort((a, b) => b.score - a.score); + + return combined; + }, [leaderboard.entries]); + + // Scale heights relative to min/max scores + const scaleHeight = useMemo(() => { + if (entries.length === 0) return () => MIN_HEIGHT_PCT; + const scores = entries.map((e) => e.score); + const maxScore = Math.max(...scores); + const minScore = Math.min(...scores); + const range = maxScore - minScore; + + if (range <= 0) return () => MAX_HEIGHT_PCT; + + return (score: number) => { + const normalized = (score - minScore) / range; + return MIN_HEIGHT_PCT + normalized * (MAX_HEIGHT_PCT - MIN_HEIGHT_PCT); + }; + }, [entries]); + + const items = useMemo(() => { + return entries.map((entry) => { + const name = entryLabel(entry, t); + const { light, dark } = entryIconPair(entry); + const aggregate = isAggregate(entry); + + return { + id: String(entry.user?.id ?? name), + name, + score: entry.score, + contributionCount: entry.contribution_count ?? 0, + iconLight: light, + iconDark: dark, + isAggregate: aggregate, + heightPct: scaleHeight(entry.score), + }; + }); + }, [entries, scaleHeight, t]); + + if (items.length === 0) { + return null; + } + + return ( +
+ {/* Header */} +
+

+ {t("aibBenchModelsTitle")} +

+

+ {t("aibBenchModelsBlurb")}{" "} + + {t("aibViewFullLeaderboard")} + +

+
+ + {/* Horizontal bar chart carousel */} +
+ ( + + )} + itemClassName="w-[40px] sm:w-[64px] h-full" + gapClassName="gap-1 sm:gap-2" + gradientFromClass="from-gray-0 dark:from-gray-950" + arrowClassName="w-8 h-8 sm:w-10 sm:h-10 text-blue-700 dark:text-blue-700-dark bg-gray-0 dark:bg-gray-0-dark rounded-full shadow-md border border-blue-700 dark:border-blue-700-dark" + slideBy={{ mode: "items", count: 3 }} + showArrows={true} + className="h-full" + viewportClassName="h-full" + listClassName="h-full items-stretch -ml-2" + /> +
+
+ ); +}; + +export default FutureEvalModelBenchmark; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx index 76adf288cf..82c0fce372 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx @@ -7,7 +7,7 @@ const FutureEvalBulletin: React.FC = () => { const { setCurrentModal } = useModal(); return ( Congratulations for finding this page! It's still a Work In diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx index fe0d2c6275..7ac5b787fc 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx @@ -6,7 +6,7 @@ type Props = HTMLAttributes; const FutureEvalContainer: React.FC = ({ className, children }) => { return ( -
+
= ({ details }) => { + const t = useTranslations(); + const searchParams = useSearchParams(); + const highlightId = searchParams.get("highlight"); + const highlightedRowRef = useRef(null); + + // Scroll to and flash highlighted row + useEffect(() => { + if (highlightId && highlightedRowRef.current) { + highlightedRowRef.current.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + } + }, [highlightId]); + + const rows = useMemo(() => { + const entries = (details.entries ?? []) + .filter((e) => shouldDisplayEntry(e)) + .map((entry, i) => { + const label = entryLabel(entry, t); + const icons = entryIconPair(entry); + const userId = entry.user?.id; + const id = String(userId ?? label); + return { + id, + rank: i + 1, + label, + username: entry.user?.username ?? "", + icons, + forecasts: Math.round((entry.contribution_count ?? 0) * 1000) / 1000, + score: entry.score, + ciLower: entry.ci_lower, + ciUpper: entry.ci_upper, + profileHref: userId ? `/accounts/profile/${userId}/` : null, + isAggregate: !entry.user?.username, + }; + }); + + return entries; + }, [details.entries, t]); + + const hasCI = rows.some((r) => r.ciLower != null || r.ciUpper != null); + + return ( + + + + + + + {hasCI && ( + <> + + + + )} + + + + + + + + {hasCI && ( + <> + + + + )} + + + + + {rows.map((r, i) => { + const isHighlighted = highlightId === r.id; + return ( + + + + + + + + + {hasCI && ( + <> + + + + )} + + ); + })} + +
+ {t("aibLbThModel")} + {t("aibLbThForecasts")} + {t("aibLbThAvgScore")} + {t("aibLbThCILower")} + + {t("aibLbThCIHigher")} +
{i + 1} +
+ {(r.icons.light || r.icons.dark) && ( + + )} +
+
+ {r.isAggregate || !r.profileHref ? ( + r.label + ) : ( + + {r.label} + + )} +
+
+ {r.username} +
+
+
+
+ {r.forecasts} + {fmt(r.score, 2)} + {fmt(r.ciLower, 2)} + + {fmt(r.ciUpper, 2)} +
+ ); +}; + +const fmt = (n: number | null | undefined, d = 2) => + n == null || Number.isNaN(n) ? "—" : n.toFixed(d); + +const Th: React.FC> = ({ + className = "", + children, +}) => ( + + {children} + +); + +const Td: React.FC> = ({ + className = "", + children, +}) => ( + + {children} + +); + +export default FutureEvalLeaderboardTable; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx index 4ae53e7e27..5f62cfa22a 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx @@ -12,7 +12,7 @@ type Props = { leaderboard: LeaderboardDetails; current: Section["value"] }; const FutureEvalScreen: React.FC = ({ leaderboard, current }) => { return ( -
+
diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx index 05a1f01628..255362e210 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx @@ -19,12 +19,8 @@ type Props = { const FutureEvalTabsShell: React.FC = ({ current, sections }) => { return ( - - + + {sections.map((tab) => ( = ({ current, sections }) => { {sections.map((tab) => ( = async ({ current }) => { /> ), label: t("aibTabsBenchmark"), - content: , + content: , }, { value: "info", diff --git a/front_end/src/app/(main)/futureeval/leaderboard/page.tsx b/front_end/src/app/(main)/futureeval/leaderboard/page.tsx index 434dfbff05..505e09ff0c 100644 --- a/front_end/src/app/(main)/futureeval/leaderboard/page.tsx +++ b/front_end/src/app/(main)/futureeval/leaderboard/page.tsx @@ -1,8 +1,10 @@ +import { Suspense } from "react"; + import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; -import AIBLeaderboardTable from "../../aib/components/aib/leaderboard/aib-leaderboard-table"; import FutureEvalContainer from "../components/futureeval-container"; import FutureEvalLeaderboardHero from "../components/futureeval-leaderboard-hero"; +import FutureEvalLeaderboardTable from "../components/futureeval-leaderboard-table"; export const metadata = { title: "Top Model Leaderboards | Metaculus", @@ -22,7 +24,9 @@ export default async function FutureEvalLeaderboardsPage() { {data?.entries?.length ? ( - + }> + + ) : (
Leaderboard data not currently available, please check back soon! diff --git a/front_end/tailwind.config.ts b/front_end/tailwind.config.ts index a7be7f81ff..c8cb594b92 100644 --- a/front_end/tailwind.config.ts +++ b/front_end/tailwind.config.ts @@ -29,11 +29,17 @@ const config: Config = { "0%": { transform: "rotate(0deg)" }, "90%, 100%": { transform: "rotate(360deg)" }, }, + "highlight-flash": { + "0%": { backgroundColor: "rgb(196 180 255 / 0.5)" }, + "50%": { backgroundColor: "rgb(196 180 255 / 0.8)" }, + "100%": { backgroundColor: "transparent" }, + }, }, animation: { "loading-slide": "loading-slide cubic-bezier(0.3, 1, 0.7, 0) 1.7s infinite", spin: "spin 1s infinite", + "highlight-flash": "highlight-flash 2s ease-out forwards", }, fontFamily: { sans: [ From 6d0f5f0aef2258dd90c8726f9e42b52df9cce1aa Mon Sep 17 00:00:00 2001 From: aseckin Date: Wed, 31 Dec 2025 09:15:46 +0100 Subject: [PATCH 03/27] fixed horizontal leaderboard hijacking the vertical scroll --- .../performance-over-time/aib-benchmark-performance-chart.tsx | 2 +- .../components/benchmark/futureeval-model-benchmark.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx index 6ef2f3103d..6698787f94 100644 --- a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx +++ b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx @@ -452,7 +452,7 @@ const LegendDot: FC<{ color: string; label: string }> = ({ color, label }) => ( {label} diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx index 5be4fe6b5c..3de9a12479 100644 --- a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx @@ -131,8 +131,9 @@ const FutureEvalModelBenchmark: React.FC = () => { arrowClassName="w-8 h-8 sm:w-10 sm:h-10 text-blue-700 dark:text-blue-700-dark bg-gray-0 dark:bg-gray-0-dark rounded-full shadow-md border border-blue-700 dark:border-blue-700-dark" slideBy={{ mode: "items", count: 3 }} showArrows={true} + wheelToHorizontal={false} className="h-full" - viewportClassName="h-full" + viewportClassName="h-full overflow-y-hidden" listClassName="h-full items-stretch -ml-2" />
From 6ba6022a10c9db72fbfe843556ea3eb585e82914 Mon Sep 17 00:00:00 2001 From: aseckin Date: Wed, 31 Dec 2025 12:11:08 +0100 Subject: [PATCH 04/27] performance chart updates --- .../aib-benchmark-forecasting-performance.tsx | 18 ++++----- .../aib-benchmark-performance-chart.tsx | 37 ++++++++++++------- .../pros-vs-bots/aib-pros-vs-bots-chart.tsx | 30 +++++++++++---- .../aib-pros-vs-bots-comparison.tsx | 2 +- 4 files changed, 56 insertions(+), 31 deletions(-) diff --git a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-forecasting-performance.tsx b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-forecasting-performance.tsx index 586d94f621..f684c18d8d 100644 --- a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-forecasting-performance.tsx +++ b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-forecasting-performance.tsx @@ -21,20 +21,20 @@ const AIBBenchmarkForecastingPerformance: React.FC = () => { if (!firstIdxByGroup.has(group)) firstIdxByGroup.set(group, i); }); + // Only show these specific groups in the legend + const allowedGroups = ["OpenAI", "Claude", "DeepSeek", "Gemini"]; const legend = [ - ...Array.from(firstIdxByGroup.entries()).map(([label, pointIndex]) => ({ - label, - pointIndex, - })), + ...Array.from(firstIdxByGroup.entries()) + .filter(([label]) => allowedGroups.includes(label)) + .map(([label, pointIndex]) => ({ + label, + pointIndex, + })), { label: t("aibSOTALinearTrend"), trend: true as const }, - { - label: t("aibSotaModels"), - sota: true as const, - }, ]; return ( -
+
); diff --git a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx index 6698787f94..39af82f01e 100644 --- a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx +++ b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx @@ -43,7 +43,7 @@ const AIBBenchmarkPerformanceChart: FC = ({ const { theme, getThemeColor } = useAppTheme(); const chartTheme = theme === "dark" ? darkTheme : lightTheme; const smUp = useBreakpoint("sm"); - const REF_STROKE = getThemeColor(METAC_COLORS.gray[700]); + const REF_STROKE = getThemeColor(METAC_COLORS.purple[700]); const referenceLines = useMemo(() => { const byKey = new Map(); @@ -171,6 +171,9 @@ const AIBBenchmarkPerformanceChart: FC = ({ fontWeight: 400, fill: (args: CallbackArgs) => colorForName((args.datum as { name?: string })?.name || ""), + pointerEvents: "none" as const, + userSelect: "none" as const, + cursor: "default", }; const edgePadLeft = smUp ? 50 : 30; const edgePadRight = rightPad; @@ -217,6 +220,7 @@ const AIBBenchmarkPerformanceChart: FC = ({ "refLabel", "sotaStars", ]} + radius={30} activateData style={{ touchAction: "pan-y" }} labels={({ @@ -248,20 +252,20 @@ const AIBBenchmarkPerformanceChart: FC = ({ tickFormat={smUp ? (d: number) => Math.round(d) : () => ""} style={{ grid: { - stroke: getThemeColor(METAC_COLORS.gray[400]), - strokeWidth: 1, - strokeDasharray: "2,5", + stroke: getThemeColor(METAC_COLORS.gray[900]), + strokeWidth: 0.1, }, axis: { stroke: "transparent" }, ticks: { stroke: "transparent" }, tickLabels: { fill: getThemeColor(METAC_COLORS.gray[500]), - fontSize: smUp ? 16 : 12, + fontSize: smUp ? 12 : 12, fontWeight: 400, + fontFeatureSettings: '"tnum"', }, axisLabel: { fill: getThemeColor(METAC_COLORS.gray[700]), - fontSize: 16, + fontSize: 14, fontWeight: 400, }, }} @@ -286,7 +290,7 @@ const AIBBenchmarkPerformanceChart: FC = ({ ticks: { stroke: "transparent" }, tickLabels: { fill: getThemeColor(METAC_COLORS.gray[500]), - fontSize: smUp ? 16 : 10, + fontSize: smUp ? 12 : 10, }, axisLabel: { fill: getThemeColor(METAC_COLORS.gray[700]), @@ -307,8 +311,8 @@ const AIBBenchmarkPerformanceChart: FC = ({ data: { stroke: REF_STROKE, strokeWidth: 1.5, - strokeDasharray: "4,8", - opacity: 0.7, + opacity: 1, + strokeDasharray: "6,5", }, }} /> @@ -324,8 +328,10 @@ const AIBBenchmarkPerformanceChart: FC = ({ dx={-65} textAnchor="start" style={{ + fontFamily: + 'interVariable, "interVariable Fallback", inter', fontWeight: 600, - fill: getThemeColor(METAC_COLORS.gray[900]), + fill: getThemeColor(METAC_COLORS.purple[700]), }} /> } @@ -347,8 +353,8 @@ const AIBBenchmarkPerformanceChart: FC = ({ data={trend} style={{ data: { - stroke: getThemeColor(METAC_COLORS.blue[800]), - strokeWidth: 2, + stroke: getThemeColor(METAC_COLORS["mc-option"][3]), + strokeWidth: 1.5, strokeDasharray: "6,5", }, }} @@ -374,6 +380,11 @@ const AIBBenchmarkPerformanceChart: FC = ({ data: { fill: ({ datum }) => colorForName((datum as { name: string }).name), + opacity: ({ datum }) => { + const d = datum as { x: Date; y: number; name: string }; + const isSota = labeledKeySet.has(pointKey(d)); + return isSota ? 1 : 0.35; + }, }, }} /> @@ -434,7 +445,7 @@ const AIBBenchmarkPerformanceChart: FC = ({ ) : "trend" in item ? ( ) : ( diff --git a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/pros-vs-bots/aib-pros-vs-bots-chart.tsx b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/pros-vs-bots/aib-pros-vs-bots-chart.tsx index f4d8937dab..da5426130a 100644 --- a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/pros-vs-bots/aib-pros-vs-bots-chart.tsx +++ b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/pros-vs-bots/aib-pros-vs-bots-chart.tsx @@ -216,7 +216,7 @@ const AIBProsVsBotsDiffChart: FC<{ ); const yTicksNoZero = useMemo(() => yTicks.filter((t) => t !== 0), [yTicks]); - const gridStroke = getThemeColor(METAC_COLORS.gray[400]); + const gridStroke = getThemeColor(METAC_COLORS.gray[900]); const axisLabelColor = getThemeColor(METAC_COLORS.gray[700]); const tickLabelColor = getThemeColor(METAC_COLORS.gray[500]); const show = categories.length > 0 && (hasS1 || hasS2); @@ -412,13 +412,21 @@ const AIBProsVsBotsDiffChart: FC<{ style={{ grid: { stroke: gridStroke, - strokeWidth: 1, - strokeDasharray: "2,5", + strokeWidth: 0.1, }, axis: { stroke: "transparent" }, ticks: { stroke: "transparent" }, - tickLabels: { fill: tickLabelColor, fontSize: 16 }, - axisLabel: { fill: axisLabelColor, fontSize: 16 }, + tickLabels: { + fill: tickLabelColor, + fontSize: smUp ? 12 : 12, + fontWeight: 400, + fontFeatureSettings: '"tnum"', + }, + axisLabel: { + fill: axisLabelColor, + fontSize: 14, + fontWeight: 400, + }, }} /> diff --git a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/pros-vs-bots/aib-pros-vs-bots-comparison.tsx b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/pros-vs-bots/aib-pros-vs-bots-comparison.tsx index 2b8c984711..46e23227f6 100644 --- a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/pros-vs-bots/aib-pros-vs-bots-comparison.tsx +++ b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/pros-vs-bots/aib-pros-vs-bots-comparison.tsx @@ -5,7 +5,7 @@ import { ALL_TYPES, BINARY_ONLY_EXAMPLE } from "./config"; export const AIBProsVsBotsDiffExample: React.FC = () => { return ( -
+
Date: Mon, 5 Jan 2026 15:52:53 +0100 Subject: [PATCH 05/27] Applied FutureEval branding to all pages --- front_end/messages/en.json | 4 + .../aib/components/aib/leaderboard/utils.ts | 2 +- .../aib-benchmark-subsection-header.tsx | 12 +-- .../benchmark/futureeval-benchmark-hero.tsx | 20 ++++ .../benchmark/futureeval-model-bar.tsx | 25 +++-- .../benchmark/futureeval-model-benchmark.tsx | 8 +- .../components/futureeval-header.tsx | 99 +++++++++++++++++++ .../components/futureeval-hero-banner.tsx | 55 +++++++++++ .../components/futureeval-methodology-tab.tsx | 19 ++++ .../components/futureeval-participate-tab.tsx | 13 +++ .../components/futureeval-screen.tsx | 11 +-- .../components/futureeval-tabs-shell.tsx | 62 ++++++------ .../futureeval/components/futureeval-tabs.tsx | 38 +++---- .../src/app/(main)/futureeval/info/page.tsx | 21 +--- .../(main)/futureeval/methodology/page.tsx | 19 ++++ .../(main)/futureeval/participate/page.tsx | 19 ++++ front_end/src/utils/fonts.ts | 15 ++- front_end/tailwind.config.ts | 2 + 18 files changed, 342 insertions(+), 102 deletions(-) create mode 100644 front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-hero.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-header.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-hero-banner.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-methodology-tab.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-participate-tab.tsx create mode 100644 front_end/src/app/(main)/futureeval/methodology/page.tsx create mode 100644 front_end/src/app/(main)/futureeval/participate/page.tsx diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 04bbbfc1d1..34bb0932f7 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -1610,7 +1610,11 @@ "aibHeroSubtitle": "Measuring the forecasting

accuracy of AI", "aibTabsBenchmark": "Benchmark", "aibTabsInfo": "Info", + "aibTabsMethodology": "Methodology", + "aibTabsParticipate": "Participate", "aibTabsNews": "News", + "aibBenchmarkHeroTitle": "AI forecasting capabilities, measured against human forecasters.", + "aibBenchmarkHeroSubtitle": "FutureEval tracks how well AI systems predict real-world outcomes through standardized tournaments, comparing their accuracy directly against professional human forecasters on thousands of questions.", "aibBenchModelsTitle": "Model Leaderboard", "aibBenchModelsBlurb": "Updated every day based on our standardized forecasting performance measurement methodology.", "aibProsVsBotsTitle": "How much Pros beat Bots", diff --git a/front_end/src/app/(main)/aib/components/aib/leaderboard/utils.ts b/front_end/src/app/(main)/aib/components/aib/leaderboard/utils.ts index e422675877..e8cdf6484f 100644 --- a/front_end/src/app/(main)/aib/components/aib/leaderboard/utils.ts +++ b/front_end/src/app/(main)/aib/components/aib/leaderboard/utils.ts @@ -40,7 +40,7 @@ export function entryLabel( } const kind = aggregateKind(entry); if (kind === "community") return t("communityPrediction"); - if (kind === "pros") return "Pros aggregate"; + if (kind === "pros") return t("aibLegendPros"); return entry.aggregation_method ?? "Aggregate"; } diff --git a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/aib-benchmark-subsection-header.tsx b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/aib-benchmark-subsection-header.tsx index 6dcaec911a..1d9c3a9bc8 100644 --- a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/aib-benchmark-subsection-header.tsx +++ b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/aib-benchmark-subsection-header.tsx @@ -18,8 +18,8 @@ const AIBBenchmarkSubsectionHeader: React.FC = ({ }) => { return ( <> -
-

+
+

{title}

{infoHref ? ( @@ -34,7 +34,7 @@ const AIBBenchmarkSubsectionHeader: React.FC = ({ ) : null}
-

+

{subtitle}

{children} @@ -51,9 +51,9 @@ export const AIBBenchmarkModelsSubsectionHeader: React.FC = () => { infoHref="/notebooks/38928/futureeval-resources-page/#what-is-the-model-leaderboard" >

{t.rich("aibBenchModelsBlurb", { br: () =>
, diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-hero.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-hero.tsx new file mode 100644 index 0000000000..a167df0737 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-hero.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useTranslations } from "next-intl"; + +const FutureEvalBenchmarkHero: React.FC = () => { + const t = useTranslations(); + + return ( +

+

+ {t("aibBenchmarkHeroTitle")} +

+

+ {t("aibBenchmarkHeroSubtitle")} +

+
+ ); +}; + +export default FutureEvalBenchmarkHero; diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx index 1035cd4f49..3d0475c352 100644 --- a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx @@ -1,6 +1,8 @@ "use client"; import { FloatingPortal } from "@floating-ui/react"; +import { faUsers } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { StaticImageData } from "next/image"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -57,7 +59,7 @@ const FutureEvalModelBar: React.FC = ({ heightPct, model }) => { className={cn( "relative flex w-full flex-col items-center rounded-t-md border pt-2 transition-all duration-200", model.isAggregate - ? "border-violet-800 bg-violet-200 hover:bg-violet-300 dark:border-violet-800-dark dark:bg-violet-800-dark dark:hover:bg-violet-700-dark" + ? "border-violet-800 bg-violet-200 hover:bg-violet-300 dark:border-violet-800-dark dark:bg-violet-800 dark:hover:bg-violet-700-dark" : "border-gray-800 bg-gray-0 hover:bg-gray-300 dark:border-gray-800-dark dark:bg-gray-0-dark dark:hover:bg-gray-300-dark" )} style={{ height: `${heightPct}%`, minHeight: "48px" }} @@ -66,14 +68,21 @@ const FutureEvalModelBar: React.FC = ({ heightPct, model }) => { onMouseMove={handleMouseMove} > {/* Model icon at the TOP of the bar */} - {(model.iconLight || model.iconDark) && ( - + ) : ( + (model.iconLight || model.iconDark) && ( + + ) )}

diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx index 3de9a12479..167e367d25 100644 --- a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx @@ -92,15 +92,15 @@ const FutureEvalModelBenchmark: React.FC = () => { return (
{/* Header */} -
-

+
+

{t("aibBenchModelsTitle")}

-

+

{t("aibBenchModelsBlurb")}{" "} {t("aibViewFullLeaderboard")} diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-header.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-header.tsx new file mode 100644 index 0000000000..84f79dde2c --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-header.tsx @@ -0,0 +1,99 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import React from "react"; + +import cn from "@/utils/core/cn"; + +import FELogoDark from "../assets/FE-logo-dark.svg?url"; +import FELogoLight from "../assets/FE-logo-light.svg?url"; + +export type TabItem = { + value: string; + href: string; + label: string; +}; + +type Props = { + tabs: TabItem[]; + activeTab: string; + onTabChange: (value: string) => void; +}; + +const FutureEvalHeader: React.FC = ({ + tabs, + activeTab, + onTabChange, +}) => { + return ( +

+ {/* Logo */} +
+ FutureEval + FutureEval +
+ + {/* Tab navigation */} + +
+ ); +}; + +type TabLinkProps = { + tab: TabItem; + isActive: boolean; + onClick: () => void; +}; + +const FutureEvalTabLink: React.FC = ({ + tab, + isActive, + onClick, +}) => { + return ( + + {tab.label} + + ); +}; + +export default FutureEvalHeader; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-hero-banner.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-hero-banner.tsx new file mode 100644 index 0000000000..7c4e4e9da4 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-hero-banner.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import React from "react"; + +import cn from "@/utils/core/cn"; + +import FutureEvalHeader, { TabItem } from "./futureeval-header"; + +type Props = { + tabs: TabItem[]; + activeTab: string; + onTabChange: (value: string) => void; +}; + +const FutureEvalHeroBanner: React.FC = ({ + tabs, + activeTab, + onTabChange, +}) => { + const t = useTranslations(); + const showHero = activeTab === "benchmark"; + + return ( +
+
+ {/* Header with logo and tabs */} + + + {/* Hero content - only on Benchmark tab */} + {showHero && ( +
+

+ {t("aibBenchmarkHeroTitle")} +

+

+ {t("aibBenchmarkHeroSubtitle")} +

+
+ )} +
+
+ ); +}; + +export default FutureEvalHeroBanner; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-methodology-tab.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-methodology-tab.tsx new file mode 100644 index 0000000000..91353ab179 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-methodology-tab.tsx @@ -0,0 +1,19 @@ +import AIBInfoIdeaDescription from "../../aib/components/aib/tabs/info/aib-info-idea-description"; +import AIBInfoTournaments from "../../aib/components/aib/tabs/info/aib-info-tournaments"; + +const FutureEvalMethodologyTab: React.FC = () => { + return ( + <> +
+ + +
+
+ + +
+ + ); +}; + +export default FutureEvalMethodologyTab; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-participate-tab.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-participate-tab.tsx new file mode 100644 index 0000000000..94e19e3a16 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-participate-tab.tsx @@ -0,0 +1,13 @@ +import AIBInfoResources from "../../aib/components/aib/tabs/info/aib-info-resources"; +import AIBInfoSubmitSteps from "../../aib/components/aib/tabs/info/aib-info-submit-steps"; + +const FutureEvalParticipateTab: React.FC = () => { + return ( + <> + + + + ); +}; + +export default FutureEvalParticipateTab; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx index 5f62cfa22a..704f7d9580 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-screen.tsx @@ -1,8 +1,5 @@ import { LeaderboardDetails } from "@/types/scoring"; -import FutureEvalBulletin from "./futureeval-bulletin"; -import FutureEvalContainer from "./futureeval-container"; -import FutureEvalHero from "./futureeval-hero"; import FutureEvalTabs from "./futureeval-tabs"; import { Section } from "./futureeval-tabs-shell"; import { AIBLeaderboardProvider } from "../../aib/components/aib/leaderboard/aib-leaderboard-provider"; @@ -12,12 +9,8 @@ type Props = { leaderboard: LeaderboardDetails; current: Section["value"] }; const FutureEvalScreen: React.FC = ({ leaderboard, current }) => { return ( -
- - - - - +
+
); diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx index 255362e210..536c5caacc 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx @@ -1,13 +1,13 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; -import { Tabs, TabsList, TabsSection, TabsTab } from "@/components/ui/tabs"; +import { TabItem } from "./futureeval-header"; +import FutureEvalHeroBanner from "./futureeval-hero-banner"; export type Section = { - value: "benchmark" | "info" | "news"; + value: "benchmark" | "methodology" | "participate" | "news"; href: string; - icon: React.ReactNode; label: string; content: React.ReactNode; }; @@ -18,33 +18,35 @@ type Props = { }; const FutureEvalTabsShell: React.FC = ({ current, sections }) => { + const [active, setActive] = useState(current); + + const activeSection = sections.find((s) => s.value === active); + + // Convert sections to tab items for the header + const tabs: TabItem[] = sections.map((s) => ({ + value: s.value, + href: s.href, + label: s.label, + })); + return ( - - - {sections.map((tab) => ( - - {tab.label} - - ))} - - {sections.map((tab) => ( - } - > - {tab.content} - - ))} - +
+ {/* Hero banner with violet background - edge to edge */} + + + {/* Tab content */} + {activeSection && ( +
+
+ {activeSection.content} +
+
+ )} +
); }; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx index 661e750268..1d3f25d63b 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx @@ -1,12 +1,10 @@ -import { faCircle } from "@fortawesome/free-regular-svg-icons"; -import { faBook, faBullseye, faInfo } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { getTranslations } from "next-intl/server"; import React from "react"; import FutureEvalBenchmarkTab from "./benchmark/futureeval-benchmark-tab"; +import FutureEvalMethodologyTab from "./futureeval-methodology-tab"; +import FutureEvalParticipateTab from "./futureeval-participate-tab"; import FutureEvalTabsShell, { Section } from "./futureeval-tabs-shell"; -import AIBInfoTab from "../../aib/components/aib/tabs/info/aib-info-tab"; import AIBNewsTab from "../../aib/components/aib/tabs/news/aib-news-tab"; type Props = { @@ -20,36 +18,24 @@ const FutureEvalTabs: React.FC = async ({ current }) => { { value: "benchmark", href: "/futureeval", - icon: ( - - ), label: t("aibTabsBenchmark"), content: , }, { - value: "info", - href: "/futureeval/info", - icon: ( - - - - - ), - label: t("aibTabsInfo"), - content: , + value: "methodology", + href: "/futureeval/methodology", + label: t("aibTabsMethodology"), + content: , + }, + { + value: "participate", + href: "/futureeval/participate", + label: t("aibTabsParticipate"), + content: , }, { value: "news", href: "/futureeval/news", - icon: ( - - ), label: t("aibTabsNews"), content: , }, diff --git a/front_end/src/app/(main)/futureeval/info/page.tsx b/front_end/src/app/(main)/futureeval/info/page.tsx index 8d2479a4a6..b6d755a844 100644 --- a/front_end/src/app/(main)/futureeval/info/page.tsx +++ b/front_end/src/app/(main)/futureeval/info/page.tsx @@ -1,19 +1,6 @@ -import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; +import { redirect } from "next/navigation"; -import FutureEvalScreen from "../components/futureeval-screen"; - -export const metadata = { - title: "About FutureEval | Metaculus", - description: - "Join the AI Forecasting Benchmark (AIB) tournament on Metaculus. Test your AI bot's ability to make accurate probabilistic forecasts on real-world questions. $30,000 prize pool per quarter. Register your bot and compete against the best AI forecasters.", -}; - -export default async function FutureEvalInfoPage() { - const leaderboard = await ServerLeaderboardApi.getGlobalLeaderboard( - null, - null, - "manual", - "Global Bot Leaderboard" - ); - return ; +// Redirect /futureeval/info to /futureeval/methodology for backwards compatibility +export default function FutureEvalInfoPage() { + redirect("/futureeval/methodology"); } diff --git a/front_end/src/app/(main)/futureeval/methodology/page.tsx b/front_end/src/app/(main)/futureeval/methodology/page.tsx new file mode 100644 index 0000000000..8bd2a100fb --- /dev/null +++ b/front_end/src/app/(main)/futureeval/methodology/page.tsx @@ -0,0 +1,19 @@ +import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; + +import FutureEvalScreen from "../components/futureeval-screen"; + +export const metadata = { + title: "Methodology | FutureEval | Metaculus", + description: + "Learn about FutureEval's methodology for measuring AI forecasting accuracy. Understand how we benchmark AI systems against human pro forecasters.", +}; + +export default async function FutureEvalMethodologyPage() { + const leaderboard = await ServerLeaderboardApi.getGlobalLeaderboard( + null, + null, + "manual", + "Global Bot Leaderboard" + ); + return ; +} diff --git a/front_end/src/app/(main)/futureeval/participate/page.tsx b/front_end/src/app/(main)/futureeval/participate/page.tsx new file mode 100644 index 0000000000..050e20db4c --- /dev/null +++ b/front_end/src/app/(main)/futureeval/participate/page.tsx @@ -0,0 +1,19 @@ +import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; + +import FutureEvalScreen from "../components/futureeval-screen"; + +export const metadata = { + title: "Participate | FutureEval | Metaculus", + description: + "Join the FutureEval AI Forecasting Benchmark. Submit your AI bot to compete against the best AI forecasters and human pros.", +}; + +export default async function FutureEvalParticipatePage() { + const leaderboard = await ServerLeaderboardApi.getGlobalLeaderboard( + null, + null, + "manual", + "Global Bot Leaderboard" + ); + return ; +} diff --git a/front_end/src/utils/fonts.ts b/front_end/src/utils/fonts.ts index 6e4731e923..229e612026 100644 --- a/front_end/src/utils/fonts.ts +++ b/front_end/src/utils/fonts.ts @@ -1,3 +1,4 @@ +import { Geist, Geist_Mono } from "next/font/google"; import localFont from "next/font/local"; export const sourceSerifPro = localFont({ @@ -86,6 +87,18 @@ export const leagueGothic = localFont({ preload: false, }); +export const geist = Geist({ + subsets: ["latin"], + variable: "--font-geist", + display: "swap", +}); + +export const geistMono = Geist_Mono({ + subsets: ["latin"], + variable: "--font-geist-mono", + display: "swap", +}); + export const getFontsString = () => { - return `${interVariable.variable} ${inter.variable} ${sourceSerifPro.variable} ${leagueGothic.variable}`; + return `${interVariable.variable} ${inter.variable} ${sourceSerifPro.variable} ${leagueGothic.variable} ${geist.variable} ${geistMono.variable}`; }; diff --git a/front_end/tailwind.config.ts b/front_end/tailwind.config.ts index c8cb594b92..68da9760a5 100644 --- a/front_end/tailwind.config.ts +++ b/front_end/tailwind.config.ts @@ -53,6 +53,8 @@ const config: Config = { ], mono: ['"Ubuntu mono"', ...defaultTheme.fontFamily.mono], "league-gothic": "var(--font-league-gothic)", + geist: ["var(--font-geist)", ...defaultTheme.fontFamily.sans], + "geist-mono": ["var(--font-geist-mono)", ...defaultTheme.fontFamily.mono], }, strokeWidth: { "3": "3px", From c8b56a7f38b7eeecdd6cb1289347433a2cbe0e03 Mon Sep 17 00:00:00 2001 From: aseckin Date: Thu, 8 Jan 2026 16:31:13 +0100 Subject: [PATCH 06/27] Improvements to Performance Over Time chart --- .../aib-benchmark-forecasting-performance.tsx | 13 +- .../aib-benchmark-performance-chart.tsx | 457 +++++++++++++++--- 2 files changed, 391 insertions(+), 79 deletions(-) diff --git a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-forecasting-performance.tsx b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-forecasting-performance.tsx index f684c18d8d..4495adbe14 100644 --- a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-forecasting-performance.tsx +++ b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-forecasting-performance.tsx @@ -21,15 +21,12 @@ const AIBBenchmarkForecastingPerformance: React.FC = () => { if (!firstIdxByGroup.has(group)) firstIdxByGroup.set(group, i); }); - // Only show these specific groups in the legend - const allowedGroups = ["OpenAI", "Claude", "DeepSeek", "Gemini"]; + // Show all companies in the legend (no filtering) const legend = [ - ...Array.from(firstIdxByGroup.entries()) - .filter(([label]) => allowedGroups.includes(label)) - .map(([label, pointIndex]) => ({ - label, - pointIndex, - })), + ...Array.from(firstIdxByGroup.entries()).map(([label, pointIndex]) => ({ + label, + pointIndex, + })), { label: t("aibSOTALinearTrend"), trend: true as const }, ]; diff --git a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx index 39af82f01e..d19801dfcd 100644 --- a/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx +++ b/front_end/src/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-performance-chart.tsx @@ -1,7 +1,10 @@ "use client"; +import { faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import classNames from "classnames"; import { useTranslations } from "next-intl"; -import { FC, useMemo } from "react"; +import { FC, useMemo, useState, useCallback } from "react"; import { VictoryAxis, VictoryChart, @@ -45,6 +48,28 @@ const AIBBenchmarkPerformanceChart: FC = ({ const smUp = useBreakpoint("sm"); const REF_STROKE = getThemeColor(METAC_COLORS.purple[700]); + // State for legend interactivity + const [hoveredCompany, setHoveredCompany] = useState(null); + const [selectedCompany, setSelectedCompany] = useState(null); + + const handleCompanyHover = useCallback((company: string | null) => { + setHoveredCompany(company); + }, []); + + const handleCompanyClick = useCallback((company: string) => { + setSelectedCompany((prev) => (prev === company ? null : company)); + }, []); + + const handleDeselectCompany = useCallback(() => { + setSelectedCompany(null); + }, []); + + // Normalize model name to company/group name + const normalizeToCompany = useCallback((name: string) => { + const first = String(name).split(" ")[0] ?? name; + return /^gpt/i.test(first) ? "OpenAI" : first; + }, []); + const referenceLines = useMemo(() => { const byKey = new Map(); for (const d of data) { @@ -94,6 +119,15 @@ const AIBBenchmarkPerformanceChart: FC = ({ return result; }, [plotPoints]); + // Filter SOTA points for labels/stars based on selected company + // (labels/stars only show for selected company when one is active) + const filteredSotaPoints = useMemo(() => { + if (!selectedCompany) return sotaPoints; + return sotaPoints.filter( + (p) => normalizeToCompany(p.name) === selectedCompany + ); + }, [sotaPoints, selectedCompany, normalizeToCompany]); + const yMeta = useMemo(() => { const vals = [ ...plotPoints.map((p) => p.y), @@ -129,13 +163,25 @@ const AIBBenchmarkPerformanceChart: FC = ({ return [first, last]; }, [timeTicks, xDomain]); + // Minimum points required to show trend line + const MIN_TREND_POINTS = 3; + + // Trend line - hide when a company is selected const trend = useMemo(() => { + // Don't show trend line when a company is selected + if (selectedCompany) return null; + + // Prefer SOTA points if available, otherwise use all points const base = sotaPoints.length >= 2 ? sotaPoints : plotPoints; + + // Don't show trend line if not enough points + if (base.length < MIN_TREND_POINTS) return null; + return fitTrend( base.map((p) => ({ x: p.x as Date, y: p.y as number })), yMeta ); - }, [sotaPoints, plotPoints, yMeta]); + }, [sotaPoints, plotPoints, selectedCompany, yMeta]); const groupIndexByLabel = useMemo(() => { const m = new Map(); @@ -146,8 +192,7 @@ const AIBBenchmarkPerformanceChart: FC = ({ }, [legend]); const colorForName = (name: string) => { - const first = String(name).split(" ")[0] ?? name; - const group = /^gpt/i.test(first) ? "OpenAI" : first; + const group = normalizeToCompany(name); const idx = groupIndexByLabel.get(group); return colorFor( typeof idx === "number" ? { index: idx } : { index: 0 } @@ -179,21 +224,211 @@ const AIBBenchmarkPerformanceChart: FC = ({ const edgePadRight = rightPad; const pointKey = (p: { x: Date; y: number; name: string }) => `${+p.x}|${p.y}|${p.name}`; - const labeledKeySet = useMemo( + + // Use sotaPoints for labeled set (not filtered) since we show all dots + const allSotaKeySet = useMemo( () => new Set(sotaPoints.map(pointKey)), [sotaPoints] ); - const hoverPoints = useMemo( + + // Labeled key set for SOTA labels/stars (filtered when company selected) + const labeledKeySet = useMemo( + () => new Set(filteredSotaPoints.map(pointKey)), + [filteredSotaPoints] + ); + + // Add company info to each point for rendering + const enrichedPlotPoints = useMemo( () => - plotPoints.map((p) => ({ + plotPoints.map((p) => { + const key = pointKey(p); + const company = normalizeToCompany(p.name); + const isSelectedCompany = selectedCompany + ? company === selectedCompany + : null; + const isSota = allSotaKeySet.has(key); + return { + ...p, + _key: key, + _company: company, + _isSota: isSota, + _isSelectedCompany: isSelectedCompany, + // Only apply legend hover highlighting when no company is selected + _isHoveredCompany: selectedCompany + ? null + : hoveredCompany + ? company === hoveredCompany + : null, + }; + }), + [ + plotPoints, + selectedCompany, + hoveredCompany, + normalizeToCompany, + allSotaKeySet, + ] + ); + + // Calculate opacity for a point based on hover/selection state + const getPointOpacity = (point: { + _isSota?: boolean; + _isSelectedCompany?: boolean | null; + _isHoveredCompany?: boolean | null; + }) => { + // If a company is selected + if (point._isSelectedCompany !== null) { + // Selected company's dots are 100%, other companies are 20% + return point._isSelectedCompany ? 1 : 0.2; + } + + // If a company is hovered in the legend + if (point._isHoveredCompany !== null) { + return point._isHoveredCompany ? 1 : 0.35; + } + + // Default: SOTA points are full opacity, others are dimmed + return point._isSota ? 1 : 0.35; + }; + + // Calculate opacity for star markers based on hover state + const getStarOpacity = useCallback( + (name: string) => { + const company = normalizeToCompany(name); + + // If a company is hovered in the legend + if (hoveredCompany) { + return company === hoveredCompany ? 1 : 0.35; + } + + return 1; + }, + [hoveredCompany, normalizeToCompany] + ); + + // Count dots per company + const companyDotCounts = useMemo(() => { + const counts = new Map(); + for (const p of plotPoints) { + const company = normalizeToCompany(p.name); + counts.set(company, (counts.get(company) ?? 0) + 1); + } + return counts; + }, [plotPoints, normalizeToCompany]); + + // Check if selected company has few dots (< 3) + const selectedCompanyHasFewDots = useMemo(() => { + if (!selectedCompany) return false; + const count = companyDotCounts.get(selectedCompany) ?? 0; + return count < 3; + }, [selectedCompany, companyDotCounts]); + + // Get all points for selected company (for labeling when few dots) + const selectedCompanyPoints = useMemo(() => { + if (!selectedCompany || !selectedCompanyHasFewDots) return []; + return plotPoints.filter( + (p) => normalizeToCompany(p.name) === selectedCompany + ); + }, [ + plotPoints, + selectedCompany, + selectedCompanyHasFewDots, + normalizeToCompany, + ]); + + // Data for hover detection + // When a company is selected, only that company's dots should be hoverable + const hoverDetectionPoints = useMemo(() => { + // Filter to only selected company's dots when one is selected + const pointsToUse = selectedCompany + ? plotPoints.filter((p) => normalizeToCompany(p.name) === selectedCompany) + : plotPoints; + + return pointsToUse.map((p) => { + const key = pointKey(p); + const company = normalizeToCompany(p.name); + const isSelectedCompanyDot = + selectedCompany && company === selectedCompany; + + // Suppress hover for: + // 1. SOTA points (they have permanent labels) + // 2. All selected company dots when company has < 3 dots (they show all labels) + const suppressHover = + labeledKeySet.has(key) || + (isSelectedCompanyDot && selectedCompanyHasFewDots); + + return { ...p, - suppressHover: labeledKeySet.has(pointKey(p)), - })), - [plotPoints, labeledKeySet] + _key: key, + suppressHover, + }; + }); + }, [ + plotPoints, + labeledKeySet, + selectedCompany, + selectedCompanyHasFewDots, + normalizeToCompany, + ]); + + // Separate company legend items from trend/sota items + const companyLegendItems = useMemo( + () => (legend ?? []).filter((item) => "pointIndex" in item), + [legend] + ); + const trendLegendItems = useMemo( + () => (legend ?? []).filter((item) => "trend" in item || "sota" in item), + [legend] ); return (
+ {/* Legend above the chart */} + {legend?.length ? ( +
+ {/* Company legend items (interactive) - hidden on mobile */} + {smUp && + companyLegendItems.map((item, i) => + "pointIndex" in item ? ( + + ) : null + )} + {/* Spacer for double gap before trend items - only needed on desktop */} + {smUp && trendLegendItems.length > 0 && ( + + )} + {/* Trend/SOTA legend items (non-interactive) */} + {trendLegendItems.map((item, i) => + "trend" in item ? ( + + ) : ( + + ) + )} +
+ ) : null} + {width === 0 &&
} {width > 0 && ( = ({ voronoiBlacklist={[ "bgPoints", "labelsLayer", + "allLabelsLayer", "points", "trend", "refLine", @@ -340,7 +576,7 @@ const AIBBenchmarkPerformanceChart: FC = ({ = ({ = ({ - colorForName((datum as { name: string }).name), - opacity: ({ datum }) => { - const d = datum as { x: Date; y: number; name: string }; - const isSota = labeledKeySet.has(pointKey(d)); - return isSota ? 1 : 0.35; + fill: (args: CallbackArgs) => + colorForName((args.datum as { name: string }).name), + opacity: (args: CallbackArgs) => { + const d = args.datum as { + _isSota?: boolean; + _isSelectedCompany?: boolean | null; + _isHoveredCompany?: boolean | null; + }; + return getPointOpacity(d); }, }, }} /> - labelText(datum as { name?: string })} - labelComponent={ - - } - style={{ data: { opacity: 0 } }} - /> + {/* Labels for SOTA points (filtered by selected company when one is active) */} + {/* Only show when company is not selected OR selected company has >= 3 dots */} + {(!selectedCompany || !selectedCompanyHasFewDots) && ( + labelText(datum as { name?: string })} + labelComponent={ + + getStarOpacity( + (args.datum as { name?: string })?.name || "" + ), + }} + /> + } + style={{ data: { opacity: 0 } }} + /> + )} + {/* Labels for ALL points when selected company has < 3 dots */} + {selectedCompanyHasFewDots && ( + labelText(datum as { name?: string })} + labelComponent={ + + } + style={{ data: { opacity: 0 } }} + /> + )} + + {/* SOTA stars - show filtered set based on selection */} = ({ colorForName((datum as { name: string }).name), stroke: getThemeColor(METAC_COLORS.gray[0]), strokeWidth: 0.5, + opacity: ({ datum }) => + getStarOpacity((datum as { name: string }).name), }, }} /> )} - - {legend?.length ? ( -
- {legend.map((item, i) => - "pointIndex" in item ? ( - - ) : "trend" in item ? ( - - ) : ( - - ) - )} -
- ) : null}
); }; -const LegendDot: FC<{ color: string; label: string }> = ({ color, label }) => ( - +type LegendDotProps = { + color: string; + label: string; + isHovered?: boolean; + isSelected?: boolean; + isDimmed?: boolean; + onHover?: (company: string | null) => void; + onClick?: (company: string) => void; + onDeselect?: () => void; +}; + +const LegendDot: FC = ({ + color, + label, + isHovered = false, + isSelected = false, + isDimmed = false, + onHover, + onClick, + onDeselect, +}) => ( + ); -const LegendTrend: FC<{ color: string; label: string }> = ({ +const LegendTrend: FC<{ color: string; label: string; isDimmed?: boolean }> = ({ color, label, + isDimmed = false, }) => ( - + Date: Fri, 9 Jan 2026 03:44:40 +0100 Subject: [PATCH 07/27] Further theming and styling --- .../src/app/(main)/components/bulletins.tsx | 1 + .../futureeval-benchmark-headers.tsx | 89 ++++++ .../benchmark/futureeval-benchmark-hero.tsx | 20 -- .../benchmark/futureeval-benchmark-tab.tsx | 14 +- .../benchmark/futureeval-model-bar.tsx | 13 +- .../benchmark/futureeval-model-benchmark.tsx | 38 +-- .../components/futureeval-bulletin.tsx | 29 -- .../components/futureeval-container.tsx | 4 +- .../components/futureeval-header.tsx | 2 +- .../components/futureeval-hero-banner.tsx | 36 ++- .../futureeval/components/futureeval-hero.tsx | 31 --- .../components/futureeval-info-popover.tsx | 149 ++++++++++ .../futureeval-leaderboard-hero.tsx | 48 +++- .../futureeval-methodology-content.tsx | 166 ++++++++++++ .../components/futureeval-methodology-tab.tsx | 18 +- .../components/futureeval-navbar.tsx | 89 ++++++ .../components/futureeval-participate-tab.tsx | 255 +++++++++++++++++- .../components/futureeval-tabs-shell.tsx | 7 +- .../futureeval/components/futureeval-tabs.tsx | 12 +- .../components/futureeval-tournaments.tsx | 91 +++++++ .../(main)/futureeval/leaderboard/page.tsx | 28 +- front_end/src/app/(main)/futureeval/theme.ts | 94 +++++++ .../src/components/gradient-carousel.tsx | 12 +- front_end/src/utils/navigation.ts | 3 +- 24 files changed, 1083 insertions(+), 166 deletions(-) create mode 100644 front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx delete mode 100644 front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-hero.tsx delete mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx delete mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-hero.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-info-popover.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-methodology-content.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-navbar.tsx create mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-tournaments.tsx create mode 100644 front_end/src/app/(main)/futureeval/theme.ts diff --git a/front_end/src/app/(main)/components/bulletins.tsx b/front_end/src/app/(main)/components/bulletins.tsx index 085dcbc62a..d3a8cd38ab 100644 --- a/front_end/src/app/(main)/components/bulletins.tsx +++ b/front_end/src/app/(main)/components/bulletins.tsx @@ -16,6 +16,7 @@ const HIDE_PREFIXES = [ "/press", "/privacy-policy", "/terms-of-use", + "/futureeval", ] as const; const Bulletins: FC = () => { diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx new file mode 100644 index 0000000000..91df98f5f9 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx @@ -0,0 +1,89 @@ +"use client"; + +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { PropsWithChildren } from "react"; + +import cn from "@/utils/core/cn"; + +import { FE_COLORS } from "../../theme"; + +type Props = PropsWithChildren<{ + title?: React.ReactNode; + subtitle?: React.ReactNode; +}>; + +/** + * FutureEval-specific subsection header with left alignment + */ +const FutureEvalSubsectionHeader: React.FC = ({ + title, + subtitle, + children, +}) => { + return ( + <> +
+

+ {title} +

+
+ +

+ {subtitle} +

+ {children} + + ); +}; + +/** + * Forecasting Performance Over Time header (left-aligned) + */ +export const FutureEvalForecastingPerformanceHeader: React.FC = () => { + const t = useTranslations(); + + return ( + + ); +}; + +/** + * Pros vs Bots header with left alignment + */ +export const FutureEvalProsVsBotsSectionHeader: React.FC = () => { + const t = useTranslations(); + + return ( + ( + + {chunks} + + ), + brSm: () =>
, + brXs: () =>
, + })} + /> + ); +}; + +export default FutureEvalSubsectionHeader; diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-hero.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-hero.tsx deleted file mode 100644 index a167df0737..0000000000 --- a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-hero.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import { useTranslations } from "next-intl"; - -const FutureEvalBenchmarkHero: React.FC = () => { - const t = useTranslations(); - - return ( -
-

- {t("aibBenchmarkHeroTitle")} -

-

- {t("aibBenchmarkHeroSubtitle")} -

-
- ); -}; - -export default FutureEvalBenchmarkHero; diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx index 1c0e6bef15..f46efce5ad 100644 --- a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx @@ -1,10 +1,10 @@ -import { - AIBBenchmarkForecastingPerformanceHeader, - AIBBenchmarkProsVsBotsSectionHeader, -} from "@/app/(main)/aib/components/aib/tabs/benchmark/aib-benchmark-subsection-header"; import AIBBenchmarkForecastingPerformance from "@/app/(main)/aib/components/aib/tabs/benchmark/performance-over-time/aib-benchmark-forecasting-performance"; import { AIBProsVsBotsDiffExample } from "@/app/(main)/aib/components/aib/tabs/benchmark/pros-vs-bots/aib-pros-vs-bots-comparison"; +import { + FutureEvalForecastingPerformanceHeader, + FutureEvalProsVsBotsSectionHeader, +} from "./futureeval-benchmark-headers"; import FutureEvalModelBenchmark from "./futureeval-model-benchmark"; const FutureEvalBenchmarkTab: React.FC = () => { @@ -14,13 +14,15 @@ const FutureEvalBenchmarkTab: React.FC = () => {
+ {/* Forecasting Performance Over Time */}
- +
+ {/* Pros vs Bots */}
- +
diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx index 3d0475c352..48bdecd06d 100644 --- a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx @@ -11,6 +11,8 @@ import { useState } from "react"; import { LightDarkIcon } from "@/app/(main)/aib/components/aib/light-dark-icon"; import cn from "@/utils/core/cn"; +import { FE_COLORS } from "../../theme"; + type Props = { heightPct: number; model: { @@ -59,7 +61,11 @@ const FutureEvalModelBar: React.FC = ({ heightPct, model }) => { className={cn( "relative flex w-full flex-col items-center rounded-t-md border pt-2 transition-all duration-200", model.isAggregate - ? "border-violet-800 bg-violet-200 hover:bg-violet-300 dark:border-violet-800-dark dark:bg-violet-800 dark:hover:bg-violet-700-dark" + ? cn( + FE_COLORS.barAggregateBorder, + FE_COLORS.barAggregateBg, + FE_COLORS.barAggregateHover + ) : "border-gray-800 bg-gray-0 hover:bg-gray-300 dark:border-gray-800-dark dark:bg-gray-0-dark dark:hover:bg-gray-300-dark" )} style={{ height: `${heightPct}%`, minHeight: "48px" }} @@ -71,7 +77,10 @@ const FutureEvalModelBar: React.FC = ({ heightPct, model }) => { {model.isAggregate ? ( ) : ( (model.iconLight || model.iconDark) && ( diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx index 167e367d25..b6c5b38677 100644 --- a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx +++ b/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx @@ -6,7 +6,6 @@ import React, { useMemo } from "react"; import ReusableGradientCarousel from "@/components/gradient-carousel"; -import FutureEvalModelBar from "./futureeval-model-bar"; import { useAIBLeaderboard } from "../../../aib/components/aib/leaderboard/aib-leaderboard-provider"; import { aggregateKind, @@ -15,6 +14,9 @@ import { isAggregate, shouldDisplayEntry, } from "../../../aib/components/aib/leaderboard/utils"; +import { FE_COLORS } from "../../theme"; +import FutureEvalInfoPopover from "../futureeval-info-popover"; +import FutureEvalModelBar from "./futureeval-model-bar"; const MAX_VISIBLE_BOTS = 18; // 18 bots + up to 2 aggregates = ~20 total const MIN_HEIGHT_PCT = 20; @@ -92,19 +94,23 @@ const FutureEvalModelBenchmark: React.FC = () => { return (
{/* Header */} -
-

- {t("aibBenchModelsTitle")} -

-

- {t("aibBenchModelsBlurb")}{" "} - +

+

+ {t("aibBenchModelsTitle")} +

+

- {t("aibViewFullLeaderboard")} - -

+ {t("aibBenchModelsBlurb")}{" "} + + {t("aibViewFullLeaderboard")} + +

+
+
{/* Horizontal bar chart carousel */} @@ -127,8 +133,10 @@ const FutureEvalModelBenchmark: React.FC = () => { )} itemClassName="w-[40px] sm:w-[64px] h-full" gapClassName="gap-1 sm:gap-2" - gradientFromClass="from-gray-0 dark:from-gray-950" - arrowClassName="w-8 h-8 sm:w-10 sm:h-10 text-blue-700 dark:text-blue-700-dark bg-gray-0 dark:bg-gray-0-dark rounded-full shadow-md border border-blue-700 dark:border-blue-700-dark" + gradientFromClass={FE_COLORS.gradientFrom} + arrowClassName={`w-7 h-7 sm:w-10 sm:h-10 ${FE_COLORS.textSubheading} ${FE_COLORS.carouselArrowBg} rounded-full shadow-md ${FE_COLORS.cardBorder}`} + arrowLeftPosition="left-1 sm:left-[18px]" + arrowRightPosition="right-1 sm:right-[18px]" slideBy={{ mode: "items", count: 3 }} showArrows={true} wheelToHorizontal={false} diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx deleted file mode 100644 index 82c0fce372..0000000000 --- a/front_end/src/app/(main)/futureeval/components/futureeval-bulletin.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import Bulletin from "@/app/(main)/components/bulletin"; -import { useModal } from "@/contexts/modal_context"; - -const FutureEvalBulletin: React.FC = () => { - const { setCurrentModal } = useModal(); - return ( - - Congratulations for finding this page! It's still a Work In - Progress, so please take everything presented here with a grain of - salt. If you would like to know more, feel free to{" "} - setCurrentModal({ type: "contactUs" })} - > - contact us - - . -

- } - /> - ); -}; - -export default FutureEvalBulletin; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx index 7ac5b787fc..f394695b41 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx @@ -2,11 +2,13 @@ import { HTMLAttributes } from "react"; import cn from "@/utils/core/cn"; +import { FE_COLORS } from "../theme"; + type Props = HTMLAttributes; const FutureEvalContainer: React.FC = ({ className, children }) => { return ( -
+
= ({ onTabChange, }) => { return ( -
+
{/* Logo */}
= ({ activeTab, onTabChange, }) => { - const t = useTranslations(); const showHero = activeTab === "benchmark"; return ( -
+
@@ -38,13 +37,30 @@ const FutureEvalHeroBanner: React.FC = ({ {/* Hero content - only on Benchmark tab */} {showHero && ( -
-

- {t("aibBenchmarkHeroTitle")} +
+

+ Measuring the forecasting accuracy of AI

-

- {t("aibBenchmarkHeroSubtitle")} -

+
    +
  • + Model Benchmark: How well AI models perform over time with a + standardized prompt +
  • +
  • + Bot Competition: Compete with credits in seasonal tournaments to + build the best forecasting bots using scaffolding and prompts. +
  • +
)}

diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-hero.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-hero.tsx deleted file mode 100644 index a1460e7252..0000000000 --- a/front_end/src/app/(main)/futureeval/components/futureeval-hero.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import Image from "next/image"; - -import FELogoDark from "../assets/FE-logo-dark.svg?url"; -import FELogoLight from "../assets/FE-logo-light.svg?url"; - -const FutureEvalHero: React.FC = () => { - return ( -
- {/* Light mode logo */} - FutureEval - {/* Dark mode logo */} - FutureEval -
- ); -}; - -export default FutureEvalHero; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-info-popover.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-info-popover.tsx new file mode 100644 index 0000000000..3244a58e50 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-info-popover.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { + autoUpdate, + flip, + FloatingPortal, + offset, + shift, + useClick, + useDismiss, + useFloating, + useInteractions, + useRole, +} from "@floating-ui/react"; +import { faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Link from "next/link"; +import React, { useState } from "react"; + +import Button from "@/components/ui/button"; +import cn from "@/utils/core/cn"; + +import { FE_COLORS } from "../theme"; + +type Props = { + defaultOpen?: boolean; +}; + +const FutureEvalInfoPopover: React.FC = ({ defaultOpen = false }) => { + const [open, setOpen] = useState(defaultOpen); + + const { refs, floatingStyles, context, isPositioned } = useFloating({ + open, + onOpenChange: setOpen, + placement: "bottom-end", + strategy: "fixed", + whileElementsMounted: autoUpdate, + middleware: [ + offset(12), + flip({ padding: 12 }), + shift({ + padding: { + top: 60, // Account for navbar + left: 12, + right: 12, + bottom: 12, + }, + }), + ], + }); + + const click = useClick(context); + const dismiss = useDismiss(context, { outsidePress: true }); + const role = useRole(context, { role: "dialog" }); + + const { getReferenceProps, getFloatingProps } = useInteractions([ + click, + dismiss, + role, + ]); + + return ( + <> + {/* Info button - ensure square aspect ratio */} + + + {/* Popover content */} + {open && ( + +
+
+

+ We run all major models with a simple prompt on most open + Metaculus forecasting questions, and collect their forecasts. As + questions resolve, we score the models' forecasts and + continuously update our leaderboard to rank them against each + other. +

+ +

+ Since we measure against real world events, it takes time for + new models to populate the leaderboard. +

+ +
+ + Learn more here. + +
+ + +
+
+
+ )} + + ); +}; + +export default FutureEvalInfoPopover; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx index 4b6c185262..585cee13a6 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx @@ -1,27 +1,51 @@ "use client"; -import Link from "next/link"; +import { faArrowLeft } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useTranslations } from "next-intl"; +import Button from "@/components/ui/button"; +import cn from "@/utils/core/cn"; + +import { FE_COLORS } from "../theme"; + const FutureEvalLeaderboardHero: React.FC = () => { const t = useTranslations(); return ( -
- - {t("aibLbBrandLink")} - +
+ {/* Back button - subtle tertiary style */} +
+ +
-

+ {/* Title - left aligned on desktop, centered on mobile */} +

{t("aibLbTitle")}

-
- {t("aibLbSubtitle")} -
+ {/* Subtitle - left aligned on desktop, centered on mobile */} +

+ {t("aibLbSubtitle")} +

); }; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-methodology-content.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-methodology-content.tsx new file mode 100644 index 0000000000..33a925634d --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-methodology-content.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { faCircleDot } from "@fortawesome/free-regular-svg-icons"; +import { faBrain, faBullseye } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { PropsWithChildren } from "react"; + +import cn from "@/utils/core/cn"; + +import { FE_COLORS } from "../theme"; + +/** + * FutureEval-specific methodology content without the main title. + * Uses monospace fonts and FutureEval theme colors. + */ +const FutureEvalMethodologyContent: React.FC = () => { + const t = useTranslations(); + + const CARDS = [ + { + icon: faCircleDot, + title: t("aibIdeaCard1Title"), + content: ( + <> +

{t("aibIdeaCard1P1")}

+

+ {t.rich("aibIdeaCard1P2", { + link: (chunks) => ( + + {chunks} + + ), + })} +

+ + ), + }, + { + icon: faBullseye, + title: t("aibIdeaCard2Title"), + content: ( + <> +

{t("aibIdeaCard2P1")}

+

+ {t.rich("aibIdeaCard2P2", { + link: (chunks) => ( + + {chunks} + + ), + })} +

+ + ), + }, + { + icon: faBrain, + title: t("aibIdeaCard3Title"), + content: ( + <> +

{t("aibIdeaCard3P1")}

+

+ {t.rich("aibIdeaCard2P2", { + link: (chunks) => ( + + {chunks} + + ), + })} +

+ + ), + }, + ] as const; + + return ( +
+ {/* Description paragraphs - use font-geist-mono like benchmark page */} +
+

+ {t("aibIdeaDesktopP1")} +

+

+ {t("aibIdeaDesktopP2")} +

+
+ +
+ {CARDS.map((card) => ( + + {card.content} + + ))} +
+
+ ); +}; + +/** + * FutureEval-specific idea card with monospace font for titles + */ +type IdeaCardProps = PropsWithChildren<{ + icon: IconDefinition; + title?: string; +}>; + +const FutureEvalIdeaCard: React.FC = ({ + icon, + title, + children, +}) => { + return ( +
+ + {/* Title uses font-geist-mono like benchmark headers */} +

+ {title} +

+ {/* Content uses font-geist-mono for consistency */} +
+ {children} +
+
+ ); +}; + +export default FutureEvalMethodologyContent; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-methodology-tab.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-methodology-tab.tsx index 91353ab179..be0641f4e9 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-methodology-tab.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-methodology-tab.tsx @@ -1,18 +1,12 @@ -import AIBInfoIdeaDescription from "../../aib/components/aib/tabs/info/aib-info-idea-description"; -import AIBInfoTournaments from "../../aib/components/aib/tabs/info/aib-info-tournaments"; +import FutureEvalMethodologyContent from "./futureeval-methodology-content"; +import FutureEvalTournaments from "./futureeval-tournaments"; const FutureEvalMethodologyTab: React.FC = () => { return ( - <> -
- - -
-
- - -
- +
+ + +
); }; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-navbar.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-navbar.tsx new file mode 100644 index 0000000000..4ad96a9380 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-navbar.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { faArrowLeft } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Link from "next/link"; +import React, { useEffect, useRef, useState } from "react"; + +import ThemeToggle from "@/components/theme_toggle"; +import Button from "@/components/ui/button"; +import cn from "@/utils/core/cn"; + +import { FE_COLORS } from "../theme"; + +const FutureEvalNavbar: React.FC = () => { + const sentinelRef = useRef(null); + const [scrolled, setScrolled] = useState(false); + + useEffect(() => { + const el = sentinelRef.current; + if (!el) return; + + const obs = new IntersectionObserver( + ([entry]) => setScrolled(!entry?.isIntersecting), + { + root: null, + threshold: 0, + rootMargin: "0px 0px 0px 0px", + } + ); + + obs.observe(el); + return () => obs.disconnect(); + }, []); + + return ( + <> + {/* Sentinel element to detect scroll */} +
+ +
+ {/* Metaculus Logo - links to FutureEval home */} + + {/* Metaculus logo SVG */} + + + + + + {/* Right side: Platform button + Dark mode toggle */} +
+ + + +
+
+ + ); +}; + +export default FutureEvalNavbar; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-participate-tab.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-participate-tab.tsx index 94e19e3a16..fe0c3217d2 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-participate-tab.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-participate-tab.tsx @@ -1,13 +1,260 @@ -import AIBInfoResources from "../../aib/components/aib/tabs/info/aib-info-resources"; -import AIBInfoSubmitSteps from "../../aib/components/aib/tabs/info/aib-info-submit-steps"; +"use client"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { + faBook, + faBookOpen, + faTrophy, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; + +import videoThumbnail from "@/app/(main)/aib/assets/video-thumbnail.png"; +import { useAuth } from "@/contexts/auth_context"; +import { useModal } from "@/contexts/modal_context"; +import cn from "@/utils/core/cn"; + +import { FE_COLORS } from "../theme"; + +/** + * FutureEval Participate Tab + * Order: Video section, Submit Your Bot in 3 steps, Resources + */ const FutureEvalParticipateTab: React.FC = () => { return ( <> - - + + ); }; +/** + * Submit steps section with video + */ +const FutureEvalSubmitSteps: React.FC = () => { + const t = useTranslations(); + const { setCurrentModal } = useModal(); + const { user } = useAuth(); + const router = useRouter(); + + const submitSteps = [ + t.rich("aibSubmitStep1", { + here: (chunks) => ( + + ), + }), + t.rich("aibSubmitStep2", { + instructions: (chunks) => ( + + {chunks} + + ), + }), + t("aibSubmitStep3"), + ] as const; + + return ( +
+ {/* Video section */} +
+

+ {t("aibSubmitLearnLine")} +

+ + + {t("aibSubmitVideoAlt")} + +
+ + {/* Steps section */} +
+

+ {t("aibSubmitHeading")} +

+
+ {submitSteps.map((step, index) => ( + + ))} +
+
+
+ ); +}; + +const FutureEvalSubmitStep: React.FC<{ + index: number; + content: React.ReactNode; +}> = ({ index, content }) => { + return ( +
+
+ {index} +
+

+ {content} +

+
+ ); +}; + +/** + * Resources section with monospace font + */ +const FutureEvalResources: React.FC = () => { + const t = useTranslations(); + + const RESOURCES_DATA = [ + { + icon: faBook, + title: t("aibResourcesFullInfoTitle"), + description: t("aibResourcesFullInfoDesc"), + href: "/notebooks/38928/futureeval-resources-page/", + }, + { + icon: faBookOpen, + title: t("aibResourcesHighlightsTitle"), + description: t("aibResourcesHighlightsDesc"), + href: "/notebooks/38928/futureeval-resources-page/#research-reports-and-overview-of-the-field", + }, + { + icon: faTrophy, + title: t("aibResourcesLeaderboardsTitle"), + description: t("aibResourcesLeaderboardsDesc"), + href: "/futureeval/leaderboard", + }, + ] as const; + + return ( +
+

+ {t("aibResourcesHeading")} +

+
+ {RESOURCES_DATA.map((resource, index) => ( + + ))} +
+
+ ); +}; + +type ResourceCardProps = { + icon: IconDefinition; + title: string; + description: string; + href: string; +}; + +const FutureEvalResourceCard: React.FC = ({ + icon, + title, + description, + href, +}) => { + return ( + +
+ + {/* Title uses font-geist-mono */} +

+ {title} +

+ {/* Description uses font-geist-mono */} +

+ {description} +

+
+ + ); +}; + export default FutureEvalParticipateTab; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx index 536c5caacc..406886bf04 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-tabs-shell.tsx @@ -4,6 +4,8 @@ import React, { useState } from "react"; import { TabItem } from "./futureeval-header"; import FutureEvalHeroBanner from "./futureeval-hero-banner"; +import FutureEvalNavbar from "./futureeval-navbar"; +import { FE_COLORS } from "../theme"; export type Section = { value: "benchmark" | "methodology" | "participate" | "news"; @@ -31,6 +33,9 @@ const FutureEvalTabsShell: React.FC = ({ current, sections }) => { return (
+ {/* Custom FutureEval navbar */} + + {/* Hero banner with violet background - edge to edge */} = ({ current, sections }) => { {/* Tab content */} {activeSection && ( -
+
{activeSection.content}
diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx index 1d3f25d63b..7db11a5820 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx +++ b/front_end/src/app/(main)/futureeval/components/futureeval-tabs.tsx @@ -27,18 +27,18 @@ const FutureEvalTabs: React.FC = async ({ current }) => { label: t("aibTabsMethodology"), content: , }, - { - value: "participate", - href: "/futureeval/participate", - label: t("aibTabsParticipate"), - content: , - }, { value: "news", href: "/futureeval/news", label: t("aibTabsNews"), content: , }, + { + value: "participate", + href: "/futureeval/participate", + label: t("aibTabsParticipate"), + content: , + }, ]; return ; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-tournaments.tsx b/front_end/src/app/(main)/futureeval/components/futureeval-tournaments.tsx new file mode 100644 index 0000000000..309a84c4a4 --- /dev/null +++ b/front_end/src/app/(main)/futureeval/components/futureeval-tournaments.tsx @@ -0,0 +1,91 @@ +"use client"; + +import Link from "next/link"; +import { useTranslations } from "next-intl"; + +import AIBInfoTournamentCard from "@/app/(main)/aib/components/aib/tabs/info/aib-info-tournament-card"; +import ReusableGradientCarousel from "@/components/gradient-carousel"; +import cn from "@/utils/core/cn"; + +import { FE_COLORS } from "../theme"; + +/** + * FutureEval-specific tournaments section with consistent theming. + * Uses the violet gradient for the carousel and matching text colors. + */ +const FutureEvalTournaments: React.FC = () => { + const t = useTranslations(); + + const CARDS_DATA = [ + { + title: "Fall 2025", + href: "/aib/2025/fall", + imgUrl: "https://cdn.metaculus.com/aib-q3.webp", + prize: "$58,000", + isLive: true, + }, + { + title: "Q2 2025", + href: "/aib/2025/q2", + imgUrl: "https://cdn.metaculus.com/aib-q2.webp", + prize: "$30,000", + }, + { + title: "Q1 2025", + href: "/aib/2025/q1", + imgUrl: "https://cdn.metaculus.com/2025-q1.webp", + prize: "$30,000", + }, + { + title: "Q4 2024", + href: "/aib/2024/q4", + imgUrl: "https://cdn.metaculus.com/hires-q4.webp", + prize: "$30,000", + }, + { + title: "Q3 2024", + href: "/aib/2024/q3", + imgUrl: "https://cdn.metaculus.com/hires-bw.webp", + prize: "$30,000", + }, + ]; + + return ( +
+

+ {t("aibTournamentsHeading")} +

+ + + items={CARDS_DATA} + renderItem={(card) => } + listClassName="-ml-2" + gradientFromClass={FE_COLORS.gradientFrom} + /> + +
+

+ {t.rich("aibMiniBenchBanner", { + link: (chunks) => ( + + {chunks} + + ), + })} +

+
+
+ ); +}; + +export default FutureEvalTournaments; diff --git a/front_end/src/app/(main)/futureeval/leaderboard/page.tsx b/front_end/src/app/(main)/futureeval/leaderboard/page.tsx index 505e09ff0c..795b91e692 100644 --- a/front_end/src/app/(main)/futureeval/leaderboard/page.tsx +++ b/front_end/src/app/(main)/futureeval/leaderboard/page.tsx @@ -5,6 +5,7 @@ import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server" import FutureEvalContainer from "../components/futureeval-container"; import FutureEvalLeaderboardHero from "../components/futureeval-leaderboard-hero"; import FutureEvalLeaderboardTable from "../components/futureeval-leaderboard-table"; +import FutureEvalNavbar from "../components/futureeval-navbar"; export const metadata = { title: "Top Model Leaderboards | Metaculus", @@ -20,18 +21,21 @@ export default async function FutureEvalLeaderboardsPage() { ); return ( - - +
+ + + - {data?.entries?.length ? ( - }> - - - ) : ( -
- Leaderboard data not currently available, please check back soon! -
- )} -
+ {data?.entries?.length ? ( + }> + + + ) : ( +
+ Leaderboard data not currently available, please check back soon! +
+ )} + +
); } diff --git a/front_end/src/app/(main)/futureeval/theme.ts b/front_end/src/app/(main)/futureeval/theme.ts new file mode 100644 index 0000000000..4936f735ed --- /dev/null +++ b/front_end/src/app/(main)/futureeval/theme.ts @@ -0,0 +1,94 @@ +/** + * FutureEval Theme Configuration + * + * This file defines the main theme colors for the FutureEval project. + * Adjust these values to easily change the color scheme across all FutureEval pages. + * + * IMPORTANT: All FutureEval components import colors from this file. + * Changing values here will update the entire FutureEval project. + * + * Usage: Import and use these class strings in your components. + * Example: import { FE_COLORS } from '../theme'; + *
...
+ */ + +export const FE_COLORS = { + // =========================================== + // PRIMARY BACKGROUND COLORS + // =========================================== + // Main page background (used for hero, content sections, containers) + bgPrimary: "bg-violet-100 dark:bg-violet-950", + + // =========================================== + // TEXT COLORS + // =========================================== + textPrimary: "text-violet-900 dark:text-violet-100", + textSecondary: "text-violet-700 dark:text-violet-300", + textMuted: "text-violet-600 dark:text-violet-400", + + // Heading colors (used for section titles) + textHeading: "text-blue-800 dark:text-blue-800-dark", + textSubheading: "text-blue-700 dark:text-blue-700-dark", + + // =========================================== + // TOOLTIP/POPOVER COLORS + // =========================================== + tooltipBg: "bg-violet-300 dark:bg-violet-800", + tooltipText: "text-violet-900 dark:text-violet-100", + tooltipTextSecondary: "text-violet-800 dark:text-violet-200", + tooltipLink: "text-violet-950 dark:text-violet-50", + + // =========================================== + // BUTTON/INTERACTIVE COLORS + // =========================================== + buttonBorder: "border-violet-500 dark:border-violet-500", + + // =========================================== + // CAROUSEL/GRADIENT COLORS + // =========================================== + // Gradient fade for carousels (should match bgPrimary) + gradientFrom: "from-violet-100 dark:from-violet-950", + + // Carousel arrow backgrounds (should match bgPrimary) + carouselArrowBg: "bg-violet-100 dark:bg-violet-950", + + // =========================================== + // NAVBAR COLORS + // =========================================== + navbarScrolled: "bg-blue-900/90 dark:bg-blue-950/90", + navbarTransparent: "bg-blue-900", + + // =========================================== + // BAR CHART COLORS (Model Leaderboard) + // =========================================== + // Aggregate bars (Community Prediction) + barAggregateBg: "bg-violet-200 dark:bg-violet-800", + barAggregateHover: "hover:bg-violet-300 dark:hover:bg-violet-700-dark", + barAggregateBorder: "border-violet-800 dark:border-violet-800-dark", + barAggregateIcon: "text-violet-800 dark:text-violet-200", + + // =========================================== + // SECONDARY BACKGROUNDS + // =========================================== + // For cards, banners with subtle contrast + bgSecondary: "bg-violet-200/50 dark:bg-violet-900/30", + bgCard: "bg-violet-50 dark:bg-violet-900/30", + cardBorder: "border-violet-400 dark:border-violet-700", + + // Step numbers in Participate tab + stepNumberBg: "bg-violet-500 dark:bg-violet-600", +} as const; + +// Raw color values for components that need inline styles +export const FE_RAW_COLORS = { + light: { + bgPrimary: "rgb(237, 233, 254)", // violet-100 + tooltipBg: "rgb(196, 181, 253)", // violet-300 + }, + dark: { + bgPrimary: "rgb(46, 16, 101)", // violet-950 + tooltipBg: "rgb(91, 33, 182)", // violet-800 + }, +} as const; + +export default FE_COLORS; diff --git a/front_end/src/components/gradient-carousel.tsx b/front_end/src/components/gradient-carousel.tsx index f6ad62f04e..1d482c8156 100644 --- a/front_end/src/components/gradient-carousel.tsx +++ b/front_end/src/components/gradient-carousel.tsx @@ -21,6 +21,8 @@ type Props = { gradientFromClass?: string; showArrows?: Resolver; arrowClassName?: string; + arrowLeftPosition?: string; + arrowRightPosition?: string; prevLabel?: string; nextLabel?: string; className?: string; @@ -42,6 +44,8 @@ function ReusableGradientCarousel({ gradientFromClass = "from-blue-200 dark:from-blue-200-dark", showArrows = true, arrowClassName = "w-10 h-10 md:w-[44px] md:h-[44px] text-blue-700 dark:text-blue-700-dark bg-gray-0 dark:bg-gray-0-dark mt-3 md:text-gray-200 md:dark:text-gray-200-dark rounded-full md:bg-blue-900 md:dark:bg-blue-900-dark", + arrowLeftPosition = "left-[18px]", + arrowRightPosition = "right-[18px]", prevLabel = "Previous", nextLabel = "Next", className, @@ -238,7 +242,7 @@ function ReusableGradientCarousel({ "touch-pan-x snap-x snap-mandatory", "[-webkit-overflow-scrolling:touch]", dragScroll && (isGrabbing ? "cursor-grabbing" : "cursor-grab"), - dragScroll && "select-none", + dragScroll && "select-none [-webkit-user-select:none]", viewportClassName )} > @@ -294,7 +298,8 @@ function ReusableGradientCarousel({ disabled={!canPrev && !loop} tabIndex={canPrev || loop ? 0 : -1} className={cn( - "absolute left-[18px] top-1/2 -translate-y-1/2", + "absolute top-1/2 -translate-y-1/2", + arrowLeftPosition, arrowClassName, fadeCls, arrowsActive && canPrev @@ -313,7 +318,8 @@ function ReusableGradientCarousel({ disabled={!canNext && !loop} tabIndex={canNext || loop ? 0 : -1} className={cn( - "absolute right-[18px] top-1/2 -translate-y-1/2", + "absolute top-1/2 -translate-y-1/2", + arrowRightPosition, arrowClassName, fadeCls, arrowsActive && canNext diff --git a/front_end/src/utils/navigation.ts b/front_end/src/utils/navigation.ts index 4bd49fc77d..23ce59dbaa 100644 --- a/front_end/src/utils/navigation.ts +++ b/front_end/src/utils/navigation.ts @@ -156,7 +156,8 @@ export const getWithDefaultHeader = (pathname: string): boolean => !pathname.match(/^\/questions\/(\d+)(\/.*)?$/) && !pathname.match(/^\/notebooks\/(\d+)(\/.*)?$/) && !pathname.startsWith("/c/") && - !pathname.startsWith("/questions/create"); + !pathname.startsWith("/questions/create") && + !pathname.startsWith("/futureeval"); /** * Ensures trailing slash is handled properly, e.g. when link is defined manually in code From 2fcdf7ad0100c4f2cf3df75ab06373c4fcec594a Mon Sep 17 00:00:00 2001 From: aseckin Date: Thu, 15 Jan 2026 13:12:37 +0100 Subject: [PATCH 08/27] Applied new FutureEval branding --- .../app/(futureeval)/futureeval/CONTEXT.md | 130 +++++++++++++ .../futureeval/assets/FE-logo-dark.svg | 27 +++ .../futureeval/assets/FE-logo-light.svg | 27 +++ .../futureeval-benchmark-headers.tsx | 10 +- .../benchmark/futureeval-benchmark-tab.tsx | 0 .../benchmark/futureeval-model-bar.tsx | 0 .../benchmark/futureeval-model-benchmark.tsx | 44 +++-- .../components/futureeval-container.tsx | 2 +- .../components/futureeval-header.tsx | 32 +-- .../components/futureeval-hero-banner.tsx | 101 ++++++++++ .../components/futureeval-info-popover.tsx | 2 +- .../futureeval-leaderboard-hero.tsx | 17 +- .../futureeval-leaderboard-table.tsx | 40 ++-- .../futureeval-methodology-content.tsx | 85 ++++---- .../components/futureeval-methodology-tab.tsx | 0 .../components/futureeval-navbar.tsx | 40 ++++ .../components/futureeval-participate-tab.tsx | 53 +++-- .../components/futureeval-screen.tsx | 2 +- .../components/futureeval-tabs-shell.tsx | 4 +- .../futureeval/components/futureeval-tabs.tsx | 3 +- .../components/futureeval-tournaments.tsx | 14 +- .../futureeval/info/page.tsx | 0 .../app/(futureeval)/futureeval/layout.tsx | 34 ++++ .../futureeval/leaderboard/page.tsx | 4 +- .../futureeval/methodology/page.tsx | 0 .../futureeval/news/page.tsx | 0 .../futureeval/page.tsx | 0 .../futureeval/participate/page.tsx | 0 .../src/app/(futureeval)/futureeval/theme.ts | 182 ++++++++++++++++++ .../(main)/futureeval/assets/FE-logo-dark.svg | 25 --- .../futureeval/assets/FE-logo-light.svg | 25 --- .../components/futureeval-hero-banner.tsx | 71 ------- .../components/futureeval-navbar.tsx | 89 --------- front_end/src/app/(main)/futureeval/theme.ts | 94 --------- front_end/src/constants/colors.ts | 6 + front_end/src/utils/fonts.ts | 12 +- front_end/tailwind.config.ts | 1 + 37 files changed, 726 insertions(+), 450 deletions(-) create mode 100644 front_end/src/app/(futureeval)/futureeval/CONTEXT.md create mode 100644 front_end/src/app/(futureeval)/futureeval/assets/FE-logo-dark.svg create mode 100644 front_end/src/app/(futureeval)/futureeval/assets/FE-logo-light.svg rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/benchmark/futureeval-benchmark-headers.tsx (85%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/benchmark/futureeval-benchmark-tab.tsx (100%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/benchmark/futureeval-model-bar.tsx (100%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/benchmark/futureeval-model-benchmark.tsx (78%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-container.tsx (86%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-header.tsx (64%) create mode 100644 front_end/src/app/(futureeval)/futureeval/components/futureeval-hero-banner.tsx rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-info-popover.tsx (98%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-leaderboard-hero.tsx (73%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-leaderboard-table.tsx (79%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-methodology-content.tsx (57%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-methodology-tab.tsx (100%) create mode 100644 front_end/src/app/(futureeval)/futureeval/components/futureeval-navbar.tsx rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-participate-tab.tsx (82%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-screen.tsx (83%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-tabs-shell.tsx (93%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-tabs.tsx (93%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/components/futureeval-tournaments.tsx (84%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/info/page.tsx (100%) create mode 100644 front_end/src/app/(futureeval)/futureeval/layout.tsx rename front_end/src/app/{(main) => (futureeval)}/futureeval/leaderboard/page.tsx (83%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/methodology/page.tsx (100%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/news/page.tsx (100%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/page.tsx (100%) rename front_end/src/app/{(main) => (futureeval)}/futureeval/participate/page.tsx (100%) create mode 100644 front_end/src/app/(futureeval)/futureeval/theme.ts delete mode 100644 front_end/src/app/(main)/futureeval/assets/FE-logo-dark.svg delete mode 100644 front_end/src/app/(main)/futureeval/assets/FE-logo-light.svg delete mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-hero-banner.tsx delete mode 100644 front_end/src/app/(main)/futureeval/components/futureeval-navbar.tsx delete mode 100644 front_end/src/app/(main)/futureeval/theme.ts diff --git a/front_end/src/app/(futureeval)/futureeval/CONTEXT.md b/front_end/src/app/(futureeval)/futureeval/CONTEXT.md new file mode 100644 index 0000000000..3760c0c044 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/CONTEXT.md @@ -0,0 +1,130 @@ +# FutureEval Subproject Improvements - Context + +## Project Overview + +We've been improving the `/futureeval` subproject of Metaculus - a page for measuring AI forecasting performance. The goal was to enhance communication, design consistency, and code quality. + +## Key Changes Made + +### 1. Custom Navbar + +- Created `futureeval-navbar.tsx` with Metaculus logo (not FutureEval logo) +- "Metaculus Platform" button with left arrow (responsive: "Platform" on mobile) +- Transparent background that transitions to semi-transparent on scroll (using IntersectionObserver) +- Added ThemeToggle (dark mode) to the right of Platform button +- Bulletins are hidden on FutureEval pages (`bulletins.tsx` updated) + +### 2. Hero Section + +- Text: "Measuring the forecasting accuracy of AI" +- Two bullet points describing Model Benchmark and Bot Competition +- Uses `FE_COLORS.textHeading` and `FE_COLORS.textSubheading` for consistent styling + +### 3. Model Leaderboard + +- Left-aligned title and subtitle +- Info popover (?) button on the right with FutureEval-themed tooltip +- Carousel with smaller arrows on mobile (`w-7 h-7 sm:w-10 sm:h-10`) +- Arrow positions: `left-1 sm:left-[18px]` and `right-1 sm:right-[18px]` + +### 4. Centralized Theme (`theme.ts`) + +```typescript +FE_COLORS = { + bgPrimary, + textPrimary, + textSecondary, + textMuted, + textHeading, + textSubheading, + tooltipBg, + tooltipText, + tooltipTextSecondary, + tooltipLink, + buttonBorder, + gradientFrom, + carouselArrowBg, + navbarScrolled, + navbarTransparent, + barAggregateBg, + barAggregateHover, + barAggregateBorder, + barAggregateIcon, + bgSecondary, + bgCard, + cardBorder, + stepNumberBg, +}; +``` + +### 5. Text Styling Paradigms (Applied Across All Pages) + +- **Headings**: `text-[24px] font-bold leading-[116%] sm:text-[32px] sm:leading-[40px] lg:text-4xl` + `FE_COLORS.textHeading` +- **Subtitles/Body**: `font-geist-mono text-sm sm:text-base` + `FE_COLORS.textSubheading` +- **Links**: Simple `underline` class +- **Text selection disabled** via `select-none` on containers + +### 6. Pages Updated + +- `futureeval-hero-banner.tsx` - Hero content +- `futureeval-methodology-content.tsx` - Uses font-geist-mono for all text +- `futureeval-participate-tab.tsx` - Video section, 3-step submit, resources cards +- `futureeval-tournaments.tsx` - Tournament carousel with FE theme +- `futureeval-leaderboard-hero.tsx` - Full leaderboard page header +- `futureeval-benchmark-headers.tsx` - Section headers for Forecasting Performance and Pros vs Bots + +### 7. Deleted Unused Files + +- `futureeval-performance-chart.tsx` (reverted to using original AIB chart) +- `futureeval-forecasting-performance.tsx` +- `futureeval-benchmark-hero.tsx` +- `futureeval-hero.tsx` +- `futureeval-bulletin.tsx` + +## Current File Structure + +``` +futureeval/ +├── components/ +│ ├── benchmark/ +│ │ ├── futureeval-benchmark-headers.tsx +│ │ ├── futureeval-benchmark-tab.tsx +│ │ ├── futureeval-model-bar.tsx +│ │ └── futureeval-model-benchmark.tsx +│ ├── futureeval-container.tsx +│ ├── futureeval-header.tsx +│ ├── futureeval-hero-banner.tsx +│ ├── futureeval-info-popover.tsx +│ ├── futureeval-leaderboard-hero.tsx +│ ├── futureeval-leaderboard-table.tsx +│ ├── futureeval-methodology-content.tsx +│ ├── futureeval-methodology-tab.tsx +│ ├── futureeval-navbar.tsx +│ ├── futureeval-participate-tab.tsx +│ ├── futureeval-screen.tsx +│ ├── futureeval-tabs-shell.tsx +│ ├── futureeval-tabs.tsx +│ └── futureeval-tournaments.tsx +├── theme.ts +├── page.tsx +├── methodology/ +├── news/ +├── participate/ +├── leaderboard/ +├── info/ +└── assets/ +``` + +## Import Order Convention + +The codebase uses ESLint import ordering: + +1. External packages (alphabetical) +2. `@/` alias imports (alphabetical) +3. Relative imports (alphabetical) + +## Notes + +- The benchmark tab uses `AIBBenchmarkForecastingPerformance` from the original AIB components (not a custom version) +- The tabs order is: Benchmark, Methodology, News, Participate +- FutureEval pages don't show the default Metaculus header (handled in `navigation.ts`) diff --git a/front_end/src/app/(futureeval)/futureeval/assets/FE-logo-dark.svg b/front_end/src/app/(futureeval)/futureeval/assets/FE-logo-dark.svg new file mode 100644 index 0000000000..829e065cf8 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/assets/FE-logo-dark.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front_end/src/app/(futureeval)/futureeval/assets/FE-logo-light.svg b/front_end/src/app/(futureeval)/futureeval/assets/FE-logo-light.svg new file mode 100644 index 0000000000..7965720bf8 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/assets/FE-logo-light.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx similarity index 85% rename from front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx rename to front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx index 91df98f5f9..b3bd57011f 100644 --- a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx +++ b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx @@ -6,7 +6,7 @@ import { PropsWithChildren } from "react"; import cn from "@/utils/core/cn"; -import { FE_COLORS } from "../../theme"; +import { FE_COLORS, FE_TYPOGRAPHY } from "../../theme"; type Props = PropsWithChildren<{ title?: React.ReactNode; @@ -26,7 +26,8 @@ const FutureEvalSubsectionHeader: React.FC = ({

@@ -36,7 +37,8 @@ const FutureEvalSubsectionHeader: React.FC = ({

@@ -73,7 +75,7 @@ export const FutureEvalProsVsBotsSectionHeader: React.FC = () => { subtitle={t.rich("aibProsVsBotsSubtitle", { link: (chunks) => ( {chunks} diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx similarity index 100% rename from front_end/src/app/(main)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx rename to front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-bar.tsx similarity index 100% rename from front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-bar.tsx rename to front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-bar.tsx diff --git a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-benchmark.tsx similarity index 78% rename from front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx rename to front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-benchmark.tsx index b6c5b38677..ae1c814fce 100644 --- a/front_end/src/app/(main)/futureeval/components/benchmark/futureeval-model-benchmark.tsx +++ b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-benchmark.tsx @@ -4,17 +4,17 @@ import Link from "next/link"; import { useTranslations } from "next-intl"; import React, { useMemo } from "react"; -import ReusableGradientCarousel from "@/components/gradient-carousel"; - -import { useAIBLeaderboard } from "../../../aib/components/aib/leaderboard/aib-leaderboard-provider"; +import { useAIBLeaderboard } from "@/app/(main)/aib/components/aib/leaderboard/aib-leaderboard-provider"; import { aggregateKind, entryIconPair, entryLabel, isAggregate, shouldDisplayEntry, -} from "../../../aib/components/aib/leaderboard/utils"; -import { FE_COLORS } from "../../theme"; +} from "@/app/(main)/aib/components/aib/leaderboard/utils"; +import ReusableGradientCarousel from "@/components/gradient-carousel"; + +import { FE_COLORS, FE_TYPOGRAPHY } from "../../theme"; import FutureEvalInfoPopover from "../futureeval-info-popover"; import FutureEvalModelBar from "./futureeval-model-bar"; @@ -96,21 +96,32 @@ const FutureEvalModelBenchmark: React.FC = () => { {/* Header */}

-

- {t("aibBenchModelsTitle")} -

+ {/* Title with info popover inline on desktop */} +
+

+ {t("aibBenchModelsTitle")} +

+ {/* Info popover - inline on desktop (sm+) */} +
+ +
+

- {t("aibBenchModelsBlurb")}{" "} - - {t("aibViewFullLeaderboard")} - + {t("aibBenchModelsBlurb")}

+ + {t("aibViewFullLeaderboard")} + +
+ {/* Info popover - right aligned on mobile only */} +
+
-
{/* Horizontal bar chart carousel */} @@ -134,7 +145,6 @@ const FutureEvalModelBenchmark: React.FC = () => { itemClassName="w-[40px] sm:w-[64px] h-full" gapClassName="gap-1 sm:gap-2" gradientFromClass={FE_COLORS.gradientFrom} - arrowClassName={`w-7 h-7 sm:w-10 sm:h-10 ${FE_COLORS.textSubheading} ${FE_COLORS.carouselArrowBg} rounded-full shadow-md ${FE_COLORS.cardBorder}`} arrowLeftPosition="left-1 sm:left-[18px]" arrowRightPosition="right-1 sm:right-[18px]" slideBy={{ mode: "items", count: 3 }} diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-container.tsx similarity index 86% rename from front_end/src/app/(main)/futureeval/components/futureeval-container.tsx rename to front_end/src/app/(futureeval)/futureeval/components/futureeval-container.tsx index f394695b41..dcc106118b 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-container.tsx +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-container.tsx @@ -11,7 +11,7 @@ const FutureEvalContainer: React.FC = ({ className, children }) => {
diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-header.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-header.tsx similarity index 64% rename from front_end/src/app/(main)/futureeval/components/futureeval-header.tsx rename to front_end/src/app/(futureeval)/futureeval/components/futureeval-header.tsx index d168bb8826..80d40209f2 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-header.tsx +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-header.tsx @@ -8,6 +8,7 @@ import cn from "@/utils/core/cn"; import FELogoDark from "../assets/FE-logo-dark.svg?url"; import FELogoLight from "../assets/FE-logo-light.svg?url"; +import { FE_COLORS, FE_LOGO_SIZES } from "../theme"; export type TabItem = { value: string; @@ -26,24 +27,33 @@ const FutureEvalHeader: React.FC = ({ activeTab, onTabChange, }) => { + // Logo sizes are controlled by FE_LOGO_SCALE in theme.ts + const logoStyle = { + "--logo-mobile": `${FE_LOGO_SIZES.mobile}px`, + "--logo-desktop": `${FE_LOGO_SIZES.desktop}px`, + } as React.CSSProperties; + return (
- {/* Logo */} -
+ {/* Logo - sizes controlled by FE_LOGO_SCALE in theme.ts */} +
FutureEval FutureEval
@@ -81,10 +91,10 @@ const FutureEvalTabLink: React.FC = ({ href={tab.href} onClick={onClick} className={cn( - "pb-1 font-geist-mono text-sm font-medium transition-colors sm:text-base", + "pb-1 font-sans text-xs font-medium transition-colors sm:text-sm", isActive - ? "text-violet-800 dark:text-violet-800-dark" - : "text-gray-600 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200" + ? FE_COLORS.textAccent + : `${FE_COLORS.textMuted} hover:${FE_COLORS.textSecondary}` )} style={{ textDecoration: "none", diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-hero-banner.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-hero-banner.tsx new file mode 100644 index 0000000000..cbcc7fc456 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-hero-banner.tsx @@ -0,0 +1,101 @@ +"use client"; + +import Link from "next/link"; +import React from "react"; + +import cn from "@/utils/core/cn"; + +import FutureEvalHeader, { TabItem } from "./futureeval-header"; +import { FE_COLORS, FE_TYPOGRAPHY } from "../theme"; + +type Props = { + tabs: TabItem[]; + activeTab: string; + onTabChange: (value: string) => void; +}; + +const FutureEvalHeroBanner: React.FC = ({ + tabs, + activeTab, + onTabChange, +}) => { + const showHero = activeTab === "benchmark"; + + return ( +
+
+ {/* Header with logo and tabs */} + + + {/* Hero content - only on Benchmark tab */} + {showHero && ( +
+ {/* Text content */} +
+

+ Measuring the forecasting accuracy of AI +

+

+ FutureEval measures AI's ability to predict future + outcomes, which is essential in many real-world tasks. Models + that score high in our benchmark will be better at planning, + risk assessment, and decision-making. +

+ + Learn more + +
+ + {/* Hero placeholder - circular on desktop, rectangular on mobile */} +
+
+ + Hero Visualization + +
+
+
+ )} +
+
+ ); +}; + +export default FutureEvalHeroBanner; diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-info-popover.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-info-popover.tsx similarity index 98% rename from front_end/src/app/(main)/futureeval/components/futureeval-info-popover.tsx rename to front_end/src/app/(futureeval)/futureeval/components/futureeval-info-popover.tsx index 3244a58e50..bcd676ee3d 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-info-popover.tsx +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-info-popover.tsx @@ -70,7 +70,7 @@ const FutureEvalInfoPopover: React.FC = ({ defaultOpen = false }) => { aria-label="Learn more about the Model Leaderboard" aria-pressed={open} className={cn( - "h-9 min-h-9 w-9 min-w-9 flex-shrink-0 text-lg", + "mb-0 h-7 min-h-7 w-7 min-w-7 flex-shrink-0 text-base sm:mb-2", FE_COLORS.buttonBorder )} {...getReferenceProps()} diff --git a/front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-leaderboard-hero.tsx similarity index 73% rename from front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx rename to front_end/src/app/(futureeval)/futureeval/components/futureeval-leaderboard-hero.tsx index 585cee13a6..bfcb291567 100644 --- a/front_end/src/app/(main)/futureeval/components/futureeval-leaderboard-hero.tsx +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-leaderboard-hero.tsx @@ -7,20 +7,25 @@ import { useTranslations } from "next-intl"; import Button from "@/components/ui/button"; import cn from "@/utils/core/cn"; -import { FE_COLORS } from "../theme"; +import { FE_COLORS, FE_TYPOGRAPHY } from "../theme"; const FutureEvalLeaderboardHero: React.FC = () => { const t = useTranslations(); return (
- {/* Back button - subtle tertiary style */} + {/* Back button - FutureEval branded */}