/* ============================================================
   NeuroRoute — Developer pages: API Keys, Cost Estimator,
   Routing Explorer, Playground — all wired to live APIs.
   ============================================================ */
const D2 = window.DATA;

/* Compression engines offered in the Playground's picker. Single source of
   truth for the three places (default-checked state, submit-time filter,
   checkbox render list) that must otherwise be kept in sync by hand. Mirrors
   the registry in internal/compress/service.go's Service.registry(). */
const COMPRESSION_ENGINES = [
  {id:'normalize', label:'Normalize'},
  {id:'command-output', label:'Command output'},
  {id:'rag-dedup', label:'RAG dedup'},
  {id:'tool-result-filter', label:'Tool result filter'},
  {id:'caveman', label:'Caveman'},
  {id:'tabular', label:'Tabular'},
  {id:'llmlingua2', label:'LLMLingua-2 (ML)'},
];

/* =====================  QUICKSTART  ===================== */
/* Drop-in integration card — base URL + OpenAI-SDK-compatible snippets.
   Customers swap base_url + api_key and it works. No request signing. */
function Quickstart(props){
  var apiKey = props.apiKey || 'nr_live_YOUR_KEY';
  var _tab = useState('curl'); var tab=_tab[0], setTab=_tab[1];
  var base = window.location.origin + '/v1';

  var snippets = {
    curl: 'curl '+base+'/chat/completions \\\n'+
          '  -H "Authorization: Bearer '+apiKey+'" \\\n'+
          '  -H "Content-Type: application/json" \\\n'+
          '  -d \'{\n'+
          '    "model": "auto",\n'+
          '    "messages": [{"role": "user", "content": "Hello!"}]\n'+
          '  }\'',
    python: 'from openai import OpenAI\n\n'+
            'client = OpenAI(\n'+
            '    base_url="'+base+'",\n'+
            '    api_key="'+apiKey+'",\n'+
            ')\n\n'+
            'resp = client.chat.completions.create(\n'+
            '    model="auto",   # let NeuroRoute pick the optimal model\n'+
            '    messages=[{"role": "user", "content": "Hello!"}],\n'+
            ')\n'+
            'print(resp.choices[0].message.content)',
    node: 'import OpenAI from "openai";\n\n'+
          'const client = new OpenAI({\n'+
          '  baseURL: "'+base+'",\n'+
          '  apiKey: "'+apiKey+'",\n'+
          '});\n\n'+
          'const resp = await client.chat.completions.create({\n'+
          '  model: "auto",\n'+
          '  messages: [{ role: "user", content: "Hello!" }],\n'+
          '});\n'+
          'console.log(resp.choices[0].message.content);',
  };
  var tabs = [['curl','cURL'],['python','Python'],['node','Node.js']];

  return (
    <SectionCard title="Quickstart — drop-in OpenAI compatible"
      sub="Point any OpenAI SDK at NeuroRoute by changing two lines: base URL + API key.">
      <div className="grid" style={{gap:14}}>
        <div className="grid" style={{gridTemplateColumns:'1fr 1fr',gap:12}}>
          <Field label="Base URL"><div className="row" style={{gap:8}}><code className="codeblock" style={{flex:1,padding:'9px 12px',fontSize:12.5}}>{base}</code><CopyBtn text={base}/></div></Field>
          <Field label="Model"><div className="row" style={{gap:8}}><code className="codeblock" style={{flex:1,padding:'9px 12px',fontSize:12.5}}>auto</code><span className="faint" style={{fontSize:11,alignSelf:'center'}}>or any model id</span></div></Field>
        </div>
        <div>
          <div className="seg" style={{marginBottom:10}}>
            {tabs.map(function(pair){return <button key={pair[0]} className={tab===pair[0]?'active':''} onClick={function(){setTab(pair[0]);}}>{pair[1]}</button>;})}
          </div>
          <div style={{position:'relative'}}>
            <pre className="codeblock" style={{padding:'14px 16px',fontSize:12.5,lineHeight:1.6,overflowX:'auto',margin:0,whiteSpace:'pre'}}>{snippets[tab]}</pre>
            <div style={{position:'absolute',top:10,right:10}}><CopyBtn text={snippets[tab]}/></div>
          </div>
        </div>
        <div className="faint" style={{fontSize:12.5,lineHeight:1.6}}>
          <b>Streaming</b> works too — set <code className="mono">stream: true</code>. Every response includes
          <code className="mono"> X-Routed-Model</code>, <code className="mono">X-Request-Cost</code> and
          <code className="mono"> X-Request-Id</code> headers. Paste the request ID into the Routing Explorer to see why a model was chosen.
        </div>
      </div>
    </SectionCard>
  );
}

/* ----- IDE integration guide — the ideal editor/agent setup ----- */
function IDEGuide(props){
  var apiKey = props.apiKey || 'nr_live_YOUR_KEY';
  var base = window.location.origin + '/v1';
  var _open=useState(false); var open=_open[0], setOpen=_open[1];

  var example =
    'from openai import OpenAI\n'+
    'import uuid\n\n'+
    'client = OpenAI(base_url="'+base+'", api_key="'+apiKey+'")\n\n'+
    '# One conversation id per editor session -> NeuroRoute remembers the\n'+
    '# thread server-side and injects prior turns automatically (retain orgs).\n'+
    'SESSION = str(uuid.uuid4())\n\n'+
    'resp = client.chat.completions.create(\n'+
    '    model="auto",\n'+
    '    messages=[{"role": "user", "content": "Refactor the auth module"}],\n'+
    '    extra_body={"conversation_id": SESSION},\n'+
    '    stream=True,\n'+
    ')\n'+
    'for chunk in resp:\n'+
    '    print(chunk.choices[0].delta.content or "", end="")';

  return (
    <SectionCard title="IDE integration guide" sub="The ideal setup for coding assistants, editor plugins, and agents calling NeuroRoute."
      action={<button className="btn btn-ghost btn-sm" onClick={function(){setOpen(!open);}}>{open?'Hide':'Show guide'}</button>}>
      {!open ? <div className="faint" style={{fontSize:12.5}}>Context layers, conversation memory, fallback behavior, response headers, and backoff — everything an IDE client should wire up.</div> :
      <div className="grid" style={{gap:16}}>
        <div>
          <div style={{fontWeight:600,marginBottom:6}}>1 · Layer your context (set once, applies to every request)</div>
          <ul className="faint" style={{fontSize:12.5,lineHeight:1.7,margin:0,paddingLeft:18}}>
            <li><b>Project context</b> (Settings → Project context, or per-key): long-lived project DNA — conventions, glossary, tone. Injected as the leading system message on every call; stable text also makes an ideal provider prompt-cache prefix.</li>
            <li><b>Sprint context</b> (Settings → Sprint context): what the team is building <i>this week</i>. Appended after project context; update it weekly without touching the layer above.</li>
            <li><b>Task context</b>: the 3–5 files relevant to the task at hand — your IDE assembles these into the request messages itself.</li>
            <li><b>Working memory</b>: pass a <code className="mono">conversation_id</code> (any UUID, one per editor session) and NeuroRoute stores the thread server-side and re-injects recent turns on each single-turn request — context survives across sessions and model switches. Works for streaming too. (Retain-mode orgs only; zero-retention orgs keep everything client-side by design.)</li>
          </ul>
        </div>
        <div>
          <div style={{fontWeight:600,marginBottom:6}}>2 · Send requests (streaming, with session memory)</div>
          <div style={{position:'relative'}}>
            <pre className="codeblock" style={{padding:'14px 16px',fontSize:12.5,lineHeight:1.6,overflowX:'auto',margin:0,whiteSpace:'pre'}}>{example}</pre>
            <div style={{position:'absolute',top:10,right:10}}><CopyBtn text={example}/></div>
          </div>
        </div>
        <div>
          <div style={{fontWeight:600,marginBottom:6}}>3 · Reliability is built in — nothing to code</div>
          <ul className="faint" style={{fontSize:12.5,lineHeight:1.7,margin:0,paddingLeft:18}}>
            <li>Provider errors and empty responses (e.g. a reasoning model exhausting its output budget) automatically fall back to the next-best model — same request, full context, no client retry needed. The swap is disclosed via <code className="mono">X-Model-Fallback: from-&gt;to</code>.</li>
            <li>Streams are probed before any bytes are sent to you: a dead or empty stream retries on a fallback model invisibly; you either get a working stream or a clean JSON error, never an empty 200.</li>
          </ul>
        </div>
        <div>
          <div style={{fontWeight:600,marginBottom:6}}>4 · Headers worth reading</div>
          <ul className="faint" style={{fontSize:12.5,lineHeight:1.7,margin:0,paddingLeft:18}}>
            <li><code className="mono">X-Routed-Model</code> — the model that actually answered · <code className="mono">X-Request-Cost</code> — this request's provider cost in USD.</li>
            <li><code className="mono">X-Key-Budget-Day</code> / <code className="mono">X-Key-Budget-Month</code> — spend vs your key's caps · <code className="mono">X-Budget-Warning</code> appears at 80%.</li>
            <li><code className="mono">X-Data-Retention</code> — per-request proof of your org's retention posture · <code className="mono">X-Conversation-Memory</code> — how many stored turns were injected.</li>
          </ul>
        </div>
        <div>
          <div style={{fontWeight:600,marginBottom:6}}>5 · Back off politely</div>
          <div className="faint" style={{fontSize:12.5,lineHeight:1.7}}>
            Rate limits (<code className="mono">429</code>) and budget caps (<code className="mono">402</code>) both include a <code className="mono">Retry-After</code> header with the seconds until the window resets — honor it instead of hammering. OpenAI SDKs retry 429s automatically; treat a 402 as "pause until the stated reset (or raise the cap in API Keys)".
          </div>
        </div>
      </div>}
    </SectionCard>
  );
}

/* =====================  API KEYS  ===================== */
/* Edit per-key spend limits — PATCH /v1/auth/keys/{id}. 0/blank clears a cap. */
/* Full key editor — everything is editable except the name and the secret
   (the secret is shown once at creation and never stored in recoverable
   form; changing identity means minting a new key). PATCH /v1/auth/keys/{id}. */
