repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | "use client" |
| #2 | |
| #3 | import { CSSProperties, useState, ReactNode, useRef } from "react" |
| #4 | import React from "react" |
| #5 | import Markdown, { Components } from "react-markdown" |
| #6 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" |
| #7 | import { coldarkCold, coldarkDark } from "react-syntax-highlighter/dist/esm/styles/prism" |
| #8 | import remarkGfm from "remark-gfm" |
| #9 | import remarkMath from "remark-math" |
| #10 | import { Button } from "@/components/ui/button" |
| #11 | import { Check, Copy } from "lucide-react" |
| #12 | import { cn } from "@/lib/utils" |
| #13 | import "./markdown.css" |
| #14 | |
| #15 | interface MarkdownRendererProps { |
| #16 | markdownText: string |
| #17 | actualCode?: string |
| #18 | className?: string |
| #19 | style?: { prism?: { [key: string]: CSSProperties } } |
| #20 | messageId?: string |
| #21 | showCopyButton?: boolean |
| #22 | isDarkMode?: boolean |
| #23 | } |
| #24 | |
| #25 | const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ |
| #26 | markdownText = '', |
| #27 | className, |
| #28 | style, |
| #29 | actualCode, |
| #30 | messageId = '', |
| #31 | showCopyButton = true, |
| #32 | isDarkMode = false |
| #33 | }) => { |
| #34 | const [copied, setCopied] = useState(false); |
| #35 | const [isStreaming, setIsStreaming] = useState(true); |
| #36 | const highlightBuffer = useRef<string[]>([]); |
| #37 | const isCollecting = useRef(false); |
| #38 | const processedTextRef = useRef<string>(''); |
| #39 | |
| #40 | const safeMarkdownText = React.useMemo(() => { |
| #41 | return typeof markdownText === 'string' ? markdownText : ''; |
| #42 | }, [markdownText]); |
| #43 | |
| #44 | const preProcessText = React.useCallback((text: unknown): string => { |
| #45 | if (typeof text !== 'string' || !text) return ''; |
| #46 | |
| #47 | // Remove highlight tags initially for clean rendering |
| #48 | return text.replace(/<highlight>.*?<\/highlight>/g, (match) => { |
| #49 | // Extract the content between tags |
| #50 | const content = match.replace(/<highlight>|<\/highlight>/g, ''); |
| #51 | return content; |
| #52 | }); |
| #53 | }, []); |
| #54 | |
| #55 | // Reset streaming state when markdownText changes |
| #56 | React.useEffect(() => { |
| #57 | // Preprocess the text first |
| #58 | processedTextRef.current = preProcessText(safeMarkdownText); |
| #59 | setIsStreaming(true); |
| #60 | const timer = setTimeout(() => { |
| #61 | setIsStreaming(false); |
| #62 | }, 500); |
| #63 | return () => clearTimeout(timer); |
| #64 | }, [safeMarkdownText, preProcessText]); |
| #65 | |
| #66 | const copyToClipboard = async (code: string) => { |
| #67 | await navigator.clipboard.writeText(code); |
| #68 | setCopied(true); |
| #69 | setTimeout(() => setCopied(false), 1000); |
| #70 | }; |
| #71 | |
| #72 | const processText = React.useCallback((text: string) => { |
| #73 | if (typeof text !== 'string') return text; |
| #74 | |
| #75 | // Only process highlights after streaming is complete |
| #76 | if (!isStreaming) { |
| #77 | if (text === '<highlight>') { |
| #78 | isCollecting.current = true; |
| #79 | return null; |
| #80 | } |
| #81 | |
| #82 | if (text === '</highlight>') { |
| #83 | isCollecting.current = false; |
| #84 | const content = highlightBuffer.current.join(''); |
| #85 | highlightBuffer.current = []; |
| #86 | |
| #87 | return ( |
| #88 | <span |
| #89 | key={`highlight-${messageId}-${content}`} |
| #90 | className={cn("highlight-text animate text-black", { |
| #91 | "dark": isDarkMode |
| #92 | })} |
| #93 | > |
| #94 | {content} |
| #95 | </span> |
| #96 | ); |
| #97 | } |
| #98 | |
| #99 | if (isCollecting.current) { |
| #100 | highlightBuffer.current.push(text); |
| #101 | return null; |
| #102 | } |
| #103 | } |
| #104 | |
| #105 | return text; |
| #106 | }, [isStreaming, messageId, isDarkMode]); |
| #107 | |
| #108 | const processChildren = React.useCallback((children: ReactNode): ReactNode => { |
| #109 | if (typeof children === 'string') { |
| #110 | return processText(children); |
| #111 | } |
| #112 | if (Array.isArray(children)) { |
| #113 | return children.map(child => { |
| #114 | const processed = processChildren(child); |
| #115 | return processed === null ? null : processed; |
| #116 | }).filter(Boolean); |
| #117 | } |
| #118 | return children; |
| #119 | }, [processText]); |
| #120 | |
| #121 | const CodeBlock = React.useCallback(({ |
| #122 | language, |
| #123 | code, |
| #124 | actualCode, |
| #125 | showCopyButton = true, |
| #126 | }: { |
| #127 | language: string; |
| #128 | code: string; |
| #129 | actualCode?: string; |
| #130 | showCopyButton?: boolean; |
| #131 | }) => ( |
| #132 | <div className="relative my-4 rounded-xl overflow-hidden bg-neutral-100 w-full max-w-full border border-neutral-200"> |
| #133 | {showCopyButton && ( |
| #134 | <div className="flex items-center justify-between px-4 py-2 rounded-t-md shadow-md"> |
| #135 | <span className="text-xs text-neutral-700 dark:text-white font-inter-display"> |
| #136 | {language} |
| #137 | </span> |
| #138 | <Button |
| #139 | variant="ghost" |
| #140 | size="icon" |
| #141 | className="h-8 w-8 text-neutral-700 dark:text-white" |
| #142 | onClick={() => copyToClipboard(actualCode || code)} |
| #143 | > |
| #144 | {copied ? ( |
| #145 | <Check className="h-4 w-4 text-green-500" /> |
| #146 | ) : ( |
| #147 | <Copy className="h-4 w-4 text-muted-foreground" /> |
| #148 | )} |
| #149 | </Button> |
| #150 | </div> |
| #151 | )} |
| #152 | <div className="max-w-full w-full overflow-hidden"> |
| #153 | <SyntaxHighlighter |
| #154 | language={language} |
| #155 | style={style?.prism || (isDarkMode ? coldarkDark : coldarkCold)} |
| #156 | customStyle={{ |
| #157 | margin: 0, |
| #158 | borderTopLeftRadius: "0", |
| #159 | borderTopRightRadius: "0", |
| #160 | padding: "16px", |
| #161 | fontSize: "0.9rem", |
| #162 | lineHeight: "1.3", |
| #163 | backgroundColor: isDarkMode ? "#262626" : "#fff", |
| #164 | wordBreak: "break-word", |
| #165 | overflowWrap: "break-word", |
| #166 | }} |
| #167 | > |
| #168 | {code} |
| #169 | </SyntaxHighlighter> |
| #170 | </div> |
| #171 | </div> |
| #172 | ), [copied, isDarkMode, style]); |
| #173 | |
| #174 | const components = { |
| #175 | p: ({ children, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => ( |
| #176 | <p className="m-0 p-0" {...props}>{processChildren(children)}</p> |
| #177 | ), |
| #178 | span: ({ children, ...props }: React.HTMLAttributes<HTMLSpanElement>) => ( |
| #179 | <span {...props}>{processChildren(children)}</span> |
| #180 | ), |
| #181 | li: ({ children, ...props }: React.HTMLAttributes<HTMLLIElement>) => ( |
| #182 | <li {...props}>{processChildren(children)}</li> |
| #183 | ), |
| #184 | strong: ({ children, ...props }: React.HTMLAttributes<HTMLElement>) => ( |
| #185 | <strong {...props}>{processChildren(children)}</strong> |
| #186 | ), |
| #187 | em: ({ children, ...props }: React.HTMLAttributes<HTMLElement>) => ( |
| #188 | <em {...props}>{processChildren(children)}</em> |
| #189 | ), |
| #190 | code: ({ className, children, ...props }: React.HTMLAttributes<HTMLElement>) => { |
| #191 | const match = /language-(\w+)/.exec(className || ""); |
| #192 | if (match) { |
| #193 | return ( |
| #194 | <CodeBlock |
| #195 | language={match[1]} |
| #196 | code={String(children)} |
| #197 | actualCode={actualCode} |
| #198 | showCopyButton={showCopyButton} |
| #199 | /> |
| #200 | ); |
| #201 | } |
| #202 | return ( |
| #203 | <code className={className} {...props}> |
| #204 | {processChildren(children)} |
| #205 | </code> |
| #206 | ); |
| #207 | } |
| #208 | } satisfies Components; |
| #209 | |
| #210 | return ( |
| #211 | <div className={cn( |
| #212 | "min-w-[100%] max-w-[100%] my-2 prose-hr:my-0 prose-h4:my-1 text-sm prose-ul:-my-2 prose-ol:-my-2 prose-li:-my-2 prose break-words prose-pre:bg-transparent prose-pre:-my-2 dark:prose-invert prose-p:leading-snug prose-pre:p-0 prose-h3:-my-2 prose-p:-my-2", |
| #213 | className |
| #214 | )}> |
| #215 | <Markdown |
| #216 | remarkPlugins={[remarkGfm, remarkMath]} |
| #217 | components={components} |
| #218 | > |
| #219 | {(isStreaming ? processedTextRef.current : safeMarkdownText)} |
| #220 | </Markdown> |
| #221 | {(isStreaming || (!isStreaming && !processedTextRef.current)) && <span className="markdown-cursor">▋</span>} |
| #222 | </div> |
| #223 | ); |
| #224 | }; |
| #225 | |
| #226 | export default MarkdownRenderer; |
| #227 |