gg
Deploy Iddaai Frontend / build-and-deploy (push) Failing after 34s

This commit is contained in:
2026-05-10 22:59:27 +03:00
parent 6dadc5f613
commit 5c8619b282
161 changed files with 6708 additions and 3435 deletions
+371 -122
View File
@@ -52,30 +52,54 @@ function formatReasonFallback(reason: string): string {
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ş.";
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 (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
.replace(/_/g, " ")
.replace(/^\w/, (char) => char.toUpperCase());
}
return reason;
}
@@ -101,7 +125,8 @@ function formatEdgeSignal(value?: number): string {
}
function getEdgePalette(value?: number): string {
if (value === undefined || value === null || Number.isNaN(value)) return "gray";
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";
@@ -211,7 +236,10 @@ function getMarketLabel(
market: string,
marketLabels?: Record<string, string>,
): string {
if (marketLabels && Object.prototype.hasOwnProperty.call(marketLabels, market)) {
if (
marketLabels &&
Object.prototype.hasOwnProperty.call(marketLabels, market)
) {
return marketLabels[market];
}
@@ -278,7 +306,13 @@ function getPredictionSport(prediction: MatchPredictionDto): SportType {
return "football";
}
const SIGNAL_TIER_ORDER: SignalTier[] = ["CORE", "VALUE", "LEAN", "LONGSHOT", "PASS"];
const SIGNAL_TIER_ORDER: SignalTier[] = [
"CORE",
"VALUE",
"LEAN",
"LONGSHOT",
"PASS",
];
function getSignalTierPalette(tier?: SignalTier) {
switch (tier) {
@@ -318,7 +352,12 @@ function TooltipIcon({ content }: { content: string }) {
positioning={{ placement: "top" }}
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
>
<IconButton aria-label="Bilgi" variant="ghost" size="2xs" colorPalette="gray">
<IconButton
aria-label="Bilgi"
variant="ghost"
size="2xs"
colorPalette="gray"
>
<LuCircleHelp />
</IconButton>
</Tooltip>
@@ -361,7 +400,13 @@ function MetricTile({
const bg = useColorModeValue("gray.50", "whiteAlpha.50");
const borderColor = useColorModeValue("gray.200", "gray.700");
return (
<Box p={3.5} bg={bg} borderWidth="1px" borderColor={borderColor} borderRadius="xl">
<Box
p={3.5}
bg={bg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="xl"
>
<HStack justify="space-between" mb={1.5}>
<Text fontSize="xs" color="fg.muted" fontWeight="medium">
{label}
@@ -388,7 +433,12 @@ function Bar({
}) {
return (
<Box h={height} w="full" bg={trackBg} borderRadius="full" overflow="hidden">
<Box h="full" w={`${Math.max(0, Math.min(100, value))}%`} bg={color} borderRadius="full" />
<Box
h="full"
w={`${Math.max(0, Math.min(100, value))}%`}
bg={color}
borderRadius="full"
/>
</Box>
);
}
@@ -484,7 +534,12 @@ function PickCard({
return (
<Card.Root bg={bg} borderColor={borderColor} borderRadius="2xl">
<Card.Body gap={4}>
<Flex justify="space-between" align={{ base: "start", md: "center" }} direction={{ base: "column", md: "row" }} gap={3}>
<Flex
justify="space-between"
align={{ base: "start", md: "center" }}
direction={{ base: "column", md: "row" }}
gap={3}
>
<VStack align="start" gap={2}>
<Badge colorPalette={palette} variant="solid" borderRadius="full">
{title}
@@ -493,30 +548,50 @@ function PickCard({
{pick.pick}
</Text>
<HStack gap={2} flexWrap="wrap">
<Badge variant="subtle">{getMarketLabel(pick.market, marketLabels)}</Badge>
<Badge colorPalette={pick.playable ? "green" : "gray"} variant="subtle">
<Badge variant="subtle">
{getMarketLabel(pick.market, marketLabels)}
</Badge>
<Badge
colorPalette={pick.playable ? "green" : "gray"}
variant="subtle"
>
{pick.bet_grade}
</Badge>
<Badge colorPalette={getSignalTierPalette(pick.signal_tier)} variant="subtle">
<Badge
colorPalette={getSignalTierPalette(pick.signal_tier)}
variant="subtle"
>
{getSignalTierLabel(pick.signal_tier)}
</Badge>
<Badge colorPalette={confidenceBandPalette} variant="subtle">
{getConfidenceBandLabel(pick.confidence_interval?.band)}
</Badge>
<Badge colorPalette={getEdgePalette(pick.ev_edge)} variant="subtle">
<Badge
colorPalette={getEdgePalette(pick.ev_edge)}
variant="subtle"
>
Teorik avantaj {formatEdgeSignal(pick.ev_edge)}
</Badge>
</HStack>
</VStack>
<SimpleGrid columns={2} gap={3} minW={{ base: "full", md: "320px" }}>
<MetricTile label={labels.confidence} value={formatPercent(pick.calibrated_confidence, 0)} />
<MetricTile
label={labels.confidence}
value={formatPercent(pick.calibrated_confidence, 0)}
/>
<MetricTile label={labels.odds} value={formatOdds(pick.odds)} />
<MetricTile
label={labels.recommendedStake}
value={formatUnits(pick.stake_units || stakeFallback)}
/>
<MetricTile label={labels.playScore} value={formatSignalScore(pick.play_score)} />
<MetricTile label="Guven Araligi" value={formatInterval(pick.confidence_interval)} />
<MetricTile
label={labels.playScore}
value={formatSignalScore(pick.play_score)}
/>
<MetricTile
label="Guven Araligi"
value={formatInterval(pick.confidence_interval)}
/>
<MetricTile
label="Band"
value={getConfidenceBandLabel(pick.confidence_interval?.band)}
@@ -524,7 +599,10 @@ function PickCard({
/>
</SimpleGrid>
</Flex>
<ProbabilitySplit modelProb={pick.probability} impliedProb={pick.implied_prob} />
<ProbabilitySplit
modelProb={pick.probability}
impliedProb={pick.implied_prob}
/>
<Box>
<HStack justify="space-between" mb={1.5}>
<Text fontSize="sm" fontWeight="semibold">
@@ -540,7 +618,10 @@ function PickCard({
trackBg={trackBg}
/>
</Box>
<ReasonList items={pick.decision_reasons} resolveReason={resolveReason} />
<ReasonList
items={pick.decision_reasons}
resolveReason={resolveReason}
/>
{pick.confidence_interval && !pick.confidence_interval.threshold_met ? (
<Box
p={3}
@@ -550,7 +631,8 @@ function PickCard({
borderColor={intervalWarningBorder}
>
<Text fontSize="sm" color="fg.muted">
Guven araligi genis. Sinyal olsa bile tek basina oynanmasi onerilmez.
Guven araligi genis. Sinyal olsa bile tek basina oynanmasi
onerilmez.
</Text>
</Box>
) : null}
@@ -582,48 +664,73 @@ function SummaryTable({
{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");
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.calibrated_confidence - left.calibrated_confidence;
})
.map((item) => (
<Flex
key={`${item.market}-${item.pick}`}
justify="space-between"
align={{ base: "start", md: "center" }}
direction={{ base: "column", md: "row" }}
gap={3}
px={3}
py={3}
borderRadius="xl"
bg={item.playable ? highlightBg : "transparent"}
borderWidth="1px"
borderColor={item.playable ? "green.200" : borderColor}
>
<HStack gap={2} flexWrap="wrap">
<Badge colorPalette={item.playable ? "green" : "gray"} variant="subtle">
{item.bet_grade}
</Badge>
<Badge colorPalette={getSignalTierPalette(item.signal_tier)} variant="subtle">
{getSignalTierLabel(item.signal_tier)}
</Badge>
<Text fontWeight="semibold">{getMarketLabel(item.market, marketLabels)}</Text>
<Text color="fg.muted">{item.pick}</Text>
</HStack>
<HStack gap={5} fontSize="sm">
<Text minW="48px">{formatOdds(item.odds)}</Text>
<Text minW="96px" color={`${getEdgePalette(item.ev_edge)}.500`} fontWeight="semibold">
{formatEdgeSignal(item.ev_edge)}
</Text>
<Text minW="48px">{formatPercent(item.calibrated_confidence, 0)}</Text>
<Badge colorPalette={getConfidenceBandPalette(item.confidence_interval?.band)} variant="subtle">
{getConfidenceBandLabel(item.confidence_interval?.band)}
</Badge>
<Badge variant="surface">{formatUnits(item.stake_units)}</Badge>
</HStack>
</Flex>
))}
<Flex
key={`${item.market}-${item.pick}`}
justify="space-between"
align={{ base: "start", md: "center" }}
direction={{ base: "column", md: "row" }}
gap={3}
px={3}
py={3}
borderRadius="xl"
bg={item.playable ? highlightBg : "transparent"}
borderWidth="1px"
borderColor={item.playable ? "green.200" : borderColor}
>
<HStack gap={2} flexWrap="wrap">
<Badge
colorPalette={item.playable ? "green" : "gray"}
variant="subtle"
>
{item.bet_grade}
</Badge>
<Badge
colorPalette={getSignalTierPalette(item.signal_tier)}
variant="subtle"
>
{getSignalTierLabel(item.signal_tier)}
</Badge>
<Text fontWeight="semibold">
{getMarketLabel(item.market, marketLabels)}
</Text>
<Text color="fg.muted">{item.pick}</Text>
</HStack>
<HStack gap={5} fontSize="sm">
<Text minW="48px">{formatOdds(item.odds)}</Text>
<Text
minW="96px"
color={`${getEdgePalette(item.ev_edge)}.500`}
fontWeight="semibold"
>
{formatEdgeSignal(item.ev_edge)}
</Text>
<Text minW="48px">
{formatPercent(item.calibrated_confidence, 0)}
</Text>
<Badge
colorPalette={getConfidenceBandPalette(
item.confidence_interval?.band,
)}
variant="subtle"
>
{getConfidenceBandLabel(item.confidence_interval?.band)}
</Badge>
<Badge variant="surface">
{formatUnits(item.stake_units)}
</Badge>
</HStack>
</Flex>
))}
</VStack>
</Card.Body>
</Card.Root>
@@ -650,7 +757,9 @@ function MarketBoardSection({
if (!marketBoard || !Object.keys(marketBoard).length) return null;
const summaryByMarket = new Map((betSummary || []).map((item) => [item.market, item]));
const summaryByMarket = new Map(
(betSummary || []).map((item) => [item.market, item]),
);
const orderedEntries = Object.entries(marketBoard).sort(([left], [right]) => {
const leftIndex = MARKET_ORDER.indexOf(left);
const rightIndex = MARKET_ORDER.indexOf(right);
@@ -662,11 +771,7 @@ function MarketBoardSection({
return (
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
<Card.Body gap={4}>
<SectionTitle
icon={LuChartNoAxesCombined}
title={title}
info={info}
/>
<SectionTitle icon={LuChartNoAxesCombined} title={title} info={info} />
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
{orderedEntries.map(([market, entry]) => {
if (!entry?.probs) return null;
@@ -690,21 +795,33 @@ function MarketBoardSection({
</Text>
<HStack gap={2} flexWrap="wrap">
{summary ? (
<Badge colorPalette={summary.playable ? "green" : "gray"} variant="subtle">
<Badge
colorPalette={summary.playable ? "green" : "gray"}
variant="subtle"
>
{summary.playable ? "Oynanabilir" : "Riskli"}
</Badge>
) : null}
{summary?.signal_tier ? (
<Badge colorPalette={getSignalTierPalette(summary.signal_tier)} variant="subtle">
<Badge
colorPalette={getSignalTierPalette(
summary.signal_tier,
)}
variant="subtle"
>
{getSignalTierLabel(summary.signal_tier)}
</Badge>
) : null}
{summary?.bet_grade ? <Badge variant="outline">{summary.bet_grade}</Badge> : null}
{summary?.bet_grade ? (
<Badge variant="outline">{summary.bet_grade}</Badge>
) : null}
</HStack>
</VStack>
{entry.pick ? (
<Badge
colorPalette={getConfidenceBandPalette(entry.confidence_band || interval?.band)}
colorPalette={getConfidenceBandPalette(
entry.confidence_band || interval?.band,
)}
variant="subtle"
borderRadius="full"
>
@@ -720,7 +837,11 @@ function MarketBoardSection({
/>
<MetricTile
label="Kalibre Guven"
value={summary ? formatPercent(summary.calibrated_confidence, 0) : "-"}
value={
summary
? formatPercent(summary.calibrated_confidence, 0)
: "-"
}
accent={summary?.playable ? "green.500" : "orange.500"}
/>
<MetricTile
@@ -807,7 +928,14 @@ function ScoreCard({
</SimpleGrid>
<SimpleGrid columns={{ base: 2, md: 5 }} gap={2}>
{prediction.scenario_top5.map((scenario) => (
<Box key={`${scenario.score}-${scenario.prob}`} p={3} bg={subBg} borderWidth="1px" borderColor={borderColor} borderRadius="xl">
<Box
key={`${scenario.score}-${scenario.prob}`}
p={3}
bg={subBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="xl"
>
<Text fontSize="lg" fontWeight="bold">
{scenario.score}
</Text>
@@ -835,7 +963,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
const ui = messages.predictions?.ui;
const uiText = (key: string, fallback: string) => ui?.[key] || fallback;
const resolveReason = (reason: string) =>
getPredictionReasonText(reason, messages.predictions?.["prediction-reasons"]);
getPredictionReasonText(
reason,
messages.predictions?.["prediction-reasons"],
);
const pageBg = useColorModeValue("gray.50", "gray.900");
const cardBg = useColorModeValue("white", "gray.800");
@@ -864,7 +995,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
value: prediction.engine_breakdown.player,
color: "green.400",
},
{ key: "odds", icon: LuTrendingUp, label: "Oran Analizi", value: prediction.engine_breakdown.odds, color: "orange.400" },
{
key: "odds",
icon: LuTrendingUp,
label: "Oran Analizi",
value: prediction.engine_breakdown.odds,
color: "orange.400",
},
{
key: "referee",
icon: LuShieldAlert,
@@ -894,30 +1031,50 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
borderRadius="xl"
>
<HStack align="start" gap={2}>
<Icon as={LuShieldAlert} boxSize={4.5} color="orange.500" mt={0.5} />
<Icon
as={LuShieldAlert}
boxSize={4.5}
color="orange.500"
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.
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>
{recommendedPick ? (
<Grid templateColumns={{ base: "1fr", xl: "1.4fr 1fr" }} gap={4}>
<Box p={4} bg={useColorModeValue("green.50", "green.950")} borderWidth="1px" borderColor={useColorModeValue("green.200", "green.800")} borderRadius="2xl">
<Box
p={4}
bg={useColorModeValue("green.50", "green.950")}
borderWidth="1px"
borderColor={useColorModeValue("green.200", "green.800")}
borderRadius="2xl"
>
<HStack justify="space-between" align="start" mb={4}>
<VStack align="start" gap={2}>
<Badge colorPalette="green" variant="solid" borderRadius="full">
<Badge
colorPalette="green"
variant="solid"
borderRadius="full"
>
{uiText("main-recommendation", "Öne Çıkan Sinyal")}
</Badge>
<Text fontSize="2xl" fontWeight="bold">
{recommendedPick.pick}
</Text>
<Text fontSize="sm" color="fg.muted">
{getMarketLabel(recommendedPick.market, marketLabels)} {uiText("best-market-copy", "marketinde en guclu secim.")}
{getMarketLabel(recommendedPick.market, marketLabels)}{" "}
{uiText("best-market-copy", "marketinde en guclu secim.")}
</Text>
<HStack gap={2} flexWrap="wrap">
<Badge colorPalette={mainBandPalette} variant="subtle">
{getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
{getConfidenceBandLabel(
prediction.bet_advice.confidence_band,
)}
</Badge>
{recommendedPick.confidence_interval ? (
<Badge variant="outline">
@@ -929,9 +1086,21 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<Icon as={LuBadgeCheck} boxSize={8} color="green.500" />
</HStack>
<SimpleGrid columns={{ base: 2, md: 4 }} gap={3}>
<MetricTile label={uiText("confidence-label", "Guven")} value={formatPercent(recommendedPick.calibrated_confidence, 0)} />
<MetricTile label={uiText("odds-label", "Oran")} value={formatOdds(recommendedPick.odds)} />
<MetricTile label="Guven Araligi" value={formatInterval(recommendedPick.confidence_interval)} />
<MetricTile
label={uiText("confidence-label", "Guven")}
value={formatPercent(
recommendedPick.calibrated_confidence,
0,
)}
/>
<MetricTile
label={uiText("odds-label", "Oran")}
value={formatOdds(recommendedPick.odds)}
/>
<MetricTile
label="Guven Araligi"
value={formatInterval(recommendedPick.confidence_interval)}
/>
<MetricTile
label={uiText("edge-label", "Teorik Avantaj")}
value={formatEdgeSignal(recommendedPick.ev_edge)}
@@ -955,7 +1124,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
</SimpleGrid>
</Box>
<Box p={4} bg={cardBg} borderWidth="1px" borderColor={borderColor} borderRadius="2xl">
<Box
p={4}
bg={cardBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="2xl"
>
<Text fontSize="sm" fontWeight="semibold" mb={3}>
{uiText("quick-read", "Hizli yorum")}
</Text>
@@ -985,12 +1160,18 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
/>
<MetricTile
label={uiText("lineup-source", "Lineup Kaynagi")}
value={getLineupSourceLabel(prediction.data_quality.lineup_source)}
value={getLineupSourceLabel(
prediction.data_quality.lineup_source,
)}
/>
<MetricTile
label={uiText("model-label", "Model")}
value={prediction.model_version}
/>
<MetricTile label={uiText("model-label", "Model")} value={prediction.model_version} />
</SimpleGrid>
{prediction.risk.is_surprise_risk || prediction.risk.warnings?.length ? (
{prediction.risk.is_surprise_risk ||
prediction.risk.warnings?.length ? (
<Box
p={4}
bg={useColorModeValue("orange.50", "orange.950")}
@@ -999,7 +1180,12 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
borderRadius="2xl"
>
<HStack align="start" gap={3}>
<Icon as={LuTriangleAlert} boxSize={5} color="orange.500" mt={0.5} />
<Icon
as={LuTriangleAlert}
boxSize={5}
color="orange.500"
mt={0.5}
/>
<VStack align="start" gap={1.5}>
<Text fontWeight="semibold">Risk Yorumu</Text>
<Text fontSize="sm" color="fg.muted">
@@ -1009,8 +1195,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
: "Model bu maçta ekstra dikkat istiyor.")}
</Text>
{prediction.risk.surprise_score !== undefined ? (
<Text fontSize="sm" fontWeight="semibold" color="orange.600">
Sürpriz skoru: {formatPercent(prediction.risk.surprise_score, 0)}
<Text
fontSize="sm"
fontWeight="semibold"
color="orange.600"
>
Sürpriz skoru:{" "}
{formatPercent(prediction.risk.surprise_score, 0)}
</Text>
) : null}
<ReasonList
@@ -1032,11 +1223,21 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<SectionTitle
icon={LuChartColumn}
title={t("engine-breakdown-title")}
info={uiText("engine-info", "Tahmini en cok hangi bilesenlerin etkiledigini gosterir.")}
info={uiText(
"engine-info",
"Tahmini en cok hangi bilesenlerin etkiledigini gosterir.",
)}
/>
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
{engineItems.map((item) => (
<Box key={item.key} p={4} bg={useColorModeValue("gray.50", "whiteAlpha.50")} borderWidth="1px" borderColor={borderColor} borderRadius="xl">
<Box
key={item.key}
p={4}
bg={useColorModeValue("gray.50", "whiteAlpha.50")}
borderWidth="1px"
borderColor={borderColor}
borderRadius="xl"
>
<HStack justify="space-between" mb={2}>
<HStack gap={2}>
<Icon as={item.icon} boxSize={4} color={item.color} />
@@ -1048,7 +1249,11 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
+{item.value.toFixed(1)}
</Text>
</HStack>
<Bar value={Math.min(item.value, 100)} color={item.color} trackBg={useColorModeValue("gray.100", "gray.700")} />
<Bar
value={Math.min(item.value, 100)}
color={item.color}
trackBg={useColorModeValue("gray.100", "gray.700")}
/>
</Box>
))}
</SimpleGrid>
@@ -1079,14 +1284,21 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<SectionTitle
icon={LuFlame}
title={uiText("alternative-markets", "Alternatif Marketler")}
info={uiText("alternative-markets-info", "Ana tahmin disindaki secenekler.")}
info={uiText(
"alternative-markets-info",
"Ana tahmin disindaki secenekler.",
)}
/>
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
{prediction.supporting_picks.map((pick) => (
<PickCard
key={`${pick.market}-${pick.pick}`}
pick={pick}
title={pick.playable ? uiText("alternative", "Alternatif") : uiText("pass-market", "PASS market")}
title={
pick.playable
? uiText("alternative", "Alternatif")
: uiText("pass-market", "PASS market")
}
resolveReason={resolveReason}
palette={pick.ev_edge > 0 ? "blue" : "orange"}
marketLabels={marketLabels}
@@ -1108,7 +1320,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
items={prediction.bet_summary || []}
marketLabels={marketLabels}
title={uiText("all-markets-title", "Tum Marketler")}
info={uiText("all-markets-info", "Butun secenekleri tek tabloda karsilastir.")}
info={uiText(
"all-markets-info",
"Butun secenekleri tek tabloda karsilastir.",
)}
/>
<ScoreCard prediction={prediction} sport={sport} />
<MarketBoardSection
@@ -1116,7 +1331,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
betSummary={prediction.bet_summary || []}
marketLabels={marketLabels}
title={t("market-board")}
info={uiText("market-board-info", "Modelin her markette gordugu olasilik dagilimi.")}
info={uiText(
"market-board-info",
"Modelin her markette gordugu olasilik dagilimi.",
)}
/>
{prediction.v27_engine ? (
@@ -1130,16 +1348,37 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
title={t("bet-advice")}
info={uiText("bet-advice-info", "Modelin nihai aksiyon onerisi.")}
/>
<HStack justify="space-between" align={{ base: "start", md: "center" }} flexDir={{ base: "column", md: "row" }} gap={3}>
<HStack
justify="space-between"
align={{ base: "start", md: "center" }}
flexDir={{ base: "column", md: "row" }}
gap={3}
>
<HStack gap={3}>
<Badge colorPalette={prediction.bet_advice.playable ? "green" : "red"} variant="solid" borderRadius="full" fontSize="sm" px={3} py={1}>
<Badge
colorPalette={prediction.bet_advice.playable ? "green" : "red"}
variant="solid"
borderRadius="full"
fontSize="sm"
px={3}
py={1}
>
{prediction.bet_advice.playable ? "OYNA" : "OYNAMA"}
</Badge>
<Badge colorPalette={mainBandPalette} variant="subtle" borderRadius="full" fontSize="sm" px={3} py={1}>
<Badge
colorPalette={mainBandPalette}
variant="subtle"
borderRadius="full"
fontSize="sm"
px={3}
py={1}
>
{getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
</Badge>
<Badge
colorPalette={getSignalTierPalette(prediction.bet_advice.signal_tier)}
colorPalette={getSignalTierPalette(
prediction.bet_advice.signal_tier,
)}
variant="subtle"
borderRadius="full"
fontSize="sm"
@@ -1148,15 +1387,25 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
>
{getSignalTierLabel(prediction.bet_advice.signal_tier)}
</Badge>
<Text color="fg.muted">{resolveReason(prediction.bet_advice.reason)}</Text>
<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")}: {formatUnits(prediction.bet_advice.suggested_stake_units)}
{uiText("recommended-stake-inline", "Onerilen miktar")}:{" "}
{formatUnits(prediction.bet_advice.suggested_stake_units)}
</Badge>
</HStack>
<Separator />
<SectionTitle icon={LuBrain} title={t("reasoning")} info="Modelin bu maci neden bu sekilde okudugunun ust seviye ozeti." />
<ReasonList items={prediction.reasoning_factors} resolveReason={resolveReason} />
<SectionTitle
icon={LuBrain}
title={t("reasoning")}
info="Modelin bu maci neden bu sekilde okudugunun ust seviye ozeti."
/>
<ReasonList
items={prediction.reasoning_factors}
resolveReason={resolveReason}
/>
</Card.Body>
</Card.Root>
</VStack>