ddgs / src /ddgs_api /ui.py
dromerosm's picture
Store UI token in session storage
d511651
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))