SPB Git

spb/worthdoing Public

Autonomous investigation agent that discovers, challenges, and ranks things genuinely worth doing — Claude + Firecrawl, Next.js 16, PostgreSQL

TypeScript 91.5% SQL 5.8% CSS 2.2%
3.4 KB · 97 lines tsx
Raw Blame History
1/**2 * WorthDoing.ai3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: src/components/wd/Markdown.tsx6 * Description: Sanitized markdown renderer for opportunity reports, with [n] citations linked to evidence.7 */8"use client";910import ReactMarkdown from "react-markdown";11import remarkGfm from "remark-gfm";12import type { ReactNode } from "react";1314/** Turn bare [n] citation markers inside text nodes into anchor links to #evidence-n. */15function linkCitations(children: ReactNode): ReactNode {16  const walk = (node: ReactNode, key = 0): ReactNode => {17    if (typeof node === "string") {18      const parts = node.split(/(\[\d{1,3}\])/g);19      if (parts.length === 1) return node;20      return parts.map((part, i) => {21        const m = part.match(/^\[(\d{1,3})\]$/);22        if (!m) return part;23        return (24          <a25            key={`${key}-${i}`}26            href={`#evidence-${m[1]}`}27            className="mx-0.5 rounded bg-verdict/10 px-1 py-0.5 font-mono text-[11px] font-medium text-verdict no-underline hover:bg-verdict/20"28          >29            {m[1]}30          </a>31        );32      });33    }34    if (Array.isArray(node)) return node.map((n, i) => walk(n, i));35    return node;36  };37  return walk(children);38}3940export function Markdown({ children }: { children: string }) {41  return (42    <div className="prose-wd">43      <ReactMarkdown44        remarkPlugins={[remarkGfm]}45        components={{46          h2: ({ children }) => (47            <h2 className="mb-3 mt-8 font-heading text-xl font-semibold text-ink first:mt-0">{children}</h2>48          ),49          h3: ({ children }) => (50            <h3 className="mb-2 mt-6 font-heading text-lg font-semibold text-ink">{children}</h3>51          ),52          p: ({ children }) => (53            <p className="mb-4 text-[15px] leading-relaxed text-ink/90">{linkCitations(children)}</p>54          ),55          li: ({ children }) => (56            <li className="mb-1.5 ml-4 list-disc text-[15px] leading-relaxed text-ink/90">57              {linkCitations(children)}58            </li>59          ),60          a: ({ href, children }) => (61            <a62              href={href}63              target="_blank"64              rel="noopener noreferrer"65              className="text-tele underline decoration-tele/40 underline-offset-2 hover:decoration-tele"66            >67              {children}68            </a>69          ),70          strong: ({ children }) => <strong className="font-semibold text-ink">{children}</strong>,71          blockquote: ({ children }) => (72            <blockquote className="mb-4 border-l-2 border-verdict/40 pl-4 italic text-ink-soft">73              {children}74            </blockquote>75          ),76          code: ({ children }) => (77            <code className="rounded bg-secondary px-1.5 py-0.5 font-mono text-[13px] text-ink">{children}</code>78          ),79          table: ({ children }) => (80            <div className="mb-4 overflow-x-auto">81              <table className="w-full border-collapse text-sm">{children}</table>82            </div>83          ),84          th: ({ children }) => (85            <th className="border-b border-line px-3 py-2 text-left font-medium text-ink">{children}</th>86          ),87          td: ({ children }) => (88            <td className="border-b border-line/60 px-3 py-2 text-ink/90">{linkCitations(children)}</td>89          ),90        }}91      >92        {children}93      </ReactMarkdown>94    </div>95  );96}97