| 1 | <!doctype html> |
| 2 | <html lang="en"> |
| 3 | <head> |
| 4 | <meta charset="utf-8" /> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| 6 | <title>Reasonix Provider Setup</title> |
| 7 | <style> |
| 8 | *,*::before,*::after{box-sizing:border-box} |
| 9 | :root{color-scheme:dark;--bg:#121316;--panel:#1c1e22;--border:#353840;--text:#f1f2f4;--muted:#a4a8b2;--accent:#e1663f;--accent-hover:#ed7650;--danger:#ff806f} |
| 10 | body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:radial-gradient(circle at top,#22242a 0,var(--bg) 52%);color:var(--text);font:15px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} |
| 11 | .card{width:min(520px,100%);padding:28px;border:1px solid var(--border);border-radius:18px;background:var(--panel);box-shadow:0 24px 80px rgba(0,0,0,.34)} |
| 12 | .brand{display:flex;align-items:center;gap:12px;margin-bottom:24px}.brand img{width:132px;height:auto}.badge{margin-left:auto;padding:4px 9px;border-radius:999px;background:rgba(225,102,63,.14);color:#ff9a79;font-size:12px} |
| 13 | h1{margin:0 0 8px;font-size:24px;line-height:1.2}p{margin:0;color:var(--muted)} |
| 14 | .meta{display:grid;grid-template-columns:92px 1fr;gap:8px 14px;margin:24px 0;padding:16px;border-radius:12px;background:#15171a}.meta dt{color:var(--muted)}.meta dd{margin:0;min-width:0;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace} |
| 15 | label{display:block;margin:0 0 8px;font-weight:600}.field{width:100%;height:44px;padding:0 12px;border:1px solid var(--border);border-radius:10px;background:#111215;color:var(--text);font:14px ui-monospace,SFMono-Regular,Menlo,monospace;outline:none}.field:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(225,102,63,.16)} |
| 16 | .hint{margin-top:8px;font-size:12px}.actions{display:flex;align-items:center;gap:12px;margin-top:22px}button{height:42px;padding:0 18px;border:0;border-radius:10px;background:var(--accent);color:white;font:600 14px system-ui;cursor:pointer}button:hover{background:var(--accent-hover)}button:disabled{cursor:wait;opacity:.62}.status{min-height:22px;color:var(--muted);font-size:13px}.status.error{color:var(--danger)} |
| 17 | </style> |
| 18 | </head> |
| 19 | <body> |
| 20 | <main class="card"> |
| 21 | <div class="brand"><img src="/assets/logo-wordmark.svg" alt="Reasonix" /><span class="badge" id="remote-badge">Remote</span></div> |
| 22 | <h1 id="title">Configure the remote Provider</h1> |
| 23 | <p id="intro">This Reasonix Serve is ready, but its selected Provider has no saved API key on this host.</p> |
| 24 | <dl class="meta"> |
| 25 | <dt id="provider-label">Provider</dt><dd id="provider">—</dd> |
| 26 | <dt id="model-label">Model</dt><dd id="model">—</dd> |
| 27 | <dt id="key-label">Credential</dt><dd id="key-env">—</dd> |
| 28 | </dl> |
| 29 | <form id="setup-form"> |
| 30 | <label for="api-key" id="api-key-label">API Key</label> |
| 31 | <input class="field" id="api-key" type="password" autocomplete="off" spellcheck="false" required /> |
| 32 | <p class="hint" id="hint">The key is stored only in this remote host's Reasonix credential file.</p> |
| 33 | <div class="actions"><button id="save" type="submit">Save and open Reasonix</button><span class="status" id="status" role="status"></span></div> |
| 34 | </form> |
| 35 | </main> |
| 36 | <script> |
| 37 | const langPref='__LANG__'; |
| 38 | const zh=String(langPref).toLowerCase().startsWith('zh'); |
| 39 | const copy=zh?{ |
| 40 | badge:'远端',title:'配置远端 Provider',intro:'Reasonix Serve 已可访问,但这台主机尚未保存当前 Provider 的 API Key。',provider:'Provider',model:'模型',credential:'凭据变量',apiKey:'API Key',hint:'密钥只会保存到这台远端主机的 Reasonix 凭据文件。',pendingHint:'API Key 已安全保存。现在只需重试激活 Provider。',save:'保存并打开 Reasonix',retry:'重试激活',saving:'正在保存…',activating:'正在激活…',failed:'保存失败,请重试。' |
| 41 | }:{badge:'Remote',title:'Configure the remote Provider',intro:'This Reasonix Serve is ready, but its selected Provider has no saved API key on this host.',provider:'Provider',model:'Model',credential:'Credential',apiKey:'API Key',hint:"The key is stored only in this remote host's Reasonix credential file.",pendingHint:'The API key is safely stored. Retry Provider activation without entering it again.',save:'Save and open Reasonix',retry:'Retry activation',saving:'Saving…',activating:'Activating…',failed:'Setup failed. Try again.'}; |
| 42 | const $=id=>document.getElementById(id); |
| 43 | $('remote-badge').textContent=copy.badge;$('title').textContent=copy.title;$('intro').textContent=copy.intro;$('provider-label').textContent=copy.provider;$('model-label').textContent=copy.model;$('key-label').textContent=copy.credential;$('api-key-label').textContent=copy.apiKey;$('hint').textContent=copy.hint;$('save').textContent=copy.save; |
| 44 | let activationPending=false; |
| 45 | async function load(){ |
| 46 | const response=await fetch('/provider-setup',{cache:'no-store'}); |
| 47 | if(!response.ok)throw new Error(copy.failed); |
| 48 | const state=await response.json(); |
| 49 | if(!state.required){location.reload();return;} |
| 50 | activationPending=state.activationPending===true; |
| 51 | const input=$('api-key'); |
| 52 | $('provider').textContent=state.provider||'—';$('model').textContent=state.model||'—';$('key-env').textContent=state.keyEnv||'—'; |
| 53 | $('api-key-label').hidden=activationPending;input.hidden=activationPending;input.required=!activationPending; |
| 54 | $('hint').textContent=activationPending?copy.pendingHint:copy.hint;$('save').textContent=activationPending?copy.retry:copy.save; |
| 55 | $('status').textContent='';$('status').classList.remove('error'); |
| 56 | if(state.error){$('status').textContent=state.error;$('status').classList.add('error');} |
| 57 | } |
| 58 | $('setup-form').addEventListener('submit',async event=>{ |
| 59 | event.preventDefault();const input=$('api-key'),button=$('save'),status=$('status'); |
| 60 | button.disabled=true;status.classList.remove('error');status.textContent=activationPending?copy.activating:copy.saving; |
| 61 | try{ |
| 62 | const response=await fetch('/provider-setup',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({apiKey:activationPending?'':input.value})}); |
| 63 | input.value=''; |
| 64 | if(!response.ok)throw new Error((await response.text()).trim()||copy.failed); |
| 65 | location.reload(); |
| 66 | }catch(error){input.value='';let refreshed=false;try{await load();refreshed=status.classList.contains('error');}catch{}if(!refreshed){status.textContent=error instanceof Error?error.message:copy.failed;status.classList.add('error');}button.disabled=false;if(!activationPending)input.focus();} |
| 67 | }); |
| 68 | load().catch(error=>{$('status').textContent=error instanceof Error?error.message:copy.failed;$('status').classList.add('error');}); |
| 69 | </script> |
| 70 | </body> |
| 71 | </html> |
| 72 |