diff --git a/apps/web-remix/app/components/chat/ChatMarkdown.tsx b/apps/web-remix/app/components/chat/ChatMarkdown.tsx index 1682c61ac..87a3822ad 100644 --- a/apps/web-remix/app/components/chat/ChatMarkdown.tsx +++ b/apps/web-remix/app/components/chat/ChatMarkdown.tsx @@ -1,10 +1,12 @@ import type { AnchorHTMLAttributes } from 'react'; -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; +import { Check, Copy } from 'lucide-react'; import Markdown from 'markdown-to-jsx'; import type { MarkdownToJSX } from 'markdown-to-jsx'; import mermaid from 'mermaid'; import { z } from 'zod'; +import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; import { cn } from '~/utils/cn'; interface ChatMarkdownProps { @@ -277,25 +279,61 @@ function Link({ ); } +const MAX_VISIBLE_LENGTH = 1000; + function Pre({ children, className, ...rest }: React.ParamHTMLAttributes) { + const { copy, isCopied } = useCopyToClipboard(getStringContent(children)); + useEffect(() => { mermaid.initialize({}); }, []); + const truncatedChildren = useMemo(() => { + return truncateChildrenContent(children, MAX_VISIBLE_LENGTH); + }, [children]); + + const language = useMemo(() => { + if (children && typeof children === 'object' && 'props' in children) { + return getLanguage(children.props.className); + } + + return null; + }, [children]); + return ( -
-      {children}
-    
+
+
+ {language ?? ''} + +
+ +
+        {truncatedChildren}
+      
+
); } @@ -331,6 +369,7 @@ function Code({ } return 'Uploaded files'; } + return ( ) { return {alt}; } + +function truncateChildrenContent( + children: React.ReactNode, + maxLength: number = 300, +): React.ReactNode { + return React.Children.map(children, (child) => { + if (typeof child === 'string') { + return truncateString(child, maxLength); + } + + if (child && typeof child === 'object') { + if ('props' in child && shouldBeTruncated(child)) { + return { + ...child, + props: { + ...child.props, + children: truncateChildrenContent(child.props.children, maxLength), + }, + }; + } + } + return child; + }); +} + +function getStringContent(children: React.ReactNode): string { + return ( + React.Children.map(children, (child) => { + if (typeof child === 'string') { + return child; + } + + if (child && typeof child === 'object') { + if ('props' in child && shouldBeTruncated(child)) { + return getStringContent(child.props.children); + } + } + return ''; + })?.join('\n') ?? '' + ); +} + +function shouldBeTruncated(child: { props: Record }) { + if ('className' in child.props && typeof child.props.className === 'string') { + const language = getLanguage(child.props.className); + + if (!language) return false; + + return [ + 'json', + 'javascript', + 'typescript', + 'html', + 'css', + 'sql', + 'python', + 'java', + 'csharp', + 'c', + 'cpp', + 'bash', + 'sh', + 'yaml', + 'xml', + ].includes(language); + } + + return false; +} + +function getLanguage(className?: string) { + return className?.match(/lang-(\w+)/)?.[1]; +} + +function truncateString(str: string, maxLength: number) { + return str.length > maxLength + ? `${str.slice(0, maxLength / 1.5)}\n\n... rest of code` + : str; +}