Unity sends a Firebase ID token, receives a one-time launch ticket, then opens this web chat. The permanent token never appears in the URL.
POST /api/unity/create-ticket.php
Current AI route
Text questions use {data.model}. Voice replies use {data.voiceMode}.
Data context
{data.firebaseSources} Firebase source paths are configured. A server cache reduces repeated Firebase reads.
}
function Coaches(){const[items,setItems]=useState(null);const[msg,setMsg]=useState('');const load=()=>api('admin/coaches.php').then(d=>setItems(d.coaches));useEffect(()=>{let active=true;api('admin/coaches.php').then(d=>{if(active)setItems(d.coaches)}).catch(e=>{if(active){setMsg(e.message);setItems([])}});return()=>{active=false}},[]);async function remove(id){if(!confirm('Delete this coach?'))return;try{await api('admin/coaches.php',{method:'DELETE',body:JSON.stringify({id})});setMsg('Coach deleted.');load()}catch(e){setMsg(e.message)}}if(!items)return
Coaches
Loading…
;return
COACH LIBRARY
AI coach templates
Create Zyne, Lina and other coach personalities. Firebase selects the matching coach ID for each player.
{children}}function Field({label,children,full}){return }function Toggle({label,value,onChange}){return }
function CoachEditor({path}){const coachId=(path||location.pathname).split('/').filter(Boolean).pop()||'default';const[form,setForm]=useState(null);const[msg,setMsg]=useState('');const[busy,setBusy]=useState(false);const[previewUid,setPreviewUid]=useState('');useEffect(()=>{let active=true;setForm(null);setMsg('');api(`admin/coaches.php?id=${encodeURIComponent(coachId)}`).then(d=>{if(active)setForm(d.coach)}).catch(e=>{if(active)setMsg(e.message)});return()=>{active=false}},[coachId]);const set=(k,v)=>setForm(f=>({...f,[k]:v}));async function save(e){e.preventDefault();if(!form)return;const name=(form.name||'').trim();if(!name){setMsg('Coach name is required.');return}setBusy(true);setMsg('Saving coach…');try{const payload={...form,name,shortName:(form.shortName||'').trim()};const d=await api('admin/coaches.php',{method:'POST',body:JSON.stringify(payload)});setForm(d.coach);setMsg(`Saved successfully: ${d.coach.name}`);if(coachId==='new'||coachId!==d.coach.id)go(`/admin/coach/${d.coach.id}`,true)}catch(x){setMsg(x.message)}finally{setBusy(false)}}async function upload(e){const file=e.target.files?.[0];if(!file)return;const fd=new FormData();fd.append('image',file);try{const d=await api('admin/upload-coach-image.php',{method:'POST',body:fd});set('imageUrl',d.url);setMsg('Character image uploaded. Save the coach to keep this change.')}catch(x){setMsg(x.message)}}async function preview(){if(!form?.id||coachId==='new'){setMsg('Save this coach first, then preview it.');return}try{await openCoachPreview(form.id,previewUid)}catch(x){setMsg(x.message)}}if(!form)return
}
function AdminSettings(){const[form,setForm]=useState(null);const[msg,setMsg]=useState('');const[testing,setTesting]=useState(false);useEffect(()=>{api('admin/settings.php').then(d=>setForm(d.settings))},[]);if(!form)return
Settings
Loading…
;const set=(k,v)=>setForm(f=>({...f,[k]:v}));async function save(e){e.preventDefault();try{const d=await api('admin/settings.php',{method:'POST',body:JSON.stringify(form)});setForm(d.settings);setMsg('Settings saved.')}catch(x){setMsg(x.message)}}async function testGemini(){setTesting(true);setMsg('Testing Gemini…');try{const d=await api('admin/test-gemini.php',{method:'POST',body:JSON.stringify({model:'gemini-2.5-flash-lite'})});setMsg(`${d.message} Model: ${d.model}`)}catch(x){setMsg(`Gemini test failed: ${x.message}`)}finally{setTesting(false)}}return
SYSTEM
General settings
Credentials remain server-side.
{msg&&
{msg}
}
}
function FirebaseSettings(){const[form,setForm]=useState(null);const[msg,setMsg]=useState('');useEffect(()=>{api('admin/firebase-settings.php').then(d=>setForm(d.settings))},[]);if(!form)return
Firebase
Loading…
;const set=(k,v)=>setForm(f=>({...f,[k]:v}));async function save(e){e.preventDefault();try{const d=await api('admin/firebase-settings.php',{method:'POST',body:JSON.stringify(form)});setForm(d.settings);setMsg('Firebase settings saved.')}catch(x){setMsg(x.message)}}async function test(){try{const d=await api('admin/test-firebase.php',{method:'POST',body:JSON.stringify({uid:form.testUid})});setMsg(`Connected. Loaded ${d.sourcesLoaded} source(s).`)}catch(x){setMsg(x.message)}}return
PLAYER DATA
Firebase connection
Unity exchanges its Firebase ID token for a one-time web launch ticket.
{msg&&
{msg}
}
}
function Knowledge(){const[text,setText]=useState('');const[msg,setMsg]=useState('');useEffect(()=>{api('admin/knowledge.php').then(d=>setText(d.text))},[]);async function save(){try{await api('admin/knowledge.php',{method:'POST',body:JSON.stringify({text})});setMsg('Knowledge saved.')}catch(x){setMsg(x.message)}}return
KNOWLEDGE BASE
UpChamp and football knowledge
Store approved facts about football, products, owners, drills and training.
{msg&&
{msg}
}
}
function Usage(){const[data,setData]=useState(null);useEffect(()=>{api('admin/usage.php').then(setData)},[]);if(!data)return
Usage
Loading…
;return
QUOTAS
Usage and safety limits
No chat content is stored.
{data.chirpPercent.toFixed(1)}% of your configured safe limit has been used.
}
function App(){const path=usePath();if(path==='/chat')return ;if(path==='/admin/login')return ;if(path.startsWith('/admin'))return ;return }
ReactDOM.createRoot(document.getElementById('root')).render();