Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| import json | |
| import threading | |
| import webbrowser | |
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer | |
| from pathlib import Path | |
| HOST = "127.0.0.1" | |
| PORT = 8765 | |
| SETTINGS_PATH = Path(__file__).with_name("local_settings.json") | |
| PAGE = """<!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Gemma Heretic Demo Settings</title> | |
| <style> | |
| :root { color-scheme: light dark; font: 16px/1.45 system-ui, sans-serif; } | |
| body { margin: 0; background: #16111f; color: #f5efff; } | |
| main { width: min(760px, calc(100% - 2rem)); margin: 3rem auto; } | |
| form { display: grid; gap: 1rem; padding: 1.5rem; border: 1px solid #604b78; | |
| border-radius: 18px; background: #21172d; box-shadow: 0 18px 60px #0005; } | |
| label { display: grid; gap: .4rem; font-weight: 650; } | |
| input, textarea { box-sizing: border-box; width: 100%; padding: .75rem; | |
| border: 1px solid #765c92; border-radius: 10px; background: #160f20; color: inherit; } | |
| .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; } | |
| button { padding: .8rem 1.2rem; border: 0; border-radius: 10px; cursor: pointer; | |
| background: #9d63da; color: white; font-weight: 750; } | |
| #status { min-height: 1.5rem; color: #cdb0ec; } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <h1>Gemma Heretic Demo Settings</h1> | |
| <p>These defaults apply the next time the local app starts.</p> | |
| <form id="settings"> | |
| <label>Model ID<input name="model_id" required></label> | |
| <label>System prompt<textarea name="system_prompt" rows="3"></textarea></label> | |
| <div class="grid"> | |
| <label>Max new tokens<input name="max_new_tokens" type="number" min="128" max="1536" step="128"></label> | |
| <label>Temperature<input name="temperature" type="number" min="0" max="1.5" step="0.05"></label> | |
| <label>Top-p<input name="top_p" type="number" min="0.1" max="1" step="0.05"></label> | |
| <label>Top-k<input name="top_k" type="number" min="1" max="128" step="1"></label> | |
| <label>Repetition penalty<input name="repetition_penalty" type="number" min="1" max="1.3" step="0.01"></label> | |
| <label>Max input tokens<input name="max_input_tokens" type="number" min="1024" max="32768" step="1024"></label> | |
| <label>History messages<input name="history_messages" type="number" min="1" max="32" step="1"></label> | |
| </div> | |
| <label><span><input name="thinking" type="checkbox" style="width:auto"> Enable thinking by default</span></label> | |
| <button type="submit">Save settings</button> | |
| <div id="status" role="status"></div> | |
| </form> | |
| </main> | |
| <script> | |
| const form = document.querySelector("#settings"); | |
| const status = document.querySelector("#status"); | |
| const numeric = ["max_new_tokens","temperature","top_p","top_k","repetition_penalty","max_input_tokens","history_messages"]; | |
| fetch("/api/settings").then(r => r.json()).then(data => { | |
| Object.entries(data).forEach(([key, value]) => { | |
| const field = form.elements.namedItem(key); | |
| if (!field) return; | |
| if (field.type === "checkbox") field.checked = Boolean(value); | |
| else field.value = value; | |
| }); | |
| }); | |
| form.addEventListener("submit", async event => { | |
| event.preventDefault(); | |
| const data = Object.fromEntries(new FormData(form)); | |
| numeric.forEach(key => data[key] = Number(data[key])); | |
| data.thinking = form.elements.namedItem("thinking").checked; | |
| const response = await fetch("/api/settings", { | |
| method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(data) | |
| }); | |
| status.textContent = response.ok ? "Saved. Restart run.bat to apply changes." : "Save failed."; | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| class SettingsHandler(BaseHTTPRequestHandler): | |
| def _send(self, status: int, content_type: str, body: bytes) -> None: | |
| self.send_response(status) | |
| self.send_header("Content-Type", content_type) | |
| self.send_header("Content-Length", str(len(body))) | |
| self.end_headers() | |
| self.wfile.write(body) | |
| def do_GET(self) -> None: | |
| if self.path == "/": | |
| self._send(200, "text/html; charset=utf-8", PAGE.encode()) | |
| return | |
| if self.path == "/api/settings": | |
| self._send(200, "application/json", SETTINGS_PATH.read_bytes()) | |
| return | |
| self._send(404, "text/plain", b"Not found") | |
| def do_POST(self) -> None: | |
| if self.path != "/api/settings": | |
| self._send(404, "text/plain", b"Not found") | |
| return | |
| length = int(self.headers.get("Content-Length", "0")) | |
| try: | |
| data = json.loads(self.rfile.read(length)) | |
| required = { | |
| "model_id", | |
| "system_prompt", | |
| "thinking", | |
| "max_new_tokens", | |
| "temperature", | |
| "top_p", | |
| "top_k", | |
| "repetition_penalty", | |
| "max_input_tokens", | |
| "history_messages", | |
| } | |
| if set(data) != required: | |
| raise ValueError("Unexpected settings fields.") | |
| SETTINGS_PATH.write_text( | |
| json.dumps(data, indent=2, ensure_ascii=False) + "\n", | |
| encoding="utf-8", | |
| ) | |
| except (ValueError, TypeError, json.JSONDecodeError): | |
| self._send(400, "application/json", b'{"ok":false}') | |
| return | |
| self._send(200, "application/json", b'{"ok":true}') | |
| def log_message(self, format: str, *args: object) -> None: | |
| return | |
| if __name__ == "__main__": | |
| server = ThreadingHTTPServer((HOST, PORT), SettingsHandler) | |
| url = f"http://{HOST}:{PORT}" | |
| threading.Timer(0.4, webbrowser.open, args=(url,)).start() | |
| print(f"Settings page: {url}") | |
| print("Press Ctrl+C to stop.") | |
| server.serve_forever() | |