function EditLimitsModal(props){
  const k=props.k;
  const models=props.models||[];
  const [strategy, setStrategy]=useState(k.strategy||'');
  const [allow, setAllow]=useState((k.allowlist||[]).slice());
  const [allowOpen, setAllowOpen]=useState(false);
  const [d, setD]=useState(k.dayCap!=null?String(k.dayCap):'');
  const [m, setM]=useState(k.monthCap!=null?String(k.monthCap):'');
  const [r, setR]=useState(k.runCap!=null?String(k.runCap):'');
  const [pctx, setPctx]=useState(k.projectContext||'');
  const [assignEmail, setAssignEmail]=useState(k.assignedTo||'');
  const [wsId, setWsId]=useState(k.workspaceId||'');
  const [saving, setSaving]=useState(false);
  const members=props.members||[];
  const workspaces=props.workspaces||[];
  const isAdmin=props.isAdmin;
  const toggleAllow=function(id){
    setAllow(function(cur){var i=cur.indexOf(id); if(i>=0){var n=cur.slice(); n.splice(i,1); return n;} return cur.concat([id]);});
  };
  const save=function(){
    setSaving(true);
    var patch={
      routing_strategy: strategy,
      model_allowlist: allow,
      daily_budget_usd: parseFloat(d)||0,
      monthly_budget_usd: parseFloat(m)||0,
      default_run_budget_usd: parseFloat(r)||0,
      project_context: pctx
    };
    if(isAdmin){ patch.assigned_to_email=assignEmail; patch.workspace_id=wsId; }
    NR_API.updateApiKey(k.id, patch).then(function(ok){
      setSaving(false);
      if(ok) props.onSaved(); else props.onError();
    }).catch(function(){ setSaving(false); props.onError(); });
  };
  return (
    <Modal title={'Edit key — '+k.name} sub="Everything except the name and the secret can be changed. Takes effect on the key's next request." onClose={props.onClose}
      footer={<><button className="btn btn-ghost" onClick={props.onClose}>Cancel</button>
        <button className="btn btn-primary" disabled={saving} onClick={save}>{saving?'Saving…':'Save changes'}</button></>}>
      <div className="grid" style={{gap:16}}>
        <Field label="Routing strategy" hint="Inherit follows your org default.">
          <select className="select" value={strategy} onChange={function(e){setStrategy(e.target.value);}}>
            <option value="">Inherit org default (Task-aware)</option>
            <option value="task-aware">Task-aware</option>
            <option value="balanced">Balanced</option>
            <option value="cheapest">Cheapest</option>
            <option value="fastest">Fastest</option>
            <option value="best-quality">Best quality</option>
            <option value="cascade">Cascade</option>
            <option value="fusion">Fusion</option>
            <option value="pipeline">Pipeline</option>
          </select>
        </Field>
        <Field label="Model allowlist" hint="None selected = all models.">
          <div style={{position:'relative'}}>
            <div className="select" onClick={function(){setAllowOpen(!allowOpen);}} style={{cursor:'pointer',display:'flex',justifyContent:'space-between',alignItems:'center'}}>
              <span>{allow.length===0 ? 'All models' : allow.length+' model'+(allow.length===1?'':'s')+' selected'}</span>
              <span style={{opacity:.6}}>{allowOpen?'▴':'▾'}</span>
            </div>
            {allowOpen && <div style={{position:'absolute',zIndex:30,top:'calc(100% + 4px)',left:0,right:0,maxHeight:220,overflowY:'auto',background:'var(--bg-2)',border:'1px solid var(--line)',borderRadius:10,padding:6,boxShadow:'0 10px 30px rgba(0,0,0,.4)'}}>
              {models.length===0 ? <div className="faint" style={{padding:'8px 10px',fontSize:12.5}}>No models available</div> :
                models.map(function(mo){
                  var checked=allow.indexOf(mo.id)>=0;
                  return (
                    <label key={mo.id} className="row" style={{gap:9,alignItems:'center',padding:'7px 9px',borderRadius:7,cursor:'pointer'}} onClick={function(e){e.preventDefault();toggleAllow(mo.id);}}>
                      <input type="checkbox" checked={checked} readOnly style={{cursor:'pointer'}}/>
                      <span style={{fontWeight:600,color:'var(--tx-0)',fontSize:13}}>{mo.name}</span>
                      <span className="faint mono" style={{fontSize:11}}>{mo.id}</span>
                    </label>
                  );
                })}
            </div>}
          </div>
          {allow.length>0 && <div className="faint" style={{fontSize:11.5,marginTop:6}}>{allow.join(', ')}</div>}
        </Field>
        <Field label="Spend limits" hint="Hard $ caps on this key's provider spend. Blank or 0 = unlimited.">
          <div className="row" style={{gap:10}}>
            <input className="input" type="number" min="0" step="0.5" placeholder="Daily $ (unlimited)" value={d} onChange={function(e){setD(e.target.value);}}/>
            <input className="input" type="number" min="0" step="1" placeholder="Monthly $ (unlimited)" value={m} onChange={function(e){setM(e.target.value);}}/>
          </div>
        </Field>
        <Field label="Default run budget (USD)" hint="Ceiling for per-run (agent) spend. A request can set a tighter X-Run-Budget-USD but never higher. Blank = unlimited.">
          <input className="input" type="number" min="0" step="0.1" placeholder="Run $ (unlimited)" value={r} onChange={function(e){setR(e.target.value);}}/>
        </Field>
        <Field label="Project context (override)" hint="Standing instructions prepended to requests made with this key. Blank = inherit your org's project context.">
          <textarea className="input" rows={4} style={{resize:'vertical',fontFamily:'inherit',lineHeight:1.5}}
            placeholder="e.g. This key is used by the billing service — respond in strict JSON only."
            value={pctx} onChange={function(e){setPctx(e.target.value);}}/>
        </Field>
        {isAdmin && <Field label="Assigned to member" hint="Hand this key to a teammate — it shows in their API Keys and usage. Blank = unassigned.">
          <select className="select" value={assignEmail} onChange={function(e){setAssignEmail(e.target.value);}}>
            <option value="">Unassigned</option>
            {members.map(function(u){return <option key={u.id||u.email} value={u.email}>{(u.name?u.name+' — ':'')+u.email}</option>;})}
            {assignEmail && members.every(function(u){return u.email!==assignEmail;}) && <option value={assignEmail}>{assignEmail}</option>}
          </select>
        </Field>}
        {isAdmin && workspaces.length>0 && <Field label="Workspace" hint="Bind this key to a workspace — every member of it sees the key. Blank = none.">
          <select className="select" value={wsId} onChange={function(e){setWsId(e.target.value);}}>
            <option value="">No workspace</option>
            {workspaces.map(function(ws){return <option key={ws.id} value={ws.id}>{ws.name}</option>;})}
          </select>
        </Field>}
      </div>
    </Modal>
  );
}

