diff --git a/app/api/calendar/route.ts b/app/api/calendar/route.ts index e5a7a33..98172f3 100644 --- a/app/api/calendar/route.ts +++ b/app/api/calendar/route.ts @@ -32,7 +32,7 @@ export interface CalendarEvent { forecast: string | null; previous: string | null; isPublished: boolean; - week: "current" | "next" | "next2"; // semaine de l'événement + week: "prev" | "current" | "next" | "next2"; // semaine de l'événement source: "ff" | "fred"; // source de la donnée groupKey: string | null; isGroupParent: boolean; @@ -322,7 +322,17 @@ export async function GET() { const fredKey = process.env.FRED_API_KEY; const { nextMonday, next2Monday } = getWeekBounds(); - const fromDate = new Date().toISOString().slice(0, 10); + // Fetch depuis le lundi de la semaine précédente pour inclure "Semaine dernière" + const now = new Date(); + const dayOfWeek = now.getDay(); // 0=dim, 1=lun…6=sam + const daysToThisMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1; + const thisMonday = new Date(now); + thisMonday.setDate(now.getDate() - daysToThisMonday); + thisMonday.setHours(0, 0, 0, 0); + const lastMonday = new Date(thisMonday); + lastMonday.setDate(thisMonday.getDate() - 7); + + const fromDate = lastMonday.toISOString().slice(0, 10); const toDateObj = new Date(); toDateObj.setDate(toDateObj.getDate() + 14); const toDate = toDateObj.toISOString().slice(0, 10); @@ -347,9 +357,10 @@ export async function GET() { const events: CalendarEvent[] = []; - const weekOf = (date: Date): "current" | "next" | "next2" => + const weekOf = (date: Date): "prev" | "current" | "next" | "next2" => date >= next2Monday ? "next2" : - date >= nextMonday ? "next" : "current"; + date >= nextMonday ? "next" : + date >= thisMonday ? "current" : "prev"; if (useScraping) { // ── BASE : TE HTML ──────────────────────────────────────────────────────── diff --git a/app/api/narrative/route.ts b/app/api/narrative/route.ts index 50a14f8..193a35e 100644 --- a/app/api/narrative/route.ts +++ b/app/api/narrative/route.ts @@ -25,7 +25,7 @@ export async function POST(req: NextRequest) { } let body: { - mode: "cb_analysis" | "expert_opinion" | "summary" | "divergence" | "report_ccy"; + mode: "cb_analysis" | "expert_opinion" | "summary" | "divergence" | "report_ccy" | "report_highlights"; currency?: string; data?: unknown; userInput?: string; @@ -94,6 +94,41 @@ Rédige un paragraphe fluide de 80 à 120 mots, style analyste macro professionn break; } + case "report_highlights": { + const d = data as { + events?: Array<{ title: string; currency: string; actual?: string | null; forecast?: string | null; previous?: string | null; impact: string }>; + drivers?: { vix?: number | null; brent?: number | null; us10y?: number | null }; + weekFrom?: string; weekTo?: string; + }; + const published = (d.events ?? []) + .filter(e => e.actual && e.impact !== "low") + .slice(0, 12) + .map(e => `${e.currency} · ${e.title} : actuel=${e.actual ?? "—"} / prévu=${e.forecast ?? "—"} / précédent=${e.previous ?? "—"}`) + .join("\n"); + const weekRange = d.weekFrom && d.weekTo ? `${d.weekFrom} au ${d.weekTo}` : "de la semaine"; + userMessage = `Génère 3 faits marquants de la semaine du ${weekRange} pour un trader Forex G10. + +Données publiées cette semaine : +${published || "Aucune publication disponible"} + +Contexte marché : VIX=${d.drivers?.vix?.toFixed(1) ?? "N/D"}, Brent=$${d.drivers?.brent?.toFixed(1) ?? "N/D"}, US 10Y=${d.drivers?.us10y?.toFixed(2) ?? "N/D"}% + +Pour chaque fait marquant, fournis : +- TITRE (6 mots max, en majuscules) +- DESCRIPTION (30-40 mots) : chiffre clé, surprise vs consensus, impact sur les devises concernées + +Formate ta réponse EXACTEMENT ainsi (3 blocs, séparés par --) : +TITRE1 +Description1 +-- +TITRE2 +Description2 +-- +TITRE3 +Description3`; + break; + } + case "summary": default: userMessage = `Génère une synthèse macro hebdomadaire pour ${currency} basée sur ces données : @@ -109,7 +144,7 @@ Résumé en 3 points : situation actuelle, signal directionnel, risque principal { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: userMessage }, ], - max_tokens: 300, + max_tokens: mode === "report_highlights" ? 500 : 300, temperature: 0.3, }); diff --git a/app/page.tsx b/app/page.tsx index 7956cea..c4c3a38 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState, useCallback } from "react"; -import { RefreshCw, Zap, Database, Activity } from "lucide-react"; +import { RefreshCw, Zap, Database, Activity, Maximize2, Minimize2 } from "lucide-react"; import { CURRENCIES, CURRENCY_META } from "@/lib/constants"; import type { Currency, DriverData, SentimentEntry, CotEntry } from "@/lib/types"; import type { RateProbData } from "@/lib/rateprobability"; @@ -40,6 +40,7 @@ export default function Dashboard() { const [activeDivergences, setActiveDivergences] = useState<{ currency: Currency; score: number }[]>([]); const [driversFromCache, setDriversFromCache] = useState(false); const [driversCacheAge, setDriversCacheAge] = useState(null); + const [isFullscreen, setIsFullscreen] = useState(false); // ── Sentiment multi-paires Myfxbook → {CCY: {longPct, shortPct, pair}} ────── // Pour chaque devise, on calcule le % "long CCY" en moyenne pondérée (par volume) @@ -255,6 +256,20 @@ export default function Dashboard() { if (activeTab === "cot" && !cotHistory) refreshCotHistory(); }, [activeTab, cotHistory, refreshCotHistory]); + const toggleFullscreen = useCallback(() => { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen().then(() => setIsFullscreen(true)).catch(() => {}); + } else { + document.exitFullscreen().then(() => setIsFullscreen(false)).catch(() => {}); + } + }, []); + + useEffect(() => { + const handler = () => setIsFullscreen(!!document.fullscreenElement); + document.addEventListener("fullscreenchange", handler); + return () => document.removeEventListener("fullscreenchange", handler); + }, []); + const handleDivergenceUpdate = useCallback((currency: Currency, score: number) => { setActiveDivergences((prev) => { const filtered = prev.filter((d) => d.currency !== currency); @@ -309,6 +324,14 @@ export default function Dashboard() { {loading ? "Chargement…" : "Rafraîchir"} + + diff --git a/components/CalendarTab.tsx b/components/CalendarTab.tsx index 952676c..013da5b 100644 --- a/components/CalendarTab.tsx +++ b/components/CalendarTab.tsx @@ -57,6 +57,8 @@ function nextMonday(): Date { function getWeekBounds() { const nm = nextMonday(); + const prevStart = new Date(nm); prevStart.setDate(nm.getDate() - 14); + const prevEnd = new Date(nm); prevEnd.setDate(nm.getDate() - 8); const currentStart = new Date(nm); currentStart.setDate(nm.getDate() - 7); const currentEnd = new Date(nm); currentEnd.setDate(nm.getDate() - 1); const nextEnd = new Date(nm); nextEnd.setDate(nm.getDate() + 6); @@ -64,6 +66,7 @@ function getWeekBounds() { const next2End = new Date(nm); next2End.setDate(nm.getDate() + 13); const fmt = (d: Date) => d.toLocaleDateString("fr-FR", { day: "numeric", month: "short" }); return { + prevWeekLabel: `${fmt(prevStart)} – ${fmt(prevEnd)}`, currentWeekLabel: `${fmt(currentStart)} – ${fmt(currentEnd)}`, nextWeekLabel: `${fmt(nm)} – ${fmt(nextEnd)}`, next2WeekLabel: `${fmt(next2Start)} – ${fmt(next2End)}`, @@ -151,7 +154,7 @@ function EventRow({ ev, isChild, expanded, onToggle }: { // ── Main ────────────────────────────────────────────────────────────────────── -type WeekTab = "current" | "next" | "next2" | "all"; +type WeekTab = "prev" | "current" | "next" | "next2" | "all"; export default function CalendarTab({ events, loading, nextWeekAvail }: Props) { const [filterCcy, setFilterCcy] = useState("ALL"); @@ -160,7 +163,7 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) { const [weekTab, setWeekTab] = useState("all"); const [fromDate, setFromDate] = useState(todayIso()); - const { currentWeekLabel, nextWeekLabel, next2StartLabel, nextMondayIso } = useMemo(getWeekBounds, []); + const { prevWeekLabel, currentWeekLabel, nextWeekLabel, next2StartLabel, nextMondayIso } = useMemo(getWeekBounds, []); void nextMondayIso; const toggle = (groupKey: string) => @@ -174,10 +177,12 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) { if (filterCcy !== "ALL" && ev.currency !== filterCcy) return false; if (!showLow && ev.impact === "low") return false; if (ev.isGroupChild && ev.groupKey && !expanded.has(ev.groupKey)) return false; + if (weekTab === "prev" && ev.week !== "prev") return false; if (weekTab === "current" && ev.week !== "current") return false; if (weekTab === "next" && ev.week !== "next") return false; if (weekTab === "next2" && ev.week !== "next2") return false; - if (isoToLocalDate(ev.date) < fromDate) return false; + // Pour "semaine dernière", ne pas filtrer par fromDate (les events sont passés) + if (weekTab !== "prev" && isoToLocalDate(ev.date) < fromDate) return false; return true; }), [events, filterCcy, showLow, expanded, weekTab, fromDate]); @@ -190,6 +195,7 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) { } days.sort(); + const countPrev = events.filter(e => e.week === "prev").length; const countCurrent = events.filter(e => e.week === "current").length; const countNext = events.filter(e => e.week === "next").length; const countNext2 = events.filter(e => e.week === "next2").length; @@ -219,10 +225,11 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) { {/* Onglets semaine */}
{([ - ["all", "Tout", null, null], - ["current", "Sem. en cours", currentWeekLabel, countCurrent], - ["next", "Sem. prochaine", nextWeekLabel, countNext], - ["next2", "Sem. +2 et +", `${next2StartLabel} et +`, countNext2], + ["all", "Tout", null, null], + ["prev", "Sem. dernière", prevWeekLabel, countPrev], + ["current", "Sem. en cours", currentWeekLabel, countCurrent], + ["next", "Sem. prochaine", nextWeekLabel, countNext], + ["next2", "Sem. +2 et +", `${next2StartLabel} et +`, countNext2], ] as [WeekTab, string, string | null, number | null][]).map(([tab, label, sub, count]) => { const isActive = weekTab === tab; const disabled = tab === "next" && !nextWeekAvail && countNext === 0; @@ -353,14 +360,18 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) { if (weekTab === "all" && w !== lastWeek) { lastWeek = w; const weekBanners: Record = { + prev: `Semaine dernière — ${prevWeekLabel}`, current: `Semaine en cours — ${currentWeekLabel}`, next: `Semaine prochaine — ${nextWeekLabel}`, next2: `À partir du ${next2StartLabel} — réunions BC + données`, }; const isNext2 = w === "next2"; + const isPrev = w === "prev"; + const bannerCls = isPrev ? "bg-slate-700/30" : isNext2 ? "bg-amber-500/15" : "bg-indigo-500/15"; + const textCls = isPrev ? "text-slate-500" : isNext2 ? "text-amber-400" : "text-indigo-400"; rows.push( - - + + 📅 {weekBanners[w] ?? w} diff --git a/components/ReportTab.tsx b/components/ReportTab.tsx index 6c01dc7..91eab8a 100644 --- a/components/ReportTab.tsx +++ b/components/ReportTab.tsx @@ -84,6 +84,69 @@ function Pct({ val }: { val: string }) { return {n > 0 ? "+" : ""}{n.toFixed(1)}%; } +// ── Bouton Groq "Faits marquants" ───────────────────────────────────────────── +function AiHighlightsButton({ calEvents, drivers, weekFrom, weekTo, onResult }: { + calEvents: CalendarEvent[]; + drivers: DriverData | null; + weekFrom: string; + weekTo: string; + onResult: (themes: Theme[]) => void; +}) { + const [loading, setLoading] = useState(false); + const [done, setDone] = useState(false); + const [err, setErr] = useState(""); + + const run = async () => { + setLoading(true); setErr(""); setDone(false); + try { + const prevEvents = calEvents.filter(e => e.week === "prev" || (e.isPublished && e.week === "current")); + const res = await fetch("/api/narrative", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + mode: "report_highlights", + data: { + events: prevEvents.map(e => ({ title: e.title, currency: e.currency, actual: e.actual, forecast: e.forecast, previous: e.previous, impact: e.impact })), + drivers: { vix: (drivers as { vix?: number | null } | null)?.vix, brent: (drivers as { brent?: number | null } | null)?.brent, us10y: (drivers as { us10y?: number | null } | null)?.us10y }, + weekFrom, weekTo, + }, + }), + }); + const json = await res.json(); + if (json.error) throw new Error(json.error); + const text: string = json.analysis ?? ""; + // Parse les 3 blocs séparés par "--" + const blocks = text.split(/\n--\n|^--$/m).map(b => b.trim()).filter(Boolean).slice(0, 3); + const themes: Theme[] = blocks.map(block => { + const lines = block.split("\n").map(l => l.trim()).filter(Boolean); + return { title: lines[0] ?? "", body: lines.slice(1).join(" ") }; + }); + onResult(themes); + setDone(true); + setTimeout(() => setDone(false), 3000); + } catch (e) { + setErr(String(e).replace(/^Error:\s*/i, "").slice(0, 60)); + } finally { setLoading(false); } + }; + + return ( +
+ + {err && ⚠ {err}} +
+ ); +} + // ── Bouton Groq par devise ──────────────────────────────────────────────────── function AiButton({ ccy, weekFrom, weekTo, pct, cotHistory, onResult }: { ccy: string; weekFrom: string; weekTo: string; pct: string; @@ -309,7 +372,16 @@ export default function ReportTab({ calEvents, drivers, cotHistory }: Props) { {/* Thèmes clés */}
-

Faits marquants de la semaine

+
+

Faits marquants de la semaine

+ upd({ themes })} + /> +
{state.themes.map((theme, i) => (