This commit is contained in:
@@ -246,27 +246,34 @@ export default function MatchDetailContent() {
|
||||
{/* Score */}
|
||||
<VStack gap={1} flexShrink={0}>
|
||||
{match.score && (isLive || isFinished) ? (
|
||||
<HStack gap={3}>
|
||||
<Text
|
||||
fontSize="5xl"
|
||||
fontWeight="900"
|
||||
lineHeight="1"
|
||||
color={isLive ? "red.500" : "fg"}
|
||||
>
|
||||
{match.score.home}
|
||||
</Text>
|
||||
<Text fontSize="2xl" color="fg.muted" fontWeight="300">
|
||||
:
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="5xl"
|
||||
fontWeight="900"
|
||||
lineHeight="1"
|
||||
color={isLive ? "red.500" : "fg"}
|
||||
>
|
||||
{match.score.away}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<>
|
||||
<HStack gap={3}>
|
||||
<Text
|
||||
fontSize="4xl"
|
||||
fontWeight="900"
|
||||
color={isLive ? "red.500" : "fg"}
|
||||
>
|
||||
{match.score.home}
|
||||
</Text>
|
||||
<Text fontSize="2xl" color="fg.muted">
|
||||
-
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="4xl"
|
||||
fontWeight="900"
|
||||
color={isLive ? "red.500" : "fg"}
|
||||
>
|
||||
{match.score.away}
|
||||
</Text>
|
||||
</HStack>
|
||||
{match.score.htHome != null &&
|
||||
match.score.htAway != null && (
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
(HT: {match.score.htHome}-{match.score.htAway})
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Text fontSize="xl" fontWeight="bold" color="fg.muted">
|
||||
{t("vs")}
|
||||
|
||||
@@ -143,6 +143,36 @@ function formatUnits(value?: number): string {
|
||||
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): string {
|
||||
switch ((label || "").toUpperCase()) {
|
||||
case "YUKSEK":
|
||||
return "Yüksek";
|
||||
case "ORTA":
|
||||
return "Orta";
|
||||
case "DUSUK":
|
||||
return "Düşük";
|
||||
case "COK_DUSUK":
|
||||
return "Çok Düşük";
|
||||
default:
|
||||
return label || "";
|
||||
}
|
||||
}
|
||||
|
||||
function getRiskPalette(level: string) {
|
||||
switch (level.toUpperCase()) {
|
||||
case "LOW":
|
||||
@@ -731,6 +761,58 @@ function SummaryTable({
|
||||
</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>
|
||||
{item.is_underdog_reference ? (
|
||||
<Badge colorPalette="gray" variant="outline" title="Underdog tarafının model olasılığı (bilgi amaçlı)">
|
||||
Underdog ref.
|
||||
</Badge>
|
||||
) : null}
|
||||
{item.betting_brain?.trap_market_flag ? (
|
||||
<Badge colorPalette="red" variant="subtle" title={`Piyasa aşırı güveniyor (gap ${(item.betting_brain.trap_market_gap || 0) * 100 | 0}pp)`}>
|
||||
Trap
|
||||
</Badge>
|
||||
) : null}
|
||||
{item.betting_brain?.action === "WATCH_NO_VALUE" ? (
|
||||
<Badge colorPalette="orange" variant="subtle" title="Model favoriyle hemfikir ama oran çok düşük">
|
||||
No-value
|
||||
</Badge>
|
||||
) : null}
|
||||
<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="68px" color={item.ev_edge > 0 ? "green.500" : "red.500"} fontWeight="semibold">
|
||||
{item.ev_edge > 0 ? "+" : ""}
|
||||
{formatPercent(item.ev_edge * 100, 1)}
|
||||
</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>
|
||||
@@ -869,7 +951,7 @@ function MarketBoardSection({
|
||||
value={probability * 100}
|
||||
color={
|
||||
entry.pick === outcome ||
|
||||
entry.pick?.toUpperCase() === outcome.toUpperCase()
|
||||
entry.pick?.toUpperCase() === outcome.toUpperCase()
|
||||
? "green.400"
|
||||
: "blue.400"
|
||||
}
|
||||
@@ -980,6 +1062,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
const sport = getPredictionSport(prediction);
|
||||
const isBasketball = sport === "basketball";
|
||||
|
||||
const engineDetail = prediction.engine_breakdown.detail;
|
||||
const engineItems = [
|
||||
{
|
||||
key: "team",
|
||||
@@ -987,6 +1070,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
label: isBasketball ? "Takim Formu" : "Takim Gucu",
|
||||
value: prediction.engine_breakdown.team,
|
||||
color: "blue.400",
|
||||
detail: engineDetail?.team,
|
||||
},
|
||||
{
|
||||
key: "player",
|
||||
@@ -994,6 +1078,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
label: isBasketball ? "Kadro Etkisi" : "Oyuncu Etkisi",
|
||||
value: prediction.engine_breakdown.player,
|
||||
color: "green.400",
|
||||
detail: engineDetail?.player,
|
||||
},
|
||||
{
|
||||
key: "odds",
|
||||
@@ -1002,17 +1087,84 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
value: prediction.engine_breakdown.odds,
|
||||
color: "orange.400",
|
||||
},
|
||||
{
|
||||
key: "odds",
|
||||
icon: LuTrendingUp,
|
||||
label: "Oran Analizi",
|
||||
value: prediction.engine_breakdown.odds,
|
||||
color: "orange.400",
|
||||
detail: engineDetail?.odds,
|
||||
},
|
||||
{
|
||||
key: "referee",
|
||||
icon: LuShieldAlert,
|
||||
label: isBasketball ? "Yardimci Sinyaller" : "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 || [];
|
||||
|
||||
return (
|
||||
<VStack align="stretch" gap={5}>
|
||||
{isLive ? (
|
||||
<Box
|
||||
p={3}
|
||||
bg={useColorModeValue("red.50", "red.950")}
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue("red.300", "red.800")}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack justify="space-between" align="center">
|
||||
<HStack gap={2}>
|
||||
<Icon as={LuFlame} color="red.500" />
|
||||
<Text fontWeight="bold" color="red.600">
|
||||
🔴 CANLI
|
||||
</Text>
|
||||
{liveScoreHome != null && liveScoreAway != null ? (
|
||||
<Text fontWeight="semibold">
|
||||
{prediction.match_info.home_team} {liveScoreHome} - {liveScoreAway}{" "}
|
||||
{prediction.match_info.away_team}
|
||||
</Text>
|
||||
) : null}
|
||||
</HStack>
|
||||
{isStale ? (
|
||||
<Badge colorPalette="orange" variant="solid">
|
||||
Maç öncesi tahmin
|
||||
</Badge>
|
||||
) : null}
|
||||
</HStack>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{contradictions.length ? (
|
||||
<Box
|
||||
p={3}
|
||||
bg={useColorModeValue("yellow.50", "yellow.950")}
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue("yellow.300", "yellow.800")}
|
||||
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>
|
||||
{contradictions.map((text, idx) => (
|
||||
<Text key={idx} fontSize="sm" color="fg.muted">
|
||||
• {text}
|
||||
</Text>
|
||||
))}
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
<Card.Root bg={pageBg} borderColor={borderColor} borderRadius="2xl">
|
||||
<Card.Body gap={5}>
|
||||
<SectionTitle
|
||||
@@ -1114,7 +1266,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
label={uiText("stake-label", "Onerilen Miktar (Stake)")}
|
||||
value={formatUnits(
|
||||
recommendedPick.stake_units ||
|
||||
prediction.bet_advice.suggested_stake_units,
|
||||
prediction.bet_advice.suggested_stake_units,
|
||||
)}
|
||||
helper={uiText(
|
||||
"stake-info",
|
||||
@@ -1171,7 +1323,7 @@ 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")}
|
||||
@@ -1204,13 +1356,31 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
{formatPercent(prediction.risk.surprise_score, 0)}
|
||||
</Text>
|
||||
) : null}
|
||||
<ReasonList
|
||||
items={[
|
||||
...(prediction.risk.surprise_reasons || []),
|
||||
...prediction.risk.warnings,
|
||||
]}
|
||||
resolveReason={resolveReason}
|
||||
/>
|
||||
{prediction.risk.surprise_breakdown?.length ? (
|
||||
<VStack align="start" gap={1} mt={1}>
|
||||
{prediction.risk.surprise_breakdown.map((entry) => (
|
||||
<HStack key={entry.code} gap={2}>
|
||||
<Badge
|
||||
colorPalette={entry.points >= 15 ? "red" : entry.points >= 8 ? "orange" : "yellow"}
|
||||
variant="subtle"
|
||||
>
|
||||
+{entry.points.toFixed(0)}
|
||||
</Badge>
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
{entry.label}
|
||||
</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
) : (
|
||||
<ReasonList
|
||||
items={[
|
||||
...(prediction.risk.surprise_reasons || []),
|
||||
...prediction.risk.warnings,
|
||||
]}
|
||||
resolveReason={resolveReason}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
@@ -1245,15 +1415,31 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
{item.label}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
+{item.value.toFixed(1)}
|
||||
</Text>
|
||||
<HStack gap={2}>
|
||||
{item.detail?.label ? (
|
||||
<Badge
|
||||
colorPalette={getEngineLabelPalette(item.detail.label)}
|
||||
variant="subtle"
|
||||
>
|
||||
{getEngineLabelText(item.detail.label)}
|
||||
</Badge>
|
||||
) : null}
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
+{item.value.toFixed(1)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</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")} />
|
||||
{item.detail?.interpretation ? (
|
||||
<Text fontSize="xs" color="fg.muted" mt={2}>
|
||||
{item.detail.interpretation}
|
||||
</Text>
|
||||
) : null}
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
@@ -1325,6 +1511,36 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
"Butun secenekleri tek tabloda karsilastir.",
|
||||
)}
|
||||
/>
|
||||
{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"
|
||||
/>
|
||||
{prediction.match_commentary.headline ? (
|
||||
<Text fontSize="md" fontWeight="bold">
|
||||
{prediction.match_commentary.headline}
|
||||
</Text>
|
||||
) : null}
|
||||
{prediction.match_commentary.summary ? (
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
{prediction.match_commentary.summary}
|
||||
</Text>
|
||||
) : null}
|
||||
{prediction.match_commentary.notes?.length ? (
|
||||
<VStack align="start" gap={1}>
|
||||
{prediction.match_commentary.notes.map((note, idx) => (
|
||||
<Text key={idx} fontSize="sm">
|
||||
• {note}
|
||||
</Text>
|
||||
))}
|
||||
</VStack>
|
||||
) : null}
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
) : null}
|
||||
<ScoreCard prediction={prediction} sport={sport} />
|
||||
<MarketBoardSection
|
||||
marketBoard={prediction.market_board}
|
||||
|
||||
Reference in New Issue
Block a user