function ApiKeysPage(){
  const t=useToast();
  const [loading, setLoading]=useState(true);
  const [keys, setKeys]=useState([]);
  const [modal, setModal]=useState(false);
  const [name, setName]=useState('');
  const [strategy, setStrategy]=useState('');   // '' = inherit org default (task-aware)
  const [allow, setAllow]=useState([]);          // selected model IDs; [] = all models
  const [allowOpen, setAllowOpen]=useState(false);
  const [models, setModels]=useState([]);        // configured platform models [{id,name}]
  const [fresh, setFresh]=useState(null);
  const [creating, setCreating]=useState(false);
  const [confirmRevoke, setConfirmRevoke]=useState(null);
  const [dayBudget, setDayBudget]=useState('');   // $ per day; '' = unlimited
  const [monthBudget, setMonthBudget]=useState(''); // $ per month; '' = unlimited
  const [runBudget, setRunBudget]=useState('');   // per-run (agent) ceiling $; '' = unlimited
  const [projectCtx, setProjectCtx]=useState('');  // per-key project-context override; '' = inherit org
  const [editLimits, setEditLimits]=useState(null); // key row being edited, or null
  const [serviceAccount, setServiceAccount]=useState(false); // 4.5: scope this key down for machine-to-machine use
  const [extraScopes, setExtraScopes]=useState([]); // additional scopes beyond "completions" when serviceAccount is on
  const [assignEmail, setAssignEmail]=useState(''); // hand this key to a member ('' = unassigned)
  const [wsId, setWsId]=useState('');               // bind this key to a workspace ('' = none)
  const [members, setMembers]=useState([]);         // org users (for the assign dropdown)
  const [workspaces, setWorkspaces]=useState([]);   // org workspaces (for the workspace dropdown)
  const isAdmin = !window.NR_SELF_SCOPED;           // only admins assign keys to others

  const loadKeys=function(){
    NR_API.listApiKeys().then(function(res){
      if(res && Array.isArray(res.keys)) {
        setKeys(res.keys.map(function(k){return {
          name: k.name, id: k.key_id, role: 'developer',
          created: k.created_at, status: k.active?'Active':'Revoked',
          dayCap: k.daily_budget_usd, monthCap: k.monthly_budget_usd, runCap: k.default_run_budget_usd,
          spendDay: k.spend_today_usd||0, spendMonth: k.spend_month_usd||0,
          strategy: k.routing_strategy||'', allowlist: k.model_allowlist||[],
          projectContext: k.project_context||'',
          assignedTo: k.assigned_to_email||'', workspaceId: k.workspace_id||'', workspaceName: k.workspace_name||''
        };}));
      }
      setLoading(false);
    }).catch(function(){ setLoading(false); });
  };

  const loadModels=function(){
    NR_API.models().then(function(m){
      var list=(m&&(m.data||m.models||m))||[];
      if(!Array.isArray(list)) list=[];
      setModels(list.map(function(x){
        var id=x.id||x;
        var info=(window.DATA&&window.DATA.modelById)?window.DATA.modelById(id):null;
        return {id:id, name:(info&&info.name)||id};
      }));
    }).catch(function(){});
  };
  const loadAssignTargets=function(){
    if(!isAdmin) return; // members can't assign to others; skip the lookups
    if(NR_API.users) NR_API.users().then(function(res){
      var list=(res&&(res.users||res))||[]; if(!Array.isArray(list)) list=[];
      setMembers(list.filter(function(u){return u.is_active!==false && u.email;}));
    }).catch(function(){});
    if(NR_API.workspaces) NR_API.workspaces().then(function(res){
      var list=(res&&(res.workspaces||res))||[]; if(!Array.isArray(list)) list=[];
      setWorkspaces(list);
    }).catch(function(){});
  };
  useEffect(function(){ loadKeys(); loadModels(); loadAssignTargets(); }, []);

  if(loading) return <LoadingPage/>;

  const roleColor=function(r){return ({admin:'magenta',developer:'blue',viewer:'gray'})[r]||'gray';};

  const toggleAllow=function(id){
    setAllow(function(cur){ var i=cur.indexOf(id); if(i>=0){var n=cur.slice();n.splice(i,1);return n;} return cur.concat([id]); });
  };

  const create=function(){
    if(!name) return;
    setCreating(true);
    var scopes = serviceAccount ? ['completions'].concat(extraScopes) : [];
    var extra = {assigned_to_email: assignEmail.trim(), workspace_id: wsId};
    NR_API.createApiKey(name, 'developer', strategy, allow, parseFloat(dayBudget)||0, parseFloat(monthBudget)||0, parseFloat(runBudget)||0, projectCtx.trim(), scopes, extra).then(function(res){
      setCreating(false);
      if(res && res.key) {
        setFresh(res.key);
        loadKeys();
        setModal(false); setName(''); setStrategy(''); setAllow([]); setAllowOpen(false); setDayBudget(''); setMonthBudget(''); setRunBudget(''); setProjectCtx(''); setServiceAccount(false); setExtraScopes([]); setAssignEmail(''); setWsId('');
        t({title:'API key created', msg:'Copy it now — it won\'t be shown again.'});
      } else {
        t({kind:'error', title:'Failed to create key', msg:(res&&res.error&&res.error.message)||'Server returned 501 — DB key persistence not yet wired.'});
      }
    }).catch(function(){ setCreating(false); t({kind:'error', title:'Failed to create key'}); });
  };

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc={window.NR_SELF_SCOPED ? "Create and manage the API keys you own. You see only keys you created." : "Create and manage keys your applications use to authenticate with the NeuroRoute gateway."}>
        <button className="btn btn-primary btn-sm" onClick={function(){setFresh(null);setModal(true);}}><Icon name="plus" size={15}/>Create new API key</button>
      </PageHead>

      {fresh && <div className="card card-pad" style={{background:'var(--amber-t)',borderColor:'rgba(245,158,11,.4)'}}>
        <div className="row" style={{gap:10,marginBottom:10}}><Icon name="alert" size={17} style={{color:'var(--amber)'}}/><b style={{color:'#fbc15a'}}>Save this key — it won't be shown again.</b></div>
        <div className="row" style={{gap:10}}>
          <code className="codeblock" style={{flex:1,padding:'10px 14px',fontSize:13,color:'#fbc15a'}}>{fresh}</code>
          <CopyBtn text={fresh}/>
          <button className="iconbtn" onClick={function(){setFresh(null);}}><Icon name="x" size={14}/></button>
        </div>
      </div>}

      {/* Quickstart — drop-in OpenAI-compatible integration */}
      <Quickstart apiKey={fresh}/>

      {/* Full IDE/agent integration walkthrough — context layers, memory, fallback, backoff */}
      <IDEGuide apiKey={fresh}/>

      {keys.length===0 ?
        <EmptyState icon="apikey" title="No API keys yet"
          desc="Create an API key to start sending requests through the NeuroRoute gateway. Keys authenticate your requests and track usage."
          actions={[{label:'Create API Key', onClick:function(){setFresh(null);setModal(true);}, primary:true}]}/> :
        <SectionCard title="Active keys" sub={keys.filter(function(k){return k.status==='Active';}).length+' active · '+keys.length+' total'} pad={false}>
          <div className="scroll-x">
            <table className="table">
              <thead><tr><th>Name</th><th>Key ID</th><th>Role</th><th>Created</th><th>Spend (day / mo)</th><th>Status</th><th></th></tr></thead>
              <tbody>
                {keys.map(function(k){return (
                  <tr key={k.id} style={k.status==='Revoked'?{opacity:.5}:null}>
                    <td style={{fontWeight:600,color:'var(--tx-0)'}}>
                      {k.name}
                      {(k.assignedTo||k.workspaceName) && <div className="faint" style={{fontSize:11,fontWeight:400,marginTop:2}}>
                        {k.assignedTo && <span>▸ {k.assignedTo}</span>}
                        {k.assignedTo && k.workspaceName && <span> · </span>}
                        {k.workspaceName && <span>⬢ {k.workspaceName}</span>}
                      </div>}
                    </td>
                    <td className="num faint" style={{fontSize:12}}>{k.id}</td>
                    <td><Badge color={roleColor(k.role)} style={{textTransform:'capitalize'}}>{k.role}</Badge></td>
                    <td className="faint">{timeAgo(k.created)}</td>
                    <td className="num" style={{fontSize:12}}>
                      ${(k.spendDay||0).toFixed(2)}{k.dayCap?' / $'+Number(k.dayCap).toFixed(0):''} · ${(k.spendMonth||0).toFixed(2)}{k.monthCap?' / $'+Number(k.monthCap).toFixed(0):''}
                      {k.runCap?<div className="faint" style={{fontSize:11,marginTop:2}}>Run cap ${Number(k.runCap).toFixed(2)}</div>:null}
                    </td>
                    <td><Badge color={k.status==='Active'?'green':'red'} dot>{k.status}</Badge></td>
                    <td style={{textAlign:'right'}}>
                      {k.status==='Active' &&
                        <button className="btn btn-ghost btn-sm" style={{marginRight:6}} onClick={function(){setEditLimits(k);}}>Edit</button>}
                      {k.status==='Active' &&
                        <button className="btn btn-danger btn-sm" onClick={function(){setConfirmRevoke(k);}}>Revoke</button>}
                    </td>
                  </tr>
                );})}
              </tbody>
            </table>
          </div>
        </SectionCard>
      }

      {editLimits && <EditLimitsModal k={editLimits} models={models} members={members} workspaces={workspaces} isAdmin={isAdmin} onClose={function(){setEditLimits(null);}}
        onSaved={function(){setEditLimits(null); loadKeys(); t({title:'Key updated', msg:editLimits.name});}}
        onError={function(){t({kind:'error', title:'Failed to update key'});}}/>}

      {confirmRevoke && <Modal title="Revoke API key?" sub={'This permanently disables "'+confirmRevoke.name+'". Apps using it will start failing immediately.'} onClose={function(){setConfirmRevoke(null);}}
        footer={<><button className="btn btn-ghost" onClick={function(){setConfirmRevoke(null);}}>Cancel</button>
          <button className="btn btn-danger" onClick={function(){
            var k=confirmRevoke; setConfirmRevoke(null);
            NR_API.revokeApiKey(k.id).then(function(ok){
              if(ok){
                setKeys(function(ks){return ks.map(function(x){return x.id===k.id?Object.assign({},x,{status:'Revoked'}):x;});});
                t({kind:'info',title:'Key revoked', msg:k.name});
              } else {
                t({kind:'error',title:'Failed to revoke key'});
              }
            });
          }}><Icon name="trash" size={14}/>Revoke key</button></>}>
        <p className="muted" style={{fontSize:14,margin:0}}>Key ID <code className="mono">{confirmRevoke.id}</code> will be invalidated. This cannot be undone.</p>
      </Modal>}

      {modal && <Modal title="Create API key" sub="A key authenticates your app's requests to the NeuroRoute API." onClose={function(){setModal(false);setAllowOpen(false);}}
        footer={<><button className="btn btn-ghost" onClick={function(){setModal(false);setAllowOpen(false);}}>Cancel</button><button className="btn btn-primary" disabled={!name||creating} onClick={create}>{creating?'Creating…':'Create key'}</button></>}>
        <div className="grid" style={{gap:16}}>
          <Field label="Key name" hint="A label to recognize this key later."><input className="input" placeholder="e.g. Production API" value={name} onChange={function(e){setName(e.target.value);}}/></Field>
          <Field label="Routing strategy" hint="Requests made with this key route using this strategy. Inherit follows your org default.">
            <select className="select" value={strategy} onChange={function(e){setStrategy(e.target.value);}}>
              <option value="">Inherit org default (Task-aware)</option>
              <option value="task-aware">Task-aware</option>
              <option value="balanced">Balanced</option>
              <option value="cheapest">Cheapest</option>
              <option value="fastest">Fastest</option>
              <option value="best-quality">Best quality</option>
              <option value="cascade">Cascade</option>
              <option value="fusion">Fusion</option>
              <option value="pipeline">Pipeline</option>
            </select>
          </Field>
          <Field label="Model allowlist (optional)" hint="Select the models this key may use. None selected = all models.">
            <div style={{position:'relative'}}>
              <div className="select" onClick={function(){setAllowOpen(!allowOpen);}} style={{cursor:'pointer',display:'flex',justifyContent:'space-between',alignItems:'center'}}>
                <span>{allow.length===0 ? 'All models' : allow.length+' model'+(allow.length===1?'':'s')+' selected'}</span>
                <span style={{opacity:.6}}>{allowOpen?'▴':'▾'}</span>
              </div>
              {allowOpen && <div style={{position:'absolute',zIndex:30,top:'calc(100% + 4px)',left:0,right:0,maxHeight:240,overflowY:'auto',background:'var(--bg-2)',border:'1px solid var(--line)',borderRadius:10,padding:6,boxShadow:'0 10px 30px rgba(0,0,0,.4)'}}>
                {models.length===0 ? <div className="faint" style={{padding:'8px 10px',fontSize:12.5}}>No models available</div> :
                  models.map(function(m){
                    var checked=allow.indexOf(m.id)>=0;
                    return (
                      <label key={m.id} className="row" style={{gap:9,alignItems:'center',padding:'7px 9px',borderRadius:7,cursor:'pointer'}} onClick={function(e){e.preventDefault();toggleAllow(m.id);}}>
                        <input type="checkbox" checked={checked} readOnly style={{cursor:'pointer'}}/>
                        <span style={{fontWeight:600,color:'var(--tx-0)',fontSize:13}}>{m.name}</span>
                        <span className="faint mono" style={{fontSize:11}}>{m.id}</span>
                      </label>
                    );
                  })}
              </div>}
            </div>
            {allow.length>0 && <div className="faint" style={{fontSize:11.5,marginTop:6}}>{allow.join(', ')}</div>}
          </Field>
          <Field label="Spend limits (optional)" hint="Hard $ caps on this key's provider spend. Requests are rejected once a cap is reached. Blank = unlimited.">
            <div className="row" style={{gap:10}}>
              <input className="input" type="number" min="0" step="0.5" placeholder="Daily $ (unlimited)" value={dayBudget} onChange={function(e){setDayBudget(e.target.value);}}/>
              <input className="input" type="number" min="0" step="1" placeholder="Monthly $ (unlimited)" value={monthBudget} onChange={function(e){setMonthBudget(e.target.value);}}/>
            </div>
          </Field>
          <Field label="Default run budget (USD)" hint="Ceiling for per-run (agent) spend. A request can set a tighter X-Run-Budget-USD but never higher. Blank = unlimited.">
            <input className="input" type="number" min="0" step="0.1" placeholder="Run $ (unlimited)" value={runBudget} onChange={function(e){setRunBudget(e.target.value);}}/>
          </Field>
          <Field label="Project context (override, optional)" hint="Standing instructions prepended to requests made with this key. Blank = inherit your org's project context.">
            <textarea className="input" rows={4} style={{resize:'vertical',fontFamily:'inherit',lineHeight:1.5}}
              placeholder="e.g. This key is used by the billing service — respond in strict JSON only."
              value={projectCtx} onChange={function(e){setProjectCtx(e.target.value);}}/>
          </Field>
          <div className="spread" style={{padding:'2px 0'}}>
            <div><div style={{fontWeight:600}}>Service account (scoped)</div><div className="faint" style={{fontSize:12.5}}>Restricts this key to calling completions only — it can't read usage/billing or manage other keys, regardless of role.</div></div>
            <Switch on={serviceAccount} onChange={setServiceAccount}/>
          </div>
          {serviceAccount && <Field label="Additional scopes" hint="Completions is always included. Add more capabilities this service account needs.">
            <div className="grid" style={{gap:8}}>
              {[['read:usage','Read usage & billing'],['admin','Manage API keys']].map(function(pair){
                var val=pair[0], label=pair[1];
                var checked=extraScopes.indexOf(val)>=0;
                return <label key={val} className="row" style={{gap:9,alignItems:'center',cursor:'pointer'}} onClick={function(e){e.preventDefault();
                  setExtraScopes(function(cur){var i=cur.indexOf(val); if(i>=0){var n=cur.slice();n.splice(i,1);return n;} return cur.concat([val]);});
                }}>
                  <input type="checkbox" checked={checked} readOnly style={{cursor:'pointer'}}/>
                  <span style={{fontSize:13}}>{label}</span>
                </label>;
              })}
            </div>
          </Field>}
          {isAdmin && <Field label="Assign to member (optional)" hint="Hand this key to a teammate — it shows up in their API Keys and usage views.">
            <select className="select" value={assignEmail} onChange={function(e){setAssignEmail(e.target.value);}}>
              <option value="">Unassigned</option>
              {members.map(function(u){return <option key={u.id||u.email} value={u.email}>{(u.name?u.name+' — ':'')+u.email}</option>;})}
            </select>
          </Field>}
          {isAdmin && workspaces.length>0 && <Field label="Assign to workspace (optional)" hint="Bind this key to a workspace — every member of that workspace sees it.">
            <select className="select" value={wsId} onChange={function(e){setWsId(e.target.value);}}>
              <option value="">No workspace</option>
              {workspaces.map(function(ws){return <option key={ws.id} value={ws.id}>{ws.name}</option>;})}
            </select>
          </Field>}
        </div>
      </Modal>}
    </div>
  );
}

