Data Components

Generated Email

A draft cold outbound email grounded in real, recent facts about the recipient. Every claim about the recipient is backed by a source.

Demo

Enter a recipient (person, company, or both). Exa researches them and drafts an email from the sender configured in email-sender.ts.

Try one of these:

How it works

One call to exa.search with type: "deep" and an outputSchema that returns a structured object with subject and body fields.

The systemPrompt carries the sender persona (name, product, ICP) and a tight list of style rules (no clichés, no fake intimacy, no em dashes). The schema description shapes the body into 3-4 short paragraphs anchored by a recent, specific fact about the recipient.

The body stays clean: no inline citation markers. Sources are flattened from output.grounding and rendered as a favicon row below the email, deduped by domain.

Source code

"use client";

import { useState } from "react";
import { CitationChip, type Citation } from "@/lib/citations";
import { SENDER } from "@/lib/email-sender";

type EmailState =
  | { status: "idle" }
  | { status: "generating" }
  | {
      status: "done";
      recipient: string;
      subject: string;
      body: string;
      citations: Citation[];
    }
  | { status: "error"; error: string };

const SUGGESTIONS = [
  "Patrick Collison, CEO at Stripe",
  "Cursor (the AI code editor)",
  "Mercury, the fintech bank",
];

export function EmailExample() {
  const [recipient, setRecipient] = useState("");
  const [email, setEmail] = useState<EmailState>({ status: "idle" });

  async function generate(input: string) {
    const r = input.trim();
    if (!r || email.status === "generating") return;
    setEmail({ status: "generating" });
    try {
      const res = await fetch("/api/email", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ recipient: r }),
      });
      const json = await res.json();
      if (!res.ok || json.error) {
        setEmail({
          status: "error",
          error: json.error || `Request failed (${res.status})`,
        });
        return;
      }
      setEmail({
        status: "done",
        recipient: json.recipient ?? r,
        subject: json.subject ?? "",
        body: json.body ?? "",
        citations: json.citations ?? [],
      });
    } catch (err) {
      setEmail({
        status: "error",
        error: err instanceof Error ? err.message : String(err),
      });
    }
  }

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

  return (
    <div className="space-y-4">
      <form
        onSubmit={(e) => {
          e.preventDefault();
          void generate(recipient);
        }}
        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={recipient}
            onChange={(e) => setRecipient(e.target.value)}
            placeholder="Recipient: person, company, or both"
            className="flex-1 text-[13px] text-[#111827] focus:outline-none"
          />
          <button
            type="submit"
            disabled={isGenerating || !recipient.trim()}
            className="bg-[var(--color-brand)] text-white text-[13px] font-medium px-3.5 py-1.5 disabled:opacity-50"
          >
            {isGenerating ? "Drafting..." : "Draft"}
          </button>
        </div>
      </form>
      {email.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={() => {
                  setRecipient(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>
      )}
      {email.status === "error" && (
        <div className="text-[13px] text-[#b91c1c]">{email.error}</div>
      )}
      {email.status === "generating" && (
        <div className="border border-[#e5e5e5] bg-white px-6 py-5">
          <DraftSkeleton />
        </div>
      )}
      {email.status === "done" && (
        <EmailCard
          recipient={email.recipient}
          subject={email.subject}
          body={email.body}
          citations={email.citations}
        />
      )}
    </div>
  );
}

function EmailCard({
  recipient,
  subject,
  body,
  citations,
}: {
  recipient: string;
  subject: string;
  body: string;
  citations: Citation[];
}) {
  const paragraphs = body.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
  // Dedupe citations by domain so the sources row shows distinct
  // sources only.
  const seen = new Set<string>();
  const uniqueCitations: Citation[] = [];
  for (const c of citations) {
    const d = safeDomain(c.url);
    if (seen.has(d)) continue;
    seen.add(d);
    uniqueCitations.push(c);
  }
  return (
    <div className="border border-[#e5e5e5] bg-white">
      <div className="px-6 py-3 border-b border-[#e5e5e5] space-y-1">
        <HeaderRow label="From" value={`${SENDER.name} · ${SENDER.product}`} />
        <HeaderRow label="To" value={recipient} />
        <HeaderRow label="Subject" value={subject} bold />
      </div>
      <div className="px-6 py-5 space-y-3 text-[13px] text-[#111827] leading-[1.7]">
        {paragraphs.map((p, i) => (
          <p key={i} className="whitespace-pre-line">
            {p}
          </p>
        ))}
      </div>
      {uniqueCitations.length > 0 && (
        <div className="px-6 py-3 border-t border-[#e5e5e5] flex items-center gap-2 flex-wrap">
          <span className="inline-flex items-center h-4 text-[11px] uppercase tracking-wider text-[#9ca3af] leading-none mr-1">
            Sources
          </span>
          <span className="inline-flex items-center gap-1">
            {uniqueCitations.map((c) => (
              <CitationChip key={c.id} citation={c} />
            ))}
          </span>
        </div>
      )}
    </div>
  );
}

function safeDomain(url: string): string {
  try {
    return new URL(url).hostname.replace(/^www\./, "");
  } catch {
    return url;
  }
}

function HeaderRow({
  label,
  value,
  bold,
}: {
  label: string;
  value: string;
  bold?: boolean;
}) {
  return (
    <div className="flex gap-2 text-[12px] leading-[1.55]">
      <span className="text-[#9ca3af] w-14 shrink-0">{label}</span>
      <span
        className={`text-[#111827] ${bold ? "font-medium" : ""} min-w-0 break-words`}
      >
        {value}
      </span>
    </div>
  );
}

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