spb/zyquo-cloud-web Public MIT
Zyquo Cloud Web — every cloud model, one beautiful chat, entirely in your browser.
TypeScript 81.9%
CSS 8.9%
JavaScript 7.5%
Shell 1.1%
HTML 0.6%
1/*2 * Markdown.tsx3 * Zyquo Cloud Web4 *5 * Author: Simon-Pierre Boucher6 * Mail: contact@spboucher.ai7 *8 * Assistant message rendering: GitHub-flavored Markdown with syntax-9 * highlighted code blocks (language label, copy, wrap toggle) mapped to the10 * Zyquo syntax tokens. Memoized — re-parsing only when text changes.11 */1213import { lazy, memo, Suspense, useState, type ReactNode } from 'react'14import ReactMarkdown from 'react-markdown'15import remarkGfm from 'remark-gfm'16import remarkMath from 'remark-math'17import rehypeHighlight from 'rehype-highlight'18import rehypeKatex from 'rehype-katex'19import 'katex/dist/katex.min.css'20import { IconCheck, IconCopy } from './icons'2122const Mermaid = lazy(() => import('./Mermaid'))2324function extractText(node: ReactNode): string {25 if (typeof node === 'string') return node26 if (Array.isArray(node)) return node.map(extractText).join('')27 if (node && typeof node === 'object' && 'props' in node) {28 return extractText((node as { props: { children?: ReactNode } }).props.children)29 }30 return ''31}3233function CodeBlock({ className, children }: { className?: string; children?: ReactNode }) {34 const [copied, setCopied] = useState(false)35 const [wrap, setWrap] = useState(false)36 const language = /language-(\w+)/.exec(className ?? '')?.[1] ?? ''3738 const copy = () => {39 void navigator.clipboard.writeText(extractText(children).replace(/\n$/, ''))40 setCopied(true)41 setTimeout(() => setCopied(false), 1200)42 }4344 return (45 <div className="code-block">46 <div className="code-block-header">47 <span>{language || 'text'}</span>48 <span className={`code-block-actions${copied ? ' confirming' : ''}`}>49 <button className="code-block-copy" onClick={() => setWrap((w) => !w)}>50 {wrap ? 'unwrap' : 'wrap'}51 </button>52 <button className={`code-block-copy${copied ? ' ok' : ''}`} onClick={copy}>53 {copied ? <IconCheck size={10} /> : <IconCopy size={10} />} {copied ? 'copied' : 'copy'}54 </button>55 </span>56 </div>57 <pre className={wrap ? 'wrap' : ''}>58 <code className={className}>{children}</code>59 </pre>60 </div>61 )62}6364export const Markdown = memo(function Markdown({ text }: { text: string }) {65 return (66 <div className="markdown">67 <ReactMarkdown68 remarkPlugins={[remarkGfm, remarkMath]}69 rehypePlugins={[70 [rehypeHighlight, { detect: false, ignoreMissing: true }],71 [rehypeKatex, { errorColor: 'var(--z-danger)' }],72 ]}73 components={{74 pre: ({ children }) => <>{children}</>,75 code: ({ className, children, ...props }) => {76 const inline = !className && typeof children === 'string' && !children.includes('\n')77 if (inline) {78 return (79 <code className={className} {...props}>80 {children}81 </code>82 )83 }84 if (/language-mermaid/.test(className ?? '')) {85 return (86 <Suspense87 fallback={88 <div style={{ color: 'var(--z-text-tertiary)', fontSize: 12 }}>89 Rendering diagram…90 </div>91 }92 >93 <Mermaid code={extractText(children).replace(/\n$/, '')} />94 </Suspense>95 )96 }97 return <CodeBlock className={className}>{children}</CodeBlock>98 },99 a: ({ href, children }) => (100 <a href={href} target="_blank" rel="noreferrer noopener">101 {children}102 </a>103 ),104 }}105 >106 {text}107 </ReactMarkdown>108 </div>109 )110})111