/* =====================  COST ESTIMATOR  ===================== */
function EstimatorPage(){
  var t=useToast();
  var _msg=useState(''); var msg=_msg[0], setMsg=_msg[1];
  var _model=useState('auto'); var model=_model[0], setModel=_model[1];
  var _strategy=useState('balanced'); var strategy=_strategy[0], setStrategy=_strategy[1];
  var _res=useState(null); var res=_res[0], setRes=_res[1];
  var _loading=useState(false); var loading=_loading[0], setLoading=_loading[1];
  var _err=useState(null); var err=_err[0], setErr=_err[1];

  var estimate=function(){
    if(!msg.trim()) return;
    setLoading(true); setErr(null);
    NR_API.estimate(msg, model, strategy).then(function(data){
      setLoading(false);
      if(data && data._ok && !data.error) {
        /* Normalize server response shape to a single internal shape */
        setRes({
          routed_model: data.selected_model || data.routed_model || (model!=='auto'?model:''),
          task_type: data.task_type || 'general',
          confidence: data.task_confidence || data.confidence || 0,
          estimated_cost: data.estimated_cost || 0,
          baseline_cost: data.counterfactual_cost || data.counterfactual_gpt4o_cost || data.baseline_cost || 0,
          estimated_savings: data.estimated_savings || 0,
          estimated_tokens: {
            input: data.estimated_input_tokens || (data.estimated_tokens && data.estimated_tokens.input) || 0,
            output: data.estimated_output_tokens || (data.estimated_tokens && data.estimated_tokens.output) || 0,
          },
          strategy: data.strategy || strategy,
          decision_time_ms: data.decision_time_ms || 0,
          client_estimated: false,
        });
      }
      else {
        /* Fallback: client-side estimation from reference data */
        var chosen = model==='auto' ? D2.modelById('gemini-flash') : D2.modelById(model);
        var inT=Math.max(40,Math.round(msg.length/3.6)), outT=Math.round(inT*2.4+260);
        var baseCost=gpt4oBaseline(inT,outT);
        // With no rate for this model, the arithmetic below yields cost 0 —
        // which the savings card then renders as "100% saved". Report the cost
        // as unavailable instead of inventing a free request.
        var priced = chosen.hasPricing !== false && (chosen.in > 0 || chosen.out > 0);
        var cost = priced ? (inT*chosen.in+outT*chosen.out)/1e6 : null;
        setRes({routed_model:chosen.id, task_type:'general', estimated_cost:cost, baseline_cost:baseCost,
          estimated_tokens:{input:inT,output:outT}, confidence:0.85, client_estimated:true,
          cost_unavailable:!priced});
      }
    }).catch(function(e){ setLoading(false); setErr(e.message||'Estimation failed'); });
  };

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Compose a request and preview the routed model, cost, and savings before you send it."/>
      <div className="grid" style={{gridTemplateColumns:'1fr 1fr',alignItems:'start'}}>
        <SectionCard title="Compose request">
          <div className="grid" style={{gap:16}}>
            <Field label="Message"><textarea className="textarea" style={{minHeight:150}} placeholder="Refactor this Python function to use async/await..." value={msg} onChange={function(e){setMsg(e.target.value);}}/></Field>
            <div className="grid" style={{gridTemplateColumns:'1fr 1fr',gap:12}}>
              <Field label="Model"><select className="select" value={model} onChange={function(e){setModel(e.target.value);}}>
                <option value="auto">Auto (recommended)</option>
                {D2.MODELS.map(function(m){return <option key={m.id} value={m.id}>{m.name}</option>;})}</select></Field>
              <Field label="Strategy"><select className="select" value={strategy} onChange={function(e){setStrategy(e.target.value);}}>
                {D2.STRATEGIES.map(function(s){return <option key={s} value={s.toLowerCase().replace(/ /g,'-')}>{s}</option>;})}</select></Field>
            </div>
            <button className="btn btn-primary btn-block" disabled={!msg.trim()||loading} onClick={estimate}>
              <Icon name="estimator" size={16}/>{loading?'Estimating…':'Estimate cost'}
            </button>
          </div>
        </SectionCard>

        {res ? (function(){
          var chosen = D2.modelById(res.routed_model||'');
          var prov = D2.providerById(chosen.provider);
          var inT = (res.estimated_tokens&&res.estimated_tokens.input)||0;
          var outT = (res.estimated_tokens&&res.estimated_tokens.output)||0;
          var cost = res.estimated_cost||0;
          var baseCost = res.baseline_cost||gpt4oBaseline(inT,outT);
          var saved = baseCost - cost;
          var pct = baseCost>0 ? Math.round((1-cost/baseCost)*100) : 0;
          return (
            <div className="card">
              <div className="card-hd"><div className="card-title">Estimate</div>
                {res.client_estimated ? <Badge color="amber">Client-side</Badge> : <Badge color="green" dot>Server</Badge>}
              </div>
              <div style={{padding:20}}>
                <div className="card card-pad" style={{background:'var(--bg-inset)',marginBottom:16}}>
                  <div className="spread">
                    <span className="row" style={{gap:10}}>
                      <ProviderLogo id={chosen.provider} size={34}/>
                      <div><div style={{fontWeight:700}}>{chosen.name}</div><div className="faint" style={{fontSize:12}}>{prov.name} · q{chosen.q}</div></div>
                    </span>
                    <span style={{textAlign:'right'}}>
                      {res.task_type && <Badge color="blue">{res.task_type}</Badge>}
                      {res.confidence && <div className="faint mono" style={{fontSize:11,marginTop:4}}>{Math.round((res.confidence||0)*100)}% confidence</div>}
                    </span>
                  </div>
                </div>
                <div className="grid" style={{gridTemplateColumns:'1fr 1fr',gap:12,marginBottom:16}}>
                  <div className="card card-pad"><div className="faint" style={{fontSize:12}}>Input · {fmtNum(inT)} tok</div><div className="mono" style={{fontSize:17,marginTop:4}}>{res.cost_unavailable?'—':'$'+(inT*chosen.in/1e6).toFixed(5)}</div></div>
                  <div className="card card-pad"><div className="faint" style={{fontSize:12}}>Output · {fmtNum(outT)} tok</div><div className="mono" style={{fontSize:17,marginTop:4}}>{res.cost_unavailable?'—':'$'+(outT*chosen.out/1e6).toFixed(5)}</div></div>
                </div>
                {/* No rate for this model: say so, rather than showing the
                    baseline minus zero as a 100%-saved headline. */}
                {res.cost_unavailable && <div className="card card-pad" style={{textAlign:'center'}}>
                  <div className="faint" style={{fontSize:12.5}}>No published rate for this model — cost can't be estimated client-side. Send it in the Playground for the real billed cost.</div>
                </div>}
                {!res.cost_unavailable && saved>0 && <div className="card card-pad" style={{textAlign:'center',background:'var(--green-t)',borderColor:'rgba(16,185,129,.3)'}}>
                  <div className="faint" style={{fontSize:12.5}}>You save vs Claude Fable 5 (${baseCost.toFixed(5)})</div>
                  <div className="display route-text" style={{fontSize:34,margin:'6px 0'}}>${saved.toFixed(5)}</div>
                  <Badge color="green">{pct}% cheaper</Badge>
                </div>}
                <button className="btn btn-grad btn-block" style={{marginTop:16}} onClick={function(){go('playground');}}><Icon name="send" size={15}/>Send in Playground</button>
              </div>
            </div>
          );
        })() : (
          <div className="card empty"><div className="e-ico"><Icon name="estimator" size={26} style={{color:'var(--tx-2)'}}/></div><h3 style={{fontSize:16}}>No estimate yet</h3><p className="faint" style={{fontSize:13,maxWidth:260}}>Compose a request and hit Estimate to preview routing and savings.</p></div>
        )}
      </div>
      {err && <div className="card card-pad" style={{background:'var(--red-t)',borderColor:'rgba(239,68,68,.3)'}}><Icon name="alert" size={16}/> {err}</div>}
    </div>
  );
}

