This commit is contained in:
@@ -46,6 +46,16 @@ interface PredictionCardProps {
|
||||
prediction: MatchPredictionDto;
|
||||
}
|
||||
|
||||
type PredictionUiMessages = Record<string, string>;
|
||||
|
||||
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)$/);
|
||||
@@ -158,16 +168,16 @@ function getEngineLabelPalette(label?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getEngineLabelText(label?: string): string {
|
||||
function getEngineLabelText(label?: string, ui?: PredictionUiMessages): string {
|
||||
switch ((label || "").toUpperCase()) {
|
||||
case "YUKSEK":
|
||||
return "Yüksek";
|
||||
return getUiText(ui, "engine-label-high", "Yüksek");
|
||||
case "ORTA":
|
||||
return "Orta";
|
||||
return getUiText(ui, "engine-label-medium", "Orta");
|
||||
case "DUSUK":
|
||||
return "Düşük";
|
||||
return getUiText(ui, "engine-label-low", "Düşük");
|
||||
case "COK_DUSUK":
|
||||
return "Çok Düşük";
|
||||
return getUiText(ui, "engine-label-very-low", "Çok Düşük");
|
||||
default:
|
||||
return label || "";
|
||||
}
|
||||
@@ -214,23 +224,30 @@ function getConfidenceBandPalette(band?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function getConfidenceBandLabel(band?: string) {
|
||||
function getConfidenceBandLabel(band?: string, ui?: PredictionUiMessages) {
|
||||
switch ((band || "").toUpperCase()) {
|
||||
case "HIGH":
|
||||
return "Yüksek";
|
||||
return getUiText(ui, "confidence-high", "Yüksek");
|
||||
case "MEDIUM":
|
||||
return "Orta";
|
||||
return getUiText(ui, "confidence-medium", "Orta");
|
||||
case "LOW":
|
||||
return "Düşük";
|
||||
return getUiText(ui, "confidence-low", "Düşük");
|
||||
default:
|
||||
return "Belirsiz";
|
||||
return getUiText(ui, "confidence-unknown", "Belirsiz");
|
||||
}
|
||||
}
|
||||
|
||||
function getLineupSourceLabel(source?: string): string {
|
||||
if (source === "confirmed_live") return "Onayli ilk 11";
|
||||
if (source === "probable_xi") return "Muhtemel ilk 11";
|
||||
return source ? formatReasonFallback(source) : "Bilinmiyor";
|
||||
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(
|
||||
@@ -359,22 +376,28 @@ function getSignalTierPalette(tier?: SignalTier) {
|
||||
}
|
||||
}
|
||||
|
||||
function getSignalTierLabel(tier?: SignalTier) {
|
||||
function getSignalTierLabel(tier?: SignalTier, ui?: PredictionUiMessages) {
|
||||
switch (tier) {
|
||||
case "CORE":
|
||||
return "Çekirdek";
|
||||
return getUiText(ui, "signal-tier-core", "Çekirdek");
|
||||
case "VALUE":
|
||||
return "Değer";
|
||||
return getUiText(ui, "signal-tier-value", "Değer");
|
||||
case "LEAN":
|
||||
return "Yorum";
|
||||
return getUiText(ui, "signal-tier-lean", "Yorum");
|
||||
case "LONGSHOT":
|
||||
return "Sürpriz";
|
||||
return getUiText(ui, "signal-tier-longshot", "Sürpriz");
|
||||
default:
|
||||
return "Pas";
|
||||
return getUiText(ui, "signal-tier-pass", "Pas");
|
||||
}
|
||||
}
|
||||
|
||||
function TooltipIcon({ content }: { content: string }) {
|
||||
function TooltipIcon({
|
||||
content,
|
||||
ariaLabel = "Bilgi",
|
||||
}: {
|
||||
content: string;
|
||||
ariaLabel?: string;
|
||||
}) {
|
||||
return (
|
||||
<Tooltip
|
||||
content={content}
|
||||
@@ -383,7 +406,7 @@ function TooltipIcon({ content }: { content: string }) {
|
||||
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
|
||||
>
|
||||
<IconButton
|
||||
aria-label="Bilgi"
|
||||
aria-label={ariaLabel}
|
||||
variant="ghost"
|
||||
size="2xs"
|
||||
colorPalette="gray"
|
||||
@@ -498,9 +521,14 @@ function ReasonList({
|
||||
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;
|
||||
@@ -508,10 +536,10 @@ function ProbabilitySplit({
|
||||
<VStack align="stretch" gap={2}>
|
||||
<Flex justify="space-between">
|
||||
<Text fontSize="xs" color="blue.600" fontWeight="semibold">
|
||||
Model {formatProbability(modelProb, 0)}
|
||||
{labels.model} {formatProbability(modelProb, 0)}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="orange.500" fontWeight="semibold">
|
||||
Piyasa {formatProbability(impliedProb, 0)}
|
||||
{labels.market} {formatProbability(impliedProb, 0)}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box position="relative">
|
||||
@@ -538,6 +566,7 @@ function PickCard({
|
||||
palette,
|
||||
marketLabels,
|
||||
labels,
|
||||
ui,
|
||||
}: {
|
||||
pick: MatchPickDto;
|
||||
stakeFallback?: number;
|
||||
@@ -545,12 +574,19 @@ function PickCard({
|
||||
resolveReason: (reason: string) => string;
|
||||
palette: string;
|
||||
marketLabels?: Record<string, string>;
|
||||
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`);
|
||||
@@ -591,16 +627,16 @@ function PickCard({
|
||||
colorPalette={getSignalTierPalette(pick.signal_tier)}
|
||||
variant="subtle"
|
||||
>
|
||||
{getSignalTierLabel(pick.signal_tier)}
|
||||
{getSignalTierLabel(pick.signal_tier, ui)}
|
||||
</Badge>
|
||||
<Badge colorPalette={confidenceBandPalette} variant="subtle">
|
||||
{getConfidenceBandLabel(pick.confidence_interval?.band)}
|
||||
{getConfidenceBandLabel(pick.confidence_interval?.band, ui)}
|
||||
</Badge>
|
||||
<Badge
|
||||
colorPalette={getEdgePalette(pick.ev_edge)}
|
||||
variant="subtle"
|
||||
>
|
||||
Teorik avantaj {formatEdgeSignal(pick.ev_edge)}
|
||||
{labels.theoreticalEdgeInline} {formatEdgeSignal(pick.ev_edge)}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</VStack>
|
||||
@@ -619,12 +655,12 @@ function PickCard({
|
||||
value={formatSignalScore(pick.play_score)}
|
||||
/>
|
||||
<MetricTile
|
||||
label="Guven Araligi"
|
||||
label={labels.confidenceInterval}
|
||||
value={formatInterval(pick.confidence_interval)}
|
||||
/>
|
||||
<MetricTile
|
||||
label="Band"
|
||||
value={getConfidenceBandLabel(pick.confidence_interval?.band)}
|
||||
label={labels.confidenceBand}
|
||||
value={getConfidenceBandLabel(pick.confidence_interval?.band, ui)}
|
||||
accent={`${confidenceBandPalette}.500`}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
@@ -632,6 +668,10 @@ function PickCard({
|
||||
<ProbabilitySplit
|
||||
modelProb={pick.probability}
|
||||
impliedProb={pick.implied_prob}
|
||||
labels={{
|
||||
model: labels.modelProbability,
|
||||
market: labels.marketProbability,
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<HStack justify="space-between" mb={1.5}>
|
||||
@@ -661,8 +701,7 @@ function PickCard({
|
||||
borderColor={intervalWarningBorder}
|
||||
>
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
Guven araligi genis. Sinyal olsa bile tek basina oynanmasi
|
||||
onerilmez.
|
||||
{labels.confidenceIntervalWarning}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
@@ -676,11 +715,13 @@ function SummaryTable({
|
||||
marketLabels,
|
||||
title,
|
||||
info,
|
||||
ui,
|
||||
}: {
|
||||
items: MatchBetSummaryItemDto[];
|
||||
marketLabels?: Record<string, string>;
|
||||
title: string;
|
||||
info: string;
|
||||
ui?: PredictionUiMessages;
|
||||
}) {
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||
@@ -728,7 +769,7 @@ function SummaryTable({
|
||||
colorPalette={getSignalTierPalette(item.signal_tier)}
|
||||
variant="subtle"
|
||||
>
|
||||
{getSignalTierLabel(item.signal_tier)}
|
||||
{getSignalTierLabel(item.signal_tier, ui)}
|
||||
</Badge>
|
||||
<Text fontWeight="semibold">
|
||||
{getMarketLabel(item.market, marketLabels)}
|
||||
@@ -753,7 +794,7 @@ function SummaryTable({
|
||||
)}
|
||||
variant="subtle"
|
||||
>
|
||||
{getConfidenceBandLabel(item.confidence_interval?.band)}
|
||||
{getConfidenceBandLabel(item.confidence_interval?.band, ui)}
|
||||
</Badge>
|
||||
<Badge variant="surface">
|
||||
{formatUnits(item.stake_units)}
|
||||
@@ -812,7 +853,6 @@ function SummaryTable({
|
||||
<Badge variant="surface">{formatUnits(item.stake_units)}</Badge>
|
||||
</HStack>
|
||||
</Flex> */}
|
||||
|
||||
</VStack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
@@ -825,12 +865,14 @@ function MarketBoardSection({
|
||||
marketLabels,
|
||||
title,
|
||||
info,
|
||||
ui,
|
||||
}: {
|
||||
marketBoard?: Record<string, MarketBoardEntryDto>;
|
||||
betSummary?: MatchBetSummaryItemDto[];
|
||||
marketLabels?: Record<string, string>;
|
||||
title: string;
|
||||
info: string;
|
||||
ui?: PredictionUiMessages;
|
||||
}) {
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||
@@ -881,7 +923,9 @@ function MarketBoardSection({
|
||||
colorPalette={summary.playable ? "green" : "gray"}
|
||||
variant="subtle"
|
||||
>
|
||||
{summary.playable ? "Oynanabilir" : "Riskli"}
|
||||
{summary.playable
|
||||
? getUiText(ui, "playable", "Oynanabilir")
|
||||
: getUiText(ui, "risky", "Riskli")}
|
||||
</Badge>
|
||||
) : null}
|
||||
{summary?.signal_tier ? (
|
||||
@@ -891,7 +935,7 @@ function MarketBoardSection({
|
||||
)}
|
||||
variant="subtle"
|
||||
>
|
||||
{getSignalTierLabel(summary.signal_tier)}
|
||||
{getSignalTierLabel(summary.signal_tier, ui)}
|
||||
</Badge>
|
||||
) : null}
|
||||
{summary?.bet_grade ? (
|
||||
@@ -913,12 +957,16 @@ function MarketBoardSection({
|
||||
</Flex>
|
||||
<SimpleGrid columns={3} gap={2} mb={3}>
|
||||
<MetricTile
|
||||
label="Tutma Olasiligi"
|
||||
label={getUiText(ui, "hit-probability", "Tutma Olasılığı")}
|
||||
value={formatPercent(entry.confidence, 0)}
|
||||
accent="green.500"
|
||||
/>
|
||||
<MetricTile
|
||||
label="Kalibre Guven"
|
||||
label={getUiText(
|
||||
ui,
|
||||
"calibrated-confidence",
|
||||
"Kalibre Güven",
|
||||
)}
|
||||
value={
|
||||
summary
|
||||
? formatPercent(summary.calibrated_confidence, 0)
|
||||
@@ -933,7 +981,8 @@ function MarketBoardSection({
|
||||
</SimpleGrid>
|
||||
{interval ? (
|
||||
<Text fontSize="xs" color="fg.muted" mb={3}>
|
||||
Guven araligi: {formatInterval(interval)}
|
||||
{getUiText(ui, "confidence-interval", "Güven Aralığı")}:{" "}
|
||||
{formatInterval(interval)}
|
||||
</Text>
|
||||
) : null}
|
||||
<VStack align="stretch" gap={2.5}>
|
||||
@@ -951,7 +1000,7 @@ function MarketBoardSection({
|
||||
value={probability * 100}
|
||||
color={
|
||||
entry.pick === outcome ||
|
||||
entry.pick?.toUpperCase() === outcome.toUpperCase()
|
||||
entry.pick?.toUpperCase() === outcome.toUpperCase()
|
||||
? "green.400"
|
||||
: "blue.400"
|
||||
}
|
||||
@@ -973,9 +1022,11 @@ function MarketBoardSection({
|
||||
function ScoreCard({
|
||||
prediction,
|
||||
sport,
|
||||
ui,
|
||||
}: {
|
||||
prediction: MatchPredictionDto;
|
||||
sport: SportType;
|
||||
ui?: PredictionUiMessages;
|
||||
}) {
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||
@@ -987,24 +1038,52 @@ function ScoreCard({
|
||||
<Card.Body gap={4}>
|
||||
<SectionTitle
|
||||
icon={LuTarget}
|
||||
title={isBasketball ? "Sayi Senaryosu" : "Skor Senaryosu"}
|
||||
title={
|
||||
isBasketball
|
||||
? getUiText(ui, "score-scenario-basketball", "Sayı Senaryosu")
|
||||
: getUiText(ui, "score-scenario-football", "Skor Senaryosu")
|
||||
}
|
||||
info={
|
||||
isBasketball
|
||||
? "Beklenen sayi dagilimi ve en olasi mac senaryolari."
|
||||
: "Beklenen skor ve en olasi senaryolar."
|
||||
? getUiText(
|
||||
ui,
|
||||
"score-scenario-info-basketball",
|
||||
"Beklenen sayı dağılımı ve en olası maç senaryoları.",
|
||||
)
|
||||
: getUiText(
|
||||
ui,
|
||||
"score-scenario-info-football",
|
||||
"Beklenen skor ve en olası senaryolar.",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} gap={3}>
|
||||
<MetricTile
|
||||
label={isBasketball ? "Mac Sonu Sayi" : "Mac Sonu"}
|
||||
label={
|
||||
isBasketball
|
||||
? getUiText(ui, "full-time-basketball", "Maç Sonu Sayı")
|
||||
: getUiText(ui, "full-time-football", "Maç Sonu")
|
||||
}
|
||||
value={prediction.score_prediction.ft}
|
||||
/>
|
||||
<MetricTile
|
||||
label={isBasketball ? "Ilk Yari Sayi" : "Ilk Yari"}
|
||||
label={
|
||||
isBasketball
|
||||
? getUiText(ui, "half-time-basketball", "İlk Yarı Sayı")
|
||||
: getUiText(ui, "half-time-football", "İlk Yarı")
|
||||
}
|
||||
value={prediction.score_prediction.ht}
|
||||
/>
|
||||
<MetricTile
|
||||
label={isBasketball ? "Beklenen Toplam Sayi" : "Toplam xG"}
|
||||
label={
|
||||
isBasketball
|
||||
? getUiText(
|
||||
ui,
|
||||
"expected-total-basketball",
|
||||
"Beklenen Toplam Sayı",
|
||||
)
|
||||
: getUiText(ui, "expected-total-football", "Toplam xG")
|
||||
}
|
||||
value={prediction.score_prediction.xg_total.toFixed(2)}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
@@ -1053,6 +1132,16 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
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;
|
||||
@@ -1067,7 +1156,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
{
|
||||
key: "team",
|
||||
icon: LuGauge,
|
||||
label: isBasketball ? "Takim Formu" : "Takim Gucu",
|
||||
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,
|
||||
@@ -1075,7 +1166,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
{
|
||||
key: "player",
|
||||
icon: LuSparkles,
|
||||
label: isBasketball ? "Kadro Etkisi" : "Oyuncu Etkisi",
|
||||
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,
|
||||
@@ -1083,14 +1176,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
{
|
||||
key: "odds",
|
||||
icon: LuTrendingUp,
|
||||
label: "Oran Analizi",
|
||||
value: prediction.engine_breakdown.odds,
|
||||
color: "orange.400",
|
||||
},
|
||||
{
|
||||
key: "odds",
|
||||
icon: LuTrendingUp,
|
||||
label: "Oran Analizi",
|
||||
label: uiText("engine-odds", "Oran Analizi"),
|
||||
value: prediction.engine_breakdown.odds,
|
||||
color: "orange.400",
|
||||
detail: engineDetail?.odds,
|
||||
@@ -1098,7 +1184,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
{
|
||||
key: "referee",
|
||||
icon: LuShieldAlert,
|
||||
label: isBasketball ? "Yardimci Sinyaller" : "Hakem Etkisi",
|
||||
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,
|
||||
@@ -1110,33 +1198,49 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
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"),
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack align="stretch" gap={5}>
|
||||
{isLive ? (
|
||||
<Box
|
||||
p={3}
|
||||
bg={useColorModeValue("red.50", "red.950")}
|
||||
bg={liveBg}
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue("red.300", "red.800")}
|
||||
borderColor={liveBorderColor}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack justify="space-between" align="center">
|
||||
<HStack gap={2}>
|
||||
<Icon as={LuFlame} color="red.500" />
|
||||
<Text fontWeight="bold" color="red.600">
|
||||
🔴 CANLI
|
||||
🔴 {uiText("live", "CANLI")}
|
||||
</Text>
|
||||
{liveScoreHome != null && liveScoreAway != null ? (
|
||||
<Text fontWeight="semibold">
|
||||
{prediction.match_info.home_team} {liveScoreHome} - {liveScoreAway}{" "}
|
||||
{prediction.match_info.away_team}
|
||||
{prediction.match_info.home_team} {liveScoreHome} -{" "}
|
||||
{liveScoreAway} {prediction.match_info.away_team}
|
||||
</Text>
|
||||
) : null}
|
||||
</HStack>
|
||||
{isStale ? (
|
||||
<Badge colorPalette="orange" variant="solid">
|
||||
Maç öncesi tahmin
|
||||
{uiText("pre-match-prediction", "Maç öncesi tahmin")}
|
||||
</Badge>
|
||||
) : null}
|
||||
</HStack>
|
||||
@@ -1146,15 +1250,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
{contradictions.length ? (
|
||||
<Box
|
||||
p={3}
|
||||
bg={useColorModeValue("yellow.50", "yellow.950")}
|
||||
bg={warningBg}
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue("yellow.300", "yellow.800")}
|
||||
borderColor={warningBorderColor}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack align="start" gap={2}>
|
||||
<Icon as={LuTriangleAlert} color="yellow.600" mt={0.5} />
|
||||
<VStack align="start" gap={1}>
|
||||
<Text fontWeight="semibold">Tahmin Çelişkileri</Text>
|
||||
<Text fontWeight="semibold">
|
||||
{uiText("prediction-contradictions", "Tahmin Çelişkileri")}
|
||||
</Text>
|
||||
{contradictions.map((text, idx) => (
|
||||
<Text key={idx} fontSize="sm" color="fg.muted">
|
||||
• {text}
|
||||
@@ -1169,17 +1275,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
<Card.Body gap={5}>
|
||||
<SectionTitle
|
||||
icon={LuBrain}
|
||||
title={uiText("summary-title", "Tahmin Ozeti")}
|
||||
title={uiText("summary-title", "Tahmin Özeti")}
|
||||
info={uiText(
|
||||
"summary-info",
|
||||
"Model sinyallerini ve belirsizlikleri sade sekilde gosterir.",
|
||||
"Model sinyallerini ve belirsizlikleri sade şekilde gösterir.",
|
||||
)}
|
||||
/>
|
||||
<Box
|
||||
p={3}
|
||||
bg={useColorModeValue("orange.50", "orange.950")}
|
||||
bg={orangeBg}
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue("orange.200", "orange.800")}
|
||||
borderColor={orangeBorderColor}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack align="start" gap={2}>
|
||||
@@ -1190,9 +1296,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
mt={0.5}
|
||||
/>
|
||||
<Text fontSize="sm" color="fg.muted" lineHeight="tall">
|
||||
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.
|
||||
{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.",
|
||||
)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
@@ -1201,9 +1308,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
<Grid templateColumns={{ base: "1fr", xl: "1.4fr 1fr" }} gap={4}>
|
||||
<Box
|
||||
p={4}
|
||||
bg={useColorModeValue("green.50", "green.950")}
|
||||
bg={greenBg}
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue("green.200", "green.800")}
|
||||
borderColor={greenBorderColor}
|
||||
borderRadius="2xl"
|
||||
>
|
||||
<HStack justify="space-between" align="start" mb={4}>
|
||||
@@ -1220,12 +1327,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
</Text>
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
{getMarketLabel(recommendedPick.market, marketLabels)}{" "}
|
||||
{uiText("best-market-copy", "marketinde en guclu secim.")}
|
||||
{uiText("best-market-copy", "marketinde en güçlü seçim.")}
|
||||
</Text>
|
||||
<HStack gap={2} flexWrap="wrap">
|
||||
<Badge colorPalette={mainBandPalette} variant="subtle">
|
||||
{getConfidenceBandLabel(
|
||||
prediction.bet_advice.confidence_band,
|
||||
ui,
|
||||
)}
|
||||
</Badge>
|
||||
{recommendedPick.confidence_interval ? (
|
||||
@@ -1239,7 +1347,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
</HStack>
|
||||
<SimpleGrid columns={{ base: 2, md: 4 }} gap={3}>
|
||||
<MetricTile
|
||||
label={uiText("confidence-label", "Guven")}
|
||||
label={uiText("confidence-label", "Güven")}
|
||||
value={formatPercent(
|
||||
recommendedPick.calibrated_confidence,
|
||||
0,
|
||||
@@ -1250,7 +1358,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
value={formatOdds(recommendedPick.odds)}
|
||||
/>
|
||||
<MetricTile
|
||||
label="Guven Araligi"
|
||||
label={uiText("confidence-interval", "Güven Aralığı")}
|
||||
value={formatInterval(recommendedPick.confidence_interval)}
|
||||
/>
|
||||
<MetricTile
|
||||
@@ -1258,19 +1366,19 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
value={formatEdgeSignal(recommendedPick.ev_edge)}
|
||||
helper={uiText(
|
||||
"edge-info",
|
||||
"Model olasiligi ile piyasa olasiligi arasindaki teorik farktir; tutma garantisi veya kesin kazanc beklentisi degildir.",
|
||||
"Model olasılığı ile piyasa olasılığı arasındaki teorik farktır; tutma garantisi veya kesin kazanç beklentisi değildir.",
|
||||
)}
|
||||
accent={`${getEdgePalette(recommendedPick.ev_edge)}.500`}
|
||||
/>
|
||||
<MetricTile
|
||||
label={uiText("stake-label", "Onerilen Miktar (Stake)")}
|
||||
label={uiText("stake-label", "Önerilen Miktar (Stake)")}
|
||||
value={formatUnits(
|
||||
recommendedPick.stake_units ||
|
||||
prediction.bet_advice.suggested_stake_units,
|
||||
prediction.bet_advice.suggested_stake_units,
|
||||
)}
|
||||
helper={uiText(
|
||||
"stake-info",
|
||||
"Stake, bu bahis icin onerilen bahis birimidir. 2.0u demek, kendi bankroll planinizdaki 2 birimlik bahis anlamina gelir.",
|
||||
"Stake, bu bahis için önerilen bahis birimidir. 2.0u, kendi bankroll planınızdaki 2 birimlik bahis anlamına gelir.",
|
||||
)}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
@@ -1284,7 +1392,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
borderRadius="2xl"
|
||||
>
|
||||
<Text fontSize="sm" fontWeight="semibold" mb={3}>
|
||||
{uiText("quick-read", "Hizli yorum")}
|
||||
{uiText("quick-read", "Hızlı yorum")}
|
||||
</Text>
|
||||
<ReasonList
|
||||
items={[
|
||||
@@ -1299,21 +1407,28 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
|
||||
<SimpleGrid columns={{ base: 1, md: 2, xl: 4 }} gap={3}>
|
||||
<MetricTile
|
||||
label="Veri Kalitesi"
|
||||
label={uiText("data-quality", "Veri Kalitesi")}
|
||||
value={formatPercent(prediction.data_quality.score * 100, 0)}
|
||||
helper="Kadro, oran ve mac verisinin ne kadar guvenilir oldugu."
|
||||
helper={uiText(
|
||||
"data-quality-info",
|
||||
"Kadro, oran ve maç verisinin ne kadar güvenilir olduğu.",
|
||||
)}
|
||||
accent={`${qualityPalette}.500`}
|
||||
/>
|
||||
<MetricTile
|
||||
label={t("risk-level")}
|
||||
value={`${prediction.risk.level} (${prediction.risk.score}/100)`}
|
||||
helper="Surpriz ihtimali ve belirsizlik seviyesi."
|
||||
helper={uiText(
|
||||
"risk-info",
|
||||
"Sürpriz ihtimali ve belirsizlik seviyesi.",
|
||||
)}
|
||||
accent={`${riskPalette}.500`}
|
||||
/>
|
||||
<MetricTile
|
||||
label={uiText("lineup-source", "Lineup Kaynagi")}
|
||||
label={uiText("lineup-source", "Kadronun Kaynağı")}
|
||||
value={getLineupSourceLabel(
|
||||
prediction.data_quality.lineup_source,
|
||||
ui,
|
||||
)}
|
||||
/>
|
||||
<MetricTile
|
||||
@@ -1323,12 +1438,12 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
</SimpleGrid>
|
||||
|
||||
{prediction.risk.is_surprise_risk ||
|
||||
prediction.risk.warnings?.length ? (
|
||||
prediction.risk.warnings?.length ? (
|
||||
<Box
|
||||
p={4}
|
||||
bg={useColorModeValue("orange.50", "orange.950")}
|
||||
bg={orangeBg}
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue("orange.200", "orange.800")}
|
||||
borderColor={orangeBorderColor}
|
||||
borderRadius="2xl"
|
||||
>
|
||||
<HStack align="start" gap={3}>
|
||||
@@ -1339,12 +1454,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
mt={0.5}
|
||||
/>
|
||||
<VStack align="start" gap={1.5}>
|
||||
<Text fontWeight="semibold">Risk Yorumu</Text>
|
||||
<Text fontWeight="semibold">
|
||||
{uiText("risk-commentary", "Risk Yorumu")}
|
||||
</Text>
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
{prediction.risk.surprise_comment ||
|
||||
(prediction.risk.surprise_type
|
||||
? `${resolveReason(prediction.risk.surprise_type)}`
|
||||
: "Model bu maçta ekstra dikkat istiyor.")}
|
||||
: uiText(
|
||||
"risk-default-comment",
|
||||
"Model bu maçta ekstra dikkat istiyor.",
|
||||
))}
|
||||
</Text>
|
||||
{prediction.risk.surprise_score !== undefined ? (
|
||||
<Text
|
||||
@@ -1352,7 +1472,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
fontWeight="semibold"
|
||||
color="orange.600"
|
||||
>
|
||||
Sürpriz skoru:{" "}
|
||||
{uiText("surprise-score", "Sürpriz skoru")}:{" "}
|
||||
{formatPercent(prediction.risk.surprise_score, 0)}
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -1361,7 +1481,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
{prediction.risk.surprise_breakdown.map((entry) => (
|
||||
<HStack key={entry.code} gap={2}>
|
||||
<Badge
|
||||
colorPalette={entry.points >= 15 ? "red" : entry.points >= 8 ? "orange" : "yellow"}
|
||||
colorPalette={
|
||||
entry.points >= 15
|
||||
? "red"
|
||||
: entry.points >= 8
|
||||
? "orange"
|
||||
: "yellow"
|
||||
}
|
||||
variant="subtle"
|
||||
>
|
||||
+{entry.points.toFixed(0)}
|
||||
@@ -1395,7 +1521,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
title={t("engine-breakdown-title")}
|
||||
info={uiText(
|
||||
"engine-info",
|
||||
"Tahmini en cok hangi bilesenlerin etkiledigini gosterir.",
|
||||
"Tahmini en çok hangi bileşenlerin etkilediğini gösterir.",
|
||||
)}
|
||||
/>
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
|
||||
@@ -1403,7 +1529,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
<Box
|
||||
key={item.key}
|
||||
p={4}
|
||||
bg={useColorModeValue("gray.50", "whiteAlpha.50")}
|
||||
bg={statCardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
@@ -1421,7 +1547,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
colorPalette={getEngineLabelPalette(item.detail.label)}
|
||||
variant="subtle"
|
||||
>
|
||||
{getEngineLabelText(item.detail.label)}
|
||||
{getEngineLabelText(item.detail.label, ui)}
|
||||
</Badge>
|
||||
) : null}
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
@@ -1432,9 +1558,8 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
<Bar
|
||||
value={Math.min(item.value, 100)}
|
||||
color={item.color}
|
||||
trackBg={useColorModeValue("gray.100", "gray.700")}
|
||||
trackBg={trackBgColor}
|
||||
/>
|
||||
<Bar value={Math.min(item.value, 100)} color={item.color} trackBg={useColorModeValue("gray.100", "gray.700")} />
|
||||
{item.detail?.interpretation ? (
|
||||
<Text fontSize="xs" color="fg.muted" mt={2}>
|
||||
{item.detail.interpretation}
|
||||
@@ -1454,13 +1579,8 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
palette="green"
|
||||
stakeFallback={prediction.bet_advice.suggested_stake_units}
|
||||
marketLabels={marketLabels}
|
||||
labels={{
|
||||
confidence: uiText("confidence-label", "Guven"),
|
||||
odds: uiText("odds-label", "Oran"),
|
||||
recommendedStake: uiText("stake-label-short", "Stake"),
|
||||
playScore: uiText("play-score-label", "Model Sinyali"),
|
||||
playability: uiText("playability-label", "Model sinyali"),
|
||||
}}
|
||||
labels={pickCardLabels}
|
||||
ui={ui}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -1472,7 +1592,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
title={uiText("alternative-markets", "Alternatif Marketler")}
|
||||
info={uiText(
|
||||
"alternative-markets-info",
|
||||
"Ana tahmin disindaki secenekler.",
|
||||
"Ana tahmin dışındaki seçenekler.",
|
||||
)}
|
||||
/>
|
||||
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
|
||||
@@ -1483,18 +1603,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
title={
|
||||
pick.playable
|
||||
? uiText("alternative", "Alternatif")
|
||||
: uiText("pass-market", "PASS market")
|
||||
: uiText("pass-market", "Elenen Market")
|
||||
}
|
||||
resolveReason={resolveReason}
|
||||
palette={pick.ev_edge > 0 ? "blue" : "orange"}
|
||||
marketLabels={marketLabels}
|
||||
labels={{
|
||||
confidence: uiText("confidence-label", "Guven"),
|
||||
odds: uiText("odds-label", "Oran"),
|
||||
recommendedStake: uiText("stake-label-short", "Stake"),
|
||||
playScore: uiText("play-score-label", "Model Sinyali"),
|
||||
playability: uiText("playability-label", "Model sinyali"),
|
||||
}}
|
||||
labels={pickCardLabels}
|
||||
ui={ui}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
@@ -1505,19 +1620,24 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
<SummaryTable
|
||||
items={prediction.bet_summary || []}
|
||||
marketLabels={marketLabels}
|
||||
title={uiText("all-markets-title", "Tum Marketler")}
|
||||
title={uiText("all-markets-title", "Tüm Marketler")}
|
||||
info={uiText(
|
||||
"all-markets-info",
|
||||
"Butun secenekleri tek tabloda karsilastir.",
|
||||
"Bütün seçenekleri tek tabloda karşılaştırır.",
|
||||
)}
|
||||
ui={ui}
|
||||
/>
|
||||
{prediction.match_commentary?.headline || prediction.match_commentary?.summary ? (
|
||||
{prediction.match_commentary?.headline ||
|
||||
prediction.match_commentary?.summary ? (
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
||||
<Card.Body gap={3}>
|
||||
<SectionTitle
|
||||
icon={LuBrain}
|
||||
title="Maç Yorumu"
|
||||
info="Modelin maç hakkındaki insan-okunabilir özeti"
|
||||
title={uiText("match-commentary-title", "Maç Yorumu")}
|
||||
info={uiText(
|
||||
"match-commentary-info",
|
||||
"Modelin maç hakkındaki insan okunabilir özeti.",
|
||||
)}
|
||||
/>
|
||||
{prediction.match_commentary.headline ? (
|
||||
<Text fontSize="md" fontWeight="bold">
|
||||
@@ -1541,7 +1661,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
) : null}
|
||||
<ScoreCard prediction={prediction} sport={sport} />
|
||||
<ScoreCard prediction={prediction} sport={sport} ui={ui} />
|
||||
<MarketBoardSection
|
||||
marketBoard={prediction.market_board}
|
||||
betSummary={prediction.bet_summary || []}
|
||||
@@ -1549,8 +1669,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
title={t("market-board")}
|
||||
info={uiText(
|
||||
"market-board-info",
|
||||
"Modelin her markette gordugu olasilik dagilimi.",
|
||||
"Modelin her markette gördüğü olasılık dağılımı.",
|
||||
)}
|
||||
ui={ui}
|
||||
/>
|
||||
|
||||
{prediction.v27_engine ? (
|
||||
@@ -1562,7 +1683,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
<SectionTitle
|
||||
icon={LuSparkles}
|
||||
title={t("bet-advice")}
|
||||
info={uiText("bet-advice-info", "Modelin nihai aksiyon onerisi.")}
|
||||
info={uiText("bet-advice-info", "Modelin nihai aksiyon önerisi.")}
|
||||
/>
|
||||
<HStack
|
||||
justify="space-between"
|
||||
@@ -1579,7 +1700,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
px={3}
|
||||
py={1}
|
||||
>
|
||||
{prediction.bet_advice.playable ? "OYNA" : "OYNAMA"}
|
||||
{prediction.bet_advice.playable
|
||||
? uiText("bet-advice-play", "OYNA")
|
||||
: uiText("bet-advice-pass", "OYNAMA")}
|
||||
</Badge>
|
||||
<Badge
|
||||
colorPalette={mainBandPalette}
|
||||
@@ -1589,7 +1712,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
px={3}
|
||||
py={1}
|
||||
>
|
||||
{getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
|
||||
{getConfidenceBandLabel(
|
||||
prediction.bet_advice.confidence_band,
|
||||
ui,
|
||||
)}
|
||||
</Badge>
|
||||
<Badge
|
||||
colorPalette={getSignalTierPalette(
|
||||
@@ -1601,14 +1727,14 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
px={3}
|
||||
py={1}
|
||||
>
|
||||
{getSignalTierLabel(prediction.bet_advice.signal_tier)}
|
||||
{getSignalTierLabel(prediction.bet_advice.signal_tier, ui)}
|
||||
</Badge>
|
||||
<Text color="fg.muted">
|
||||
{resolveReason(prediction.bet_advice.reason)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Badge variant="surface" fontSize="sm" px={3} py={1}>
|
||||
{uiText("recommended-stake-inline", "Onerilen miktar")}:{" "}
|
||||
{uiText("recommended-stake-inline", "Önerilen miktar")}:{" "}
|
||||
{formatUnits(prediction.bet_advice.suggested_stake_units)}
|
||||
</Badge>
|
||||
</HStack>
|
||||
@@ -1616,7 +1742,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
<SectionTitle
|
||||
icon={LuBrain}
|
||||
title={t("reasoning")}
|
||||
info="Modelin bu maci neden bu sekilde okudugunun ust seviye ozeti."
|
||||
info={uiText(
|
||||
"reasoning-info",
|
||||
"Modelin bu maçı neden bu şekilde okuduğunun üst seviye özeti.",
|
||||
)}
|
||||
/>
|
||||
<ReasonList
|
||||
items={prediction.reasoning_factors}
|
||||
|
||||
Reference in New Issue
Block a user