sync: 2026-03-15 16:00 — 1 files from Alexandria
Some checks failed
Lint & Format / detect (push) Failing after 33s
Monorepo Lint / lint-shell (push) Failing after 34s
Monorepo Lint / lint-js (push) Failing after 42s
Lint & Format / js-lint (push) Has been skipped
Lint & Format / py-lint (push) Has been skipped
Lint & Format / sh-lint (push) Has been skipped
Lint & Format / go-lint (push) Has been skipped
Some checks failed
Lint & Format / detect (push) Failing after 33s
Monorepo Lint / lint-shell (push) Failing after 34s
Monorepo Lint / lint-js (push) Failing after 42s
Lint & Format / js-lint (push) Has been skipped
Lint & Format / py-lint (push) Has been skipped
Lint & Format / sh-lint (push) Has been skipped
Lint & Format / go-lint (push) Has been skipped
RoadChain-SHA2048: 4b551c47708033bb RoadChain-Identity: alexa@sovereign RoadChain-Full: 4b551c47708033bbefae87722a428aeee5d1b8c0fef6bc3abdc639773aa6fb2664d9c879f3e60a3fc50e6a3a8e9f64f781c7f807a3240ab043ec59279fa761e9087e7e972d0b8df6ed4f30263d1298c0af595efd9ca7d9b45efcebb202e188d3b15133d0f6bd7ea2237f53226c378e6f8f49535a51163f92fa6488c179ce48277f4ba8e8a9d73878bdef8110a36c4b277cf7fb8916ca8697f632a605a095b3442305d085f2ffe4f78ed2ab400d3cd64c6b58c6eaf17e2bdaa2ebf23193dd302cacb6dc15055216a17cffa4432ac212f51e6205a3592fd61020ce9c3178df6ddc8b948faa8c49661bcff9a36d05ed63c34e41f2e9cb987f32022567204b0715d8
This commit is contained in:
@@ -453,6 +453,475 @@ async function handleLucky(request, env) {
|
||||
return Response.json({ error: 'No results found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// ─── Search Frontend HTML ─────────────────────────────────────────────
|
||||
const SEARCH_HTML = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RoadSearch — Search the Road. Find the Way.</title>
|
||||
<meta name="description" content="BlackRoad OS sovereign search engine. Search across all BlackRoad domains, agents, and services.">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔍</text></svg>">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#000;--fg:#fff;--muted:#666;--dim:#444;--border:#333;--surface:#111;--surface2:#1a1a1a;
|
||||
--link:#7ab8ff;--link-hover:#aad4ff;--url:#4a9;
|
||||
--grad:linear-gradient(90deg,#FF6B2B,#FF2255,#CC00AA,#8844FF,#4488FF,#00D4FF);
|
||||
--grotesk:'Space Grotesk',sans-serif;--mono:'JetBrains Mono',monospace;--inter:'Inter',sans-serif;
|
||||
}
|
||||
html{height:100%}
|
||||
body{min-height:100%;background:var(--bg);color:var(--fg);font-family:var(--inter);display:flex;flex-direction:column}
|
||||
a{color:var(--link);text-decoration:none}a:hover{color:var(--link-hover)}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes pulseGlow{0%,100%{box-shadow:0 0 0 0 rgba(136,68,255,0)}50%{box-shadow:0 0 20px 2px rgba(136,68,255,.15)}}
|
||||
|
||||
/* ─── Layout ─── */
|
||||
.app{flex:1;display:flex;flex-direction:column}
|
||||
.hero{flex:1;display:flex;flex-direction:column;align-items:center;transition:all .3s ease}
|
||||
.hero.home{justify-content:center}
|
||||
.hero.results{justify-content:flex-start;padding-top:24px}
|
||||
.footer{text-align:center;padding:20px 16px;border-top:1px solid var(--surface)}
|
||||
.footer-text{font-family:var(--mono);font-size:11px;color:var(--border)}
|
||||
.footer-links{display:flex;gap:16px;justify-content:center;flex-wrap:wrap;margin-bottom:8px}
|
||||
.footer-links a{font-family:var(--mono);font-size:11px;color:var(--dim);transition:color .2s}
|
||||
.footer-links a:hover{color:var(--fg)}
|
||||
|
||||
/* ─── Title ─── */
|
||||
.title{font-family:var(--grotesk);font-weight:700;letter-spacing:-.02em;cursor:pointer;transition:font-size .3s ease}
|
||||
.home .title{font-size:clamp(36px,8vw,56px);margin-bottom:8px}
|
||||
.results .title{font-size:24px;margin-bottom:0}
|
||||
.subtitle{font-family:var(--mono);font-size:13px;color:var(--muted);margin-bottom:28px}
|
||||
|
||||
/* ─── Search Bar ─── */
|
||||
.search-wrap{position:relative;width:100%;padding:0 20px;transition:max-width .3s ease}
|
||||
.home .search-wrap{max-width:560px}
|
||||
.results .search-wrap{max-width:680px}
|
||||
.search-form{display:flex;gap:0;position:relative}
|
||||
.search-input{
|
||||
width:100%;padding:14px 100px 14px 18px;font-size:16px;font-family:var(--inter);
|
||||
background:var(--surface);color:var(--fg);border:1px solid var(--border);border-radius:8px;
|
||||
outline:none;transition:border-color .2s,box-shadow .2s;
|
||||
}
|
||||
.search-input::placeholder{color:var(--dim)}
|
||||
.search-input:focus{border-color:transparent;border-image:var(--grad) 1;animation:pulseGlow 2s ease infinite}
|
||||
.search-btns{position:absolute;right:8px;top:50%;transform:translateY(-50%);display:flex;gap:4px;align-items:center}
|
||||
.btn{
|
||||
font-family:var(--mono);font-size:12px;padding:6px 12px;border-radius:6px;border:1px solid var(--border);
|
||||
background:transparent;color:var(--muted);cursor:pointer;transition:all .2s;white-space:nowrap;
|
||||
}
|
||||
.btn:hover{color:var(--fg);border-color:var(--muted)}
|
||||
.btn-primary{background:#222;color:#aaa}
|
||||
.btn-primary:hover{background:#333;color:var(--fg)}
|
||||
.hint{font-family:var(--mono);font-size:11px;color:var(--dim);margin-top:8px;text-align:center}
|
||||
.hint kbd{background:var(--surface);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-size:10px}
|
||||
|
||||
/* ─── Suggestions ─── */
|
||||
.suggest-box{
|
||||
position:absolute;top:100%;left:20px;right:20px;margin-top:4px;
|
||||
background:var(--surface);border:1px solid var(--border);border-radius:8px;
|
||||
overflow:hidden;z-index:100;animation:fadeIn .15s ease;
|
||||
}
|
||||
.suggest-item{
|
||||
padding:10px 16px;font-size:14px;font-family:var(--inter);color:#aaa;cursor:pointer;transition:background .15s;
|
||||
display:flex;align-items:center;gap:8px;
|
||||
}
|
||||
.suggest-item:hover,.suggest-item.active{background:#222;color:var(--fg)}
|
||||
.suggest-icon{font-size:12px;color:var(--dim);flex-shrink:0}
|
||||
.suggest-section{font-family:var(--mono);font-size:10px;color:var(--dim);padding:8px 16px 4px;text-transform:uppercase;letter-spacing:.1em}
|
||||
|
||||
/* ─── Category Pills ─── */
|
||||
.pills{display:flex;gap:8px;justify-content:center;margin-top:16px;flex-wrap:wrap;padding:0 20px}
|
||||
.pill{
|
||||
font-family:var(--mono);font-size:12px;padding:5px 14px;border-radius:20px;
|
||||
border:1px solid var(--border);background:transparent;color:var(--fg);cursor:pointer;transition:all .2s;
|
||||
}
|
||||
.pill:hover{border-color:var(--muted)}
|
||||
.pill.active{border:none;background:var(--grad);font-weight:600}
|
||||
|
||||
/* ─── Stats Bar ─── */
|
||||
.stats-bar{
|
||||
display:flex;gap:24px;justify-content:center;margin-top:24px;padding:0 20px;
|
||||
}
|
||||
.stat{font-family:var(--mono);font-size:12px;color:var(--dim)}
|
||||
.stat-val{color:var(--muted);font-weight:600}
|
||||
|
||||
/* ─── Trending ─── */
|
||||
.trending{margin-top:36px;text-align:center;padding:0 20px}
|
||||
.trending-label{font-family:var(--mono);font-size:11px;color:#555;margin-bottom:10px;text-transform:uppercase;letter-spacing:.1em}
|
||||
.trending-item{
|
||||
font-family:var(--inter);font-size:13px;color:#888;cursor:pointer;padding:4px 12px;
|
||||
display:inline-block;transition:color .2s;
|
||||
}
|
||||
.trending-item:hover{color:#ccc}
|
||||
|
||||
/* ─── Results ─── */
|
||||
.results-area{width:100%;max-width:680px;padding:0 20px;margin-top:20px}
|
||||
.results-meta{font-family:var(--mono);font-size:12px;color:#555;margin-bottom:16px}
|
||||
.spinner{display:inline-block;width:20px;height:20px;border:2px solid var(--border);border-top-color:#888;border-radius:50%;animation:spin .6s linear infinite}
|
||||
.loading-wrap{text-align:center;padding-top:40px}
|
||||
.no-results{text-align:center;padding-top:40px}
|
||||
.no-results h3{font-family:var(--grotesk);font-size:18px;color:var(--muted);margin-bottom:8px}
|
||||
.no-results p{font-family:var(--inter);font-size:14px;color:var(--dim)}
|
||||
|
||||
/* ─── AI Answer ─── */
|
||||
.ai-box{
|
||||
background:#0a0a0a;border-left:3px solid transparent;border-image:var(--grad) 1;
|
||||
padding:16px 20px;border-radius:0 8px 8px 0;margin-bottom:24px;animation:fadeIn .3s ease;
|
||||
}
|
||||
.ai-label{font-family:var(--mono);font-size:11px;color:#888;margin-bottom:8px;text-transform:uppercase;letter-spacing:.08em}
|
||||
.ai-text{font-family:var(--inter);font-size:14px;color:#ccc;line-height:1.65;white-space:pre-wrap}
|
||||
.ai-text a{color:var(--link)}
|
||||
|
||||
/* ─── Result Card ─── */
|
||||
.result-card{padding:16px 0;border-bottom:1px solid var(--surface2);transition:background .2s;animation:fadeIn .2s ease}
|
||||
.result-card:hover{background:#0a0a0a}
|
||||
.result-title{font-family:var(--grotesk);font-size:17px;font-weight:600;color:var(--link);cursor:pointer;transition:color .2s}
|
||||
.result-title:hover{color:var(--link-hover)}
|
||||
.result-url{font-family:var(--mono);font-size:12px;color:var(--url);margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.result-snippet{font-family:var(--inter);font-size:14px;color:#999;line-height:1.55;margin-top:5px}
|
||||
.result-meta{margin-top:6px;display:flex;align-items:center;flex-wrap:wrap;gap:4px}
|
||||
.badge{font-family:var(--mono);font-size:10px;padding:2px 8px;border-radius:4px;border:1px solid var(--border);color:#888;text-transform:uppercase}
|
||||
.tag{font-family:var(--mono);font-size:10px;color:#555;margin-left:4px}
|
||||
.relevance{font-family:var(--mono);font-size:10px;color:var(--dim);margin-left:auto}
|
||||
|
||||
/* ─── Pagination ─── */
|
||||
.pagination{display:flex;justify-content:center;align-items:center;gap:12px;padding:24px 0}
|
||||
.page-btn{
|
||||
font-family:var(--mono);font-size:13px;padding:6px 16px;border:1px solid var(--border);border-radius:6px;
|
||||
background:transparent;color:#aaa;cursor:pointer;transition:all .2s;
|
||||
}
|
||||
.page-btn:hover:not(:disabled){background:var(--surface);color:var(--fg)}
|
||||
.page-btn:disabled{color:var(--border);cursor:default}
|
||||
.page-info{font-family:var(--mono);font-size:12px;color:#555}
|
||||
|
||||
/* ─── Lucky bar ─── */
|
||||
.lucky-bar{font-family:var(--mono);font-size:12px;color:#555;cursor:pointer;text-align:center;padding:8px 0 20px;transition:color .2s}
|
||||
.lucky-bar:hover{color:#888}
|
||||
|
||||
/* ─── Responsive ─── */
|
||||
@media(max-width:600px){
|
||||
.search-input{padding:12px 80px 12px 14px;font-size:15px}
|
||||
.btn{font-size:11px;padding:5px 8px}
|
||||
.stats-bar{gap:12px}
|
||||
.result-title{font-size:15px}
|
||||
.pills{gap:6px}
|
||||
.pill{font-size:11px;padding:4px 10px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app" id="app">
|
||||
<div class="hero home" id="hero">
|
||||
<div class="title" id="title" onclick="goHome()">RoadSearch</div>
|
||||
<div class="subtitle" id="subtitle">Search the Road. Find the Way.</div>
|
||||
|
||||
<div class="search-wrap">
|
||||
<form class="search-form" onsubmit="doSearch(event)" autocomplete="off">
|
||||
<input class="search-input" id="q" type="text" placeholder="Search BlackRoad..." autofocus
|
||||
oninput="onInput()" onkeydown="onKeyDown(event)" onfocus="onFocus()" />
|
||||
<div class="search-btns">
|
||||
<button type="submit" class="btn btn-primary" title="Search">⌕</button>
|
||||
<button type="button" class="btn" onclick="feelingLucky()" title="I'm Feeling Lucky">Lucky</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="suggest-box" id="suggestions" style="display:none"></div>
|
||||
<div class="hint" id="hint">Press <kbd>/</kbd> to focus · <kbd>Esc</kbd> to clear</div>
|
||||
</div>
|
||||
|
||||
<div class="pills" id="pills"></div>
|
||||
<div class="stats-bar" id="statsBar"></div>
|
||||
<div class="trending" id="trending"></div>
|
||||
<div class="results-area" id="resultsArea" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="footer-links">
|
||||
<a href="https://blackroad.io">Home</a>
|
||||
<a href="https://blackroad.network">Network</a>
|
||||
<a href="https://blackroadai.com">AI</a>
|
||||
<a href="https://status.blackroad.io">Status</a>
|
||||
<a href="https://blackroad.company">Company</a>
|
||||
<a href="https://brand.blackroad.io">Brand</a>
|
||||
<a href="https://blackroad.io/pricing">Pricing</a>
|
||||
<a href="https://github.com/blackboxprogramming">GitHub</a>
|
||||
</div>
|
||||
<div class="footer-text" id="footerStats"></div>
|
||||
<div class="footer-text" style="margin-top:4px">BlackRoad OS — Pave Tomorrow.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const CATEGORIES = ['All','Sites','Agents','Tech','API','Apps'];
|
||||
const API = '';
|
||||
|
||||
let state = {
|
||||
query: '', submitted: '', category: 'All', results: null, aiAnswer: null,
|
||||
loading: false, duration: null, total: 0, page: 1,
|
||||
suggestions: [], suggestIdx: -1, showSuggest: false,
|
||||
stats: { indexed: 0, queries: 0, queries24h: 0 }, trending: [],
|
||||
};
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
const qInput = () => $('q');
|
||||
|
||||
// ─── Init ──────────────────────────────────────────────────────────
|
||||
function init() {
|
||||
renderPills();
|
||||
loadStats();
|
||||
const params = new URLSearchParams(location.search);
|
||||
const q = params.get('q');
|
||||
const cat = params.get('category');
|
||||
if (cat) { state.category = CATEGORIES.find(c => c.toLowerCase() === cat.toLowerCase()) || 'All'; renderPills(); }
|
||||
if (q) { state.query = q; qInput().value = q; search(q, state.category, 1); }
|
||||
document.addEventListener('keydown', globalKey);
|
||||
document.addEventListener('click', e => {
|
||||
if (!$('suggestions').contains(e.target) && e.target !== qInput()) closeSuggest();
|
||||
});
|
||||
}
|
||||
|
||||
function globalKey(e) {
|
||||
if (e.key === '/' && document.activeElement !== qInput()) { e.preventDefault(); qInput().focus(); }
|
||||
if (e.key === 'Escape') { closeSuggest(); qInput().blur(); }
|
||||
}
|
||||
|
||||
// ─── API ──────────────────────────────────────────────────────────
|
||||
async function api(path) {
|
||||
const res = await fetch(API + path, { headers: { 'Accept': 'application/json' } });
|
||||
if (!res.ok) throw new Error(res.status);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const d = await api('/stats');
|
||||
state.stats = { indexed: d.indexed_pages || 0, queries: d.total_queries || 0, queries24h: d.queries_24h || 0 };
|
||||
state.trending = d.trending || [];
|
||||
renderStats();
|
||||
renderTrending();
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// ─── Search ──────────────────────────────────────────────────────
|
||||
function doSearch(e) { e && e.preventDefault(); search(state.query, state.category, 1); }
|
||||
|
||||
async function search(q, cat, pg) {
|
||||
q = (q || '').trim();
|
||||
if (!q) return;
|
||||
state.submitted = q; state.loading = true; state.page = pg;
|
||||
closeSuggest();
|
||||
updateURL(q, cat);
|
||||
setMode('results');
|
||||
renderLoading();
|
||||
|
||||
const catParam = cat && cat !== 'All' ? '&category=' + encodeURIComponent(cat.toLowerCase().replace(/s$/, '')) : '';
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
const data = await api('/search?q=' + encodeURIComponent(q) + catParam + '&ai=true&page=' + pg + '&limit=10');
|
||||
state.duration = Math.round(performance.now() - start);
|
||||
state.results = data.results || [];
|
||||
state.total = data.total || state.results.length;
|
||||
state.aiAnswer = data.ai_answer || null;
|
||||
state.page = pg;
|
||||
} catch(e) {
|
||||
state.results = []; state.total = 0; state.aiAnswer = null; state.duration = null;
|
||||
}
|
||||
state.loading = false;
|
||||
renderResults();
|
||||
}
|
||||
|
||||
// ─── Suggestions ────────────────────────────────────────────────
|
||||
let suggestTimer = null;
|
||||
function onInput() {
|
||||
state.query = qInput().value;
|
||||
clearTimeout(suggestTimer);
|
||||
if (state.query.length < 2) { closeSuggest(); return; }
|
||||
suggestTimer = setTimeout(async () => {
|
||||
if (state.query === state.submitted) return;
|
||||
try {
|
||||
const d = await api('/suggest?q=' + encodeURIComponent(state.query));
|
||||
state.suggestions = (d.suggestions || []).concat(d.recent || []);
|
||||
state.suggestions = [...new Set(state.suggestions)].slice(0, 8);
|
||||
state.suggestIdx = -1;
|
||||
if (state.suggestions.length) { state.showSuggest = true; renderSuggestions(); }
|
||||
else closeSuggest();
|
||||
} catch(e) {}
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function onFocus() { if (state.suggestions.length && !state.showSuggest) { state.showSuggest = true; renderSuggestions(); } }
|
||||
function closeSuggest() { state.showSuggest = false; $('suggestions').style.display = 'none'; }
|
||||
|
||||
function onKeyDown(e) {
|
||||
if (state.showSuggest && state.suggestions.length) {
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); state.suggestIdx = Math.min(state.suggestIdx + 1, state.suggestions.length - 1); renderSuggestions(); return; }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); state.suggestIdx = Math.max(state.suggestIdx - 1, -1); renderSuggestions(); return; }
|
||||
if (e.key === 'Enter' && state.suggestIdx >= 0) { e.preventDefault(); const pick = state.suggestions[state.suggestIdx]; state.query = pick; qInput().value = pick; search(pick, state.category, 1); return; }
|
||||
}
|
||||
if (e.key === 'Escape') { closeSuggest(); qInput().blur(); }
|
||||
}
|
||||
|
||||
function pickSuggestion(text) { state.query = text; qInput().value = text; search(text, state.category, 1); }
|
||||
|
||||
// ─── Lucky ───────────────────────────────────────────────────────
|
||||
function feelingLucky() {
|
||||
const q = (state.query || state.submitted || '').trim();
|
||||
if (q) window.location.href = '/lucky?q=' + encodeURIComponent(q);
|
||||
}
|
||||
|
||||
// ─── Navigation ─────────────────────────────────────────────────
|
||||
function goHome() {
|
||||
state.submitted = ''; state.results = null; state.aiAnswer = null; state.query = '';
|
||||
qInput().value = '';
|
||||
history.replaceState(null, '', location.pathname);
|
||||
setMode('home');
|
||||
$('resultsArea').style.display = 'none';
|
||||
$('resultsArea').innerHTML = '';
|
||||
loadStats();
|
||||
}
|
||||
|
||||
function setMode(mode) {
|
||||
const hero = $('hero');
|
||||
hero.className = 'hero ' + mode;
|
||||
$('subtitle').style.display = mode === 'home' ? '' : 'none';
|
||||
$('hint').style.display = mode === 'home' ? '' : 'none';
|
||||
$('trending').style.display = mode === 'home' ? '' : 'none';
|
||||
$('statsBar').style.display = mode === 'home' ? '' : 'none';
|
||||
}
|
||||
|
||||
function updateURL(q, cat) {
|
||||
const p = new URLSearchParams();
|
||||
if (q) p.set('q', q);
|
||||
if (cat && cat !== 'All') p.set('category', cat);
|
||||
const s = p.toString();
|
||||
history.replaceState(null, '', s ? '?' + s : location.pathname);
|
||||
}
|
||||
|
||||
function setCategory(cat) {
|
||||
state.category = cat;
|
||||
renderPills();
|
||||
if (state.submitted) search(state.submitted, cat, 1);
|
||||
}
|
||||
|
||||
// ─── Render ─────────────────────────────────────────────────────
|
||||
function renderPills() {
|
||||
$('pills').innerHTML = CATEGORIES.map(c =>
|
||||
'<button class="pill' + (state.category === c ? ' active' : '') + '" onclick="setCategory(\\''+c+'\\')">'+c+'</button>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
function renderStats() {
|
||||
const s = state.stats;
|
||||
$('statsBar').innerHTML =
|
||||
'<span class="stat"><span class="stat-val">' + (s.indexed || 0).toLocaleString() + '</span> pages indexed</span>' +
|
||||
'<span class="stat"><span class="stat-val">' + (s.queries24h || 0).toLocaleString() + '</span> searches today</span>' +
|
||||
'<span class="stat"><span class="stat-val">' + (s.queries || 0).toLocaleString() + '</span> total queries</span>';
|
||||
$('footerStats').textContent = (s.indexed || 0).toLocaleString() + ' pages indexed \\u00B7 ' + (s.queries24h || 0).toLocaleString() + ' queries today';
|
||||
}
|
||||
|
||||
function renderTrending() {
|
||||
if (!state.trending.length) { $('trending').innerHTML = ''; return; }
|
||||
let html = '<div class="trending-label">Trending</div><div>';
|
||||
state.trending.slice(0, 8).forEach(t => {
|
||||
const text = typeof t === 'string' ? t : (t.query || t.text || '');
|
||||
html += '<span class="trending-item" onclick="pickSuggestion(\\''+esc(text)+'\\')">'+esc(text)+'</span>';
|
||||
});
|
||||
html += '</div>';
|
||||
$('trending').innerHTML = html;
|
||||
}
|
||||
|
||||
function renderSuggestions() {
|
||||
const box = $('suggestions');
|
||||
if (!state.showSuggest || !state.suggestions.length) { box.style.display = 'none'; return; }
|
||||
let html = '';
|
||||
state.suggestions.forEach((s, i) => {
|
||||
const text = typeof s === 'string' ? s : (s.query || s.text || '');
|
||||
html += '<div class="suggest-item' + (i === state.suggestIdx ? ' active' : '') + '" '
|
||||
+ 'onmouseenter="state.suggestIdx='+i+';renderSuggestions()" '
|
||||
+ 'onclick="pickSuggestion(\\''+esc(text)+'\\')"><span class="suggest-icon">🔍</span>'+esc(text)+'</div>';
|
||||
});
|
||||
box.innerHTML = html;
|
||||
box.style.display = 'block';
|
||||
}
|
||||
|
||||
function renderLoading() {
|
||||
const area = $('resultsArea');
|
||||
area.style.display = 'block';
|
||||
area.innerHTML = '<div class="loading-wrap"><div class="spinner"></div></div>';
|
||||
}
|
||||
|
||||
function renderResults() {
|
||||
const area = $('resultsArea');
|
||||
area.style.display = 'block';
|
||||
|
||||
if (!state.results || !state.results.length) {
|
||||
area.innerHTML = '<div class="no-results"><h3>No results found</h3><p>Try different keywords or broaden your search.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
|
||||
// Meta
|
||||
if (state.duration !== null) {
|
||||
html += '<div class="results-meta">' + state.total + ' result' + (state.total !== 1 ? 's' : '') + ' in ' + state.duration + 'ms</div>';
|
||||
}
|
||||
|
||||
// AI Answer
|
||||
if (state.aiAnswer) {
|
||||
const rendered = state.aiAnswer.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
||||
html += '<div class="ai-box"><div class="ai-label">AI Answer</div><div class="ai-text">' + rendered + '</div></div>';
|
||||
}
|
||||
|
||||
// Results
|
||||
state.results.forEach((r, i) => {
|
||||
const title = esc(r.title || r.name || 'Untitled');
|
||||
const url = r.url || r.link || '#';
|
||||
const snippet = esc(r.snippet || r.description || '');
|
||||
const cat = r.category || '';
|
||||
const tags = r.tags || [];
|
||||
const rel = r.relevance ? r.relevance.toFixed(2) : '';
|
||||
|
||||
html += '<div class="result-card">'
|
||||
+ '<a class="result-title" href="'+esc(url)+'" target="_blank" rel="noopener">'+title+'</a>'
|
||||
+ '<div class="result-url">'+esc(url)+'</div>'
|
||||
+ '<div class="result-snippet">'+snippet+'</div>'
|
||||
+ '<div class="result-meta">';
|
||||
if (cat) html += '<span class="badge">'+esc(cat)+'</span>';
|
||||
tags.forEach(t => { html += '<span class="tag">#'+esc(t)+'</span>'; });
|
||||
if (rel) html += '<span class="relevance">rel: '+rel+'</span>';
|
||||
html += '</div></div>';
|
||||
});
|
||||
|
||||
// Lucky bar
|
||||
if (state.results.length > 0) {
|
||||
html += '<div class="lucky-bar" onclick="feelingLucky()">I'm Feeling Lucky</div>';
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const totalPages = Math.ceil(state.total / 10);
|
||||
if (totalPages > 1) {
|
||||
html += '<div class="pagination">'
|
||||
+ '<button class="page-btn" ' + (state.page <= 1 ? 'disabled' : 'onclick="search(state.submitted,state.category,'+(state.page-1)+')"') + '>Prev</button>'
|
||||
+ '<span class="page-info">' + state.page + ' / ' + totalPages + '</span>'
|
||||
+ '<button class="page-btn" ' + (state.page >= totalPages ? 'disabled' : 'onclick="search(state.submitted,state.category,'+(state.page+1)+')"') + '>Next</button>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
area.innerHTML = html;
|
||||
}
|
||||
|
||||
function esc(s) { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// ─── Router ───────────────────────────────────────────────────────────
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
@@ -495,20 +964,28 @@ export default {
|
||||
response = await handleIndex(request, env);
|
||||
break;
|
||||
|
||||
default:
|
||||
response = Response.json({
|
||||
engine: 'RoadSearch',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
search: 'GET /search?q=query&category=&domain=&page=1&limit=20&ai=true',
|
||||
suggest: 'GET /suggest?q=prefix',
|
||||
lucky: 'GET /lucky?q=query (redirects to top result)',
|
||||
stats: 'GET /stats',
|
||||
index: 'POST /index (auth required)',
|
||||
health: 'GET /health',
|
||||
},
|
||||
tagline: 'Search the Road. Find the Way.',
|
||||
});
|
||||
default: {
|
||||
const accept = request.headers.get('Accept') || '';
|
||||
if (accept.includes('application/json') && !accept.includes('text/html')) {
|
||||
response = Response.json({
|
||||
engine: 'RoadSearch',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
search: 'GET /search?q=query&category=&domain=&page=1&limit=20&ai=true',
|
||||
suggest: 'GET /suggest?q=prefix',
|
||||
lucky: 'GET /lucky?q=query (redirects to top result)',
|
||||
stats: 'GET /stats',
|
||||
index: 'POST /index (auth required)',
|
||||
health: 'GET /health',
|
||||
},
|
||||
tagline: 'Search the Road. Find the Way.',
|
||||
});
|
||||
} else {
|
||||
response = new Response(SEARCH_HTML, {
|
||||
headers: { 'Content-Type': 'text/html;charset=UTF-8' },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('RoadSearch error:', err);
|
||||
|
||||
Reference in New Issue
Block a user