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/AnswerPanel.tsx6 * Description: Streaming answer renderer — sanitized markdown, citation links, copy action.7 */89"use client";1011import { useMemo, useState } from "react";12import { marked } from "marked";13import DOMPurify from "dompurify";14import { Pastille } from "./Pastille";1516export function AnswerPanel({17 markdown,18 done,19 live20}: {21 markdown: string;22 done: boolean;23 live: boolean;24}) {25 const [copied, setCopied] = useState(false);2627 const html = useMemo(() => {28 if (!markdown) return "";29 const raw = marked.parse(markdown, { async: false, gfm: true });30 const safe = DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } });31 // Citations are mechanical [n] markers from state — link them to the sources board.32 return safe.replace(33 /\[(\d{1,3})\]/g,34 (_m, n: string) => `<sup class="cite"><a href="#src-${n}" title="source ${n}">[${n}]</a></sup>`35 );36 }, [markdown]);3738 async function copy() {39 try {40 await navigator.clipboard.writeText(markdown);41 setCopied(true);42 setTimeout(() => setCopied(false), 1600);43 } catch {44 // clipboard unavailable — ignore45 }46 }4748 return (49 <div className="answer-panel">50 <div className="answer-head">51 <Pastille kind="synthesis" size="sm" spinning={markdown.length > 0 && !done} />52 <span className="lbl">{done ? "final answer" : markdown ? "writing…" : "answer"}</span>53 {done && (54 <button className="btn-ghost" onClick={() => void copy()}>55 {copied ? "copied ✓" : "copy markdown"}56 </button>57 )}58 </div>59 <div className="answer-body">60 {!markdown ? (61 <div className="answer-waiting">62 <p>63 {live64 ? "The answer is written here, token by token, once the evidence base is ready."65 : "No answer was produced for this session."}66 </p>67 {live && (68 <>69 <span className="skel" style={{ width: "82%" }} />70 <span className="skel" style={{ width: "95%" }} />71 <span className="skel" style={{ width: "64%" }} />72 </>73 )}74 </div>75 ) : (76 <>77 <div className="answer-md" dangerouslySetInnerHTML={{ __html: html }} />78 {!done && <span className="caret" aria-hidden />}79 </>80 )}81 </div>82 </div>83 );84}85