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