From 397f606c8b9a4f77002fbc00018b92dd66d24cbd Mon Sep 17 00:00:00 2001 From: David Chee <104849031+SirSaltyy@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:10:10 -0800 Subject: [PATCH] Ai dashboard (#3338) * 1st working version * Move AI page into explore * Update ai-contracts.ts --- web/components/ai-content.tsx | 191 +++++++++++++++ web/components/ai-contracts.ts | 138 +++++++++++ .../horizontal-contracts-carousel.tsx | 80 +++++++ web/pages/ai/[[...slug]].tsx | 217 ------------------ web/pages/explore/index.tsx | 9 + 5 files changed, 418 insertions(+), 217 deletions(-) create mode 100644 web/components/ai-content.tsx create mode 100644 web/components/ai-contracts.ts create mode 100644 web/components/horizontal-contracts-carousel.tsx delete mode 100644 web/pages/ai/[[...slug]].tsx diff --git a/web/components/ai-content.tsx b/web/components/ai-content.tsx new file mode 100644 index 0000000000..079944bb12 --- /dev/null +++ b/web/components/ai-content.tsx @@ -0,0 +1,191 @@ +import { useEffect, useState } from 'react' +import { Contract } from 'common/contract' +import { getAiContracts } from './ai-contracts' + +import { LoadingIndicator } from 'web/components/widgets/loading-indicator' +import { QueryUncontrolledTabs, Tab } from 'web/components/layout/tabs' +import { Col } from 'web/components/layout/col' +import { FeedContractCard } from 'web/components/contract/feed-contract-card' +import { ContractsTable } from 'web/components/contract/contracts-table' +import { HorizontalContractsCarousel } from './horizontal-contracts-carousel' + +export interface AiContentData { + newAiModel: Contract | null + closingSoonContracts: Contract[] + surveyContracts: Contract[] + shortBenchmarks: Contract[] + longBenchmarks: Contract[] + aiPolicyContracts: Contract[] + aiCompaniesContracts: Contract[] + recentActivityContracts: Contract[] + justResolvedContracts: Contract[] +} + +async function getAiContentData(): Promise { + try { + const aiContracts = await getAiContracts() + if (!aiContracts) return null + + const { + surveyContracts, + closingSoonContracts, + shortBenchmarks, + longBenchmarks, + aiPolicyContracts, + aiCompaniesContracts, + newAiModel, + recentActivityContracts, + justResolvedContracts, + } = aiContracts + + return { + newAiModel, + closingSoonContracts, + surveyContracts, + shortBenchmarks, + longBenchmarks, + aiPolicyContracts, + aiCompaniesContracts, + recentActivityContracts, + justResolvedContracts, + } + } catch (error) { + console.error('Error fetching AI contracts:', error) + return null + } +} + +export function AiContent() { + const [data, setData] = useState(null) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + const fetchData = async () => { + try { + const aiData = await getAiContentData() + setData(aiData) + } catch (error) { + console.error(error) + } finally { + setIsLoading(false) + } + } + fetchData() + }, []) + + if (isLoading) { + return + } + + if (!data) { + return
No AI data found.
+ } + + const { + newAiModel, + closingSoonContracts, + surveyContracts, + shortBenchmarks, + longBenchmarks, + aiPolicyContracts, + aiCompaniesContracts, + recentActivityContracts, + justResolvedContracts, + } = data + + const AI_TABS: Tab[] = [ + { + title: 'Happening Now', + content: ( + + + + + + + + ), + }, + { + title: 'Companies', + content: ( + + + + ), + }, + { + title: 'AI Digest 2025', + content: ( + +
+ Manifold has partnered with AI Digest to bring you high quality + markets on AI benchmarks and indicators in 2025.{' '} + + Complete their survey + {' '} + and trade on our corresponding markets! +
+ + + ), + }, + { + title: 'Short Benchmarks', + content: ( + + + + ), + }, + { + title: 'Long Benchmarks', + content: ( + + + + ), + }, + { + title: 'Policy', + content: ( + + + + ), + }, + ] + + return ( + + {newAiModel && ( + + + + )} + + + ) +} diff --git a/web/components/ai-contracts.ts b/web/components/ai-contracts.ts new file mode 100644 index 0000000000..501ae2de73 --- /dev/null +++ b/web/components/ai-contracts.ts @@ -0,0 +1,138 @@ +import { Contract } from 'common/contract' +import { api } from 'web/lib/api/api' +import { getContract, getContracts } from '../../common/src/supabase/contracts' +import { db } from 'web/lib/supabase/db' + +export const aiSurveyContractIds = [ + '9cy09yhQd2', + 'hRU0NuZhSy', + 'AELz6Q2usA', + 'EzN2u8OQq2', + 'Uul0EZt0td', + 'z8sPq6NSqQ', + 'dL2Rl06NUI', + 'EQqSEhAuOd', + 'Uu5q0usuQg', +] + +export const shortAiBenchmarkContractIds = [ + 'CkzcqS69tr1hOS56mjZY', + '4cobxeU2KSBo5BPSFCKe', + 'tXYiojNCLmLSJVfGUGK5', + 'hhnUhg5pty', + 'SfBHzKtfZhIqeV2gcLuZ', + 'BcJbQTDX1rdmaLYGKUOz', + 'osbD00CDUgcQGPHhH0mn', + '7yaoogxozx', + 'INyzgSlIAq', +] + +export const longAiBenchmarkContractIds = [ + 'dI5U6ps6IP', + 'DKWUoTfIrbxHwQloZLG3', + 'j7IOXyBOzFiYHtVFXWP3', + 'Red8L367S1DreBesRRu3', + 'ymaev6DmK5AlzKdaTqOt', + 'HJdflF0LTJwPNKQmaf6G', +] + +export const aiPolicyContractIds = [ + '0YCYyjBNcZ2XW9Qer4sD', + 'Twyw0JCFW7VXfa4vl0d6', + 'zo6v3r95mq', + 'PEAh4AsufK9Xdy8kKker', + 'Lb3FC1lLBtU5KySEbVzJ', + 'Uxu9dll7SdYVTGUEmebV', + 'p83DN95Vy7eQPXRsSgpR', + 'U58ue9CkiHRqrgmPlr0S', + 'DKtHVhTHJu2lcEfwmnlP', + '0odd65dzft', + 'QuNoQ7wHgdFBetB3K4jT', + 'g6Cz8nQZy5', + '8wp0xc905e', +] + +export const aiCompaniesContractIds = [ + 'uA88Oc5Uqs', + '5M2I0YYBYCstkwDI3yDK', + 'BmK6wCA9ol7sjaqB9ZGt', + 'rALUJE3xQLyBEGoW1j9Q', + 'C8MtRn2ixX8Y0rV2Oqcy', + 'h6yuo5ag84', + 'lp17jc8bxl', + 'iz2ovejkv3', + 'UpfNoFH6Q6sU3HAZ2SzR', + 'RB1446KxI8aNMAiaIEDl', + 'a9QwFRF9xYWbjhGi4dde', + '1m08c366fh', + 'cnowgnMl9y1YEhE32NUG', +] + +export async function getAiContracts(): Promise<{ + surveyContracts: Contract[] + closingSoonContracts: Contract[] + shortBenchmarks: Contract[] + longBenchmarks: Contract[] + aiPolicyContracts: Contract[] + aiCompaniesContracts: Contract[] + recentActivityContracts: Contract[] + justResolvedContracts: Contract[] + newAiModel: Contract | null +}> { + const fetchContractsByIds = async (ids: string[]): Promise => { + const contracts = await Promise.all(ids.map((id) => getContract(db, id))) + return contracts.filter((c): c is Contract => c !== null) + } + + const [ + surveyContracts, + closingSoonContracts, + shortBenchmarks, + longBenchmarks, + aiPolicyContracts, + aiCompaniesContracts, + recentActivityContracts, + justResolvedContracts, + newAiModel, + ] = await Promise.all([ + getContracts(db, aiSurveyContractIds, 'id'), + api('search-markets-full', { + term: '', + sort: 'most-popular', + filter: 'closing-month', + limit: 7, + gids: 'yEWvvwFFIqzf8JklMewp', + }), + fetchContractsByIds(shortAiBenchmarkContractIds), + fetchContractsByIds(longAiBenchmarkContractIds), + fetchContractsByIds(aiPolicyContractIds), + fetchContractsByIds(aiCompaniesContractIds), + api('search-markets-full', { + term: '', + sort: 'last-updated', + filter: 'open', + limit: 7, + gids: 'yEWvvwFFIqzf8JklMewp', + }), + api('search-markets-full', { + term: '', + sort: 'resolve-date', + filter: 'all', + limit: 7, + gids: 'yEWvvwFFIqzf8JklMewp', + }), + getContract(db, 'sPsE8AZl06'), // New AI Model releases, update monthly with new contract + ]) + + return { + surveyContracts, + closingSoonContracts, + shortBenchmarks, + longBenchmarks, + aiPolicyContracts, + aiCompaniesContracts, + recentActivityContracts, + justResolvedContracts, + newAiModel, + } +} diff --git a/web/components/horizontal-contracts-carousel.tsx b/web/components/horizontal-contracts-carousel.tsx new file mode 100644 index 0000000000..0c50351672 --- /dev/null +++ b/web/components/horizontal-contracts-carousel.tsx @@ -0,0 +1,80 @@ +import clsx from 'clsx' +import { Contract } from 'common/contract' +import { Col } from 'web/components/layout/col' +import { Row } from 'web/components/layout/row' +import { HorizontalDashboardCard } from './dashboard/horizontal-dashboard-card' +import { Carousel } from './widgets/carousel' + +export function HorizontalContractsCarousel(props: { + contracts: Contract[] + title?: string + className?: string +}) { + const { contracts, title, className } = props + + if (contracts.length === 0) return null + + return ( + + {title && ( + +
+
+
+
+ {title} + + )} + + {/* If there's only one contract, just show it */} + {contracts.length === 1 && ( + + )} + + {/* If there are two contracts, show them side by side on desktop, or as a carousel on mobile */} + {contracts.length === 2 && ( + <> + + {contracts.map((contract) => ( + + ))} + + + {contracts.map((contract) => ( + + ))} + + + )} + + {/* If there are more than two contracts, show them all in a carousel */} + {contracts.length > 2 && ( + + {contracts.map((contract) => ( + + ))} + + )} + + ) +} diff --git a/web/pages/ai/[[...slug]].tsx b/web/pages/ai/[[...slug]].tsx deleted file mode 100644 index b9f8478063..0000000000 --- a/web/pages/ai/[[...slug]].tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { - NewsDashboardPageProps, - SuccesNewsDashboardPageProps, -} from 'web/public/data/elections-data' -import { Page } from 'web/components/layout/page' -import { SEO } from 'web/components/SEO' -import { capitalize, first } from 'lodash' -import { Col } from 'web/components/layout/col' -import { getDashboardProps } from 'web/lib/politics/news-dashboard' -import Custom404 from 'web/pages/404' -import NewsPage from 'web/pages/news/[slug]' -import { useSaveCampaign } from 'web/hooks/use-save-campaign' -import { useMultiDashboard } from 'web/hooks/use-multi-dashboard' -import { MultiDashboardHeadlineTabs } from 'web/components/dashboard/multi-dashboard-header' -import { api } from 'web/lib/api/api' -import { Headline } from 'common/news' -import { DashboardPage } from 'web/components/dashboard/dashboard-page' -import { Row } from 'web/components/layout/row' -import { HorizontalDashboard } from 'web/components/dashboard/horizontal-dashboard' -import { contractPath, CPMMNumericContract } from 'common/contract' -import { getExpectedValue } from 'common/multi-numeric' -import { Clock } from 'web/components/clock/clock' -import { NumericBetPanel } from 'web/components/answers/numeric-bet-panel' -import Link from 'next/link' -import clsx from 'clsx' -import { linkClass } from 'web/components/widgets/site-link' -import { useLiveContract } from 'web/hooks/use-contract' -import { getContract } from 'common/supabase/contracts' -import { db } from 'web/lib/supabase/db' -import { ENV_CONFIG } from 'common/envs/constants' -import { CopyLinkOrShareButton } from 'web/components/buttons/copy-link-button' - -// In order to duplicate: -// - duplicate this directory (endpoint/[[...slug]].tsx) -// - edit ENDPOINT, TOP_SLUG, SEO, title, description copy -// - create ${ENDPOINT}_importance_score in dashboard table -// - create `${ENDPOINT}headlines` dashboard to fill trending markets carousel -// - edit schema to accept your new ENDPOINT -const ENDPOINT = 'ai' -const TOP_SLUG = 'home' - -export async function getStaticPaths() { - return { paths: [], fallback: 'blocking' } -} - -const revalidate = 60 -export async function getStaticProps(props: { params: { slug: string[] } }) { - const slug = first(props.params.slug) - if (slug) { - try { - const props = await getDashboardProps(slug, { - topSlug: TOP_SLUG, - slug: ENDPOINT, - }) - return { - props, - revalidate, - } - } catch (e) { - return { - props: { state: 'not found' }, - revalidate, - } - } - } - const headlines = await api('headlines', { slug: ENDPOINT }) - - const newsDashboards = await Promise.all( - headlines.map(async (headline) => getDashboardProps(headline.slug)) - ) - const trendingDashboard = await getDashboardProps(ENDPOINT + 'headline') - headlines.unshift({ - id: TOP_SLUG, - slug: TOP_SLUG, - title: capitalize(TOP_SLUG), - }) - const whenAgi = await getContract(db, 'Gtv5mhjKaiLD6Bkvfhcv') - - return { - props: { - newsDashboards, - headlines, - trendingDashboard, - whenAgi, - } as MultiDashboardProps, - revalidate, - } -} -type MultiDashboardProps = { - newsDashboards: NewsDashboardPageProps[] - headlines: Headline[] - trendingDashboard: NewsDashboardPageProps - whenAgi: CPMMNumericContract -} -export default function MultiOrSingleDashboardPage( - props: MultiDashboardProps | NewsDashboardPageProps -) { - useSaveCampaign() - - // Unknown dashboard - if ('state' in props && props.state === 'not found') { - return - } - // Dashboard - if ('initialDashboard' in props) { - return - } - // Multi dasbhoard home page - return -} - -// Note: I previously saw INSUFFICIENT_RESOURCES errors when trying to render all the dashboards -const MAX_DASHBOARDS = 12 - -function MultiDashboard(props: MultiDashboardProps) { - const { trendingDashboard } = props - const newsDashboards = props.newsDashboards.slice(0, MAX_DASHBOARDS) - const headlines = props.headlines.slice(0, MAX_DASHBOARDS) - const { currentSlug, headlineSlugsToRefs, onClick } = useMultiDashboard( - headlines, - ENDPOINT, - TOP_SLUG - ) - const whenAgi = useLiveContract(props.whenAgi) - - const expectedValueAGI = getExpectedValue(whenAgi) - const eventYear = Math.floor(expectedValueAGI) - const eventMonth = Math.round((expectedValueAGI - eventYear) * 12) - const expectedYear = new Date(eventYear, eventMonth, 1) - - return ( - - - - - - - - - Countdown to AGI - - - - - The market expects we achieve artificial general intelligence by{' '} - {expectedYear.getFullYear()}... What do you think? - - - - - - - - - - - -
-
-
-
- Trending - - {trendingDashboard.state === 'success' && ( - - )} - - - {newsDashboards.map((dashboard) => - dashboard.state === 'not found' ? null : ( - -
- - - ) - )} - - ) -} diff --git a/web/pages/explore/index.tsx b/web/pages/explore/index.tsx index bc75ac79b1..7f49a29ba3 100644 --- a/web/pages/explore/index.tsx +++ b/web/pages/explore/index.tsx @@ -28,6 +28,7 @@ import { api } from 'web/lib/api/api' import { LoadingIndicator } from 'web/components/widgets/loading-indicator' import { User } from 'common/user' import { FaBaseballBall } from 'react-icons/fa' +import { AiContent } from 'web/components/ai-content' import { Contract } from 'common/contract' import { tsToMillis } from 'common/supabase/utils' @@ -319,6 +320,14 @@ function MarketsContent() { ), }, + { + title: 'AI', + content: ( + + + + ), + }, !sportsFirst && { title: 'Sports', content: } )