Data Components

Research Brief

A one-page, citation-grounded brief about a company, person, or market. The whole brief is a single markdown string with section headings. Use it for pre-meeting prep, account research, or competitive deep-dives.

Demo

Type a subject and hit Generate. Exa runs a deep search across the web and streams a structured markdown brief back with inline citations.

Try one of these:

How it works

One call to exa.search with type: "deep" produces a long markdown document grounded in web sources. Depth beats latency for a brief, so expect 15-30 seconds end-to-end.

Section structure comes from outputSchema.description, which lists the required headings (Overview, Product, Market, People, Recent News, Talking Points) and the formatting rules. Citation markers like [1] appear at the ends of sentences and map 1-to-1 to entries flattened from output.grounding.

The client renders the markdown with a small purpose-built parser: headings, paragraphs, bullets, and inline favicon citation chips deduped by domain.

Source code

"use client";

import { useState } from "react";
import type { Citation } from "@/lib/citations";
import { MarkdownBrief } from "@/lib/markdown";

type BriefState =
  | { status: "idle" }
  | { status: "generating" }
  | { status: "done"; content: string; citations: Citation[] }
  | { status: "error"; error: string };

const SUGGESTIONS = [
  "Stripe",
  "Patrick Collison",
  "AI coding agents market",
  "Anthropic Series G round",
];

export function BriefExample() {
  const [subject, setSubject] = useState("");
  const [brief, setBrief] = useState<BriefState>({ status: "idle" });

  async function generate(input: string) {
    const s = input.trim();
    if (!s || brief.status === "generating") return;
    setBrief({ status: "generating" });

    try {
      const res = await fetch("/api/brief", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ subject: s }),
      });
      const json = await res.json();
      if (!res.ok || json.error) {
        setBrief({
          status: "error",
          error: json.error || `Request failed (${res.status})`,
        });
        return;
      }
      setBrief({
        status: "done",
        content: json.content ?? "",
        citations: json.citations ?? [],
      });
    } catch (err) {
      setBrief({
        status: "error",
        error: err instanceof Error ? err.message : String(err),
      });
    }
  }

  const isGenerating = brief.status === "generating";

  return (
    <div className="space-y-4">
      <form
        onSubmit={(e) => {
          e.preventDefault();
          void generate(subject);
        }}
        className="border border-[#e5e5e5] bg-white overflow-hidden focus-within:border-[var(--color-brand)]"
      >
        <div className="flex items-center gap-2 px-3 py-2">
          <input
            type="text"
            value={subject}
            onChange={(e) => setSubject(e.target.value)}
            placeholder="Company, person, or market"
            className="flex-1 text-[13px] text-[#111827] focus:outline-none"
          />
          <button
            type="submit"
            disabled={isGenerating || !subject.trim()}
            className="bg-[var(--color-brand)] text-white text-[13px] font-medium px-3.5 py-1.5 disabled:opacity-50"
          >
            {isGenerating ? "Researching..." : "Generate"}
          </button>
        </div>
      </form>
      {brief.status === "idle" && (
        <div className="text-[13px] text-[#6b7280]">
          <p className="mb-2">Try one of these:</p>
          <div className="flex flex-wrap gap-2">
            {SUGGESTIONS.map((s) => (
              <button
                key={s}
                type="button"
                onClick={() => {
                  setSubject(s);
                  void generate(s);
                }}
                className="rounded-full border border-[#e5e5e5] bg-white px-3 py-1 text-[12px] text-[#374151] hover:border-[var(--color-brand)] hover:text-[#111827]"
              >
                {s}
              </button>
            ))}
          </div>
        </div>
      )}
      {brief.status === "error" && (
        <div className="text-[13px] text-[#b91c1c]">{brief.error}</div>
      )}
      {brief.status === "generating" && (
        <div className="border border-[#e5e5e5] bg-white px-6 py-5">
          <ResearchingState />
        </div>
      )}
      {brief.status === "done" && (
        <div className="border border-[#e5e5e5] bg-white px-6 py-5">
          <MarkdownBrief
            markdown={brief.content}
            citations={brief.citations}
            streaming={false}
          />
        </div>
      )}
    </div>
  );
}

function ResearchingState() {
  return (
    <div className="space-y-2">
      <div className="h-4 w-1/3 rounded bg-[#e5e7eb] animate-pulse" />
      <div className="h-3 w-full rounded bg-[#e5e7eb] animate-pulse" />
      <div className="h-3 w-5/6 rounded bg-[#e5e7eb] animate-pulse" />
      <div className="h-3 w-3/4 rounded bg-[#e5e7eb] animate-pulse" />
    </div>
  );
}