From 6036252976a4cbe4c178849ccdb67b4e83566b60 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Thu, 11 Sep 2025 16:42:37 -0300 Subject: [PATCH] Added code component and markdown renderer --- frontend/components/code-component.tsx | 56 +++++++++ frontend/components/markdown-renderer.tsx | 142 ++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 frontend/components/code-component.tsx create mode 100644 frontend/components/markdown-renderer.tsx diff --git a/frontend/components/code-component.tsx b/frontend/components/code-component.tsx new file mode 100644 index 00000000..e612484d --- /dev/null +++ b/frontend/components/code-component.tsx @@ -0,0 +1,56 @@ +import { Check, Copy } from "lucide-react"; +import { useState } from "react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { tomorrow } from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { Button } from "./ui/button"; + +type CodeComponentProps = { + code: string; + language: string; +}; + +export default function CodeComponent({ code, language }: CodeComponentProps) { + const [isCopied, setIsCopied] = useState(false); + + const copyToClipboard = () => { + if (!navigator.clipboard || !navigator.clipboard.writeText) { + return; + } + + navigator.clipboard.writeText(code).then(() => { + setIsCopied(true); + + setTimeout(() => { + setIsCopied(false); + }, 2000); + }); + }; + + return ( +
+ + + {code} + +
+ ); +} diff --git a/frontend/components/markdown-renderer.tsx b/frontend/components/markdown-renderer.tsx new file mode 100644 index 00000000..d6c8c81a --- /dev/null +++ b/frontend/components/markdown-renderer.tsx @@ -0,0 +1,142 @@ +import Markdown from "react-markdown"; +import rehypeMathjax from "rehype-mathjax"; +import rehypeRaw from "rehype-raw"; +import remarkGfm from "remark-gfm"; +import { cn } from "@/lib/utils"; +import CodeComponent from "./code-component"; + +type MarkdownRendererProps = { + chatMessage: string; +}; + +const preprocessChatMessage = (text: string): string => { + // Handle tags + let processed = text + .replace(//g, "``") + .replace(/<\/think>/g, "``"); + + // Clean up tables if present + if (isMarkdownTable(processed)) { + processed = cleanupTableEmptyCells(processed); + } + + return processed; +}; + +export const isMarkdownTable = (text: string): boolean => { + if (!text?.trim()) return false; + + // Single regex to detect markdown table with header separator + return /\|.*\|.*\n\s*\|[\s\-:]+\|/m.test(text); +}; + +export const cleanupTableEmptyCells = (text: string): string => { + return text + .split("\n") + .filter((line) => { + const trimmed = line.trim(); + + // Keep non-table lines + if (!trimmed.includes("|")) return true; + + // Keep separator rows (contain only |, -, :, spaces) + if (/^\|[\s\-:]+\|$/.test(trimmed)) return true; + + // For data rows, check if any cell has content + const cells = trimmed.split("|").slice(1, -1); // Remove delimiter cells + return cells.some((cell) => cell.trim() !== ""); + }) + .join("\n"); +}; +export const MarkdownRenderer = ({ chatMessage }: MarkdownRendererProps) => { + // Process the chat message to handle tags and clean up tables + const processedChatMessage = preprocessChatMessage(chatMessage); + + return ( +
+ {props.children}

; + }, + ol({ node, ...props }) { + return
    {props.children}
; + }, + h1({ node, ...props }) { + return

{props.children}

; + }, + h2({ node, ...props }) { + return

{props.children}

; + }, + h3({ node, ...props }) { + return

{props.children}

; + }, + hr({ node, ...props }) { + return
; + }, + ul({ node, ...props }) { + return
    {props.children}
; + }, + pre({ node, ...props }) { + return <>{props.children}; + }, + table: ({ node, ...props }) => { + return ( +
+
+ {props.children}
+
+
+ ); + }, + + code: ({ node, className, inline, children, ...props }) => { + let content = children as string; + if ( + Array.isArray(children) && + children.length === 1 && + typeof children[0] === "string" + ) { + content = children[0] as string; + } + if (typeof content === "string") { + if (content.length) { + if (content[0] === "▍") { + return ; + } + + // Specifically handle tags that were wrapped in backticks + if (content === "" || content === "") { + return {content}; + } + } + + const match = /language-(\w+)/.exec(className || ""); + + return !inline ? ( + + ) : ( + + {content} + + ); + } + }, + }} + > + {processedChatMessage} +
+
+ ); +};