mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-22 00:48:06 +00:00
feat: calendar prev week, fullscreen mode, AI weekly highlights
Calendar — "Semaine dernière" tab: - API now fetches from last Monday (instead of today) and tags events as "prev" - New tab in CalendarTab with last week's published events (actual values visible) - fromDate filter bypassed for prev tab; week banner styled in slate Dashboard — fullscreen: - Maximize2/Minimize2 button in header using Fullscreen API - Listens to fullscreenchange to sync icon state Report — AI "Faits marquants": - New report_highlights mode in /api/narrative: reads published events from prev+current week + drivers, generates 3 structured highlights via Groq - AiHighlightsButton next to "Faits marquants de la semaine" label - Parses AI response (-- delimited) into title+body Theme objects Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ce53f97b5b
commit
a3d847ce59
@@ -32,7 +32,7 @@ export interface CalendarEvent {
|
|||||||
forecast: string | null;
|
forecast: string | null;
|
||||||
previous: string | null;
|
previous: string | null;
|
||||||
isPublished: boolean;
|
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
|
source: "ff" | "fred"; // source de la donnée
|
||||||
groupKey: string | null;
|
groupKey: string | null;
|
||||||
isGroupParent: boolean;
|
isGroupParent: boolean;
|
||||||
@@ -322,7 +322,17 @@ export async function GET() {
|
|||||||
const fredKey = process.env.FRED_API_KEY;
|
const fredKey = process.env.FRED_API_KEY;
|
||||||
const { nextMonday, next2Monday } = getWeekBounds();
|
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();
|
const toDateObj = new Date();
|
||||||
toDateObj.setDate(toDateObj.getDate() + 14);
|
toDateObj.setDate(toDateObj.getDate() + 14);
|
||||||
const toDate = toDateObj.toISOString().slice(0, 10);
|
const toDate = toDateObj.toISOString().slice(0, 10);
|
||||||
@@ -347,9 +357,10 @@ export async function GET() {
|
|||||||
|
|
||||||
const events: CalendarEvent[] = [];
|
const events: CalendarEvent[] = [];
|
||||||
|
|
||||||
const weekOf = (date: Date): "current" | "next" | "next2" =>
|
const weekOf = (date: Date): "prev" | "current" | "next" | "next2" =>
|
||||||
date >= next2Monday ? "next2" :
|
date >= next2Monday ? "next2" :
|
||||||
date >= nextMonday ? "next" : "current";
|
date >= nextMonday ? "next" :
|
||||||
|
date >= thisMonday ? "current" : "prev";
|
||||||
|
|
||||||
if (useScraping) {
|
if (useScraping) {
|
||||||
// ── BASE : TE HTML ────────────────────────────────────────────────────────
|
// ── BASE : TE HTML ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let body: {
|
let body: {
|
||||||
mode: "cb_analysis" | "expert_opinion" | "summary" | "divergence" | "report_ccy";
|
mode: "cb_analysis" | "expert_opinion" | "summary" | "divergence" | "report_ccy" | "report_highlights";
|
||||||
currency?: string;
|
currency?: string;
|
||||||
data?: unknown;
|
data?: unknown;
|
||||||
userInput?: string;
|
userInput?: string;
|
||||||
@@ -94,6 +94,41 @@ Rédige un paragraphe fluide de 80 à 120 mots, style analyste macro professionn
|
|||||||
break;
|
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":
|
case "summary":
|
||||||
default:
|
default:
|
||||||
userMessage = `Génère une synthèse macro hebdomadaire pour ${currency} basée sur ces données :
|
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: "system", content: SYSTEM_PROMPT },
|
||||||
{ role: "user", content: userMessage },
|
{ role: "user", content: userMessage },
|
||||||
],
|
],
|
||||||
max_tokens: 300,
|
max_tokens: mode === "report_highlights" ? 500 : 300,
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+24
-1
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
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 { CURRENCIES, CURRENCY_META } from "@/lib/constants";
|
||||||
import type { Currency, DriverData, SentimentEntry, CotEntry } from "@/lib/types";
|
import type { Currency, DriverData, SentimentEntry, CotEntry } from "@/lib/types";
|
||||||
import type { RateProbData } from "@/lib/rateprobability";
|
import type { RateProbData } from "@/lib/rateprobability";
|
||||||
@@ -40,6 +40,7 @@ export default function Dashboard() {
|
|||||||
const [activeDivergences, setActiveDivergences] = useState<{ currency: Currency; score: number }[]>([]);
|
const [activeDivergences, setActiveDivergences] = useState<{ currency: Currency; score: number }[]>([]);
|
||||||
const [driversFromCache, setDriversFromCache] = useState(false);
|
const [driversFromCache, setDriversFromCache] = useState(false);
|
||||||
const [driversCacheAge, setDriversCacheAge] = useState<string | null>(null);
|
const [driversCacheAge, setDriversCacheAge] = useState<string | null>(null);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
// ── Sentiment multi-paires Myfxbook → {CCY: {longPct, shortPct, pair}} ──────
|
// ── Sentiment multi-paires Myfxbook → {CCY: {longPct, shortPct, pair}} ──────
|
||||||
// Pour chaque devise, on calcule le % "long CCY" en moyenne pondérée (par volume)
|
// 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();
|
if (activeTab === "cot" && !cotHistory) refreshCotHistory();
|
||||||
}, [activeTab, 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) => {
|
const handleDivergenceUpdate = useCallback((currency: Currency, score: number) => {
|
||||||
setActiveDivergences((prev) => {
|
setActiveDivergences((prev) => {
|
||||||
const filtered = prev.filter((d) => d.currency !== currency);
|
const filtered = prev.filter((d) => d.currency !== currency);
|
||||||
@@ -309,6 +324,14 @@ export default function Dashboard() {
|
|||||||
<RefreshCw size={13} className={loading ? "animate-spin" : ""} />
|
<RefreshCw size={13} className={loading ? "animate-spin" : ""} />
|
||||||
{loading ? "Chargement…" : "Rafraîchir"}
|
{loading ? "Chargement…" : "Rafraîchir"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
title={isFullscreen ? "Quitter le plein écran" : "Plein écran"}
|
||||||
|
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 transition-colors border border-slate-800 hover:border-slate-600 rounded-md px-2 py-1"
|
||||||
|
>
|
||||||
|
{isFullscreen ? <Minimize2 size={13} /> : <Maximize2 size={13} />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ function nextMonday(): Date {
|
|||||||
|
|
||||||
function getWeekBounds() {
|
function getWeekBounds() {
|
||||||
const nm = nextMonday();
|
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 currentStart = new Date(nm); currentStart.setDate(nm.getDate() - 7);
|
||||||
const currentEnd = new Date(nm); currentEnd.setDate(nm.getDate() - 1);
|
const currentEnd = new Date(nm); currentEnd.setDate(nm.getDate() - 1);
|
||||||
const nextEnd = new Date(nm); nextEnd.setDate(nm.getDate() + 6);
|
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 next2End = new Date(nm); next2End.setDate(nm.getDate() + 13);
|
||||||
const fmt = (d: Date) => d.toLocaleDateString("fr-FR", { day: "numeric", month: "short" });
|
const fmt = (d: Date) => d.toLocaleDateString("fr-FR", { day: "numeric", month: "short" });
|
||||||
return {
|
return {
|
||||||
|
prevWeekLabel: `${fmt(prevStart)} – ${fmt(prevEnd)}`,
|
||||||
currentWeekLabel: `${fmt(currentStart)} – ${fmt(currentEnd)}`,
|
currentWeekLabel: `${fmt(currentStart)} – ${fmt(currentEnd)}`,
|
||||||
nextWeekLabel: `${fmt(nm)} – ${fmt(nextEnd)}`,
|
nextWeekLabel: `${fmt(nm)} – ${fmt(nextEnd)}`,
|
||||||
next2WeekLabel: `${fmt(next2Start)} – ${fmt(next2End)}`,
|
next2WeekLabel: `${fmt(next2Start)} – ${fmt(next2End)}`,
|
||||||
@@ -151,7 +154,7 @@ function EventRow({ ev, isChild, expanded, onToggle }: {
|
|||||||
|
|
||||||
// ── Main ──────────────────────────────────────────────────────────────────────
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type WeekTab = "current" | "next" | "next2" | "all";
|
type WeekTab = "prev" | "current" | "next" | "next2" | "all";
|
||||||
|
|
||||||
export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
||||||
const [filterCcy, setFilterCcy] = useState<Currency | "ALL">("ALL");
|
const [filterCcy, setFilterCcy] = useState<Currency | "ALL">("ALL");
|
||||||
@@ -160,7 +163,7 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
|||||||
const [weekTab, setWeekTab] = useState<WeekTab>("all");
|
const [weekTab, setWeekTab] = useState<WeekTab>("all");
|
||||||
const [fromDate, setFromDate] = useState<string>(todayIso());
|
const [fromDate, setFromDate] = useState<string>(todayIso());
|
||||||
|
|
||||||
const { currentWeekLabel, nextWeekLabel, next2StartLabel, nextMondayIso } = useMemo(getWeekBounds, []);
|
const { prevWeekLabel, currentWeekLabel, nextWeekLabel, next2StartLabel, nextMondayIso } = useMemo(getWeekBounds, []);
|
||||||
void nextMondayIso;
|
void nextMondayIso;
|
||||||
|
|
||||||
const toggle = (groupKey: string) =>
|
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 (filterCcy !== "ALL" && ev.currency !== filterCcy) return false;
|
||||||
if (!showLow && ev.impact === "low") return false;
|
if (!showLow && ev.impact === "low") return false;
|
||||||
if (ev.isGroupChild && ev.groupKey && !expanded.has(ev.groupKey)) 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 === "current" && ev.week !== "current") return false;
|
||||||
if (weekTab === "next" && ev.week !== "next") return false;
|
if (weekTab === "next" && ev.week !== "next") return false;
|
||||||
if (weekTab === "next2" && ev.week !== "next2") 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;
|
return true;
|
||||||
}), [events, filterCcy, showLow, expanded, weekTab, fromDate]);
|
}), [events, filterCcy, showLow, expanded, weekTab, fromDate]);
|
||||||
|
|
||||||
@@ -190,6 +195,7 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
|||||||
}
|
}
|
||||||
days.sort();
|
days.sort();
|
||||||
|
|
||||||
|
const countPrev = events.filter(e => e.week === "prev").length;
|
||||||
const countCurrent = events.filter(e => e.week === "current").length;
|
const countCurrent = events.filter(e => e.week === "current").length;
|
||||||
const countNext = events.filter(e => e.week === "next").length;
|
const countNext = events.filter(e => e.week === "next").length;
|
||||||
const countNext2 = events.filter(e => e.week === "next2").length;
|
const countNext2 = events.filter(e => e.week === "next2").length;
|
||||||
@@ -219,10 +225,11 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
|||||||
{/* Onglets semaine */}
|
{/* Onglets semaine */}
|
||||||
<div className="flex gap-0 border-b border-slate-800 bg-slate-900/40">
|
<div className="flex gap-0 border-b border-slate-800 bg-slate-900/40">
|
||||||
{([
|
{([
|
||||||
["all", "Tout", null, null],
|
["all", "Tout", null, null],
|
||||||
["current", "Sem. en cours", currentWeekLabel, countCurrent],
|
["prev", "Sem. dernière", prevWeekLabel, countPrev],
|
||||||
["next", "Sem. prochaine", nextWeekLabel, countNext],
|
["current", "Sem. en cours", currentWeekLabel, countCurrent],
|
||||||
["next2", "Sem. +2 et +", `${next2StartLabel} et +`, countNext2],
|
["next", "Sem. prochaine", nextWeekLabel, countNext],
|
||||||
|
["next2", "Sem. +2 et +", `${next2StartLabel} et +`, countNext2],
|
||||||
] as [WeekTab, string, string | null, number | null][]).map(([tab, label, sub, count]) => {
|
] as [WeekTab, string, string | null, number | null][]).map(([tab, label, sub, count]) => {
|
||||||
const isActive = weekTab === tab;
|
const isActive = weekTab === tab;
|
||||||
const disabled = tab === "next" && !nextWeekAvail && countNext === 0;
|
const disabled = tab === "next" && !nextWeekAvail && countNext === 0;
|
||||||
@@ -353,14 +360,18 @@ export default function CalendarTab({ events, loading, nextWeekAvail }: Props) {
|
|||||||
if (weekTab === "all" && w !== lastWeek) {
|
if (weekTab === "all" && w !== lastWeek) {
|
||||||
lastWeek = w;
|
lastWeek = w;
|
||||||
const weekBanners: Record<string, string> = {
|
const weekBanners: Record<string, string> = {
|
||||||
|
prev: `Semaine dernière — ${prevWeekLabel}`,
|
||||||
current: `Semaine en cours — ${currentWeekLabel}`,
|
current: `Semaine en cours — ${currentWeekLabel}`,
|
||||||
next: `Semaine prochaine — ${nextWeekLabel}`,
|
next: `Semaine prochaine — ${nextWeekLabel}`,
|
||||||
next2: `À partir du ${next2StartLabel} — réunions BC + données`,
|
next2: `À partir du ${next2StartLabel} — réunions BC + données`,
|
||||||
};
|
};
|
||||||
const isNext2 = w === "next2";
|
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(
|
rows.push(
|
||||||
<tr key={`wsep_${w}`} className={isNext2 ? "bg-amber-500/15" : "bg-indigo-500/15"}>
|
<tr key={`wsep_${w}`} className={bannerCls}>
|
||||||
<td colSpan={7} className={`px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest ${isNext2 ? "text-amber-400" : "text-indigo-400"}`}>
|
<td colSpan={7} className={`px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest ${textCls}`}>
|
||||||
📅 {weekBanners[w] ?? w}
|
📅 {weekBanners[w] ?? w}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -84,6 +84,69 @@ function Pct({ val }: { val: string }) {
|
|||||||
return <span className={`font-mono font-bold text-sm ${c}`}>{n > 0 ? "+" : ""}{n.toFixed(1)}%</span>;
|
return <span className={`font-mono font-bold text-sm ${c}`}>{n > 0 ? "+" : ""}{n.toFixed(1)}%</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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 (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button onClick={run} disabled={loading}
|
||||||
|
className={`no-print flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[10px] font-semibold transition-all ${
|
||||||
|
done ? "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30" :
|
||||||
|
loading ? "bg-sky-500/10 text-sky-400 border border-sky-500/20 cursor-wait" :
|
||||||
|
"bg-sky-500/15 text-sky-400 border border-sky-500/25 hover:bg-sky-500/25"
|
||||||
|
}`}>
|
||||||
|
{loading ? <Loader2 size={10} className="animate-spin" />
|
||||||
|
: done ? <Check size={10} />
|
||||||
|
: <Sparkles size={10} />}
|
||||||
|
{loading ? "Génération…" : done ? "Injectés !" : "Générer avec IA"}
|
||||||
|
</button>
|
||||||
|
{err && <span className="text-[9px] text-red-400 truncate max-w-[140px]" title={err}>⚠ {err}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Bouton Groq par devise ────────────────────────────────────────────────────
|
// ── Bouton Groq par devise ────────────────────────────────────────────────────
|
||||||
function AiButton({ ccy, weekFrom, weekTo, pct, cotHistory, onResult }: {
|
function AiButton({ ccy, weekFrom, weekTo, pct, cotHistory, onResult }: {
|
||||||
ccy: string; weekFrom: string; weekTo: string; pct: string;
|
ccy: string; weekFrom: string; weekTo: string; pct: string;
|
||||||
@@ -309,7 +372,16 @@ export default function ReportTab({ calEvents, drivers, cotHistory }: Props) {
|
|||||||
|
|
||||||
{/* Thèmes clés */}
|
{/* Thèmes clés */}
|
||||||
<div className="space-y-2 mt-4">
|
<div className="space-y-2 mt-4">
|
||||||
<p className="text-slate-600 text-[9px] uppercase tracking-widest font-semibold no-print">Faits marquants de la semaine</p>
|
<div className="flex items-center justify-between no-print">
|
||||||
|
<p className="text-slate-600 text-[9px] uppercase tracking-widest font-semibold">Faits marquants de la semaine</p>
|
||||||
|
<AiHighlightsButton
|
||||||
|
calEvents={calEvents}
|
||||||
|
drivers={drivers}
|
||||||
|
weekFrom={state.weekFrom}
|
||||||
|
weekTo={state.weekTo}
|
||||||
|
onResult={themes => upd({ themes })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{state.themes.map((theme, i) => (
|
{state.themes.map((theme, i) => (
|
||||||
<div key={i} className="group relative flex gap-3 p-3 rounded-lg bg-white/[0.03] border-l-2 border-sky-500/60">
|
<div key={i} className="group relative flex gap-3 p-3 rounded-lg bg-white/[0.03] border-l-2 border-sky-500/60">
|
||||||
<div className="flex-1 space-y-0.5">
|
<div className="flex-1 space-y-0.5">
|
||||||
|
|||||||
Reference in New Issue
Block a user