Files
2026FIFAWorldCup/platform/web/app/ml-edge/page.tsx

263 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState } from 'react';
import {
analyzeMlEdge,
type MlEdgeRequestPayload,
type MlEdgeResponse,
type MlTrainRequestPayload,
type MlTrainResponse,
type MlTrainingRow,
trainMlModel,
} from '@/lib/analytics-api';
const defaultTrainingRows: MlTrainingRow[] = [
{ home_rest_days: 4, away_rest_days: 3, home_travel_distance_km: 520, away_travel_distance_km: 1100, recent_5_xg_home: 1.8, recent_5_xg_away: 1.0, match_result: 'home' },
{ home_rest_days: 2, away_rest_days: 5, home_travel_distance_km: 220, away_travel_distance_km: 780, recent_5_xg_home: 1.1, recent_5_xg_away: 1.7, match_result: 'away' },
{ home_rest_days: 6, away_rest_days: 4, home_travel_distance_km: 120, away_travel_distance_km: 960, recent_5_xg_home: 2.3, recent_5_xg_away: 1.8, match_result: 'home' },
{ home_rest_days: 3, away_rest_days: 3, home_travel_distance_km: 900, away_travel_distance_km: 900, recent_5_xg_home: 1.2, recent_5_xg_away: 1.3, match_result: 'draw' },
];
export default function MlEdgePage() {
const [modelId, setModelId] = useState('default');
const [matchId, setMatchId] = useState('MATCH-2026-FINALS-07');
const [homeRest, setHomeRest] = useState(6);
const [awayRest, setAwayRest] = useState(4);
const [homeTravel, setHomeTravel] = useState(420);
const [awayTravel, setAwayTravel] = useState(970);
const [homeXg, setHomeXg] = useState(2.0);
const [awayXg, setAwayXg] = useState(1.1);
const [homeOdds, setHomeOdds] = useState(2.05);
const [drawOdds, setDrawOdds] = useState(3.35);
const [awayOdds, setAwayOdds] = useState(3.75);
const [loading, setLoading] = useState(false);
const [trainLoading, setTrainLoading] = useState(false);
const [edgeResult, setEdgeResult] = useState<MlEdgeResponse | null>(null);
const [trainResult, setTrainResult] = useState<MlTrainResponse | null>(null);
const [errorMessage, setErrorMessage] = useState('');
const [trainMessage, setTrainMessage] = useState('');
async function runEdge() {
setLoading(true);
setErrorMessage('');
try {
const payload: MlEdgeRequestPayload = {
model_id: modelId,
match_id: matchId,
home_rest_days: homeRest,
away_rest_days: awayRest,
home_travel_distance_km: homeTravel,
away_travel_distance_km: awayTravel,
recent_5_xg_home: homeXg,
recent_5_xg_away: awayXg,
home_implied_odds: homeOdds,
draw_implied_odds: drawOdds,
away_implied_odds: awayOdds,
};
const data = await analyzeMlEdge(payload);
setEdgeResult(data);
setModelId(data.model_id);
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'ML 邊界分析暫時失敗');
setEdgeResult(null);
} finally {
setLoading(false);
}
}
async function trainModel() {
setTrainLoading(true);
setTrainMessage('');
try {
const payload: MlTrainRequestPayload = {
model_id: `model-${Date.now()}`,
rows: defaultTrainingRows,
};
const result = await trainMlModel(payload);
setTrainResult(result);
setModelId(result.model_id);
setTrainMessage(`訓練完成(${result.training_size}筆)`);
} catch (error) {
setTrainMessage(error instanceof Error ? error.message : '模型訓練失敗');
} finally {
setTrainLoading(false);
}
}
return (
<div className="space-y-4">
<h2 className="dot-matrix text-2xl text-[#7d2a15]">ML Ensemble 15 </h2>
<section className="panel-glow rounded-2xl p-4">
<h3 className="dot-matrix text-lg text-[#7d2a15]"></h3>
<div className="mt-3 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
<label className="text-sm text-[#7a5b46]">
使 ID
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
value={modelId}
onChange={(event) => setModelId(event.target.value)}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
value={matchId}
onChange={(event) => setMatchId(event.target.value)}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
value={homeRest}
onChange={(event) => setHomeRest(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
value={awayRest}
onChange={(event) => setAwayRest(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
km
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
value={homeTravel}
onChange={(event) => setHomeTravel(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
km
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
value={awayTravel}
onChange={(event) => setAwayTravel(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
xG
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
step={0.1}
value={homeXg}
onChange={(event) => setHomeXg(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
xG
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
step={0.1}
value={awayXg}
onChange={(event) => setAwayXg(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
step={0.01}
value={homeOdds}
onChange={(event) => setHomeOdds(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
step={0.01}
value={drawOdds}
onChange={(event) => setDrawOdds(Number(event.target.value))}
/>
</label>
<label className="text-sm text-[#7a5b46]">
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
step={0.01}
value={awayOdds}
onChange={(event) => setAwayOdds(Number(event.target.value))}
/>
</label>
</div>
<div className="mt-4 flex flex-wrap gap-3">
<button
className="rounded-lg bg-[#7d2a15] px-4 py-2 text-white"
type="button"
onClick={runEdge}
disabled={loading}
>
{loading ? '分析中…' : '執行 ML Edge 推論'}
</button>
<button
className="rounded-lg bg-[#d1432d] px-4 py-2 text-white"
type="button"
onClick={trainModel}
disabled={trainLoading}
>
{trainLoading ? '訓練中…' : '快速訓練演示模型'}
</button>
</div>
{errorMessage ? <p className="mt-3 text-xs text-[#8c2f2f]">{errorMessage}</p> : null}
{trainMessage ? <p className="mt-2 text-xs text-[#6f4f3c]">{trainMessage}</p> : null}
</section>
<section className="panel-glow rounded-2xl p-4">
<h3 className="dot-matrix text-lg text-[#7d2a15]"></h3>
{edgeResult ? (
<div className="mt-3 grid gap-3 md:grid-cols-2">
<article className="rounded-xl border border-[#e7c89b] bg-white/70 p-4">
<p className="text-sm text-[#7a5b46]"></p>
<p className="mt-1 text-sm text-[#5f4330]"> {edgeResult.model_probs.home.toFixed(2)} / {edgeResult.model_probs.draw.toFixed(2)} / {edgeResult.model_probs.away.toFixed(2)}</p>
<p className="mt-3 text-xs text-[#6f4f3c]">{edgeResult.is_fallback_model ? '規則回退' : 'ML Ensemble'}</p>
</article>
<article className="rounded-xl border border-[#e7c89b] bg-white/70 p-4">
<p className="text-sm text-[#7a5b46]"></p>
<p className="mt-1 text-2xl font-semibold text-[#b83822]">{edgeResult.strongest_outcome}</p>
<p className="text-sm text-[#5f4330]">Edge{edgeResult.strongest_edge_percent.toFixed(2)}%</p>
<p className="mt-2 text-xs text-[#7a5b46]"> Strong Buy{edgeResult.strong_buy ? '是' : '否'}</p>
</article>
<article className="rounded-xl border border-[#e7c89b] bg-white/70 p-4 md:col-span-2">
<p className="text-sm text-[#7a5b46]"> Edge </p>
<ul className="mt-2 space-y-1 text-sm text-[#5f4330]">
<li>{(edgeResult.edges.home.edge * 100).toFixed(2)}% {edgeResult.edges.home.strong_buy ? 'Strong Buy' : ''}</li>
<li>{(edgeResult.edges.draw.edge * 100).toFixed(2)}% {edgeResult.edges.draw.strong_buy ? 'Strong Buy' : ''}</li>
<li>{(edgeResult.edges.away.edge * 100).toFixed(2)}% {edgeResult.edges.away.strong_buy ? 'Strong Buy' : ''}</li>
</ul>
</article>
</div>
) : (
<p className="mt-3 text-sm text-[#7a5b46]"> ML Edge </p>
)}
</section>
<section className="panel-glow rounded-2xl p-4">
<h3 className="dot-matrix text-lg text-[#7d2a15]"></h3>
{trainResult ? (
<p className="mt-2 text-sm text-[#7a5b46]">
{trainResult.model_id} | {trainResult.training_size} |
{trainResult.accuracy === null ? 'N/A' : trainResult.accuracy.toFixed(4)} | 退{trainResult.is_fallback ? '是' : '否'}
</p>
) : (
<p className="mt-2 text-sm text-[#7a5b46]"></p>
)}
</section>
</div>
);
}