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/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx new file mode 100644 index 0000000000..1ca0d5c8ef --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-headers.tsx @@ -0,0 +1,88 @@ +"use client"; + +import Link from "next/link"; +import { PropsWithChildren } from "react"; + +import cn from "@/utils/core/cn"; + +import { FE_COLORS, FE_TYPOGRAPHY } 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 != null && ( +

+ {title} +

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

+ {subtitle} +

+ )} + {children} + + ); +}; + +/** + * Forecasting Performance Over Time header (left-aligned) + */ +export const FutureEvalForecastingPerformanceHeader: React.FC = () => { + return ( + + ); +}; + +/** + * Pros vs Bots header with left alignment + */ +export const FutureEvalProsVsBotsSectionHeader: React.FC = () => { + return ( + + Metaculus Pro Forecasters have beaten Bots every quarter of our{" "} + + AI Benchmarking Tournaments + {" "} + so far. + + } + /> + ); +}; + +export default FutureEvalSubsectionHeader; diff --git a/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx new file mode 100644 index 0000000000..f46efce5ad --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-benchmark-tab.tsx @@ -0,0 +1,32 @@ +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 = () => { + return ( + <> +
+ +
+ + {/* Forecasting Performance Over Time */} +
+ + +
+ + {/* Pros vs Bots */} +
+ + +
+ + ); +}; + +export default FutureEvalBenchmarkTab; diff --git a/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-bar.tsx b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-bar.tsx new file mode 100644 index 0000000000..98b7db4d66 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-bar.tsx @@ -0,0 +1,161 @@ +"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 { 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: { + id: string; + name: string; + score: number; + contributionCount: number; + iconLight?: StaticImageData | string; + iconDark?: StaticImageData | string; + isAggregate?: boolean; + }; +}; + +const FutureEvalModelBar: React.FC = ({ heightPct, model }) => { + 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 }); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") { + e.preventDefault(); + handleClick(); + } + }; + + 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.isAggregate ? ( + + ) : ( + (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 && ( + +
+
+
+ + Score: + + + {score} + +
+
+ + Forecasts: + + + {model.contributionCount} + +
+
+
+
+ )} + + ); +}; + +export default FutureEvalModelBar; diff --git a/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-benchmark.tsx b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-benchmark.tsx new file mode 100644 index 0000000000..9228ddea90 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/benchmark/futureeval-model-benchmark.tsx @@ -0,0 +1,167 @@ +"use client"; + +import Link from "next/link"; +import React, { useMemo } from "react"; + +import { useAIBLeaderboard } from "@/app/(main)/aib/components/aib/leaderboard/aib-leaderboard-provider"; +import { + aggregateKind, + entryIconPair, + entryLabel, + isAggregate, + shouldDisplayEntry, +} 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"; + +// Mock translation function for entryLabel - returns hardcoded English values +const mockTranslate = ((key: string) => { + const translations: Record = { + communityPrediction: "Community Prediction", + aibLegendPros: "Pro Forecasters", + }; + return translations[key] ?? key; +}) as ReturnType; + +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 { 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, sorted by score (highest first) + const bots = allEntries + .filter((e) => !isAggregate(e) && shouldDisplayEntry(e, 300)) + .sort((a, b) => 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, mockTranslate); + 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]); + + if (items.length === 0) { + return null; + } + + return ( +
+ {/* Header */} +
+
+ {/* Title with info popover inline on desktop */} +
+

+ Model Leaderboard +

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

+ Updated every day based on our standardized forecasting performance + measurement methodology. +

+ + View full leaderboard + +
+ {/* Info popover - right aligned on mobile only */} +
+ +
+
+ + {/* Horizontal bar chart carousel */} +
+ ( + + )} + itemClassName="w-[40px] sm:w-[64px] h-full" + gapClassName="gap-1 sm:gap-2" + gradientFromClass={FE_COLORS.gradientFrom} + arrowLeftPosition="left-1 sm:left-[18px]" + arrowRightPosition="right-1 sm:right-[18px]" + slideBy={{ mode: "items", count: 3 }} + showArrows={true} + wheelToHorizontal={false} + className="h-full" + viewportClassName="h-full overflow-y-hidden" + listClassName="h-full items-stretch -ml-2" + /> +
+
+ ); +}; + +export default FutureEvalModelBenchmark; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-container.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-container.tsx new file mode 100644 index 0000000000..dcc106118b --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-container.tsx @@ -0,0 +1,24 @@ +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 ( +
+
+ {children} +
+
+ ); +}; + +export default FutureEvalContainer; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-header.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-header.tsx new file mode 100644 index 0000000000..0ff020af95 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-header.tsx @@ -0,0 +1,95 @@ +"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"; +import { FE_COLORS, FE_LOGO_SIZES } from "../theme"; + +export type TabItem = { + value: string; + href: string; + label: string; +}; + +type Props = { + tabs: TabItem[]; + activeTab: string; +}; + +const FutureEvalHeader: React.FC = ({ tabs, activeTab }) => { + // 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 - sizes controlled by FE_LOGO_SCALE in theme.ts */} +
+ FutureEval + FutureEval +
+ + {/* Tab navigation */} + +
+ ); +}; + +type TabLinkProps = { + tab: TabItem; + isActive: boolean; +}; + +const FutureEvalTabLink: React.FC = ({ tab, isActive }) => { + return ( + + {tab.label} + + ); +}; + +export default FutureEvalHeader; 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..f62cd6c99b --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-hero-banner.tsx @@ -0,0 +1,82 @@ +"use client"; + +import Link from "next/link"; +import React from "react"; + +import cn from "@/utils/core/cn"; + +import FutureEvalHeader, { TabItem } from "./futureeval-header"; +import FutureEvalOrbit from "./orbit"; +import { FE_COLORS, FE_TYPOGRAPHY } from "../theme"; + +type Props = { + tabs: TabItem[]; + activeTab: string; +}; + +const FutureEvalHeroBanner: React.FC = ({ tabs, activeTab }) => { + const showHero = activeTab === "benchmark"; + + return ( +
+
+ {/* Header with logo and tabs */} + + + {/* Hero content - only on Benchmark tab */} + {showHero && ( +
+ {/* Text content - 50% on tablet+ */} +
+

+ 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 + +
+ + {/* Orbit visualization - 50% on tablet+, centered below on mobile */} +
+ +
+
+ )} +
+
+ ); +}; + +export default FutureEvalHeroBanner; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-info-popover.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-info-popover.tsx new file mode 100644 index 0000000000..bcd676ee3d --- /dev/null +++ b/front_end/src/app/(futureeval)/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/(futureeval)/futureeval/components/futureeval-leaderboard-hero.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-leaderboard-hero.tsx new file mode 100644 index 0000000000..e8b5fda270 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-leaderboard-hero.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { faArrowLeft } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import Button from "@/components/ui/button"; +import cn from "@/utils/core/cn"; + +import { FE_COLORS, FE_TYPOGRAPHY } from "../theme"; + +const FutureEvalLeaderboardHero: React.FC = () => { + return ( +
+ {/* Back button - FutureEval branded */} +
+ +
+ + {/* Title - left aligned on desktop, centered on mobile */} +

+ Model Leaderboard +

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

+ Updated every day based on our standardized forecasting performance + measurement methodology. +

+
+ ); +}; + +export default FutureEvalLeaderboardHero; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-leaderboard-table.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-leaderboard-table.tsx new file mode 100644 index 0000000000..c4f9a4c9bb --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-leaderboard-table.tsx @@ -0,0 +1,209 @@ +"use client"; + +import { useSearchParams, useRouter } from "next/navigation"; +import { useEffect, useMemo, useRef } from "react"; + +import { + entryIconPair, + entryLabel, + shouldDisplayEntry, +} from "@/app/(main)/aib/components/aib/leaderboard/utils"; +import { LightDarkIcon } from "@/app/(main)/aib/components/aib/light-dark-icon"; +import type { LeaderboardDetails } from "@/types/scoring"; +import cn from "@/utils/core/cn"; + +// Mock translation function for entryLabel - returns hardcoded English values +const mockTranslate = ((key: string) => { + const translations: Record = { + communityPrediction: "Community Prediction", + aibLegendPros: "Pro Forecasters", + }; + return translations[key] ?? key; +}) as ReturnType; + +type Props = { details: LeaderboardDetails }; + +const FutureEvalLeaderboardTable: React.FC = ({ details }) => { + const router = useRouter(); + const searchParams = useSearchParams(); + const highlightId = searchParams.get("highlight"); + const highlightedRowRef = useRef(null); + + const rows = useMemo(() => { + const entries = (details.entries ?? []) + .filter((e) => shouldDisplayEntry(e)) + .map((entry, i) => { + const label = entryLabel(entry, mockTranslate); + 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]); + + // Scroll to and flash highlighted row - depends on rows so it runs after entries are populated + useEffect(() => { + if (highlightId && highlightedRowRef.current) { + highlightedRowRef.current.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + } + }, [highlightId, rows]); + + const hasCI = rows.some((r) => r.ciLower != null || r.ciUpper != null); + + return ( + + + + + + + {hasCI && ( + <> + + + + )} + + + + + + + + {hasCI && ( + <> + + + + )} + + + + + {rows.map((r, i) => { + const isHighlighted = highlightId === r.id; + const profileHref = r.profileHref; + const isClickable = !r.isAggregate && profileHref; + + const handleRowKeyDown = (e: React.KeyboardEvent) => { + if ( + isClickable && + (e.key === "Enter" || e.key === " " || e.key === "Spacebar") + ) { + e.preventDefault(); + router.push(profileHref); + } + }; + + return ( + router.push(profileHref) : undefined} + onKeyDown={handleRowKeyDown} + tabIndex={isClickable ? 0 : -1} + role={isClickable ? "button" : undefined} + className={cn( + "h-[61px] border-b border-gray-300 last:border-0 dark:border-gray-300-dark", + isHighlighted && "animate-highlight-flash", + isClickable && + "cursor-pointer hover:bg-futureeval-primary-light/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-futureeval-primary-light dark:hover:bg-futureeval-primary-dark/10 dark:focus-visible:ring-futureeval-primary-dark" + )} + > + + + + + + + + {hasCI && ( + <> + + + + )} + + ); + })} + +
+ ModelForecastsAvg Score + 95% CI lower + + 95% CI higher +
{i + 1} +
+ {(r.icons.light || r.icons.dark) && ( + + )} +
+
+ {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/(futureeval)/futureeval/components/futureeval-methodology-content.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-methodology-content.tsx new file mode 100644 index 0000000000..c431d43fd5 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-methodology-content.tsx @@ -0,0 +1,177 @@ +"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 { PropsWithChildren } from "react"; + +import cn from "@/utils/core/cn"; + +import { FE_COLORS, FE_TYPOGRAPHY } from "../theme"; + +/** + * FutureEval-specific methodology content without the main title. + * Uses Newsreader serif fonts for headings and sans-serif for body text, + * with FutureEval theme colors (green accents, dark/light backgrounds). + */ +const FutureEvalMethodologyContent: React.FC = () => { + const CARDS = [ + { + icon: faCircleDot, + title: "Model Leaderboard", + linkHref: + "/notebooks/38928/futureeval-resources-page/#what-is-the-model-leaderboard", + content: ( + <> +

+ 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. We also plot trends + in model release date and score over time. +

+ + ), + }, + { + icon: faBullseye, + title: "Bots vs Humans", + linkHref: + "/notebooks/38928/futureeval-resources-page/#what-do-the-tournaments-look-like", + content: ( + <> +

+ We also run seasonal and biweekly Benchmarking Tournaments with + $175k in combined prizes. They are open to all, and the best + scaffold builders compete to share the prize pool in proportion to + their bot's accuracy. Some of the forecasting questions are + also submitted to our top human forecasters, allowing a direct + comparison. +

+ + ), + }, + { + icon: faBrain, + title: "Reasoning Beyond Memorization", + linkHref: + "/notebooks/38928/futureeval-resources-page/#what-is-unique-about-futureeval", + content: ( + <> +

+ Our diverse question topics range from economics, politics, tech, + sports, war, elections, society, and more. It forces models to + generalize beyond memorization on actively evolving + interdisciplinary domains relevant to the world. This correlates + with skill in long-term planning and decision-making. +

+ + ), + }, + ] as const; + + return ( +
+ {/* Hero headline */} +

+ Predicting the future is one of the few ways to evaluate{" "} + reasoning against reality. +

+ + {/* Description paragraphs */} +
+

+ 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. FutureEval is guaranteed leak-proof, since answers + are not known yet at test time. +

+

+ FutureEval has two arms: a fixed-prompt benchmark to compare model + performance directly, and a bots vs. humans tournament to probe the + frontier of scaffolding. +

+
+ +
+ {CARDS.map((card) => ( + + {card.content} + + ))} +
+
+ ); +}; + +/** + * FutureEval-specific idea card + */ +type IdeaCardProps = PropsWithChildren<{ + icon: IconDefinition; + title?: string; + linkHref?: string; +}>; + +const FutureEvalIdeaCard: React.FC = ({ + icon, + title, + linkHref, + children, +}) => { + return ( +
+ +

+ {title} +

+
+ {children} +
+ {linkHref && ( + + Learn more + + )} +
+ ); +}; + +export default FutureEvalMethodologyContent; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-methodology-tab.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-methodology-tab.tsx new file mode 100644 index 0000000000..be0641f4e9 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-methodology-tab.tsx @@ -0,0 +1,13 @@ +import FutureEvalMethodologyContent from "./futureeval-methodology-content"; +import FutureEvalTournaments from "./futureeval-tournaments"; + +const FutureEvalMethodologyTab: React.FC = () => { + return ( +
+ + +
+ ); +}; + +export default FutureEvalMethodologyTab; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-navbar.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-navbar.tsx new file mode 100644 index 0000000000..104eb6dafb --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-navbar.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { faArrowLeft } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React 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 = () => { + return ( +
+ {/* Left side on mobile: Platform button */} + + + {/* Right side: Dark mode toggle */} + +
+ ); +}; + +export default FutureEvalNavbar; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-participate-tab.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-participate-tab.tsx new file mode 100644 index 0000000000..ed04aeb58f --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-participate-tab.tsx @@ -0,0 +1,260 @@ +"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 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, FE_TYPOGRAPHY } 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 { setCurrentModal } = useModal(); + const { user } = useAuth(); + const router = useRouter(); + + const submitSteps = [ + <> + Create a new bot account{" "} + + . + , + <> + Build your bot using our premade template in the{" "} + + instructions + {" "} + provided. + , + "Watch your bot forecast and compete for prizes!", + ] as const; + + return ( +
+ {/* Video section */} +
+

