feat: AI-enriched news scoring + directional alignment on pattern matching
Hook ai_score_news_batch() into /api/geo/news so every news fetch is enriched by GPT-4o-mini: corrected impact_score, ai_dir_energy/metals/indices, ai_resolution (ceasefire/peace deal flag), ai_insight (1 French sentence). Gracefully no-ops when OpenAI is not configured. Add _compute_ai_alignment() in geo_analyzer: for each pattern compares the news AI directional signals against the pattern's expected_move direction and produces a -25..+25 bonus injected into similarity/relevance scores. Contra-signals (e.g. peace deal → oil bearish while pattern expects oil spike) are flagged. Frontend GeoRadar: PatternRelevanceCard shows AI alignment badge (green = aligned, red = contra-signal) + base relevance diff + AI insights. NewsCard shows ai_insight, directional arrows per asset class (⛽↑ 🥇↓) and resolution badge when expanded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from 'react'
|
||||
import { useGeoNews, usePatternRelevance, useGeoRiskScore } from '../hooks/useApi'
|
||||
import clsx from 'clsx'
|
||||
import type { GeoNews } from '../types'
|
||||
import { Globe, ExternalLink, Search } from 'lucide-react'
|
||||
import { Globe, ExternalLink, Search, Brain } from 'lucide-react'
|
||||
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
military: 'badge-red', sanctions: 'badge-orange', elections: 'badge-purple',
|
||||
@@ -67,6 +67,33 @@ function NewsCard({ news }: { news: GeoNews }) {
|
||||
|
||||
{expanded && (
|
||||
<>
|
||||
{/* AI insight */}
|
||||
{(news as any).ai_insight && (
|
||||
<div className="flex items-start gap-1.5 mb-2 text-xs text-blue-300 italic bg-blue-900/10 border border-blue-800/30 rounded px-2 py-1">
|
||||
<Brain className="w-3 h-3 mt-0.5 shrink-0 text-blue-400" />
|
||||
{(news as any).ai_insight}
|
||||
</div>
|
||||
)}
|
||||
{/* AI directional signals */}
|
||||
{((news as any).ai_dir_energy || (news as any).ai_dir_metals || (news as any).ai_dir_indices) && (
|
||||
<div className="flex gap-1 flex-wrap mb-2">
|
||||
{(['ai_dir_energy', 'ai_dir_metals', 'ai_dir_indices'] as const).map(field => {
|
||||
const dir = (news as any)[field]
|
||||
if (!dir || dir === 'neutral') return null
|
||||
const labels: Record<string, string> = { ai_dir_energy: '⛽', ai_dir_metals: '🥇', ai_dir_indices: '📊' }
|
||||
return (
|
||||
<span key={field} className={clsx('text-[10px] px-1.5 py-0.5 rounded font-semibold',
|
||||
dir === 'bullish' ? 'bg-emerald-900/40 text-emerald-400' : 'bg-red-900/40 text-red-400'
|
||||
)}>
|
||||
{labels[field]} {dir === 'bullish' ? '↑' : '↓'}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
{(news as any).ai_resolution && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-900/30 text-blue-400">🕊 résolution</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-slate-400 leading-relaxed mb-3">{news.summary}</p>
|
||||
{Object.keys(news.asset_impacts).length > 0 && (
|
||||
<div className="mb-3">
|
||||
@@ -135,9 +162,25 @@ function PatternRelevanceCard({ p }: { p: any }) {
|
||||
<Search className="w-2.5 h-2.5" />{p.matching_news.length} news
|
||||
</span>
|
||||
)}
|
||||
{/* AI alignment badge */}
|
||||
{p.ai_scored_count > 0 && p.ai_alignment !== 0 && (
|
||||
<span className={clsx('text-[10px] px-1.5 py-0.5 rounded flex items-center gap-0.5 font-semibold',
|
||||
p.ai_contra_signal
|
||||
? 'bg-red-900/30 text-red-400 border border-red-700/30'
|
||||
: 'bg-emerald-900/30 text-emerald-400 border border-emerald-700/30'
|
||||
)}>
|
||||
<Brain className="w-2.5 h-2.5" />
|
||||
{p.ai_contra_signal ? `contre-signal (${p.ai_alignment > 0 ? '+' : ''}${p.ai_alignment}pts)` : `IA aligné (+${p.ai_alignment}pts)`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RelevanceBar pct={p.relevance} />
|
||||
<div className="text-right">
|
||||
<RelevanceBar pct={p.relevance} />
|
||||
{p.base_relevance !== undefined && p.ai_alignment !== 0 && (
|
||||
<div className="text-[10px] text-slate-600 mt-0.5">base {p.base_relevance}%</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Matched keywords */}
|
||||
@@ -151,6 +194,18 @@ function PatternRelevanceCard({ p }: { p: any }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI insights */}
|
||||
{p.ai_insights?.length > 0 && (
|
||||
<div className="mb-2 space-y-1">
|
||||
{p.ai_insights.map((insight: string, i: number) => (
|
||||
<div key={i} className="flex items-start gap-1 text-[10px] text-blue-300 italic">
|
||||
<Brain className="w-2.5 h-2.5 mt-0.5 shrink-0 text-blue-400" />
|
||||
{insight}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toggle matching news */}
|
||||
{hasNews && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user