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%
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