+ Learn how to submit your forecasting bot in 30 minutes +

+ + + Submission walkthrough video thumbnail + +
+ + {/* Steps section */} +
+

+ Submit Your Bot in 3 Steps +

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

+ {content} +

+
+ ); +}; + +/** + * Resources section + */ +const FutureEvalResources: React.FC = () => { + const RESOURCES_DATA = [ + { + icon: faBook, + title: "Full Benchmark Information", + description: "Benchmark deep dive, scoring, analysis, etc", + href: "/notebooks/38928/futureeval-resources-page/", + }, + { + icon: faBookOpen, + title: "Research Highlights", + description: "Key findings and methodology papers from our research.", + href: "/notebooks/38928/futureeval-resources-page/#research-reports-and-overview-of-the-field", + }, + { + icon: faTrophy, + title: "Full Leaderboards", + description: "Complete rankings across all questions and time periods.", + href: "/futureeval/leaderboard", + }, + ] as const; + + return ( +
+

+ Resources +

+
+ {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} +

+

+ {description} +

+
+ + ); +}; + +export default FutureEvalParticipateTab; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-screen.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-screen.tsx new file mode 100644 index 0000000000..c7b46f6a88 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-screen.tsx @@ -0,0 +1,19 @@ +import { AIBLeaderboardProvider } from "@/app/(main)/aib/components/aib/leaderboard/aib-leaderboard-provider"; +import { LeaderboardDetails } from "@/types/scoring"; + +import FutureEvalTabs from "./futureeval-tabs"; +import { Section } from "./futureeval-tabs-shell"; + +type Props = { leaderboard: LeaderboardDetails; current: Section["value"] }; + +const FutureEvalScreen: React.FC = ({ leaderboard, current }) => { + return ( + +
+ +
+
+ ); +}; + +export default FutureEvalScreen; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-tabs-shell.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-tabs-shell.tsx new file mode 100644 index 0000000000..25948b4058 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-tabs-shell.tsx @@ -0,0 +1,52 @@ +"use client"; + +import React 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"; + href: string; + label: string; + content: React.ReactNode; +}; + +type Props = { + current: Section["value"]; + sections: Section[]; +}; + +const FutureEvalTabsShell: React.FC = ({ current, sections }) => { + const activeSection = sections.find((s) => s.value === current); + + // Convert sections to tab items for the header + const tabs: TabItem[] = sections.map((s) => ({ + value: s.value, + href: s.href, + label: s.label, + })); + + return ( +
+ {/* Custom FutureEval navbar */} + + + {/* Hero banner - edge to edge */} + + + {/* Tab content */} + {activeSection && ( +
+
+ {activeSection.content} +
+
+ )} +
+ ); +}; + +export default FutureEvalTabsShell; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-tabs.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-tabs.tsx new file mode 100644 index 0000000000..8ca172647c --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-tabs.tsx @@ -0,0 +1,44 @@ +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 FutureEvalNewsTab from "./news/futureeval-news-tab"; + +type Props = { + current: Section["value"]; +}; + +const FutureEvalTabs: React.FC = async ({ current }) => { + const sections: Section[] = [ + { + value: "benchmark", + href: "/futureeval", + label: "Benchmark", + content: , + }, + { + value: "methodology", + href: "/futureeval/methodology", + label: "Methodology", + content: , + }, + { + value: "news", + href: "/futureeval/news", + label: "News", + content: , + }, + { + value: "participate", + href: "/futureeval/participate", + label: "Participate", + content: , + }, + ]; + + return ; +}; + +export default FutureEvalTabs; diff --git a/front_end/src/app/(futureeval)/futureeval/components/futureeval-tournaments.tsx b/front_end/src/app/(futureeval)/futureeval/components/futureeval-tournaments.tsx new file mode 100644 index 0000000000..d3639b6c61 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/futureeval-tournaments.tsx @@ -0,0 +1,90 @@ +"use client"; + +import Link from "next/link"; + +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, FE_TYPOGRAPHY } from "../theme"; + +/** + * FutureEval-specific tournaments section with consistent theming. + */ +const FutureEvalTournaments: React.FC = () => { + 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 ( +
+

+ Benchmarking Tournaments +

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

+ Make sure to check out{" "} + + MiniBench + + , our shorter-term experimental Bot Tournament! +

+
+
+ ); +}; + +export default FutureEvalTournaments; diff --git a/front_end/src/app/(futureeval)/futureeval/components/news/futureeval-news-card.tsx b/front_end/src/app/(futureeval)/futureeval/components/news/futureeval-news-card.tsx new file mode 100644 index 0000000000..0880d2581e --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/news/futureeval-news-card.tsx @@ -0,0 +1,88 @@ +"use client"; + +import Link from "next/link"; +import { useLocale } from "next-intl"; +import { FC } from "react"; + +import CircleDivider from "@/components/ui/circle_divider"; +import { NotebookPost } from "@/types/post"; +import cn from "@/utils/core/cn"; +import { formatDate } from "@/utils/formatters/date"; +import { getPostLink } from "@/utils/navigation"; + +import { FE_COLORS, FE_TYPOGRAPHY } from "../../theme"; + +type Props = { + post: NotebookPost; +}; + +/** + * FutureEval News Card + * + * A themed variant of the NewsCard for the FutureEval project. + * Differences from the main NewsCard: + * - No article type label (serif blue text) + * - No cover image + * - No description/summary + * - Uses FutureEval theme colors and typography + * - Background matches the FutureEval page background + */ +const FutureEvalNewsCard: FC = ({ post }) => { + const locale = useLocale(); + const commentsCount = post.comment_count ?? 0; + + // Pluralize comments count + const commentsText = + commentsCount === 0 + ? "no comments" + : commentsCount === 1 + ? "comment" + : "comments"; + + return ( +
+ + {/* Title - using FE typography h3 */} +

+ {post.title} +

+ + {/* Metadata - date, author, comments, reading time */} +
+ + {formatDate(locale, new Date(post.published_at))} + + + by {post.author_username} + + + {commentsCount > 0 && commentsCount} {commentsText} + +
+ +
+ ); +}; + +export default FutureEvalNewsCard; diff --git a/front_end/src/app/(futureeval)/futureeval/components/news/futureeval-news-feed.tsx b/front_end/src/app/(futureeval)/futureeval/components/news/futureeval-news-feed.tsx new file mode 100644 index 0000000000..64027386ea --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/news/futureeval-news-feed.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { isNil } from "lodash"; +import { FC, useEffect, useState } from "react"; + +import PostsFeedScrollRestoration from "@/components/posts_feed/feed_scroll_restoration"; +import Button from "@/components/ui/button"; +import { FormErrorMessage } from "@/components/ui/form_field"; +import LoadingIndicator from "@/components/ui/loading_indicator"; +import { POSTS_PER_PAGE, POST_PAGE_FILTER } from "@/constants/posts_feed"; +import { useContentTranslatedBannerContext } from "@/contexts/translations_banner_context"; +import useSearchParams from "@/hooks/use_search_params"; +import ClientPostsApi from "@/services/api/posts/posts.client"; +import { PostsParams } from "@/services/api/posts/posts.shared"; +import { NotebookPost, PostWithForecasts } from "@/types/post"; +import { sendAnalyticsEvent } from "@/utils/analytics"; +import cn from "@/utils/core/cn"; +import { logError } from "@/utils/core/errors"; +import { isNotebookPost } from "@/utils/questions/helpers"; + +import FutureEvalNewsCard from "./futureeval-news-card"; +import { FE_COLORS, FE_TYPOGRAPHY } from "../../theme"; + +type Props = { + initialQuestions: PostWithForecasts[]; + filters: PostsParams; +}; + +/** + * FutureEval News Feed + * + * A paginated news feed using FutureEval-themed news cards. + * Based on PaginatedPostsFeed but simplified for news-only display. + */ +const FutureEvalNewsFeed: FC = ({ initialQuestions, filters }) => { + const { params, setParam, replaceUrlWithoutNavigation } = useSearchParams(); + const pageNumberParam = params.get(POST_PAGE_FILTER); + const pageNumber = !isNil(pageNumberParam) + ? Number(params.get(POST_PAGE_FILTER)) + : 1; + const [clientPageNumber, setClientPageNumber] = useState(pageNumber); + const [paginatedPosts, setPaginatedPosts] = + useState(initialQuestions); + const [offset, setOffset] = useState( + !isNaN(pageNumber) && pageNumber > 0 + ? pageNumber * POSTS_PER_PAGE + : POSTS_PER_PAGE + ); + + const [hasMoreData, setHasMoreData] = useState( + initialQuestions.length >= POSTS_PER_PAGE + ); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState< + (Error & { digest?: string }) | undefined + >(); + + const { setBannerIsVisible } = useContentTranslatedBannerContext(); + + useEffect(() => { + if ( + initialQuestions.filter((q) => q.is_current_content_translated).length > 0 + ) { + setBannerIsVisible(true); + } + }, [initialQuestions, setBannerIsVisible]); + + useEffect(() => { + setClientPageNumber(pageNumber); + }, [pageNumber]); + + useEffect(() => { + sendAnalyticsEvent("feedSearch", { + event_category: JSON.stringify(filters), + }); + }, [filters]); + + const loadMorePosts = async () => { + if (!hasMoreData) return; + + setIsLoading(true); + setError(undefined); + try { + sendAnalyticsEvent("feedSearch", { + event_category: JSON.stringify(filters), + }); + const response = await ClientPostsApi.getPostsWithCP({ + ...filters, + offset, + limit: POSTS_PER_PAGE, + }); + const newPosts = response.results; + const hasNextPage = !!response.next && newPosts.length >= POSTS_PER_PAGE; + + if (newPosts.some((q) => q.is_current_content_translated)) { + setBannerIsVisible(true); + } + + if (!hasNextPage) setHasMoreData(false); + if (newPosts.length) { + setPaginatedPosts((prev) => [...prev, ...newPosts]); + const nextPage = offset / POSTS_PER_PAGE + 1; + setParam(POST_PAGE_FILTER, String(nextPage), false); + replaceUrlWithoutNavigation(); + setClientPageNumber(nextPage); + setOffset((prevOffset) => prevOffset + POSTS_PER_PAGE); + } + } catch (err) { + logError(err); + setError(err as Error & { digest?: string }); + } finally { + setIsLoading(false); + } + }; + + return ( + <> +
+ {!paginatedPosts.length && ( + + No results found. + + )} + {paginatedPosts.map( + (p) => + isNotebookPost(p) && ( + + ) + )} + +
+ + {hasMoreData ? ( +
+ {isLoading ? ( + + ) : ( +
+ + +
+ )} +
+ ) : ( +
+ )} + + ); +}; + +export default FutureEvalNewsFeed; diff --git a/front_end/src/app/(futureeval)/futureeval/components/news/futureeval-news-tab.tsx b/front_end/src/app/(futureeval)/futureeval/components/news/futureeval-news-tab.tsx new file mode 100644 index 0000000000..c0e3c278d7 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/news/futureeval-news-tab.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary"; +import { POSTS_PER_PAGE } from "@/constants/posts_feed"; +import ServerPostsApi from "@/services/api/posts/posts.server"; + +import FutureEvalNewsFeed from "./futureeval-news-feed"; + +/** + * FutureEval News Tab + * + * A themed variant of the AIBNewsTab for the FutureEval project. + * Uses FutureEval-specific news cards and styling. + */ +const FutureEvalNewsTab: React.FC = async () => { + const filters = { + tournaments: "futureeval-posts", + }; + + const { results: questions } = await ServerPostsApi.getPostsWithCP({ + ...filters, + limit: POSTS_PER_PAGE, + }); + + return ( +
+
+ +
+
+ ); +}; + +export default WithServerComponentErrorBoundary(FutureEvalNewsTab); diff --git a/front_end/src/app/(futureeval)/futureeval/components/orbit/futureeval-orbit.tsx b/front_end/src/app/(futureeval)/futureeval/components/orbit/futureeval-orbit.tsx new file mode 100644 index 0000000000..b24149c4d5 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/orbit/futureeval-orbit.tsx @@ -0,0 +1,239 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import React, { useState, useCallback, useRef, useEffect } from "react"; + +import cn from "@/utils/core/cn"; + +import MetaculusHub from "./metaculus-hub"; +import OrbitCircle from "./orbit-circle"; +import { + ORBIT_ANIMATION_DURATION, + ORBIT_CONFIG, + ORBIT_ITEMS, + OrbitItem, +} from "./orbit-constants"; + +// =========================================== +// COMPONENT +// =========================================== + +type FutureEvalOrbitProps = { + className?: string; +}; + +/** + * FutureEvalOrbit - Orbit visualization with rotating circles + * Works on both desktop and mobile + * On mobile (touch devices): tap expands the item, showing close button to dismiss + * On desktop: hover to expand, click to navigate + */ +const FutureEvalOrbit: React.FC = ({ className }) => { + const router = useRouter(); + const [expandedItem, setExpandedItem] = useState(null); + const [mobileExpandedItem, setMobileExpandedItem] = useState( + null + ); + // Use hover capability detection instead of touch detection + // This properly handles laptops with touchscreens using a mouse + const [hasHover, setHasHover] = useState(true); + + // Refs + const containerRef = useRef(null); + + // Detect hover capability using media query (more reliable than touch detection) + useEffect(() => { + const mediaQuery = window.matchMedia("(hover: hover)"); + setHasHover(mediaQuery.matches); + + const handler = (e: MediaQueryListEvent) => setHasHover(e.matches); + mediaQuery.addEventListener("change", handler); + return () => mediaQuery.removeEventListener("change", handler); + }, []); + + // Pause animation when an item is expanded + const isPaused = expandedItem !== null || mobileExpandedItem !== null; + + // Handle item click actions + const handleItemClick = useCallback( + (item: OrbitItem) => { + switch (item.action.type) { + case "scroll": { + const element = document.getElementById(item.action.target); + if (element) { + element.scrollIntoView({ behavior: "smooth", block: "start" }); + } + break; + } + case "tab-scroll": + if (item.action.tabHref) { + router.push(`${item.action.tabHref}#${item.action.target}`); + } + break; + case "navigate": + router.push(item.action.target); + break; + } + }, + [router] + ); + + // Handle mobile tap - expand instead of navigate + const handleMobileTap = useCallback((item: OrbitItem) => { + setMobileExpandedItem(item.id); + }, []); + + // Handle mobile close - return to default state + const handleMobileClose = useCallback(() => { + setMobileExpandedItem(null); + }, []); + + // Calculate circle position as percentage offsets from center (static positions) + // The actual rotation animation is handled by CSS for maximum smoothness + const getCirclePosition = useCallback((index: number) => { + const angle = ORBIT_CONFIG.startAngle + index * ORBIT_CONFIG.angleIncrement; + const angleInRad = (angle * Math.PI) / 180; + // Position is percentage of container, orbit radius is half of orbit diameter + const radius = ORBIT_CONFIG.orbitDiameter / 2; + const x = Math.cos(angleInRad) * radius; + const y = Math.sin(angleInRad) * radius; + return { x, y }; + }, []); + + // Handle click on container background (mobile: close expanded item) + const handleContainerClick = useCallback( + (e: React.MouseEvent) => { + // Only handle clicks directly on the container (not bubbled from children) + if (e.target === e.currentTarget && !hasHover && mobileExpandedItem) { + setMobileExpandedItem(null); + } + }, + [hasHover, mobileExpandedItem] + ); + + return ( +
+ {/* Mobile backdrop with 75% opacity overlay - closes expanded item when tapped */} + {/* Extends beyond container to cover orbit circles that may extend outside */} + {/* z-index must be lower than expanded items (z-50) but covers other content */} + {!hasHover && mobileExpandedItem && ( +
setMobileExpandedItem(null)} + aria-hidden="true" + /> + )} + + {/* Orbit ring - centered in container */} +
+ + {/* Central hub - centered in container */} +
+ +
+ + {/* Rotating orbit group - CSS animation for smooth GPU-accelerated rotation */} +
0 + ? `orbit-rotate ${ORBIT_ANIMATION_DURATION}s linear infinite` + : "none", + animationPlayState: isPaused ? "paused" : "running", + transformOrigin: "50% 50%", + willChange: "transform", + }} + > + {ORBIT_ITEMS.map((item, index) => { + const pos = getCirclePosition(index); + const isItemExpanded = + expandedItem === item.id || mobileExpandedItem === item.id; + return ( +
+ {/* Counter-rotation wrapper - CSS animation perfectly synced with parent */} +
0 + ? `orbit-counter-rotate ${ORBIT_ANIMATION_DURATION}s linear infinite` + : "none", + animationPlayState: isPaused ? "paused" : "running", + willChange: "transform", + }} + > + hasHover && setExpandedItem(item.id)} + onMouseLeave={() => hasHover && setExpandedItem(null)} + onClick={ + hasHover + ? () => handleItemClick(item) + : () => handleMobileTap(item) + } + strokeWidth={ORBIT_CONFIG.strokeWidth} + shadowBlur={ORBIT_CONFIG.shadow.blur} + shadowSpread={ORBIT_CONFIG.shadow.spread} + mobileSpreadRatio={ORBIT_CONFIG.shadow.mobileSpreadRatio} + expandedSpreadRatio={ORBIT_CONFIG.shadow.expandedSpreadRatio} + isMobile={!hasHover} + isMobileExpanded={!hasHover && mobileExpandedItem === item.id} + onMobileClose={handleMobileClose} + onNavigate={() => handleItemClick(item)} + containerRef={containerRef} + /> +
+
+ ); + })} +
+
+ ); +}; + +export default FutureEvalOrbit; diff --git a/front_end/src/app/(futureeval)/futureeval/components/orbit/index.tsx b/front_end/src/app/(futureeval)/futureeval/components/orbit/index.tsx new file mode 100644 index 0000000000..685e519532 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/orbit/index.tsx @@ -0,0 +1,9 @@ +"use client"; + +import FutureEvalOrbit from "./futureeval-orbit"; + +export { FutureEvalOrbit }; +export { ORBIT_ROTATION_SPEED, ORBIT_CONFIG } from "./orbit-constants"; +export type { OrbitItem } from "./orbit-constants"; + +export default FutureEvalOrbit; diff --git a/front_end/src/app/(futureeval)/futureeval/components/orbit/metaculus-hub.tsx b/front_end/src/app/(futureeval)/futureeval/components/orbit/metaculus-hub.tsx new file mode 100644 index 0000000000..1dc5502232 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/orbit/metaculus-hub.tsx @@ -0,0 +1,76 @@ +"use client"; + +import React from "react"; + +import cn from "@/utils/core/cn"; + +import { FE_COLORS } from "../../theme"; + +/** + * MetaculusHub - The central element of the orbit showing Metaculus Platform branding + */ +const MetaculusHub: React.FC = () => { + return ( +
+ {/* Logo and Title - side by side */} +
+ {/* M Logo Icon */} +
+ + + +
+ + {/* Title */} + + Metaculus +
+ Platform +
+
+ + {/* Stats */} +
+ + 3.2M+ predictions in 12 years + +
+ + {/* Subtext link */} + + with the best human forecasters + +
+ ); +}; + +export default MetaculusHub; diff --git a/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-auto-carousel.tsx b/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-auto-carousel.tsx new file mode 100644 index 0000000000..36d85ab1bd --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-auto-carousel.tsx @@ -0,0 +1,199 @@ +"use client"; + +import Image from "next/image"; +import React, { useRef, useEffect, useState } from "react"; + +import cn from "@/utils/core/cn"; + +import { FE_COLORS } from "../../theme"; + +// =========================================== +// TYPES +// =========================================== + +export type CarouselChip = { + id: string; + label: string; + iconLight?: string; + iconDark?: string; +}; + +type OrbitAutoCarouselProps = { + chips: CarouselChip[]; + /** Speed in pixels per second */ + speed?: number; + className?: string; +}; + +// =========================================== +// AUTO-SCROLLING CAROUSEL +// =========================================== + +/** + * OrbitAutoCarousel - Infinite auto-scrolling carousel with fade gradients + * Not user-interactable, purely decorative + */ +const OrbitAutoCarousel: React.FC = ({ + chips, + speed = 30, + className, +}) => { + const containerRef = useRef(null); + const [offset, setOffset] = useState(0); + const animationRef = useRef(null); + const lastTimeRef = useRef(null); + const contentWidthRef = useRef(0); + + // Measure content width and set up animation + useEffect(() => { + if (!containerRef.current) return; + + const measureWidth = () => { + const container = containerRef.current; + if (!container) return; + const firstSet = container.querySelector("[data-carousel-set]"); + if (firstSet) { + contentWidthRef.current = firstSet.scrollWidth; + } + }; + + measureWidth(); + + const animate = (timestamp: number) => { + if (lastTimeRef.current === null) { + lastTimeRef.current = timestamp; + } + + const delta = timestamp - lastTimeRef.current; + lastTimeRef.current = timestamp; + + setOffset((prev) => { + const newOffset = prev + (speed * delta) / 1000; + // Reset when we've scrolled one full set width + if ( + contentWidthRef.current > 0 && + newOffset >= contentWidthRef.current + ) { + return newOffset - contentWidthRef.current; + } + return newOffset; + }); + + animationRef.current = requestAnimationFrame(animate); + }; + + animationRef.current = requestAnimationFrame(animate); + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [speed]); + + if (chips.length === 0) return null; + + return ( +
+ {/* Left fade gradient */} +
+ + {/* Right fade gradient */} +
+ + {/* Scrolling content */} +
+ {/* First set - used for measurement */} +
+ {chips.map((chip, i) => ( + + ))} +
+ {/* Additional sets for seamless loop */} +
+ {chips.map((chip, i) => ( + + ))} +
+
+ {chips.map((chip, i) => ( + + ))} +
+
+
+ ); +}; + +// =========================================== +// CHIP ITEM +// =========================================== + +type CarouselChipItemProps = { + chip: CarouselChip; +}; + +const CarouselChipItem: React.FC = ({ chip }) => { + const hasIcon = chip.iconLight || chip.iconDark; + + return ( + + {hasIcon && ( + <> + {/* Light mode icon */} + {chip.iconLight && ( + + )} + {/* Dark mode icon */} + {(chip.iconDark ?? chip.iconLight) && ( + + )} + + )} + {chip.label} + + ); +}; + +export default OrbitAutoCarousel; diff --git a/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-circle.tsx b/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-circle.tsx new file mode 100644 index 0000000000..21f08d2295 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-circle.tsx @@ -0,0 +1,380 @@ +"use client"; + +import { faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Link from "next/link"; +import React from "react"; +import { createPortal } from "react-dom"; + +import Button from "@/components/ui/button"; +import cn from "@/utils/core/cn"; + +import OrbitAutoCarousel from "./orbit-auto-carousel"; +import { OrbitItem } from "./orbit-constants"; +import { getCarouselChips, getLinkInfo } from "./orbit-utils"; +import { FE_COLORS } from "../../theme"; + +// =========================================== +// TYPES +// =========================================== + +type OrbitCircleProps = { + item: OrbitItem; + isExpanded: boolean; + onMouseEnter: () => void; + onMouseLeave: () => void; + onClick: () => void; + strokeWidth?: number; + shadowBlur?: number; + shadowSpread?: number; + mobileSpreadRatio?: number; + expandedSpreadRatio?: number; + className?: string; + // Mobile-specific props + isMobile?: boolean; + isMobileExpanded?: boolean; + onMobileClose?: () => void; + onNavigate?: () => void; + containerRef?: React.RefObject; +}; + +// =========================================== +// MAIN COMPONENT +// =========================================== + +/** + * OrbitCircle - A circle that sits on the orbit and can expand to show details + * Fills its container (sized by parent) + * + * Shadow color is controlled via CSS variable --orbit-shadow set on parent container + */ +const OrbitCircle: React.FC = ({ + item, + isExpanded, + onMouseEnter, + onMouseLeave, + onClick, + strokeWidth = 1, + shadowBlur = 20, + shadowSpread = 24, + mobileSpreadRatio = 0.5, + expandedSpreadRatio = 0.5, + className, + isMobile = false, + isMobileExpanded = false, + onMobileClose, + onNavigate, + containerRef, +}) => { + const circleRef = React.useRef(null); + + // Calculate shadow spread values using configurable ratios: + // - Mobile default: uses mobileSpreadRatio of original spread + // - Desktop default: 100% of original spread + // - Expanded (both): uses expandedSpreadRatio of original spread + const defaultSpread = isMobile + ? shadowSpread * mobileSpreadRatio + : shadowSpread; + const expandedSpread = shadowSpread * expandedSpreadRatio; + + // Shadow styles using CSS variable for theme-aware color + const defaultShadowStyle = { + boxShadow: `0 0 ${shadowBlur}px ${defaultSpread}px var(--orbit-shadow)`, + }; + + const expandedShadowStyle = { + boxShadow: `0 0 ${shadowBlur}px ${expandedSpread}px var(--orbit-shadow)`, + }; + + // Show expanded card for either desktop hover or mobile tap + const showExpanded = isExpanded || isMobileExpanded; + + // Desktop expanded card renders inline, mobile uses portal to escape rotation context + const desktopExpandedCard = isExpanded && !isMobileExpanded && ( + + ); + + // Mobile expanded card rendered via portal to container (outside rotating context) + const mobileExpandedCard = + isMobileExpanded && + containerRef?.current && + createPortal( + , + containerRef.current + ); + + return ( +
+ {/* The circle itself - fills container */} + + + {/* Desktop expanded card - rendered inline */} + {desktopExpandedCard} + + {/* Mobile expanded card - rendered via portal outside rotating context */} + {mobileExpandedCard} +
+ ); +}; + +// =========================================== +// EXPANDED CARD (Desktop - rendered inline) +// =========================================== + +type ExpandedCardProps = { + item: OrbitItem; + onClick: () => void; + strokeWidth: number; + shadowStyle: React.CSSProperties; +}; + +const ExpandedCard: React.FC = ({ + item, + onClick, + strokeWidth, + shadowStyle, +}) => { + const carouselChips = getCarouselChips(item.id); + const linkInfo = getLinkInfo(item); + + // Handle link click for scroll actions + const handleLinkClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (item.action.type === "scroll") { + e.preventDefault(); + onClick(); + } + }; + + return ( +
+ {/* Title */} +

