import { useState, useEffect, useCallback } from 'react'
import { useSearchParams } from 'react-router-dom'
import { ChevronLeft, ChevronRight, Sparkles, Calendar, Clock, Layers, RefreshCw, List, Cpu } from 'lucide-react'
import axios from 'axios'
import clsx from 'clsx'
import TimelineVertical from '../components/TimelineVertical'
import EventManager from '../components/EventManager'
const api = axios.create({ baseURL: '/api' })
interface MarketEvent {
id: number
name: string
start_date: string
end_date: string | null
level: 'long' | 'medium' | 'short'
category: string
description: string
market_impact: string
affected_assets: string
impact_score: number
}
interface DayContext {
ref_date: string
events: {
long: MarketEvent | null
medium: MarketEvent | null
short: MarketEvent | null
}
long_commentary: string
medium_commentary: string
short_commentary: string
evidence_headlines: string[]
generated_at: string | null
has_commentary: boolean
}
const LEVEL_CONFIG = {
long: {
label: 'Long Terme',
sublabel: 'Régime structurel',
color: 'text-violet-400',
border: 'border-violet-700/50',
bg: 'bg-violet-950/30',
dot: 'bg-violet-500',
badge: 'bg-violet-900/50 text-violet-300 border-violet-700/50',
},
medium: {
label: 'Moyen Terme',
sublabel: 'Régime courant',
color: 'text-blue-400',
border: 'border-blue-700/50',
bg: 'bg-blue-950/30',
dot: 'bg-blue-500',
badge: 'bg-blue-900/50 text-blue-300 border-blue-700/50',
},
short: {
label: 'Court Terme',
sublabel: 'Catalyseur récent',
color: 'text-emerald-400',
border: 'border-emerald-700/50',
bg: 'bg-emerald-950/30',
dot: 'bg-emerald-500',
badge: 'bg-emerald-900/50 text-emerald-300 border-emerald-700/50',
},
}
function daysSince(refDate: string, startDate: string): number {
const d0 = new Date(startDate)
const d1 = new Date(refDate)
return Math.floor((d1.getTime() - d0.getTime()) / 86_400_000)
}
function fmtDate(d: string): string {
return new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })
}
function offsetDate(d: string, days: number): string {
const dt = new Date(d)
dt.setDate(dt.getDate() + days)
return dt.toISOString().split('T')[0]
}
function todayStr(): string {
return new Date().toISOString().split('T')[0]
}
function ContextPanel({
level,
event,
commentary,
refDate,
}: {
level: 'long' | 'medium' | 'short'
event: MarketEvent | null
commentary: string
refDate: string
}) {
const cfg = LEVEL_CONFIG[level]
return (
{/* Header */}
{cfg.label}
{cfg.sublabel}
{event ? (
<>
{/* Event card */}
{event.name}
{event.category}
J+{daysSince(refDate, event.start_date)} ({fmtDate(event.start_date)})
{event.end_date && (
→ {fmtDate(event.end_date)}
)}
{!event.end_date && (
en cours
)}
{event.description}
{event.market_impact && (
{event.market_impact}
)}
{/* Affected assets */}
{event.affected_assets && (() => {
try {
const assets = JSON.parse(event.affected_assets)
if (assets.length > 0) return (
{assets.map((a: string) => (
{a}
))}
)
} catch { /* ignore */ }
return null
})()}
{/* AI Commentary */}
{commentary ? (
) : (
Commentaire IA non généré — cliquez "Générer IA"
)}
>
) : (
Aucun événement {cfg.label.toLowerCase()} actif à cette date
)}
)
}
export default function Timeline() {
const [searchParams] = useSearchParams()
const [refDate, setRefDate] = useState(() => {
const p = searchParams.get('date')
return p && p >= '2020-01-01' && p <= todayStr() ? p : todayStr()
})
const [ctx, setCtx] = useState(null)
const [allEvents, setAllEvents] = useState([])
const [loading, setLoading] = useState(false)
const [generating, setGenerating] = useState(false)
const [bootstrapped, setBootstrapped] = useState(false)
const [showManager, setShowManager] = useState(false)
const [bootstrappingMA, setBootstrappingMA] = useState(false)
const [maResult, setMaResult] = useState<{ detected: number; saved: number } | null>(null)
const fetchDay = useCallback(async (d: string) => {
setLoading(true)
try {
const res = await api.get(`/timeline/day/${d}`)
setCtx(res.data)
} catch { /* ignore */ } finally {
setLoading(false)
}
}, [])
const fetchEvents = useCallback(async () => {
try {
const res = await api.get('/timeline/events')
setAllEvents(res.data)
} catch { /* ignore */ }
}, [])
const bootstrap = useCallback(async () => {
try {
await api.post('/timeline/bootstrap')
setBootstrapped(true)
await fetchEvents()
await fetchDay(refDate)
} catch { /* ignore */ }
}, [refDate, fetchDay, fetchEvents])
const generateAI = useCallback(async () => {
setGenerating(true)
try {
const res = await api.post(`/timeline/generate/${refDate}`)
const data = res.data
setCtx(prev => prev ? {
...prev,
long_commentary: data.long_commentary,
medium_commentary: data.medium_commentary,
short_commentary: data.short_commentary,
has_commentary: true,
} : prev)
} catch { /* ignore */ } finally {
setGenerating(false)
}
}, [refDate])
useEffect(() => {
fetchEvents()
}, [fetchEvents])
useEffect(() => {
fetchDay(refDate)
}, [refDate, fetchDay])
// Auto-bootstrap on first load if no events
useEffect(() => {
if (!bootstrapped && allEvents.length === 0) {
bootstrap()
}
}, [allEvents.length, bootstrapped, bootstrap])
const bootstrapMA = useCallback(async () => {
setBootstrappingMA(true)
setMaResult(null)
try {
const res = await api.post('/timeline/bootstrap-ma')
setMaResult({ detected: res.data.detected, saved: res.data.saved })
await fetchEvents()
} catch { /* ignore */ } finally {
setBootstrappingMA(false)
}
}, [fetchEvents])
const navigate = (days: number) => setRefDate(prev => offsetDate(prev, days))
return (
{/* Header */}
Timeline Navigator
Contexte historique 3 temporalités — COVID (2020) → aujourd'hui
{maResult && (
{maResult.saved} périodes ajoutées
)}
{/* Date Navigator */}
{/* Prev buttons */}
{/* Date input */}
setRefDate(e.target.value)}
className="bg-dark-700 border border-slate-700/40 rounded-lg px-3 py-1.5 text-sm text-white focus:outline-none focus:border-violet-500/50"
/>
{fmtDate(refDate)}
{/* Next buttons */}
{/* Generate AI */}
{/* Vertical timeline */}
{allEvents.length > 0 && (
)}
{/* 3 context panels */}
{loading ? (
Chargement du contexte...
) : ctx ? (
) : (
Aucun contexte disponible
)}
{/* Event Manager */}
{showManager && (
)}
)
}