ollama / app /web.py
charlielin0121's picture
Upload 13 files
340ee2f verified
Raw
History Blame Contribute Delete
7.35 kB
from html import escape
CSS = """
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
* { box-sizing: border-box; }
body { margin: 0; background: #0b1020; color: #eef2ff; min-height: 100vh; }
main { width: min(760px, calc(100% - 32px)); margin: 48px auto; }
.card { background: #151b2f; border: 1px solid #303957; border-radius: 18px; padding: 28px; box-shadow: 0 18px 60px #0007; }
h1 { margin: 0 0 8px; font-size: 30px; }
h2 { margin-top: 28px; font-size: 20px; }
p, li { color: #cbd5e1; line-height: 1.6; }
label { display: block; margin: 18px 0 7px; font-weight: 700; }
input, textarea { width: 100%; border: 1px solid #465171; border-radius: 10px; background: #0d1325; color: #fff; padding: 12px 13px; font: inherit; }
textarea { min-height: 120px; resize: vertical; }
button, .button { display: inline-block; margin-top: 20px; border: 0; border-radius: 10px; background: #facc15; color: #151515; font-weight: 800; padding: 12px 18px; cursor: pointer; text-decoration: none; }
button.secondary, .button.secondary { background: #334155; color: #fff; }
button.danger { background: #ef4444; color: #fff; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 12px; }
.stat { background: #0d1325; border: 1px solid #303957; border-radius: 12px; padding: 14px; }
.stat b { display: block; margin-bottom: 6px; }
code { color: #fde68a; overflow-wrap: anywhere; }
.notice { border-left: 4px solid #facc15; background: #272313; padding: 12px 14px; border-radius: 8px; }
.error { border-left-color: #ef4444; background: #31151b; }
.success { border-left-color: #22c55e; background: #10281d; }
.row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.row form { margin: 0; }
.check { display: flex; gap: 10px; align-items: flex-start; font-weight: 500; }
.check input { width: auto; margin-top: 5px; }
small { color: #94a3b8; }
hr { border: 0; border-top: 1px solid #303957; margin: 28px 0; }
"""
def shell(title: str, content: str) -> str:
return f"""<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{escape(title)}</title>
<style>{CSS}</style>
</head>
<body><main><section class="card">{content}</section></main></body>
</html>"""
def setup_page(error: str = "") -> str:
error_html = f'<p class="notice error">{escape(error)}</p>' if error else ""
return shell(
"首次設定 — Arena Playwright API",
f"""
<h1>🎭 首次設定</h1>
<p>所有執行設定都會在這個 Space 內完成。本版本使用暫存設定,Space 重啟後需重新設定。</p>
{error_html}
<p class="notice">請先從 Space 的 <b>Logs</b> 找到本次啟動產生的 <code>SETUP CODE</code>。這可避免陌生人搶先設定公開 Space。</p>
<form method="post" action="/setup">
<label for="bootstrap_code">SETUP CODE</label>
<input id="bootstrap_code" name="bootstrap_code" type="password" required autocomplete="one-time-code">
<label for="admin_password">管理員密碼</label>
<input id="admin_password" name="admin_password" type="password" minlength="12" autocomplete="new-password" placeholder="若已設定 ADMIN_PASSWORD Secret,可留空">
<small>至少 12 個字元,用於稍後登入管理頁。</small>
<label for="arena_auth_token">自己的 Arena Cookie(選填後備)</label>
<textarea id="arena_auth_token" name="arena_auth_token" placeholder="若已設定 ARENA_EMAIL+ARENA_PASSWORD Secrets,可留空"></textarea>
<small>可使用 Cookie,或由 Space 內的 Playwright 使用非 Google Email/Password 登入。</small>
<label for="bridge_api_key">API Key(可使用 BRIDGE_API_KEY Secret,或留空自動產生)</label>
<input id="bridge_api_key" name="bridge_api_key" type="password" minlength="20" autocomplete="new-password">
<label class="check">
<input name="authorized_use" type="checkbox" value="yes" required>
<span>我確認這是我自己的帳號/獲准工作階段,且已在 Arena 官方網站自行閱讀並處理必要條款與人工驗證。</span>
</label>
<button type="submit">儲存並啟動 Playwright</button>
</form>
""",
)
def setup_complete_page(api_key: str, base_url: str) -> str:
return shell(
"設定完成 — Arena Playwright API",
f"""
<h1>✅ 設定完成</h1>
<p class="notice success">Playwright 瀏覽器已啟動。請立即保存下面的 API Key。</p>
<label>OpenAI API Base URL</label>
<input readonly value="{escape(base_url.rstrip('/') + '/v1')}">
<label>API Key</label>
<input readonly value="{escape(api_key)}">
<p><b>OpenWebUI:</b>URL 使用上面的 <code>/v1</code>,API Key 使用這個值。</p>
<div class="row">
<a class="button" href="/admin">前往管理頁</a>
<a class="button secondary" href="/docs">API 文件</a>
</div>
""",
)
def login_page(error: str = "") -> str:
error_html = f'<p class="notice error">{escape(error)}</p>' if error else ""
return shell(
"管理員登入",
f"""
<h1>管理員登入</h1>
{error_html}
<form method="post" action="/login">
<label for="password">管理員密碼</label>
<input id="password" name="password" type="password" required autocomplete="current-password">
<button type="submit">登入</button>
</form>
""",
)
def admin_page(
*,
browser_ready: bool,
api_key: str,
masked_token: str,
message: str = "",
error: bool = False,
) -> str:
message_html = ""
if message:
kind = "error" if error else "success"
message_html = f'<p class="notice {kind}">{escape(message)}</p>'
status = "已啟動" if browser_ready else "未啟動"
return shell(
"管理 — Arena Playwright API",
f"""
<h1>管理設定</h1>
<p>所有設定都是暫存;Space 重啟後會清除並產生新的 SETUP CODE。</p>
{message_html}
<div class="grid">
<div class="stat"><b>Playwright</b>{escape(status)}</div>
<div class="stat"><b>Arena Cookie</b><code>{escape(masked_token)}</code></div>
</div>
<h2>目前 API 連線</h2>
<label>API Base URL</label>
<input readonly value="/v1">
<label>API Key</label>
<input readonly value="{escape(api_key)}">
<h2>更新設定</h2>
<form method="post" action="/admin/update">
<label for="arena_auth_token">新的 Arena Cookie</label>
<textarea id="arena_auth_token" name="arena_auth_token" placeholder="留空代表維持目前 Cookie"></textarea>
<label for="bridge_api_key">新的 API Key</label>
<input id="bridge_api_key" name="bridge_api_key" type="password" minlength="20" placeholder="留空代表維持目前 API Key">
<label for="admin_password">新的管理員密碼</label>
<input id="admin_password" name="admin_password" type="password" minlength="12" placeholder="留空代表維持目前密碼">
<button type="submit">更新並重新啟動瀏覽器</button>
</form>
<hr>
<div class="row">
<form method="post" action="/admin/regenerate-key"><button class="secondary" type="submit">重新產生 API Key</button></form>
<form method="post" action="/admin/restart"><button class="secondary" type="submit">立即重新登入/重製 Cookie</button></form>
<form method="post" action="/logout"><button class="danger" type="submit">登出</button></form>
</div>
""",
)