+ {item.label.replace(/\n/g, " ")} +

+ + {/* Description */} +

+ {item.description} +

+ + {/* Auto-scrolling carousel */} + {carouselChips.length > 0 && ( +
+ +
+ )} + + {/* Action link */} + {linkInfo && ( + + {linkInfo.text} + + )} +
+ ); +}; + +// =========================================== +// MOBILE EXPANDED CARD (rendered via portal) +// =========================================== + +type MobileExpandedCardProps = { + item: OrbitItem; + onClick: () => void; + strokeWidth: number; + shadowStyle: React.CSSProperties; + onClose?: () => void; + containerRef?: React.RefObject; +}; + +const MobileExpandedCard: React.FC = ({ + item, + onClick, + strokeWidth, + shadowStyle, + onClose, + containerRef, +}) => { + const [width, setWidth] = React.useState(240); + + // Calculate 90% of container width + React.useEffect(() => { + if (!containerRef?.current) return; + const containerRect = containerRef.current.getBoundingClientRect(); + setWidth(containerRect.width * 0.9); + }, [containerRef]); + + const carouselChips = getCarouselChips(item.id); + const linkInfo = getLinkInfo(item); + + // Handle close button click + const handleCloseClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onClose?.(); + }; + + // Handle link click for scroll actions + const handleLinkClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (item.action.type === "scroll") { + e.preventDefault(); + onClick(); + } + }; + + return ( +
+ {/* Close button */} + + + {/* Title */} +

