| import json |
|
|
| HTML = """<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>DDGS Search Lab</title> |
| <style> |
| @import url("https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Space+Grotesk:wght@400;500;700&display=swap"); |
| |
| :root { |
| --bg: #f4efe7; |
| --panel: rgba(255, 252, 246, 0.84); |
| --panel-strong: rgba(255, 255, 255, 0.94); |
| --ink: #1e2430; |
| --muted: #5f6778; |
| --accent: #176b5f; |
| --accent-strong: #0f4f46; |
| --accent-soft: rgba(23, 107, 95, 0.14); |
| --line: rgba(30, 36, 48, 0.1); |
| --shadow: 0 24px 80px rgba(43, 54, 72, 0.12); |
| --radius: 24px; |
| } |
| |
| * { |
| box-sizing: border-box; |
| } |
| |
| html, |
| body { |
| margin: 0; |
| min-height: 100%; |
| background: |
| radial-gradient(circle at top left, rgba(255, 183, 77, 0.22), transparent 24rem), |
| radial-gradient(circle at top right, rgba(23, 107, 95, 0.18), transparent 26rem), |
| linear-gradient(180deg, #f7f2ea 0%, #f2ece4 48%, #efe8df 100%); |
| color: var(--ink); |
| font-family: "Space Grotesk", "Avenir Next", sans-serif; |
| } |
| |
| body { |
| padding: 32px 20px 48px; |
| } |
| |
| .shell { |
| max-width: 1240px; |
| margin: 0 auto; |
| } |
| |
| .hero { |
| position: relative; |
| overflow: hidden; |
| border: 1px solid rgba(255, 255, 255, 0.6); |
| border-radius: 32px; |
| padding: 28px; |
| background: |
| linear-gradient(135deg, rgba(255, 248, 239, 0.92), rgba(250, 255, 252, 0.8)), |
| rgba(255, 255, 255, 0.7); |
| box-shadow: var(--shadow); |
| } |
| |
| .hero::after { |
| content: ""; |
| position: absolute; |
| inset: auto -5rem -6rem auto; |
| width: 18rem; |
| height: 18rem; |
| border-radius: 999px; |
| background: radial-gradient(circle, rgba(23, 107, 95, 0.18), transparent 68%); |
| pointer-events: none; |
| } |
| |
| .eyebrow { |
| display: inline-flex; |
| align-items: center; |
| gap: 10px; |
| padding: 10px 14px; |
| border-radius: 999px; |
| border: 1px solid var(--line); |
| background: rgba(255, 255, 255, 0.76); |
| color: var(--muted); |
| font-size: 13px; |
| letter-spacing: 0.02em; |
| text-transform: uppercase; |
| } |
| |
| .dot { |
| width: 10px; |
| height: 10px; |
| border-radius: 999px; |
| background: #2ab381; |
| box-shadow: 0 0 0 6px rgba(42, 179, 129, 0.14); |
| } |
| |
| h1 { |
| margin: 20px 0 12px; |
| max-width: 11ch; |
| font-family: "Instrument Serif", Georgia, serif; |
| font-size: clamp(3rem, 8vw, 5.6rem); |
| line-height: 0.94; |
| font-weight: 400; |
| letter-spacing: -0.04em; |
| } |
| |
| .lede { |
| max-width: 60ch; |
| color: var(--muted); |
| font-size: 1.02rem; |
| line-height: 1.7; |
| } |
| |
| .hero-grid, |
| .app-grid { |
| display: grid; |
| gap: 24px; |
| } |
| |
| .hero-grid { |
| grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.9fr); |
| align-items: end; |
| } |
| |
| .app-grid { |
| margin-top: 24px; |
| grid-template-columns: minmax(320px, 420px) minmax(0, 1fr); |
| } |
| |
| .card { |
| border: 1px solid rgba(255, 255, 255, 0.68); |
| border-radius: var(--radius); |
| padding: 22px; |
| background: var(--panel); |
| backdrop-filter: blur(18px); |
| box-shadow: var(--shadow); |
| } |
| |
| .hero-card { |
| align-self: stretch; |
| display: grid; |
| gap: 14px; |
| background: |
| linear-gradient(160deg, rgba(15, 79, 70, 0.95), rgba(33, 54, 86, 0.92)), |
| var(--panel-strong); |
| color: #eff7f3; |
| } |
| |
| .hero-card strong { |
| font-size: 2rem; |
| font-weight: 700; |
| } |
| |
| .hero-card p, |
| .hero-card li { |
| color: rgba(239, 247, 243, 0.76); |
| } |
| |
| .card-title { |
| display: flex; |
| justify-content: space-between; |
| gap: 16px; |
| align-items: center; |
| margin-bottom: 18px; |
| } |
| |
| .card-title h2, |
| .card-title h3 { |
| margin: 0; |
| font-size: 1rem; |
| letter-spacing: 0.04em; |
| text-transform: uppercase; |
| } |
| |
| .muted { |
| color: var(--muted); |
| } |
| |
| .field-grid { |
| display: grid; |
| gap: 14px; |
| } |
| |
| .split { |
| display: grid; |
| gap: 14px; |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| } |
| |
| label { |
| display: grid; |
| gap: 8px; |
| color: var(--muted); |
| font-size: 0.88rem; |
| } |
| |
| input, |
| textarea, |
| select, |
| button { |
| font: inherit; |
| } |
| |
| input, |
| textarea, |
| select { |
| width: 100%; |
| border: 1px solid rgba(30, 36, 48, 0.12); |
| border-radius: 18px; |
| background: rgba(255, 255, 255, 0.78); |
| padding: 13px 15px; |
| color: var(--ink); |
| outline: none; |
| transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; |
| } |
| |
| input:focus, |
| textarea:focus, |
| select:focus { |
| border-color: rgba(23, 107, 95, 0.45); |
| box-shadow: 0 0 0 4px rgba(23, 107, 95, 0.12); |
| } |
| |
| textarea { |
| min-height: 104px; |
| resize: vertical; |
| } |
| |
| .checkbox { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| padding: 13px 15px; |
| border-radius: 18px; |
| background: rgba(255, 255, 255, 0.72); |
| border: 1px solid rgba(30, 36, 48, 0.12); |
| color: var(--ink); |
| } |
| |
| .checkbox input { |
| width: 18px; |
| height: 18px; |
| } |
| |
| .actions { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 12px; |
| margin-top: 6px; |
| } |
| |
| button { |
| appearance: none; |
| border: 0; |
| border-radius: 999px; |
| padding: 14px 20px; |
| cursor: pointer; |
| transition: transform 140ms ease, opacity 140ms ease, box-shadow 140ms ease; |
| } |
| |
| button:hover { |
| transform: translateY(-1px); |
| } |
| |
| button:disabled { |
| cursor: wait; |
| opacity: 0.72; |
| } |
| |
| .primary { |
| background: linear-gradient(135deg, var(--accent), var(--accent-strong)); |
| color: #f7faf9; |
| box-shadow: 0 14px 40px rgba(23, 107, 95, 0.26); |
| } |
| |
| .secondary { |
| background: rgba(255, 255, 255, 0.82); |
| color: var(--ink); |
| border: 1px solid rgba(30, 36, 48, 0.12); |
| } |
| |
| .stats { |
| display: grid; |
| gap: 14px; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| margin-bottom: 18px; |
| } |
| |
| .stat { |
| padding: 16px; |
| border-radius: 18px; |
| background: var(--panel-strong); |
| border: 1px solid rgba(30, 36, 48, 0.08); |
| } |
| |
| .stat span { |
| display: block; |
| color: var(--muted); |
| font-size: 0.82rem; |
| margin-bottom: 8px; |
| } |
| |
| .stat strong { |
| display: block; |
| font-size: 1.1rem; |
| } |
| |
| .status-bar { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 16px; |
| margin-bottom: 16px; |
| } |
| |
| .pill { |
| display: inline-flex; |
| align-items: center; |
| gap: 8px; |
| padding: 10px 14px; |
| border-radius: 999px; |
| background: var(--accent-soft); |
| color: var(--accent-strong); |
| font-size: 0.88rem; |
| } |
| |
| .response { |
| min-height: 340px; |
| border-radius: 20px; |
| padding: 18px; |
| background: #1d2230; |
| color: #d6e1ff; |
| overflow: auto; |
| white-space: pre-wrap; |
| word-break: break-word; |
| line-height: 1.55; |
| font-family: "SFMono-Regular", "JetBrains Mono", "Menlo", monospace; |
| font-size: 0.9rem; |
| } |
| |
| .results { |
| display: grid; |
| gap: 14px; |
| margin-top: 18px; |
| } |
| |
| .raw-json { |
| margin-top: 18px; |
| border-radius: 20px; |
| background: rgba(14, 18, 28, 0.92); |
| border: 1px solid rgba(255, 255, 255, 0.08); |
| overflow: hidden; |
| } |
| |
| .raw-json summary { |
| list-style: none; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 16px; |
| padding: 16px 18px; |
| color: #eef4ff; |
| font-size: 0.92rem; |
| user-select: none; |
| } |
| |
| .raw-json summary::-webkit-details-marker { |
| display: none; |
| } |
| |
| .raw-json summary::after { |
| content: "+"; |
| font-size: 1.2rem; |
| color: rgba(238, 244, 255, 0.72); |
| } |
| |
| .raw-json[open] summary::after { |
| content: "-"; |
| } |
| |
| .result { |
| padding: 18px; |
| border-radius: 20px; |
| background: rgba(255, 255, 255, 0.9); |
| border: 1px solid rgba(30, 36, 48, 0.08); |
| } |
| |
| .result a { |
| color: var(--accent-strong); |
| text-decoration: none; |
| } |
| |
| .result a:hover { |
| text-decoration: underline; |
| } |
| |
| .result h4 { |
| margin: 0 0 10px; |
| font-size: 1.04rem; |
| } |
| |
| .result p { |
| margin: 0; |
| color: var(--muted); |
| line-height: 1.65; |
| } |
| |
| .tiny { |
| font-size: 0.82rem; |
| color: var(--muted); |
| } |
| |
| .kbd { |
| display: inline-flex; |
| align-items: center; |
| min-width: 24px; |
| justify-content: center; |
| padding: 2px 7px; |
| border-radius: 8px; |
| border: 1px solid rgba(255, 255, 255, 0.24); |
| background: rgba(255, 255, 255, 0.1); |
| font-size: 0.75rem; |
| } |
| |
| @media (max-width: 980px) { |
| .hero-grid, |
| .app-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| @media (max-width: 720px) { |
| body { |
| padding: 20px 14px 34px; |
| } |
| |
| .hero, |
| .card { |
| padding: 18px; |
| border-radius: 22px; |
| } |
| |
| .split, |
| .stats { |
| grid-template-columns: 1fr; |
| } |
| |
| .status-bar { |
| align-items: flex-start; |
| flex-direction: column; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <main class="shell"> |
| <section class="hero"> |
| <div class="hero-grid"> |
| <div> |
| <div class="eyebrow"><span class="dot"></span> Live playground for your DDGS Space</div> |
| <h1>Search API, now with a UI that feels intentional.</h1> |
| <p class="lede"> |
| Use this page to hit <code>/search</code> with your bearer token, tune the request, |
| inspect the raw JSON, and preview the first results without leaving the browser. |
| </p> |
| </div> |
| <aside class="card hero-card"> |
| <div class="card-title"> |
| <h2>Flow</h2> |
| <span class="tiny">Fast local or HF validation</span> |
| </div> |
| <strong>Paste token. Tune request. Hit search.</strong> |
| <p> |
| The token stays in your browser via <code>sessionStorage</code>. No hidden server-side |
| injection, no extra backend state. |
| </p> |
| <p class="tiny"> |
| Shortcut: <span class="kbd">Cmd</span> + <span class="kbd">Enter</span> or |
| <span class="kbd">Ctrl</span> + <span class="kbd">Enter</span> |
| </p> |
| </aside> |
| </div> |
| </section> |
| |
| <section class="app-grid"> |
| <section class="card"> |
| <div class="card-title"> |
| <h3>Request</h3> |
| <span class="tiny">POST /search</span> |
| </div> |
| |
| <div class="field-grid"> |
| <label> |
| Bearer token |
| <input |
| id="token" |
| type="password" |
| placeholder="Paste API_BEARER_TOKEN" |
| autocomplete="off" |
| /> |
| </label> |
| |
| <label> |
| Query |
| <textarea id="query" placeholder="Search for something specific.">openai</textarea> |
| </label> |
| |
| <div class="split"> |
| <label> |
| Region |
| <input id="region" type="text" /> |
| </label> |
| <label> |
| Safe search |
| <select id="safesearch"> |
| <option value="on">on</option> |
| <option value="moderate">moderate</option> |
| <option value="off">off</option> |
| </select> |
| </label> |
| </div> |
| |
| <div class="split"> |
| <label> |
| Time limit |
| <select id="timelimit"> |
| <option value="">none</option> |
| <option value="d">day</option> |
| <option value="w">week</option> |
| <option value="m">month</option> |
| <option value="y">year</option> |
| </select> |
| </label> |
| <label> |
| Backend |
| <input id="backend" type="text" /> |
| </label> |
| </div> |
| |
| <div class="split"> |
| <label> |
| Max results |
| <input id="max_results" type="number" min="1" max="25" step="1" /> |
| </label> |
| <label> |
| Timeout |
| <input id="timeout" type="number" min="1" max="120" step="1" /> |
| </label> |
| </div> |
| |
| <label class="checkbox"> |
| <input id="verify" type="checkbox" /> |
| Verify SSL certificates |
| </label> |
| |
| <div class="actions"> |
| <button class="primary" id="run">Run search</button> |
| <button class="secondary" id="example" type="button">Load example</button> |
| <button class="secondary" id="clear" type="button">Clear token</button> |
| </div> |
| </div> |
| </section> |
| |
| <section class="card"> |
| <div class="card-title"> |
| <h3>Response</h3> |
| <span class="tiny">Live API output</span> |
| </div> |
| |
| <div class="stats"> |
| <div class="stat"> |
| <span>Status</span> |
| <strong id="statusCode">Idle</strong> |
| </div> |
| <div class="stat"> |
| <span>Latency</span> |
| <strong id="latency">-</strong> |
| </div> |
| <div class="stat"> |
| <span>Result count</span> |
| <strong id="resultCount">-</strong> |
| </div> |
| </div> |
| |
| <div class="status-bar"> |
| <div class="pill" id="statusLabel">Ready for a request</div> |
| <div class="tiny">The top section is a card view. The lower section is raw JSON.</div> |
| </div> |
| |
| <div id="results" class="results"></div> |
| <details id="rawDetails" class="raw-json"> |
| <summary>Raw JSON payload</summary> |
| <pre id="response" class="response">No request sent yet.</pre> |
| </details> |
| </section> |
| </section> |
| </main> |
| |
| <script> |
| const defaults = __DEFAULTS__; |
| const tokenKey = "ddgs-api-bearer-token"; |
| |
| const elements = { |
| token: document.getElementById("token"), |
| query: document.getElementById("query"), |
| region: document.getElementById("region"), |
| safesearch: document.getElementById("safesearch"), |
| timelimit: document.getElementById("timelimit"), |
| backend: document.getElementById("backend"), |
| maxResults: document.getElementById("max_results"), |
| timeout: document.getElementById("timeout"), |
| verify: document.getElementById("verify"), |
| run: document.getElementById("run"), |
| example: document.getElementById("example"), |
| clear: document.getElementById("clear"), |
| statusCode: document.getElementById("statusCode"), |
| latency: document.getElementById("latency"), |
| resultCount: document.getElementById("resultCount"), |
| statusLabel: document.getElementById("statusLabel"), |
| rawDetails: document.getElementById("rawDetails"), |
| response: document.getElementById("response"), |
| results: document.getElementById("results"), |
| }; |
| |
| function applyDefaults(nextDefaults) { |
| elements.region.value = nextDefaults.region; |
| elements.safesearch.value = nextDefaults.safesearch; |
| elements.timelimit.value = nextDefaults.timelimit || ""; |
| elements.backend.value = nextDefaults.backend || ""; |
| elements.maxResults.value = nextDefaults.max_results; |
| elements.timeout.value = nextDefaults.timeout; |
| elements.verify.checked = Boolean(nextDefaults.verify); |
| } |
| |
| function loadSavedToken() { |
| const saved = sessionStorage.getItem(tokenKey); |
| if (saved) { |
| elements.token.value = saved; |
| } |
| } |
| |
| function saveToken() { |
| if (elements.token.value.trim()) { |
| sessionStorage.setItem(tokenKey, elements.token.value.trim()); |
| } |
| } |
| |
| function buildPayload() { |
| const payload = { |
| query: elements.query.value.trim(), |
| region: elements.region.value.trim(), |
| safesearch: elements.safesearch.value, |
| timelimit: elements.timelimit.value || null, |
| max_results: Number(elements.maxResults.value || defaults.max_results), |
| backend: elements.backend.value.trim() || null, |
| timeout: Number(elements.timeout.value || defaults.timeout), |
| verify: elements.verify.checked, |
| }; |
| |
| return payload; |
| } |
| |
| function setState({ label, code, latency, count, raw, expandRaw = false }) { |
| elements.statusLabel.textContent = label; |
| elements.statusCode.textContent = code; |
| elements.latency.textContent = latency; |
| elements.resultCount.textContent = count; |
| elements.response.textContent = raw; |
| elements.rawDetails.open = expandRaw; |
| } |
| |
| function renderResults(payload) { |
| elements.results.innerHTML = ""; |
| const results = Array.isArray(payload.results) ? payload.results.slice(0, 6) : []; |
| |
| if (!results.length) { |
| return; |
| } |
| |
| results.forEach((item, index) => { |
| const article = document.createElement("article"); |
| article.className = "result"; |
| const title = item.title || item.href || `Result ${index + 1}`; |
| const body = item.body || item.markdown || "No summary returned."; |
| const safeBody = String(body).slice(0, 420); |
| const href = item.href |
| ? `<a href="${item.href}" target="_blank" rel="noreferrer">${item.href}</a>` |
| : "No URL"; |
| article.innerHTML = ` |
| <h4>${title}</h4> |
| <p class="tiny">${href}</p> |
| <p>${safeBody}</p> |
| `; |
| elements.results.appendChild(article); |
| }); |
| } |
| |
| async function runSearch() { |
| const token = elements.token.value.trim(); |
| const payload = buildPayload(); |
| |
| if (!token) { |
| setState({ |
| label: "Missing bearer token", |
| code: "Blocked", |
| latency: "-", |
| count: "-", |
| raw: "Add a bearer token before sending the request.", |
| expandRaw: true, |
| }); |
| return; |
| } |
| |
| if (!payload.query) { |
| setState({ |
| label: "Query required", |
| code: "Blocked", |
| latency: "-", |
| count: "-", |
| raw: "Enter a search query before sending the request.", |
| expandRaw: true, |
| }); |
| return; |
| } |
| |
| saveToken(); |
| elements.run.disabled = true; |
| setState({ |
| label: "Request in flight", |
| code: "Loading", |
| latency: "...", |
| count: "...", |
| raw: JSON.stringify(payload, null, 2), |
| expandRaw: false, |
| }); |
| elements.results.innerHTML = ""; |
| |
| const started = performance.now(); |
| |
| try { |
| const response = await fetch("/search", { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| "Authorization": `Bearer ${token}`, |
| }, |
| body: JSON.stringify(payload), |
| }); |
| |
| const elapsed = `${Math.round(performance.now() - started)} ms`; |
| const text = await response.text(); |
| let parsed = null; |
| |
| try { |
| parsed = JSON.parse(text); |
| } catch (error) { |
| parsed = null; |
| } |
| |
| setState({ |
| label: response.ok ? "Search completed" : "Request returned an error", |
| code: String(response.status), |
| latency: elapsed, |
| count: parsed && typeof parsed.count !== "undefined" ? String(parsed.count) : "-", |
| raw: parsed ? JSON.stringify(parsed, null, 2) : text, |
| expandRaw: !response.ok, |
| }); |
| |
| if (parsed) { |
| renderResults(parsed); |
| } |
| } catch (error) { |
| setState({ |
| label: "Network error", |
| code: "Failed", |
| latency: "-", |
| count: "-", |
| raw: String(error), |
| expandRaw: true, |
| }); |
| } finally { |
| elements.run.disabled = false; |
| } |
| } |
| |
| elements.example.addEventListener("click", () => { |
| elements.query.value = "site:openai.com safety"; |
| applyDefaults({ |
| ...defaults, |
| timelimit: "m", |
| max_results: 3, |
| }); |
| }); |
| |
| elements.clear.addEventListener("click", () => { |
| elements.token.value = ""; |
| sessionStorage.removeItem(tokenKey); |
| elements.token.focus(); |
| }); |
| |
| elements.run.addEventListener("click", runSearch); |
| elements.query.addEventListener("keydown", (event) => { |
| if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { |
| runSearch(); |
| } |
| }); |
| |
| applyDefaults(defaults); |
| loadSavedToken(); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
|
|
| def render_homepage(defaults: dict[str, object]) -> str: |
| return HTML.replace("__DEFAULTS__", json.dumps(defaults)) |
|
|