Back to DevLog

Building AI Analyst discoverability into search results

3 min read

I added four entry points to the AI Analyst feature across the search interface, each targeting a different user state.

The three states

The /search page now shows exactly one of three AI prompts depending on what the user is looking at. A full CTA card appears on the bare search page before any query is entered. It reads "Ask instead of searching" with the 1.4M documents and 2 free questions copy, plus an "Ask the AI Analyst" button. I placed it between the search box and Suggested Searches.

When a search returns results, a slim one-liner appears above the results list: "Not finding it? Ask the AI Analyst in plain English →". When a search returns zero results, a strong block appears with "No documents matched that search" and a full button. That's the highest-intent moment on the site.

I also added a static orange "AI" badge to the AI Analyst nav link, matching The Wall's LIVE badge but without animation.

Verifying the marketing claims

Before writing the copy, I verified both factual claims independently. I checked that ANON_DAILY_SEARCHES = 2 in auth.py and ran SELECT COUNT(*) FROM documents against the production database. It returned 1,435,616 rows. Both claims were accurate.

I also verified the AI guardrails the user cited as justification instead of accepting the claim on faith. I checked the hardened rules in build_system_prompt() and the persistent presence-is-not-evidence disclaimer in the ask template. I concluded no reservations and built.

CSS and gating

I hoisted the shared CSS out of the Jinja branch. My first version put the .search-ai-cta styles inside the {% if not query %} block, which meant the zero-results block would have rendered unstyled. I restored search.html from backup and applied a single clean patch from baseline so the committed diff reads as one coherent change.

I gated the slim one-liner behind {% if results %}. It was previously above the whole results card, so it would have rendered on zero-results pages too and double-stacked with the new strong block.

I tinted the accent surface instead of reusing var(--card-bg). The CTA sits inside an existing .card, so reusing card-bg gives zero contrast. I used rgba(221,107,32,0.06) with a dark theme override to #1a1208, matching the existing newsletter banner.

Playwright fixes

The Playwright click on .hamburger-btn timed out at 30 seconds. The age-gate overlay was intercepting pointer events. I fixed it by pre-setting epsteinscan_age_verified=true on the browser context. That also cleared the gate's body.style.overflow='hidden', which made the horizontal-overflow assertions meaningful for the first time.

One test failed because my assertion threshold was under 45px and the measured height was 46. I diagnosed it with Range.getClientRects(). Every drawer link is 46px whether badged or not, and AI Analyst renders on exactly one line, identical to The Wall's LIVE-badge link. I corrected the assertion to compare against the existing LIVE link instead of a magic number.

Production verification

All curl commands to the public domain returned HTTP 000 from both the server and my local machine. I wasted two 40-second timeout rounds assuming Cloudflare was challenging the curl user agent. The actual cause was that the domain is epsteinscan.org, not .com. I had the wrong TLD. I confirmed via grep server_name /etc/nginx/sites-enabled/.

I ran the full headless Chrome verification suite at six viewport widths in both dark and light themes on dev and against the production process. Zero failures both runs.

Share this post