+ {item.label.replace(/\n/g, " ")} +

+ + {/* Description */} +

+ {item.description} +

+ + {/* Auto-scrolling carousel */} + {carouselChips.length > 0 && ( +
+ +
+ )} + + {/* Action link */} + {linkInfo && ( + + {linkInfo.text} + + )} +
+ ); +}; + +export default OrbitCircle; diff --git a/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-constants.ts b/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-constants.ts new file mode 100644 index 0000000000..7bd115ec12 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-constants.ts @@ -0,0 +1,157 @@ +import anthropicDark from "@/app/(main)/aib/assets/ai-models/anthropic-black.png"; +import anthropicLight from "@/app/(main)/aib/assets/ai-models/anthropic-white.webp"; +import googleLight from "@/app/(main)/aib/assets/ai-models/google.svg?url"; +import openaiLight from "@/app/(main)/aib/assets/ai-models/openai.svg?url"; +import openaiDark from "@/app/(main)/aib/assets/ai-models/openai_dark.svg?url"; +import xLight from "@/app/(main)/aib/assets/ai-models/x.svg?url"; +import xDark from "@/app/(main)/aib/assets/ai-models/x_dark.svg?url"; + +import { CarouselChip } from "./orbit-auto-carousel"; + +// =========================================== +// ORBIT ITEM TYPES +// =========================================== + +export type OrbitItem = { + id: string; + label: string; + description: string; + action: { + type: "scroll" | "navigate" | "tab-scroll"; + target: string; + tabHref?: string; + }; +}; + +// =========================================== +// ORBIT ITEMS DATA +// =========================================== + +export const ORBIT_ITEMS: OrbitItem[] = [ + { + id: "model-benchmark", + label: "Model\nBenchmark", + description: + "Latest models forecast Metaculus questions and scored against best human and AI forecasters", + action: { + type: "scroll", + target: "model-leaderboard", + }, + }, + { + id: "bot-tournaments", + label: "Bot\nTournaments", + description: + "Seasonal forecasting tournaments where the best bot makers compete", + action: { + type: "tab-scroll", + target: "tournaments", + tabHref: "/futureeval/methodology", + }, + }, + { + id: "minibench", + label: "MiniBench", + description: + "Automated question set, quick turnaround space for testing and rapid iteration for bot makers", + action: { + type: "navigate", + target: "/aib/minibench/", + }, + }, +]; + +// =========================================== +// CAROUSEL DATA +// =========================================== + +/** + * Top models to show in the Model Benchmark carousel + * Hardcoded based on typical top performers + */ +export const MODEL_BENCHMARK_CHIPS: CarouselChip[] = [ + { + id: "o3", + label: "o3", + iconLight: openaiLight as unknown as string, + iconDark: openaiDark as unknown as string, + }, + { + id: "grok-3", + label: "Grok 3", + iconLight: xLight as unknown as string, + iconDark: xDark as unknown as string, + }, + { + id: "gemini", + label: "Gemini", + iconLight: googleLight as unknown as string, + }, + { + id: "claude", + label: "Claude", + iconLight: anthropicLight.src, + iconDark: anthropicDark.src, + }, + { + id: "gpt-5", + label: "GPT-5", + iconLight: openaiLight as unknown as string, + iconDark: openaiDark as unknown as string, + }, + { + id: "grok-4", + label: "Grok 4", + iconLight: xLight as unknown as string, + iconDark: xDark as unknown as string, + }, +]; + +/** + * Bot Tournaments carousel shows prize/credit info + */ +export const BOT_TOURNAMENTS_CHIPS: CarouselChip[] = [ + { id: "prizes", label: "$240k paid in prizes" }, + { id: "credits", label: "Complimentary AI credits provided" }, +]; + +// =========================================== +// SIZING CONFIGURATION +// =========================================== +// All values are percentages relative to the container size +// This ensures the orbit scales proportionally + +export const ORBIT_CONFIG = { + // Orbit ring diameter as % of container + orbitDiameter: 75, + // Circle size as % of container + circleSize: 30, + // Starting angle for first circle (Model Benchmark at upper-left) + // Using CSS coordinate system where 0° = right, positive = clockwise + startAngle: 135, + // Angle increment between circles (negative = counter-clockwise) + angleIncrement: -120, + // Stroke width for orbit ring and circle borders (in pixels) + strokeWidth: 2, + // Drop shadow for orbit circles + // Uses theme background color for a subtle glow effect + shadow: { + blur: 20, // px - how soft/spread the shadow edges are + spread: 24, // px - how far the shadow extends (desktop) + mobileSpreadRatio: 0.5, // Mobile uses this ratio of spread (50%) + expandedSpreadRatio: 0.5, // Expanded uses this ratio of spread (50%) + }, +}; + +/** + * Rotation speed in degrees per second + * Set to 0 to disable rotation + */ +export const ORBIT_ROTATION_SPEED: number = 4; // degrees per second + +/** + * Calculate the CSS animation duration for a full rotation + * 360 degrees / speed = seconds for full rotation + */ +export const ORBIT_ANIMATION_DURATION = + ORBIT_ROTATION_SPEED > 0 ? 360 / ORBIT_ROTATION_SPEED : 0; diff --git a/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-utils.ts b/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-utils.ts new file mode 100644 index 0000000000..896d07f10a --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/components/orbit/orbit-utils.ts @@ -0,0 +1,41 @@ +import { CarouselChip } from "./orbit-auto-carousel"; +import { + BOT_TOURNAMENTS_CHIPS, + MODEL_BENCHMARK_CHIPS, + OrbitItem, +} from "./orbit-constants"; + +/** + * Get carousel chips based on orbit item type + */ +export function getCarouselChips(itemId: string): CarouselChip[] { + switch (itemId) { + case "model-benchmark": + return MODEL_BENCHMARK_CHIPS; + case "bot-tournaments": + return BOT_TOURNAMENTS_CHIPS; + default: + return []; + } +} + +/** + * Get the link text and href based on orbit item type + */ +export function getLinkInfo( + item: OrbitItem +): { text: string; href: string } | null { + switch (item.id) { + case "model-benchmark": + return { text: "View Leaderboard →", href: `#${item.action.target}` }; + case "bot-tournaments": + return { + text: "View Tournaments →", + href: `${item.action.tabHref}#${item.action.target}`, + }; + case "minibench": + return { text: "Visit MiniBench →", href: item.action.target }; + default: + return null; + } +} diff --git a/front_end/src/app/(futureeval)/futureeval/info/page.tsx b/front_end/src/app/(futureeval)/futureeval/info/page.tsx new file mode 100644 index 0000000000..b6d755a844 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/info/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from "next/navigation"; + +// Redirect /futureeval/info to /futureeval/methodology for backwards compatibility +export default function FutureEvalInfoPage() { + redirect("/futureeval/methodology"); +} diff --git a/front_end/src/app/(futureeval)/futureeval/layout.tsx b/front_end/src/app/(futureeval)/futureeval/layout.tsx new file mode 100644 index 0000000000..c6a19fbedd --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/layout.tsx @@ -0,0 +1,34 @@ +import { config } from "@fortawesome/fontawesome-svg-core"; +import "@fortawesome/fontawesome-svg-core/styles.css"; +import type { Metadata } from "next"; + +import CookiesBanner from "@/app/(main)/components/cookies_banner"; +import VersionChecker from "@/app/(main)/components/version_checker"; +import { defaultDescription } from "@/constants/metadata"; + +config.autoAddCss = false; + +export const metadata: Metadata = { + title: "FutureEval | Metaculus", + description: defaultDescription, +}; + +/** + * FutureEval Layout + * + * This layout removes the global header and footer for a cleaner FutureEval experience. + * FutureEval pages use their own navbar (FutureEvalNavbar) instead. + */ +export default function FutureEvalLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+
{children}
+ + +
+ ); +} diff --git a/front_end/src/app/(futureeval)/futureeval/leaderboard/page.tsx b/front_end/src/app/(futureeval)/futureeval/leaderboard/page.tsx new file mode 100644 index 0000000000..9d71f749c1 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/leaderboard/page.tsx @@ -0,0 +1,47 @@ +import { Suspense } from "react"; + +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", + description: "Full AI model leaderboard for Metaculus FutureEval", +}; + +export default async function FutureEvalLeaderboardsPage() { + let data = null; + + try { + data = await ServerLeaderboardApi.getGlobalLeaderboard( + null, + null, + "manual", + "Global Bot Leaderboard" + ); + } catch (error) { + console.error("Failed to fetch leaderboard data:", error); + } + + return ( +
+ + + + + {data?.entries?.length ? ( + }> + + + ) : ( +
+ Leaderboard data not currently available, please check back soon! +
+ )} +
+
+ ); +} diff --git a/front_end/src/app/(futureeval)/futureeval/methodology/page.tsx b/front_end/src/app/(futureeval)/futureeval/methodology/page.tsx new file mode 100644 index 0000000000..fd54151d4c --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/methodology/page.tsx @@ -0,0 +1,32 @@ +import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; +import { LeaderboardDetails } from "@/types/scoring"; + +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() { + let leaderboard: LeaderboardDetails | null = null; + + try { + leaderboard = await ServerLeaderboardApi.getGlobalLeaderboard( + null, + null, + "manual", + "Global Bot Leaderboard" + ); + } catch (error) { + console.error("Failed to fetch leaderboard data:", error); + } + + const safeLeaderboard = + leaderboard ?? ({ entries: [] } as unknown as LeaderboardDetails); + + return ( + + ); +} diff --git a/front_end/src/app/(futureeval)/futureeval/news/page.tsx b/front_end/src/app/(futureeval)/futureeval/news/page.tsx new file mode 100644 index 0000000000..9fc7955c0e --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/news/page.tsx @@ -0,0 +1,30 @@ +import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; +import { LeaderboardDetails } from "@/types/scoring"; + +import FutureEvalScreen from "../components/futureeval-screen"; + +export const metadata = { + title: "FutureEval News | Metaculus", + description: + "Stay updated with the latest news from Metaculus FutureEval, the benchmark measuring AI's ability to predict future outcomes.", +}; + +export default async function FutureEvalNewsPage() { + let leaderboard: LeaderboardDetails | null = null; + + try { + leaderboard = await ServerLeaderboardApi.getGlobalLeaderboard( + null, + null, + "manual", + "Global Bot Leaderboard" + ); + } catch (error) { + console.error("Failed to fetch leaderboard data:", error); + } + + const safeLeaderboard = + leaderboard ?? ({ entries: [] } as unknown as LeaderboardDetails); + + return ; +} diff --git a/front_end/src/app/(futureeval)/futureeval/page.tsx b/front_end/src/app/(futureeval)/futureeval/page.tsx new file mode 100644 index 0000000000..be74c7c759 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/page.tsx @@ -0,0 +1,31 @@ +import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; +import { LeaderboardDetails } from "@/types/scoring"; + +import FutureEvalScreen from "./components/futureeval-screen"; + +export const metadata = { + title: "FutureEval | Metaculus", + description: + "Metaculus FutureEval measures AI's ability to predict future outcomes. Compare AI models against human pro forecasters on real-world forecasting questions.", +}; + +export default async function FutureEvalPage() { + let leaderboard: LeaderboardDetails | null = null; + + try { + leaderboard = await ServerLeaderboardApi.getGlobalLeaderboard( + null, + null, + "manual", + "Global Bot Leaderboard" + ); + } catch (error) { + console.error("Failed to fetch leaderboard data:", error); + } + + // Provide fallback for error cases - components handle empty entries gracefully + const safeLeaderboard = + leaderboard ?? ({ entries: [] } as unknown as LeaderboardDetails); + + return ; +} diff --git a/front_end/src/app/(futureeval)/futureeval/participate/page.tsx b/front_end/src/app/(futureeval)/futureeval/participate/page.tsx new file mode 100644 index 0000000000..7a00337680 --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/participate/page.tsx @@ -0,0 +1,32 @@ +import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; +import { LeaderboardDetails } from "@/types/scoring"; + +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() { + let leaderboard: LeaderboardDetails | null = null; + + try { + leaderboard = await ServerLeaderboardApi.getGlobalLeaderboard( + null, + null, + "manual", + "Global Bot Leaderboard" + ); + } catch (error) { + console.error("Failed to fetch leaderboard data:", error); + } + + const safeLeaderboard = + leaderboard ?? ({ entries: [] } as unknown as LeaderboardDetails); + + return ( + + ); +} diff --git a/front_end/src/app/(futureeval)/futureeval/theme.ts b/front_end/src/app/(futureeval)/futureeval/theme.ts new file mode 100644 index 0000000000..eb51928ade --- /dev/null +++ b/front_end/src/app/(futureeval)/futureeval/theme.ts @@ -0,0 +1,195 @@ +/** + * FutureEval Theme Configuration + * + * This file defines the main theme colors and typography for the FutureEval project. + * Adjust these values to easily change the color scheme across all FutureEval pages. + * + * IMPORTANT: All FutureEval components import from this file. + * Changing values here will update the entire FutureEval project. + * + * COLOR PALETTE: + * - Primary Light: #00A99E (teal) + * - Primary Dark: #23FBE3 (bright cyan) + * - Background Light: #FBFFFC (off-white) + * - Background Dark: #030C07 (near-black) + */ + +// =========================================== +// LOGO SIZING +// =========================================== +// Adjust this value to scale the logo (1.0 = default, 1.15 = 15% larger, etc.) +// Desktop is always 40% larger than mobile, then this scale is applied on top +export const FE_LOGO_SCALE = 1.2; + +// Base sizes before scaling (do not edit these directly, edit FE_LOGO_SCALE instead) +const LOGO_BASE_MOBILE = 120; // px +const LOGO_BASE_DESKTOP = LOGO_BASE_MOBILE * 1.4; // 40% larger + +// Computed logo sizes (used in components) +export const FE_LOGO_SIZES = { + mobile: Math.round(LOGO_BASE_MOBILE * FE_LOGO_SCALE), + desktop: Math.round(LOGO_BASE_DESKTOP * FE_LOGO_SCALE), +} as const; + +// =========================================== +// COLOR DEFINITIONS +// =========================================== + +export const FE_COLORS = { + // =========================================== + // PRIMARY BACKGROUND COLORS + // =========================================== + // Main page background + bgPrimary: "bg-futureeval-bg-light dark:bg-futureeval-bg-dark", + + // =========================================== + // TEXT COLORS + // =========================================== + // Primary text (high contrast) + textPrimary: "text-futureeval-bg-dark dark:text-futureeval-bg-light", + // Secondary/muted text + textSecondary: "text-futureeval-bg-dark/80 dark:text-futureeval-bg-light/80", + textMuted: "text-futureeval-bg-dark/60 dark:text-futureeval-bg-light/60", + // Hover state for muted text (use in className directly, not dynamically) + textMutedHover: + "text-futureeval-bg-dark/60 dark:text-futureeval-bg-light/60 hover:text-futureeval-bg-dark/80 dark:hover:text-futureeval-bg-light/80", + + // Heading colors (uses primary accent) + textHeading: "text-futureeval-bg-dark dark:text-futureeval-bg-light", + // Subheading/body text + textSubheading: "text-futureeval-bg-dark/80 dark:text-futureeval-bg-light/80", + + // Accent colors for links and emphasis + textAccent: "text-futureeval-primary-light dark:text-futureeval-primary-dark", + + // =========================================== + // TOOLTIP/POPOVER COLORS + // =========================================== + tooltipBg: "bg-futureeval-bg-dark/90 dark:bg-futureeval-bg-light/90", + tooltipText: "text-futureeval-bg-light dark:text-futureeval-bg-dark", + tooltipTextSecondary: + "text-futureeval-bg-light/80 dark:text-futureeval-bg-dark/80", + tooltipLink: + "text-futureeval-primary-dark dark:text-futureeval-primary-light", + + // =========================================== + // BUTTON/INTERACTIVE COLORS + // =========================================== + buttonBorder: + "border-futureeval-primary-light dark:border-futureeval-primary-dark", + buttonPrimary: + "bg-futureeval-primary-light dark:bg-futureeval-primary-dark text-futureeval-bg-light dark:text-futureeval-bg-dark", + + // =========================================== + // CAROUSEL/GRADIENT COLORS + // =========================================== + // Gradient fade for carousels (should match bgPrimary) + gradientFrom: "from-futureeval-bg-light dark:from-futureeval-bg-dark", + + // Carousel arrow backgrounds + carouselArrowBg: "bg-futureeval-bg-light dark:bg-futureeval-bg-dark", + + // =========================================== + // NAVBAR COLORS + // =========================================== + navbarScrolled: "bg-futureeval-bg-dark/95 dark:bg-futureeval-bg-dark/95", + navbarTransparent: "bg-transparent", + + // =========================================== + // BAR CHART COLORS (Model Leaderboard) + // =========================================== + // Primary bars (AI models) + barPrimaryBg: "bg-futureeval-primary-light dark:bg-futureeval-primary-dark", + barPrimaryBorder: + "border-futureeval-primary-light dark:border-futureeval-primary-dark", + + // Aggregate bars (Community Prediction) + barAggregateBg: + "bg-futureeval-primary-light/30 dark:bg-futureeval-primary-dark/30", + barAggregateHover: + "hover:bg-futureeval-primary-light/40 dark:hover:bg-futureeval-primary-dark/40", + barAggregateBorder: + "border-futureeval-primary-light dark:border-futureeval-primary-dark", + barAggregateIcon: + "text-futureeval-primary-light dark:text-futureeval-primary-dark", + + // =========================================== + // SECONDARY BACKGROUNDS + // =========================================== + // For cards, banners with subtle contrast + bgSecondary: "bg-futureeval-bg-dark/5 dark:bg-futureeval-bg-light/5", + bgCard: "bg-futureeval-bg-light dark:bg-futureeval-bg-dark", + cardBorder: + "border-futureeval-primary-light/30 dark:border-futureeval-primary-dark/30", + + // Step numbers in Participate tab + stepNumberBg: "bg-futureeval-primary-light dark:bg-futureeval-primary-dark", + + // =========================================== + // BORDER COLORS + // =========================================== + borderPrimary: + "border-futureeval-primary-light dark:border-futureeval-primary-dark", + borderSubtle: + "border-futureeval-bg-dark/20 dark:border-futureeval-bg-light/20", + + // =========================================== + // ORBIT HERO COLORS + // =========================================== + // Solid background for orbit circles (must be opaque to hide orbit ring) + orbitCircleBg: "bg-futureeval-bg-light dark:bg-futureeval-bg-dark", + // Solid hover background for orbit circles - slightly tinted with primary + // Light: #F0FFFD (very subtle teal tint on off-white) + // Dark: #0A1A15 (very subtle teal tint on near-black) + orbitCircleHoverBg: "bg-[#F0FFFD] dark:bg-[#0A1A15]", +} as const; + +// =========================================== +// TYPOGRAPHY CLASSES +// =========================================== +// Centralized typography styles for consistent heading and body text +// Uses Newsreader for headings, Inter for body (via font-sans) + +export const FE_TYPOGRAPHY = { + // Page title - large display heading + h1: "font-newsreader text-2xl font-normal sm:text-3xl md:text-4xl lg:text-5xl text-balance", + + // Section headings + h2: "font-newsreader text-2xl font-normal sm:text-2xl md:text-3xl lg:text-4xl text-balance", + + // Subsection headings + h3: "font-newsreader text-lg font-normal sm:text-3xl", + + // Card titles and smaller headings + h4: "font-newsreader text-base font-normal sm:text-2xl", + + // Body text - primary + body: "font-sans text-sm leading-[1.6] sm:text-base", + + // Body text - small + bodySmall: "font-sans text-xs leading-[1.5] sm:text-[14px]", + + // Labels and UI elements + label: "font-sans text-xs font-medium leading-[1.4] sm:text-[14px]", + + // Link styling + link: "underline underline-offset-2 hover:opacity-80 transition-opacity", +} as const; + +// =========================================== +// RAW COLOR VALUES +// =========================================== +// For components that need inline styles (charts, etc.) + +export const FE_RAW_COLORS = { + light: { + primary: "#00A99E", + background: "#FBFFFC", + }, + dark: { + primary: "#23FBE3", + background: "#030C07", + }, +} as const; + +export default FE_COLORS; 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 1b0bcfcfae..7da0028f02 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)/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..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,20 +21,17 @@ const AIBBenchmarkForecastingPerformance: React.FC = () => { if (!firstIdxByGroup.has(group)) firstIdxByGroup.set(group, i); }); + // Show all companies in the legend (no filtering) const legend = [ ...Array.from(firstIdxByGroup.entries()).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 6ef2f3103d..1ad9921ba3 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,14 @@ "use client"; +import { + faArrowsLeftRight, + faArrowsUpDown, + 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, @@ -43,7 +50,29 @@ 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]); + + // 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(); @@ -94,6 +123,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 +167,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 +196,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 } @@ -171,26 +220,240 @@ 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; + const edgePadLeft = smUp ? 50 : 8; + const edgePadRight = smUp ? rightPad : 8; 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/selection state + const getStarOpacity = useCallback( + (name: string) => { + const company = normalizeToCompany(name); + + // If a company is selected + if (selectedCompany) { + // Selected company's stars are 100%, other companies are 20% + return company === selectedCompany ? 1 : 0.2; + } + + // If a company is hovered in the legend + if (hoveredCompany) { + return company === hoveredCompany ? 1 : 0.35; + } + + return 1; + }, + [selectedCompany, 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 ? ( + + ) : ( + + ) + )} + {/* Mobile-only axis legends (shown when company legends are hidden) */} + {!smUp && ( + <> + + + + )} +
+ ) : null} + {width === 0 &&
} {width > 0 && ( = ({ domainPadding={{ x: 24 }} padding={{ top: 16, - bottom: 68, - left: smUp ? 50 : 30, - right: rightPad, + bottom: smUp ? 68 : 36, + left: smUp ? 50 : 8, + right: smUp ? rightPad : 8, }} containerComponent={ = ({ > } @@ -248,20 +513,21 @@ const AIBBenchmarkPerformanceChart: FC = ({ tickFormat={smUp ? (d: number) => Math.round(d) : () => ""} style={{ grid: { - stroke: getThemeColor(METAC_COLORS.gray[400]), + stroke: getThemeColor(METAC_COLORS.gray[500]), strokeWidth: 1, - strokeDasharray: "2,5", + opacity: 0.15, }, axis: { stroke: "transparent" }, ticks: { stroke: "transparent" }, tickLabels: { fill: getThemeColor(METAC_COLORS.gray[500]), - fontSize: smUp ? 16 : 12, + fontSize: smUp ? 12 : 10, fontWeight: 400, + fontFeatureSettings: '"tnum"', }, axisLabel: { fill: getThemeColor(METAC_COLORS.gray[700]), - fontSize: 16, + fontSize: 14, fontWeight: 400, }, }} @@ -270,7 +536,7 @@ const AIBBenchmarkPerformanceChart: FC = ({ } tickFormat={(d: Date) => d.toLocaleDateString(undefined, { @@ -278,15 +544,17 @@ const AIBBenchmarkPerformanceChart: FC = ({ year: "numeric", }) } - offsetY={68} + offsetY={smUp ? 68 : 36} tickValues={timeTicks} - tickLabelComponent={} + tickLabelComponent={ + + } style={{ axis: { stroke: "transparent" }, 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 +575,8 @@ const AIBBenchmarkPerformanceChart: FC = ({ data: { stroke: REF_STROKE, strokeWidth: 1.5, - strokeDasharray: "4,8", - opacity: 0.7, + opacity: 1, + strokeDasharray: "6,5", }, }} /> @@ -321,11 +589,13 @@ const AIBBenchmarkPerformanceChart: FC = ({ labels={[rl.label]} labelComponent={ } @@ -334,7 +604,7 @@ 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", }, }} @@ -357,7 +627,7 @@ const AIBBenchmarkPerformanceChart: FC = ({ = ({ - colorForName((datum as { name: string }).name), + 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 }) => ( - - - - {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 LegendTrend: FC<{ color: string; label: string }> = ({ +const LegendDot: FC = ({ color, label, + isHovered = false, + isSelected = false, + isDimmed = false, + onHover, + onClick, + onDeselect, +}) => { + // Toggle behavior: select when not selected, deselect when selected + const handleClick = () => { + if (isSelected) { + onDeselect?.(); + } else { + onClick?.(label); + } + }; + + return ( + + ); +}; + +const LegendTrend: FC<{ color: string; label: string; isDimmed?: boolean }> = ({ + color, + label, + isDimmed = false, }) => ( - + = ({ style={{ borderTop: `2px dashed ${color}` }} /> - + {label} @@ -487,7 +842,25 @@ const LegendStar: FC<{ label: string }> = ({ label }) => ( > - + + {label} + + +); + +const LegendAxisIcon: FC<{ + icon: typeof faArrowsLeftRight; + label: string; + color: string; +}> = ({ icon, label, color }) => ( + + + {label} 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..5f444eddcf 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[500]); const axisLabelColor = getThemeColor(METAC_COLORS.gray[700]); const tickLabelColor = getThemeColor(METAC_COLORS.gray[500]); const show = categories.length > 0 && (hasS1 || hasS2); @@ -225,7 +225,7 @@ const AIBProsVsBotsDiffChart: FC<{ const paddingRight = 0; const paddingTop = 16; const paddingBottom = 44; - const chartH = 360; + const chartH = smUp ? 360 : 216; // Mobile: 60% of desktop height const plotW = Math.max(0, width - paddingLeft - paddingRight); const domainSpan = xDomain[1] - xDomain[0]; @@ -322,7 +322,7 @@ const AIBProsVsBotsDiffChart: FC<{ return (
{show && ( -
+
{s1 && ( - + {s1.label} @@ -342,12 +342,12 @@ const AIBProsVsBotsDiffChart: FC<{ className="inline-block h-[14px] w-[14px] rounded-[2px]" style={{ background: getThemeColor(s2.colorToken) }} /> - + {s2.label} )} - + - {width === 0 &&
} + {width === 0 &&
}
{width > 0 && ( } tickValues={yTicksNoZero} - label={smUp ? "Average score difference" : "Score"} + label="Score Difference" style={{ grid: { stroke: gridStroke, strokeWidth: 1, - strokeDasharray: "2,5", + opacity: 0.15, }, axis: { stroke: "transparent" }, ticks: { stroke: "transparent" }, - tickLabels: { fill: tickLabelColor, fontSize: 16 }, - axisLabel: { fill: axisLabelColor, fontSize: 16 }, + tickLabels: { + fill: tickLabelColor, + fontSize: smUp ? 12 : 10, + fontWeight: 400, + fontFeatureSettings: '"tnum"', + }, + axisLabel: { + fill: axisLabelColor, + fontSize: smUp ? 14 : 10, + 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 ( -
+
{ diff --git a/front_end/src/app/(main)/futureeval/info/page.tsx b/front_end/src/app/(main)/futureeval/info/page.tsx deleted file mode 100644 index 5ba4cc4660..0000000000 --- a/front_end/src/app/(main)/futureeval/info/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; - -import AIBScreen from "../../aib/components/aib/aib-screen"; - -export const metadata = { - title: "About AIB | 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 ; -} diff --git a/front_end/src/app/(main)/futureeval/leaderboard/page.tsx b/front_end/src/app/(main)/futureeval/leaderboard/page.tsx deleted file mode 100644 index 29be0bc565..0000000000 --- a/front_end/src/app/(main)/futureeval/leaderboard/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -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"; - -export const metadata = { - title: "Top Model Leaderboards | Metaculus", - description: "Full AI model leaderboard for Metaculus FutureEval", -}; - -export default async function FutureEvalLeaderboardsPage() { - const data = await ServerLeaderboardApi.getGlobalLeaderboard( - null, - null, - "manual", - "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 deleted file mode 100644 index 5c00bf079f..0000000000 --- a/front_end/src/app/(main)/futureeval/news/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; - -import AIBScreen from "../../aib/components/aib/aib-screen"; - -export const metadata = { - title: "AIB 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.", -}; - -export default async function FutureEvalNewsPage() { - const leaderboard = await ServerLeaderboardApi.getGlobalLeaderboard( - null, - null, - "manual", - "Global Bot Leaderboard" - ); - return ; -} diff --git a/front_end/src/app/(main)/futureeval/page.tsx b/front_end/src/app/(main)/futureeval/page.tsx deleted file mode 100644 index b2c5a4d239..0000000000 --- a/front_end/src/app/(main)/futureeval/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server"; - -import AIBScreen from "../aib/components/aib/aib-screen"; - -export const metadata = { - title: "AI Forecasting Benchmark Tournament | 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 FutureEvalPage() { - const leaderboard = await ServerLeaderboardApi.getGlobalLeaderboard( - null, - null, - "manual", - "Global Bot Leaderboard" - ); - return ; -} diff --git a/front_end/src/app/globals.css b/front_end/src/app/globals.css index 97447b1a39..407643b635 100644 --- a/front_end/src/app/globals.css +++ b/front_end/src/app/globals.css @@ -1,224 +1,244 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - *, - ::before, - ::after { - box-sizing: border-box; - border: 0 solid #e5e7eb; - } - /* required to fix a open dialog bug - https://github.com/tailwindlabs/headlessui/discussions/2181 */ - html:has(#headlessui-portal-root) { - @apply !overflow-visible !p-0; - } -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } - - .scrollbar-none { - -ms-overflow-style: none; - scrollbar-width: none; - } - - .scrollbar-none::-webkit-scrollbar { - display: none; - } -} - -input[type="search"]::-webkit-search-decoration, -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-results-button, -input[type="search"]::-webkit-search-results-decoration { - display: none; -} -input { - @apply text-sm; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - @apply mb-2 ml-0 mr-0 mt-4 text-gray-800 dark:text-gray-800-dark; -} - -h1 { - @apply text-3xl font-bold leading-8; -} - -h2 { - @apply text-2xl font-bold leading-7; -} - -h3 { - @apply text-xl font-bold leading-6; -} - -h4 { - @apply text-base font-semibold leading-5; -} - -h5 { - @apply text-sm font-semibold leading-4; -} - -h6 { - @apply text-xs font-bold; -} - -hr { - @apply my-5 border-t border-gray-500 dark:border-gray-500-dark; -} - -p { - margin: 0.92em 0; -} - -a { - @apply text-inherit underline; -} - -.content { - @apply break-words; -} - -.content p, -.content ul, -.content ol { - @apply block text-gray-800 dark:text-gray-800-dark; -} - -.content ul, -.content ol { - @apply mt-3.5 list-outside pl-10; -} - -.content ol li ol { - @apply list-lower-alpha; -} - -.content ol li ol li ol { - @apply list-lower-roman; -} - -.content ol li ol li ol li ol { - @apply list-decimal; -} - -.content ol li ol li ol li ol li ol { - @apply list-lower-alpha; -} - -.content ol li ol li ol li ol li ol li ol { - @apply list-lower-roman; -} - -.content aside { - max-width: calc(720px - 2.5em); -} - -.content ul { - @apply list-disc; -} - -.content ol { - @apply list-decimal; -} - -.content b, -.content strong { - @apply font-bold; -} - -.content a { - @apply text-gray-900 dark:text-gray-900-dark; -} - -.content table { - @apply w-full; -} - -.content table th { - @apply text-sm font-medium leading-4; -} - -.content table td { - @apply text-xs font-normal leading-[14px]; -} - -.content table th, -.content table td { - @apply !border-0 p-0 align-top; -} - -.content table .sticky-column { - @apply sticky left-0 bg-gray-50 dark:bg-gray-100-dark; -} - -.content table tbody > tr > td { - @apply px-3 py-2; -} - -.content table thead td, -.content table thead th { - @apply border-b border-gray-300 pr-2 text-gray-900 dark:border-gray-300-dark dark:text-gray-900-dark; -} - -.content table tbody tr:last-child td, -.content table tbody tr:last-child th { - @apply border-b-0; -} - -.content table.bw-table td { - @apply text-xs font-normal leading-[14px]; -} - -.content table.bw-table tbody > tr > td { - @apply px-6 py-2; -} - -.content table.bw-table thead th { - @apply pr-6; -} - -.content img { - @apply mx-auto block max-h-[500px]; -} - -.content blockquote { - @apply my-4 border-s-4 border-gray-300 bg-gray-100 p-4 text-gray-800 dark:border-gray-300-dark dark:bg-gray-200-dark dark:text-gray-800-dark; -} - -.hidden-scrollable-input { - position: absolute; - overflow: hidden; - clip: rect(0 0 0 0); - height: 1px; - width: 1px; - margin: -1px; - padding: 0; - border: 0; -} - -@supports (-webkit-touch-callout: none) { - .h-screen { - height: -webkit-fill-available; - } - - .min-h-screen { - min-height: -webkit-fill-available; - } -} +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + *, + ::before, + ::after { + box-sizing: border-box; + border: 0 solid #e5e7eb; + } + /* required to fix a open dialog bug + https://github.com/tailwindlabs/headlessui/discussions/2181 */ + html:has(#headlessui-portal-root) { + @apply !overflow-visible !p-0; + } +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } + + .scrollbar-none { + -ms-overflow-style: none; + scrollbar-width: none; + } + + .scrollbar-none::-webkit-scrollbar { + display: none; + } +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-results-button, +input[type="search"]::-webkit-search-results-decoration { + display: none; +} +input { + @apply text-sm; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + @apply mb-2 ml-0 mr-0 mt-4 text-gray-800 dark:text-gray-800-dark; +} + +h1 { + @apply text-3xl font-bold leading-8; +} + +h2 { + @apply text-2xl font-bold leading-7; +} + +h3 { + @apply text-xl font-bold leading-6; +} + +h4 { + @apply text-base font-semibold leading-5; +} + +h5 { + @apply text-sm font-semibold leading-4; +} + +h6 { + @apply text-xs font-bold; +} + +hr { + @apply my-5 border-t border-gray-500 dark:border-gray-500-dark; +} + +p { + margin: 0.92em 0; +} + +a { + @apply text-inherit underline; +} + +.content { + @apply break-words; +} + +.content p, +.content ul, +.content ol { + @apply block text-gray-800 dark:text-gray-800-dark; +} + +.content ul, +.content ol { + @apply mt-3.5 list-outside pl-10; +} + +.content ol li ol { + @apply list-lower-alpha; +} + +.content ol li ol li ol { + @apply list-lower-roman; +} + +.content ol li ol li ol li ol { + @apply list-decimal; +} + +.content ol li ol li ol li ol li ol { + @apply list-lower-alpha; +} + +.content ol li ol li ol li ol li ol li ol { + @apply list-lower-roman; +} + +.content aside { + max-width: calc(720px - 2.5em); +} + +.content ul { + @apply list-disc; +} + +.content ol { + @apply list-decimal; +} + +.content b, +.content strong { + @apply font-bold; +} + +.content a { + @apply text-gray-900 dark:text-gray-900-dark; +} + +.content table { + @apply w-full; +} + +.content table th { + @apply text-sm font-medium leading-4; +} + +.content table td { + @apply text-xs font-normal leading-[14px]; +} + +.content table th, +.content table td { + @apply !border-0 p-0 align-top; +} + +.content table .sticky-column { + @apply sticky left-0 bg-gray-50 dark:bg-gray-100-dark; +} + +.content table tbody > tr > td { + @apply px-3 py-2; +} + +.content table thead td, +.content table thead th { + @apply border-b border-gray-300 pr-2 text-gray-900 dark:border-gray-300-dark dark:text-gray-900-dark; +} + +.content table tbody tr:last-child td, +.content table tbody tr:last-child th { + @apply border-b-0; +} + +.content table.bw-table td { + @apply text-xs font-normal leading-[14px]; +} + +.content table.bw-table tbody > tr > td { + @apply px-6 py-2; +} + +.content table.bw-table thead th { + @apply pr-6; +} + +.content img { + @apply mx-auto block max-h-[500px]; +} + +.content blockquote { + @apply my-4 border-s-4 border-gray-300 bg-gray-100 p-4 text-gray-800 dark:border-gray-300-dark dark:bg-gray-200-dark dark:text-gray-800-dark; +} + +.hidden-scrollable-input { + position: absolute; + overflow: hidden; + clip: rect(0 0 0 0); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + border: 0; +} + +@supports (-webkit-touch-callout: none) { + .h-screen { + height: -webkit-fill-available; + } + + .min-h-screen { + min-height: -webkit-fill-available; + } +} + +/* FutureEval Orbit Animation Keyframes */ +/* Using pure CSS animations for perfectly synchronized, GPU-accelerated rotation */ +@keyframes orbit-rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes orbit-counter-rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(-360deg); + } +} \ No newline at end of file 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/constants/colors.ts b/front_end/src/constants/colors.ts index af8dabb1b8..6fb6fc19b6 100644 --- a/front_end/src/constants/colors.ts +++ b/front_end/src/constants/colors.ts @@ -189,6 +189,13 @@ export const METAC_COLORS = { bell: { DEFAULT: "#b79d00", dark: "#dac024" }, twitter: { DEFAULT: "#1da1f2" }, + // FutureEval brand colors + futureeval: { + "primary-light": "#00A99E", + "primary-dark": "#23FBE3", + "bg-light": "#FBFFFC", + "bg-dark": "#030C07", + }, } as const; export const MULTIPLE_CHOICE_COLOR_SCALE = Object.values( diff --git a/front_end/src/utils/fonts.ts b/front_end/src/utils/fonts.ts index 6e4731e923..ca4a9e6461 100644 --- a/front_end/src/utils/fonts.ts +++ b/front_end/src/utils/fonts.ts @@ -1,3 +1,4 @@ +import { Geist, Geist_Mono, Newsreader } from "next/font/google"; import localFont from "next/font/local"; export const sourceSerifPro = localFont({ @@ -86,6 +87,26 @@ 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 newsreader = Newsreader({ + subsets: ["latin"], + variable: "--font-newsreader", + display: "swap", + weight: ["400", "500", "600"], + style: ["normal", "italic"], +}); + 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} ${newsreader.variable}`; }; diff --git a/front_end/src/utils/navigation.ts b/front_end/src/utils/navigation.ts index fb4c53bea5..425c5ab0b4 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 diff --git a/front_end/tailwind.config.ts b/front_end/tailwind.config.ts index a7be7f81ff..1af695b924 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: [ @@ -47,6 +53,9 @@ 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], + newsreader: ["var(--font-newsreader)", ...defaultTheme.fontFamily.serif], }, strokeWidth: { "3": "3px",