From 06777e959cd158d19baf61dc56798c9008d5f807 Mon Sep 17 00:00:00 2001 From: Peter Xing Date: Wed, 7 Jan 2026 12:28:29 +0800 Subject: [PATCH] Start predictions in 2026 --- src/App.tsx | 8 +- src/components/LivedExperienceSummary.tsx | 128 +++-- src/components/NarrativeDialog.tsx | 5 +- src/components/TechTreeChecklist.tsx | 85 +++- src/lib/predictions.ts | 578 +++++++--------------- src/lib/spark-llm.ts | 75 +++ src/lib/supabase-client.ts | 113 +++++ src/lib/tech-tree.ts | 39 +- src/lib/types.ts | 2 + 9 files changed, 591 insertions(+), 442 deletions(-) create mode 100644 src/lib/spark-llm.ts create mode 100644 src/lib/supabase-client.ts diff --git a/src/App.tsx b/src/App.tsx index 1734414..98c61d9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useKV } from '@github/spark/hooks'; import type { Domain, MonthData, Goal, Prediction } from '@/lib/types'; -import { generateTimelineData } from '@/lib/predictions'; +import { generateTimelineData, getPredictionYearRange } from '@/lib/predictions'; import { CircularTimeline } from '@/components/CircularTimeline'; import { LinearTimeline } from '@/components/LinearTimeline'; import { DomainSelector } from '@/components/DomainSelector'; @@ -28,9 +28,11 @@ function App() { const [goals, setGoals] = useKV('rehoboam-goals', []); useEffect(() => { - const data = generateTimelineData(currentYear, currentYear + 10); + const { minYear, maxYear } = getPredictionYearRange(); + const endYear = Math.max(maxYear, minYear + 10); + const data = generateTimelineData(minYear, endYear); setTimelineData(data); - }, [currentYear]); + }, []); const handleDomainToggle = (domain: Domain) => { setActiveDomains((prev) => diff --git a/src/components/LivedExperienceSummary.tsx b/src/components/LivedExperienceSummary.tsx index f563f4e..6b360cd 100644 --- a/src/components/LivedExperienceSummary.tsx +++ b/src/components/LivedExperienceSummary.tsx @@ -1,13 +1,44 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useKV } from '@github/spark/hooks'; import type { TechTreeState, TechTreeStatus, MonthData } from '@/lib/types'; -import { getCumulativeTechNodes } from '@/lib/tech-tree'; +import { getCumulativeTechNodes, getNodeStatusForDate } from '@/lib/tech-tree'; +import { generateSparkText } from '@/lib/spark-llm'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Sparkle, User } from '@phosphor-icons/react'; import { toast } from 'sonner'; +interface SummaryContext { + monthLabel: string; + activeNodes: ReturnType; + activeNodesList: string; + statusList: string; + topImpactsList: string; + predictionsList: string; +} + +const buildFallbackSummary = (context: SummaryContext) => { + const { monthLabel, activeNodes, activeNodesList, topImpactsList, predictionsList } = context; + + const notableTech = activeNodes.slice(0, 3).map(node => node.title).join(', ') || 'subtle background systems'; + const focusAreas = topImpactsList || 'everyday routines and work patterns'; + + const predictionLine = predictionsList + ? `The month's headlines orbit predictions like ${predictionsList.replace(/^- /gm, '').split('\n').join(', ')}.` + : 'Headlines are a mix of incremental improvements and cautious optimism.'; + + const techLine = activeNodesList + ? `Technologies such as ${notableTech} quietly hum in the background, stitched together by steady deployment teams.` + : `Even without a single headline technology, your devices quietly coordinate the day in ways that would have felt uncanny a few years ago.`; + + return [ + `It's ${monthLabel}, and your day is quietly shaped by ${notableTech}. You wake to a home that already knows your schedule, adjusts the lights, and queues up a breakfast that matches your health preferences. Commuting is less stressful as automation handles most logistics, letting you reclaim mental space for reflection.`, + `Work has become a conversation with systems rather than a grind through interfaces. Agents prepare briefs and drafts, leaving you to edit and steer. Collaboration happens asynchronously with teammates and their tools, and the biggest change is how quickly ideas turn into tested pilots. The focus areas that feel most different are ${focusAreas}.`, + `Social life keeps pace with the technology curve. Some interactions feel hyper-mediated, but there is still novelty in the way gatherings blend physical and digital presence. ${predictionLine} ${techLine} The month feels like a waypoint rather than a destination.`, + ].join('\n\n'); +}; + interface LivedExperienceSummaryProps { monthData: MonthData | null; } @@ -17,6 +48,10 @@ export function LivedExperienceSummary({ monthData }: LivedExperienceSummaryProp const [summary, setSummary] = useState(''); const [isGenerating, setIsGenerating] = useState(false); + useEffect(() => { + setSummary(''); + }, [monthData?.month, monthData?.year]); + if (!monthData) { return ( @@ -30,40 +65,52 @@ export function LivedExperienceSummary({ monthData }: LivedExperienceSummaryProp const generateSummary = async () => { setIsGenerating(true); - try { - const cumulativeNodes = getCumulativeTechNodes(monthData.year, monthData.month); - - const activeNodes = cumulativeNodes.filter(node => { - const state = techStates?.find(s => s.nodeId === node.id); - const status = state?.status || 'not-started'; - return status !== 'not-started' && status !== 'r-and-d'; - }); - - const lifeVariableImpacts = new Map(); - activeNodes.forEach(node => { - node.tags.forEach(tag => { - lifeVariableImpacts.set(tag, (lifeVariableImpacts.get(tag) || 0) + 1); - }); - }); + setSummary(''); - const topImpacts = Array.from(lifeVariableImpacts.entries()) - .sort((a, b) => b[1] - a[1]) - .slice(0, 10) - .map(([tag]) => tag); + const cumulativeNodes = getCumulativeTechNodes(monthData.year, monthData.month); - const statusBreakdown = activeNodes.reduce((acc, node) => { - const state = techStates?.find(s => s.nodeId === node.id); - const status = state?.status || 'not-started'; - acc[status] = (acc[status] || 0) + 1; - return acc; - }, {} as Record); + const activeNodes = cumulativeNodes.filter(node => { + const status = getNodeStatusForDate(techStates, node.id, monthData.year, monthData.month, 'pilot'); + return status !== 'not-started' && status !== 'r-and-d'; + }); - const monthLabel = new Date(monthData.year, monthData.month).toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); - const activeNodesList = activeNodes.slice(0, 20).map(n => `- ${n.title} (${techStates?.find(s => s.nodeId === n.id)?.status || 'pilot'})`).join('\n'); - const statusList = Object.entries(statusBreakdown).map(([status, count]) => `- ${status}: ${count} breakthroughs`).join('\n'); - const topImpactsList = topImpacts.join(', '); - const predictionsList = monthData.predictions.slice(0, 3).map(p => `- ${p.title}`).join('\n'); + const lifeVariableImpacts = new Map(); + activeNodes.forEach(node => { + node.tags.forEach(tag => { + lifeVariableImpacts.set(tag, (lifeVariableImpacts.get(tag) || 0) + 1); + }); + }); + + const topImpacts = Array.from(lifeVariableImpacts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) + .map(([tag]) => tag); + + const statusBreakdown = activeNodes.reduce((acc, node) => { + const status = getNodeStatusForDate(techStates, node.id, monthData.year, monthData.month, 'pilot'); + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {} as Record); + + const monthLabel = new Date(monthData.year, monthData.month).toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); + const activeNodesList = activeNodes + .slice(0, 20) + .map(n => `- ${n.title} (${getNodeStatusForDate(techStates, n.id, monthData.year, monthData.month, 'pilot')})`) + .join('\n'); + const statusList = Object.entries(statusBreakdown).map(([status, count]) => `- ${status}: ${count} breakthroughs`).join('\n'); + const topImpactsList = topImpacts.join(', '); + const predictionsList = monthData.predictions.slice(0, 3).map(p => `- ${p.title}`).join('\n'); + + const context: SummaryContext = { + monthLabel, + activeNodes, + activeNodesList, + statusList, + topImpactsList, + predictionsList, + }; + try { const promptText = `You are a futurist writing a vivid "lived experience" narrative for someone living in ${monthLabel}. Based on the following technological breakthroughs that have occurred or are underway, write a compelling 3-4 paragraph narrative describing what daily life is like for an average person in a developed country. @@ -88,12 +135,21 @@ Write in second person ("you wake up...", "your morning starts...") and make it Be specific about technologies in use but keep the tone human and relatable. Aim for 300-400 words.`; - const result = await window.spark.llm(promptText, 'gpt-4o'); - setSummary(result); - toast.success('Lived experience summary generated'); + const aiSummary = await generateSparkText(promptText, 'gpt-4o'); + + if (aiSummary) { + setSummary(aiSummary); + toast.success('Lived experience summary generated'); + } else { + const fallback = buildFallbackSummary(context); + setSummary(fallback); + toast.warning('Using offline summary while AI is unavailable'); + } } catch (error) { console.error('Error generating summary:', error); - toast.error('Failed to generate summary'); + const fallback = buildFallbackSummary(context); + setSummary(fallback); + toast.error('Failed to generate summary with AI. Showing synthesized version instead'); } finally { setIsGenerating(false); } diff --git a/src/components/NarrativeDialog.tsx b/src/components/NarrativeDialog.tsx index bbaa8d9..4899538 100644 --- a/src/components/NarrativeDialog.tsx +++ b/src/components/NarrativeDialog.tsx @@ -11,6 +11,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import type { MonthData, Domain } from '@/lib/types'; import { getMonthName, DOMAIN_LABELS } from '@/lib/predictions'; import { Brain, ArrowsClockwise } from '@phosphor-icons/react'; +import { generateSparkText } from '@/lib/spark-llm'; interface NarrativeDialogProps { open: boolean; @@ -71,8 +72,8 @@ Write a vivid 2-3 paragraph narrative describing what daily life might feel like Make it personal, sensory, and grounded. Avoid generic statements. This should read like a dispatch from the future.`; - const result = await window.spark.llm(promptText, 'gpt-4o'); - setNarrative(result); + const result = await generateSparkText(promptText, 'gpt-4o'); + setNarrative(result || 'Unable to generate narrative. Please try again.'); } catch (error) { setNarrative('Unable to generate narrative. Please try again.'); } finally { diff --git a/src/components/TechTreeChecklist.tsx b/src/components/TechTreeChecklist.tsx index 7c6c481..f2d04f0 100644 --- a/src/components/TechTreeChecklist.tsx +++ b/src/components/TechTreeChecklist.tsx @@ -1,13 +1,20 @@ -import { useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useKV } from '@github/spark/hooks'; import type { TechTreeNode, TechTreeState, TechTreeStatus } from '@/lib/types'; -import { getCumulativeTechNodes } from '@/lib/tech-tree'; +import { getCumulativeTechNodes, getNodeStatusForDate } from '@/lib/tech-tree'; import { Card } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import { ScrollArea } from '@/components/ui/scroll-area'; import { CheckCircle, Circle, Flask, Users, Rocket, Globe, Lock } from '@phosphor-icons/react'; +import { toast } from 'sonner'; +import { + fetchTechTreeStates, + getSupabaseConfig, + getUserInstanceId, + upsertTechTreeState, +} from '@/lib/supabase-client'; interface TechTreeChecklistProps { year: number; @@ -32,9 +39,54 @@ const CATEGORY_LABELS = { geopolitics: 'Geopolitics', }; +const mergeStates = (existing: TechTreeState[], incoming: TechTreeState[]) => { + const byKey = new Map(); + + const pushState = (state: TechTreeState) => { + const key = `${state.nodeId}-${state.effectiveYear ?? 'all'}-${state.effectiveMonth ?? 'all'}`; + const current = byKey.get(key); + if (!current || current.updatedAt < state.updatedAt) { + byKey.set(key, state); + } + }; + + (existing || []).forEach(pushState); + (incoming || []).forEach(pushState); + + return Array.from(byKey.values()).sort((a, b) => a.updatedAt - b.updatedAt); +}; + export function TechTreeChecklist({ year, month }: TechTreeChecklistProps) { const [techStates, setTechStates] = useKV('tech-tree-states', []); const [expandedCategories, setExpandedCategories] = useState(['individual']); + const [warnedAboutLocalOnly, setWarnedAboutLocalOnly] = useState(false); + const supabaseConfig = useMemo(() => getSupabaseConfig(), []); + const userInstanceId = useMemo(() => getUserInstanceId(), []); + + useEffect(() => { + let isCancelled = false; + + const pullRemoteState = async () => { + if (!supabaseConfig || !userInstanceId) return; + + try { + const remote = await fetchTechTreeStates(supabaseConfig, userInstanceId); + if (isCancelled || !remote) return; + + setTechStates(current => mergeStates(current || [], remote)); + } catch (error) { + if (isCancelled) return; + const description = error instanceof Error ? error.message : 'Unable to reach Supabase'; + toast.error('Failed to load saved tech selections', { description }); + } + }; + + pullRemoteState(); + + return () => { + isCancelled = true; + }; + }, [setTechStates, supabaseConfig, userInstanceId]); const cumulativeNodes = getCumulativeTechNodes(year, month); @@ -47,14 +99,33 @@ export function TechTreeChecklist({ year, month }: TechTreeChecklistProps) { }, {} as Record); const getNodeStatus = (nodeId: string): TechTreeStatus => { - const state = techStates?.find(s => s.nodeId === nodeId); - return state?.status || 'not-started'; + return getNodeStatusForDate(techStates, nodeId, year, month, 'not-started'); }; const updateNodeStatus = (nodeId: string, status: TechTreeStatus) => { - setTechStates(current => { - const filtered = (current || []).filter(s => s.nodeId !== nodeId); - return [...filtered, { nodeId, status, updatedAt: Date.now() }]; + const nextState: TechTreeState = { + nodeId, + status, + effectiveYear: year, + effectiveMonth: month, + updatedAt: Date.now(), + }; + + setTechStates(current => mergeStates(current || [], [nextState])); + + if (!supabaseConfig || !userInstanceId) { + if (!warnedAboutLocalOnly) { + setWarnedAboutLocalOnly(true); + toast.message('Selections saved locally', { + description: 'Add Supabase env vars to sync selections per user.', + }); + } + return; + } + + upsertTechTreeState(supabaseConfig, userInstanceId, nextState).catch(error => { + const description = error instanceof Error ? error.message : 'Unable to reach Supabase'; + toast.error('Failed to store selection', { description }); }); }; diff --git a/src/lib/predictions.ts b/src/lib/predictions.ts index b91cafe..c5787f2 100644 --- a/src/lib/predictions.ts +++ b/src/lib/predictions.ts @@ -26,399 +26,7 @@ export function getMonthName(month: number): string { return months[month]; } -const PREDICTION_DATABASE: Prediction[] = [ - { - id: 'tech-2025-01-1', - domain: 'tech', - month: 0, - year: 2025, - probability: 0.85, - title: 'GPT-5 and Claude 4 released with major advances', - description: 'Next-generation LLMs with dramatically improved reasoning, multimodal capabilities, and reduced hallucinations.', - impact: 'high', - sources: [ - { name: 'AI Futures Model', url: 'https://www.aifuturesmodel.com/forecast', confidence: 0.82 }, - { name: 'Future Timeline', url: 'https://www.futuretimeline.net/21stcentury/2025.htm', confidence: 0.88 }, - ], - }, - { - id: 'individual-2025-01-1', - domain: 'individual', - month: 0, - year: 2025, - probability: 0.72, - title: 'AI personal assistants manage daily schedules autonomously', - description: 'Advanced AI capable of autonomous scheduling, email management, and task prioritization becomes commonplace.', - impact: 'medium', - sources: [ - { name: 'Technology Adoption Survey', confidence: 0.69 }, - { name: 'Consumer Technology Report', confidence: 0.75 }, - ], - }, - { - id: 'economic-2025-01-1', - domain: 'economic', - month: 0, - year: 2025, - probability: 0.78, - title: 'Global recession risks diminish', - description: 'Economic indicators stabilize as central banks achieve soft landing after interest rate hikes.', - impact: 'high', - sources: [ - { name: 'IMF World Economic Outlook', confidence: 0.81 }, - { name: 'Federal Reserve Analysis', confidence: 0.75 }, - ], - }, - { - id: 'tech-2025-02-1', - domain: 'tech', - month: 1, - year: 2025, - probability: 0.81, - title: 'Apple Vision Pro 2 launches with mass appeal', - description: 'Second-gen spatial computing headset at lower price point drives mainstream AR adoption.', - impact: 'high', - sources: [ - { name: 'Apple Supply Chain Reports', confidence: 0.84 }, - { name: 'Consumer Electronics Forecast', confidence: 0.78 }, - ], - }, - { - id: 'social-2025-02-1', - domain: 'social', - month: 1, - year: 2025, - probability: 0.75, - title: 'Gen Z becomes largest workforce cohort', - description: 'Generation Z overtakes Millennials, bringing new workplace culture expectations.', - impact: 'medium', - sources: [ - { name: 'Bureau of Labor Statistics', confidence: 0.79 }, - { name: 'Workforce Demographics Study', confidence: 0.71 }, - ], - }, - { - id: 'governance-2025-02-1', - domain: 'governance', - month: 1, - year: 2025, - probability: 0.77, - title: 'Carbon border taxes implemented in EU', - description: 'Carbon Border Adjustment Mechanism comes into effect, reshaping global trade.', - impact: 'high', - sources: [ - { name: 'EU Policy Timeline', url: 'https://ec.europa.eu/', confidence: 0.88 }, - { name: 'Trade Policy Analysis', confidence: 0.66 }, - ], - }, - { - id: 'geopolitical-2025-03-1', - domain: 'geopolitical', - month: 2, - year: 2025, - probability: 0.82, - title: 'India becomes third largest economy', - description: 'India surpasses Japan and Germany in GDP rankings.', - impact: 'high', - sources: [ - { name: 'World Bank Projections', confidence: 0.85 }, - { name: 'Future Timeline', url: 'https://www.futuretimeline.net/21stcentury/2025.htm', confidence: 0.79 }, - ], - }, - { - id: 'economic-2025-03-1', - domain: 'economic', - month: 2, - year: 2025, - probability: 0.64, - title: 'Bitcoin surpasses $150,000', - description: 'Institutional adoption and regulatory clarity drive cryptocurrency to new highs.', - impact: 'medium', - sources: [ - { name: 'Prediction Markets', confidence: 0.61 }, - { name: 'Financial Analyst Consensus', confidence: 0.67 }, - ], - }, - { - id: 'tech-2025-03-1', - domain: 'tech', - month: 2, - year: 2025, - probability: 0.79, - title: 'Nvidia H200 chips power 80% of AI training', - description: 'Next-gen AI accelerators dominate machine learning infrastructure.', - impact: 'medium', - sources: [ - { name: 'Semiconductor Industry Report', confidence: 0.82 }, - { name: 'AI Infrastructure Analysis', confidence: 0.76 }, - ], - }, - { - id: 'individual-2025-04-1', - domain: 'individual', - month: 3, - year: 2025, - probability: 0.68, - title: 'Mental health apps reach 500M active users', - description: 'Digital mental health platforms achieve mainstream adoption globally.', - impact: 'medium', - sources: [ - { name: 'Digital Health Report', confidence: 0.71 }, - { name: 'Mental Health Technology Survey', confidence: 0.65 }, - ], - }, - { - id: 'social-2025-04-1', - domain: 'social', - month: 3, - year: 2025, - probability: 0.83, - title: 'TikTok reaches 2 billion monthly users', - description: 'Short-form video continues explosive growth, reshaping media consumption.', - impact: 'medium', - sources: [ - { name: 'Social Media Analytics', confidence: 0.86 }, - { name: 'Digital Trends Report', confidence: 0.80 }, - ], - }, - { - id: 'tech-2025-04-1', - domain: 'tech', - month: 3, - year: 2025, - probability: 0.76, - title: '6G network trials begin in Asia', - description: 'Next-generation wireless technology testing starts with 10x faster speeds than 5G.', - impact: 'medium', - sources: [ - { name: 'Telecom Industry Roadmap', confidence: 0.73 }, - { name: 'Future Timeline', url: 'https://www.futuretimeline.net/21stcentury/2025.htm', confidence: 0.79 }, - ], - }, - { - id: 'economic-2025-05-1', - domain: 'economic', - month: 4, - year: 2025, - probability: 0.71, - title: 'AI startups receive record $120B in funding', - description: 'Venture capital investment in artificial intelligence hits unprecedented levels.', - impact: 'high', - sources: [ - { name: 'VC Investment Tracker', confidence: 0.74 }, - { name: 'AI Industry Report', confidence: 0.68 }, - ], - }, - { - id: 'governance-2025-05-1', - domain: 'governance', - month: 4, - year: 2025, - probability: 0.89, - title: 'EU AI Act fully enforced', - description: 'Comprehensive AI regulations set global standards for AI governance and safety.', - impact: 'high', - sources: [ - { name: 'EU Official Timeline', url: 'https://digital-strategy.ec.europa.eu/', confidence: 0.95 }, - { name: 'Future Timeline', url: 'https://www.futuretimeline.net/21stcentury/2025.htm', confidence: 0.83 }, - ], - }, - { - id: 'tech-2025-06-1', - domain: 'tech', - month: 5, - year: 2025, - probability: 0.73, - title: 'AI writes 60% of new software code', - description: 'GitHub Copilot and competitors fundamentally transform software development.', - impact: 'high', - sources: [ - { name: 'GitHub Developer Survey', confidence: 0.70 }, - { name: 'AI Futures Model', url: 'https://www.aifuturesmodel.com/forecast', confidence: 0.76 }, - ], - }, - { - id: 'individual-2025-06-1', - domain: 'individual', - month: 5, - year: 2025, - probability: 0.69, - title: 'Wearable tech tracks 50 health metrics continuously', - description: 'Advanced biosensors enable comprehensive health monitoring via smartwatches.', - impact: 'medium', - sources: [ - { name: 'Wearable Technology Report', confidence: 0.72 }, - { name: 'Digital Health Survey', confidence: 0.66 }, - ], - }, - { - id: 'geopolitical-2025-07-1', - domain: 'geopolitical', - month: 6, - year: 2025, - probability: 0.76, - title: 'BRICS currency initiative advances', - description: 'Brazil, Russia, India, China, South Africa formalize alternative to dollar system.', - impact: 'high', - sources: [ - { name: 'International Economics Institute', confidence: 0.73 }, - { name: 'Geopolitical Forecast', confidence: 0.79 }, - ], - }, - { - id: 'social-2025-07-1', - domain: 'social', - month: 6, - year: 2025, - probability: 0.72, - title: 'Digital nomads exceed 60 million globally', - description: 'Location-independent work becomes widespread lifestyle choice.', - impact: 'medium', - sources: [ - { name: 'Remote Work Institute', confidence: 0.69 }, - { name: 'Future of Work Report', confidence: 0.75 }, - ], - }, - { - id: 'tech-2025-08-1', - domain: 'tech', - month: 7, - year: 2025, - probability: 0.74, - title: 'Solid-state batteries enter mass production', - description: 'Next-gen batteries with 2x energy density begin commercial manufacturing.', - impact: 'high', - sources: [ - { name: 'Battery Technology Roadmap', confidence: 0.71 }, - { name: 'Future Timeline', url: 'https://www.futuretimeline.net/21stcentury/2025.htm', confidence: 0.77 }, - ], - }, - { - id: 'geopolitical-2025-08-1', - domain: 'geopolitical', - month: 7, - year: 2025, - probability: 0.79, - title: 'Taiwan-China tensions escalate significantly', - description: 'Military exercises and economic pressure raise international concerns.', - impact: 'high', - sources: [ - { name: 'Strategic Affairs Institute', confidence: 0.82 }, - { name: 'Future Timeline', url: 'https://www.futuretimeline.net/21stcentury/2025.htm', confidence: 0.76 }, - ], - }, - { - id: 'social-2025-09-1', - domain: 'social', - month: 8, - year: 2025, - probability: 0.78, - title: 'Remote work stabilizes at 45% of knowledge workers', - description: 'Hybrid arrangements become permanent, reshaping cities and real estate.', - impact: 'high', - sources: [ - { name: 'McKinsey Future of Work', confidence: 0.81 }, - { name: 'Workplace Trends Report', confidence: 0.75 }, - ], - }, - { - id: 'economic-2025-09-1', - domain: 'economic', - month: 8, - year: 2025, - probability: 0.81, - title: 'Electric vehicles reach 30% of new car sales', - description: 'EVs achieve critical mass as infrastructure and costs improve.', - impact: 'high', - sources: [ - { name: 'Automotive Industry Report', confidence: 0.84 }, - { name: 'Future Timeline', url: 'https://www.futuretimeline.net/21stcentury/2025.htm', confidence: 0.78 }, - ], - }, - { - id: 'tech-2025-10-1', - domain: 'tech', - month: 9, - year: 2025, - probability: 0.77, - title: 'AI translation indistinguishable from human', - description: 'Real-time translation services achieve human-quality performance.', - impact: 'medium', - sources: [ - { name: 'Language AI Report', confidence: 0.74 }, - { name: 'Translation Technology Survey', confidence: 0.80 }, - ], - }, - { - id: 'governance-2025-10-1', - domain: 'governance', - month: 9, - year: 2025, - probability: 0.71, - title: 'Digital identity systems go live nationally', - description: 'Major countries deploy comprehensive digital ID for citizens.', - impact: 'medium', - sources: [ - { name: 'Digital Government Report', confidence: 0.68 }, - { name: 'Identity Tech Institute', confidence: 0.74 }, - ], - }, - { - id: 'individual-2025-11-1', - domain: 'individual', - month: 10, - year: 2025, - probability: 0.66, - title: 'Wearables prevent 600k deaths through early detection', - description: 'Continuous health monitoring achieves significant mortality reduction.', - impact: 'high', - sources: [ - { name: 'WHO Digital Health Report', confidence: 0.63 }, - { name: 'Medical Technology Forecast', confidence: 0.69 }, - ], - }, - { - id: 'geopolitical-2025-11-1', - domain: 'geopolitical', - month: 10, - year: 2025, - probability: 0.68, - title: 'Climate migration reaches 15 million annually', - description: 'Environmental displacement becomes major humanitarian challenge.', - impact: 'high', - sources: [ - { name: 'UN Climate Migration Report', confidence: 0.71 }, - { name: 'Environmental Institute', confidence: 0.65 }, - ], - }, - { - id: 'social-2025-12-1', - domain: 'social', - month: 11, - year: 2025, - probability: 0.75, - title: 'Esports viewership surpasses traditional sports', - description: 'Competitive gaming audiences exceed conventional sports in key demographics.', - impact: 'medium', - sources: [ - { name: 'Entertainment Industry Report', confidence: 0.78 }, - { name: 'Digital Media Analytics', confidence: 0.72 }, - ], - }, - { - id: 'tech-2025-12-1', - domain: 'tech', - month: 11, - year: 2025, - probability: 0.82, - title: 'AI content detection becomes standard practice', - description: 'Reliable systems for identifying AI-generated content widely deployed.', - impact: 'medium', - sources: [ - { name: 'AI Safety Research', confidence: 0.79 }, - { name: 'Digital Media Institute', confidence: 0.85 }, - ], - }, +const RAW_PREDICTIONS: Prediction[] = [ { id: 'tech-2026-01-1', domain: 'tech', @@ -1105,8 +713,192 @@ const PREDICTION_DATABASE: Prediction[] = [ { name: 'Future Timeline', url: 'https://www.futuretimeline.net/21stcentury/2035.htm', confidence: 0.87 }, ], }, + { + id: 'edu-2026-04-assessment-bifurcates', + domain: 'governance', + month: 3, + year: 2026, + probability: 0.72, + title: 'Assessment splits into AI-permitted and AI-restricted tracks', + description: + 'As AI tutoring normalizes, systems formalize dual pathways: open-book, AI-assisted work that rewards synthesis and iteration, alongside proctored AI-restricted checkpoints for foundational skills and integrity.', + impact: 'medium', + sources: [ + { name: 'UNESCO', url: 'https://www.unesco.org/en/digital-education/ai-future-learning', confidence: 0.72 }, + ], + }, + { + id: 'edu-2026-06-teacher-orchestration', + domain: 'social', + month: 5, + year: 2026, + probability: 0.77, + title: 'Teachers shift toward orchestration and pastoral roles', + description: + 'With AI covering first-draft explanations and routine feedback after broad tutor adoption, teachers emphasize motivation, class culture, project guidance, and personalized interventions.', + impact: 'medium', + sources: [ + { name: 'AI 2027', url: 'https://ai-2027.com/summary', confidence: 0.68 }, + { name: 'AI Futures Model', url: 'https://www.aifuturesmodel.com/about', confidence: 0.66 }, + ], + }, + { + id: 'edu-2028-03-mastery-maps', + domain: 'governance', + month: 2, + year: 2028, + probability: 0.7, + title: 'Curricula unbundle into mastery maps with individualized pacing', + description: + 'After AI tutors and dual-track assessment bed in, course pacing guides give way to competency maps that unlock concepts as students demonstrate understanding, coordinated by AI tutors that manage sequencing at scale.', + impact: 'medium', + sources: [ + { name: 'AI Futures Model', url: 'https://www.aifuturesmodel.com/about', confidence: 0.67 }, + ], + }, + { + id: 'edu-2029-04-immersive-learning-mainstream', + domain: 'tech', + month: 3, + year: 2029, + probability: 0.74, + title: 'VR and immersive learning become mainstream', + description: + 'Virtual and hybrid instruction expands into rich simulations and labs as consumer-grade VR/AR hardware matures and AI-generated content pipelines make scenario authoring cheap.', + impact: 'high', + sources: [ + { name: 'FutureTimeline', url: 'https://futuretimeline.net/21stcentury/2030.htm', confidence: 0.73 }, + ], + }, + { + id: 'edu-2029-11-dynamic-content-platforms', + domain: 'economic', + month: 10, + year: 2029, + probability: 0.7, + title: 'Dynamic content platforms replace static textbooks', + description: + 'Following mastery-map curricula, schools license adaptive platforms that generate local, level-appropriate materials and continuously refresh content instead of buying fixed textbooks.', + impact: 'medium', + sources: [ + { name: 'FutureTimeline', url: 'https://futuretimeline.net/21stcentury/2030.htm', confidence: 0.62 }, + ], + }, + { + id: 'edu-2029-12-private-tutoring-disrupted', + domain: 'economic', + month: 11, + year: 2029, + probability: 0.72, + title: 'Private tutoring market is disrupted by AI baselines', + description: + 'Affordable AI tutoring substitutes most entry-level private tutoring once dynamic content platforms and pervasive AI coaches are established, pushing human tutors toward high-touch, premium niches.', + impact: 'medium', + sources: [ + { name: 'AI Futures Model', url: 'https://www.aifuturesmodel.com/about', confidence: 0.65 }, + ], + }, + { + id: 'edu-2031-02-microcredentials-rise', + domain: 'governance', + month: 1, + year: 2031, + probability: 0.74, + title: 'Micro-credentials and verified skills eclipse many degrees', + description: + 'Stackable, tightly mapped credentials gain status as autonomous course-building systems enable rapid updates and clearer links to demonstrable skills, reinforced by AI-rich assessments.', + impact: 'high', + sources: [ + { name: 'LessWrong', url: 'https://www.lesswrong.com/posts/YABG5JmztGGPwNFq2/ai-futures-timelines-and-takeoff-model-dec-2025-update', confidence: 0.74 }, + ], + }, + { + id: 'edu-2032-01-continuous-verification', + domain: 'governance', + month: 0, + year: 2032, + probability: 0.72, + title: 'Continuous identity and provenance checks reshape assessment', + description: + 'Assessment relies on lightweight identity verification, oral defenses, and practical tasks, reducing reliance on unproctored essays as AI capabilities and autonomous courseware mature.', + impact: 'medium', + sources: [ + { name: 'AI Futures Model', url: 'https://www.aifuturesmodel.com/about', confidence: 0.68 }, + ], + }, + { + id: 'edu-2033-03-degree-decomposition', + domain: 'social', + month: 2, + year: 2033, + probability: 0.7, + title: 'Traditional degrees decompose into verified cores plus specializations', + description: + 'Universities restructure programs into verified foundational cores paired with stackable specializations tied to labor-market signals and rapid refresh cycles, accelerating after continuous verification becomes standard.', + impact: 'medium', + sources: [ + { name: 'LessWrong', url: 'https://www.lesswrong.com/posts/YABG5JmztGGPwNFq2/ai-futures-timelines-and-takeoff-model-dec-2025-update', confidence: 0.67 }, + ], + }, + { + id: 'edu-2034-02-ambient-learning-layer', + domain: 'individual', + month: 1, + year: 2034, + probability: 0.72, + title: 'Ambient lifelong learning layers into daily tools', + description: + 'Workflows and consumer tools embed coaching, just-in-time explanations, and simulations so learning becomes a continual background activity across life, extending the AI tutor baseline to adults.', + impact: 'medium', + sources: [ + { name: 'FutureTimeline', url: 'https://futuretimeline.net/21stcentury/2030.htm', confidence: 0.66 }, + ], + }, + { + id: 'edu-2035-07-human-centered-schools', + domain: 'social', + month: 6, + year: 2035, + probability: 0.7, + title: 'Schools center community, leadership, and well-being over content', + description: + 'Education systems shift toward cultivating collaboration, mentorship, and civic norms while AI handles most instruction, reflecting education as social development and curation.', + impact: 'high', + sources: [ + { name: 'AI 2027', url: 'https://ai-2027.com/summary', confidence: 0.64 }, + { name: 'AI Futures Model', url: 'https://www.aifuturesmodel.com/about', confidence: 0.62 }, + ], + }, ]; +function dedupePredictions(predictions: Prediction[]): Prediction[] { + const ids = new Set(); + const contentKeys = new Set(); + + return predictions.filter((prediction) => { + const idKey = prediction.id.trim(); + const contentKey = `${prediction.title.trim()}|${prediction.year}|${prediction.month}|${prediction.domain}`; + + if (ids.has(idKey) || contentKeys.has(contentKey)) { + return false; + } + + ids.add(idKey); + contentKeys.add(contentKey); + return true; + }); +} + +const PREDICTION_DATABASE: Prediction[] = dedupePredictions(RAW_PREDICTIONS); + +export function getPredictionYearRange(): { minYear: number; maxYear: number } { + const years = PREDICTION_DATABASE.map((p) => p.year); + return { + minYear: Math.min(...years), + maxYear: Math.max(...years), + }; +} + export function generateTimelineData(startYear: number, endYear: number): MonthData[] { const data: MonthData[] = []; diff --git a/src/lib/spark-llm.ts b/src/lib/spark-llm.ts new file mode 100644 index 0000000..b5605af --- /dev/null +++ b/src/lib/spark-llm.ts @@ -0,0 +1,75 @@ +const normalizeContentArray = (content: unknown): string | null => { + if (!Array.isArray(content)) return null; + + const combined = content + .map((part) => { + if (typeof part === 'string') return part; + if (part && typeof (part as any).text === 'string') return (part as any).text; + if (part && typeof (part as any).content === 'string') return (part as any).content; + return ''; + }) + .join('') + .trim(); + + return combined || null; +}; + +const normalizeSparkOutput = (raw: unknown): string | null => { + if (typeof raw === 'string') return raw.trim(); + if (!raw || typeof raw !== 'object') return null; + + const output = + (raw as any).output || + (raw as any).text || + (raw as any).content || + (raw as any).message?.content || + (raw as any).choices?.[0]?.message?.content; + + if (typeof output === 'string') return output.trim(); + + const normalizedFromArray = normalizeContentArray(output); + if (normalizedFromArray) return normalizedFromArray; + + const messageContent = (raw as any).message?.content; + const normalizedFromMessageArray = normalizeContentArray(messageContent); + if (normalizedFromMessageArray) return normalizedFromMessageArray; + + return null; +}; + +export async function generateSparkText(prompt: string, model = 'gpt-4o'): Promise { + if (typeof window === 'undefined') return null; + + const spark = (window as any)?.spark; + + if (!spark) return null; + + // Prefer new actions-based API when available + const actionsLlm = spark.actions?.generateText; + if (typeof actionsLlm === 'function') { + try { + const response = await actionsLlm({ + model, + messages: [{ role: 'user', content: prompt }], + }); + + const normalized = normalizeSparkOutput(response); + if (normalized) return normalized; + } catch (error) { + console.error('Spark actions.generateText failed', error); + } + } + + const legacyLlm = spark.llm; + if (typeof legacyLlm === 'function') { + try { + const legacyResponse = await legacyLlm(prompt, model); + const normalized = normalizeSparkOutput(legacyResponse); + if (normalized) return normalized; + } catch (error) { + console.error('Spark legacy llm failed', error); + } + } + + return null; +} diff --git a/src/lib/supabase-client.ts b/src/lib/supabase-client.ts new file mode 100644 index 0000000..94e3898 --- /dev/null +++ b/src/lib/supabase-client.ts @@ -0,0 +1,113 @@ +import type { TechTreeState } from './types'; + +const LOCAL_USER_KEY = 'rehoboam-user-instance'; + +interface SupabaseConfig { + url: string; + anonKey: string; +} + +interface TechTreeStateRow { + user_id: string; + node_id: string; + status: TechTreeState['status']; + effective_year?: number | null; + effective_month?: number | null; + updated_at?: string | number | null; +} + +const ensureWindow = () => (typeof window === 'undefined' ? null : window as any); + +export function getSupabaseConfig(): SupabaseConfig | null { + const w = ensureWindow(); + const url = import.meta?.env?.VITE_SUPABASE_URL || w?.env?.SUPABASE_URL; + const anonKey = import.meta?.env?.VITE_SUPABASE_ANON_KEY || w?.env?.SUPABASE_ANON_KEY; + + if (!url || !anonKey) return null; + return { url, anonKey }; +} + +export function getUserInstanceId(): string | null { + const w = ensureWindow(); + if (!w) return null; + + const sparkUser = w.spark?.user; + if (sparkUser?.id) return `spark-${sparkUser.id}`; + if (sparkUser?.login) return `spark-login-${sparkUser.login}`; + + try { + const existing = w.localStorage?.getItem(LOCAL_USER_KEY); + if (existing) return existing; + + const fallback = `local-${(w.crypto?.randomUUID?.() || Math.random().toString(36).slice(2))}`; + w.localStorage?.setItem(LOCAL_USER_KEY, fallback); + return fallback; + } catch { + return `local-${Math.random().toString(36).slice(2)}`; + } +} + +function mapRowToState(row: TechTreeStateRow): TechTreeState { + return { + nodeId: row.node_id, + status: row.status, + effectiveYear: row.effective_year ?? undefined, + effectiveMonth: row.effective_month ?? undefined, + updatedAt: row.updated_at ? new Date(row.updated_at).getTime() : Date.now(), + }; +} + +export async function fetchTechTreeStates( + config: SupabaseConfig, + userId: string +): Promise { + const response = await fetch( + `${config.url}/rest/v1/tech_tree_states?user_id=eq.${encodeURIComponent(userId)}&select=node_id,status,effective_year,effective_month,updated_at`, + { + headers: { + apikey: config.anonKey, + Authorization: `Bearer ${config.anonKey}`, + Accept: 'application/json', + }, + } + ); + + if (!response.ok) { + const text = await response.text(); + throw new Error(text || 'Failed to load saved selections'); + } + + const rows = (await response.json()) as TechTreeStateRow[]; + return rows.map(mapRowToState); +} + +export async function upsertTechTreeState( + config: SupabaseConfig, + userId: string, + state: TechTreeState +): Promise { + const payload = [{ + user_id: userId, + node_id: state.nodeId, + status: state.status, + effective_year: state.effectiveYear ?? null, + effective_month: state.effectiveMonth ?? null, + updated_at: new Date(state.updatedAt).toISOString(), + }]; + + const response = await fetch(`${config.url}/rest/v1/tech_tree_states`, { + method: 'POST', + headers: { + apikey: config.anonKey, + Authorization: `Bearer ${config.anonKey}`, + 'Content-Type': 'application/json', + Prefer: 'return=minimal,resolution=merge-duplicates', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(text || 'Failed to save selection'); + } +} diff --git a/src/lib/tech-tree.ts b/src/lib/tech-tree.ts index 3771b58..e4ddc77 100644 --- a/src/lib/tech-tree.ts +++ b/src/lib/tech-tree.ts @@ -1,4 +1,4 @@ -import type { TechTreeNode } from './types'; +import type { TechTreeNode, TechTreeState, TechTreeStatus } from './types'; export const TECH_TREE_NODES: TechTreeNode[] = [ { @@ -542,3 +542,40 @@ export function getAllNodesGroupedByCategory() { export function getCumulativeTechNodes(year: number, month: number): TechTreeNode[] { return getNodesUpToMonth(year, month); } + +const toMonthIndex = (year: number, month: number) => year * 12 + month; + +export function getNodeStatusForDate( + states: TechTreeState[] | undefined, + nodeId: string, + year: number, + month: number, + fallback: TechTreeStatus = 'not-started' +): TechTreeStatus { + const targetIndex = toMonthIndex(year, month); + + const applicableStates = (states || []).filter((state) => { + if (state.nodeId !== nodeId) return false; + if (state.effectiveYear == null || state.effectiveMonth == null) return true; + + return toMonthIndex(state.effectiveYear, state.effectiveMonth) <= targetIndex; + }); + + if (!applicableStates.length) return fallback; + + const latest = applicableStates.sort((a, b) => { + const aIndex = + a.effectiveYear == null || a.effectiveMonth == null + ? -Infinity + : toMonthIndex(a.effectiveYear, a.effectiveMonth); + const bIndex = + b.effectiveYear == null || b.effectiveMonth == null + ? -Infinity + : toMonthIndex(b.effectiveYear, b.effectiveMonth); + + if (aIndex !== bIndex) return aIndex - bIndex; + return (a.updatedAt || 0) - (b.updatedAt || 0); + })[applicableStates.length - 1]; + + return latest.status; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 0cad6fa..b4c4672 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -96,5 +96,7 @@ export interface TechTreeNode { export interface TechTreeState { nodeId: string; status: TechTreeStatus; + effectiveYear?: number; + effectiveMonth?: number; updatedAt: number; }