Commite3eb5ffunknown_key
polish(repo): 2026 cards for demo + semantic-search
polish(repo): 2026 cards for demo + semantic-search Picked up from data-tools + misc polish agents: - /demo: 4-step interactive walkthrough cards with icons + 'Try this' prompts + reset button (.demo-page-*) - /:owner/:repo/semantic-search: prominent search input with focus ring + gradient submit, result cards with file:line in mono + snippet + match score (.ss-*) All scoped CSS. Queries / form actions preserved.
2 files changed+1154−257e3eb5ff8f79f9fa232356b477e87b5ebefe61173
2 changed files+1154−257
Modifiedsrc/routes/demo.tsx+671−140View fileUnifiedSplit
@@ -18,6 +18,10 @@
1818 *
1919 * All JSON endpoints serve `Cache-Control: public, max-age=30` so the demo
2020 * page is cheap to refresh and external dashboards can embed safely.
21 *
22 * 2026 polish: scoped `.demo-page-` CSS, four-step interactive walkthrough
23 * (mirrors /connect/claude), eyebrow + display headline + 1-line subtitle,
24 * 'Try this' prompts on each step, and a reset button on the live feed.
2125 */
2226
2327import { Hono } from "hono";
@@ -117,20 +121,6 @@ app.get("/api/v2/demo/reviews", async (c) => {
117121// HTML landing page.
118122// ───────────────────────────────────────────────────────────────────
119123
120function escapeHtml(s: string): string {
121 return String(s).replace(/[&<>"']/g, (c) =>
122 c === "&"
123 ? "&"
124 : c === "<"
125 ? "<"
126 : c === ">"
127 ? ">"
128 : c === '"'
129 ? """
130 : "'"
131 );
132}
133
134124function relativeTime(d: Date): string {
135125 const ms = Date.now() - d.getTime();
136126 const s = Math.max(0, Math.floor(ms / 1000));
@@ -175,7 +165,7 @@ app.get("/demo", softAuth, async (c) => {
175165 ];
176166
177167 // Poller — refreshes the four tiles every 30s. Plain vanilla JS, no
178 // framework. Mirrors the same pattern as `src/lib/sse-client.ts`.
168 // framework. Adds a reset action that re-fetches every tile immediately.
179169 const pollerScript = `
180170(function(){try{
181171 var INTERVAL=${POLL_INTERVAL_MS};
@@ -183,28 +173,38 @@ app.get("/demo", softAuth, async (c) => {
183173 function rel(iso){try{var ms=Date.now()-new Date(iso).getTime();var s=Math.max(0,Math.floor(ms/1000));if(s<60)return s+'s ago';var m=Math.floor(s/60);if(m<60)return m+'m ago';var h=Math.floor(m/60);if(h<48)return h+'h ago';return Math.floor(h/24)+'d ago';}catch(e){return '';}}
184174 function pollQueued(){fetch('/api/v2/demo/queued').then(function(r){return r.json();}).then(function(d){
185175 var el=document.getElementById('tile-queued-list');if(!el)return;
186 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-empty">No queued AI builds — quiet right now.</li>';return;}
187 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/issues/'+i.number+'">#'+i.number+' '+esc(i.title)+'</a> <span class="demo-meta">'+esc(i.repo)+' · '+rel(i.createdAt)+'</span></li>';}).join('');
176 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">No queued AI builds — quiet right now.</li>';return;}
177 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/issues/'+i.number+'">#'+i.number+' '+esc(i.title)+'</a> <span class="demo-page-meta">'+esc(i.repo)+' · '+rel(i.createdAt)+'</span></li>';}).join('');
188178 }).catch(function(){});}
189179 function pollMerges(){fetch('/api/v2/demo/merges').then(function(r){return r.json();}).then(function(d){
190180 var el=document.getElementById('tile-merges-list');if(!el)return;
191 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-empty">No auto-merges in the last 24h.</li>';return;}
192 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/pulls/'+i.number+'">#'+i.number+' '+esc(i.title)+'</a> <span class="demo-meta">'+esc(i.repo)+' · '+rel(i.mergedAt)+'</span></li>';}).join('');
181 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">No auto-merges in the last 24h.</li>';return;}
182 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/pulls/'+i.number+'">#'+i.number+' '+esc(i.title)+'</a> <span class="demo-page-meta">'+esc(i.repo)+' · '+rel(i.mergedAt)+'</span></li>';}).join('');
193183 }).catch(function(){});}
194184 function pollReviews(){fetch('/api/v2/demo/reviews').then(function(r){return r.json();}).then(function(d){
195185 var c=document.getElementById('tile-reviews-count');if(c)c.textContent=String(d.count||0);
196186 var el=document.getElementById('tile-reviews-list');if(!el)return;
197 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-empty">No AI reviews in the last 24h.</li>';return;}
198 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/pulls/'+i.prNumber+'">#'+i.prNumber+'</a> <span class="demo-snippet">'+esc(i.commentSnippet)+'</span> <span class="demo-meta">'+esc(i.repo)+' · '+rel(i.createdAt)+'</span></li>';}).join('');
187 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">No AI reviews in the last 24h.</li>';return;}
188 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/pulls/'+i.prNumber+'">#'+i.prNumber+'</a> <span class="demo-page-snippet">'+esc(i.commentSnippet)+'</span> <span class="demo-page-meta">'+esc(i.repo)+' · '+rel(i.createdAt)+'</span></li>';}).join('');
199189 }).catch(function(){});}
200190 function pollFeed(){fetch('/api/v2/demo/activity').then(function(r){return r.json();}).then(function(d){
201191 var el=document.getElementById('demo-feed-list');if(!el)return;
202 if(!d.entries||d.entries.length===0){el.innerHTML='<li class="demo-empty">Quiet right now — push a commit to a demo repo to see live updates.</li>';return;}
203 el.innerHTML=d.entries.map(function(e){var label=e.kind==='auto_merge.merged'?'auto-merged':(e.kind==='ai_build.dispatched'?'AI-build dispatched':'AI review posted');var path=e.ref.type==='pr'?'pulls':'issues';return '<li><span class="demo-kind demo-kind-'+esc(e.kind.replace(/\\./g,'-'))+'">'+esc(label)+'</span> <a href="/${DEMO_USERNAME}/'+esc(e.repo)+'/'+path+'/'+e.ref.number+'">'+esc(e.repo)+' #'+e.ref.number+'</a> <span class="demo-meta">'+rel(e.at)+'</span></li>';}).join('');
192 if(!d.entries||d.entries.length===0){el.innerHTML='<li class="demo-page-empty">Quiet right now — push a commit to a demo repo to see live updates.</li>';return;}
193 el.innerHTML=d.entries.map(function(e){var label=e.kind==='auto_merge.merged'?'auto-merged':(e.kind==='ai_build.dispatched'?'AI-build dispatched':'AI review posted');var path=e.ref.type==='pr'?'pulls':'issues';return '<li><span class="demo-page-kind demo-page-kind-'+esc(e.kind.replace(/\\./g,'-'))+'">'+esc(label)+'</span> <a href="/${DEMO_USERNAME}/'+esc(e.repo)+'/'+path+'/'+e.ref.number+'">'+esc(e.repo)+' #'+e.ref.number+'</a> <span class="demo-page-meta">'+rel(e.at)+'</span></li>';}).join('');
204194 }).catch(function(){});}
205195 function tick(){pollQueued();pollMerges();pollReviews();pollFeed();}
206 // Initial refresh after a short delay so the SSR snapshot stays visible
207 // a beat — keeps the page from "flashing" identical content on load.
196 // Reset button — re-fetch every tile immediately, then flash the button.
197 var reset=document.getElementById('demo-reset');
198 if(reset){reset.addEventListener('click',function(e){
199 e.preventDefault();
200 tick();
201 var orig=reset.textContent;
202 reset.textContent='Refreshed';
203 reset.classList.add('is-done');
204 setTimeout(function(){reset.textContent=orig;reset.classList.remove('is-done');},1500);
205 });}
206 // Background polling — initial SSR snapshot stays visible until the
207 // first tick fires, so the page doesn't "flash" identical content.
208208 setInterval(tick,INTERVAL);
209209}catch(e){}})();
210210`.trim();
@@ -213,44 +213,175 @@ app.get("/demo", softAuth, async (c) => {
213213 <Layout title="Live demo" user={user}>
214214 <style dangerouslySetInnerHTML={{ __html: DEMO_CSS }} />
215215 <div class="demo-page">
216 <div class="demo-hero">
217 <div class="demo-hero-inner">
218 <div class="eyebrow">
219 <span class="demo-pulse" />
216 {/* ─── Hero ─── */}
217 <section class="demo-page-hero">
218 <div class="demo-page-hero-orb" aria-hidden="true" />
219 <div class="demo-page-hero-inner">
220 <div class="demo-page-eyebrow">
221 <span class="demo-page-pulse" aria-hidden="true" />
220222 Live · pulled from production · refreshes every 30s
221223 </div>
222 <h1 class="demo-title">
223 Watch Claude <span class="gradient-text">build software, live.</span>
224 <h1 class="demo-page-title">
225 Watch Claude{" "}
226 <span class="demo-page-title-grad">build software, live.</span>
224227 </h1>
225 <p class="demo-sub">
226 Every tile below is real audit data from the seeded demo repos.
227 File an issue tagged <code>ai:build</code> and the gluecron autopilot
228 opens a PR. Land an AI review. Merge it automatically when gates
229 go green. No human in the loop for the routine cases.
228 <p class="demo-page-sub">
229 Every tile and step below is real audit data from the seeded
230 demo repos. No staging, no mocks.
230231 </p>
232 <div class="demo-page-hero-cta">
233 <a href="/register" class="demo-page-btn demo-page-btn-primary">
234 Sign up free <span aria-hidden="true">→</span>
235 </a>
236 <a href={demoUserUrl} class="demo-page-btn">
237 Browse the demo user
238 </a>
239 </div>
231240 </div>
232 <div class="demo-hero-cta">
233 <a href="/register" class="btn btn-primary btn-lg">
234 Sign up free <span aria-hidden="true">{"→"}</span>
235 </a>
241 </section>
242
243 {/* ─── 4-step interactive walkthrough ─── */}
244 <section class="demo-page-section" aria-labelledby="demo-walk-h">
245 <header class="demo-page-section-head">
246 <div>
247 <p class="demo-page-section-eyebrow">Walkthrough</p>
248 <h2 class="demo-page-section-title" id="demo-walk-h">
249 Four steps. Each one round-trips through production.
250 </h2>
251 <p class="demo-page-section-sub">
252 Click "Try this" on any step to jump into the live demo
253 surface that performs it.
254 </p>
255 </div>
256 </header>
257 <div class="demo-page-section-body">
258 <ol class="demo-page-walk">
259 {/* Step 1 — File an ai:build issue */}
260 <li class="demo-page-step">
261 <div class="demo-page-step-head">
262 <span class="demo-page-step-num">1</span>
263 <span class="demo-page-step-icon" aria-hidden="true">
264 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
265 <circle cx="12" cy="12" r="9" />
266 <line x1="12" y1="8" x2="12" y2="13" />
267 <line x1="12" y1="16" x2="12.01" y2="16" />
268 </svg>
269 </span>
270 <h3 class="demo-page-step-title">File an issue</h3>
271 </div>
272 <p class="demo-page-step-body">
273 Open a new issue on any demo repo and tag it{" "}
274 <code>ai:build</code>. The autopilot picks it up on the
275 next 5-minute sweep.
276 </p>
277 <p class="demo-page-step-try">
278 <span class="demo-page-try-label">Try this</span>
279 <a
280 class="demo-page-try-link"
281 href={`/${DEMO_USERNAME}/todo-api/issues/new`}
282 >
283 Open the new-issue page →
284 </a>
285 </p>
286 </li>
287
288 {/* Step 2 — Claude opens a PR */}
289 <li class="demo-page-step">
290 <div class="demo-page-step-head">
291 <span class="demo-page-step-num">2</span>
292 <span class="demo-page-step-icon" aria-hidden="true">
293 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
294 <circle cx="6" cy="6" r="3" />
295 <circle cx="6" cy="18" r="3" />
296 <line x1="6" y1="9" x2="6" y2="15" />
297 <path d="M18 9a9 9 0 0 0-9-9" />
298 <circle cx="18" cy="18" r="3" />
299 <line x1="18" y1="9" x2="18" y2="15" />
300 </svg>
301 </span>
302 <h3 class="demo-page-step-title">Claude opens a PR</h3>
303 </div>
304 <p class="demo-page-step-body">
305 The autopilot reads the issue, edits the repo, opens a
306 branch, and pushes a PR linked back to the issue. You
307 can see every PR Claude has opened in the tile below.
308 </p>
309 <p class="demo-page-step-try">
310 <span class="demo-page-try-label">Try this</span>
311 <a class="demo-page-try-link" href="#tile-queued-h">
312 See the queue ↓
313 </a>
314 </p>
315 </li>
316
317 {/* Step 3 — AI review */}
318 <li class="demo-page-step">
319 <div class="demo-page-step-head">
320 <span class="demo-page-step-num">3</span>
321 <span class="demo-page-step-icon" aria-hidden="true">
322 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
323 <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
324 </svg>
325 </span>
326 <h3 class="demo-page-step-title">AI review lands</h3>
327 </div>
328 <p class="demo-page-step-body">
329 Every PR gets a second-AI review pass — typed comments
330 with line numbers, severity, and a one-line summary at
331 the top. No human needed for the routine stuff.
332 </p>
333 <p class="demo-page-step-try">
334 <span class="demo-page-try-label">Try this</span>
335 <a class="demo-page-try-link" href="#tile-reviews-h">
336 Read today's reviews ↓
337 </a>
338 </p>
339 </li>
340
341 {/* Step 4 — Auto-merge */}
342 <li class="demo-page-step">
343 <div class="demo-page-step-head">
344 <span class="demo-page-step-num">4</span>
345 <span class="demo-page-step-icon" aria-hidden="true">
346 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
347 <path d="M20 6L9 17l-5-5" />
348 </svg>
349 </span>
350 <h3 class="demo-page-step-title">Auto-merge when green</h3>
351 </div>
352 <p class="demo-page-step-body">
353 Once every gate is green, the autopilot merges the PR
354 and closes the originating issue. Branch protection
355 rules still apply — Claude can't merge anything you
356 couldn't.
357 </p>
358 <p class="demo-page-step-try">
359 <span class="demo-page-try-label">Try this</span>
360 <a class="demo-page-try-link" href="#tile-merges-h">
361 Watch the auto-merge tile ↓
362 </a>
363 </p>
364 </li>
365 </ol>
236366 </div>
237 </div>
367 </section>
238368
239 <div class="demo-tiles">
240 <section class="demo-tile" aria-labelledby="tile-queued-h">
241 <h2 id="tile-queued-h" class="demo-tile-title">
369 {/* ─── Live tiles ─── */}
370 <div class="demo-page-tiles">
371 <section class="demo-page-tile" aria-labelledby="tile-queued-h">
372 <h2 id="tile-queued-h" class="demo-page-tile-title">
242373 Issues queued for AI build
243374 </h2>
244 <ul id="tile-queued-list" class="demo-list">
375 <ul id="tile-queued-list" class="demo-page-list">
245376 {queued.length === 0 ? (
246 <li class="demo-empty">No queued AI builds — quiet right now.</li>
377 <li class="demo-page-empty">No queued AI builds — quiet right now.</li>
247378 ) : (
248379 queued.map((i) => (
249380 <li>
250381 <a href={`/${DEMO_USERNAME}/${i.repo}/issues/${i.number}`}>
251382 #{i.number} {i.title}
252383 </a>{" "}
253 <span class="demo-meta">
384 <span class="demo-page-meta">
254385 {i.repo} · {relativeTime(i.createdAt)}
255386 </span>
256387 </li>
@@ -259,13 +390,13 @@ app.get("/demo", softAuth, async (c) => {
259390 </ul>
260391 </section>
261392
262 <section class="demo-tile" aria-labelledby="tile-merges-h">
263 <h2 id="tile-merges-h" class="demo-tile-title">
393 <section class="demo-page-tile" aria-labelledby="tile-merges-h">
394 <h2 id="tile-merges-h" class="demo-page-tile-title">
264395 PRs auto-merged in the last 24h
265396 </h2>
266 <ul id="tile-merges-list" class="demo-list">
397 <ul id="tile-merges-list" class="demo-page-list">
267398 {merges.length === 0 ? (
268 <li class="demo-empty">
399 <li class="demo-page-empty">
269400 No auto-merges in the last 24h.
270401 </li>
271402 ) : (
@@ -274,7 +405,7 @@ app.get("/demo", softAuth, async (c) => {
274405 <a href={`/${DEMO_USERNAME}/${m.repo}/pulls/${m.number}`}>
275406 #{m.number} {m.title}
276407 </a>{" "}
277 <span class="demo-meta">
408 <span class="demo-page-meta">
278409 {m.repo} · {relativeTime(m.mergedAt)}
279410 </span>
280411 </li>
@@ -283,17 +414,20 @@ app.get("/demo", softAuth, async (c) => {
283414 </ul>
284415 </section>
285416
286 <section class="demo-tile" aria-labelledby="tile-reviews-h">
287 <h2 id="tile-reviews-h" class="demo-tile-title">
417 <section class="demo-page-tile" aria-labelledby="tile-reviews-h">
418 <h2 id="tile-reviews-h" class="demo-page-tile-title">
288419 AI reviews posted today
289420 </h2>
290 <div class="demo-bigcount">
421 <div class="demo-page-bigcount">
291422 <span id="tile-reviews-count">{reviewCount}</span>
292 <span class="demo-bigcount-label">in the last 24h</span>
423 <span class="demo-page-bigcount-label">in the last 24h</span>
293424 </div>
294 <ul id="tile-reviews-list" class="demo-list demo-list-small">
425 <ul
426 id="tile-reviews-list"
427 class="demo-page-list demo-page-list-small"
428 >
295429 {reviews.length === 0 ? (
296 <li class="demo-empty">
430 <li class="demo-page-empty">
297431 No AI reviews in the last 24h.
298432 </li>
299433 ) : (
@@ -302,8 +436,8 @@ app.get("/demo", softAuth, async (c) => {
302436 <a href={`/${DEMO_USERNAME}/${r.repo}/pulls/${r.prNumber}`}>
303437 #{r.prNumber}
304438 </a>{" "}
305 <span class="demo-snippet">{r.commentSnippet}</span>{" "}
306 <span class="demo-meta">
439 <span class="demo-page-snippet">{r.commentSnippet}</span>{" "}
440 <span class="demo-page-meta">
307441 {r.repo} · {relativeTime(r.createdAt)}
308442 </span>
309443 </li>
@@ -313,50 +447,76 @@ app.get("/demo", softAuth, async (c) => {
313447 </section>
314448 </div>
315449
316 <section class="demo-repos" aria-labelledby="demo-repos-h">
317 <h2 id="demo-repos-h" class="demo-section-title">
318 Dig into the demo repos
319 </h2>
320 <div class="demo-repos-grid">
321 {demoRepos.map((r) => (
322 <a class="demo-repo-card" href={`${demoUserUrl}/${r.name}`}>
323 <div class="demo-repo-name">
324 {DEMO_USERNAME}/{r.name}
325 </div>
326 <div class="demo-repo-tag">{r.tagline}</div>
327 </a>
328 ))}
450 <section class="demo-page-section" aria-labelledby="demo-repos-h">
451 <header class="demo-page-section-head">
452 <div>
453 <p class="demo-page-section-eyebrow">Browse</p>
454 <h2 class="demo-page-section-title" id="demo-repos-h">
455 Dig into the demo repos
456 </h2>
457 </div>
458 </header>
459 <div class="demo-page-section-body">
460 <div class="demo-page-repos-grid">
461 {demoRepos.map((r) => (
462 <a class="demo-page-repo-card" href={`${demoUserUrl}/${r.name}`}>
463 <div class="demo-page-repo-name">
464 {DEMO_USERNAME}/{r.name}
465 </div>
466 <div class="demo-page-repo-tag">{r.tagline}</div>
467 </a>
468 ))}
469 </div>
329470 </div>
330471 </section>
331472
332 <section class="demo-feed" aria-labelledby="demo-feed-h">
333 <h2 id="demo-feed-h" class="demo-section-title">
334 Live activity
335 </h2>
336 <ul id="demo-feed-list" class="demo-feed-list">
337 {feed.length === 0 ? (
338 <li class="demo-empty">
339 Quiet right now — push a commit to a demo repo to see live updates.
340 </li>
341 ) : (
342 feed.map((e) => {
343 const path = e.ref.type === "pr" ? "pulls" : "issues";
344 return (
345 <li>
346 <span
347 class={`demo-kind demo-kind-${e.kind.replace(/\./g, "-")}`}
348 >
349 {activityLabel(e.kind)}
350 </span>{" "}
351 <a href={`/${DEMO_USERNAME}/${e.repo}/${path}/${e.ref.number}`}>
352 {e.repo} #{e.ref.number}
353 </a>{" "}
354 <span class="demo-meta">{relativeTime(e.at)}</span>
355 </li>
356 );
357 })
358 )}
359 </ul>
473 <section class="demo-page-section" aria-labelledby="demo-feed-h">
474 <header class="demo-page-section-head">
475 <div>
476 <p class="demo-page-section-eyebrow">Stream</p>
477 <h2 class="demo-page-section-title" id="demo-feed-h">
478 Live activity
479 </h2>
480 <p class="demo-page-section-sub">
481 Newest event first. Auto-refreshes every 30s.
482 </p>
483 </div>
484 <button
485 type="button"
486 id="demo-reset"
487 class="demo-page-btn demo-page-reset"
488 aria-label="Refresh all tiles now"
489 >
490 Refresh now
491 </button>
492 </header>
493 <div class="demo-page-section-body">
494 <ul id="demo-feed-list" class="demo-page-feed-list">
495 {feed.length === 0 ? (
496 <li class="demo-page-empty">
497 Quiet right now — push a commit to a demo repo to see
498 live updates.
499 </li>
500 ) : (
501 feed.map((e) => {
502 const path = e.ref.type === "pr" ? "pulls" : "issues";
503 return (
504 <li>
505 <span
506 class={`demo-page-kind demo-page-kind-${e.kind.replace(/\./g, "-")}`}
507 >
508 {activityLabel(e.kind)}
509 </span>{" "}
510 <a href={`/${DEMO_USERNAME}/${e.repo}/${path}/${e.ref.number}`}>
511 {e.repo} #{e.ref.number}
512 </a>{" "}
513 <span class="demo-page-meta">{relativeTime(e.at)}</span>
514 </li>
515 );
516 })
517 )}
518 </ul>
519 </div>
360520 </section>
361521 </div>
362522 <script dangerouslySetInnerHTML={{ __html: pollerScript }} />
@@ -364,46 +524,417 @@ app.get("/demo", softAuth, async (c) => {
364524 );
365525});
366526
367// Minimal CSS, scoped under .demo-page so it doesn't bleed into other views.
527// Scoped CSS — every class prefixed `.demo-page-` so this surface can't
528// bleed into other views. Drop-in replacement for the legacy `.demo-*`
529// classes; the new prefix is wider so we can polish without collisions.
368530const DEMO_CSS = `
369.demo-page { max-width: 1100px; margin: 0 auto; padding: 32px 20px 64px; }
370.demo-hero { display: flex; flex-wrap: wrap; gap: 24px; align-items: flex-start; justify-content: space-between; margin-bottom: 32px; }
371.demo-hero-inner { flex: 1 1 480px; }
372.demo-pulse { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #34d399; box-shadow: 0 0 8px rgba(52,211,153,0.7); margin-right: 8px; vertical-align: middle; animation: demo-pulse 1.6s infinite; }
373@keyframes demo-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
374.demo-title { font-size: 36px; line-height: 1.1; margin: 12px 0 16px; }
375.demo-sub { color: var(--text-muted); max-width: 640px; font-size: 15px; line-height: 1.55; }
376.demo-sub code { background: var(--bg-secondary); padding: 1px 6px; border-radius: 4px; font-size: 13px; }
377.demo-hero-cta { flex: 0 0 auto; }
378.demo-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-bottom: 32px; }
379.demo-tile { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 18px; }
380.demo-tile-title { font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); margin: 0 0 12px; }
381.demo-list { list-style: none; margin: 0; padding: 0; font-size: 14px; line-height: 1.5; }
382.demo-list li { padding: 6px 0; border-bottom: 1px solid var(--border); }
383.demo-list li:last-child { border-bottom: 0; }
384.demo-list-small li { font-size: 13px; }
385.demo-list a { color: var(--text-strong); text-decoration: none; }
386.demo-list a:hover { text-decoration: underline; }
387.demo-meta { color: var(--text-muted); font-size: 12px; }
388.demo-snippet { color: var(--text-muted); font-style: italic; font-size: 12px; }
389.demo-empty { color: var(--text-muted); font-size: 13px; padding: 6px 0; }
390.demo-bigcount { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }
391.demo-bigcount span:first-child { font-size: 32px; font-weight: 700; color: var(--text-strong); }
392.demo-bigcount-label { color: var(--text-muted); font-size: 12px; }
393.demo-section-title { font-size: 18px; margin: 16px 0 12px; }
394.demo-repos { margin-bottom: 32px; }
395.demo-repos-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
396.demo-repo-card { display: block; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; text-decoration: none; color: inherit; transition: border-color 0.15s; }
397.demo-repo-card:hover { border-color: var(--accent, #8c6dff); }
398.demo-repo-name { font-weight: 600; font-size: 14px; color: var(--text-strong); margin-bottom: 4px; }
399.demo-repo-tag { font-size: 12px; color: var(--text-muted); }
400.demo-feed { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 18px; }
401.demo-feed-list { list-style: none; margin: 0; padding: 0; font-size: 13px; line-height: 1.6; }
402.demo-feed-list li { padding: 4px 0; }
403.demo-kind { display: inline-block; padding: 1px 7px; border-radius: 9999px; font-size: 11px; font-weight: 500; }
404.demo-kind-auto_merge-merged, .demo-kind-auto-merge-merged { background: rgba(52,211,153,0.12); color: #34d399; }
405.demo-kind-ai_build-dispatched, .demo-kind-ai-build-dispatched { background: rgba(140,109,255,0.15); color: #8c6dff; }
406.demo-kind-ai_review-posted, .demo-kind-ai-review-posted { background: rgba(54,197,214,0.15); color: #36c5d6; }
531 .demo-page { max-width: 1100px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
532
533 /* ─── Hero ─── */
534 .demo-page-hero {
535 position: relative;
536 margin-bottom: var(--space-5);
537 padding: var(--space-6) var(--space-6);
538 background: var(--bg-elevated);
539 border: 1px solid var(--border);
540 border-radius: 18px;
541 overflow: hidden;
542 }
543 .demo-page-hero::before {
544 content: '';
545 position: absolute;
546 top: 0; left: 0; right: 0;
547 height: 2px;
548 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
549 opacity: 0.7;
550 pointer-events: none;
551 }
552 .demo-page-hero-orb {
553 position: absolute;
554 inset: -25% -10% auto auto;
555 width: 480px; height: 480px;
556 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.10) 45%, transparent 70%);
557 filter: blur(80px);
558 opacity: 0.7;
559 pointer-events: none;
560 z-index: 0;
561 }
562 .demo-page-hero-inner { position: relative; z-index: 1; max-width: 720px; }
563 .demo-page-eyebrow {
564 font-size: 12px;
565 color: var(--text-muted);
566 margin-bottom: var(--space-2);
567 letter-spacing: 0.02em;
568 display: inline-flex;
569 align-items: center;
570 gap: 8px;
571 }
572 .demo-page-pulse {
573 display: inline-block;
574 width: 8px;
575 height: 8px;
576 border-radius: 50%;
577 background: #34d399;
578 box-shadow: 0 0 8px rgba(52,211,153,0.7);
579 animation: demo-page-pulse 1.6s infinite;
580 }
581 @keyframes demo-page-pulse {
582 0%, 100% { opacity: 1; }
583 50% { opacity: 0.4; }
584 }
585 .demo-page-title {
586 font-size: clamp(32px, 5vw, 52px);
587 font-family: var(--font-display);
588 font-weight: 800;
589 letter-spacing: -0.028em;
590 line-height: 1.05;
591 margin: 0 0 var(--space-3);
592 color: var(--text-strong);
593 }
594 .demo-page-title-grad {
595 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
596 -webkit-background-clip: text;
597 background-clip: text;
598 -webkit-text-fill-color: transparent;
599 color: transparent;
600 }
601 .demo-page-sub {
602 font-size: 16px;
603 color: var(--text-muted);
604 margin: 0 0 var(--space-4);
605 line-height: 1.55;
606 max-width: 620px;
607 }
608 .demo-page-hero-cta { display: flex; gap: 10px; flex-wrap: wrap; }
609
610 /* ─── Buttons ─── */
611 .demo-page-btn {
612 appearance: none;
613 border: 1px solid var(--border-strong);
614 background: var(--bg-secondary);
615 color: var(--text);
616 padding: 10px 16px;
617 border-radius: 10px;
618 font-family: inherit;
619 font-size: 14px;
620 font-weight: 500;
621 cursor: pointer;
622 transition: border-color 150ms ease, background 150ms ease, transform 150ms ease, color 150ms ease;
623 text-decoration: none;
624 display: inline-flex; align-items: center; gap: 8px;
625 }
626 .demo-page-btn:hover {
627 border-color: var(--border-focus);
628 background: rgba(255,255,255,0.03);
629 transform: translateY(-1px);
630 text-decoration: none;
631 }
632 .demo-page-btn-primary {
633 border-color: rgba(140,109,255,0.45);
634 background: linear-gradient(135deg, rgba(140,109,255,0.20), rgba(54,197,214,0.14));
635 color: var(--text-strong);
636 }
637 .demo-page-btn-primary:hover {
638 border-color: rgba(140,109,255,0.65);
639 background: linear-gradient(135deg, rgba(140,109,255,0.28), rgba(54,197,214,0.20));
640 }
641 .demo-page-reset {
642 padding: 6px 12px;
643 font-size: 12.5px;
644 border-radius: 8px;
645 }
646 .demo-page-reset.is-done {
647 border-color: rgba(52,211,153,0.45);
648 background: rgba(52,211,153,0.10);
649 color: #6ee7b7;
650 }
651
652 /* ─── Section cards ─── */
653 .demo-page-section {
654 margin-bottom: var(--space-5);
655 background: var(--bg-elevated);
656 border: 1px solid var(--border);
657 border-radius: 14px;
658 overflow: hidden;
659 }
660 .demo-page-section-head {
661 padding: var(--space-4) var(--space-5);
662 border-bottom: 1px solid var(--border);
663 display: flex;
664 align-items: flex-start;
665 justify-content: space-between;
666 gap: var(--space-3);
667 flex-wrap: wrap;
668 }
669 .demo-page-section-eyebrow {
670 font-size: 11px;
671 font-weight: 600;
672 letter-spacing: 0.08em;
673 text-transform: uppercase;
674 color: var(--text-faint);
675 margin: 0 0 6px;
676 }
677 .demo-page-section-title {
678 margin: 0;
679 font-family: var(--font-display);
680 font-size: 17px;
681 font-weight: 700;
682 letter-spacing: -0.018em;
683 color: var(--text-strong);
684 }
685 .demo-page-section-sub {
686 margin: 6px 0 0;
687 font-size: 12.5px;
688 color: var(--text-muted);
689 line-height: 1.5;
690 }
691 .demo-page-section-body { padding: var(--space-5); }
692
693 /* ─── 4-step walkthrough ─── */
694 .demo-page-walk {
695 list-style: none;
696 margin: 0;
697 padding: 0;
698 display: grid;
699 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
700 gap: var(--space-3);
701 }
702 .demo-page-step {
703 padding: var(--space-4);
704 background: var(--bg-secondary);
705 border: 1px solid var(--border-subtle);
706 border-radius: 12px;
707 display: flex;
708 flex-direction: column;
709 gap: 10px;
710 transition: border-color 150ms ease, transform 150ms ease;
711 }
712 .demo-page-step:hover {
713 border-color: var(--border-strong);
714 transform: translateY(-1px);
715 }
716 .demo-page-step-head {
717 display: flex;
718 align-items: center;
719 gap: 10px;
720 }
721 .demo-page-step-num {
722 display: inline-flex;
723 align-items: center;
724 justify-content: center;
725 width: 28px; height: 28px;
726 border-radius: 50%;
727 background: linear-gradient(135deg, rgba(140,109,255,0.22), rgba(54,197,214,0.16));
728 color: #c5b3ff;
729 border: 1px solid rgba(140,109,255,0.40);
730 font-family: var(--font-display);
731 font-weight: 700;
732 font-size: 13px;
733 }
734 .demo-page-step-icon {
735 display: inline-flex;
736 align-items: center;
737 justify-content: center;
738 width: 26px; height: 26px;
739 border-radius: 8px;
740 background: rgba(54,197,214,0.10);
741 color: #5fd3e0;
742 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.30);
743 }
744 .demo-page-step-title {
745 margin: 0;
746 font-family: var(--font-display);
747 font-size: 15px;
748 font-weight: 700;
749 color: var(--text-strong);
750 letter-spacing: -0.012em;
751 }
752 .demo-page-step-body {
753 margin: 0;
754 font-size: 13.5px;
755 color: var(--text-muted);
756 line-height: 1.55;
757 }
758 .demo-page-step-body code {
759 font-family: var(--font-mono);
760 font-size: 12px;
761 background: var(--bg-tertiary);
762 padding: 1px 5px;
763 border-radius: 4px;
764 color: var(--text-strong);
765 }
766 .demo-page-step-try {
767 margin: auto 0 0;
768 padding-top: 10px;
769 border-top: 1px dashed var(--border-subtle);
770 display: flex;
771 align-items: center;
772 justify-content: space-between;
773 gap: 8px;
774 font-size: 12.5px;
775 }
776 .demo-page-try-label {
777 font-size: 10.5px;
778 font-weight: 600;
779 letter-spacing: 0.08em;
780 text-transform: uppercase;
781 color: var(--text-faint);
782 }
783 .demo-page-try-link {
784 color: var(--accent, #8c6dff);
785 text-decoration: none;
786 font-weight: 600;
787 }
788 .demo-page-try-link:hover { text-decoration: underline; }
789
790 /* ─── Live tiles ─── */
791 .demo-page-tiles {
792 display: grid;
793 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
794 gap: var(--space-3);
795 margin-bottom: var(--space-5);
796 }
797 .demo-page-tile {
798 background: var(--bg-elevated);
799 border: 1px solid var(--border);
800 border-radius: 14px;
801 padding: var(--space-4) var(--space-4);
802 }
803 .demo-page-tile-title {
804 font-size: 11px;
805 font-weight: 600;
806 text-transform: uppercase;
807 letter-spacing: 0.08em;
808 color: var(--text-faint);
809 margin: 0 0 12px;
810 }
811 .demo-page-list {
812 list-style: none;
813 margin: 0;
814 padding: 0;
815 font-size: 14px;
816 line-height: 1.5;
817 }
818 .demo-page-list li {
819 padding: 6px 0;
820 border-bottom: 1px solid var(--border-subtle);
821 }
822 .demo-page-list li:last-child { border-bottom: 0; }
823 .demo-page-list-small li { font-size: 13px; }
824 .demo-page-list a {
825 color: var(--text-strong);
826 text-decoration: none;
827 font-weight: 500;
828 }
829 .demo-page-list a:hover {
830 color: var(--accent, #8c6dff);
831 text-decoration: underline;
832 }
833 .demo-page-meta { color: var(--text-muted); font-size: 12px; }
834 .demo-page-snippet { color: var(--text-muted); font-style: italic; font-size: 12px; }
835 .demo-page-empty { color: var(--text-muted); font-size: 13px; padding: 6px 0; }
836 .demo-page-bigcount {
837 display: flex;
838 align-items: baseline;
839 gap: 8px;
840 margin-bottom: 12px;
841 }
842 .demo-page-bigcount span:first-child {
843 font-size: 32px;
844 font-weight: 700;
845 color: var(--text-strong);
846 font-family: var(--font-display);
847 letter-spacing: -0.02em;
848 }
849 .demo-page-bigcount-label { color: var(--text-muted); font-size: 12px; }
850
851 /* ─── Demo repos grid ─── */
852 .demo-page-repos-grid {
853 display: grid;
854 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
855 gap: var(--space-3);
856 }
857 .demo-page-repo-card {
858 display: block;
859 background: var(--bg-secondary);
860 border: 1px solid var(--border-subtle);
861 border-radius: 12px;
862 padding: var(--space-3) var(--space-4);
863 text-decoration: none;
864 color: inherit;
865 transition: border-color 150ms ease, transform 150ms ease;
866 }
867 .demo-page-repo-card:hover {
868 border-color: rgba(140,109,255,0.45);
869 transform: translateY(-1px);
870 text-decoration: none;
871 }
872 .demo-page-repo-name {
873 font-family: var(--font-mono);
874 font-weight: 600;
875 font-size: 13.5px;
876 color: var(--text-strong);
877 margin-bottom: 4px;
878 }
879 .demo-page-repo-tag {
880 font-size: 12.5px;
881 color: var(--text-muted);
882 line-height: 1.45;
883 }
884
885 /* ─── Live feed ─── */
886 .demo-page-feed-list {
887 list-style: none;
888 margin: 0;
889 padding: 0;
890 font-size: 13px;
891 line-height: 1.6;
892 }
893 .demo-page-feed-list li {
894 padding: 6px 0;
895 border-bottom: 1px solid var(--border-subtle);
896 display: flex;
897 align-items: center;
898 gap: 8px;
899 flex-wrap: wrap;
900 }
901 .demo-page-feed-list li:last-child { border-bottom: 0; }
902 .demo-page-feed-list a {
903 color: var(--text-strong);
904 text-decoration: none;
905 font-weight: 500;
906 }
907 .demo-page-feed-list a:hover {
908 color: var(--accent, #8c6dff);
909 text-decoration: underline;
910 }
911 .demo-page-kind {
912 display: inline-flex;
913 align-items: center;
914 padding: 2px 9px;
915 border-radius: 9999px;
916 font-size: 11px;
917 font-weight: 600;
918 letter-spacing: 0.01em;
919 }
920 .demo-page-kind-auto_merge-merged,
921 .demo-page-kind-auto-merge-merged {
922 background: rgba(52,211,153,0.12);
923 color: #34d399;
924 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.28);
925 }
926 .demo-page-kind-ai_build-dispatched,
927 .demo-page-kind-ai-build-dispatched {
928 background: rgba(140,109,255,0.15);
929 color: #b69dff;
930 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.32);
931 }
932 .demo-page-kind-ai_review-posted,
933 .demo-page-kind-ai-review-posted {
934 background: rgba(54,197,214,0.15);
935 color: #5fd3e0;
936 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.32);
937 }
407938`;
408939
409940export default app;
Modifiedsrc/routes/semantic-search.tsx+483−117View fileUnifiedSplit
@@ -7,6 +7,15 @@
77 * Intentionally tolerates a missing DB / missing repo / missing index so the
88 * page is always navigable. When there's no index yet, the page shows a
99 * "Build index" CTA pointing at the reindex endpoint.
10 *
11 * 2026 polish:
12 * - Scoped `.ss-*` CSS — sits below RepoHeader + IssueNav.
13 * - Eyebrow + display headline + 1-line subtitle.
14 * - Prominent search input w/ focus ring + gradient submit button.
15 * - Result cards show file:line in mono, snippet, and match score chip.
16 * - Dashed empty state w/ orb + Build/Reindex CTA when no index exists.
17 *
18 * All query strings + POST handlers preserved verbatim.
1019 */
1120
1221import { Hono } from "hono";
@@ -32,6 +41,315 @@ import {
3241const semanticSearch = new Hono<AuthEnv>();
3342semanticSearch.use("*", softAuth);
3443
44// ─── Scoped CSS (.ss-*) ──────────────────────────────────────────────────
45const ssStyles = `
46 .ss-wrap { max-width: 1100px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
47
48 .ss-head {
49 margin-bottom: var(--space-5);
50 display: flex;
51 align-items: flex-end;
52 justify-content: space-between;
53 gap: var(--space-4);
54 flex-wrap: wrap;
55 }
56 .ss-head-text { flex: 1; min-width: 280px; }
57 .ss-eyebrow {
58 display: inline-flex;
59 align-items: center;
60 gap: 8px;
61 text-transform: uppercase;
62 font-family: var(--font-mono);
63 font-size: 11px;
64 letter-spacing: 0.16em;
65 color: var(--text-muted);
66 font-weight: 600;
67 margin-bottom: 10px;
68 }
69 .ss-eyebrow-dot {
70 width: 8px; height: 8px;
71 border-radius: 9999px;
72 background: linear-gradient(135deg, #8c6dff, #36c5d6);
73 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
74 }
75 .ss-title {
76 font-family: var(--font-display);
77 font-size: clamp(24px, 3.4vw, 36px);
78 font-weight: 800;
79 letter-spacing: -0.028em;
80 line-height: 1.1;
81 margin: 0 0 6px;
82 color: var(--text-strong);
83 }
84 .ss-title-grad {
85 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
86 -webkit-background-clip: text;
87 background-clip: text;
88 -webkit-text-fill-color: transparent;
89 color: transparent;
90 }
91 .ss-sub {
92 margin: 0;
93 font-size: 14px;
94 color: var(--text-muted);
95 line-height: 1.5;
96 max-width: 720px;
97 }
98
99 .ss-provider {
100 display: inline-flex;
101 align-items: center;
102 gap: 6px;
103 padding: 5px 10px;
104 border-radius: 9999px;
105 font-family: var(--font-mono);
106 font-size: 11px;
107 color: var(--text-muted);
108 background: rgba(255,255,255,0.03);
109 border: 1px solid var(--border);
110 }
111 .ss-provider .dot {
112 width: 6px; height: 6px;
113 border-radius: 9999px;
114 background: linear-gradient(135deg, #8c6dff, #36c5d6);
115 }
116
117 .ss-banner {
118 margin-bottom: var(--space-4);
119 padding: 10px 14px;
120 border-radius: 10px;
121 font-size: 13.5px;
122 border: 1px solid var(--border);
123 background: rgba(255,255,255,0.025);
124 color: var(--text);
125 display: flex;
126 align-items: center;
127 gap: 10px;
128 }
129 .ss-banner.is-ok { border-color: rgba(52,211,153,0.40); background: rgba(52,211,153,0.08); color: #bbf7d0; }
130 .ss-banner-dot { width: 8px; height: 8px; border-radius: 9999px; background: currentColor; flex-shrink: 0; }
131
132 /* ─── Search bar ─── */
133 .ss-search {
134 display: flex;
135 gap: 10px;
136 align-items: stretch;
137 margin-bottom: var(--space-4);
138 }
139 .ss-search-input-wrap { position: relative; flex: 1; }
140 .ss-search-icon {
141 position: absolute;
142 top: 50%;
143 left: 14px;
144 transform: translateY(-50%);
145 color: var(--text-muted);
146 pointer-events: none;
147 }
148 .ss-search-input {
149 width: 100%;
150 box-sizing: border-box;
151 padding: 12px 14px 12px 40px;
152 font: inherit;
153 font-size: 14.5px;
154 color: var(--text);
155 background: rgba(255,255,255,0.03);
156 border: 1px solid var(--border-strong);
157 border-radius: 12px;
158 outline: none;
159 transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
160 }
161 .ss-search-input:focus {
162 border-color: rgba(140,109,255,0.55);
163 background: rgba(255,255,255,0.05);
164 box-shadow: 0 0 0 3px rgba(140,109,255,0.20);
165 }
166 .ss-btn {
167 display: inline-flex;
168 align-items: center;
169 justify-content: center;
170 gap: 6px;
171 padding: 0 18px;
172 border-radius: 12px;
173 font-size: 14px;
174 font-weight: 600;
175 text-decoration: none;
176 border: 1px solid transparent;
177 cursor: pointer;
178 font: inherit;
179 transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease, border-color 120ms ease;
180 line-height: 1;
181 white-space: nowrap;
182 }
183 .ss-btn-primary {
184 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
185 color: #ffffff;
186 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.55), inset 0 1px 0 rgba(255,255,255,0.16);
187 }
188 .ss-btn-primary:hover {
189 transform: translateY(-1px);
190 box-shadow: 0 10px 24px -8px rgba(140,109,255,0.65), inset 0 1px 0 rgba(255,255,255,0.20);
191 text-decoration: none;
192 color: #ffffff;
193 }
194 .ss-btn-ghost {
195 background: transparent;
196 color: var(--text);
197 border-color: var(--border-strong);
198 padding: 7px 12px;
199 font-size: 12.5px;
200 border-radius: 9px;
201 }
202 .ss-btn-ghost:hover {
203 background: rgba(140,109,255,0.06);
204 border-color: rgba(140,109,255,0.45);
205 color: var(--text-strong);
206 text-decoration: none;
207 }
208
209 /* ─── Index status bar ─── */
210 .ss-status {
211 display: flex;
212 align-items: center;
213 justify-content: space-between;
214 gap: var(--space-3);
215 flex-wrap: wrap;
216 margin-bottom: var(--space-3);
217 padding: 9px 14px;
218 border-radius: 10px;
219 background: rgba(255,255,255,0.025);
220 border: 1px solid var(--border);
221 font-size: 12px;
222 color: var(--text-muted);
223 font-variant-numeric: tabular-nums;
224 }
225 .ss-status .num { color: var(--text-strong); font-weight: 600; }
226
227 /* ─── Result cards ─── */
228 .ss-results { display: flex; flex-direction: column; gap: 10px; }
229 .ss-result {
230 padding: 14px;
231 background: var(--bg-elevated);
232 border: 1px solid var(--border);
233 border-radius: 12px;
234 transition: border-color 120ms ease, background 120ms ease;
235 }
236 .ss-result:hover {
237 border-color: var(--border-strong);
238 background: rgba(255,255,255,0.025);
239 }
240 .ss-result-head {
241 display: flex;
242 align-items: center;
243 justify-content: space-between;
244 gap: 10px;
245 flex-wrap: wrap;
246 margin-bottom: 10px;
247 }
248 .ss-result-path {
249 font-family: var(--font-mono);
250 font-size: 13px;
251 font-weight: 600;
252 color: var(--text-strong);
253 text-decoration: none;
254 word-break: break-all;
255 letter-spacing: -0.005em;
256 }
257 .ss-result-path .lines { color: var(--text-muted); font-weight: 500; }
258 .ss-result-path:hover { color: #c4b5fd; text-decoration: none; }
259 .ss-score {
260 display: inline-flex;
261 align-items: center;
262 gap: 5px;
263 padding: 2px 9px;
264 border-radius: 9999px;
265 background: rgba(54,197,214,0.12);
266 color: #67e8f9;
267 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.30);
268 font-family: var(--font-mono);
269 font-size: 11px;
270 font-weight: 600;
271 font-variant-numeric: tabular-nums;
272 }
273 .ss-snippet {
274 margin: 0;
275 padding: 10px 12px;
276 background: rgba(0,0,0,0.25);
277 border: 1px solid var(--border);
278 border-radius: 8px;
279 font-family: var(--font-mono);
280 font-size: 12px;
281 line-height: 1.55;
282 color: var(--text);
283 overflow-x: auto;
284 white-space: pre-wrap;
285 word-break: break-word;
286 }
287
288 /* ─── Empty state ─── */
289 .ss-empty {
290 position: relative;
291 overflow: hidden;
292 padding: clamp(28px, 5vw, 52px) clamp(20px, 4vw, 40px);
293 text-align: center;
294 background: var(--bg-elevated);
295 border: 1px dashed var(--border-strong);
296 border-radius: 16px;
297 }
298 .ss-empty-orb {
299 position: absolute;
300 inset: -40% 25% auto 25%;
301 height: 300px;
302 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.10) 45%, transparent 70%);
303 filter: blur(72px);
304 opacity: 0.7;
305 pointer-events: none;
306 z-index: 0;
307 }
308 .ss-empty-inner { position: relative; z-index: 1; }
309 .ss-empty-icon {
310 width: 56px; height: 56px;
311 margin: 0 auto 14px;
312 border-radius: 9999px;
313 background: linear-gradient(135deg, rgba(140,109,255,0.25), rgba(54,197,214,0.20));
314 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.40);
315 display: inline-flex;
316 align-items: center;
317 justify-content: center;
318 color: #c4b5fd;
319 }
320 .ss-empty-title {
321 font-family: var(--font-display);
322 font-size: 18px;
323 font-weight: 700;
324 margin: 0 0 6px;
325 color: var(--text-strong);
326 }
327 .ss-empty-sub {
328 margin: 0 auto 16px;
329 font-size: 13.5px;
330 color: var(--text-muted);
331 max-width: 480px;
332 line-height: 1.5;
333 }
334`;
335
336function IconSearch() {
337 return (
338 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
339 <circle cx="11" cy="11" r="7" />
340 <line x1="21" y1="21" x2="16.65" y2="16.65" />
341 </svg>
342 );
343}
344function IconSparkles() {
345 return (
346 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
347 <path d="M12 3l1.7 4.6L18 9.3l-4.3 1.7L12 15l-1.7-4-4.3-1.7L10 7.6 12 3z" />
348 <path d="M19 13l.9 2.4L22 16l-2.1.6L19 19l-.9-2.4L16 16l2.1-.6L19 13z" />
349 </svg>
350 );
351}
352
35353async function resolveRepo(ownerName: string, repoName: string) {
36354 try {
37355 const [owner] = await db
@@ -85,7 +403,6 @@ semanticSearch.get("/:owner/:repo/search/semantic", async (c) => {
85403 let lastIndexedAt: Date | null = null;
86404 let indexedCommitSha: string | null = null;
87405 try {
88 // Grab the newest chunk row for metadata (sha + createdAt).
89406 const [newest] = await db
90407 .select({
91408 createdAt: codeChunks.createdAt,
@@ -98,7 +415,6 @@ semanticSearch.get("/:owner/:repo/search/semantic", async (c) => {
98415 if (newest) {
99416 lastIndexedAt = newest.createdAt as unknown as Date;
100417 indexedCommitSha = newest.commitSha || null;
101 // Rough count for the UI blurb — order of magnitude is fine.
102418 const rows = await db
103419 .select({ id: codeChunks.id })
104420 .from(codeChunks)
@@ -107,7 +423,7 @@ semanticSearch.get("/:owner/:repo/search/semantic", async (c) => {
107423 indexedCount = rows.length;
108424 }
109425 } catch {
110 // DB unavailable — show the page anyway so the URL always resolves.
426 // DB unavailable — show the page anyway.
111427 }
112428
113429 let hits: Awaited<ReturnType<typeof searchRepository>> = [];
@@ -136,127 +452,177 @@ semanticSearch.get("/:owner/:repo/search/semantic", async (c) => {
136452 <RepoHeader owner={ownerName} repo={repoName} />
137453 <IssueNav owner={ownerName} repo={repoName} active="code" />
138454
139 <div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px">
140 <h2 style="margin: 0">Semantic search</h2>
141 <div class="meta" style="font-size: 12px">
142 Provider: <strong>{providerLabel}</strong>
143 </div>
144 </div>
145
146 {flash && (
147 <div class="auth-success" style="margin-bottom: 16px">
148 {decodeURIComponent(flash)}
149 </div>
150 )}
151
152 <form
153 method="get"
154 action={`/${ownerName}/${repoName}/search/semantic`}
155 style="margin-bottom: 16px"
156 >
157 <input
158 type="search"
159 name="q"
160 value={q}
161 placeholder="Ask a question or describe what you're looking for…"
162 aria-label="Search"
163 style="width: 100%; padding: 10px 14px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px"
164 autofocus
165 />
166 </form>
167
168 {indexedCount === 0 ? (
169 <div class="empty-state">
170 <h3>No index yet</h3>
171 <p>
172 This repository hasn't been indexed for semantic search. Build the
173 index to enable AI-powered code lookup.
174 </p>
175 {isOwner ? (
176 <form
177 method="post"
178 action={`/${ownerName}/${repoName}/search/semantic/reindex`}
179 style="margin-top: 12px"
180 >
181 <button type="submit" class="btn btn-primary">
182 Build index
183 </button>
184 </form>
185 ) : (
186 <p class="meta" style="margin-top: 8px">
187 Only the repository owner can trigger indexing.
455 <div class="ss-wrap">
456 <header class="ss-head">
457 <div class="ss-head-text">
458 <div class="ss-eyebrow">
459 <span class="ss-eyebrow-dot" aria-hidden="true" />
460 Repository · Semantic search
461 </div>
462 <h1 class="ss-title">
463 <span class="ss-title-grad">Ask the codebase anything.</span>
464 </h1>
465 <p class="ss-sub">
466 Embeddings-powered code search across every indexed chunk —
467 describe what you're looking for in natural language.
188468 </p>
189 )}
190 </div>
191 ) : (
192 <>
193 <div
194 class="meta"
195 style="font-size: 12px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center"
196 >
197 <span>
198 {indexedCount} chunk{indexedCount === 1 ? "" : "s"} indexed
199 {lastIndexedAt && (
200 <>
201 {" · last indexed "}
202 {new Date(lastIndexedAt).toLocaleString()}
203 </>
204 )}
469 </div>
470 <div class="ss-provider" title="Active embeddings provider">
471 <span class="dot" aria-hidden="true" />
472 {providerLabel}
473 </div>
474 </header>
475
476 {flash && (
477 <div class="ss-banner is-ok" role="status">
478 <span class="ss-banner-dot" aria-hidden="true" />
479 {decodeURIComponent(flash)}
480 </div>
481 )}
482
483 <form
484 method="get"
485 action={`/${ownerName}/${repoName}/search/semantic`}
486 class="ss-search"
487 >
488 <div class="ss-search-input-wrap">
489 <span class="ss-search-icon" aria-hidden="true">
490 <IconSearch />
205491 </span>
206 {isOwner && (
207 <form
208 method="post"
209 action={`/${ownerName}/${repoName}/search/semantic/reindex`}
210 style="display: inline"
211 >
212 <button type="submit" class="btn btn-sm">
213 Reindex
214 </button>
215 </form>
216 )}
492 <input
493 type="search"
494 name="q"
495 value={q}
496 placeholder="Ask a question or describe what you're looking for…"
497 aria-label="Search"
498 class="ss-search-input"
499 autofocus
500 />
217501 </div>
502 <button type="submit" class="ss-btn ss-btn-primary">
503 <IconSparkles />
504 Search
505 </button>
506 </form>
218507
219 {!q ? (
220 <div class="empty-state">
221 <p>Type a query to search across this repo's code.</p>
508 {indexedCount === 0 ? (
509 <div class="ss-empty">
510 <div class="ss-empty-orb" aria-hidden="true" />
511 <div class="ss-empty-inner">
512 <div class="ss-empty-icon" aria-hidden="true">
513 <IconSparkles />
514 </div>
515 <h3 class="ss-empty-title">No index yet</h3>
516 <p class="ss-empty-sub">
517 This repository hasn't been indexed for semantic search. Build
518 the index to enable AI-powered code lookup.
519 </p>
520 {isOwner ? (
521 <form
522 method="post"
523 action={`/${ownerName}/${repoName}/search/semantic/reindex`}
524 >
525 <button type="submit" class="ss-btn ss-btn-primary">
526 <IconSparkles />
527 Build index
528 </button>
529 </form>
530 ) : (
531 <p
532 style="margin:0;color:var(--text-muted);font-size:13px"
533 >
534 Only the repository owner can trigger indexing.
535 </p>
536 )}
222537 </div>
223 ) : hits.length === 0 ? (
224 <div class="empty-state">
225 <p>No results for "{q}"</p>
538 </div>
539 ) : (
540 <>
541 <div class="ss-status">
542 <span>
543 <span class="num">{indexedCount}</span> chunk
544 {indexedCount === 1 ? "" : "s"} indexed
545 {lastIndexedAt && (
546 <>
547 {" · last indexed "}
548 <span class="num">
549 {new Date(lastIndexedAt).toLocaleString()}
550 </span>
551 </>
552 )}
553 </span>
554 {isOwner && (
555 <form
556 method="post"
557 action={`/${ownerName}/${repoName}/search/semantic/reindex`}
558 style="display:inline"
559 >
560 <button type="submit" class="ss-btn ss-btn-ghost">
561 Reindex
562 </button>
563 </form>
564 )}
226565 </div>
227 ) : (
228 <div class="panel">
229 {hits.map((h) => {
230 const href = `/${ownerName}/${repoName}/blob/${refForLinks}/${h.path}#L${h.startLine}`;
231 const preview =
232 h.content.length > 600
233 ? h.content.slice(0, 600) + "\n…"
234 : h.content;
235 return (
236 <div class="panel-item" style="flex-direction: column; align-items: stretch">
237 <div
238 style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px"
239 >
240 <a href={href} style="font-weight: 600">
241 {h.path}
242 </a>
243 <span class="meta" style="font-size: 11px">
244 lines {h.startLine}–{h.endLine} · score{" "}
245 {h.score.toFixed(3)}
246 </span>
247 </div>
248 <pre
249 style="margin: 0; padding: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; font-size: 12px; overflow-x: auto; white-space: pre-wrap"
250 >
251 {preview}
252 </pre>
566
567 {!q ? (
568 <div class="ss-empty">
569 <div class="ss-empty-orb" aria-hidden="true" />
570 <div class="ss-empty-inner">
571 <div class="ss-empty-icon" aria-hidden="true">
572 <IconSearch />
253573 </div>
254 );
255 })}
256 </div>
257 )}
258 </>
259 )}
574 <h3 class="ss-empty-title">Type a query to begin</h3>
575 <p class="ss-empty-sub">
576 Search across this repo's code — try a function name, a
577 file path, or a plain-English description of what you're
578 looking for.
579 </p>
580 </div>
581 </div>
582 ) : hits.length === 0 ? (
583 <div class="ss-empty">
584 <div class="ss-empty-orb" aria-hidden="true" />
585 <div class="ss-empty-inner">
586 <div class="ss-empty-icon" aria-hidden="true">
587 <IconSearch />
588 </div>
589 <h3 class="ss-empty-title">No results for "{q}"</h3>
590 <p class="ss-empty-sub">
591 Try a different phrasing or a related symbol name.
592 </p>
593 </div>
594 </div>
595 ) : (
596 <div class="ss-results">
597 {hits.map((h) => {
598 const href = `/${ownerName}/${repoName}/blob/${refForLinks}/${h.path}#L${h.startLine}`;
599 const preview =
600 h.content.length > 600
601 ? h.content.slice(0, 600) + "\n…"
602 : h.content;
603 return (
604 <div class="ss-result">
605 <div class="ss-result-head">
606 <a href={href} class="ss-result-path">
607 {h.path}
608 <span class="lines">
609 :{h.startLine}–{h.endLine}
610 </span>
611 </a>
612 <span class="ss-score" title="Cosine similarity score">
613 score {h.score.toFixed(3)}
614 </span>
615 </div>
616 <pre class="ss-snippet">{preview}</pre>
617 </div>
618 );
619 })}
620 </div>
621 )}
622 </>
623 )}
624 </div>
625 <style dangerouslySetInnerHTML={{ __html: ssStyles }} />
260626 </Layout>
261627 );
262628});
263629