SPB Git

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%
2.6 KB · 84 lines tsx
Raw Blame History
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