const {useState,useEffect,useRef,useMemo,useCallback}=React;
const iconMap={ArrowLeft:'‹',Mic:'🎙',MoreVertical:'⋮',Send:'➤',Volume2:'🔊',VolumeX:'🔇',MessageCircle:'◯',LoaderCircle:'↻',Eye:'◉',Minimize2:'—',LockKeyhole:'🔐',Bot:'🤖',Database:'🗄',Gauge:'◴',LayoutDashboard:'▦',LogOut:'↪',Settings:'⚙',BookOpen:'📖',ShieldCheck:'✓',ExternalLink:'↗'};
function I({name,size=20,className=''}){return <span className={`svg-icon ${className}`} style={{fontSize:size}}>{iconMap[name]||'•'}</span>}
async function api(path,options={}){const r=await fetch(`/api/${path.replace(/^\//,'')}`,{credentials:'include',headers:{...(options.body instanceof FormData?{}:{'Content-Type':'application/json'}),...(options.headers||{})},...options});const t=r.headers.get('content-type')||'';const p=t.includes('application/json')?await r.json().catch(()=>({})):await r.text();if(!r.ok)throw new Error(typeof p==='object'&&p.error?p.error:`Request failed (${r.status})`);return p}
const postJson=(path,body)=>api(path,{method:'POST',body:JSON.stringify(body)});
function usePath(){const[path,setPath]=useState(location.pathname);useEffect(()=>{const f=()=>setPath(location.pathname);addEventListener('popstate',f);return()=>removeEventListener('popstate',f)},[]);return path}
function go(path,replace=false){history[replace?'replaceState':'pushState']({},'',path);dispatchEvent(new PopStateEvent('popstate'))}
function A({to,children,className=''}){return <a href={to} className={className} onClick={e=>{e.preventDefault();go(to)}}>{children}</a>}
const now=()=>new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});

async function openCoachPreview(coachId,firebaseUid=''){
  const popup=window.open('about:blank','upchamp-coach-preview');
  if(popup){popup.document.title='Opening coach preview…';popup.document.body.innerHTML='<div style="font-family:system-ui;background:#080909;color:#d7ff2f;display:grid;place-items:center;height:100vh;margin:0">Opening coach preview…</div>'}
  try{
    const d=await postJson('admin/preview-coach.php',{coachId,firebaseUid:firebaseUid.trim()});
    if(popup)popup.location.href=d.url;else location.href=d.url;
  }catch(e){if(popup)popup.close();throw e}
}

