spb/search-box Public
Agentic web research engine — hypotheses, verbatim evidence, contradictions, sourced answers streamed live. Claude Opus 5 + Firecrawl + PostgreSQL.
TypeScript 76.9%
CSS 18.7%
SQL 2.1%
JavaScript 1.8%
Shell 0.5%
1/**2 * Search-box.ai3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: apps/web/components/NewResearchForm.tsx6 * Description: Question composer — example chips, keyboard submit, session start.7 */89"use client";1011import { useRouter } from "next/navigation";12import { useState } from "react";13import { Glyph } from "./Pastille";1415const EXAMPLES = [16 "Can transformer KV cache be compressed 10x without hurting quality?",17 "Do heat pumps still beat gas boilers in very cold climates?",18 "Is open-source AI closing the gap with frontier labs?"19];2021export function NewResearchForm() {22 const router = useRouter();23 const [question, setQuestion] = useState("");24 const [busy, setBusy] = useState(false);25 const [error, setError] = useState<string | null>(null);2627 async function submit() {28 const q = question.trim();29 if (q.length < 8 || busy) return;30 setBusy(true);31 setError(null);32 try {33 const res = await fetch("/api/research", {34 method: "POST",35 headers: { "Content-Type": "application/json" },36 body: JSON.stringify({ question: q })37 });38 if (!res.ok) {39 const body = (await res.json().catch(() => ({}))) as { error?: string };40 throw new Error(body.error ?? `request failed (${res.status})`);41 }42 const { id } = (await res.json()) as { id: string };43 router.push(`/r/${id}`);44 } catch (err) {45 setError(err instanceof Error ? err.message : String(err));46 setBusy(false);47 }48 }4950 return (51 <div className="ask">52 <div className="box">53 <textarea54 value={question}55 onChange={(e) => setQuestion(e.target.value)}56 onKeyDown={(e) => {57 if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {58 e.preventDefault();59 void submit();60 }61 }}62 placeholder="Ask a question that deserves real research…"63 aria-label="Research question"64 />65 <div className="box-foot">66 <span className="hint">⌘⏎ to launch</span>67 <button className="btn" onClick={() => void submit()} disabled={busy || question.trim().length < 8}>68 <Glyph kind="search" size={14} />69 {busy ? "starting…" : "Start research"}70 </button>71 </div>72 </div>73 <div className="chips">74 {EXAMPLES.map((ex) => (75 <button key={ex} className="chip" onClick={() => setQuestion(ex)}>76 {ex}77 </button>78 ))}79 </div>80 {error && <div className="error-box">{error}</div>}81 </div>82 );83}84