Ultimate Race Summary software

Copy code from Top to Bottom


<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HandicapPro | Neural Race Analytics</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;900&display=swap');
body { font-family: 'Inter', sans-serif; background-color: #f8fafc; color: #0f172a; }

.prose h3 {
font-size: 1.25rem;
font-weight: 900;
color: #3730a3;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 0.25rem;
text-transform: uppercase;
display: flex;
align-items: center;
justify-content: space-between;
}
.prose p { margin-bottom: 1rem; line-height: 1.6; color: #475569; }
.prose ul { list-style-type: disc; padding-left: 1.5rem; margin-bottom: 1rem; }
.prose li { margin-bottom: 0.5rem; }

.confidence-badge {
font-size: 0.7rem;
padding: 2px 8px;
border-radius: 99px;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 800;
}

@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.animate-fade-in { animation: fadeIn 0.4s ease-out forwards; }

.custom-scrollbar::-webkit-scrollbar { width: 6px; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
</style>
</head>
<body class="min-h-screen p-4 lg:p-8">

<div class="max-w-7xl mx-auto">
<!-- Header -->
<header class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4">
<div>
<div class="flex items-center gap-2 mb-1">
<div class="bg-indigo-600 p-1.5 rounded-lg shadow-lg">
<i data-lucide="zap" class="text-white w-6 h-6"></i>
</div>
<h1 class="text-2xl font-black tracking-tighter text-slate-900 uppercase">Handicap<span class="text-indigo-600">Pro</span></h1>
</div>
<p class="text-slate-500 text-sm font-medium">Neural Race Card Analysis Engine</p>
</div>

<div class="flex items-center gap-3">
<button id="reset-engine-btn" class="flex items-center gap-2 px-4 py-3 bg-white border border-slate-200 rounded-2xl text-[10px] font-black text-red-500 uppercase hover:bg-red-50 transition-all shadow-sm group">
<i data-lucide="rotate-ccw" class="w-4 h-4 transition-transform group-hover:-rotate-90"></i>
Reset Engine
</button>
<div class="flex items-center gap-4 bg-white border border-slate-200 p-2 px-4 rounded-2xl shadow-sm">
<div class="flex -space-x-2">
<div class="w-8 h-8 rounded-full bg-indigo-50 border-2 border-white flex items-center justify-center"><i data-lucide="brain" class="w-4 h-4 text-indigo-600"></i></div>
</div>
<div class="text-left">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest leading-none">Intelligence</p>
<p class="text-xs font-bold text-slate-700">Gemini 2.5 Flash</p>
</div>
</div>
</div>
</header>

<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">

<aside class="lg:col-span-4 space-y-6">
<!-- STEP 1: UPLOAD -->
<div class="bg-white rounded-3xl p-6 shadow-sm border border-slate-200">
<h2 class="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2 mb-4">
<i data-lucide="file-up" class="w-4 h-4"></i> 1. Race Document
</h2>
<div id="drop-zone" class="relative group border-2 border-dashed border-slate-200 rounded-2xl p-8 transition-all hover:border-indigo-400 text-center cursor-pointer">
<input type="file" id="pdf-upload" accept=".pdf" class="hidden">
<label for="pdf-upload" class="cursor-pointer">
<i data-lucide="file-text" class="w-8 h-8 text-slate-300 mx-auto mb-2"></i>
<p id="file-name" class="text-sm font-bold text-slate-700 truncate">Select Race PDF</p>
<p class="text-[10px] text-slate-400 mt-1 uppercase font-bold tracking-wider">DRF or Program</p>
</label>
</div>

<!-- SCRATCHES -->
<div class="mt-8 pt-6 border-t border-slate-100">
<h2 class="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2 mb-4">
<i data-lucide="scissors" class="w-4 h-4"></i> Active Scratches
</h2>
<div class="flex gap-2 mb-3">
<input type="text" id="scr-input" placeholder="Name/Number..." class="flex-1 bg-slate-50 border border-slate-200 rounded-xl px-4 py-2 text-xs focus:outline-none">
<button id="add-scr" class="p-2 bg-indigo-600 text-white rounded-xl"><i data-lucide="plus" class="w-4 h-4"></i></button>
</div>
<div id="scr-list" class="flex flex-wrap gap-2"></div>
</div>
</div>

<!-- STEP 2: SCOPE -->
<div class="bg-white rounded-3xl p-6 shadow-sm border border-slate-200">
<h2 class="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center gap-2 mb-4">
<i data-lucide="target" class="w-4 h-4"></i> 2. Analysis Scope
</h2>
<div id="race-grid" class="grid grid-cols-4 gap-2"></div>
</div>

<button id="analyze-btn" disabled class="w-full py-5 rounded-3xl bg-slate-100 text-slate-400 font-black uppercase tracking-[0.2em] shadow-lg flex items-center justify-center gap-3 transition-all cursor-not-allowed group">
<i data-lucide="sparkles" class="w-5 h-5 group-enabled:text-yellow-400"></i>
<span>Execute Analysis</span>
</button>
</aside>

<main class="lg:col-span-8 min-h-[600px] flex flex-col">
<!-- ERROR DISPLAY -->
<div id="error-box" class="hidden mb-6 bg-red-50 border border-red-200 p-4 rounded-2xl flex items-start gap-3 animate-fade-in">
<i data-lucide="alert-circle" class="w-5 h-5 text-red-600 mt-0.5"></i>
<div>
<p class="text-xs font-black text-red-700 uppercase mb-1">Engine Error</p>
<p id="error-msg" class="text-xs text-red-600 leading-relaxed"></p>
</div>
</div>

<!-- CONTENT AREAS -->
<div id="empty-state" class="flex-1 bg-white rounded-[2.5rem] border border-slate-200 flex flex-col items-center justify-center text-center p-12">
<div class="w-20 h-20 bg-indigo-50 rounded-full flex items-center justify-center mb-6">
<i data-lucide="brain-circuit" class="w-10 h-10 text-indigo-200"></i>
</div>
<h3 class="text-lg font-black text-slate-800 uppercase">Neural Processor Ready</h3>
<p class="text-slate-500 text-sm mt-2 max-w-xs">Upload your PDF race card to start the neural handicapping process with integrated confidence scoring.</p>
</div>

<div id="loading-state" class="hidden flex-1 bg-white rounded-[2.5rem] border border-slate-200 p-12 text-center flex flex-col items-center justify-center">
<div class="w-14 h-14 border-4 border-indigo-600 border-t-transparent rounded-full animate-spin mb-6"></div>
<p class="text-xs font-black text-indigo-600 uppercase tracking-widest animate-pulse">Calculating Neural Confidence for each race...</p>
</div>

<div id="results-state" class="hidden flex-1 flex flex-col bg-white rounded-[2.5rem] border border-slate-200 shadow-xl overflow-hidden animate-fade-in">
<div class="bg-slate-50 border-b border-slate-200 p-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="bg-green-500 w-2 h-2 rounded-full"></div>
<h2 class="text-lg font-black text-slate-800 uppercase tracking-tight">HandicapPro Report</h2>
</div>
<div class="flex items-center gap-2">
<button id="next-race-shortcut" class="flex items-center gap-2 px-4 py-2 bg-indigo-50 text-[10px] font-black text-indigo-600 border border-indigo-100 rounded-xl uppercase hover:bg-indigo-100 transition-all">
<i data-lucide="arrow-right-circle" class="w-3.5 h-3.5"></i> Next Race
</button>
<button id="download-txt" class="flex items-center gap-2 px-4 py-2 bg-white border border-slate-200 rounded-xl text-[10px] font-black text-slate-600 uppercase hover:bg-slate-50 shadow-sm transition-all">
<i data-lucide="download" class="w-3.5 h-3.5"></i> Export
</button>
</div>
</div>
<div class="flex-1 p-8 lg:p-12 overflow-y-auto custom-scrollbar bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:20px_20px]">
<div id="analysis-content" class="prose max-w-none bg-white p-8 rounded-3xl shadow-sm border border-slate-100"></div>
</div>
</div>
</main>
</div>
</div>

<script>
/**
* HANDICAPPRO CORE LOGIC - POWERED BY NATIVE GEMINI 2.5 FLASH
* LOCK STATUS: FINAL
*/
const apiKey = "";
let pdfBase64 = null;
let currentRace = "All";
let scratchedHorses = [];
let rawReport = "";

const el = (id) => document.getElementById(id);
const refreshIcons = () => lucide.createIcons();
refreshIcons();

// Reset Logic
const resetEngine = () => {
pdfBase64 = null;
scratchedHorses = [];
currentRace = "All";
rawReport = "";
el('file-name').textContent = "Select Race PDF";
el('pdf-upload').value = "";
el('scr-input').value = "";
el('error-box').classList.add('hidden');
el('results-state').classList.add('hidden');
el('loading-state').classList.add('hidden');
el('empty-state').classList.remove('hidden');
renderScratches();
renderRaces();
checkReadiness();
};

el('reset-engine-btn').onclick = resetEngine;
el('next-race-shortcut').onclick = () => {
el('results-state').classList.add('hidden');
el('empty-state').classList.remove('hidden');
if (!isNaN(currentRace)) {
let next = parseInt(currentRace) + 1;
if (next <= 12) {
currentRace = next.toString();
renderRaces();
}
}
};

const renderScratches = () => {
const list = el('scr-list');
list.innerHTML = '';
scratchedHorses.forEach(h => {
const tag = document.createElement('div');
tag.className = "bg-slate-100 px-3 py-1.5 rounded-xl text-[10px] font-bold border border-slate-200 flex items-center gap-2 shadow-sm animate-fade-in";
tag.innerHTML = `<span>${h}</span><button class="text-red-400 hover:text-red-600 transition-colors"><i data-lucide="x-circle" class="w-3.5 h-3.5"></i></button>`;
tag.querySelector('button').onclick = () => {
scratchedHorses = scratchedHorses.filter(item => item !== h);
renderScratches();
};
list.appendChild(tag);
});
refreshIcons();
};

el('add-scr').onclick = () => {
const val = el('scr-input').value.trim();
if (val && !scratchedHorses.includes(val)) {
scratchedHorses.push(val);
el('scr-input').value = '';
renderScratches();
}
};

const races = ["All", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"];
const renderRaces = () => {
const grid = el('race-grid');
grid.innerHTML = '';
races.forEach(r => {
const btn = document.createElement('button');
btn.className = `py-3 text-[10px] font-black rounded-xl border transition-all ${currentRace === r ? 'bg-indigo-600 text-white border-indigo-700 shadow-lg shadow-indigo-100' : 'bg-slate-50 text-slate-500 border-slate-200 hover:bg-slate-100'}`;
btn.textContent = r;
btn.onclick = () => { currentRace = r; renderRaces(); };
grid.appendChild(btn);
});
};
renderRaces();

const checkReadiness = () => {
const btn = el('analyze-btn');
if (pdfBase64) {
btn.disabled = false;
btn.className = "w-full py-5 rounded-3xl bg-indigo-600 text-white font-black uppercase tracking-[0.2em] shadow-xl hover:bg-indigo-700 transition-all active:scale-[0.98]";
} else {
btn.disabled = true;
btn.className = "w-full py-5 rounded-3xl bg-slate-100 text-slate-400 font-black uppercase tracking-[0.2em] shadow-lg flex items-center justify-center gap-3 transition-all cursor-not-allowed group";
}
};

el('pdf-upload').onchange = (e) => {
const file = e.target.files[0];
if (file && file.type === "application/pdf") {
el('file-name').textContent = file.name;
const reader = new FileReader();
reader.onload = () => {
pdfBase64 = reader.result.split(',')[1];
checkReadiness();
};
reader.readAsDataURL(file);
}
};

el('analyze-btn').onclick = async () => {
el('empty-state').classList.add('hidden');
el('results-state').classList.add('hidden');
el('error-box').classList.add('hidden');
el('loading-state').classList.remove('hidden');

const scratchSection = scratchedHorses.length ? `CRITICAL: The following horses are SCRATCHED and must not be analyzed: ${scratchedHorses.join(', ')}.` : "";
const scopeSection = currentRace === "All" ? "Handicap every race in this card." : `Analyze ONLY Race ${currentRace}.`;

const systemPrompt = "You are an elite Horse Racing Statistician and AI Handicapper.";
const userQuery = `Task: Professional Race Analysis with Neural Confidence Scoring.
Scope: ${scopeSection}
Exclusions: ${scratchSection}

REQUIRED FORMAT FOR EACH RACE:
### RACE [Number] [CONFIDENCE: LOW/MED/HIGH - 0-100%]
- Selection Order: Win, Place, Show.
- Beyer/Speed Stats: Key historical performance.
- Neural Logic: Why this confidence level? (e.g., strong favorite, chaotic field, track bias).
- Betting Strategy: Strategic plays based on confidence.

Always include the [CONFIDENCE: ...] tag in the Header line.`;

const fetchWithRetry = async (url, options, retries = 5, backoff = 1000) => {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
const data = await response.json();
if (response.ok) return data;
if (data.error && (data.error.code === 429 || data.error.code === 500)) {
await new Promise(r => setTimeout(r, backoff * Math.pow(2, i)));
continue;
}
throw new Error(data.error?.message || "Analysis Error");
} catch (err) {
if (i === retries - 1) throw err;
await new Promise(r => setTimeout(r, backoff * Math.pow(2, i)));
}
}
};

try {
const result = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{
parts: [
{ text: userQuery },
{ inlineData: { mimeType: "application/pdf", data: pdfBase64 } }
]
}],
systemInstruction: { parts: [{ text: systemPrompt }] }
})
});

