feat: enrich Dashboard cockpit cards with data + view toggles
- PnL card: rename to P&L, add Simulé/Portfolio toggle (localStorage), simulated shows target/stop alert counts, portfolio shows invested € + P&L € - Risk card: add asset class allocation horizontal bars (bullish=color/bearish=red) from simPortfolioRisk.concentration, conflicts badge - Dernier Cycle: full bilan layout — patterns added / scored / trades logged in 3 mini-stat boxes - Trades du cycle: show direction, ticker, strategy, PnL%, score for each trade - Top Patterns: replaces Meilleur Pattern — shows top 4 scored with rank + ticker - Top News: replaces Patterns Actifs — useGeoNews sorted by impact_score, top 3 with title + impact bar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import {
|
||||
useGeoRiskScore, useAllQuotes,
|
||||
useCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime,
|
||||
useTradeMtm, useRiskDashboard,
|
||||
useTradeMtm, useRiskDashboard, useGeoNews,
|
||||
useSimPortfolioRisk, useCycleStatus, useKnowledgeState,
|
||||
} from '../hooks/useApi'
|
||||
import { Clock, Globe, ShieldAlert, ArrowUpRight, Brain } from 'lucide-react'
|
||||
import { Clock, Globe, ShieldAlert, ArrowUpRight, Brain, Newspaper } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import clsx from 'clsx'
|
||||
import type { Quote } from '../types'
|
||||
@@ -26,6 +26,32 @@ const assetEmoji: Record<string, string> = {
|
||||
indices: '📊', forex: '💱', crypto: '₿', rates: '📉',
|
||||
}
|
||||
|
||||
const ASSET_COLORS: Record<string, string> = {
|
||||
energy: '#f97316', metals: '#eab308', agriculture: '#22c55e',
|
||||
indices: '#3b82f6', equities: '#06b6d4', forex: '#8b5cf6',
|
||||
rates: '#64748b', unknown: '#334155',
|
||||
}
|
||||
|
||||
function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) {
|
||||
const click = (v: 'simulated' | 'portfolio') => (e: React.MouseEvent) => {
|
||||
e.preventDefault(); e.stopPropagation(); onChange(v)
|
||||
}
|
||||
return (
|
||||
<div className="flex gap-0.5 bg-dark-800 rounded p-0.5 text-[9px]">
|
||||
<button onClick={click('simulated')}
|
||||
className={clsx('px-1.5 py-0.5 rounded transition-colors',
|
||||
value === 'simulated' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
|
||||
Simulé
|
||||
</button>
|
||||
<button onClick={click('portfolio')}
|
||||
className={clsx('px-1.5 py-0.5 rounded transition-colors',
|
||||
value === 'portfolio' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
|
||||
Portfolio
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function QuoteRow({ q }: { q: Quote }) {
|
||||
if (!q.price) return null
|
||||
const pos = q.change_pct >= 0
|
||||
@@ -55,6 +81,21 @@ export default function Dashboard() {
|
||||
const { data: simRisk } = useSimPortfolioRisk()
|
||||
const { data: cycleStatusData } = useCycleStatus()
|
||||
const { data: knowledgeState } = useKnowledgeState()
|
||||
const { data: geoNews } = useGeoNews()
|
||||
|
||||
const [pnlView, setPnlView] = useState<'simulated' | 'portfolio'>(() =>
|
||||
(localStorage.getItem('dash_pnl_view') as any) ?? 'simulated'
|
||||
)
|
||||
const [riskView, setRiskView] = useState<'simulated' | 'portfolio'>(() =>
|
||||
(localStorage.getItem('dash_risk_view') as any) ?? 'simulated'
|
||||
)
|
||||
|
||||
const setPnlViewPersist = (v: 'simulated' | 'portfolio') => {
|
||||
setPnlView(v); localStorage.setItem('dash_pnl_view', v)
|
||||
}
|
||||
const setRiskViewPersist = (v: 'simulated' | 'portfolio') => {
|
||||
setRiskView(v); localStorage.setItem('dash_risk_view', v)
|
||||
}
|
||||
|
||||
const allPatterns: any[] = allPatternsData ?? []
|
||||
|
||||
@@ -80,6 +121,27 @@ export default function Dashboard() {
|
||||
? Object.entries(riskScore.breakdown).map(([k, v]) => ({ subject: k.replace('_', ' '), score: v }))
|
||||
: []
|
||||
|
||||
// Last cycle run_id for filtering trades
|
||||
const lastRunId: string | null = (cycleStatusData as any)?.last_cycle?.run_id ?? null
|
||||
const mtmTrades: any[] = (tradeMtmData as any)?.trades ?? []
|
||||
const cycleTradesAll = lastRunId ? mtmTrades.filter((t: any) => t.run_id === lastRunId) : []
|
||||
const cycleTrades = cycleTradesAll.length > 0 ? cycleTradesAll : mtmTrades.slice(0, 4)
|
||||
|
||||
// Top scored patterns
|
||||
const topScoredPatterns = useMemo(() =>
|
||||
[...allPatterns]
|
||||
.filter(p => scoreMap[p.id]?.score != null)
|
||||
.sort((a, b) => (scoreMap[b.id]?.score ?? 0) - (scoreMap[a.id]?.score ?? 0))
|
||||
.slice(0, 4)
|
||||
, [allPatterns, scoreMap])
|
||||
|
||||
// Top impactful news
|
||||
const topNews = useMemo(() =>
|
||||
[...(geoNews ?? [])]
|
||||
.sort((a, b) => b.impact_score - a.impact_score)
|
||||
.slice(0, 3)
|
||||
, [geoNews])
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
@@ -231,57 +293,137 @@ export default function Dashboard() {
|
||||
{/* ── Command Center: Résumé Opérationnel ── */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
|
||||
{/* PnL Simulé */}
|
||||
{/* PnL */}
|
||||
{(() => {
|
||||
const trades: any[] = (tradeMtmData as any)?.trades ?? []
|
||||
const trades: any[] = mtmTrades
|
||||
const withPnl = trades.filter((t: any) => t.pnl_pct != null)
|
||||
const avgPnl = withPnl.length
|
||||
? withPnl.reduce((s: number, t: any) => s + t.pnl_pct, 0) / withPnl.length
|
||||
: null
|
||||
const winners = withPnl.filter((t: any) => t.pnl_pct > 0).length
|
||||
const losers = withPnl.filter((t: any) => t.pnl_pct < 0).length
|
||||
const targetHit = trades.filter((t: any) => t.alert_type === 'target_reached').length
|
||||
const stopHit = trades.filter((t: any) => t.alert_type === 'stop_loss').length
|
||||
|
||||
const pf = portfolio as any
|
||||
const pfPnlPct = pf?.unrealized_pnl_pct ?? null
|
||||
const pfPnl = pf?.unrealized_pnl ?? null
|
||||
const pfInvest = pf?.total_invested ?? null
|
||||
const pfReal = pf?.realized_pnl ?? null
|
||||
|
||||
return (
|
||||
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">📊 PnL Simulé</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className={clsx('text-2xl font-bold font-mono mt-1',
|
||||
avgPnl === null ? 'text-slate-500' : avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{trades.length} trades · <span className="text-emerald-500">{winners}✓</span>{' '}
|
||||
<span className="text-red-400">{losers}✗</span>
|
||||
<span className="section-title mb-0 text-[10px]">📊 P&L</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ViewToggle value={pnlView} onChange={setPnlViewPersist} />
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
</div>
|
||||
{pnlView === 'simulated' ? (
|
||||
<div className="flex items-end justify-between mt-1">
|
||||
<div>
|
||||
<div className={clsx('text-2xl font-bold font-mono',
|
||||
avgPnl === null ? 'text-slate-500' : avgPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{trades.length} trades · <span className="text-emerald-500">{winners}✓</span>{' '}
|
||||
<span className="text-red-400">{losers}✗</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right space-y-0.5">
|
||||
{targetHit > 0 && <div className="text-[10px] text-emerald-400">🎯 {targetHit} cible{targetHit > 1 ? 's' : ''}</div>}
|
||||
{stopHit > 0 && <div className="text-[10px] text-red-400">⛔ {stopHit} stop</div>}
|
||||
<div className="text-[10px] text-slate-600">{withPnl.length} pricés</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-end justify-between mt-1">
|
||||
<div>
|
||||
<div className={clsx('text-2xl font-bold font-mono',
|
||||
pfPnlPct === null ? 'text-slate-500' : pfPnlPct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{pfPnlPct !== null ? `${pfPnlPct >= 0 ? '+' : ''}${pfPnlPct.toFixed(1)}%` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{pf?.open_positions ?? 0} positions ouvertes
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right space-y-0.5">
|
||||
{pfInvest != null && <div className="text-[10px] text-slate-400 font-mono">{pfInvest.toFixed(0)}€ investi</div>}
|
||||
{pfPnl != null && <div className={clsx('text-[10px] font-mono font-bold', pfPnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{pfPnl >= 0 ? '+' : ''}{pfPnl.toFixed(0)}€
|
||||
</div>}
|
||||
{pfReal != null && pfReal !== 0 && <div className={clsx('text-[10px] font-mono', pfReal >= 0 ? 'text-emerald-600' : 'text-red-600')}>
|
||||
réal. {pfReal >= 0 ? '+' : ''}{pfReal.toFixed(0)}€
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Risque Simulé */}
|
||||
{/* Risk */}
|
||||
{(() => {
|
||||
const risk = simRisk as any
|
||||
const alertCount: number = risk?.alerts?.length ?? 0
|
||||
const conflictCount: number = risk?.conflicts?.length ?? 0
|
||||
const openCount: number = risk?.open_count ?? 0
|
||||
const concentration: Record<string, any> = risk?.concentration ?? {}
|
||||
const sortedClasses = Object.entries(concentration)
|
||||
.sort(([, a], [, b]) => (b as any).pct - (a as any).pct)
|
||||
.slice(0, 5)
|
||||
|
||||
const pf = portfolio as any
|
||||
return (
|
||||
<Link to="/risk" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🛡️ Risque Simulé</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
<span className="section-title mb-0 text-[10px]">🛡️ Risque</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ViewToggle value={riskView} onChange={setRiskViewPersist} />
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx('text-2xl font-bold mt-1', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}>
|
||||
<div className={clsx('text-lg font-bold mt-0.5', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}>
|
||||
{alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{openCount} positions
|
||||
{conflictCount > 0 && <span className="text-red-400 ml-1">· {conflictCount} conflit{conflictCount > 1 ? 's' : ''}</span>}
|
||||
</div>
|
||||
{/* Allocation bars */}
|
||||
{sortedClasses.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{sortedClasses.map(([cls, data]: [string, any]) => (
|
||||
<div key={cls} className="flex items-center gap-1.5">
|
||||
<span className="text-[9px] text-slate-500 w-12 truncate capitalize">{cls}</span>
|
||||
<div className="flex-1 h-2 bg-dark-700 rounded-full overflow-hidden flex">
|
||||
{data.bullish > 0 && (
|
||||
<div className="h-full rounded-l-full" title={`${data.bullish} haussier`}
|
||||
style={{ width: `${(data.bullish / openCount) * 100}%`, background: ASSET_COLORS[cls] ?? '#3b82f6', opacity: 0.9 }} />
|
||||
)}
|
||||
{data.bearish > 0 && (
|
||||
<div className="h-full rounded-r-full" title={`${data.bearish} baissier`}
|
||||
style={{ width: `${(data.bearish / openCount) * 100}%`, background: '#ef4444', opacity: 0.7 }} />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[9px] text-slate-600 w-7 text-right font-mono">{data.pct}%</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-1 text-[9px] text-slate-700 pt-0.5">
|
||||
<span className="inline-block w-2 h-1.5 rounded-sm bg-blue-500/60"></span>haussier
|
||||
<span className="inline-block w-2 h-1.5 rounded-sm bg-red-500/60 ml-1"></span>baissier
|
||||
{conflictCount > 0 && <span className="ml-auto text-red-400">{conflictCount} conflit{conflictCount > 1 ? 's' : ''}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{sortedClasses.length === 0 && (
|
||||
<div className="text-[10px] text-slate-600 mt-1">
|
||||
{openCount > 0 ? `${openCount} positions` : 'Aucune position'}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Dernier Cycle */}
|
||||
{/* Dernier Cycle — bilan complet */}
|
||||
{(() => {
|
||||
const last = (cycleStatusData as any)?.last_cycle
|
||||
const ts = last?.ts
|
||||
@@ -292,18 +434,36 @@ export default function Dashboard() {
|
||||
: elapsed < 60 ? `${elapsed}min`
|
||||
: elapsed < 1440 ? `${Math.round(elapsed / 60)}h`
|
||||
: `${Math.round(elapsed / 1440)}j`
|
||||
const patternsAdded: number = last?.patterns_added ?? 0
|
||||
const patternsScored: number = last?.patterns_scored ?? 0
|
||||
const tradesLogged: number = lastRunId
|
||||
? mtmTrades.filter((t: any) => t.run_id === lastRunId).length
|
||||
: 0
|
||||
return (
|
||||
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">🔄 Dernier Cycle</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-blue-400 mt-1">{elapsedStr}</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
{last
|
||||
? `${last.patterns_added ?? 0} loggés · géo ${last.geo_score ?? '—'}`
|
||||
: 'Aucun cycle enregistré'}
|
||||
</div>
|
||||
<div className="text-xl font-bold text-blue-400 mt-0.5 font-mono">{elapsedStr}</div>
|
||||
{last ? (
|
||||
<div className="mt-1.5 grid grid-cols-3 gap-1 text-center">
|
||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||
<div className="text-sm font-bold text-slate-200">+{patternsAdded}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight">patterns</div>
|
||||
</div>
|
||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||
<div className="text-sm font-bold text-emerald-400">{patternsScored}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight">scorés</div>
|
||||
</div>
|
||||
<div className="bg-dark-700/60 rounded px-1 py-1">
|
||||
<div className="text-sm font-bold text-blue-400">{tradesLogged}</div>
|
||||
<div className="text-[9px] text-slate-600 leading-tight">loggés</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Aucun cycle enregistré</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
@@ -364,83 +524,116 @@ export default function Dashboard() {
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Trades du dernier cycle */}
|
||||
{/* Trades du dernier cycle — détaillé */}
|
||||
{(() => {
|
||||
const last = (cycleStatusData as any)?.last_cycle
|
||||
const added: number = last?.patterns_added ?? 0
|
||||
const scored: number = last?.patterns_scored ?? 0
|
||||
const geoScore: number | null = last?.geo_score ?? null
|
||||
const mtmTrades: any[] = (tradeMtmData as any)?.trades ?? []
|
||||
const recent = mtmTrades.slice(0, 3)
|
||||
return (
|
||||
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">📥 Trades du cycle</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-emerald-400 mt-1">+{added}</div>
|
||||
{recent.length > 0 ? (
|
||||
<div className="space-y-0.5 mt-1">
|
||||
{recent.map((t: any) => (
|
||||
<div key={t.id} className="flex justify-between text-[10px]">
|
||||
<span className="text-slate-500 truncate font-mono">{t.underlying ?? '—'}</span>
|
||||
<span className="text-slate-400 shrink-0 ml-1">
|
||||
{t.direction === 'bearish' ? '🐻' : '🐂'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-xl font-bold text-emerald-400 font-mono">+{added}</div>
|
||||
{cycleTrades.length > 0 ? (
|
||||
<div className="space-y-1 mt-1.5">
|
||||
{cycleTrades.slice(0, 3).map((t: any, i) => {
|
||||
const pnl: number | null = t.pnl_pct ?? null
|
||||
const isBear = t.direction === 'bearish'
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-1.5 text-[10px]">
|
||||
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
|
||||
<span className="font-mono text-slate-300 font-semibold w-10 truncate">{t.underlying ?? '—'}</span>
|
||||
<span className="text-slate-600 truncate flex-1">{t.strategy ?? ''}</span>
|
||||
{pnl !== null && (
|
||||
<span className={clsx('font-mono font-bold shrink-0', pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
|
||||
</span>
|
||||
)}
|
||||
{t.score_at_entry != null && (
|
||||
<span className={clsx('font-mono shrink-0 text-[9px]', scoreColor(t.score_at_entry))}>
|
||||
{t.score_at_entry}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">
|
||||
{scored > 0 ? `${scored} scorés` : 'Aucun cycle enregistré'}
|
||||
{geoScore !== null && ` · géo ${geoScore}`}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Meilleur Pattern scoré */}
|
||||
{/* Top patterns scorés du cycle */}
|
||||
{(() => {
|
||||
const best = [...allPatterns]
|
||||
.map(p => ({ p, sp: scoreMap[p.id] }))
|
||||
.filter(x => x.sp?.score != null)
|
||||
.sort((a, b) => (b.sp.score ?? 0) - (a.sp.score ?? 0))[0]
|
||||
const score = best?.sp?.score ?? null
|
||||
const name = best?.p?.name ?? null
|
||||
const ticker = best?.sp?.recommended_trade?.underlying ?? null
|
||||
return (
|
||||
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">⭐ Meilleur Pattern</span>
|
||||
<span className="section-title mb-0 text-[10px]">⭐ Top Patterns</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className={clsx('text-2xl font-bold font-mono mt-1', score !== null ? scoreColor(score) : 'text-slate-500')}>
|
||||
{score ?? '—'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1 line-clamp-1">
|
||||
{name ?? 'Aucun scoré'}{ticker ? ` · ${ticker}` : ''}
|
||||
</div>
|
||||
{topScoredPatterns.length > 0 ? (
|
||||
<div className="space-y-1.5 mt-1">
|
||||
{topScoredPatterns.map((p, i) => {
|
||||
const sp = scoreMap[p.id]
|
||||
const score = sp?.score ?? null
|
||||
const ticker = sp?.recommended_trade?.underlying ?? null
|
||||
return (
|
||||
<div key={p.id} className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-slate-700 w-3 shrink-0 font-mono">{i + 1}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[10px] text-slate-300 truncate leading-tight">{p.name}</div>
|
||||
{ticker && <div className="text-[9px] text-slate-600 font-mono">{ticker}</div>}
|
||||
</div>
|
||||
{score !== null && (
|
||||
<span className={clsx('text-sm font-bold font-mono shrink-0', scoreColor(score))}>{score}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Aucun scoré — lancer le scoring IA</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Patterns Actifs */}
|
||||
{/* Top news impactantes */}
|
||||
{(() => {
|
||||
const total = allPatterns.length
|
||||
const scored = allPatterns.filter(p => scoreMap[p.id]).length
|
||||
const unscored = total - scored
|
||||
return (
|
||||
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<Link to="/geo" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="section-title mb-0 text-[10px]">📋 Patterns Actifs</span>
|
||||
<span className="section-title mb-0 text-[10px] flex items-center gap-1">
|
||||
<Newspaper className="w-2.5 h-2.5" /> Top News
|
||||
</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-slate-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-slate-200 mt-1">{total}</div>
|
||||
<div className="text-[10px] text-slate-500 mt-1">
|
||||
<span className="text-emerald-500">{scored} scorés</span>
|
||||
{unscored > 0 && <span className="text-slate-600"> · {unscored} à scorer</span>}
|
||||
</div>
|
||||
{topNews.length > 0 ? (
|
||||
<div className="space-y-2 mt-1">
|
||||
{topNews.map((n, i) => {
|
||||
const impact = Math.round(n.impact_score * 100)
|
||||
const impactColor = impact >= 70 ? 'text-red-400' : impact >= 40 ? 'text-orange-400' : 'text-yellow-400'
|
||||
const barColor = impact >= 70 ? 'bg-red-500' : impact >= 40 ? 'bg-orange-500' : 'bg-yellow-500'
|
||||
return (
|
||||
<div key={i} className="space-y-0.5">
|
||||
<div className="flex items-start gap-1.5">
|
||||
<span className="text-[9px] text-slate-700 font-mono mt-0.5 shrink-0">{i + 1}</span>
|
||||
<p className="text-[10px] text-slate-300 leading-tight line-clamp-2 flex-1">{n.title}</p>
|
||||
<span className={clsx('text-[10px] font-bold font-mono shrink-0', impactColor)}>{impact}</span>
|
||||
</div>
|
||||
<div className="ml-3 bg-dark-700 rounded-full h-0.5">
|
||||
<div className={clsx('h-0.5 rounded-full', barColor)} style={{ width: `${impact}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-slate-600 mt-1">Chargement des news…</div>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user