SPB Git

spb/chat-spboucher Public

Private universal chat interface over the OpenRouter ecosystem — 400+ models, branching, streaming, usage tracking. Next.js 16 + SQLite, PWA, deployed on m4m64a at chat.spboucher.ai

TypeScript 78.8% CSS 15.1% JavaScript 4.9% Shell 1.2%
2.2 KB · 73 lines tsx
Raw Blame History
1// Author: Simon-Pierre Boucher2// Contact: contact@spboucher.ai3// Project: chat.spboucher.ai45"use client";67import { memo, useState, type ReactNode } from "react";8import ReactMarkdown from "react-markdown";9import remarkGfm from "remark-gfm";10import remarkMath from "remark-math";11import rehypeKatex from "rehype-katex";12import rehypeHighlight from "rehype-highlight";1314function CodeBlock({ className, children }: { className?: string; children?: ReactNode }) {15  const [copied, setCopied] = useState(false);16  const language = /language-(\w+)/.exec(className ?? "")?.[1] ?? "";1718  const extractText = (node: ReactNode): string => {19    if (typeof node === "string") return node;20    if (Array.isArray(node)) return node.map(extractText).join("");21    if (node && typeof node === "object" && "props" in node) {22      return extractText((node as { props: { children?: ReactNode } }).props.children);23    }24    return "";25  };2627  const copy = async () => {28    try {29      await navigator.clipboard.writeText(extractText(children));30      setCopied(true);31      setTimeout(() => setCopied(false), 1500);32    } catch {33      /* clipboard unavailable */34    }35  };3637  return (38    <pre>39      <div className="code-block-header">40        <span>{language || "text"}</span>41        <button type="button" onClick={copy}>42          {copied ? "Copied" : "Copy"}43        </button>44      </div>45      <code className={className}>{children}</code>46    </pre>47  );48}4950export const Markdown = memo(function Markdown({ content }: { content: string }) {51  return (52    <ReactMarkdown53      remarkPlugins={[remarkGfm, remarkMath]}54      rehypePlugins={[rehypeKatex, [rehypeHighlight, { ignoreMissing: true, detect: false }]]}55      components={{56        pre: ({ children }) => <>{children}</>,57        code: ({ className, children }) => {58          const isBlock = /language-/.test(className ?? "") || String(children).includes("\n");59          if (isBlock) return <CodeBlock className={className}>{children}</CodeBlock>;60          return <code className={className}>{children}</code>;61        },62        a: ({ href, children }) => (63          <a href={href} target="_blank" rel="noopener noreferrer">64            {children}65          </a>66        ),67      }}68    >69      {content}70    </ReactMarkdown>71  );72});73