function Launch(){const[status,setStatus]=useState('Preparing your coach…');const ticket=new URLSearchParams(location.search).get('ticket');useEffect(()=>{if(!ticket)return;postJson('unity/redeem-ticket.php',{ticket}).then(()=>{setStatus('Player verified. Opening coach chat…');history.replaceState({},'','/');go('/chat',true)}).catch(e=>setStatus(e.message))},[ticket]);return <main className="launch-page"><div className="launch-logo">UC</div><h1>UPCHAMP AI</h1><p>{ticket?status:'Open this coach chat from inside the signed-in UpChamp app.'}</p><I name={ticket?'LoaderCircle':'ShieldCheck'} className={ticket?'spin':''}/><A className="ghost-link" to="/admin/login">Admin portal</A></main>}
function CoachWindow({coach,speaking,collapsed,onToggle}){if(!coach?.showCharacterWindow)return null;return <section className={`coach-window ${collapsed?'collapsed':''}`}><div className="coach-window-title"><I name="Eye"/><span>Coach View</span></div><button className="icon-button minimize" onClick={onToggle}><I name="Minimize2"/></button>{!collapsed&&<><div className="coach-wave"><i/><i/><i/><i/><i/><i/><i/><i/><i/><i/><i/><i/></div><div className="coach-character-wrap">{coach.imageUrl?<img src={coach.imageUrl} alt={coach.name} className="coach-character"/>:<div className="coach-placeholder"><span>{coach.name?.[0]||'C'}</span></div>}</div><div className={`speaking-pill ${speaking?'active':''}`}><I name="Volume2"/><span>{speaking?`${coach.name} is speaking…`:`${coach.name} is ready`}</span></div></>}</section>}
function MessageBubble({message,coach}){const u=message.role==='user';return <div className={`message-row ${u?'user':'assistant'}`}>{!u&&<div className="message-avatar">{coach?.shortName||coach?.name?.[0]||'C'}</div>}<div className="message-stack"><div className="message-bubble">{!u&&<div className="message-author">{coach?.name}</div>}<div>{message.text}</div><small>{message.time||''}{u?'  ✓✓':''}</small></div>{message.stats&&<div className="stats-card">{message.stats.map(x=><div key={x.label}><span>{x.label}</span><strong>{x.value}</strong></div>)}</div>}</div></div>}
function CoachChat(){const[boot,setBoot]=useState({loading:true,error:''});const[user,setUser]=useState(null);const[coach,setCoach]=useState(null);const[isPreview,setIsPreview]=useState(false);const[messages,setMessages]=useState([]);const[input,setInput]=useState('');const[sending,setSending]=useState(false);const[listening,setListening]=useState(false);const[speaking,setSpeaking]=useState(false);const[voiceOn,setVoiceOn]=useState(true);const[collapsed,setCollapsed]=useState(false);const scrollRef=useRef(null);const recognitionRef=useRef(null);
useEffect(()=>{api('user/context.php').then(d=>{setUser(d.user);setCoach(d.coach);setIsPreview(!!d.preview);setVoiceOn(d.coach?.voiceDefaultOn!==false&&d.coach?.voiceMode!=='off');setMessages([{role:'assistant',text:d.greeting,time:now(),stats:d.highlightStats||null}]);setBoot({loading:false,error:''})}).catch(e=>setBoot({loading:false,error:e.message}))},[]);useEffect(()=>{scrollRef.current?.scrollTo({top:scrollRef.current.scrollHeight,behavior:'smooth'})},[messages,sending]);
const speakBrowser=useCallback(text=>{if(!('speechSynthesis'in window)||!voiceOn)return;speechSynthesis.cancel();const u=new SpeechSynthesisUtterance(text);u.lang=coach?.browserVoiceLanguage||'en-US';const vs=speechSynthesis.getVoices();if(coach?.browserVoiceName){const m=vs.find(v=>v.name.includes(coach.browserVoiceName));if(m)u.voice=m}u.onstart=()=>setSpeaking(true);u.onend=u.onerror=()=>setSpeaking(false);speechSynthesis.speak(u)},[coach,voiceOn]);
const speakChirp=useCallback(async text=>{if(!voiceOn)return;setSpeaking(true);try{const d=await postJson('chat/tts.php',{text});const a=new Audio(`data:${d.mime};base64,${d.audio}`);a.onended=a.onerror=()=>setSpeaking(false);await a.play()}catch{setSpeaking(false);speakBrowser(text)}},[voiceOn,speakBrowser]);
const playReply=useCallback(text=>{if(!voiceOn||!coach)return;if(coach.voiceMode==='chirp')speakChirp(text);else if(coach.voiceMode==='browser')speakBrowser(text)},[coach,voiceOn,speakBrowser,speakChirp]);
const submit=useCallback(async text=>{const clean=text.trim();if(!clean||sending)return;const m={role:'user',text:clean,time:now()};const next=[...messages,m];setMessages(next);setInput('');setSending(true);try{const d=await postJson('chat/message.php',{message:clean,history:next.slice(-12).map(({role,text})=>({role,text}))});setMessages(c=>[...c,{role:'assistant',text:d.text,time:now(),stats:d.stats||null}]);playReply(d.text)}catch(e){setMessages(c=>[...c,{role:'assistant',text:`I couldn't answer that right now: ${e.message}`,time:now()}])}finally{setSending(false)}},[messages,sending,playReply]);
const SR=useMemo(()=>window.SpeechRecognition||window.webkitSpeechRecognition,[]);const startListening=useCallback(()=>{if(sending||listening)return;if(!SR){alert('Speech recognition is not available in this embedded browser. You can still type.');return}window.speechSynthesis?.cancel();const r=new SR();r.lang=coach?.speechLanguage||'en-US';r.continuous=false;r.interimResults=true;let finalText='';r.onresult=e=>{let interim='';for(let i=e.resultIndex;i<e.results.length;i++){const t=e.results[i][0].transcript;if(e.results[i].isFinal)finalText+=t;else interim+=t}setInput(finalText||interim)};r.onend=()=>{setListening(false);recognitionRef.current=null;if(finalText.trim())submit(finalText)};r.onerror=()=>{setListening(false);recognitionRef.current=null};recognitionRef.current=r;r.start();setListening(true)},[SR,coach,listening,sending,submit]);const stopListening=useCallback(()=>{try{recognitionRef.current?.stop()}catch{}},[]);
if(boot.loading)return <main className="chat-loading"><I name="LoaderCircle" className="spin"/><span>Loading your personal coach…</span></main>;if(boot.error)return <main className="chat-loading error"><span>{boot.error}</span><A to="/">Return</A></main>;
return <main className="chat-app"><header className="chat-header"><button className="icon-button" onClick={()=>history.back()}><I name="ArrowLeft" size={30}/></button><div className="uc-mark">UC</div><div className="coach-title"><strong>{coach.name}</strong><span>{isPreview?'Admin preview':`Coaching ${user.displayName} now`} <i/></span></div><button className="icon-button"><I name="MoreVertical"/></button></header><CoachWindow coach={coach} speaking={speaking} collapsed={collapsed} onToggle={()=>setCollapsed(v=>!v)}/><section className="messages" ref={scrollRef}>{messages.map((m,i)=><MessageBubble key={`${m.time}-${i}`} message={m} coach={coach}/>)}{sending&&<div className="typing-row"><div className="message-avatar">{coach.shortName||coach.name[0]}</div><div className="typing-bubble"><i/><i/><i/></div></div>}</section><footer className="composer-zone"><div className="voice-toggle"><button onClick={()=>setVoiceOn(v=>!v)}><I name={voiceOn?'Volume2':'VolumeX'}/></button><span>Voice responses</span><strong>{voiceOn?'ON':'OFF'}</strong></div><form className="text-composer" onSubmit={e=>{e.preventDefault();submit(input)}}><I name="MessageCircle"/><input value={input} onChange={e=>setInput(e.target.value)} placeholder="Type your message…"/><button disabled={!input.trim()||sending}><I name="Send"/></button></form><button className={`ptt-button ${listening?'listening':''}`} onPointerDown={e=>{e.preventDefault();startListening()}} onPointerUp={e=>{e.preventDefault();stopListening()}} onPointerCancel={stopListening} onContextMenu={e=>e.preventDefault()}><I name="Mic"/><span>{listening?'Listening… release to send':'Hold to Talk'}</span></button><small className="ptt-note">Push to talk • Release to send</small></footer></main>}

