TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1"use client";2// Rendu Markdown des réponses : GFM + LaTeX (KaTeX), sans HTML brut (XSS),3// citations [Sx] transformées en puces cliquables.4import { memo } from "react";5import ReactMarkdown from "react-markdown";6import remarkGfm from "remark-gfm";7import remarkMath from "remark-math";8import rehypeKatex from "rehype-katex";910function prepareCitations(content: string): string {11 // [S3] → lien markdown interne que le composant « a » rend comme puce.12 return content.replace(/\[S(\d+)\]/g, "[S$1](#citation-$1)");13}1415export const Markdown = memo(function Markdown({16 content,17 onCitationClick,18 streaming,19}: {20 content: string;21 onCitationClick?: (index: number) => void;22 streaming?: boolean;23}) {24 return (25 <div className={`prose-immbot ${streaming ? "stream-cursor" : ""}`}>26 <ReactMarkdown27 remarkPlugins={[remarkGfm, remarkMath]}28 rehypePlugins={[rehypeKatex]}29 skipHtml30 components={{31 a({ href, children, ...props }) {32 if (href?.startsWith("#citation-")) {33 const index = parseInt(href.slice("#citation-".length), 10);34 return (35 <button36 type="button"37 className="citation-chip"38 title={`Voir la source S${index}`}39 onClick={(e) => {40 e.preventDefault();41 onCitationClick?.(index);42 }}43 >44 {children}45 </button>46 );47 }48 return (49 <a href={href} target="_blank" rel="noopener noreferrer" {...props}>50 {children}51 </a>52 );53 },54 }}55 >56 {prepareCitations(content)}57 </ReactMarkdown>58 </div>59 );60});61