"use client"; import { Badge, Box, Card, Flex, Grid, HStack, Icon, IconButton, Separator, SimpleGrid, Text, VStack, } from "@chakra-ui/react"; import { useMessages, useTranslations } from "next-intl"; import { LuBadgeCheck, LuBrain, LuChartColumn, LuChartNoAxesCombined, LuCircleHelp, LuFlame, LuGauge, LuShieldAlert, LuSparkles, LuTarget, LuTriangleAlert, LuTrendingUp, } from "react-icons/lu"; import { useColorModeValue } from "@/components/ui/color-mode"; import { Tooltip } from "@/components/ui/overlays/tooltip"; import type { MarketBoardEntryDto, MatchBetSummaryItemDto, MatchPickDto, MatchPredictionDto, SignalTier, V27EngineDto, } from "@/lib/api/predictions/types"; import type { SportType } from "@/lib/api/matches/types"; import V28OddsBandPanel from "@/components/matches/v28-odds-band-panel"; interface PredictionCardProps { prediction: MatchPredictionDto; } type PredictionUiMessages = Record; function getUiText( ui: PredictionUiMessages | undefined, key: string, fallback: string, ): string { return ui?.[key] || fallback; } function formatReasonFallback(reason: string): string { if (reason.startsWith("risk:")) return formatReasonFallback(reason.slice(5)); const evMatch = reason.match(/^ev_edge_([+\-][\d.]+%)_grade_(\w)$/); if (evMatch) return `Teorik avantaj sinyali: Not ${evMatch[2]}`; const negMatch = reason.match(/^negative_model_edge_([+\-][\d.]+)$/); if (negMatch) return `Model avantajı negatif (${negMatch[1]})`; const thresholdMatch = reason.match( /^below_market_edge_threshold_([+\-]?[\d.]+)$/, ); if (thresholdMatch) return `Piyasa avantaj eşiğinin altında (${thresholdMatch[1]})`; if (reason === "confidence_interval_too_wide") return "Güven aralığı fazla geniş."; if (reason === "confidence_band_low") return "Güven bandı düşük."; if (reason === "draw_probability_elevated") return "Beraberlik olasılığı yükselmiş görünüyor."; if (reason === "balanced_match_risk") return "Maç dengeli görünüyor, sürpriz riski var."; if (reason === "high_total_goal_volatility") return "Yüksek gol temposu sürpriz riskini artırıyor."; if (reason === "mutual_goal_pressure") return "İki takım da gol tehdidi üretiyor."; if (reason === "late_goal_swing_risk") return "Geç gol veya skor kırılması riski yüksek."; if (reason === "live_match_open_state") return "Canlı maç tamamen açık oyuna dönmüş durumda."; if (reason === "live_match_active_state") return "Canlı maç beklenenden daha hareketli ilerliyor."; if (reason === "live_state_impossible_market") return "Canlı maç durumu bu marketi geçersiz kılıyor."; if (reason === "live_score_exceeds_under_line") return "Canlı skor, alt seçeneğinin üst sınırına çok yaklaştı veya geçti."; if (reason === "score_model_conflicts_with_under_pick") return "Skor ve xG modeli bu alt seçeneğiyle çelişiyor."; if (reason === "score_model_conflicts_with_over_pick") return "Skor ve xG modeli bu üst seçeneğiyle çelişiyor."; if (reason === "market_stack_conflict_over25") return "2.5 üst sinyali bu marketle çelişiyor."; if (reason === "market_stack_conflict_btts") return "KG Var sinyali bu marketle çelişiyor."; if (reason === "live_total_goals_close_to_line") return "Canlı toplam gol sayısı bu çizgiye fazla yaklaştı."; if (reason === "score_model_conflicts_with_btts_no") return "Skor ve xG modeli KG Yok seçeneğiyle çelişiyor."; if (reason === "score_model_conflicts_with_draw_pick") return "Skor modeli beraberlik seçeneğini desteklemiyor."; if (reason === "score_model_conflicts_with_home_pick") return "Skor modeli ev sahibi seçeneğini desteklemiyor."; if (reason === "score_model_conflicts_with_away_pick") return "Skor modeli deplasman seçeneğini desteklemiyor."; if (/^[a-z0-9_]+$/i.test(reason)) { return reason .replace(/_/g, " ") .replace(/^\w/, (char) => char.toUpperCase()); } return reason; } function formatPercent(value?: number, digits = 0): string { if (value === undefined || value === null || Number.isNaN(value)) return "-"; return `${value.toFixed(digits)}%`; } function formatProbability(value?: number, digits = 1): string { if (value === undefined || value === null || Number.isNaN(value)) return "-"; return `${(value * 100).toFixed(digits)}%`; } function formatSignalScore(value?: number): string { if (value === undefined || value === null || Number.isNaN(value)) return "-"; return `${Math.max(0, Math.min(100, value)).toFixed(0)}/100`; } function formatEdgeSignal(value?: number): string { if (value === undefined || value === null || Number.isNaN(value)) return "-"; return `${value > 0 ? "+" : ""}${formatPercent(value * 100, 1)}`; } function getEdgePalette(value?: number): string { if (value === undefined || value === null || Number.isNaN(value)) return "gray"; if (value <= 0) return "red"; if (value < 0.08) return "yellow"; if (value < 0.15) return "orange"; return "purple"; } function formatOdds(value?: number | null): string { if (!value || value <= 1.01) return "-"; return value.toFixed(2); } function formatUnits(value?: number): string { if (!value || value <= 0) return "-"; return `${value.toFixed(1)}u`; } function getEngineLabelPalette(label?: string): string { switch ((label || "").toUpperCase()) { case "YUKSEK": return "green"; case "ORTA": return "yellow"; case "DUSUK": return "orange"; case "COK_DUSUK": return "red"; default: return "gray"; } } function getEngineLabelText(label?: string, ui?: PredictionUiMessages): string { switch ((label || "").toUpperCase()) { case "YUKSEK": return getUiText(ui, "engine-label-high", "Yüksek"); case "ORTA": return getUiText(ui, "engine-label-medium", "Orta"); case "DUSUK": return getUiText(ui, "engine-label-low", "Düşük"); case "COK_DUSUK": return getUiText(ui, "engine-label-very-low", "Çok Düşük"); default: return label || ""; } } function getRiskPalette(level: string) { switch (level.toUpperCase()) { case "LOW": return "green"; case "MEDIUM": return "yellow"; case "HIGH": return "orange"; case "EXTREME": return "red"; default: return "gray"; } } function getQualityPalette(label: string) { switch (label.toUpperCase()) { case "HIGH": return "green"; case "MEDIUM": return "yellow"; case "LOW": return "red"; default: return "gray"; } } function getConfidenceBandPalette(band?: string) { switch ((band || "").toUpperCase()) { case "HIGH": return "green"; case "MEDIUM": return "yellow"; case "LOW": return "red"; default: return "gray"; } } function getConfidenceBandLabel(band?: string, ui?: PredictionUiMessages) { switch ((band || "").toUpperCase()) { case "HIGH": return getUiText(ui, "confidence-high", "Yüksek"); case "MEDIUM": return getUiText(ui, "confidence-medium", "Orta"); case "LOW": return getUiText(ui, "confidence-low", "Düşük"); default: return getUiText(ui, "confidence-unknown", "Belirsiz"); } } function getLineupSourceLabel( source?: string, ui?: PredictionUiMessages, ): string { if (source === "confirmed_live") return getUiText(ui, "lineup-confirmed-live", "Onaylı ilk 11"); if (source === "probable_xi") return getUiText(ui, "lineup-probable-xi", "Muhtemel ilk 11"); return source ? formatReasonFallback(source) : getUiText(ui, "unknown", "Bilinmiyor"); } function formatInterval( interval?: { lower?: number; upper?: number }, digits = 0, ): string { if ( !interval || interval.lower === undefined || interval.upper === undefined || Number.isNaN(interval.lower) || Number.isNaN(interval.upper) ) { return "-"; } return `${interval.lower.toFixed(digits)}-${interval.upper.toFixed(digits)}%`; } function getPredictionReasonText( reason: string, reasonMessages?: Record, ): string { if ( reasonMessages && Object.prototype.hasOwnProperty.call(reasonMessages, reason) ) { return reasonMessages[reason]; } return formatReasonFallback(reason); } function getMarketLabel( market: string, marketLabels?: Record, ): string { if ( marketLabels && Object.prototype.hasOwnProperty.call(marketLabels, market) ) { return marketLabels[market]; } const fallbackLabels: Record = { ML: "Moneyline", MS: "Maç Sonucu", DC: "Çifte Şans", TOTAL: "Toplam Sayı", SPREAD: "Handikap", OU15: "Toplam Gol 1.5", OU25: "Toplam Gol 2.5", OU35: "Toplam Gol 3.5", BTTS: "Karşılıklı Gol", HT: "İlk Yarı Sonucu", HT_OU05: "İlk Yarı 0.5 Gol", HT_OU15: "İlk Yarı 1.5 Gol", HTFT: "İlk Yarı / Maç Sonu", "HT/FT": "İlk Yarı / Maç Sonu", OE: "Tek / Çift", CARDS: "Kartlar 4.5", HCAP: "Handikap Sonucu", }; if (fallbackLabels[market]) { return fallbackLabels[market]; } return market; } const MARKET_ORDER = [ "ML", "TOTAL", "SPREAD", "MS", "DC", "OU15", "OU25", "OU35", "BTTS", "HT", "HT_OU05", "HT_OU15", "HTFT", "OE", "CARDS", "HCAP", ]; function getPredictionSport(prediction: MatchPredictionDto): SportType { const explicitSport = prediction.match_info?.sport; if (explicitSport === "basketball" || explicitSport === "football") { return explicitSport; } if ( prediction.model_version?.toLowerCase().includes("basketball") || Object.keys(prediction.market_board || {}).some((market) => ["ML", "TOTAL", "SPREAD"].includes(market), ) ) { return "basketball"; } return "football"; } const SIGNAL_TIER_ORDER: SignalTier[] = [ "CORE", "VALUE", "LEAN", "LONGSHOT", "PASS", ]; function getSignalTierPalette(tier?: SignalTier) { switch (tier) { case "CORE": return "green"; case "VALUE": return "blue"; case "LEAN": return "orange"; case "LONGSHOT": return "pink"; default: return "gray"; } } function getSignalTierLabel(tier?: SignalTier, ui?: PredictionUiMessages) { switch (tier) { case "CORE": return getUiText(ui, "signal-tier-core", "Çekirdek"); case "VALUE": return getUiText(ui, "signal-tier-value", "Değer"); case "LEAN": return getUiText(ui, "signal-tier-lean", "Yorum"); case "LONGSHOT": return getUiText(ui, "signal-tier-longshot", "Sürpriz"); default: return getUiText(ui, "signal-tier-pass", "Pas"); } } function TooltipIcon({ content, ariaLabel = "Bilgi", }: { content: string; ariaLabel?: string; }) { return ( ); } function SectionTitle({ icon, title, info, }: { icon: React.ElementType; title: string; info?: string; }) { return ( {title} {info ? : null} ); } function MetricTile({ label, value, helper, accent, }: { label: string; value: string; helper?: string; accent?: string; }) { const bg = useColorModeValue("gray.50", "whiteAlpha.50"); const borderColor = useColorModeValue("gray.200", "gray.700"); return ( {label} {helper ? : null} {value} ); } function Bar({ value, color, trackBg, height = "8px", }: { value: number; color: string; trackBg: string; height?: string; }) { return ( ); } function ReasonList({ items, resolveReason, }: { items?: string[]; resolveReason: (reason: string) => string; }) { if (!items?.length) return null; return ( {items.map((item, index) => ( {resolveReason(item)} ))} ); } function ProbabilitySplit({ modelProb, impliedProb, labels, }: { modelProb: number; impliedProb: number; labels: { model: string; market: string; }; }) { const trackBg = useColorModeValue("gray.100", "gray.700"); if (!impliedProb || impliedProb <= 0) return null; return ( {labels.model} {formatProbability(modelProb, 0)} {labels.market} {formatProbability(impliedProb, 0)} ); } function PickCard({ pick, stakeFallback, title, resolveReason, palette, marketLabels, labels, ui, }: { pick: MatchPickDto; stakeFallback?: number; title: string; resolveReason: (reason: string) => string; palette: string; marketLabels?: Record; ui?: PredictionUiMessages; labels: { confidence: string; odds: string; recommendedStake: string; playScore: string; playability: string; confidenceInterval: string; confidenceBand: string; confidenceIntervalWarning: string; theoreticalEdgeInline: string; modelProbability: string; marketProbability: string; }; }) { const bg = useColorModeValue(`${palette}.50`, `${palette}.950`); const borderColor = useColorModeValue(`${palette}.200`, `${palette}.800`); const trackBg = useColorModeValue("gray.100", "gray.700"); const intervalWarningBg = useColorModeValue("orange.50", "orange.950"); const intervalWarningBorder = useColorModeValue("orange.200", "orange.800"); const confidenceBandPalette = getConfidenceBandPalette( pick.confidence_interval?.band, ); return ( {title} {pick.pick} {getMarketLabel(pick.market, marketLabels)} {pick.bet_grade} {getSignalTierLabel(pick.signal_tier, ui)} {getConfidenceBandLabel(pick.confidence_interval?.band, ui)} {labels.theoreticalEdgeInline} {formatEdgeSignal(pick.ev_edge)} {labels.playability} {formatSignalScore(pick.play_score)} 0 ? "blue.400" : "orange.400"} trackBg={trackBg} /> {pick.confidence_interval && !pick.confidence_interval.threshold_met ? ( {labels.confidenceIntervalWarning} ) : null} ); } function SummaryTable({ items, marketLabels, title, info, ui, }: { items: MatchBetSummaryItemDto[]; marketLabels?: Record; title: string; info: string; ui?: PredictionUiMessages; }) { const cardBg = useColorModeValue("white", "gray.800"); const borderColor = useColorModeValue("gray.200", "gray.700"); const highlightBg = useColorModeValue("green.50", "green.950"); if (!items.length) return null; return ( {items .slice() .sort((left, right) => { const leftIndex = SIGNAL_TIER_ORDER.indexOf( left.signal_tier || "PASS", ); const rightIndex = SIGNAL_TIER_ORDER.indexOf( right.signal_tier || "PASS", ); if (leftIndex !== rightIndex) return leftIndex - rightIndex; return (right.unified_score ?? right.calibrated_confidence) - (left.unified_score ?? left.calibrated_confidence); }) .map((item) => ( {item.bet_grade} {getSignalTierLabel(item.signal_tier, ui)} {getMarketLabel(item.market, marketLabels)} {item.pick} {formatOdds(item.odds)} {formatPercent( (item.model_probability ?? 0) * 100, 0, )}{" "} {getUiText(ui, "probability-short", "olasılık")} {formatEdgeSignal(item.ev_edge)} {formatPercent(item.unified_score ?? item.calibrated_confidence, 0)} {getConfidenceBandLabel(item.confidence_interval?.band, ui)} {formatUnits(item.stake_units)} ))} {/* {item.bet_grade} {getSignalTierLabel(item.signal_tier)} {item.is_underdog_reference ? ( Underdog ref. ) : null} {item.betting_brain?.trap_market_flag ? ( Trap ) : null} {item.betting_brain?.action === "WATCH_NO_VALUE" ? ( No-value ) : null} {getMarketLabel(item.market, marketLabels)} {item.pick} {formatOdds(item.odds)} 0 ? "green.500" : "red.500"} fontWeight="semibold"> {item.ev_edge > 0 ? "+" : ""} {formatPercent(item.ev_edge * 100, 1)} {formatPercent(item.calibrated_confidence, 0)} {getConfidenceBandLabel(item.confidence_interval?.band)} {formatUnits(item.stake_units)} */} ); } function MarketBoardSection({ marketBoard, betSummary, marketLabels, title, info, ui, }: { marketBoard?: Record; betSummary?: MatchBetSummaryItemDto[]; marketLabels?: Record; title: string; info: string; ui?: PredictionUiMessages; }) { const cardBg = useColorModeValue("white", "gray.800"); const borderColor = useColorModeValue("gray.200", "gray.700"); const trackBg = useColorModeValue("gray.100", "gray.700"); const innerBg = useColorModeValue("gray.50", "whiteAlpha.50"); if (!marketBoard || !Object.keys(marketBoard).length) return null; // Key by market:pick so each card resolves the summary row for the EXACT // outcome it displays (graph pick). Keying by market alone collided on // multi-row markets (MS 1/X/2) and surfaced the wrong odds + confidence. const summaryByMarket = new Map( (betSummary || []).map((item) => [`${item.market}:${item.pick}`, item]), ); // Fallback: first row of a market, for cards whose pick has no exact row. for (const item of betSummary || []) { if (!summaryByMarket.has(item.market)) { summaryByMarket.set(item.market, item); } } const orderedEntries = Object.entries(marketBoard).sort(([left], [right]) => { const leftIndex = MARKET_ORDER.indexOf(left); const rightIndex = MARKET_ORDER.indexOf(right); const safeLeft = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex; const safeRight = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex; return safeLeft - safeRight; }); return ( {orderedEntries.map(([market, entry]) => { if (!entry?.probs) return null; const summary = summaryByMarket.get(`${market}:${entry.pick}`) ?? summaryByMarket.get(market); const interval = summary?.confidence_interval || entry.confidence_interval; // Hit probability == the dominant (green) bar in the graph below, // so the headline never contradicts the distribution it sits on. const pickProbPct = Math.max( 0, ...Object.values(entry.probs).map((p) => Number(p) || 0), ) * 100; return ( {market} {getMarketLabel(market, marketLabels)} {summary ? ( {summary.playable ? getUiText(ui, "playable", "Oynanabilir") : getUiText(ui, "risky", "Riskli")} ) : null} {summary?.signal_tier ? ( {getSignalTierLabel(summary.signal_tier, ui)} ) : null} {summary?.bet_grade ? ( {summary.bet_grade} ) : null} {entry.pick ? ( {entry.pick} ({formatPercent(pickProbPct, 0)}) ) : null} {interval ? ( {getUiText(ui, "confidence-interval", "Güven Aralığı")}:{" "} {formatInterval(interval)} ) : null} {Object.entries(entry.probs).map(([outcome, probability]) => ( {outcome.toUpperCase()} {formatProbability(probability, 1)} = pickProbPct - 1e-6 ? "green.400" : "blue.400" } trackBg={trackBg} height="6px" /> ))} ); })} ); } function ScoreCard({ prediction, sport, ui, }: { prediction: MatchPredictionDto; sport: SportType; ui?: PredictionUiMessages; }) { const cardBg = useColorModeValue("white", "gray.800"); const borderColor = useColorModeValue("gray.200", "gray.700"); const subBg = useColorModeValue("gray.50", "whiteAlpha.50"); const isBasketball = sport === "basketball"; return ( {prediction.scenario_top5.map((scenario) => ( {scenario.score} {formatProbability(scenario.prob, 1)} ))} ); } export default function PredictionCard({ prediction }: PredictionCardProps) { const t = useTranslations("predictions"); const messages = useMessages() as { predictions?: { "prediction-reasons"?: Record; "market-labels"?: Record; ui?: Record; }; }; const marketLabels = messages.predictions?.["market-labels"]; const ui = messages.predictions?.ui; const uiText = (key: string, fallback: string) => ui?.[key] || fallback; const resolveReason = (reason: string) => getPredictionReasonText( reason, messages.predictions?.["prediction-reasons"], ); const pageBg = useColorModeValue("gray.50", "gray.900"); const cardBg = useColorModeValue("white", "gray.800"); const borderColor = useColorModeValue("gray.200", "gray.700"); const liveBg = useColorModeValue("red.50", "red.950"); const liveBorderColor = useColorModeValue("red.300", "red.800"); const warningBg = useColorModeValue("yellow.50", "yellow.950"); const warningBorderColor = useColorModeValue("yellow.300", "yellow.800"); const orangeBg = useColorModeValue("orange.50", "orange.950"); const orangeBorderColor = useColorModeValue("orange.200", "orange.800"); const greenBg = useColorModeValue("green.50", "green.950"); const greenBorderColor = useColorModeValue("green.200", "green.800"); const statCardBg = useColorModeValue("gray.50", "whiteAlpha.50"); const trackBgColor = useColorModeValue("gray.100", "gray.700"); const riskPalette = getRiskPalette(prediction.risk.level); const qualityPalette = getQualityPalette(prediction.data_quality.label); const recommendedPick = prediction.main_pick; const mainBandPalette = getConfidenceBandPalette( prediction.bet_advice.confidence_band, ); // ── Maç Sonucu Tahmini (model'in EN OLASI sonucu) ────────────────── // The value-bet hero below optimizes ROI (often a high-odds draw/underdog, // ~27% hit) which users misread as a "bad prediction". This block instead // shows what the model thinks will actually happen: the highest-probability // 1X2 outcome (~55% hit, on par with the market). Sourced from market_board.MS. const msBoard = prediction.market_board?.MS; const matchResultPrediction = (() => { const probs = msBoard?.probs; if (!probs) return null; const labelMap: Record = { "1": prediction.match_info?.home_team || uiText("home", "Ev Sahibi"), X: uiText("draw", "Beraberlik"), "2": prediction.match_info?.away_team || uiText("away", "Deplasman"), }; let bestKey = ""; let bestProb = -1; for (const [k, v] of Object.entries(probs)) { const p = typeof v === "number" ? v : 0; if (p > bestProb) { bestProb = p; bestKey = k; } } if (!bestKey) return null; return { pick: bestKey, label: labelMap[bestKey] ?? bestKey, prob: bestProb, all: probs as Record, }; })(); const sport = getPredictionSport(prediction); const isBasketball = sport === "basketball"; const engineDetail = prediction.engine_breakdown.detail; const engineItems = [ { key: "team", icon: LuGauge, label: isBasketball ? uiText("engine-team-basketball", "Takım Formu") : uiText("engine-team-football", "Takım Gücü"), value: prediction.engine_breakdown.team, color: "blue.400", detail: engineDetail?.team, }, { key: "player", icon: LuSparkles, label: isBasketball ? uiText("engine-player-basketball", "Kadro Etkisi") : uiText("engine-player-football", "Oyuncu Etkisi"), value: prediction.engine_breakdown.player, color: "green.400", detail: engineDetail?.player, }, { key: "odds", icon: LuTrendingUp, label: uiText("engine-odds", "Oran Analizi"), value: prediction.engine_breakdown.odds, color: "orange.400", detail: engineDetail?.odds, }, { key: "referee", icon: LuShieldAlert, label: isBasketball ? uiText("engine-referee-basketball", "Yardımcı Sinyaller") : uiText("engine-referee-football", "Hakem Etkisi"), value: prediction.engine_breakdown.referee, color: "purple.400", detail: engineDetail?.referee, }, ]; const liveScoreHome = prediction.match_info?.current_score_home; const liveScoreAway = prediction.match_info?.current_score_away; const isLive = Boolean(prediction.match_info?.is_live); const isStale = Boolean(prediction.prediction_freshness?.is_stale_for_live); const contradictions = prediction.match_commentary?.contradictions || []; const pickCardLabels = { confidence: uiText("confidence-label", "Güven"), odds: uiText("odds-label", "Oran"), recommendedStake: uiText("stake-label-short", "Stake"), playScore: uiText("play-score-label", "Model Sinyali"), playability: uiText("playability-label", "Model sinyali"), confidenceInterval: uiText("confidence-interval", "Güven Aralığı"), confidenceBand: uiText("confidence-band", "Band"), confidenceIntervalWarning: uiText( "confidence-interval-warning", "Güven aralığı geniş. Sinyal olsa bile tek başına oynanması önerilmez.", ), theoreticalEdgeInline: uiText("theoretical-edge-inline", "Teorik avantaj"), modelProbability: uiText("model-probability-short", "Model"), marketProbability: uiText("market-probability-short", "Piyasa"), }; const leagueConfidence = prediction.match_info?.league_confidence; const leagueConfStyles: Record = { high: { color: "green", label: uiText("league-conf-high", "Bu ligde model güçlü"), }, medium: { color: "yellow", label: uiText("league-conf-medium", "Bu ligde model orta"), }, low: { color: "red", label: uiText("league-conf-low", "Bu ligde model zayıf"), }, }; const leagueConfMeta = leagueConfidence ? leagueConfStyles[leagueConfidence.label] : null; return ( {leagueConfidence && leagueConfMeta ? ( {leagueConfMeta.label} {uiText("league-conf-basis", "geçmiş performans")}: ROI{" "} {leagueConfidence.bet_roi > 0 ? "+" : ""} {leagueConfidence.bet_roi}% · {leagueConfidence.bet_n}{" "} {uiText("bets-short", "bahis")} ) : null} {isLive ? ( 🔴 {uiText("live", "CANLI")} {liveScoreHome != null && liveScoreAway != null ? ( {prediction.match_info.home_team} {liveScoreHome} -{" "} {liveScoreAway} {prediction.match_info.away_team} ) : null} {isStale ? ( {uiText("pre-match-prediction", "Maç öncesi tahmin")} ) : null} ) : null} {contradictions.length ? ( {uiText("prediction-contradictions", "Tahmin Çelişkileri")} {contradictions.map((text, idx) => ( • {text} ))} ) : null} {uiText( "model-signal-disclaimer", "Bu bir model sinyalidir; kesin sonuç, garanti veya tutma yüzdesi değildir. Sinyal puanı maç içi varyans, kadro ve veri kalitesi nedeniyle yanılabilir.", )} {matchResultPrediction ? ( {uiText("match-result-prediction", "Maç Sonucu Tahmini")} {matchResultPrediction.label} {uiText( "match-result-copy", "Modelin en olası gördüğü sonuç (kim kazanır).", )} {formatPercent(matchResultPrediction.prob * 100, 0)} {uiText("probability-short", "olasılık")} {/* 1X2 dağılımı */} {["1", "X", "2"].map((k) => { const p = matchResultPrediction.all[k] ?? 0; const lbl: Record = { "1": prediction.match_info?.home_team || uiText("home", "Ev Sahibi"), X: uiText("draw", "Beraberlik"), "2": prediction.match_info?.away_team || uiText("away", "Deplasman"), }; const isTop = k === matchResultPrediction.pick; return ( {lbl[k]} {formatPercent(p * 100, 0)} ); })} {uiText( "match-result-vs-value", "Bu en olası sonuçtur. Aşağıdaki “Değerli Bahis” ise orana göre en kârlı görülen seçimdir — ikisi farklı olabilir.", )} ) : null} {recommendedPick ? ( {uiText("main-recommendation", "Değerli Bahis")} {recommendedPick.pick} {getMarketLabel(recommendedPick.market, marketLabels)}{" "} {uiText("best-market-copy", "marketinde en güçlü seçim.")} {getConfidenceBandLabel( prediction.bet_advice.confidence_band, ui, )} {recommendedPick.confidence_interval ? ( {formatInterval(recommendedPick.confidence_interval)} ) : null} {uiText("quick-read", "Hızlı yorum")} ) : null} {prediction.risk.is_surprise_risk || prediction.risk.warnings?.length ? ( {uiText("risk-commentary", "Risk Yorumu")} {prediction.risk.surprise_comment || (prediction.risk.surprise_type ? `${resolveReason(prediction.risk.surprise_type)}` : uiText( "risk-default-comment", "Model bu maçta ekstra dikkat istiyor.", ))} {prediction.risk.surprise_score !== undefined ? ( {uiText("surprise-score", "Sürpriz skoru")}:{" "} {formatPercent(prediction.risk.surprise_score, 0)} ) : null} {prediction.risk.surprise_breakdown?.length ? ( {prediction.risk.surprise_breakdown.map((entry) => ( = 15 ? "red" : entry.points >= 8 ? "orange" : "yellow" } variant="subtle" > +{entry.points.toFixed(0)} {entry.label} ))} ) : ( )} ) : null} {engineItems.map((item) => ( {item.label} {item.detail?.label ? ( {getEngineLabelText(item.detail.label, ui)} ) : null} +{item.value.toFixed(1)} {item.detail?.interpretation ? ( {item.detail.interpretation} ) : null} ))} {recommendedPick ? ( ) : null} {prediction.supporting_picks?.length ? ( {prediction.supporting_picks.map((pick) => ( 0 ? "blue" : "orange"} marketLabels={marketLabels} labels={pickCardLabels} ui={ui} /> ))} ) : null} {prediction.match_commentary?.headline || prediction.match_commentary?.summary ? ( {prediction.match_commentary.headline ? ( {prediction.match_commentary.headline} ) : null} {prediction.match_commentary.summary ? ( {prediction.match_commentary.summary} ) : null} {prediction.match_commentary.notes?.length ? ( {prediction.match_commentary.notes.map((note, idx) => ( • {note} ))} ) : null} ) : null} {prediction.v27_engine ? ( ) : null} {prediction.bet_advice.playable ? uiText("bet-advice-play", "OYNA") : uiText("bet-advice-pass", "OYNAMA")} {getConfidenceBandLabel( prediction.bet_advice.confidence_band, ui, )} {getSignalTierLabel(prediction.bet_advice.signal_tier, ui)} {resolveReason(prediction.bet_advice.reason)} {uiText("recommended-stake-inline", "Önerilen miktar")}:{" "} {formatUnits(prediction.bet_advice.suggested_stake_units)} ); }