/* =====================  ROUTING EXPLORER  ===================== */
function ExplorerPage(){
  var _reqId=useState(''); var reqId=_reqId[0], setReqId=_reqId[1];
  var _data=useState(null); var data=_data[0], setData=_data[1];
  var _loading=useState(false); var loading=_loading[0], setLoading=_loading[1];
  var _error=useState(null); var error=_error[0], setError=_error[1];
  var _recent=useState([]); var recent=_recent[0], setRecent=_recent[1];

  useEffect(function(){
    NR_API.dashboardRouting(15).then(function(res){
      if(res && Array.isArray(res.decisions)) setRecent(res.decisions);
    }).catch(function(){});

    // Deep-link support: #/explorer?req=<id> (e.g. from the Playground chip)
    // auto-fills the box and traces the request immediately.
    var h = window.location.hash || '';
    var qi = h.indexOf('?');
    if(qi >= 0){
      try {
        var rid = new URLSearchParams(h.slice(qi+1)).get('req');
        if(rid){ lookup(rid); }
      } catch(e){}
    }
  },[]);

  var lookup=function(idOverride){
    var rid = (idOverride||reqId||'').trim();
    if(!rid) return;
    setReqId(rid);
    setLoading(true); setError(null); setData(null);
    NR_API.routingExplain(rid).then(function(res){
      setLoading(false);
      if(res) { setData(res); }
      else { setError('Request not found. It may have expired from the in-memory store (kept for ~5 min).'); }
    }).catch(function(){ setError('Failed to fetch routing explanation.'); setLoading(false); });
  };

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Inspect exactly why a request was routed the way it was — scores, cascade trace, and cost proof."/>
      <div className="card card-pad">
        <div className="row" style={{gap:10}}>
          <div className="searchbar" style={{flex:1,maxWidth:'none'}}><Icon name="search" size={16}/><input value={reqId} onChange={function(e){setReqId(e.target.value);}} placeholder="Enter request ID (from X-Request-Id response header)" onKeyDown={function(e){if(e.key==='Enter') lookup();}}/></div>
          <button className="btn btn-primary" disabled={!reqId.trim()||loading} onClick={function(){lookup();}}>{loading?'Looking up…':'Trace request'}</button>
        </div>
      </div>

      {recent.length>0 && !data && !error && <SectionCard title="Recent routing decisions" sub="Click any request to inspect" pad={false}>
        <table className="table">
          <thead><tr><th>Time</th><th>Request ID</th><th>Model</th><th>Task</th><th style={{textAlign:'right'}}>Latency</th></tr></thead>
          <tbody>{recent.slice(0,10).map(function(r){
            var rid = r.request_id||r.RequestID||'';
            return (
              <tr key={rid} style={{cursor:'pointer'}} onClick={function(){lookup(rid);}}>
                <td className="faint">{timeAgo(r.created_at||r.CreatedAt)}</td>
                <td className="mono faint" style={{fontSize:12}}>{rid.slice(0,18)}…</td>
                <td><ModelCell id={r.model||r.Model||''}/></td>
                <td><Badge color="blue">{r.task_type||r.TaskType||'—'}</Badge></td>
                <td className="num faint" style={{textAlign:'right'}}>{(r.routing_latency_ms||r.RoutingLatencyMs||0)}ms</td>
              </tr>
            );
          })}</tbody>
        </table>
      </SectionCard>}

      {error && <div className="card card-pad" style={{background:'var(--red-t)',borderColor:'rgba(239,68,68,.3)'}}><Icon name="alert" size={16}/> {error}</div>}

      {data && (function(){
        var winner = D2.modelById(data.selected_model||data.SelectedModel||data.model||'');
        var taskType = data.task_type||data.TaskType||'general';
        var strat = data.strategy||data.Strategy||data.routing_strategy||'—';
        var conf = data.task_confidence||data.confidence||data.Confidence||0;
        var scores = data.candidate_scores||data.scores||data.Scores||[];
        var cascade = data.cascade_trace||data.CascadeTrace||[];
        var cost = data.request_cost||data.cost||data.Cost||0;
        var baseCost = data.baseline_cost||0;

        return <>
          <div className="grid" style={{gridTemplateColumns:'repeat(4,1fr)'}}>
            <div className="card card-pad"><div className="faint" style={{fontSize:12}}>Task classification</div><div style={{marginTop:8}}><Badge color="blue">{taskType}</Badge></div><div className="faint mono" style={{fontSize:11,marginTop:6}}>{Math.round(conf*100)}% confidence</div></div>
            <div className="card card-pad"><div className="faint" style={{fontSize:12}}>Strategy</div><div style={{marginTop:8,fontWeight:700}}>{strat}</div></div>
            <div className="card card-pad"><div className="faint" style={{fontSize:12}}>Routed model</div><div className="row" style={{gap:8,marginTop:8}}><ProviderLogo id={winner.provider} size={24}/><b>{winner.name}</b></div></div>
            {baseCost>0 ? <div className="card card-pad" style={{background:'var(--green-t)',borderColor:'rgba(16,185,129,.3)'}}><div className="faint" style={{fontSize:12}}>Saved vs Claude Fable 5</div><div className="display route-text" style={{fontSize:22,marginTop:6}}>{Math.round((1-cost/baseCost)*100)}%</div></div>
            : <div className="card card-pad"><div className="faint" style={{fontSize:12}}>Request cost</div><div className="mono" style={{fontSize:18,marginTop:6,color:'var(--green)'}}>${(+cost).toFixed(6)}</div></div>}
          </div>

          {scores.length>0 && <SectionCard title="Candidate models — scored" sub="Higher total wins" pad={false}>
            <table className="table">
              <thead><tr><th>Model</th><th style={{textAlign:'right'}}>Quality</th><th style={{textAlign:'right'}}>Cost</th><th style={{textAlign:'right'}}>Latency</th><th style={{textAlign:'right'}}>Total</th></tr></thead>
              <tbody>
                {scores.map(function(s,i){
                  var mid = s.model_id||s.model||s.Model||'';
                  var q = s.quality_score!=null?s.quality_score:(s.quality||s.Quality||0);
                  var c = s.cost_score!=null?s.cost_score:(s.cost||s.Cost||0);
                  var l = s.latency_score!=null?s.latency_score:(s.latency||s.Latency||0);
                  var tot = s.total_score!=null?s.total_score:(s.total||s.Total||0);
                  return (
                    <tr key={mid} style={i===0?{background:'rgba(16,185,129,.08)'}:null}>
                      <td><span className="row" style={{gap:9}}><ModelCell id={mid}/>{i===0&&<Badge color="green" dot>Winner</Badge>}</span></td>
                      <td className="num" style={{textAlign:'right'}}>{q.toFixed(2)}</td>
                      <td className="num" style={{textAlign:'right'}}>{c.toFixed(2)}</td>
                      <td className="num" style={{textAlign:'right'}}>{l.toFixed(2)}</td>
                      <td className="num" style={{textAlign:'right',fontWeight:700,color:i===0?'var(--green)':'var(--tx-0)'}}>{tot.toFixed(2)}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </SectionCard>}

          {cascade.length>0 && <SectionCard title="Cascade trace" sub="Try the cheapest model first, escalate only if quality fails">
            <div className="grid" style={{gap:10}}>
              {cascade.map(function(step,i){
                var ok = step.accepted||step.Accepted||false;
                var mid = step.model||step.Model||'';
                var note = step.reason||step.Reason||step.note||'';
                return (
                  <div key={i} className="row" style={{gap:12,padding:'12px 14px',background:'var(--bg-inset)',borderRadius:10}}>
                    <span className="badge" style={{width:24,height:24,padding:0,justifyContent:'center',borderRadius:7,background:ok?'var(--green-t)':'var(--red-t)',color:ok?'var(--green)':'#ff8095'}}>{i+1}</span>
                    <ModelCell id={mid}/>
                    <span className="faint" style={{fontSize:12.5,marginLeft:'auto'}}>{note}</span>
                    <Icon name={ok?'checkcircle':'x'} size={16} style={{color:ok?'var(--green)':'#ff8095'}}/>
                  </div>
                );
              })}
            </div>
          </SectionCard>}
        </>;
      })()}

      {!data && !error && !loading && recent.length===0 && <EmptyState icon="route" title="Inspect routing decisions"
        desc="Send a request via the Playground first — every routing decision shows up here automatically. You can also paste a request ID from any X-Request-Id response header."/>}
    </div>
  );
}

/* =====================  PLAYGROUND  ===================== */
/* Renders markdown safely. Returns sanitized HTML string. */
function renderMarkdown(text){
  if(!text) return '';
  try {
    if(window.marked && window.DOMPurify){
      window.marked.setOptions({breaks:true, gfm:true});
      var html = window.marked.parse(text);
      return window.DOMPurify.sanitize(html);
    }
  } catch(e){}
  /* Fallback: just escape + preserve newlines */
  return text.replace(/[&<>]/g, function(c){return ({'&':'&amp;','<':'&lt;','>':'&gt;'})[c];}).replace(/\n/g,'<br/>');
}

function costTierFor2(modelId){
  var m = D2.modelById(modelId);
  // See costTierFor in portal-core.jsx: no pricing != zero pricing. A green
  // "Free" badge on a model we simply have no rate for is a claim about money.
  if(!m || m.unknown || m.hasPricing === false) return 'unknown';
  var maxRate = Math.max(m.in||0, m.out||0) / 1000;
  if(maxRate === 0)    return 'free';
  if(maxRate < 0.005)  return 'near-zero';
  if(maxRate < 0.025)  return 'standard';
  return 'premium';
}
var COST_TIER_COLOR2 = {free:'green', 'near-zero':'cyan', standard:'cyan', premium:'magenta', unknown:'gray'};

function PlaygroundPage(){
  var _model=useState('auto'); var model=_model[0], setModel=_model[1];
  var _strategy=useState('balanced'); var strategy=_strategy[0], setStrategy=_strategy[1];
  var _msgs=useState(function(){
    try { var saved=localStorage.getItem('nr_playground_msgs'); return saved?JSON.parse(saved):[]; }
    catch(e){ return []; }
  });
  var msgs=_msgs[0], setMsgs=_msgs[1];
  var _input=useState(''); var input=_input[0], setInput=_input[1];
  var _streaming=useState(false); var streaming=_streaming[0], setStreaming=_streaming[1];
  var _attach=useState(null); var attach=_attach[0], setAttach=_attach[1];
  var _estimate=useState(null); var estimate=_estimate[0], setEstimate=_estimate[1];
  var _estimating=useState(false); var estimating=_estimating[0], setEstimating=_estimating[1];
  var _activeModels=useState(D2.MODELS); var activeModels=_activeModels[0], setActiveModels=_activeModels[1];
  var _decision=useState(null); var decision=_decision[0], setDecision=_decision[1];
  var _selKey=useState(null); var selKey=_selKey[0], setSelKey=_selKey[1];
  var _engines=useState(function(){
    var init={};
    COMPRESSION_ENGINES.forEach(function(e){ init[e.id]=true; });
    return init;
  });
  var engines=_engines[0], setEngines=_engines[1];

  /* 3.3 Prompt template integration */
  var _templates=useState([]); var templates=_templates[0], setTemplates=_templates[1];
  var _showTemplates=useState(false); var showTemplates=_showTemplates[0], setShowTemplates=_showTemplates[1];
  var _activeTemplate=useState(null); var activeTemplate=_activeTemplate[0], setActiveTemplate=_activeTemplate[1]; // {id,slug,name}
  var _saveTplModal=useState(false); var saveTplModal=_saveTplModal[0], setSaveTplModal=_saveTplModal[1];
  var _tplName=useState(''); var tplName=_tplName[0], setTplName=_tplName[1];
  var _tplSlug=useState(''); var tplSlug=_tplSlug[0], setTplSlug=_tplSlug[1];
  var _tplSaving=useState(false); var tplSaving=_tplSaving[0], setTplSaving=_tplSaving[1];
  var t2=useToast();

  var loadTemplates=function(){
    NR_API.promptTemplates().then(function(res){ setTemplates((res&&res.templates)||[]); }).catch(function(){});
  };
  useEffect(function(){ loadTemplates(); },[]);

  var loadTemplateIntoCompose=function(tpl){
    NR_API.getPromptTemplate(tpl.id).then(function(res){
      if(!res || !res.template) return;
      var active = (res.versions||[]).filter(function(v){return v.version===res.template.active_version;})[0];
      if(!active || !active.messages || active.messages.length===0) return;
      // Load the first user-role message's raw ({{var}}-containing) text into
      // the compose box — the user fills in variables inline before sending,
      // same mental model as any templating tool's "fill in the blanks".
      var firstUser = active.messages.filter(function(m){return m.role==='user';})[0] || active.messages[0];
      setInput(firstUser.content);
      setActiveTemplate({id:tpl.id, slug:tpl.slug, name:tpl.name});
      setShowTemplates(false);
      t2({title:'Loaded "'+tpl.name+'"', msg: active.variables&&active.variables.length ? ('Fill in: '+active.variables.join(', ')) : 'No variables to fill in'});
    }).catch(function(){ t2({kind:'error',title:'Failed to load template'}); });
  };

  var saveAsTemplate=function(){
    if(!tplName.trim() || !tplSlug.trim() || !input.trim()) return;
    setTplSaving(true);
    NR_API.createPromptTemplate(tplName.trim(), tplSlug.trim(), [{role:'user', content: input}], model!=='auto'?model:'', strategy).then(function(res){
      setTplSaving(false);
      if(res && res.id){
        t2({title:'Template saved', msg: tplName.trim()});
        setSaveTplModal(false); setTplName(''); setTplSlug('');
        setActiveTemplate({id:res.id, slug:tplSlug.trim(), name:tplName.trim()});
        loadTemplates();
      } else {
        t2({kind:'error',title:'Failed to save template', msg:(res&&res.error)||''});
      }
    }).catch(function(){ setTplSaving(false); t2({kind:'error',title:'Failed to save template'}); });
  };

  var chatRef=useRef(null);
  var fileRef=useRef(null);
  var streamReaderRef=useRef(null);
  var t=useToast();

  /* Fetch health probe results and filter the model picker to only show active models.
     Falls back to the full reference list if /v1/health is unavailable. */
  useEffect(function(){
    NR_API.health().then(function(res){
      if(!res || !res.providers) return;
      var ok = D2.MODELS.filter(function(m){
        var s = res.providers[m.id];
        /* If the model isn't in the probe report at all, hide it (means it's not registered).
           If present and 'ok', show it. Anything else (error/unknown) → hide. */
        return s === 'ok';
      });
      if(ok.length>0) setActiveModels(ok);
    }).catch(function(){});
  },[]);

  /* Persist chat history (skip image attachments — too large) */
  useEffect(function(){
    try {
      var safe = msgs.map(function(m){
        if(m.attachment && m.attachment.kind==='image') {
          return Object.assign({}, m, {attachment:{kind:'image',name:m.attachment.name,size:m.attachment.size}});
        }
        return m;
      });
      localStorage.setItem('nr_playground_msgs', JSON.stringify(safe.slice(-50)));
    } catch(e){}
  }, [msgs]);

  var stopStream = function(){
    if(streamReaderRef.current) {
      try { streamReaderRef.current.cancel(); } catch(e){}
      streamReaderRef.current = null;
    }
    setStreaming(false);
  };

  var clearChat = function(){
    if(streaming) stopStream();
    setMsgs([]);
    setDecision(null);
    setEstimate(null);
    setSelKey(null);
    setActiveTemplate(null);
    try { localStorage.removeItem('nr_playground_msgs'); } catch(e){}
  };

  var runEstimate = function(){
    if(!input.trim() || estimating) return;
    setEstimate(null);   /* clear previous result immediately so user sees fresh evaluation */
    setEstimating(true);
    NR_API.estimate(input.trim(), model, strategy).then(function(data){
      setEstimating(false);
      if(data && data._ok && !data.error){
        setEstimate({
          model: data.selected_model || (model!=='auto'?model:''),
          inputTokens: data.estimated_input_tokens || 0,
          outputTokens: data.estimated_output_tokens || 0,
          cost: data.estimated_cost || 0,
          baseline: data.counterfactual_cost || data.counterfactual_gpt4o_cost || 0,
          savings: data.estimated_savings || 0,
          taskType: data.task_type || 'general',
          /* Token-saving features (per-org) */
          compressionEnabled: !!data.compression_enabled,
          rawInputTokens: data.raw_input_tokens || 0,
          compressedInputTokens: data.compressed_input_tokens || 0,
          compressionSavings: data.compression_savings || 0,
          cacheEnabled: !!data.cache_enabled,
          cacheHit: !!data.cache_hit,
          cacheSimilarity: data.cache_similarity || 0,
          cacheSavings: data.cache_savings || 0,
          totalSavings: data.total_savings || 0,
        });
      } else {
        var msg = (data && data.error && data.error.message) || (data && data.error) || 'Server returned status '+(data&&data._status);
        t({kind:'error', title:'Estimate failed', msg: String(msg)});
      }
    }).catch(function(e){ setEstimating(false); t({kind:'error', title:'Estimate failed', msg:e&&e.message||'network error'}); });
  };

  var scrollBottom=function(){ if(chatRef.current) chatRef.current.scrollTop = chatRef.current.scrollHeight; };

  var handleFile = function(e){
    var file = e.target.files && e.target.files[0];
    if(!file) return;
    /* 10MB cap */
    if(file.size > 10*1024*1024) { t({kind:'error',title:'File too large',msg:'Max 10MB allowed.'}); return; }
    var reader = new FileReader();
    var isImage = file.type.indexOf('image/')===0;
    var isText = /\.(txt|md|csv|json|js|jsx|ts|tsx|py|go|java|rb|sh|yaml|yml|xml|html|css|sql|log)$/i.test(file.name) || file.type.indexOf('text/')===0 || file.type==='application/json';

    reader.onload = function(ev){
      if(isImage) {
        setAttach({kind:'image', name:file.name, size:file.size, dataUrl:ev.target.result, mime:file.type});
      } else if(isText) {
        setAttach({kind:'text', name:file.name, size:file.size, content:ev.target.result});
      } else {
        t({kind:'error',title:'Unsupported file',msg:'Attach images or text files (.txt, .md, .csv, .json, code).'});
      }
    };
    reader.onerror = function(){ t({kind:'error',title:'Could not read file'}); };
    if(isImage) reader.readAsDataURL(file);
    else if(isText) reader.readAsText(file);
    else { t({kind:'error',title:'Unsupported file'}); return; }
    /* Reset input so same file can be picked again */
    e.target.value = '';
  };

  var send = async function(){
    if((!input.trim() && !attach) || streaming) return;

    /* Build user message content (text or multimodal) */
    var displayText = input.trim();
    var apiContent;
    if(attach && attach.kind==='image') {
      /* Multimodal: text + image */
      apiContent = [
        {type:'text', text: displayText || 'Please describe this image.'},
        {type:'image_url', image_url:{url: attach.dataUrl}}
      ];
    } else if(attach && attach.kind==='text') {
      /* Inline text file content with the prompt */
      var label = 'Attached file: '+attach.name+'\n\n```\n'+attach.content+'\n```\n\n';
      apiContent = label + (displayText || 'Please analyze the attached file.');
    } else {
      apiContent = displayText;
    }

    var userMsg = {role:'user', content:apiContent, display:displayText, attachment:attach};
    var newMsgs = msgs.concat([userMsg]);
    setMsgs(newMsgs);
    setInput('');
    setAttach(null);
    setEstimate(null);
    setDecision(null); // reset the bottom routing cards so they show "—" (not stale) until this run's metadata lands
    setStreaming(true);
    setTimeout(scrollBottom, 50);

    /* Add placeholder assistant message */
    setMsgs(newMsgs.concat([{role:'assistant', content:'', meta:null}]));

    try {
      /* Build messages array for API — skip error/placeholder messages that
         may have been persisted to localStorage from an interrupted stream,
         and ensure the array ends with a user turn. */
      var apiMessages = newMsgs
        .filter(function(m){ return m.content !== '' && !m.error; })
        .map(function(m){ return {role:m.role, content:m.content}; });
      while (apiMessages.length > 0 && apiMessages[apiMessages.length-1].role === 'assistant') {
        apiMessages.pop();
      }
      var modelParam = model==='auto' ? 'auto' : model;
      var strategyParam = strategy;
      var selectedEngines = COMPRESSION_ENGINES.map(function(e){return e.id;}).filter(function(n){return engines[n];});

      var response = await NR_API.chatStream(apiMessages, modelParam, strategyParam, selectedEngines, activeTemplate?activeTemplate.slug:null);

      if(!response.ok) {
        var errBody = await response.json().catch(function(){return {error:{message:'Request failed ('+response.status+')'}};});
        setMsgs(function(prev){
          var copy=prev.slice();
          copy[copy.length-1] = {role:'assistant', content:'Error: '+(errBody.error?errBody.error.message:'Request failed'), error:true};
          return copy;
        });
        setStreaming(false);
        return;
      }

      /* Read response headers for routing metadata + token-saving features.
         X-Compression-Tokens ("raw->comp") + X-Compression-Ratio are set only when
         compression actually saved input tokens; X-Cache is HIT|SEMANTIC-HIT|MISS. */
      var meta = {
        model: response.headers.get('X-Routed-Model') || model,
        cost: response.headers.get('X-Request-Cost'),
        requestId: response.headers.get('X-Request-Id'),
        strategy: response.headers.get('X-Routing-Strategy') || strategy,
        compressionTokens: response.headers.get('X-Compression-Tokens'),
        compressionRatio: response.headers.get('X-Compression-Ratio'),
        compressionApplied: response.headers.get('X-Compression-Engines-Applied'),
        cache: response.headers.get('X-Cache'),
        costTier: response.headers.get('X-Cost-Tier'),
      };

      /* Stream SSE response */
      var reader = response.body.getReader();
      streamReaderRef.current = reader;
      var decoder = new TextDecoder();
      var content = '';
      var buf = '';

      while(true) {
        var chunk = await reader.read();
        if(chunk.done) break;
        buf += decoder.decode(chunk.value, {stream:true});
        var lines = buf.split('\n');
        buf = lines.pop() || '';
        for(var li=0; li<lines.length; li++) {
          var line = lines[li];
          if(!line.startsWith('data: ')) continue;
          var payload = line.slice(6).trim();
          if(payload === '[DONE]') continue;
          try {
            var json = JSON.parse(payload);
            var delta = json.choices && json.choices[0] && json.choices[0].delta && json.choices[0].delta.content;
            if(delta) {
              content += delta;
              /* Update last message with streamed content */
              (function(c, m){
                setMsgs(function(prev){
                  var copy=prev.slice();
                  copy[copy.length-1] = {role:'assistant', content:c, meta:m};
                  return copy;
                });
              })(content, meta);
              scrollBottom();
            }
            /* Check for model info in the chunk */
            if(json.model && !meta.model) meta.model = json.model;
            /* Capture token usage from the final chunk if the provider sends it. */
            if(json.usage) meta.usage = json.usage;
          } catch(pe){}
        }
      }

      /* Final update */
      setMsgs(function(prev){
        var copy=prev.slice();
        copy[copy.length-1] = {role:'assistant', content: content||'(empty response)', meta:meta};
        return copy;
      });

      /* Routing-decision cards (bottom of Playground) — populate from headers
         immediately, then enrich with task type, confidence, and the actual
         request cost from the explain endpoint (best-effort; ownership-checked
         server-side). The cost is filled in server-side once the stream finishes,
         so retry once if it's not there yet. */
      var dec = {model: meta.model, cost: meta.cost, strategy: meta.strategy, requestId: meta.requestId, taskType: '', confidence: 0};
      setDecision(dec);

      /* Estimate tokens for the actual-run cost card on the RHS. Prefer real
         provider usage from the stream; fall back to a rough char-based estimate. */
      var inTok = 0, outTok = 0;
      if(meta.usage){
        inTok  = meta.usage.prompt_tokens || meta.usage.input_tokens || 0;
        outTok = meta.usage.completion_tokens || meta.usage.output_tokens || 0;
      }
      if(!inTok)  inTok  = Math.max(1, Math.round(apiMessages.reduce(function(a,m){return a+(m.content?String(m.content).length:0);},0)/4));
      if(!outTok) outTok = Math.max(1, Math.round((content||'').length/4));

      if(meta.requestId){
        var applyExplain = function(res, attempt){
          if(!res){ return; }
          var actualModel = res.selected_model || res.SelectedModel || dec.model;
          var actualCost  = (res.request_cost != null ? res.request_cost : (res.RequestCost != null ? res.RequestCost : null));
          var taskType    = res.task_type || res.TaskType || '';
          var conf        = res.task_confidence || res.confidence || res.TaskConfidence || 0;
          var newDec = {
            model: actualModel, requestId: meta.requestId,
            strategy: res.strategy || res.Strategy || dec.strategy,
            taskType: taskType, confidence: conf,
            cost: (actualCost != null ? actualCost : dec.cost),
          };
          setDecision(newDec);
          /* Populate the RHS with the ACTUAL run (cost + routed model), replacing
             the empty "type a prompt to estimate" state after every send. */
          if(actualCost != null){
            var baseline = (typeof gpt4oBaseline==='function') ? gpt4oBaseline(inTok, outTok) : 0;
            /* Compression: parse "raw->comp" from the response header (present only
               when compression actually saved tokens), value the saved tokens at the
               routed model's input rate. */
            var compRaw=0, compComp=0, compSaved=0, compEnabled=false;
            if(meta.compressionTokens){
              var cp = String(meta.compressionTokens).split('->');
              if(cp.length===2){ compRaw=parseInt(cp[0],10)||0; compComp=parseInt(cp[1],10)||0; compEnabled = compRaw>0; }
            }
            var aInfo = D2.modelById(actualModel||'');
            if(compEnabled && compRaw>compComp && aInfo && aInfo.in){ compSaved = (compRaw-compComp)*aInfo.in/1e6; }
            /* Cache: streaming requests are never cache-served, so this is normally a
               miss — only surface a cache row on an actual hit. */
            var cacheHit = (meta.cache==='HIT' || meta.cache==='SEMANTIC-HIT');
            var est = {
              model: actualModel, taskType: taskType,
              inputTokens: inTok, outputTokens: outTok,
              cost: actualCost, baseline: baseline,
              savings: Math.max(0, baseline - actualCost),
              compressionEnabled: compEnabled,
              rawInputTokens: compRaw, compressedInputTokens: compComp, compressionSavings: compSaved,
              compressionTokensRaw: meta.compressionTokens, compressionRatio: meta.compressionRatio, compressionApplied: meta.compressionApplied,
              cacheEnabled: cacheHit, cacheHit: cacheHit, cacheSavings: cacheHit ? actualCost : 0, cacheSimilarity: 0,
              totalSavings: Math.max(0, baseline - actualCost) + compSaved + (cacheHit ? actualCost : 0),
              actual: true,
              costTier: meta.costTier || costTierFor2(actualModel),
            };
            setEstimate(est);
            /* Persist the built panel + decision onto the assistant message so the
               cost/routing data survives reload & tab-switch and can be re-shown by
               clicking the message later (until Clear). */
            (function(panelObj, decObj){
              setMsgs(function(prev){
                var copy=prev.slice();
                for(var k=copy.length-1;k>=0;k--){
                  if(copy[k].role==='assistant'){
                    copy[k]=Object.assign({}, copy[k], {meta:Object.assign({}, copy[k].meta, {panel:panelObj, dec:decObj, taskType:taskType, confidence:conf})});
                    break;
                  }
                }
                return copy;
              });
            })(est, newDec);
            setSelKey(meta.requestId||null);
          } else if(attempt < 1){
            /* cost not written yet — retry once shortly */
            setTimeout(function(){ NR_API.routingExplain(meta.requestId).then(function(r2){applyExplain(r2, 1);}).catch(function(){}); }, 600);
          }
        };
        NR_API.routingExplain(meta.requestId).then(function(res){ applyExplain(res, 0); }).catch(function(){});
      }

    } catch(e) {
      setMsgs(function(prev){
        var copy=prev.slice();
        copy[copy.length-1] = {role:'assistant', content:'Error: '+e.message, error:true};
        return copy;
      });
    }
    streamReaderRef.current = null;
    setStreaming(false);
    setTimeout(scrollBottom, 50);
  };

  var handleFeedback = function(requestId, rating){
    if(!requestId) return;
    NR_API.feedback(requestId, rating);
  };

  /* Stable per-message key for selection highlighting. */
  var msgKey = function(m, i){ return (m && m.meta && m.meta.requestId) ? m.meta.requestId : ('i'+i); };

  /* Rebuild the RHS cost panel + bottom decision cards from a message's stored
     metadata. New runs stash the fully-built panel (meta.panel/meta.dec); for
     older messages we reconstruct from the raw header meta so history still works. */
  var panelFromMeta = function(m){
    if(!m || !m.meta) return null;
    if(m.meta.panel){ return {panel:m.meta.panel, dec:m.meta.dec||null}; }
    var meta = m.meta;
    var model = meta.model||'';
    var cost = (meta.cost!=null && meta.cost!=='') ? parseFloat(meta.cost) : null;
    if(!model && cost==null) return null;
    var outTok = meta.usage ? (meta.usage.completion_tokens||meta.usage.output_tokens||0) : 0;
    if(!outTok) outTok = Math.max(1, Math.round(((typeof m.content==='string'?m.content:'')||'').length/4));
    var inTok = meta.usage ? (meta.usage.prompt_tokens||meta.usage.input_tokens||0) : 0;
    if(!inTok) inTok = 1;
    var baseline = (typeof gpt4oBaseline==='function') ? gpt4oBaseline(inTok, outTok) : 0;
    var compRaw=0, compComp=0, compEnabled=false, compSaved=0;
    if(meta.compressionTokens){ var cp=String(meta.compressionTokens).split('->'); if(cp.length===2){ compRaw=parseInt(cp[0],10)||0; compComp=parseInt(cp[1],10)||0; compEnabled=compRaw>0; } }
    var aInfo=D2.modelById(model||''); if(compEnabled && compRaw>compComp && aInfo && aInfo.in){ compSaved=(compRaw-compComp)*aInfo.in/1e6; }
    var cacheHit=(meta.cache==='HIT'||meta.cache==='SEMANTIC-HIT');
    var c = cost!=null ? cost : 0;
    var panel = {
      model:model, taskType:meta.taskType||'', inputTokens:inTok, outputTokens:outTok,
      cost:c, baseline:baseline, savings:Math.max(0, baseline-c),
      compressionEnabled:compEnabled, rawInputTokens:compRaw, compressedInputTokens:compComp, compressionSavings:compSaved,
      compressionTokensRaw:meta.compressionTokens, compressionRatio:meta.compressionRatio, compressionApplied:meta.compressionApplied,
      cacheEnabled:cacheHit, cacheHit:cacheHit, cacheSavings:cacheHit?c:0, cacheSimilarity:0,
      totalSavings:Math.max(0, baseline-c)+compSaved+(cacheHit?c:0), actual:true,
      costTier: meta.costTier || costTierFor2(model),
    };
    var dec = {model:model, cost:(cost!=null?cost:meta.cost), strategy:meta.strategy||'', requestId:meta.requestId, taskType:meta.taskType||'', confidence:meta.confidence||0};
    return {panel:panel, dec:dec};
  };

  /* Click a past prompt/response to re-show its routing + cost metadata. */
  var selectMessage = function(m, i){
    if(!m || m.role!=='assistant' || !m.meta) return;
    var p = panelFromMeta(m);
    if(!p) return;
    setEstimate(p.panel); setDecision(p.dec); setSelKey(msgKey(m, i));
  };

  /* On mount (e.g. after reload / re-login), restore the panels for the most
     recent completed response so cost/routing data isn't blank next to the
     persisted conversation. Runs once. */
  useEffect(function(){
    if(!msgs || !msgs.length) return;
    for(var k=msgs.length-1; k>=0; k--){
      if(msgs[k].role==='assistant' && msgs[k].meta && (msgs[k].meta.panel || msgs[k].meta.model)){
        selectMessage(msgs[k], k); break;
      }
    }
  }, []); // eslint-disable-line

  /* RHS estimate card panel — empty state, loading state, or filled result */
  var estimatePanel = (function(){
    if(estimating){
      return (
        <div className="card card-pad" style={{textAlign:'center',padding:'40px 20px'}}>
          <div style={{display:'inline-block',width:32,height:32,border:'3px solid var(--line-2)',borderTopColor:'var(--blue)',borderRadius:'50%',animation:'spin 0.8s linear infinite'}}/>
          <div className="faint" style={{marginTop:14,fontSize:13}}>Estimating routing cost…</div>
        </div>
      );
    }
    if(!estimate){
      return (
        <div className="card card-pad" style={{textAlign:'center',padding:'32px 20px',background:'var(--bg-inset)'}}>
          <Icon name="estimator" size={26} style={{color:'var(--tx-2)',marginBottom:10}}/>
          <h3 style={{margin:'0 0 6px',fontSize:15}}>Cost estimate</h3>
          <p className="faint" style={{fontSize:12.5,lineHeight:1.55,margin:0}}>Type a prompt and click <b>Estimate</b> to preview the routed model, token counts, and savings before sending. Or click any past response to re-show its routing & cost.</p>
        </div>
      );
    }
    var mInfo = D2.modelById(estimate.model||'');
    var prov = D2.providerById(mInfo.provider);
    var savings = (estimate.baseline||0) - (estimate.cost||0);
    var savingsPct = estimate.baseline>0 ? Math.round((1-estimate.cost/estimate.baseline)*100) : 0;
    return (
      <div className="grid" style={{gap:12}}>
        {/* Header card: routed model */}
        <div className="card card-pad" style={{borderLeft:'3px solid '+prov.color}}>
          <div className="row" style={{gap:10,marginBottom:10}}>
            <Icon name="estimator" size={15} style={{color:'var(--green)'}}/>
            <b style={{fontSize:13,letterSpacing:'.02em',textTransform:'uppercase'}} className="faint">{estimate.actual ? 'Actual run' : 'Cost estimate'}</b>
            <Badge color={estimate.actual?'blue':'green'} dot style={{marginLeft:'auto',fontSize:10}}>{estimate.actual ? 'billed' : 'fresh'}</Badge>
          </div>
          <div className="row" style={{gap:11}}>
            <ProviderLogo id={mInfo.provider} size={36}/>
            <div className="stack" style={{flex:1,minWidth:0}}>
              <div style={{fontWeight:700,fontSize:14,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{mInfo.name}</div>
              <div className="row" style={{gap:6,marginTop:2,flexWrap:'wrap'}}>
                <span className="faint" style={{fontSize:11}}>{prov.name}</span>
                <Badge color="blue" style={{fontSize:10}}>{estimate.taskType}</Badge>
                {estimate.costTier && <Badge color={COST_TIER_COLOR2[estimate.costTier]||'gray'} style={{fontSize:10,textTransform:'capitalize'}}>{estimate.costTier}</Badge>}
              </div>
            </div>
          </div>
        </div>

        {/* Tokens card */}
        <div className="card card-pad">
          <div className="faint" style={{fontSize:11.5,letterSpacing:'.04em',textTransform:'uppercase',marginBottom:8}}>Estimated tokens</div>
          <div className="grid" style={{gridTemplateColumns:'1fr 1fr',gap:10}}>
            <div style={{padding:'10px 12px',background:'var(--bg-inset)',borderRadius:8}}>
              <div className="faint" style={{fontSize:11}}>Input</div>
              <div className="mono" style={{fontSize:16,marginTop:2,fontWeight:700}}>{fmtNum(estimate.inputTokens)}</div>
              <div className="mono faint" style={{fontSize:10.5,marginTop:2}}>${(estimate.inputTokens*mInfo.in/1e6).toFixed(5)}</div>
            </div>
            <div style={{padding:'10px 12px',background:'var(--bg-inset)',borderRadius:8}}>
              <div className="faint" style={{fontSize:11}}>Output</div>
              <div className="mono" style={{fontSize:16,marginTop:2,fontWeight:700}}>{fmtNum(estimate.outputTokens)}</div>
              <div className="mono faint" style={{fontSize:10.5,marginTop:2}}>${(estimate.outputTokens*mInfo.out/1e6).toFixed(5)}</div>
            </div>
          </div>
        </div>

        {/* Cost card */}
        <div className="card card-pad" style={{background:'var(--bg-inset)'}}>
          <div className="spread" style={{marginBottom:10}}>
            <span className="faint" style={{fontSize:11.5,letterSpacing:'.04em',textTransform:'uppercase'}}>Total cost</span>
            <span className="mono" style={{fontSize:20,fontWeight:700,color:'var(--green)'}}>${estimate.cost.toFixed(6)}</span>
          </div>
          {estimate.baseline>0 && <div className="spread faint" style={{fontSize:12,paddingTop:8,borderTop:'1px solid var(--line)'}}>
            <span>vs Claude Fable 5 baseline</span>
            <span className="mono">${estimate.baseline.toFixed(6)}</span>
          </div>}
        </div>

        {/* Semantic cache: hit banner, or enabled-but-miss status (only shown when the org has caching on) */}
        {estimate.cacheEnabled && (estimate.cacheHit ? (
          <div className="card card-pad" style={{background:'var(--blue-t,rgba(59,130,246,.12))',borderColor:'rgba(59,130,246,.35)'}}>
            <div className="row" style={{gap:8,marginBottom:6}}>
              <Icon name="layers" size={14} style={{color:'var(--blue)'}}/>
              <b style={{fontSize:12.5}}>Semantic cache hit</b>
              <Badge color="blue" style={{marginLeft:'auto',fontSize:10}}>{Math.round((estimate.cacheSimilarity||0)*100)}% match</Badge>
            </div>
            <p className="faint" style={{fontSize:11.5,lineHeight:1.5,margin:0}}>A near-duplicate prompt is cached — this request would be served without a model call, avoiding <b className="mono" style={{color:'var(--blue)'}}>${(estimate.cacheSavings||0).toFixed(6)}</b>.</p>
          </div>
        ) : (
          <div className="card card-pad">
            <div className="spread">
              <span className="faint" style={{fontSize:11.5,letterSpacing:'.04em',textTransform:'uppercase'}}>Semantic cache</span>
              <Badge color="blue" style={{fontSize:10}}>enabled</Badge>
            </div>
            <p className="faint" style={{fontSize:11,lineHeight:1.5,margin:'6px 0 0'}}>No near-duplicate cached yet (miss). A repeat of this prompt would be served for $0.</p>
          </div>
        ))}

        {/* Context compression: token reduction, or enabled-but-nothing-to-shrink (only when org has it on) */}
        {estimate.compressionEnabled && (function(){
          var saved = (estimate.rawInputTokens||0) - (estimate.compressedInputTokens||0);
          return (
            <div className="card card-pad">
              <div className="spread" style={{marginBottom:6}}>
                <span className="faint" style={{fontSize:11.5,letterSpacing:'.04em',textTransform:'uppercase'}}>Context compression</span>
                {saved>0
                  ? <span className="mono" style={{fontSize:12,color:'var(--cyan,var(--blue))'}}>{fmtNum(estimate.rawInputTokens)} → {fmtNum(estimate.compressedInputTokens)} tok</span>
                  : <Badge color="blue" style={{fontSize:10}}>enabled</Badge>}
              </div>
              {saved>0
                ? <div className="spread faint" style={{fontSize:12}}><span>saved on input tokens</span><span className="mono" style={{color:'var(--green)'}}>${(estimate.compressionSavings||0).toFixed(6)}</span></div>
                : <div className="faint" style={{fontSize:11,lineHeight:1.5}}>Nothing to compress in this prompt (no extra whitespace, JSON, or repeated lines). Kicks in on large/repetitive inputs.</div>}
            </div>
          );
        })()}

        {/* Per-engine compression result (actual run) — driven by the response headers. */}
        {estimate.actual && (function(){
          var ctok = estimate.compressionTokensRaw;
          // X-Compression-Ratio is a 0..1 fraction; render it as a percentage.
          var cratio = estimate.compressionRatio ? Math.round(parseFloat(estimate.compressionRatio) * 100) : null;
          var capplied = estimate.compressionApplied;
          return (
            <div className="card card-pad">
              <div className="spread" style={{marginBottom:6}}>
                <span className="faint" style={{fontSize:11.5,letterSpacing:'.04em',textTransform:'uppercase'}}>Compression</span>
              </div>
              {ctok
                ? <div className="faint" style={{fontSize:11.5,lineHeight:1.5}}>Compressed <span className="mono" style={{color:'var(--tx-0)'}}>{ctok}</span> tokens{cratio ? <span> (<span className="mono" style={{color:'var(--green)'}}>{cratio}%</span>)</span> : null}{capplied ? <span> · engines: <span className="mono">{capplied}</span></span> : null}</div>
                : <div className="faint" style={{fontSize:11.5,lineHeight:1.5}}>no compression applied</div>}
            </div>
          );
        })()}

        {/* Savings card: total across routing + compression + cache */}
        {(function(){
          var total = (estimate.totalSavings!=null && estimate.totalSavings>0) ? estimate.totalSavings : savings;
          if(total<=0) return null;
          var routing = estimate.savings||0;
          var comp = estimate.compressionSavings||0;
          var cache = estimate.cacheSavings||0;
          /* Show the per-lever breakdown whenever either feature is active for the org. */
          var hasBreakdown = estimate.compressionEnabled || estimate.cacheEnabled || comp>0 || cache>0;
          // % saved = savings as a share of the would-be bill (what you saved + what you paid).
          var pct = (estimate.cost!=null && (total+estimate.cost)>0) ? Math.round(total/(total+estimate.cost)*100) : (savingsPct||0);
          return (
            <div className="card" style={{padding:'12px 16px',background:'var(--green-t)',borderColor:'rgba(16,185,129,.3)',textAlign:'center'}}>
              <div className="faint" style={{fontSize:11,letterSpacing:'.04em',textTransform:'uppercase',marginBottom:3}}>You save{hasBreakdown?' (total)':''}</div>
              <div className="row" style={{gap:8,justifyContent:'center',alignItems:'center'}}>
                <div className="display route-text" style={{fontSize:20,lineHeight:1}}>${total.toFixed(5)}</div>
                {pct>0 && <Badge color="green">{pct}% saved</Badge>}
              </div>
              {hasBreakdown && <div className="grid" style={{gap:2,marginTop:6,textAlign:'left'}}>
                <div className="spread faint" style={{fontSize:11}}><span>Smart routing</span><span className="mono">${routing.toFixed(6)}</span></div>
                {estimate.compressionEnabled && <div className="spread faint" style={{fontSize:11}}><span>Compression</span><span className="mono">${comp.toFixed(6)}</span></div>}
                {estimate.cacheEnabled && <div className="spread faint" style={{fontSize:11}}><span>Cache {estimate.cacheHit?'hit':'(miss)'}</span><span className="mono">${cache.toFixed(6)}</span></div>}
              </div>}
            </div>
          );
        })()}

        {!estimate.actual && <button className="btn btn-grad btn-block" disabled={streaming} onClick={function(){send();}}>
          <Icon name="send" size={14}/>Send through this model
        </button>}
      </div>
    );
  })();

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Chat against the gateway. Every response streams from real models with routing metadata.">
        <select className="select" style={{width:180}} value={model} onChange={function(e){setModel(e.target.value); if(estimate) setEstimate(null);}}>
          <option value="auto">Auto (recommended)</option>
          {activeModels.map(function(m){return <option key={m.id} value={m.id}>{m.name}</option>;})}
        </select>
        <select className="select" style={{width:150}} value={strategy} onChange={function(e){setStrategy(e.target.value); if(estimate) setEstimate(null);}}>
          {D2.STRATEGIES.map(function(s){return <option key={s} value={s.toLowerCase().replace(/ /g,'-')}>{s}</option>;})}
        </select>
        <div className="row" style={{gap:10,flexWrap:'wrap',alignItems:'center'}}>
          <span className="faint" style={{fontSize:11.5,letterSpacing:'.03em',textTransform:'uppercase'}}>Compression</span>
          {COMPRESSION_ENGINES.map(function(e){
            return (
              <label key={e.id} className="row" style={{gap:5,alignItems:'center',cursor:'pointer',fontSize:12.5}}>
                <input type="checkbox" checked={!!engines[e.id]} onChange={function(){var next=Object.assign({},engines); next[e.id]=!engines[e.id]; setEngines(next);}} style={{cursor:'pointer'}}/>
                <span style={{color:'var(--tx-0)'}}>{e.label}</span>
              </label>
            );
          })}
        </div>
        <div className="row" style={{gap:8,position:'relative'}}>
          <button className="btn btn-soft btn-sm" onClick={function(){setShowTemplates(!showTemplates);}}>
            <Icon name={showTemplates?'chevdown':'chevron'} size={12}/>Templates{activeTemplate?(' · '+activeTemplate.name):''}
          </button>
          {showTemplates && <div className="card" style={{position:'absolute',top:'110%',right:0,zIndex:50,width:280,padding:12}}>
            {templates.length===0 ? <div className="faint" style={{fontSize:12.5,padding:'6px 2px'}}>No saved templates yet.</div> :
              templates.map(function(tpl){
                return <div key={tpl.id} className="spread" style={{padding:'6px 2px',cursor:'pointer'}} onClick={function(){loadTemplateIntoCompose(tpl);}}>
                  <div><div style={{fontWeight:600,fontSize:13}}>{tpl.name}</div><div className="faint mono" style={{fontSize:11}}>{tpl.slug}</div></div>
                  <Badge color="blue" style={{fontSize:10}}>v{tpl.active_version}</Badge>
                </div>;
              })
            }
          </div>}
        </div>
        <button className="btn btn-ghost btn-sm" onClick={clearChat}>Clear</button>
      </PageHead>
      <div className="grid playground-2col" style={{gridTemplateColumns:'minmax(0, 1fr) 340px',gap:14,alignItems:'stretch',height:'calc(100vh - 224px)',minHeight:420}}>
      <div style={{display:'flex',flexDirection:'column',gap:14,minWidth:0,minHeight:0}}>
      <div className="card" style={{display:'flex',flexDirection:'column',flex:'1 1 auto',minHeight:0}}>
        <div ref={chatRef} style={{flex:1,overflowY:'auto',padding:'22px'}}>
          <div className="grid" style={{gap:18,maxWidth:760,margin:'0 auto'}}>
            {msgs.length===0 && <div style={{display:'flex',alignItems:'center',justifyContent:'center',height:'100%',minHeight:300}}>
              <div style={{textAlign:'center',maxWidth:400}}>
                <Icon name="playground" size={32} style={{color:'var(--tx-2)',marginBottom:12}}/>
                <h3 style={{margin:'0 0 8px'}}>NeuroRoute Playground</h3>
                <p className="faint" style={{lineHeight:1.6}}>Send a message to chat with AI models through the NeuroRoute gateway. Responses stream in real-time from actual model APIs.</p>
              </div>
            </div>}

            {msgs.map(function(m,i){
              var mInfo = m.meta && m.meta.model ? D2.modelById(m.meta.model) : null;
              var isUser = m.role==='user';
              var displayContent = isUser ? (m.display||(typeof m.content==='string'?m.content:'')) : (m.content||'');
              /* renderMarkdown() sanitizes via DOMPurify before returning HTML — safe to inject */
              var sanitizedHtml = !isUser && displayContent ? renderMarkdown(displayContent) : (streaming && !isUser && i===msgs.length-1 ? '<span style="opacity:.6">Thinking…</span>' : '');
              var selectable = !isUser && m.meta && (m.meta.panel || m.meta.model);
              var isSelected = selectable && msgKey(m, i) === selKey;
              return (
                <div key={i} style={{alignSelf:isUser?'flex-end':'flex-start',maxWidth:'82%'}}>
                  {isUser && m.attachment && m.attachment.kind==='image' && <div style={{marginBottom:8,textAlign:'right'}}>
                    <img src={m.attachment.dataUrl} alt={m.attachment.name} style={{maxWidth:260,maxHeight:200,borderRadius:10,border:'1px solid var(--line)'}}/>
                  </div>}
                  {isUser && m.attachment && m.attachment.kind==='text' && <div style={{marginBottom:8,textAlign:'right'}}>
                    <span className="chip"><Icon name="layers" size={13}/>{m.attachment.name}</span>
                  </div>}
                  {isUser ? (
                    <div style={{padding:'13px 16px',borderRadius:14,fontSize:14,lineHeight:1.6,whiteSpace:'pre-wrap',
                      background:'var(--grad-brand)', color:'#fff',
                      borderBottomRightRadius:4, borderBottomLeftRadius:14}}>
                      {displayContent || (m.attachment ? <span style={{opacity:.85}}>({m.attachment.kind==='image'?'image':'file'} attached)</span> : '')}
                    </div>
                  ) : (
                    <div className="md-content" title={selectable ? 'Click to view this response’s routing & cost' : undefined}
                      onClick={selectable ? function(){ selectMessage(m, i); } : undefined}
                      style={{padding:'13px 16px',borderRadius:14,fontSize:14,lineHeight:1.6,
                      background: m.error?'var(--red-t)':'var(--bg-3)', color:'var(--tx-0)',
                      border:'1px solid '+(isSelected?'var(--blue)':'var(--line)'),
                      boxShadow: isSelected?'0 0 0 1px var(--blue)':'none',
                      cursor: selectable?'pointer':'default',
                      borderBottomLeftRadius:4, borderBottomRightRadius:14}}
                      dangerouslySetInnerHTML={{__html: sanitizedHtml}}/>
                  )}
                  {m.meta && mInfo && !isUser && <div className="row" style={{gap:8,marginTop:8,flexWrap:'wrap'}}>
                    <span className="chip"><ProviderLogo id={mInfo.provider} size={16}/>{mInfo.name}</span>
                    {m.meta.cost && <span className="chip mono">${parseFloat(m.meta.cost).toFixed(4)}</span>}
                    {m.meta.requestId && <span className="chip mono" style={{cursor:'pointer'}} onClick={function(){go('explorer?req='+encodeURIComponent(m.meta.requestId));}} title="Trace this request in the Routing Explorer">{m.meta.requestId.slice(0,12)}…</span>}
                    <button className="iconbtn" style={{width:28,height:28}} onClick={function(){handleFeedback(m.meta.requestId,'thumbs_up');}} title="Good response"><Icon name="thumbup" size={13}/></button>
                    <button className="iconbtn" style={{width:28,height:28}} onClick={function(){handleFeedback(m.meta.requestId,'thumbs_down');}} title="Poor response"><Icon name="thumbdown" size={13}/></button>
                  </div>}
                </div>
              );
            })}
          </div>
        </div>
        <div style={{borderTop:'1px solid var(--line)',padding:16}}>
          <div style={{maxWidth:760,margin:'0 auto'}}>
            {attach && <div className="row" style={{gap:8,marginBottom:10,padding:'8px 10px',background:'var(--bg-inset)',borderRadius:10}}>
              {attach.kind==='image' ? <img src={attach.dataUrl} style={{width:36,height:36,objectFit:'cover',borderRadius:6}}/> : <Icon name="layers" size={18} style={{color:'var(--tx-2)'}}/>}
              <div className="stack" style={{flex:1,minWidth:0}}>
                <span style={{fontSize:13,fontWeight:600,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{attach.name}</span>
                <span className="faint" style={{fontSize:11}}>{attach.kind==='image' ? 'Image' : 'Text file'} · {(attach.size/1024).toFixed(1)} KB</span>
              </div>
              <button className="iconbtn" onClick={function(){setAttach(null);}} title="Remove attachment"><Icon name="x" size={14}/></button>
            </div>}
            <div className="row" style={{gap:10}}>
              <input ref={fileRef} type="file" style={{display:'none'}} onChange={handleFile} accept="image/*,.txt,.md,.csv,.json,.js,.jsx,.ts,.tsx,.py,.go,.java,.rb,.sh,.yaml,.yml,.xml,.html,.css,.sql,.log,text/*"/>
              <button className="iconbtn" onClick={function(){if(fileRef.current) fileRef.current.click();}} disabled={streaming} title="Attach file or image" style={{width:42,height:42}}>
                <Icon name="plus" size={18}/>
              </button>
              <input className="input" style={{flex:1}} placeholder="Message NeuroRoute…" value={input} onChange={function(e){setInput(e.target.value); if(estimate) setEstimate(null); if(decision && !streaming) setDecision(null);}} onKeyDown={function(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();}}} disabled={streaming}/>
              <button className="btn btn-soft" disabled={!input.trim()||estimating} onClick={runEstimate} title="Preview cost before sending — works even while streaming">
                <Icon name="estimator" size={15}/>{estimating?'Estimating…':'Estimate'}
              </button>
              <button className="btn btn-soft" disabled={!input.trim()} onClick={function(){setTplName(activeTemplate?activeTemplate.name:''); setTplSlug(activeTemplate?activeTemplate.slug:''); setSaveTplModal(true);}} title="Save this message as a reusable prompt template">
                <Icon name="layers" size={15}/>Save as template
              </button>
              {streaming ?
                <button className="btn btn-stop" onClick={stopStream} title="Stop streaming"><Icon name="x" size={15}/>Stop</button> :
                <button className="btn btn-grad" disabled={!input.trim()&&!attach} onClick={send}><Icon name="send" size={16}/>Send</button>
              }
            </div>
          </div>
        </div>
      </div>
      {/* Routing decision — sits directly under the chat, filling the left column. */}
      {(decision || streaming || (input && input.trim())) && (function(){
        // The cards mirror the RHS: while a new prompt is typed or streaming (no
        // decision for THIS run yet) they show a dimmed "—"/"…" reset state instead
        // of the previous run's stale numbers, then populate with the RHS once this
        // run's routing metadata arrives.
        var d = decision || {};
        var pend = !decision;
        var mInfo = d.model ? D2.modelById(d.model) : null;
        var hasCost = d.cost != null && d.cost !== '';
        var dash = streaming ? '…' : '—';
        // Reserve a consistent 2-line label height so the values line up across all
        // four cards even when a label wraps (e.g. "Task classification").
        var ebStyle = {minHeight:30, lineHeight:1.35, display:'block'};
        return (
          <div className="grid" style={{gridTemplateColumns:'repeat(4, 1fr)', gap:12, opacity: pend?0.6:1, transition:'opacity .2s'}}>
            <div className="card card-pad" style={{minHeight:104,display:'flex',flexDirection:'column',gap:6}}>
              <div className="eyebrow" style={ebStyle}>Task classification</div>
              <div>{d.taskType ? <Badge color="blue">{d.taskType}</Badge> : <span className="faint">{dash}</span>}</div>
              {d.confidence>0 && <div className="faint mono" style={{fontSize:11,marginTop:'auto'}}>{Math.round(d.confidence*100)}% confidence</div>}
            </div>
            <div className="card card-pad" style={{minHeight:104,display:'flex',flexDirection:'column',gap:6}}>
              <div className="eyebrow" style={ebStyle}>Strategy</div>
              <div style={{fontWeight:700,fontSize:15}}>{d.strategy||dash}</div>
            </div>
            <div className="card card-pad" style={{minHeight:104,display:'flex',flexDirection:'column',gap:6}}>
              <div className="eyebrow" style={ebStyle}>Routed model</div>
              {mInfo ? (
                <div className="row" style={{gap:8,alignItems:'center',minWidth:0}}>
                  <ProviderLogo id={mInfo.provider} size={20}/>
                  <b style={{fontSize:14.5,lineHeight:1.25,minWidth:0}}>{mInfo.name}</b>
                </div>
              ) : <span className="faint">{dash}</span>}
            </div>
            <div className="card card-pad" style={{minHeight:104,display:'flex',flexDirection:'column',gap:6}}>
              <div className="eyebrow" style={ebStyle}>Request cost</div>
              <div className="mono" style={{fontSize:18,color:'var(--green)'}}>{hasCost ? '$'+parseFloat(d.cost).toFixed(6) : dash}</div>
              {d.requestId && <div className="mono faint" style={{fontSize:10.5,marginTop:'auto',cursor:'pointer'}} onClick={function(){go('explorer?req='+encodeURIComponent(d.requestId));}} title="Open full trace in Routing Explorer">{d.requestId.slice(0,12)}… ↗</div>}
            </div>
          </div>
        );
      })()}
      </div>
      {/* RHS: estimate panel */}
      <div style={{alignSelf:'stretch',overflowY:'auto',minHeight:0,paddingRight:2}}>
        {estimatePanel}
      </div>
      </div>

      {saveTplModal && <Modal title="Save as prompt template" sub="Use {{variable}} placeholders anywhere in the text — fill them in each time you load the template."
        onClose={function(){setSaveTplModal(false);}}
        footer={<><button className="btn btn-ghost" onClick={function(){setSaveTplModal(false);}}>Cancel</button>
          <button className="btn btn-primary" disabled={!tplName.trim()||!tplSlug.trim()||tplSaving} onClick={saveAsTemplate}>{tplSaving?'Saving…':'Save template'}</button></>}>
        <div className="grid" style={{gap:16}}>
          <Field label="Name"><input className="input" placeholder="Support reply" value={tplName} onChange={function(e){setTplName(e.target.value);}}/></Field>
          <Field label="Slug" hint="Lowercase, hyphens only — used as prompt:<slug> in the model field.">
            <input className="input mono" placeholder="support-reply" value={tplSlug} onChange={function(e){setTplSlug(e.target.value.toLowerCase());}}/>
          </Field>
          <Field label="Message (saved as-is)"><div className="codeblock" style={{fontSize:12.5,maxHeight:160,overflowY:'auto',whiteSpace:'pre-wrap'}}>{input}</div></Field>
        </div>
      </Modal>}
    </div>
  );
}

window.PAGES = Object.assign(window.PAGES||{}, {apikeys:ApiKeysPage, estimator:EstimatorPage, explorer:ExplorerPage, playground:PlaygroundPage});
