ch
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m27s

This commit is contained in:
2026-06-02 03:37:07 +03:00
parent 9540ff9d2e
commit 2695cfffb4
7 changed files with 437 additions and 1 deletions
@@ -0,0 +1,329 @@
"use client";
import {
Box,
Flex,
Heading,
Text,
SimpleGrid,
Card,
VStack,
HStack,
Badge,
Spinner,
Table,
} from "@chakra-ui/react";
import {
NativeSelectRoot,
NativeSelectField,
} from "@/components/ui/forms/native-select";
import { useColorModeValue } from "@/components/ui/color-mode";
import { AnimatedCounter } from "@/components/motion";
import { useModelPerformance } from "@/lib/api/admin/use-hooks";
import type { ModelPerformanceMarketDto } from "@/lib/api/admin/types";
import { LuTarget, LuTrendingUp, LuCircleCheck, LuLayers } from "react-icons/lu";
import { useState } from "react";
const MARKET_LABELS: Record<string, string> = {
MS: "Maç Sonucu",
DC: "Çifte Şans",
OU15: "Üst/Alt 1.5",
OU25: "Üst/Alt 2.5",
OU35: "Üst/Alt 3.5",
BTTS: "Karşılıklı Gol",
HT: "İlk Yarı Sonucu",
HT_OU05: "İY Üst/Alt 0.5",
HT_OU15: "İY Üst/Alt 1.5",
HTFT: "İlk Yarı / Maç Sonu",
OE: "Tek / Çift",
CARDS: "Kart Üst/Alt",
HCAP: "Handikap",
};
const calibrationLabel = (c: string): { text: string; color: string } => {
if (c === "good") return { text: "Dürüst", color: "green" };
if (c === "overconfident") return { text: "Fazla iyimser", color: "red" };
return { text: "Düşük tahmin", color: "orange" };
};
const roiColor = (roi: number): string =>
roi > 3 ? "green" : roi < -3 ? "red" : "gray";
export default function ModelPerformanceContent() {
const [days, setDays] = useState(90);
const { data, isLoading } = useModelPerformance(days);
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.200", "gray.700");
const headBg = useColorModeValue("gray.50", "whiteAlpha.50");
const perf = data?.data;
const markets = perf?.markets ?? [];
return (
<VStack align="stretch" gap={6}>
<Flex justify="space-between" align="center" flexWrap="wrap" gap={3}>
<Box>
<Heading size="md">Model Performansı (İleri Test)</Heading>
<Text fontSize="sm" color="fg.muted">
Her market için &quot;model %X dedi gerçekte %Y oldu&quot;.
Sonuçlanmış gerçek maçlardan otomatik hesaplanır (lookahead yok).
</Text>
</Box>
<NativeSelectRoot maxW="180px">
<NativeSelectField
value={String(days)}
onChange={(e) => setDays(Number(e.target.value))}
>
<option value="30">Son 30 gün</option>
<option value="90">Son 90 gün</option>
<option value="180">Son 180 gün</option>
<option value="365">Son 1 yıl</option>
</NativeSelectField>
</NativeSelectRoot>
</Flex>
{isLoading ? (
<Flex justify="center" py={12}>
<Spinner size="lg" />
</Flex>
) : !perf || markets.length === 0 ? (
<Card.Root bg={cardBg} borderColor={borderColor}>
<Card.Body>
<VStack gap={2} py={8}>
<Text fontWeight="semibold">Henüz yeterli sonuçlanmış veri yok</Text>
<Text fontSize="sm" color="fg.muted" textAlign="center">
Tahminler maçlar oynandıkça sonuçlanır. Güvenilir kalibrasyon
için market başına ~100 sonuçlanmış tahmin gerekir; bu birkaç
hafta içinde birikir.
</Text>
</VStack>
</Card.Body>
</Card.Root>
) : (
<>
{/* Summary cards */}
<SimpleGrid columns={{ base: 2, md: 4 }} gap={4}>
<StatCard
icon={<LuLayers />}
label="Sonuçlanan tahmin"
value={perf.settled_runs}
bg={cardBg}
border={borderColor}
/>
<StatCard
icon={<LuTarget />}
label="Sonuçlanan market"
value={perf.settled_markets}
bg={cardBg}
border={borderColor}
/>
<StatCard
icon={<LuCircleCheck />}
label="Dürüst market"
value={markets.filter((m) => m.calibration === "good").length}
bg={cardBg}
border={borderColor}
/>
<StatCard
icon={<LuTrendingUp />}
label="Kârlı market (BET)"
value={markets.filter((m) => m.bet_roi_pct > 3).length}
bg={cardBg}
border={borderColor}
/>
</SimpleGrid>
{/* Calibration + ROI table */}
<Card.Root bg={cardBg} borderColor={borderColor}>
<Card.Body p={0}>
<Box overflowX="auto">
<Table.Root size="sm" striped>
<Table.Header bg={headBg}>
<Table.Row>
<Table.ColumnHeader>Market</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">N</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
Model %
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
Gerçek %
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
Fark
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="center">
Kalibrasyon
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
Bahis
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
İsabet %
</Table.ColumnHeader>
<Table.ColumnHeader textAlign="end">
ROI %
</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{markets.map((m) => {
const cal = calibrationLabel(m.calibration);
return (
<Table.Row key={m.market}>
<Table.Cell>
<VStack align="start" gap={0}>
<Text fontWeight="semibold">{m.market}</Text>
<Text fontSize="xs" color="fg.muted">
{MARKET_LABELS[m.market] ?? m.market}
</Text>
</VStack>
</Table.Cell>
<Table.Cell textAlign="end">{m.samples}</Table.Cell>
<Table.Cell textAlign="end">
{m.shown_pct.toFixed(1)}%
</Table.Cell>
<Table.Cell textAlign="end" fontWeight="semibold">
{m.actual_pct.toFixed(1)}%
</Table.Cell>
<Table.Cell textAlign="end">
<Text
color={`${cal.color}.500`}
fontWeight="medium"
>
{m.gap > 0 ? "+" : ""}
{m.gap.toFixed(1)}
</Text>
</Table.Cell>
<Table.Cell textAlign="center">
<Badge colorPalette={cal.color} variant="subtle">
{cal.text}
</Badge>
</Table.Cell>
<Table.Cell textAlign="end">
{m.bet_count}
</Table.Cell>
<Table.Cell textAlign="end">
{m.bet_count > 0
? `${m.bet_hit_pct.toFixed(0)}%`
: "—"}
</Table.Cell>
<Table.Cell textAlign="end">
{m.bet_count > 0 ? (
<Text
color={`${roiColor(m.bet_roi_pct)}.500`}
fontWeight="semibold"
>
{m.bet_roi_pct > 0 ? "+" : ""}
{m.bet_roi_pct.toFixed(1)}%
</Text>
) : (
"—"
)}
</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
</Table.Root>
</Box>
</Card.Body>
</Card.Root>
{/* Decision rationale (why picks come out) */}
<Card.Root bg={cardBg} borderColor={borderColor}>
<Card.Body>
<Heading size="sm" mb={1}>
Öneri gerekçesi (karar dağılımı)
</Heading>
<Text fontSize="sm" color="fg.muted" mb={4}>
Her market için betting brain&apos;in ne karar verdiği:
BET (oyna), WATCH (izle), REJECT (ele).
</Text>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} gap={3}>
{markets.map((m) => (
<Box
key={m.market}
p={3}
borderWidth="1px"
borderColor={borderColor}
borderRadius="lg"
>
<HStack justify="space-between" mb={2}>
<Text fontWeight="semibold">{m.market}</Text>
<Text fontSize="xs" color="fg.muted">
{MARKET_LABELS[m.market] ?? ""}
</Text>
</HStack>
<HStack gap={1} flexWrap="wrap">
{Object.entries(m.actions)
.sort((a, b) => b[1] - a[1])
.map(([action, count]) => (
<Badge
key={action}
variant="subtle"
colorPalette={
action === "BET"
? "green"
: action === "WATCH"
? "blue"
: action === "REJECT"
? "gray"
: "purple"
}
>
{action}: {count}
</Badge>
))}
</HStack>
</Box>
))}
</SimpleGrid>
</Card.Body>
</Card.Root>
<Text fontSize="xs" color="fg.muted">
Son güncelleme:{" "}
{perf.generated_at
? new Date(perf.generated_at).toLocaleString("tr-TR")
: "—"}{" "}
· Pencere: {perf.window_days} gün · ECE düşük = daha dürüst
</Text>
</>
)}
</VStack>
);
}
function StatCard({
icon,
label,
value,
bg,
border,
}: {
icon: React.ReactNode;
label: string;
value: number;
bg: string;
border: string;
}) {
return (
<Card.Root bg={bg} borderColor={border}>
<Card.Body>
<HStack gap={3}>
<Box color="primary.400" fontSize="xl">
{icon}
</Box>
<VStack align="start" gap={0}>
<Text fontSize="2xl" fontWeight="bold">
<AnimatedCounter value={value} />
</Text>
<Text fontSize="xs" color="fg.muted">
{label}
</Text>
</VStack>
</HStack>
</Card.Body>
</Card.Root>
);
}