feat: translate all UI strings to English for international release
Complete French→English translation across all frontend pages and backend services — every label, button, header, empty state, toast, and nav item is now in English. Build verified clean (tsc + vite). No i18n library added; direct string replacement throughout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,14 +16,14 @@ function jaccard(a: string[], b: string[]): number {
|
||||
const TRIGGERS = ['military', 'sanctions', 'elections', 'natural_disaster', 'health_crisis', 'resource_scarcity', 'trade_war', 'energy', 'political_speech', 'financial_crisis']
|
||||
const ASSET_CLASSES = ['energy', 'metals', 'agriculture', 'equities', 'indices', 'forex', 'rates']
|
||||
const ASSET_CLASS_LABELS: Record<string, string> = {
|
||||
energy: '⚡ Énergie', metals: '🥇 Métaux', agriculture: '🌾 Agri',
|
||||
equities: '📈 Actions', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Taux',
|
||||
energy: '⚡ Energy', metals: '🥇 Metals', agriculture: '🌾 Agri',
|
||||
equities: '📈 Equities', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Rates',
|
||||
}
|
||||
const TRIGGER_LABELS: Record<string, string> = {
|
||||
military: '⚔️ Militaire', sanctions: '🚫 Sanctions', elections: '🗳️ Élections',
|
||||
natural_disaster: '🌪️ Catastrophe', health_crisis: '🏥 Santé',
|
||||
resource_scarcity: '⚠️ Ressources', trade_war: '🤝 Commerce',
|
||||
energy: '⚡ Énergie', political_speech: '🎙️ Discours', financial_crisis: '💸 Finance',
|
||||
military: '⚔️ Military', sanctions: '🚫 Sanctions', elections: '🗳️ Elections',
|
||||
natural_disaster: '🌪️ Disaster', health_crisis: '🏥 Health',
|
||||
resource_scarcity: '⚠️ Resources', trade_war: '🤝 Trade',
|
||||
energy: '⚡ Energy', political_speech: '🎙️ Speech', financial_crisis: '💸 Finance',
|
||||
}
|
||||
|
||||
const EMPTY_PATTERN = {
|
||||
@@ -35,7 +35,7 @@ const EMPTY_PATTERN = {
|
||||
|
||||
function QualityBadge({ score }: { score: number }) {
|
||||
const color = score >= 75 ? 'badge-green' : score >= 50 ? 'badge-yellow' : 'badge-red'
|
||||
const label = score >= 75 ? 'Excellent' : score >= 50 ? 'Bon' : 'Faible'
|
||||
const label = score >= 75 ? 'Excellent' : score >= 50 ? 'Good' : 'Weak'
|
||||
return <span className={clsx('badge', color)}>{score}/100 — {label}</span>
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
|
||||
<div className="text-sm font-semibold text-white">{p.name}</div>
|
||||
{isCustom && <span className="badge badge-purple text-xs">Custom</span>}
|
||||
{!isActive && <span className="badge badge-red text-xs">Désactivé</span>}
|
||||
{!isActive && <span className="badge badge-red text-xs">Disabled</span>}
|
||||
{p.ai_quality_score && <QualityBadge score={p.ai_quality_score} />}
|
||||
<ReliabilityBadge rel={reliability} />
|
||||
</div>
|
||||
@@ -83,15 +83,15 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
<div className={clsx('text-sm font-bold', p.expected_move_pct > 0 ? 'positive' : 'negative')}>
|
||||
{p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%
|
||||
</div>
|
||||
<div className="text-xs text-slate-600">{p.horizon_days}j</div>
|
||||
<div className="text-xs text-slate-600">{p.horizon_days}d</div>
|
||||
</div>
|
||||
<button onClick={onEdit} title="Modifier" className="text-slate-500 hover:text-blue-400 transition-colors">
|
||||
<button onClick={onEdit} title="Edit" className="text-slate-500 hover:text-blue-400 transition-colors">
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={onToggle} title={isActive ? 'Désactiver' : 'Activer'} className={clsx('transition-colors', isActive ? 'text-slate-500 hover:text-yellow-400' : 'text-yellow-500 hover:text-yellow-300')}>
|
||||
<button onClick={onToggle} title={isActive ? 'Disable' : 'Enable'} className={clsx('transition-colors', isActive ? 'text-slate-500 hover:text-yellow-400' : 'text-yellow-500 hover:text-yellow-300')}>
|
||||
{isActive ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
|
||||
</button>
|
||||
<button onClick={onDelete} title="Supprimer" className="text-slate-500 hover:text-red-400 transition-colors">
|
||||
<button onClick={onDelete} title="Delete" className="text-slate-500 hover:text-red-400 transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -101,7 +101,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{similarTo.map((s, i) => (
|
||||
<span key={i} className="inline-flex items-center gap-1 text-[10px] bg-amber-900/30 border border-amber-600/30 text-amber-400 rounded px-1.5 py-0.5">
|
||||
⚠ Similaire à <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
|
||||
⚠ Similar to <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
|
||||
<span className="font-mono">{Math.round(s.similarity * 100)}%</span>
|
||||
</span>
|
||||
))}
|
||||
@@ -115,7 +115,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
<span className="badge badge-blue text-xs">{p.asset_class}</span>
|
||||
{aiScore != null ? (
|
||||
<span className={clsx('badge text-xs font-bold', aiScore >= 50 ? 'badge-green' : aiScore >= 25 ? 'badge-yellow' : 'badge-red')}>
|
||||
{aiScore}/100 IA
|
||||
{aiScore}/100 AI
|
||||
</span>
|
||||
) : (
|
||||
<span className="badge badge-purple text-xs">{Math.round(p.probability * 100)}% prob.</span>
|
||||
@@ -123,14 +123,14 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
</div>
|
||||
|
||||
<button onClick={() => setExpanded(!expanded)} className="text-xs text-slate-600 hover:text-slate-400">
|
||||
{expanded ? '▲ Réduire' : '▼ Voir détails'}
|
||||
{expanded ? '▲ Collapse' : '▼ See details'}
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{p.keywords?.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-600 mb-1">Mots-clés de détection</div>
|
||||
<div className="text-xs text-slate-600 mb-1">Detection keywords</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{p.keywords.map((kw: string) => (
|
||||
<span key={kw} className="text-xs bg-dark-700 text-slate-400 px-1.5 py-0.5 rounded">#{kw}</span>
|
||||
@@ -140,7 +140,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
)}
|
||||
{p.historical_instances?.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-600 mb-1">Instances historiques</div>
|
||||
<div className="text-xs text-slate-600 mb-1">Historical instances</div>
|
||||
{p.historical_instances.map((h: any, i: number) => (
|
||||
<div key={i} className="card-sm mb-1">
|
||||
<span className="text-xs text-slate-500">{h.date}</span>
|
||||
@@ -151,7 +151,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
)}
|
||||
{p.suggested_trades?.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-600 mb-1">Trades suggérés</div>
|
||||
<div className="text-xs text-slate-600 mb-1">Suggested trades</div>
|
||||
{p.suggested_trades.map((t: any, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2 text-xs mb-1 flex-wrap">
|
||||
<span className="badge badge-green">{t.strategy}</span>
|
||||
@@ -165,11 +165,11 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
{(p.counter_thesis || p.invalidation_trigger) && (
|
||||
<div className="card-sm border-red-700/30 bg-red-900/10">
|
||||
<div className="text-xs text-red-400 font-semibold mb-1 flex items-center gap-1">
|
||||
<ShieldAlert className="w-3 h-3" /> Contre-thèse & Invalidation
|
||||
<ShieldAlert className="w-3 h-3" /> Counter-thesis & Invalidation
|
||||
</div>
|
||||
{p.counter_thesis && (
|
||||
<div className="text-xs text-slate-300 mb-1">
|
||||
<span className="text-slate-500">Contre-thèse :</span> {p.counter_thesis}
|
||||
<span className="text-slate-500">Counter-thesis:</span> {p.counter_thesis}
|
||||
</div>
|
||||
)}
|
||||
{p.invalidation_trigger && (
|
||||
@@ -184,7 +184,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
|
||||
)}
|
||||
{p.ai_evaluation && Object.keys(p.ai_evaluation).length > 0 && (
|
||||
<div className="card-sm border-blue-700/30">
|
||||
<div className="text-xs text-blue-400 font-semibold mb-1">Évaluation IA</div>
|
||||
<div className="text-xs text-blue-400 font-semibold mb-1">AI Evaluation</div>
|
||||
{p.ai_evaluation.strengths?.length > 0 && (
|
||||
<div className="text-xs text-emerald-400 mb-1">✓ {p.ai_evaluation.strengths[0]}</div>
|
||||
)}
|
||||
@@ -233,10 +233,10 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<div className="flex items-center justify-between p-4 border-b border-slate-700/30">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-white flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-blue-400" /> Suggérer des patterns par l'IA
|
||||
<Sparkles className="w-4 h-4 text-blue-400" /> Suggest patterns with AI
|
||||
</h2>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
GPT-4o analyse l'actualité géo, les prix, le calendrier et le <span className="text-blue-400">régime macro actuel</span> pour proposer des patterns cohérents avec le scénario dominant
|
||||
GPT-4o analyzes geo news, prices, calendar and the <span className="text-blue-400">current macro regime</span> to propose patterns aligned with the dominant scenario
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-slate-500 hover:text-white"><X className="w-5 h-5" /></button>
|
||||
@@ -248,11 +248,11 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<div className="text-center py-12">
|
||||
<Sparkles className="w-10 h-10 text-blue-400/40 mx-auto mb-3" />
|
||||
<p className="text-sm text-slate-400 mb-4">
|
||||
L'IA va analyser les news géopolitiques actuelles, les mouvements de prix et le calendrier économique pour proposer des patterns pertinents <em>aujourd'hui</em>.
|
||||
The AI will analyze current geopolitical news, price movements, and the economic calendar to propose patterns relevant <em>today</em>.
|
||||
</p>
|
||||
<button onClick={() => suggest()}
|
||||
className="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded text-sm font-semibold">
|
||||
Lancer l'analyse (GPT-4o)
|
||||
Run analysis (GPT-4o)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -260,7 +260,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
{isPending && (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-3"></div>
|
||||
<p className="text-sm text-slate-400">GPT-4o analyse le contexte du moment…</p>
|
||||
<p className="text-sm text-slate-400">GPT-4o analyzing current context…</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -277,7 +277,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<span className={clsx('badge text-xs font-bold', score >= 50 ? 'badge-green' : score >= 25 ? 'badge-yellow' : 'badge-red')}>
|
||||
{score}/100 IA
|
||||
</span>
|
||||
<span className="text-xs text-slate-500">{p.horizon_days}j · {p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%</span>
|
||||
<span className="text-xs text-slate-500">{p.horizon_days}d · {p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -287,7 +287,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
'bg-emerald-700/30 border border-emerald-700/40 text-emerald-400 cursor-default': saved.has(idx),
|
||||
'bg-blue-600 hover:bg-blue-500 text-white': !saved.has(idx),
|
||||
})}>
|
||||
{saved.has(idx) ? <><Check className="w-3 h-3" /> Sauvegardé</> : <><Save className="w-3 h-3" /> Sauvegarder</>}
|
||||
{saved.has(idx) ? <><Check className="w-3 h-3" /> Saved</> : <><Save className="w-3 h-3" /> Save</>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -295,7 +295,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<div className="flex flex-wrap gap-1 mb-2">
|
||||
{similars.map((s, i) => (
|
||||
<span key={i} className="inline-flex items-center gap-1 text-[10px] bg-amber-900/30 border border-amber-600/30 text-amber-400 rounded px-1.5 py-0.5">
|
||||
⚠ Similaire à <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
|
||||
⚠ Similar to <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
|
||||
<span className="font-mono">{Math.round(s.similarity * 100)}%</span>
|
||||
</span>
|
||||
))}
|
||||
@@ -312,7 +312,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
{(p.counter_thesis || p.invalidation_trigger) && (
|
||||
<div className="mb-2 text-[11px] bg-red-900/10 border border-red-700/20 rounded px-2 py-1.5 space-y-0.5">
|
||||
{p.counter_thesis && (
|
||||
<div className="text-slate-300"><span className="text-red-400/80">⚠ Contre-thèse :</span> {p.counter_thesis}</div>
|
||||
<div className="text-slate-300"><span className="text-red-400/80">⚠ Counter-thesis:</span> {p.counter_thesis}</div>
|
||||
)}
|
||||
{p.invalidation_trigger && (
|
||||
<div className="text-orange-300/70"><span className="text-slate-500">Trigger :</span> {p.invalidation_trigger}{p.invalidation_probability != null ? ` (${Math.round(p.invalidation_probability * 100)}%)` : ''}</div>
|
||||
@@ -336,7 +336,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
|
||||
{data && suggestions.length === 0 && (
|
||||
<div className="card text-center py-8 text-slate-500 text-sm">
|
||||
Aucun pattern suggéré. Réessayer.
|
||||
No patterns suggested. Try again.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -345,18 +345,18 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
|
||||
<div className="p-4 border-t border-slate-700/30 flex justify-between items-center">
|
||||
{suggestions.length > 0 && (
|
||||
<span className="text-xs text-slate-500">
|
||||
{saved.size}/{suggestions.length} sauvegardé{saved.size > 1 ? 's' : ''}
|
||||
{saved.size}/{suggestions.length} saved
|
||||
</span>
|
||||
)}
|
||||
<div className="flex gap-2 ml-auto">
|
||||
{data && suggestions.length > 0 && (
|
||||
<button onClick={() => suggest()} disabled={isPending}
|
||||
className="flex items-center gap-1 border border-slate-700 text-slate-400 hover:text-slate-200 px-3 py-1.5 rounded text-xs">
|
||||
<RotateCcw className="w-3 h-3" /> Régénérer
|
||||
<RotateCcw className="w-3 h-3" /> Regenerate
|
||||
</button>
|
||||
)}
|
||||
<button onClick={onClose} className="border border-slate-700 text-slate-400 hover:text-slate-200 px-4 py-1.5 rounded text-xs">
|
||||
Fermer
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -431,18 +431,18 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
{/* AI Suggest from context */}
|
||||
{aiStatus?.enabled && (
|
||||
<div className="card border-blue-500/30">
|
||||
<div className="section-title flex items-center gap-1"><Brain className="w-3 h-3 text-blue-400" /> Générer depuis un contexte (IA)</div>
|
||||
<div className="section-title flex items-center gap-1"><Brain className="w-3 h-3 text-blue-400" /> Generate from context (AI)</div>
|
||||
<div className="flex gap-2">
|
||||
<textarea
|
||||
value={suggestInput}
|
||||
onChange={e => setSuggestInput(e.target.value)}
|
||||
placeholder="Décris le contexte géopolitique... ex: 'Quand la Chine impose des restrictions sur les terres rares, que se passe-t-il sur le marché des semi-conducteurs et des métaux ?'"
|
||||
placeholder="Describe the geopolitical context... e.g. 'When China imposes restrictions on rare earths, what happens to semiconductor and metals markets?'"
|
||||
rows={2}
|
||||
className="flex-1 bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500 resize-none"
|
||||
/>
|
||||
<button onClick={suggestFromContext} disabled={suggestLoading || !suggestInput}
|
||||
className="shrink-0 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-3 rounded text-xs">
|
||||
{suggestLoading ? '...' : '⚡ Générer'}
|
||||
{suggestLoading ? '...' : '⚡ Generate'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -451,22 +451,22 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
{/* Form fields */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-slate-500 mb-1 block">Nom du pattern</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Pattern name</label>
|
||||
<input value={form.name} onChange={e => set('name', e.target.value)}
|
||||
placeholder="Ex: Chine tensions Taiwan → Semi-conducteurs spike"
|
||||
placeholder="E.g.: China Taiwan tensions → Semiconductors spike"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-slate-500 mb-1 block">Description</label>
|
||||
<textarea value={form.description} onChange={e => set('description', e.target.value)}
|
||||
rows={2} placeholder="Mécanisme géopolitique → impact marché..."
|
||||
rows={2} placeholder="Geopolitical mechanism → market impact..."
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500 resize-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Triggers */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Déclencheurs</label>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Triggers</label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{TRIGGERS.map(t => (
|
||||
<button key={t} onClick={() => toggleTrigger(t)}
|
||||
@@ -482,11 +482,11 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
|
||||
{/* Keywords */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Mots-clés de détection (anglais)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Detection keywords (English)</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input value={kwInput} onChange={e => setKwInput(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && addKeyword()}
|
||||
placeholder="Ajouter un mot-clé..."
|
||||
placeholder="Add a keyword..."
|
||||
className="flex-1 bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
<button onClick={addKeyword} className="bg-dark-600 border border-slate-700 hover:border-slate-500 text-slate-300 px-3 rounded text-xs">+</button>
|
||||
</div>
|
||||
@@ -504,48 +504,48 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
{/* Metrics */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Classe d'actif</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Asset class</label>
|
||||
<select value={form.asset_class} onChange={e => set('asset_class', e.target.value)}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500">
|
||||
{ASSET_CLASSES.map(a => <option key={a}>{a}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Move attendu (%)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Expected move (%)</label>
|
||||
<input type="number" value={form.expected_move_pct} onChange={e => set('expected_move_pct', Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Probabilité (0-1)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Probability (0-1)</label>
|
||||
<input type="number" step="0.05" min="0" max="1" value={form.probability} onChange={e => set('probability', Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Horizon (jours)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Horizon (days)</label>
|
||||
<input type="number" value={form.horizon_days} onChange={e => set('horizon_days', Number(e.target.value))}
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contre-thèse & Invalidation */}
|
||||
{/* Counter-thesis & Invalidation */}
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block flex items-center gap-1">
|
||||
<ShieldAlert className="w-3 h-3 text-red-400" /> Contre-thèse (scénario adverse principal)
|
||||
<ShieldAlert className="w-3 h-3 text-red-400" /> Counter-thesis (main adverse scenario)
|
||||
</label>
|
||||
<textarea value={form.counter_thesis || ''} onChange={e => set('counter_thesis', e.target.value)}
|
||||
rows={2} placeholder="Ex: accord de paix inattendu, données inflation sous 3%, pivot Fed…"
|
||||
rows={2} placeholder="E.g.: unexpected peace deal, inflation below 3%, Fed pivot…"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-red-500/50 resize-none" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs text-slate-500 mb-1 block">Trigger d'invalidation (événement précis & mesurable)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Invalidation trigger (precise & measurable event)</label>
|
||||
<input value={form.invalidation_trigger || ''} onChange={e => set('invalidation_trigger', e.target.value)}
|
||||
placeholder="Ex: prix pétrole < 70$/b 3j consécutifs"
|
||||
placeholder="E.g.: oil price < $70/b for 3 consecutive days"
|
||||
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-red-500/50" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Prob. invalidation (0-1)</label>
|
||||
<label className="text-xs text-slate-500 mb-1 block">Invalidation prob. (0-1)</label>
|
||||
<input type="number" step="0.05" min="0" max="1" value={form.invalidation_probability ?? ''}
|
||||
onChange={e => set('invalidation_probability', e.target.value ? Number(e.target.value) : undefined)}
|
||||
placeholder="0.20"
|
||||
@@ -559,19 +559,19 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
<div className="card border-blue-700/40">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm font-semibold text-blue-400 flex items-center gap-1">
|
||||
<Brain className="w-4 h-4" /> Évaluation IA
|
||||
<Brain className="w-4 h-4" /> AI Evaluation
|
||||
</div>
|
||||
<QualityBadge score={aiResult.quality_score || 0} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 text-xs">
|
||||
<div>
|
||||
<div className="text-slate-500 mb-1 font-semibold">✓ Points forts</div>
|
||||
<div className="text-slate-500 mb-1 font-semibold">✓ Strengths</div>
|
||||
{aiResult.strengths?.map((s: string, i: number) => (
|
||||
<div key={i} className="text-emerald-400 mb-0.5">• {s}</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-500 mb-1 font-semibold">⚠ Faiblesses</div>
|
||||
<div className="text-slate-500 mb-1 font-semibold">⚠ Weaknesses</div>
|
||||
{aiResult.weaknesses?.map((w: string, i: number) => (
|
||||
<div key={i} className="text-orange-400 mb-0.5">• {w}</div>
|
||||
))}
|
||||
@@ -579,7 +579,7 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
</div>
|
||||
{aiResult.suggested_improvements?.additional_keywords?.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-slate-500 mb-1">Mots-clés suggérés par l'IA</div>
|
||||
<div className="text-xs text-slate-500 mb-1">Keywords suggested by AI</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{aiResult.suggested_improvements.additional_keywords.map((kw: string) => (
|
||||
<button key={kw} onClick={() => set('keywords', [...form.keywords, kw])}
|
||||
@@ -597,7 +597,7 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
)}
|
||||
{aiResult.counter_scenarios?.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-slate-500 mb-1">Scénarios d'invalidation</div>
|
||||
<div className="text-xs text-slate-500 mb-1">Invalidation scenarios</div>
|
||||
{aiResult.counter_scenarios.map((s: string, i: number) => (
|
||||
<div key={i} className="text-xs text-red-400 mb-0.5">✗ {s}</div>
|
||||
))}
|
||||
@@ -611,15 +611,15 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
|
||||
<button onClick={evaluateWithAI} disabled={aiLoading || !form.name}
|
||||
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 px-3 py-1.5 rounded text-sm transition-colors disabled:opacity-40">
|
||||
<Brain className="w-3.5 h-3.5" />
|
||||
{aiLoading ? 'Évaluation IA...' : 'Évaluer avec IA'}
|
||||
{aiLoading ? 'AI Evaluation...' : 'Evaluate with AI'}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={onCancel} className="flex items-center gap-1.5 border border-slate-700 text-slate-400 hover:text-slate-200 px-3 py-1.5 rounded text-sm">
|
||||
<RotateCcw className="w-3.5 h-3.5" /> Annuler
|
||||
<RotateCcw className="w-3.5 h-3.5" /> Cancel
|
||||
</button>
|
||||
<button onClick={() => onSave(form)} disabled={!form.name || form.triggers.length === 0}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-1.5 rounded text-sm font-semibold ml-auto">
|
||||
<Save className="w-3.5 h-3.5" /> Sauvegarder le pattern
|
||||
<Save className="w-3.5 h-3.5" /> Save pattern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -725,23 +725,23 @@ export default function PatternEditor() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-blue-400" /> Patterns Géopolitiques
|
||||
<Zap className="w-5 h-5 text-blue-400" /> Geopolitical Patterns
|
||||
</h1>
|
||||
<p className="text-xs text-slate-500 mt-0.5">
|
||||
{patterns?.length ?? 0} patterns ·{' '}
|
||||
{patterns?.filter((p: any) => p.source === 'custom').length ?? 0} personnalisés
|
||||
{patterns?.filter((p: any) => p.source === 'custom').length ?? 0} custom
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{aiStatus?.enabled && (
|
||||
<button onClick={() => setShowAiSuggest(true)}
|
||||
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 px-3 py-1.5 rounded text-sm font-semibold transition-colors">
|
||||
<Sparkles className="w-4 h-4" /> Suggérer par l'IA
|
||||
<Sparkles className="w-4 h-4" /> Suggest with AI
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => { setCreating(true); setEditing(null) }}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white px-3 py-1.5 rounded text-sm font-semibold">
|
||||
<Plus className="w-4 h-4" /> Créer un pattern
|
||||
<Plus className="w-4 h-4" /> Create pattern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -749,7 +749,7 @@ export default function PatternEditor() {
|
||||
{(creating || editing) && (
|
||||
<div className="card border-blue-500/30">
|
||||
<div className="text-sm font-semibold text-white mb-4">
|
||||
{editing ? `Modifier: ${editing.name}` : 'Nouveau pattern géopolitique'}
|
||||
{editing ? `Edit: ${editing.name}` : 'New geopolitical pattern'}
|
||||
</div>
|
||||
<PatternForm
|
||||
initial={editing}
|
||||
@@ -763,9 +763,9 @@ export default function PatternEditor() {
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex gap-1 bg-dark-700 p-1 rounded">
|
||||
{[
|
||||
{ key: 'all', label: `Tous (${patterns?.length ?? 0})` },
|
||||
{ key: 'builtin', label: `Intégrés (${patterns?.filter((p: any) => p.source === 'builtin').length ?? 0})` },
|
||||
{ key: 'custom', label: `Mes patterns (${patterns?.filter((p: any) => p.source === 'custom').length ?? 0})` },
|
||||
{ key: 'all', label: `All (${patterns?.length ?? 0})` },
|
||||
{ key: 'builtin', label: `Built-in (${patterns?.filter((p: any) => p.source === 'builtin').length ?? 0})` },
|
||||
{ key: 'custom', label: `My patterns (${patterns?.filter((p: any) => p.source === 'custom').length ?? 0})` },
|
||||
].map(({ key, label }) => (
|
||||
<button key={key} onClick={() => setTab(key as any)}
|
||||
className={clsx('px-3 py-1.5 rounded text-sm', {
|
||||
@@ -799,9 +799,9 @@ export default function PatternEditor() {
|
||||
{/* Period filter */}
|
||||
<div className="flex gap-1 bg-dark-700 p-1 rounded">
|
||||
{([
|
||||
{ key: null, label: 'Tout' },
|
||||
{ key: 7, label: '7j' },
|
||||
{ key: 30, label: '30j' },
|
||||
{ key: null, label: 'All' },
|
||||
{ key: 7, label: '7d' },
|
||||
{ key: 30, label: '30d' },
|
||||
] as const).map(({ key, label }) => (
|
||||
<button key={String(key)} onClick={() => setFilterDays(key)}
|
||||
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
|
||||
@@ -834,7 +834,7 @@ export default function PatternEditor() {
|
||||
'bg-slate-600/40 border-slate-500/60 text-white': filterAsset === 'all',
|
||||
'border-slate-700/40 text-slate-500 hover:border-slate-500 hover:text-slate-300': filterAsset !== 'all',
|
||||
})}>
|
||||
Tous types
|
||||
All types
|
||||
</button>
|
||||
{ASSET_CLASSES.map(ac => (
|
||||
<button key={ac} onClick={() => setFilterAsset(filterAsset === ac ? 'all' : ac)}
|
||||
@@ -847,7 +847,7 @@ export default function PatternEditor() {
|
||||
))}
|
||||
{(filterAsset !== 'all' || filterDays !== null) && (
|
||||
<span className="text-xs text-slate-600 ml-1">
|
||||
{displayPatterns.length} résultat{displayPatterns.length !== 1 ? 's' : ''}
|
||||
{displayPatterns.length} result{displayPatterns.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -874,12 +874,12 @@ export default function PatternEditor() {
|
||||
<Zap className="w-8 h-8 mx-auto mb-2 opacity-20" />
|
||||
<div>
|
||||
{filterAsset !== 'all'
|
||||
? `Aucun pattern dans "${ASSET_CLASS_LABELS[filterAsset]}"`
|
||||
? `No patterns in "${ASSET_CLASS_LABELS[filterAsset]}"`
|
||||
: filterDays !== null
|
||||
? `Aucun pattern dans les ${filterDays} derniers jours`
|
||||
: 'Aucun pattern personnalisé'}
|
||||
? `No patterns in the last ${filterDays} days`
|
||||
: 'No custom patterns'}
|
||||
</div>
|
||||
<div className="text-xs mt-1">Créer un pattern avec l'aide de l'IA</div>
|
||||
<div className="text-xs mt-1">Create a pattern with AI assistance</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user