function AdminLogin(){const[email,setEmail]=useState('');const[password,setPassword]=useState('');const[error,setError]=useState('');const[busy,setBusy]=useState(false);async function submit(e){e.preventDefault();setBusy(true);setError('');try{await postJson('auth/admin-login.php',{email,password});go('/admin')}catch(x){setError(x.message)}finally{setBusy(false)}}return <main className="admin-login-page"><form className="admin-login-card" onSubmit={submit}><div className="uc-mark large">UC</div><h1>UPCHAMP AI ADMIN</h1><p>Manage coaches, Firebase, Gemini, voices and knowledge.</p>{error&&<div className="alert error">{error}</div>}<label>Email<input type="email" value={email} onChange={e=>setEmail(e.target.value)} required/></label><label>Password<input type="password" value={password} onChange={e=>setPassword(e.target.value)} required/></label><button className="primary-button" disabled={busy}><I name="LockKeyhole"/>{busy?'Signing in…':'Sign in'}</button></form></main>}
function AdminShell({path}){const[ready,setReady]=useState(false);useEffect(()=>{api('auth/admin-session.php').then(()=>setReady(true)).catch(()=>go('/admin/login',true))},[]);async function logout(){await postJson('auth/admin-logout.php',{});go('/admin/login')}if(!ready)return <main className="chat-loading">Checking admin session…</main>;let Page=AdminDashboard;if(path==='/admin/coaches')Page=Coaches;else if(path.startsWith('/admin/coach/'))Page=CoachEditor;else if(path==='/admin/firebase')Page=FirebaseSettings;else if(path==='/admin/knowledge')Page=Knowledge;else if(path==='/admin/usage')Page=Usage;else if(path==='/admin/settings')Page=AdminSettings;const nav=[['/admin','LayoutDashboard','Overview'],['/admin/coaches','Bot','Coaches'],['/admin/firebase','Database','Firebase'],['/admin/knowledge','BookOpen','Knowledge'],['/admin/usage','Gauge','Usage'],['/admin/settings','Settings','Settings']];return <div className="admin-shell"><aside className="admin-sidebar"><div className="brand"><div className="uc-mark">UC</div><div><strong>UPCHAMP AI</strong><span>Control center</span></div></div><nav>{nav.map(([to,ic,label])=><A key={to} to={to} className={(path===to||(to.includes('/coach')&&path.includes('/coach')))?'active':''}><I name={ic}/>{label}</A>)}</nav><button className="logout-button" onClick={logout}><I name="LogOut"/>Log out</button></aside><main className="admin-main"><Page key={path} path={path}/></main></div>}
function Metric({name,label,value}){return <div className="metric-card"><div><I name={name}/></div><span>{label}</span><strong>{value}</strong></div>}
function AdminDashboard(){const[data,setData]=useState(null);useEffect(()=>{api('admin/dashboard.php').then(setData)},[]);if(!data)return <div className="admin-page"><h1>Dashboard</h1><p>Loading…</p></div>;return <div className="admin-page"><div className="page-head"><div><span className="eyebrow">UPCHAMP AI</span><h1>Coach control center</h1><p>Manage the embedded Unity coach experience from one place.</p></div><A className="primary-button" to="/admin/coaches">Manage coaches</A></div><div className="metric-grid"><Metric name="Bot" label="Active coach" value={data.coachName}/><Metric name="Database" label="Firebase" value={data.firebaseConfigured?'Connected':'Not configured'}/><Metric name="MessageCircle" label="Text turns today" value={data.textTurnsToday}/><Metric name="Volume2" label="Chirp characters" value={`${data.chirpUsed.toLocaleString()} / ${data.chirpStop.toLocaleString()}`}/></div><div className="admin-card-grid"><section className="admin-card"><h2>Unity handoff</h2><p>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.</p><code>POST /api/unity/create-ticket.php</code></section><section className="admin-card"><h2>Current AI route</h2><p>Text questions use <strong>{data.model}</strong>. Voice replies use <strong>{data.voiceMode}</strong>.</p></section><section className="admin-card"><h2>Data context</h2><p>{data.firebaseSources} Firebase source paths are configured. A server cache reduces repeated Firebase reads.</p></section></div></div>}

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 <div className="admin-page"><h1>Coaches</h1><p>Loading…</p></div>;return <div className="admin-page"><div className="page-head"><div><span className="eyebrow">COACH LIBRARY</span><h1>AI coach templates</h1><p>Create Zyne, Lina and other coach personalities. Firebase selects the matching coach ID for each player.</p></div><A className="primary-button" to="/admin/coach/new">Create coach</A></div>{msg&&<div className="alert">{msg}</div>}<div className="admin-card-grid">{items.map(c=><section className="admin-card" key={c.id}>{c.imageUrl?<img src={c.imageUrl} className="coach-list-image"/>:<div className="coach-list-placeholder">{c.name?.[0]}</div>}<h2>{c.name}</h2><p>{c.subtitle}</p><p><strong>{c.model}</strong><br/>{c.voiceMode}</p><div className="coach-card-actions"><button className="secondary-button" onClick={async()=>{try{await openCoachPreview(c.id)}catch(e){setMsg(e.message)}}}><I name="ExternalLink"/> Preview</button><A className="secondary-button" to={`/admin/coach/${c.id}`}>Edit</A>{c.id!=='default'&&<button className="danger-button" onClick={()=>remove(c.id)}>Delete</button>}</div></section>)}</div></div>}
const modelOptions=[['gemini-2.5-flash-lite','Gemini 2.5 Flash-Lite — default, free tier available'],['gemini-2.5-flash','Gemini 2.5 Flash — stronger, free tier available'],['gemini-3-flash-preview','Gemini 3 Flash Preview — project availability varies']];const voiceNames=['Achernar','Achird','Algenib','Algieba','Alnilam','Aoede','Autonoe','Callirrhoe','Charon','Despina','Enceladus','Erinome','Fenrir','Gacrux','Iapetus','Kore','Laomedeia','Leda','Orus','Puck','Pulcherrima','Rasalgethi','Sadachbia','Sadaltager','Schedar','Sulafat','Umbriel','Vindemiatrix','Zephyr','Zubenelgenubi'];
function Section({title,children}){return <section className="settings-section"><h2>{title}</h2>{children}</section>}function Field({label,children,full}){return <label className={`admin-field ${full?'full':''}`}><span>{label}</span>{children}</label>}function Toggle({label,value,onChange}){return <label className="admin-toggle"><input type="checkbox" checked={!!value} onChange={e=>onChange(e.target.checked)}/><span>{label}</span></label>}
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 <div className="admin-page"><h1>AI Coach</h1><p>{msg||'Loading…'}</p>{msg&&<A className="secondary-button" to="/admin/coaches">Back to coaches</A>}</div>;return <div className="admin-page"><div className="page-head"><div><span className="eyebrow">COACH TEMPLATE</span><h1>Coach identity and AI</h1><p>The coach is personalized with the signed-in player's private Firebase context.</p></div><A className="secondary-button" to="/admin/coaches">Back to coaches</A></div>{msg&&<div className="alert">{msg}</div>}<form className="settings-form" onSubmit={save}><Section title="Identity and character"><div className="form-grid"><Field label="Coach name"><input required maxLength="80" value={form.name||''} onChange={e=>set('name',e.target.value)}/></Field><Field label="Short avatar letter"><input maxLength="2" value={form.shortName||''} onChange={e=>set('shortName',e.target.value)}/></Field><Field label="Subtitle"><input maxLength="160" value={form.subtitle||''} onChange={e=>set('subtitle',e.target.value)}/></Field><Field label="Upload coach character image"><input type="file" accept="image/*" onChange={upload}/></Field><Field label="Character image URL" full><input value={form.imageUrl||''} onChange={e=>set('imageUrl',e.target.value)}/></Field><Toggle label="Show optional speaking coach window" value={form.showCharacterWindow} onChange={v=>set('showCharacterWindow',v)}/></div>{form.imageUrl&&<img className="coach-admin-preview" src={form.imageUrl}/>}</Section><Section title="Gemini intelligence"><div className="form-grid"><Field label="Gemini text model"><select value={form.model} onChange={e=>set('model',e.target.value)}>{modelOptions.map(([v,l])=><option key={v} value={v}>{l}</option>)}</select></Field><Field label="Maximum reply words"><input type="number" min="30" max="500" value={form.maxReplyWords} onChange={e=>set('maxReplyWords',Number(e.target.value))}/></Field><Field label="System prompt" full><textarea rows="12" value={form.systemPrompt||''} onChange={e=>set('systemPrompt',e.target.value)}/></Field></div></Section><Section title="Voice and push-to-talk"><div className="form-grid"><Field label="AI reply voice"><select value={form.voiceMode} onChange={e=>set('voiceMode',e.target.value)}><option value="off">Text only</option><option value="browser">Browser/device TTS — no Google TTS charge</option><option value="chirp">Google Chirp 3 HD</option></select></Field><Field label="Browser voice preference"><input value={form.browserVoiceName||''} onChange={e=>set('browserVoiceName',e.target.value)}/></Field><Field label="Speech recognition language"><input value={form.speechLanguage||'en-US'} onChange={e=>set('speechLanguage',e.target.value)}/></Field><Field label="Chirp language"><select value={form.chirpLanguage||'en-US'} onChange={e=>set('chirpLanguage',e.target.value)}><option value="en-US">English US</option><option value="en-GB">English UK</option><option value="en-IN">English India</option><option value="ur-IN">Urdu</option><option value="hi-IN">Hindi</option></select></Field><Field label="Chirp HD voice"><select value={form.chirpVoice||'Kore'} onChange={e=>set('chirpVoice',e.target.value)}>{voiceNames.map(v=><option key={v} value={v}>{v}</option>)}</select></Field><Toggle label="Voice responses enabled by default" value={form.voiceDefaultOn} onChange={v=>set('voiceDefaultOn',v)}/></div></Section><Section title="Test this coach"><p className="muted">Preview with safe demo data, or enter a real Firebase UID to load that player's configured data. The preview uses the selected coach even if the Firebase user selected another coach.</p><div className="form-grid"><Field label="Optional Firebase UID"><input value={previewUid} onChange={e=>setPreviewUid(e.target.value)} placeholder="Leave blank for demo player Alex"/></Field></div><button type="button" className="secondary-button" onClick={preview} disabled={coachId==='new'||!form.id}><I name="ExternalLink"/> Open coach preview</button></Section><div className="knowledge-actions"><button type="submit" className="primary-button" disabled={busy}>{busy?'Saving…':'Save coach'}</button><button type="button" className="secondary-button" onClick={preview} disabled={coachId==='new'||!form.id}><I name="ExternalLink"/> Preview</button><A className="secondary-button" to="/admin/coaches">Return to coaches</A></div></form></div>}
function AdminSettings(){const[form,setForm]=useState(null);const[msg,setMsg]=useState('');useEffect(()=>{api('admin/settings.php').then(d=>setForm(d.settings))},[]);if(!form)return <div className="admin-page"><h1>Settings</h1><p>Loading…</p></div>;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)}}return <div className="admin-page"><div className="page-head"><div><span className="eyebrow">SYSTEM</span><h1>General settings</h1><p>Credentials remain server-side.</p></div></div>{msg&&<div className="alert">{msg}</div>}<form className="settings-form" onSubmit={save}><Section title="Website and Gemini"><div className="form-grid"><Field label="Site name"><input value={form.siteName||''} onChange={e=>set('siteName',e.target.value)}/></Field><Field label="Public URL"><input value={form.publicUrl||''} onChange={e=>set('publicUrl',e.target.value)}/></Field><Field label="Gemini API key" full><input type="password" value={form.geminiApiKey||''} onChange={e=>set('geminiApiKey',e.target.value)} placeholder={form.hasGeminiKey?'Saved — enter only to replace':'Paste Gemini API key'}/></Field></div></Section><Section title="Quota controls"><div className="form-grid"><Field label="Text messages per player/day"><input type="number" value={form.dailyTextLimit} onChange={e=>set('dailyTextLimit',Number(e.target.value))}/></Field><Field label="Voice replies per player/day"><input type="number" value={form.dailyVoiceLimit} onChange={e=>set('dailyVoiceLimit',Number(e.target.value))}/></Field><Field label="Firebase cache seconds"><input type="number" value={form.firebaseCacheSeconds} onChange={e=>set('firebaseCacheSeconds',Number(e.target.value))}/></Field></div></Section><Section title="Google Chirp 3 HD safety"><div className="form-grid"><Field label="Published monthly characters"><input type="number" value={form.chirpMonthlyAllowance} onChange={e=>set('chirpMonthlyAllowance',Number(e.target.value))}/></Field><Field label="Hard stop percentage"><input type="number" value={form.chirpStopPercent} onChange={e=>set('chirpStopPercent',Number(e.target.value))}/></Field><Field label="Upload Google Cloud TTS service account JSON" full><input type="file" accept="application/json" onChange={async e=>{const file=e.target.files?.[0];if(!file)return;const fd=new FormData();fd.append('serviceAccount',file);try{await api('admin/upload-google-service-account.php',{method:'POST',body:fd});setMsg('Google TTS service account uploaded.')}catch(x){setMsg(x.message)}}}/></Field></div></Section><button className="primary-button">Save settings</button></form></div>}
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 <div className="admin-page"><h1>Firebase</h1><p>Loading…</p></div>;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 <div className="admin-page"><div className="page-head"><div><span className="eyebrow">PLAYER DATA</span><h1>Firebase connection</h1><p>Unity exchanges its Firebase ID token for a one-time web launch ticket.</p></div></div>{msg&&<div className="alert">{msg}</div>}<form className="settings-form" onSubmit={save}><Section title="Firebase project"><div className="form-grid"><Field label="Firebase project ID"><input value={form.projectId||''} onChange={e=>set('projectId',e.target.value)}/></Field><Field label="Realtime Database base URL"><input value={form.rtdbBaseUrl||''} onChange={e=>set('rtdbBaseUrl',e.target.value)}/></Field><Field label="Upload Firebase service account JSON" full><input type="file" accept="application/json" onChange={async e=>{const file=e.target.files?.[0];if(!file)return;const fd=new FormData();fd.append('serviceAccount',file);try{await api('admin/upload-firebase-service-account.php',{method:'POST',body:fd});setMsg('Firebase service account uploaded.')}catch(x){setMsg(x.message)}}}/></Field></div></Section><Section title="AI context sources"><p className="muted">One source per line: Label|firestore|users/&#123;uid&#125; or Label|rtdb|users/&#123;uid&#125;</p><textarea className="source-editor" rows="10" value={form.sourcesText||''} onChange={e=>set('sourcesText',e.target.value)}/><div className="form-grid"><Field label="Selected coach field names"><input value={form.coachIdKeys||''} onChange={e=>set('coachIdKeys',e.target.value)}/></Field><Field label="Test Firebase UID"><input value={form.testUid||''} onChange={e=>set('testUid',e.target.value)}/></Field></div><button type="button" className="secondary-button" onClick={test}>Test connection</button></Section><button className="primary-button">Save Firebase settings</button></form></div>}
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 <div className="admin-page"><div className="page-head"><div><span className="eyebrow">KNOWLEDGE BASE</span><h1>UpChamp and football knowledge</h1><p>Store approved facts about football, products, owners, drills and training.</p></div></div>{msg&&<div className="alert">{msg}</div>}<Section><textarea className="knowledge-editor" rows="28" value={text} onChange={e=>setText(e.target.value)}/><div className="knowledge-actions"><input type="file" accept=".txt,.md,.json,.csv" onChange={async e=>{const file=e.target.files?.[0];if(!file)return;const fd=new FormData();fd.append('file',file);try{const d=await api('admin/upload-knowledge.php',{method:'POST',body:fd});setText(d.text);setMsg('Knowledge file uploaded.')}catch(x){setMsg(x.message)}}}/><button className="primary-button" onClick={save}>Save knowledge</button></div></Section></div>}
function Usage(){const[data,setData]=useState(null);useEffect(()=>{api('admin/usage.php').then(setData)},[]);if(!data)return <div className="admin-page"><h1>Usage</h1><p>Loading…</p></div>;return <div className="admin-page"><div className="page-head"><div><span className="eyebrow">QUOTAS</span><h1>Usage and safety limits</h1><p>No chat content is stored.</p></div></div><div className="metric-grid"><Metric name="MessageCircle" label="Text turns today" value={data.textToday}/><Metric name="Volume2" label="Voice replies today" value={data.voiceToday}/><Metric name="Gauge" label="Chirp used this month" value={data.chirpUsed.toLocaleString()}/><Metric name="Gauge" label="Chirp hard stop" value={data.chirpStop.toLocaleString()}/></div><Section title="Chirp monthly meter"><div className="usage-bar"><i style={{width:`${Math.min(100,data.chirpPercent)}%`}}/></div><p>{data.chirpPercent.toFixed(1)}% of your configured safe limit has been used.</p></Section></div>}
function App(){const path=usePath();if(path==='/chat')return <CoachChat/>;if(path==='/admin/login')return <AdminLogin/>;if(path.startsWith('/admin'))return <AdminShell path={path}/>;return <Launch/>}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