const contentText = result.candidates?.[0]?.content?.parts?.[0]?.text;
if (!contentText) throw new Error("Could not parse racing data.");

rawReport = contentText;
displayReport(contentText);
el('loading-state').classList.add('hidden');
el('results-state').classList.remove('hidden');

} catch (err) {
el('loading-state').classList.add('hidden');
el('error-box').classList.remove('hidden');
el('error-msg').textContent = err.message;
}
};

const displayReport = (text) => {
const box = el('analysis-content');
box.innerHTML = '';

const lines = text.split('\n');
lines.forEach(line => {
if (line.startsWith('###')) {
const h3 = document.createElement('h3');
let rawText = line.replace(/#/g, '').trim();
const confMatch = rawText.match(/\[CONFIDENCE:\s*(LOW|MED|HIGH|.*?)\s*-\s*(\d+%|.*?)\]/i);

if (confMatch) {
const titleOnly = rawText.split('[')[0].trim();
const level = confMatch[1];
const percent = confMatch[2];
let colorClass = "bg-yellow-100 text-yellow-700";
if (level.toUpperCase().includes('HIGH')) colorClass = "bg-green-100 text-green-700";
if (level.toUpperCase().includes('LOW')) colorClass = "bg-red-100 text-red-700";
h3.innerHTML = `<span>${titleOnly}</span> <span class="confidence-badge ${colorClass}">${level} | ${percent}</span>`;
} else {
h3.textContent = rawText;
}
box.appendChild(h3);
} else if (line.trim().startsWith('*') || line.trim().startsWith('-')) {
const li = document.createElement('li');
li.textContent = line.replace(/^[\*\-]/, '').trim();
let ul = box.lastElementChild;
if (!ul || ul.tagName !== 'UL') {
ul = document.createElement('ul');
box.appendChild(ul);
}
ul.appendChild(li);
} else if (line.trim()) {
const p = document.createElement('p');
p.textContent = line;
box.appendChild(p);
}
});
};

el('download-txt').onclick = () => {
const blob = new Blob([rawReport], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `HandicapPro_Confidence_Report.txt`;
a.click();
URL.revokeObjectURL(url);
};
</script>
</body>
</html>